diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index c5a35c01d..baef57002 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.download import android.content.Context -import android.os.Build import com.hippo.unifile.UniFile import eu.kanade.domain.chapter.model.toSChapter import eu.kanade.domain.manga.model.getComicInfo @@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.storage.CbzCrypto +import eu.kanade.tachiyomi.util.storage.CbzCrypto.addFilesToZip import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE import eu.kanade.tachiyomi.util.storage.saveTo @@ -43,8 +43,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import logcat.LogPriority -import net.lingala.zip4j.ZipFile -import net.lingala.zip4j.model.ZipParameters import nl.adaptivity.xmlutil.serialization.XML import okhttp3.Response import tachiyomi.core.common.i18n.stringResource @@ -66,7 +64,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.BufferedOutputStream import java.io.File -import java.nio.charset.StandardCharsets import java.util.Locale import java.util.zip.CRC32 import java.util.zip.ZipEntry @@ -663,31 +660,15 @@ class Downloader( dirname: String, tmpDir: UniFile, ) { - val zipFile = File(context.externalCacheDir, "$dirname.cbz$TMP_DIR_SUFFIX") - val zip = ZipFile(zipFile) - val zipParameters = ZipParameters() - - CbzCrypto.setZipParametersEncrypted(zipParameters) - zip.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) zip.charset = StandardCharsets.ISO_8859_1 - tmpDir.filePath?.let { addPaddingToImage(File(it)) } - zip.addFiles( - tmpDir.listFiles()?.map { img -> img.filePath?.let { File(it) } }, - zipParameters, - ) - zip.close() - - val realZip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!! - realZip.openOutputStream().use { out -> - zipFile.inputStream().use { - it.copyTo(out) - } + tmpDir.listFiles()?.toList()?.let { files -> + mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX") + ?.addFilesToZip(files, CbzCrypto.getDecryptedPasswordCbz()) } + mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz") tmpDir.delete() - zipFile.delete() } private fun addPaddingToImage(imageDir: File) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index d022b0782..6b5b6ec01 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -37,10 +37,6 @@ internal class ZipPageLoader(file: File) : PageLoader() { } init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - zip4j.charset = StandardCharsets.ISO_8859_1 - } - Zip4jFile(file).use { zip -> if (zip.isEncrypted) { if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) { diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt index 31c749cf1..c1f442c99 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt @@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.util.storage import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 +import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.core.security.SecurityPreferences +import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -11,9 +13,14 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import logcat.LogPriority import net.lingala.zip4j.ZipFile +import net.lingala.zip4j.exception.ZipException +import net.lingala.zip4j.io.inputstream.ZipInputStream +import net.lingala.zip4j.io.outputstream.ZipOutputStream +import net.lingala.zip4j.model.LocalFileHeader import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.model.enums.AesKeyStrength import net.lingala.zip4j.model.enums.EncryptionMethod +import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.injectLazy import java.io.ByteArrayInputStream @@ -221,6 +228,133 @@ object CbzCrypto { } return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true) } + + fun UniFile.isEncryptedZip(): Boolean { + return try { + val stream = ZipInputStream(this.openInputStream()) + stream.nextEntry + stream.close() + false + } catch (zipException: ZipException) { + if (zipException.type == ZipException.Type.WRONG_PASSWORD) { + true + } else throw zipException + } + } + + fun UniFile.testCbzPassword(): Boolean { + return try { + val stream = ZipInputStream(this.openInputStream()) + stream.setPassword(getDecryptedPasswordCbz()) + stream.nextEntry + stream.close() + true + } catch (zipException: ZipException) { + if (zipException.type == ZipException.Type.WRONG_PASSWORD) { + false + } else throw zipException + } + } + + fun UniFile.addStreamToZip(inputStream: InputStream, filename: String, password: CharArray? = null) { + val zipOutputStream = + if (password != null) ZipOutputStream(this.openOutputStream(), password) + else ZipOutputStream(this.openOutputStream()) + + val zipParameters = ZipParameters() + zipParameters.fileNameInZip = filename + + if (password != null) setZipParametersEncrypted(zipParameters) + zipOutputStream.putNextEntry(zipParameters) + + zipOutputStream.use { output -> + inputStream.use { input -> + input.copyTo(output) + } + } + } + + + fun UniFile.addFilesToZip(files: List, password: CharArray? = null) { + val zipOutputStream = + if (password != null) ZipOutputStream(this.openOutputStream(), password) + else ZipOutputStream(this.openOutputStream()) + + + files.forEach { + val zipParameters = ZipParameters() + if (password != null) setZipParametersEncrypted(zipParameters) + zipParameters.fileNameInZip = it.name + + zipOutputStream.putNextEntry(zipParameters) + + it.openInputStream().use { input -> + input.copyTo(zipOutputStream) + } + zipOutputStream.closeEntry() + } + zipOutputStream.close() + } + + fun UniFile.getZipInputStream(filename: String): InputStream? { + val zipInputStream = ZipInputStream(this.openInputStream()) + var fileHeader: LocalFileHeader? + + if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz()) + + try { + while (run { + fileHeader = zipInputStream.nextEntry + fileHeader != null + }) { + if (fileHeader?.fileName == filename) return zipInputStream + } + + } catch (zipException: ZipException) { + if (zipException.type == ZipException.Type.WRONG_PASSWORD) { + logcat(LogPriority.WARN) { + "Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}" + } + } else throw zipException + } + return null + } + fun UniFile.getCoverStreamFromZip(): InputStream? { + val zipInputStream = ZipInputStream(this.openInputStream()) + var fileHeader: LocalFileHeader? + val fileHeaderList: MutableList = mutableListOf() + + if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz()) + + try { + while (run { + fileHeader = zipInputStream.nextEntry + fileHeader != null + }) { + fileHeaderList.add(fileHeader) + } + + var coverHeader = fileHeaderList + .mapNotNull { it } + .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } + .find { !it.isDirectory && ImageUtil.isImage(it.fileName) } + + + val coverStream = coverHeader?.fileName?.let { this.getZipInputStream(it) } + if (coverStream != null) { + if (!ImageUtil.isImage(coverHeader?.fileName) { coverStream }) coverHeader = null + } + return coverHeader?.fileName?.let { getZipInputStream(it) } + + } catch (zipException: ZipException) { + if (zipException.type == ZipException.Type.WRONG_PASSWORD) { + logcat(LogPriority.WARN) { + "Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}" + } + return null + } else throw zipException + } + } } private const val BUFFER_SIZE = 2048 diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 667654718..6036ab6e3 100755 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -11,6 +11,10 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.CbzCrypto +import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip +import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStreamFromZip +import eu.kanade.tachiyomi.util.storage.CbzCrypto.getZipInputStream +import eu.kanade.tachiyomi.util.storage.CbzCrypto.isEncryptedZip import eu.kanade.tachiyomi.util.storage.EpubFile import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -18,8 +22,6 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream import logcat.LogPriority -import net.lingala.zip4j.ZipFile -import net.lingala.zip4j.model.ZipParameters import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.serialization.XML import tachiyomi.core.common.i18n.stringResource @@ -44,7 +46,6 @@ import tachiyomi.source.local.io.Format import tachiyomi.source.local.io.LocalSourceFileSystem import tachiyomi.source.local.metadata.fillMetadata import uy.kohesive.injekt.injectLazy -import java.io.File import java.io.InputStream import java.nio.charset.StandardCharsets import kotlin.time.Duration.Companion.days @@ -199,15 +200,12 @@ actual class LocalSource( } // SY --> comicInfoArchiveFile != null -> { - val comicInfoArchive = ZipFile(tempFileManager.createTempFile(comicInfoArchiveFile)) noXmlFile?.delete() - if (CbzCrypto.checkCbzPassword(comicInfoArchive, CbzCrypto.getDecryptedPasswordCbz())) { - comicInfoArchive.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - val comicInfoEntry = comicInfoArchive.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE } - setMangaDetailsFromComicInfoFile(comicInfoArchive.getInputStream(comicInfoEntry), manga) - } + comicInfoArchiveFile.getZipInputStream(COMIC_INFO_FILE) + ?.let { setMangaDetailsFromComicInfoFile(it, manga) } } + // SY <-- // Old custom JSON format @@ -239,18 +237,13 @@ actual class LocalSource( .filter(Archive::isSupported) .toList() - val folderPath = mangaDir?.filePath + val copiedFile = mangaDir?.let { copyComicInfoFileFromArchive(chapterArchives, it) } - val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath) // SY --> if (copiedFile != null && copiedFile.name != COMIC_INFO_ARCHIVE) { - setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga) + setMangaDetailsFromComicInfoFile(copiedFile.openInputStream(), manga) } else if (copiedFile != null && copiedFile.name == COMIC_INFO_ARCHIVE) { - val comicInfoArchive = ZipFile(copiedFile) - comicInfoArchive.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - val comicInfoEntry = comicInfoArchive.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE } - - setMangaDetailsFromComicInfoFile(comicInfoArchive.getInputStream(comicInfoEntry), manga) + copiedFile.getZipInputStream(COMIC_INFO_FILE)?.let { setMangaDetailsFromComicInfoFile(it, manga) } } // SY <-- else { // Avoid re-scanning @@ -265,33 +258,20 @@ actual class LocalSource( return@withIOContext manga } - private fun copyComicInfoFileFromArchive(chapterArchives: List, folderPath: String?): File? { + private fun copyComicInfoFileFromArchive(chapterArchives: List, folder: UniFile): UniFile? { for (chapter in chapterArchives) { when (Format.valueOf(chapter)) { is Format.Zip -> { - ZipFile(tempFileManager.createTempFile(chapter)).use { zip: ZipFile -> - // SY --> - if (zip.isEncrypted && !CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz()) - ) { - return null - } else if ( - zip.isEncrypted && CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz()) - ) { - zip.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - } - zip.getFileHeader(COMIC_INFO_FILE)?.let { comicInfoFile -> - // SY <-- - zip.getInputStream(comicInfoFile).buffered().use { stream -> - return copyComicInfoFile(stream, folderPath) - } - } + // SY --> + chapter.getZipInputStream(COMIC_INFO_FILE)?.buffered().use { stream -> + return stream?.let { copyComicInfoFile(it, folder) } } } is Format.Rar -> { JunrarArchive(tempFileManager.createTempFile(chapter)).use { rar -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.getInputStream(comicInfoFile).buffered().use { stream -> - return copyComicInfoFile(stream, folderPath) + return copyComicInfoFile(stream, folder) } } } @@ -302,24 +282,20 @@ actual class LocalSource( return null } - private fun copyComicInfoFile(comicInfoFileStream: InputStream, folderPath: String?): File { + private fun copyComicInfoFile(comicInfoFileStream: InputStream, folder: UniFile): UniFile? { // SY --> if ( CbzCrypto.getPasswordProtectDlPref() && CbzCrypto.isPasswordSet() ) { - val zipParameters = ZipParameters() - CbzCrypto.setZipParametersEncrypted(zipParameters) - zipParameters.fileNameInZip = COMIC_INFO_FILE + val comicInfoArchive = folder.createFile(COMIC_INFO_ARCHIVE) + comicInfoArchive?.addStreamToZip(comicInfoFileStream, COMIC_INFO_FILE, CbzCrypto.getDecryptedPasswordCbz()) - val zipEncrypted = ZipFile("$folderPath/$COMIC_INFO_ARCHIVE") - zipEncrypted.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - zipEncrypted.addStream(comicInfoFileStream, zipParameters) - return zipEncrypted.file + return comicInfoArchive } else { // SY <-- - return File("$folderPath/$COMIC_INFO_FILE").apply { - outputStream().use { outputStream -> + return folder.createFile(COMIC_INFO_FILE)?.apply { + openOutputStream().use { outputStream -> comicInfoFileStream.use { it.copyTo(outputStream) } } } @@ -413,19 +389,15 @@ actual class LocalSource( entry?.let { coverManager.update(manga, it.openInputStream()) } } is Format.Zip -> { - ZipFile(tempFileManager.createTempFile(format.file)).use { zip -> - // SY --> - var encrypted = false - if (zip.isEncrypted) { - zip.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - encrypted = true - } - val entry = zip.fileHeaders.toList() - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip.getInputStream(it) } } - entry?.let { coverManager.update(manga, zip.getInputStream(it), encrypted) } - // SY <-- + // SY --> + format.file.getCoverStreamFromZip()?.let { inputStream -> + coverManager.update( + manga, + inputStream, + format.file.isEncryptedZip() + ) } + // SY <-- } is Format.Rar -> { JunrarArchive(tempFileManager.createTempFile(format.file)).use { archive -> diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index a09abba5c..0738ce1c4 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt @@ -4,13 +4,11 @@ import android.content.Context import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.storage.CbzCrypto +import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip import eu.kanade.tachiyomi.util.storage.DiskUtil -import net.lingala.zip4j.ZipFile -import net.lingala.zip4j.model.ZipParameters import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.source.local.io.LocalSourceFileSystem -import java.io.File import java.io.InputStream private const val DEFAULT_COVER_NAME = "cover.jpg" @@ -60,23 +58,7 @@ actual class LocalCoverManager( inputStream.use { input -> // SY --> if (encrypted) { - val tempFile = File.createTempFile( - targetFile.nameWithoutExtension.orEmpty().padEnd(3), // Prefix must be 3+ chars - null, - ) - val zip4j = ZipFile(tempFile) - val zipParameters = ZipParameters() - zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - CbzCrypto.setZipParametersEncrypted(zipParameters) - zipParameters.fileNameInZip = DEFAULT_COVER_NAME - zip4j.addStream(input, zipParameters) - zip4j.close() - targetFile.openOutputStream().use { output -> - tempFile.inputStream().use { input -> - input.copyTo(output) - } - } - + targetFile.addStreamToZip(inputStream, DEFAULT_COVER_NAME, CbzCrypto.getDecryptedPasswordCbz()) DiskUtil.createNoMediaFile(directory, context) manga.thumbnail_url = targetFile.uri.toString()