From 2dbf798f0c4278b3766393e0afc815beaf617de0 Mon Sep 17 00:00:00 2001 From: KenjieDec <65448230+KenjieDec@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:13:26 +0700 Subject: [PATCH] Add Panda Chaika Extension (#3801) * Add Panda Chaika Extension - Add "Panda Chaika" extension from panda.chaika.moe * Add Support for Zip64 - Add support for Zip64 type of .zip ( large zip [ size/pages ] ) -> For Example: https://panda.chaika.moe/archive/49406/ - Use Little Endian for All signatures - Apply AwkwardPeak7's suggestions * Fix null Genres? * Fix "null" genre if there's no genre * Fix mistakes caused by previous commit Sorry... * Improve description readability - Sorry for the commits spam - Make manga description more readable * Fix Broken Filters, Apply Suggestions * Apply suggestions - Apply AwkwardPeak's suggestions --- src/all/pandachaika/build.gradle | 8 + .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3993 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2121 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5854 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 11546 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 17639 bytes .../extension/all/pandachaika/PandaChaika.kt | 253 +++++++++++++++ .../all/pandachaika/PandaChaikaDto.kt | 102 +++++++ .../all/pandachaika/PandaChaikaFactory.kt | 29 ++ .../all/pandachaika/PandaChaikaFilters.kt | 62 ++++ .../all/pandachaika/PandaChaikaUtils.kt | 287 ++++++++++++++++++ 11 files changed, 741 insertions(+) create mode 100644 src/all/pandachaika/build.gradle create mode 100644 src/all/pandachaika/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/all/pandachaika/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/all/pandachaika/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/all/pandachaika/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/all/pandachaika/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaika.kt create mode 100644 src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaDto.kt create mode 100644 src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaFactory.kt create mode 100644 src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaFilters.kt create mode 100644 src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaUtils.kt diff --git a/src/all/pandachaika/build.gradle b/src/all/pandachaika/build.gradle new file mode 100644 index 000000000..6c786858b --- /dev/null +++ b/src/all/pandachaika/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'PandaChaika' + extClass = '.PandaChaikaFactory' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/pandachaika/res/mipmap-hdpi/ic_launcher.png b/src/all/pandachaika/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b34e853def470f4f37a238a8772e3220779bd324 GIT binary patch literal 3993 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 literal 0 HcmV?d00001 diff --git a/src/all/pandachaika/res/mipmap-mdpi/ic_launcher.png b/src/all/pandachaika/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..972505757b5b97a73a5d334bfa0d112cf52d7dca GIT binary patch literal 2121 zcmV-P2)6f$P)Px-14%?dRA@u(T3c)sRT%zeyR+9WNLg)xN-sbWTS_e&^}(W8Vnh;B9-xpA??M3| zv=ATE_@LE9Q34O7h>en<5HU@N0^)-d)Tju-3q*}n7PKu`YTNC0x4Ye)G3V@D&Y79r zoozIP#7QSJv$H$%edqiB|NQ5i3b@WU;X22U{|Q`c1+ytY?m2*Ku96Hu0zi(Y&5D3f zU45@VWm$QGOit6!OaJNj)bG&G)%&@Q>ZA1Usm~`zL6ApWF4um^ z9e=j}+_{L6ffWH~MeW_~kx1&(~z|vkSFcu#t!~ z6bb_PJ12pu6(IjgP#f)+zRr_?ZWR_V2i|7uD`sJ`RrbKnWKr7$ zippwr1(-}@1>Vv!9Il?SRf4%r*&W*-N*RGQKkE$R$$N7&J5!*$nhaSmf#5#p{i;w%5I)PyYfptz4+y~5F+uE-T|^1%QATR z)Gbeh0N3i7h|mZdhdg=(F42kR=4Nc%xDgc<6-cF07#|-;B9Xv_3m4G7`(1P%`W%Ur z1f=4S1rZPmOuamBqcjoQ1a7#whEu>4#4HiDptzwdxObNu}ak|H1 zF?_IZKVE5j6;d(*2oAtA6|&Qvr8G&~XhH;71$5kM8NsAZDTSAx+kmH^-o*Cw^qi$wUFI5K80Ge6F3dc^{~qI}=Ag z{s>;L7lI(royxB4LBcs%q-R62g*1fF@}b;v>iUj$|-7HumKAOvzKQ0f;ENdX1P zI3B32!TViDsh?dg7k2L4NdX6gLA18EQu{1kyqJ>d@9(G9X>4r7p~FY;(i^+Ts6`Js zlL|}?W>y+eUgpHwrSHQtX{nuFTMO4KKt^FMaO0yd(a=+B~VBR@cuE`iQEo21vJ+!KdJva#t z0hm@-hx4J|(el)zC@C(1D2h0E@F33o@DslNF+_vbJj=nB*8JcpZv}jW05=Q6z-B(z zK9$J?QWB*zyl8!_1rM!gqV><4?`*@NQ%B$(aijb6Z!}9uzG6Ud8j3d?;PVM$Uvbq{ z4u2K}=q+g+4>m5u6AwRz+M4+^$CHwxx3?E7x2!?vyWf!vCs15mjK87{`fMTxC#z@w^ooDTS4*7NPW}>6lwtg|TEDqf!({4jsYqPma-+C#3GVrvWF< zoQDi1?`I2IBMZ9(JU7iTmD8%Fm_kD9s)>~fJu)77as?V17oq!7H_nOO_-pVAx?cPg z-jo~BXcX~80_C(IwHpl?Rc0!RHeLuBo%kvl42{nuXw}+BIizU7j|J&8N?-hB4o? ztAMBx$fkfE0DQCtpTeYfeGhCGE6RZQgR<6y<~}LB-{D`d5jxymkEKQHo+k-1wgt@^Ogam(LT9w z<-9K=3MqLQ(ZT0|_(d`^!~Vu{l~Mscg^U^jh&f;K-+qDAA*9vG(o)p8+?r{fn0)S3 zcCuD{EjKd%NLD7vZpsG#|Es~Zl!mw*I1tm<2n0x;(00000NkvXXu0mjf6eQ`? literal 0 HcmV?d00001 diff --git a/src/all/pandachaika/res/mipmap-xhdpi/ic_launcher.png b/src/all/pandachaika/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4a7af4ce8cd6d663591d54bcda3e3c815136a8ea GIT binary patch literal 5854 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%0AiK8dCA@@H;c>jgKE8bL^l$x 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%0D|9xyriE(KxM8)}m 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 { + val num = query.filter(Char::isDigit).toIntOrNull() ?: -1 + fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages) + + if (num < 0) return minPages to maxPages + return when (query.firstOrNull()) { + '<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1) + '>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages + '=' -> when (query[1]) { + '>' -> limitedNum() to maxPages + '<' -> 1 to limitedNum(maxPages) + else -> limitedNum() to limitedNum() + } + else -> limitedNum() to limitedNum() + } + } + + override fun searchMangaParse(response: Response): MangasPage { + val library = response.parseAs() + + val mangas = library.archives.map(LongArchive::toSManga) + + val hasNextPage = library.has_next + + return MangasPage(mangas, hasNextPage) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseSearchUrl.toHttpUrl().newBuilder().apply { + val tags = mutableListOf() + var reason = "" + var uploader = "" + var pagesMin = 1 + var pagesMax = 9999 + + tags.add(searchLang) + + filters.forEach { + when (it) { + is SortFilter -> { + addQueryParameter("sort", it.getValue()) + addQueryParameter("asc_desc", if (it.state!!.ascending) "asc" else "desc") + } + + is SelectFilter -> { + addQueryParameter("category", it.vals[it.state].replace("All", "")) + } + + is PageFilter -> { + if (it.state.isNotBlank()) { + val (min, max) = parsePageRange(it.state) + pagesMin = min + pagesMax = max + } + } + + is TextFilter -> { + if (it.state.isNotEmpty()) { + when (it.type) { + "reason" -> reason = it.state + "uploader" -> uploader = it.state + else -> { + it.state.split(",").filter(String::isNotBlank).map { tag -> + val trimmed = tag.trim() + tags.add( + buildString { + if (trimmed.startsWith('-')) append("-") + append(it.type) + if (it.type.isNotBlank()) append(":") + append(trimmed.lowercase().removePrefix("-")) + }, + ) + } + } + } + } + } + else -> {} + } + } + + addQueryParameter("title", query) + addQueryParameter("tags", tags.joinToString()) + addQueryParameter("filecount_from", pagesMin.toString()) + addQueryParameter("filecount_to", pagesMax.toString()) + addQueryParameter("reason", reason) + addQueryParameter("uploader", uploader) + addQueryParameter("page", page.toString()) + addQueryParameter("apply", "") + addQueryParameter("json", "") + }.build() + + return GET(url, headers) + } + + override fun chapterListRequest(manga: SManga): Request { + return GET("$baseUrl/api?archive=${manga.url}", headers) + } + + override fun getFilterList() = getFilters() + + // Details + + override fun fetchMangaDetails(manga: SManga): Observable { + return Observable.just(manga.apply { initialized = true }) + } + + // Chapters + + override fun chapterListParse(response: Response): List { + val archive = response.parseAs() + + return listOf( + SChapter.create().apply { + name = "Chapter" + url = archive.download.substringBefore("/download/") + date_upload = archive.posted * 1000 + }, + ) + } + + override fun getMangaUrl(manga: SManga) = "$baseUrl/archive/${manga.url}" + override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}" + + // Pages + override fun fetchPageList(chapter: SChapter): Observable> { + fun List.sort() = this.sortedWith(compareBy(CASE_INSENSITIVE_ORDER) { it }) + val url = "$baseUrl${chapter.url}/download/" + val (fileType, contentLength) = getZipType(url) + + val remoteZip = ZipHandler(url, client, headers, fileType, contentLength).populate() + val fileListing = remoteZip.files().sort() + + val files = remoteZip.toJson() + return Observable.just( + fileListing.mapIndexed { index, filename -> + Page(index, imageUrl = "https://127.0.0.1/#$filename&$files") + }, + ) + } + + private fun getZipType(url: String): Pair { + val request = Request.Builder() + .url(url) + .headers(headers) + .method("HEAD", null) + .build() + + val contentLength = ( + client.newCall(request).execute().header("content-length") + ?: throw Exception("Could not get Content-Length of URL") + ) + .toBigInteger() + + return (if (contentLength > Int.MAX_VALUE.toBigInteger()) "zip64" else "zip") to contentLength + } + + private fun Intercept(chain: Interceptor.Chain): Response { + val url = chain.request().url.toString() + return if (url.startsWith("https://127.0.0.1/#")) { + val fragment = url.toHttpUrl().fragment!! + val remoteZip = fragment.substringAfter("&").parseAs() + val filename = fragment.substringBefore("&") + + val byteArray = remoteZip.fetch(filename, client) + var type = filename.substringAfterLast('.').lowercase() + type = if (type == "jpg") "jpeg" else type + + Response.Builder().body(byteArray.toResponseBody("image/$type".toMediaType())) + .request(chain.request()) + .protocol(Protocol.HTTP_1_0) + .code(200) + .message("") + .build() + } else { + chain.proceed(chain.request()) + } + } + + private inline fun Response.parseAs(): T { + return json.decodeFromString(body.string()) + } + + private inline fun String.parseAs(): T { + return json.decodeFromString(this) + } + + private fun Zip.toJson(): String { + return json.encodeToString(this) + } + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + override fun pageListParse(response: Response): List = throw UnsupportedOperationException() + override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException() +} diff --git a/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaDto.kt b/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaDto.kt new file mode 100644 index 000000000..17ce374a0 --- /dev/null +++ b/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaDto.kt @@ -0,0 +1,102 @@ +package eu.kanade.tachiyomi.extension.all.pandachaika + +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.UpdateStrategy +import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH) +fun filterTags(include: String = "", exclude: List = emptyList(), tags: List): String { + return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } } + .joinToString { + it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s -> + s.replaceFirstChar { sr -> + if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString() + } + } + } +} +fun getReadableSize(bytes: Double): String { + return when { + bytes >= 300 * 1024 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0 * 1024.0))} GB" + bytes >= 100 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0))} MB" + bytes >= 1024 -> "${"%.2f".format(bytes / (1024.0))} KB" + else -> "$bytes B" + } +} + +@Serializable +class Archive( + val download: String, + val posted: Long, +) + +@Serializable +class LongArchive( + private val thumbnail: String, + private val title: String, + private val id: Int, + private val posted: Long?, + private val public_date: Long?, + private val filecount: Int, + private val filesize: Double, + private val tags: List, + private val title_jpn: String?, + private val uploader: String, +) { + fun toSManga() = SManga.create().apply { + val groups = filterTags("group", tags = tags) + val artists = filterTags("artist", tags = tags) + val publishers = filterTags("publisher", tags = tags) + val male = filterTags("male", tags = tags) + val female = filterTags("female", tags = tags) + val others = filterTags(exclude = listOf("female", "male", "artist", "publisher", "group", "parody"), tags = tags) + val parodies = filterTags("parody", tags = tags) + url = id.toString() + title = this@LongArchive.title + thumbnail_url = thumbnail + author = groups.ifEmpty { artists } + artist = artists + genre = listOf(male, female, others).joinToString() + description = buildString { + append("Uploader: ", uploader.ifEmpty { "Anonymous" }, "\n") + publishers.takeIf { it.isNotBlank() }?.let { + append("Publishers: ", it, "\n\n") + } + parodies.takeIf { it.isNotBlank() }?.let { + append("Parodies: ", it, "\n\n") + } + male.takeIf { it.isNotBlank() }?.let { + append("Male tags: ", it, "\n\n") + } + female.takeIf { it.isNotBlank() }?.let { + append("Female tags: ", it, "\n\n") + } + others.takeIf { it.isNotBlank() }?.let { + append("Other tags: ", it, "\n\n") + } + + title_jpn?.let { append("Japanese Title: ", it, "\n") } + append("Pages: ", filecount, "\n") + append("File Size: ", getReadableSize(filesize), "\n") + + try { + append("Public Date: ", dateReformat.format(Date(public_date!! * 1000)), "\n") + } catch (_: Exception) {} + try { + append("Posted: ", dateReformat.format(Date(posted!! * 1000)), "\n") + } catch (_: Exception) {} + } + status = SManga.COMPLETED + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + initialized = true + } +} + +@Serializable +class ArchiveResponse( + val archives: List, + val has_next: Boolean, +) diff --git a/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaFactory.kt b/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaFactory.kt new file mode 100644 index 000000000..2d118c974 --- /dev/null +++ b/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaFactory.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.extension.all.pandachaika + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +class PandaChaikaFactory : SourceFactory { + override fun createSources(): List = listOf( + PandaChaika(), + PandaChaika("en", "english"), + PandaChaika("zh", "chinese"), + PandaChaika("ko", "korean"), + PandaChaika("es", "spanish"), + PandaChaika("ru", "russian"), + PandaChaika("pt", "portuguese"), + PandaChaika("fr", "french"), + PandaChaika("th", "thai"), + PandaChaika("vi", "vietnamese"), + PandaChaika("ja", "japanese"), + PandaChaika("id", "indonesian"), + PandaChaika("ar", "arabic"), + PandaChaika("uk", "ukrainian"), + PandaChaika("tr", "turkish"), + PandaChaika("cs", "czech"), + PandaChaika("tl", "tagalog"), + PandaChaika("fi", "finnish"), + PandaChaika("jv", "javanese"), + PandaChaika("el", "greek"), + ) +} diff --git a/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaFilters.kt b/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaFilters.kt new file mode 100644 index 000000000..4c6f31c22 --- /dev/null +++ b/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaFilters.kt @@ -0,0 +1,62 @@ +package eu.kanade.tachiyomi.extension.all.pandachaika + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.Filter.Sort.Selection +import eu.kanade.tachiyomi.source.model.FilterList + +fun getFilters(): FilterList { + return FilterList( + SortFilter("Sort by", Selection(0, false), getSortsList), + SelectFilter("Types", getTypes), + Filter.Separator(), + Filter.Header("Separate tags with commas (,)"), + Filter.Header("Prepend with dash (-) to exclude"), + Filter.Header("Use 'Male Tags' or 'Female Tags' for specific categories. 'Tags' searches all categories."), + TextFilter("Tags", ""), + TextFilter("Male Tags", "male"), + TextFilter("Female Tags", "female"), + TextFilter("Artists", "artist"), + TextFilter("Parodies", "parody"), + Filter.Separator(), + TextFilter("Reason", "reason"), + TextFilter("Uploader", "reason"), + Filter.Separator(), + Filter.Header("Filter by pages, for example: (>20)"), + PageFilter("Pages"), + ) +} + +internal open class PageFilter(name: String) : Filter.Text(name) + +internal open class TextFilter(name: String, val type: String) : Filter.Text(name) + +internal open class SelectFilter(name: String, val vals: List, state: Int = 0) : + Filter.Select(name, vals.map { it }.toTypedArray(), state) + +internal open class SortFilter(name: String, selection: Selection, private val vals: List>) : + Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) { + fun getValue() = vals[state!!.index].second +} + +private val getTypes = listOf( + "All", + "Doujinshi", + "Manga", + "Image Set", + "Artist CG", + "Game CG", + "Western", + "Non-H", + "Misc", +) + +private val getSortsList: List> = listOf( + Pair("Public Date", "public_date"), + Pair("Posted Date", "posted_date"), + Pair("Title", "title"), + Pair("Japanese Title", "title_jpn"), + Pair("Rating", "rating"), + Pair("Images", "images"), + Pair("File Size", "size"), + Pair("Category", "category"), +) diff --git a/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaUtils.kt b/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaUtils.kt new file mode 100644 index 000000000..e46805acb --- /dev/null +++ b/src/all/pandachaika/src/eu/kanade/tachiyomi/extension/all/pandachaika/PandaChaikaUtils.kt @@ -0,0 +1,287 @@ +package eu.kanade.tachiyomi.extension.all.pandachaika + +import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.inflateRaw +import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseAllCDs +import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseEOCD +import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseEOCD64 +import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseLocalFile +import eu.kanade.tachiyomi.network.GET +import kotlinx.serialization.Serializable +import okhttp3.Headers +import okhttp3.OkHttpClient +import java.io.ByteArrayOutputStream +import java.math.BigInteger +import java.nio.ByteBuffer +import java.nio.ByteOrder.LITTLE_ENDIAN +import java.util.zip.Inflater +import kotlin.text.Charsets.UTF_8 + +const val CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE = 0x02014b50 +const val END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50 +const val END_OF_CENTRAL_DIRECTORY_64_SIGNATURE = 0x06064b50 +const val LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50 + +class EndOfCentralDirectory( + val centralDirectoryByteSize: BigInteger, + val centralDirectoryByteOffset: BigInteger, +) + +@Serializable +class CentralDirectoryRecord( + val length: Int, + val compressedSize: Int, + val localFileHeaderRelativeOffset: Int, + val filename: String, +) + +class LocalFileHeader( + val compressedData: ByteArray, + val compressionMethod: Int, +) + +@Serializable +class Zip( + private val url: String, + private val centralDirectoryRecords: List, +) { + fun files(): List { + return centralDirectoryRecords.map { + it.filename + } + } + + fun fetch(path: String, client: OkHttpClient): ByteArray { + val file = centralDirectoryRecords.find { it.filename == path } + ?: throw Exception("File not found in ZIP: $path") + + val MAX_LOCAL_FILE_HEADER_SIZE = 256 + 32 + 30 + 100 + + val headersBuilder = Headers.Builder() + .set( + "Range", + "bytes=${file.localFileHeaderRelativeOffset}-${ + file.localFileHeaderRelativeOffset + + file.compressedSize + + MAX_LOCAL_FILE_HEADER_SIZE + }", + ).build() + + val request = GET(url, headersBuilder) + + val response = client.newCall(request).execute() + + val byteArray = response.body.byteStream().use { it.readBytes() } + + val localFile = parseLocalFile(byteArray, file.compressedSize) + ?: throw Exception("Failed to parse local file header in ZIP") + + return if (localFile.compressionMethod == 0) { + localFile.compressedData + } else { + inflateRaw(localFile.compressedData) + } + } +} + +class ZipHandler( + private val url: String, + private val client: OkHttpClient, + private val additionalHeaders: Headers = Headers.Builder().build(), + private val zipType: String = "zip", + private val contentLength: BigInteger, +) { + fun populate(): Zip { + val endOfCentralDirectory = fetchEndOfCentralDirectory(contentLength, zipType) + val centralDirectoryRecords = fetchCentralDirectoryRecords(endOfCentralDirectory) + + return Zip( + url, + centralDirectoryRecords, + ) + } + + private fun fetchEndOfCentralDirectory(zipByteLength: BigInteger, zipType: String): EndOfCentralDirectory { + val EOCD_MAX_BYTES = 128.toBigInteger() + val eocdInitialOffset = maxOf(0.toBigInteger(), zipByteLength - EOCD_MAX_BYTES) + + val headers = additionalHeaders + .newBuilder() + .set("Range", "bytes=$eocdInitialOffset-$zipByteLength") + .build() + val request = GET(url, headers) + + val response = client.newCall(request).execute() + + if (!response.isSuccessful) { + throw Exception("Could not fetch ZIP: HTTP status ${response.code}") + } + + val eocdBuffer = response.body.byteStream().use { it.readBytes() } + + if (eocdBuffer.isEmpty()) throw Exception("Could not get Range request to start looking for EOCD") + + val eocd = + (if (zipType == "zip64") parseEOCD64(eocdBuffer) else parseEOCD(eocdBuffer)) + ?: throw Exception("Could not get EOCD record of the ZIP") + + return eocd + } + + private fun fetchCentralDirectoryRecords(endOfCentralDirectory: EndOfCentralDirectory): List { + val headersBuilder = Headers.Builder() + .set( + "Range", + "bytes=${endOfCentralDirectory.centralDirectoryByteOffset}-${ + endOfCentralDirectory.centralDirectoryByteOffset + + endOfCentralDirectory.centralDirectoryByteSize + }", + ).build() + + val request = GET(url, headersBuilder) + + val response = client.newCall(request).execute() + + val cdBuffer = response.body.byteStream().use { it.readBytes() } + + return parseAllCDs(cdBuffer) + } +} + +object ZipParser { + + fun parseAllCDs(buffer: ByteArray): List { + val cds = ArrayList() + val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN) + + var i = 0 + while (i <= buffer.size - 4) { + val signature = view.getInt(i) + if (signature == CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE) { + val cd = parseCD(buffer.sliceArray(i until buffer.size)) + if (cd != null) { + cds.add(cd) + i += cd.length - 1 + continue + } + } else if (signature == END_OF_CENTRAL_DIRECTORY_SIGNATURE) { + break + } + i++ + } + + return cds + } + + fun parseCD(buffer: ByteArray): CentralDirectoryRecord? { + val MIN_CD_LENGTH = 46 + val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN) + + for (i in 0..buffer.size - MIN_CD_LENGTH) { + if (view.getInt(i) == CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE) { + val filenameLength = view.getShort(i + 28).toInt() + val extraFieldLength = view.getShort(i + 30).toInt() + val fileCommentLength = view.getShort(i + 32).toInt() + + return CentralDirectoryRecord( + length = 46 + filenameLength + extraFieldLength + fileCommentLength, + compressedSize = view.getInt(i + 20), + localFileHeaderRelativeOffset = view.getInt(i + 42), + filename = buffer.sliceArray(i + 46 until i + 46 + filenameLength).toString(UTF_8), + ) + } + } + return null + } + + fun parseEOCD(buffer: ByteArray): EndOfCentralDirectory? { + val MIN_EOCD_LENGTH = 22 + val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN) + + for (i in 0 until buffer.size - MIN_EOCD_LENGTH + 1) { + if (view.getInt(i) == END_OF_CENTRAL_DIRECTORY_SIGNATURE) { + return EndOfCentralDirectory( + centralDirectoryByteSize = view.getInt(i + 12).toBigInteger(), + centralDirectoryByteOffset = view.getInt(i + 16).toBigInteger(), + ) + } + } + return null + } + + fun parseEOCD64(buffer: ByteArray): EndOfCentralDirectory? { + val MIN_EOCD_LENGTH = 56 + val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN) + + for (i in 0 until buffer.size - MIN_EOCD_LENGTH + 1) { + if (view.getInt(i) == END_OF_CENTRAL_DIRECTORY_64_SIGNATURE) { + return EndOfCentralDirectory( + centralDirectoryByteSize = view.getLong(i + 40).toBigInteger(), + centralDirectoryByteOffset = view.getLong(i + 48).toBigInteger(), + ) + } + } + return null + } + + fun parseLocalFile(buffer: ByteArray, compressedSizeOverride: Int = 0): LocalFileHeader? { + val MIN_LOCAL_FILE_LENGTH = 30 + + val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN) + + for (i in 0..buffer.size - MIN_LOCAL_FILE_LENGTH) { + if (view.getInt(i) == LOCAL_FILE_HEADER_SIGNATURE) { + val filenameLength = view.getShort(i + 26).toInt() and 0xFFFF + val extraFieldLength = view.getShort(i + 28).toInt() and 0xFFFF + + val bitflags = view.getShort(i + 6).toInt() and 0xFFFF + val hasDataDescriptor = (bitflags shr 3) and 1 != 0 + + val headerEndOffset = i + 30 + filenameLength + extraFieldLength + val regularCompressedSize = view.getInt(i + 18) + + val compressedData = if (hasDataDescriptor) { + buffer.copyOfRange( + headerEndOffset, + headerEndOffset + compressedSizeOverride, + ) + } else { + buffer.copyOfRange( + headerEndOffset, + headerEndOffset + regularCompressedSize, + ) + } + + return LocalFileHeader( + compressedData = compressedData, + compressionMethod = view.getShort(i + 8).toInt(), + ) + } + } + + return null + } + + fun inflateRaw(compressedData: ByteArray): ByteArray { + val inflater = Inflater(true) + inflater.setInput(compressedData) + + val buffer = ByteArray(8192) + val output = ByteArrayOutputStream() + + try { + while (!inflater.finished()) { + val count = inflater.inflate(buffer) + if (count > 0) { + output.write(buffer, 0, count) + } + } + } catch (e: Exception) { + throw Exception("Invalid compressed data format: ${e.message}", e) + } finally { + inflater.end() + output.close() + } + + return output.toByteArray() + } +}