diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e3adf6bd3..04c406dc8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -212,10 +212,6 @@ dependencies { // Disk implementation(libs.disklrucache) implementation(libs.unifile) - implementation(libs.bundles.archive) - // SY --> - implementation(libs.zip4j) - // SY <-- // Preferences implementation(libs.preferencektx) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 5dc9af508..b8cf3413f 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -127,9 +127,6 @@ # XmlUtil -keep public enum nl.adaptivity.xmlutil.EventType { *; } -# Apache Commons Compress --keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { (); } - # Firebase -keep class com.google.firebase.installations.** { *; } -keep interface com.google.firebase.installations.** { *; } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt index 4a55578e3..037b9d1a8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.data.coil +import android.app.Application import android.graphics.Bitmap import android.os.Build import coil3.ImageLoader @@ -11,37 +12,37 @@ import coil3.decode.ImageSource import coil3.fetch.SourceFetchResult import coil3.request.Options import coil3.request.bitmapConfig +import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.util.storage.CbzCrypto +import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStream import eu.kanade.tachiyomi.util.system.GLUtil -import net.lingala.zip4j.ZipFile -import net.lingala.zip4j.model.FileHeader +import mihon.core.common.archive.archiveReader import okio.BufferedSource import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.decoder.ImageDecoder +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.BufferedInputStream /** * A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system. */ class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder { + private val context = Injekt.get() override suspend fun decode(): DecodeResult { // SY --> - var zip4j: ZipFile? = null - var entry: FileHeader? = null - + var coverStream: BufferedInputStream? = null if (resources.sourceOrNull()?.peek()?.use { CbzCrypto.detectCoverImageArchive(it.inputStream()) } == true) { if (resources.source().peek().use { ImageUtil.findImageType(it.inputStream()) == null }) { - zip4j = ZipFile(resources.file().toFile().absolutePath) - entry = zip4j.fileHeaders.firstOrNull { - it.fileName.equals(CbzCrypto.DEFAULT_COVER_NAME, ignoreCase = true) - } - - if (zip4j.isEncrypted) zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz()) + coverStream = UniFile.fromFile(resources.file().toFile()) + ?.archiveReader(context = context) + ?.getCoverStream() } } val decoder = resources.sourceOrNull()?.use { - zip4j.use { zipFile -> - ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream(), options.cropBorders, displayProfile) + coverStream.use { coverStream -> + ImageDecoder.newInstance(coverStream ?: it.inputStream(), options.cropBorders, displayProfile) } } // SY <-- 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 3760db0f6..08e0baa15 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 @@ -44,10 +44,10 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import logcat.LogPriority +import mihon.core.common.archive.ZipWriter import nl.adaptivity.xmlutil.serialization.XML import okhttp3.Response import tachiyomi.core.common.i18n.stringResource -import tachiyomi.core.common.storage.addFilesToZip import tachiyomi.core.common.storage.extension import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNow @@ -65,12 +65,8 @@ import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.BufferedOutputStream import java.io.File import java.util.Locale -import java.util.zip.CRC32 -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream /** * This class is the one in charge of downloading chapters. @@ -619,70 +615,19 @@ class Downloader( tmpDir: UniFile, ) { // SY --> - if (CbzCrypto.getPasswordProtectDlPref() && CbzCrypto.isPasswordSet()) { - archiveEncryptedChapter(mangaDir, dirname, tmpDir) - return - } + val encrypt = CbzCrypto.getPasswordProtectDlPref() && CbzCrypto.isPasswordSet() // SY <-- val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!! - ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut -> - zipOut.setMethod(ZipEntry.STORED) - - tmpDir.listFiles()?.forEach { img -> - img.openInputStream().use { input -> - val data = input.readBytes() - val size = img.length() - val entry = ZipEntry(img.name).apply { - val crc = CRC32().apply { - update(data) - } - setCrc(crc.value) - - compressedSize = size - setSize(size) - } - zipOut.putNextEntry(entry) - zipOut.write(data) - } + ZipWriter(context, zip, /* SY --> */ encrypt /* SY <-- */).use { writer -> + tmpDir.listFiles()?.forEach { file -> + writer.write(file) } } zip.renameTo("$dirname.cbz") tmpDir.delete() } - // SY --> - - private fun archiveEncryptedChapter( - mangaDir: UniFile, - dirname: String, - tmpDir: UniFile, - ) { - tmpDir.filePath?.let { addPaddingToImage(File(it)) } - - 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() - } - - private fun addPaddingToImage(imageDir: File) { - imageDir.listFiles() - // using ImageUtils isImage and findImageType functions causes IO errors when deleting files to set Exif Metadata - // it should be safe to assume that all files with image extensions are actual images at this point - ?.filter { - it.extension.equals("jpg", true) || - it.extension.equals("jpeg", true) || - it.extension.equals("png", true) || - it.extension.equals("webp", true) - } - ?.forEach { ImageUtil.addPaddingToImageExif(it) } - } - // SY <-- - /** * Creates a ComicInfo.xml file inside the given directory. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt similarity index 52% rename from app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt index 04c357a0e..f00fc25df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt @@ -1,9 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.loader import android.app.Application -import android.os.Build -import com.github.junrar.Archive -import com.github.junrar.rarfile.FileHeader import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage @@ -16,80 +13,69 @@ import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import tachiyomi.core.common.storage.UniFileTempFileManager +import mihon.core.common.archive.ArchiveReader import tachiyomi.core.common.util.system.ImageUtil import uy.kohesive.injekt.injectLazy import java.io.File -import java.io.InputStream -import java.io.PipedInputStream -import java.io.PipedOutputStream -import java.util.concurrent.Executors /** - * Loader used to load a chapter from a .rar or .cbr file. + * Loader used to load a chapter from an archive file. */ -internal class RarPageLoader(file: UniFile) : PageLoader() { - +internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() { // SY --> - private val tempFileManager: UniFileTempFileManager by injectLazy() - - private val rar = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - Archive(tempFileManager.createTempFile(file)) - } else { - Archive(file.openInputStream()) - } - + private val mutex = Mutex() private val context: Application by injectLazy() private val readerPreferences: ReaderPreferences by injectLazy() - private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { + private val tmpDir = File(context.externalCacheDir, "reader_${reader.archiveHashCode}").also { it.deleteRecursively() } init { + reader.wrongPassword?.let { wrongPassword -> + if (wrongPassword) { + error("Incorrect archive password") + } + } if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) { tmpDir.mkdirs() - rar.fileHeaders.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } } - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .forEach { header -> - File(tmpDir, header.fileName.substringAfterLast("/")) - .also { it.createNewFile() } - .outputStream() - .use { output -> - rar.getInputStream(header).use { input -> - input.copyTo(output) + reader.useEntries { entries -> + entries + .filter { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } } + .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + .forEach { entry -> + File(tmpDir, entry.name.substringAfterLast("/")) + .also { it.createNewFile() } + .outputStream() + .use { output -> + reader.getInputStream(entry.name)?.use { input -> + input.copyTo(output) + } } - } - } + } + } } } // SY <-- override var isLocal: Boolean = true - /** - * Pool for copying compressed files to an input stream. - */ - private val pool = Executors.newFixedThreadPool(1) - - override suspend fun getPages(): List { + override suspend fun getPages(): List = reader.useEntries { entries -> // SY --> if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) { return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages() } - val mutex = Mutex() // SY <-- - return rar.fileHeaders.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } } - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .mapIndexed { i, header -> + entries + .filter { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } } + .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + .mapIndexed { i, entry -> // SY --> val imageBytesDeferred: Deferred? = when (readerPreferences.archiveReaderMode().get()) { ReaderPreferences.ArchiveReaderMode.LOAD_INTO_MEMORY -> { CoroutineScope(Dispatchers.IO).async { mutex.withLock { - getStream(header).buffered().use { stream -> + reader.getInputStream(entry.name)!!.buffered().use { stream -> stream.readBytes() } } @@ -98,12 +84,11 @@ internal class RarPageLoader(file: UniFile) : PageLoader() { else -> null } - val imageBytes by lazy { runBlocking { imageBytesDeferred?.await() } } // SY <-- ReaderPage(i).apply { // SY --> - stream = { imageBytes?.copyOf()?.inputStream() ?: getStream(header) } + stream = { imageBytes?.copyOf()?.inputStream() ?: reader.getInputStream(entry.name)!! } // SY <-- status = Page.State.READY } @@ -117,27 +102,9 @@ internal class RarPageLoader(file: UniFile) : PageLoader() { override fun recycle() { super.recycle() - rar.close() + reader.close() // SY --> tmpDir.deleteRecursively() // SY <-- - pool.shutdown() - } - - /** - * Returns an input stream for the given [header]. - */ - private fun getStream(header: FileHeader): InputStream { - val pipeIn = PipedInputStream() - val pipeOut = PipedOutputStream(pipeIn) - pool.execute { - try { - pipeOut.use { - rar.extractFile(header, it) - } - } catch (e: Exception) { - } - } - return pipeIn } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index c05563f63..3c60b1a02 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.loader import android.content.Context -import com.github.junrar.exception.UnsupportedRarV5Exception import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.source.Source @@ -9,6 +8,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences +import mihon.core.common.archive.archiveReader import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat @@ -124,34 +124,26 @@ class ChapterLoader( source is LocalSource -> source.getFormat(chapter.chapter).let { format -> when (format) { is Format.Directory -> DirectoryPageLoader(format.file) - is Format.Zip -> ZipPageLoader(format.file, context) - is Format.Rar -> try { - RarPageLoader(format.file) - } catch (e: UnsupportedRarV5Exception) { - error(context.stringResource(MR.strings.loader_rar5_error)) - } - is Format.Epub -> EpubPageLoader(format.file, context) + is Format.Archive -> ArchivePageLoader(format.file.archiveReader(context)) + is Format.Epub -> EpubPageLoader(format.file.archiveReader(context)) } } else -> error(context.stringResource(MR.strings.loader_not_implemented_error)) } } // SY <-- - isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider) + isDownloaded -> DownloadPageLoader( + chapter, + manga, + source, + downloadManager, + downloadProvider, + ) source is LocalSource -> source.getFormat(chapter.chapter).let { format -> when (format) { is Format.Directory -> DirectoryPageLoader(format.file) - // SY --> - is Format.Zip -> ZipPageLoader(format.file, context) - is Format.Rar -> try { - RarPageLoader(format.file) - // SY <-- - } catch (e: UnsupportedRarV5Exception) { - error(context.stringResource(MR.strings.loader_rar5_error)) - } - // SY --> - is Format.Epub -> EpubPageLoader(format.file, context) - // SY <-- + is Format.Archive -> ArchivePageLoader(format.file.archiveReader(context)) + is Format.Epub -> EpubPageLoader(format.file.archiveReader(context)) } } source is HttpSource -> HttpPageLoader(chapter, source) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index ff70fec7f..c6a6aeadc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import mihon.core.common.archive.archiveReader import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.injectLazy @@ -26,7 +27,7 @@ internal class DownloadPageLoader( private val context: Application by injectLazy() - private var zipPageLoader: ZipPageLoader? = null + private var archivePageLoader: ArchivePageLoader? = null override var isLocal: Boolean = true @@ -42,13 +43,11 @@ internal class DownloadPageLoader( override fun recycle() { super.recycle() - zipPageLoader?.recycle() + archivePageLoader?.recycle() } private suspend fun getPagesFromArchive(file: UniFile): List { - // SY --> - val loader = ZipPageLoader(file, context).also { zipPageLoader = it } - // SY <-- + val loader = ArchivePageLoader(file.archiveReader(context)).also { archivePageLoader = it } return loader.getPages() } @@ -64,6 +63,6 @@ internal class DownloadPageLoader( } override suspend fun loadPage(page: ReaderPage) { - zipPageLoader?.loadPage(page) + archivePageLoader?.loadPage(page) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index 3a4d7388c..8ace2fdee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -1,26 +1,23 @@ package eu.kanade.tachiyomi.ui.reader.loader -import android.content.Context -import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.storage.EpubFile +import mihon.core.common.archive.ArchiveReader /** * Loader used to load a chapter from a .epub file. */ -// SY --> -internal class EpubPageLoader(file: UniFile, context: Context) : PageLoader() { +internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() { - private val epub = EpubFile(file, context) - // SY <-- + private val epub = EpubFile(reader) override var isLocal: Boolean = true override suspend fun getPages(): List { return epub.getImagesFromPages() .mapIndexed { i, path -> - val streamFn = { epub.getInputStream(epub.getEntry(path)!!) } + val streamFn = { epub.getInputStream(path)!! } ReaderPage(i).apply { stream = streamFn status = Page.State.READY 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 deleted file mode 100644 index 75f3278e8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ /dev/null @@ -1,168 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.loader - -import android.content.Context -import android.os.Build -import com.hippo.unifile.UniFile -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences -import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import eu.kanade.tachiyomi.util.storage.CbzCrypto -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.apache.commons.compress.archivers.zip.ZipFile -import tachiyomi.core.common.i18n.stringResource -import tachiyomi.core.common.storage.UniFileTempFileManager -import tachiyomi.core.common.storage.isEncryptedZip -import tachiyomi.core.common.storage.openReadOnlyChannel -import tachiyomi.core.common.storage.testCbzPassword -import tachiyomi.core.common.storage.unzip -import tachiyomi.core.common.util.system.ImageUtil -import tachiyomi.i18n.sy.SYMR -import uy.kohesive.injekt.injectLazy -import java.io.File -import java.nio.channels.SeekableByteChannel -import net.lingala.zip4j.ZipFile as Zip4jFile - -/** - * Loader used to load a chapter from a .zip or .cbz file. - */ -internal class ZipPageLoader(file: UniFile, context: Context) : PageLoader() { - - // SY --> - private val channel: SeekableByteChannel = file.openReadOnlyChannel(context) - private val tempFileManager: UniFileTempFileManager by injectLazy() - private val readerPreferences: ReaderPreferences by injectLazy() - private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { - it.deleteRecursively() - } - - private val apacheZip: ZipFile? = if (!file.isEncryptedZip() && Build.VERSION.SDK_INT > Build.VERSION_CODES.N) { - ZipFile.Builder() - .setSeekableByteChannel(channel) - .get() - } else { - null - } - - private val tmpFile = - if ( - apacheZip == null && - readerPreferences.archiveReaderMode().get() != ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK - ) { - tempFileManager.createTempFile(file) - } else { - null - } - - private val zip4j = - if (apacheZip == null && tmpFile != null) { - Zip4jFile(tmpFile) - } else { - null - } - - init { - if (file.isEncryptedZip()) { - if (!file.testCbzPassword()) { - this.recycle() - throw IllegalStateException(context.stringResource(SYMR.strings.wrong_cbz_archive_password)) - } - zip4j?.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - } - if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) { - file.unzip(tmpDir, onlyCopyImages = true) - } - } - - // SY <-- - override fun recycle() { - super.recycle() - apacheZip?.close() - // SY --> - zip4j?.close() - tmpDir.deleteRecursively() - } - - override var isLocal: Boolean = true - - override suspend fun getPages(): List { - if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) { - return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages() - } - return if (apacheZip == null) { - loadZip4j() - } else { - loadApacheZip(apacheZip) - } - } - - private fun loadZip4j(): List { - val mutex = Mutex() - return zip4j!!.fileHeaders.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip4j.getInputStream(it) } } - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .mapIndexed { i, entry -> - val imageBytesDeferred: Deferred? = - when (readerPreferences.archiveReaderMode().get()) { - ReaderPreferences.ArchiveReaderMode.LOAD_INTO_MEMORY -> { - CoroutineScope(Dispatchers.IO).async { - mutex.withLock { - zip4j.getInputStream(entry).buffered().use { stream -> - stream.readBytes() - } - } - } - } - - else -> null - } - val imageBytes by lazy { runBlocking { imageBytesDeferred?.await() } } - ReaderPage(i).apply { - stream = { imageBytes?.copyOf()?.inputStream() ?: zip4j.getInputStream(entry) } - status = Page.State.READY - } - }.toList() - } - - private fun loadApacheZip(zip: ZipFile): List { - val mutex = Mutex() - return zip.entries.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } - .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - .mapIndexed { i, entry -> - val imageBytesDeferred: Deferred? = - when (readerPreferences.archiveReaderMode().get()) { - ReaderPreferences.ArchiveReaderMode.LOAD_INTO_MEMORY -> { - CoroutineScope(Dispatchers.IO).async { - mutex.withLock { - zip.getInputStream(entry).buffered().use { stream -> - stream.readBytes() - } - } - } - } - - else -> null - } - val imageBytes by lazy { runBlocking { imageBytesDeferred?.await() } } - ReaderPage(i).apply { - stream = { imageBytes?.copyOf()?.inputStream() ?: zip.getInputStream(entry) } - status = Page.State.READY - } - }.toList() - } - // SY <-- - - /** - * No additional action required to load the page - */ - override suspend fun loadPage(page: ReaderPage) { - check(!isRecycled) - } -} diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index ec640936f..6a5e6eaf5 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -558,6 +558,7 @@ LongParameterList:UpdatesRepositoryImpl.kt$UpdatesRepositoryImpl$( mangaId: Long, mangaTitle: String, chapterId: Long, chapterName: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, sourceId: Long, favorite: Boolean, thumbnailUrl: String?, coverLastModified: Long, dateUpload: Long, dateFetch: Long, ) LongParameterList:UpdatesUiItem.kt$( uiModels: List<UpdatesUiModel>, selectionMode: Boolean, // SY --> preserveReadingPosition: Boolean, // SY <-- onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onClickCover: (UpdatesItem) -> Unit, onClickUpdate: (UpdatesItem) -> Unit, onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, ) LongParameterList:WebtoonRecyclerView.kt$WebtoonRecyclerView$( fromRate: Float, toRate: Float, fromX: Float, toX: Float, fromY: Float, toY: Float, ) + LoopWithTooManyJumpStatements:ArchiveReader.kt$ArchiveReader$while LoopWithTooManyJumpStatements:DownloadStore.kt$DownloadStore$for LoopWithTooManyJumpStatements:EHentaiUpdateWorker.kt$EHentaiUpdateWorker$for LoopWithTooManyJumpStatements:ImageUtil.kt$ImageUtil$for @@ -1510,6 +1511,7 @@ NestedBlockDepth:Anilist.kt$Anilist$override suspend fun update(track: Track, didReadChapter: Boolean): Track NestedBlockDepth:ApiMangaParser.kt$ApiMangaParser$fun parseIntoMetadata( metadata: MangaDexSearchMetadata, mangaDto: MangaDto, simpleChapters: List<String>, statistics: StatisticsMangaDto?, ) NestedBlockDepth:AppLanguageScreen.kt$AppLanguageScreen$private fun getLangs(context: Context): ImmutableList<Language> + NestedBlockDepth:ArchiveReader.kt$ArchiveReader$private fun isPasswordIncorrect(): Boolean? NestedBlockDepth:BackupRestorer.kt$BackupRestorer$private fun writeErrorLog(): File NestedBlockDepth:BrowseSourceScreenModel.kt$BrowseSourceScreenModel$fun searchGenre(genreName: String) NestedBlockDepth:ChapterLoader.kt$ChapterLoader$private fun getPageLoader(chapter: ReaderChapter): PageLoader diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 150793bc5..771cdfe69 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { implementation(libs.image.decoder) implementation(libs.unifile) - implementation(libs.bundles.archive) + implementation(libs.libarchive) api(kotlinx.coroutines.core) api(kotlinx.serialization.json) @@ -56,7 +56,6 @@ dependencies { // SY --> implementation(sylibs.xlog) - implementation(libs.zip4j) implementation(libs.injekt.core) implementation(sylibs.exifinterface) // SY <-- diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt index df266988d..f7749c15a 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt @@ -56,7 +56,6 @@ class SecurityPreferences( // SY --> enum class EncryptionType(val titleRes: StringResource) { AES_256(SYMR.strings.aes_256), - AES_192(SYMR.strings.aes_192), AES_128(SYMR.strings.aes_128), ZIP_STANDARD(SYMR.strings.standard_zip_encryption), } 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 465534825..178fa3ac0 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 @@ -9,14 +9,13 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import net.lingala.zip4j.model.ZipParameters -import net.lingala.zip4j.model.enums.AesKeyStrength -import net.lingala.zip4j.model.enums.EncryptionMethod +import mihon.core.common.archive.ArchiveReader +import tachiyomi.core.common.util.system.ImageUtil import uy.kohesive.injekt.injectLazy +import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream -import java.nio.ByteBuffer import java.nio.CharBuffer import java.security.KeyStore import java.security.SecureRandom @@ -33,7 +32,7 @@ import javax.crypto.spec.IvParameterSpec */ object CbzCrypto { const val DATABASE_NAME = "tachiyomiEncrypted.db" - const val DEFAULT_COVER_NAME = "cover.jpg" + private const val DEFAULT_COVER_NAME = "cover.jpg" private val securityPreferences: SecurityPreferences by injectLazy() private val keyStore = KeyStore.getInstance(Keystore).apply { load(null) @@ -129,15 +128,11 @@ object CbzCrypto { return encrypt(password.toByteArray(), encryptionCipherCbz) } - fun getDecryptedPasswordCbz(): CharArray { + fun getDecryptedPasswordCbz(): ByteArray { val encryptedPassword = securityPreferences.cbzPassword().get() if (encryptedPassword.isBlank()) error("This archive is encrypted please set a password") - val cbzBytes = decrypt(encryptedPassword, AliasCbz) - return Charsets.UTF_8.decode(ByteBuffer.wrap(cbzBytes)).array() - .also { - cbzBytes.fill('#'.code.toByte()) - } + return decrypt(encryptedPassword, AliasCbz) } private fun generateAndEncryptSqlPw() { @@ -185,27 +180,12 @@ object CbzCrypto { } } - fun setZipParametersEncrypted(zipParameters: ZipParameters) { - zipParameters.isEncryptFiles = true - + fun getPreferredEncryptionAlgo(): ByteArray = when (securityPreferences.encryptionType().get()) { - SecurityPreferences.EncryptionType.AES_256 -> { - zipParameters.encryptionMethod = EncryptionMethod.AES - zipParameters.aesKeyStrength = AesKeyStrength.KEY_STRENGTH_256 - } - SecurityPreferences.EncryptionType.AES_192 -> { - zipParameters.encryptionMethod = EncryptionMethod.AES - zipParameters.aesKeyStrength = AesKeyStrength.KEY_STRENGTH_192 - } - SecurityPreferences.EncryptionType.AES_128 -> { - zipParameters.encryptionMethod = EncryptionMethod.AES - zipParameters.aesKeyStrength = AesKeyStrength.KEY_STRENGTH_128 - } - SecurityPreferences.EncryptionType.ZIP_STANDARD -> { - zipParameters.encryptionMethod = EncryptionMethod.ZIP_STANDARD - } + SecurityPreferences.EncryptionType.AES_256 -> "zip:encryption=aes256".toByteArray() + SecurityPreferences.EncryptionType.AES_128 -> "zip:encryption=aes128".toByteArray() + SecurityPreferences.EncryptionType.ZIP_STANDARD -> "zip:encryption=zipcrypt".toByteArray() } - } fun detectCoverImageArchive(stream: InputStream): Boolean { val bytes = ByteArray(128) @@ -217,6 +197,15 @@ object CbzCrypto { } return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true) } + + fun ArchiveReader.getCoverStream(): BufferedInputStream? { + this.getInputStream(DEFAULT_COVER_NAME)?.let { stream -> + if (ImageUtil.isImage(DEFAULT_COVER_NAME) { stream }) { + return this.getInputStream(DEFAULT_COVER_NAME)?.buffered() + } + } + return null + } } private const val BufferSize = 2048 diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt index ed8339300..b194c5ee3 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt @@ -1,15 +1,8 @@ package eu.kanade.tachiyomi.util.storage -import android.content.Context -import android.os.Build -import com.hippo.unifile.UniFile -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipFile +import mihon.core.common.archive.ArchiveReader import org.jsoup.Jsoup import org.jsoup.nodes.Document -import tachiyomi.core.common.storage.UniFileTempFileManager -import tachiyomi.core.common.storage.openReadOnlyChannel -import uy.kohesive.injekt.injectLazy import java.io.Closeable import java.io.File import java.io.InputStream @@ -17,45 +10,18 @@ import java.io.InputStream /** * Wrapper over ZipFile to load files in epub format. */ -// SY --> -class EpubFile(file: UniFile, context: Context) : Closeable { - - private val tempFileManager: UniFileTempFileManager by injectLazy() - - /** - * Zip file of this epub. - */ - private val zip = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - ZipFile.Builder().setFile(tempFileManager.createTempFile(file)).get() - } else { - ZipFile.Builder().setSeekableByteChannel(file.openReadOnlyChannel(context)).get() - } - // SY <-- +class EpubFile(private val reader: ArchiveReader) : Closeable by reader { /** * Path separator used by this epub. */ private val pathSeparator = getPathSeparator() - /** - * Closes the underlying zip file. - */ - override fun close() { - zip.close() - } - /** * Returns an input stream for reading the contents of the specified zip file entry. */ - fun getInputStream(entry: ZipArchiveEntry): InputStream { - return zip.getInputStream(entry) - } - - /** - * Returns the zip file entry for the specified name, or null if not found. - */ - fun getEntry(name: String): ZipArchiveEntry? { - return zip.getEntry(name) + fun getInputStream(entryName: String): InputStream? { + return reader.getInputStream(entryName) } /** @@ -72,9 +38,9 @@ class EpubFile(file: UniFile, context: Context) : Closeable { * Returns the path to the package document. */ fun getPackageHref(): String { - val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml")) + val meta = getInputStream(resolveZipPath("META-INF", "container.xml")) if (meta != null) { - val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") } + val metaDoc = meta.use { Jsoup.parse(it, null, "") } val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path") if (path != null) { return path @@ -87,8 +53,7 @@ class EpubFile(file: UniFile, context: Context) : Closeable { * Returns the package document where all the files are listed. */ fun getPackageDocument(ref: String): Document { - val entry = zip.getEntry(ref) - return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") } + return getInputStream(ref)!!.use { Jsoup.parse(it, null, "") } } /** @@ -111,8 +76,7 @@ class EpubFile(file: UniFile, context: Context) : Closeable { val basePath = getParentDirectory(packageHref) pages.forEach { page -> val entryPath = resolveZipPath(basePath, page) - val entry = zip.getEntry(entryPath) - val document = zip.getInputStream(entry).use { Jsoup.parse(it, null, "") } + val document = getInputStream(entryPath)!!.use { Jsoup.parse(it, null, "") } val imageBasePath = getParentDirectory(entryPath) document.allElements.forEach { @@ -130,8 +94,9 @@ class EpubFile(file: UniFile, context: Context) : Closeable { * Returns the path separator used by the epub file. */ private fun getPathSeparator(): String { - val meta = zip.getEntry("META-INF\\container.xml") + val meta = getInputStream("META-INF\\container.xml") return if (meta != null) { + meta.close() "\\" } else { "/" diff --git a/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveEntry.kt b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveEntry.kt new file mode 100644 index 000000000..f9cd9c190 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveEntry.kt @@ -0,0 +1,7 @@ +package mihon.core.common.archive + +class ArchiveEntry( + val name: String, + val isFile: Boolean, + val isEncrypted: Boolean, +) diff --git a/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveInputStream.kt b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveInputStream.kt new file mode 100644 index 000000000..17c46d6ba --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveInputStream.kt @@ -0,0 +1,84 @@ +package mihon.core.common.archive + +import eu.kanade.tachiyomi.util.storage.CbzCrypto +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +import me.zhanghai.android.libarchive.ArchiveException +import java.io.InputStream +import java.nio.ByteBuffer +import kotlin.concurrent.Volatile + +class ArchiveInputStream( + buffer: Long, + size: Long, + // SY --> + encrypted: Boolean, + // SY <-- +) : InputStream() { + private val lock = Any() + + @Volatile + private var isClosed = false + + private val archive = Archive.readNew() + + init { + try { + // SY --> + if (encrypted) { + Archive.readAddPassphrase(archive, CbzCrypto.getDecryptedPasswordCbz()) + } + // SY <-- + Archive.setCharset(archive, Charsets.UTF_8.name().toByteArray()) + Archive.readSupportFilterAll(archive) + Archive.readSupportFormatAll(archive) + Archive.readOpenMemoryUnsafe(archive, buffer, size) + } catch (e: ArchiveException) { + close() + throw e + } + } + + private val oneByteBuffer = ByteBuffer.allocateDirect(1) + + override fun read(): Int { + read(oneByteBuffer) + return if (oneByteBuffer.hasRemaining()) oneByteBuffer.get().toUByte().toInt() else -1 + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + val buffer = ByteBuffer.wrap(b, off, len) + read(buffer) + return if (buffer.hasRemaining()) buffer.remaining() else -1 + } + + private fun read(buffer: ByteBuffer) { + buffer.clear() + Archive.readData(archive, buffer) + buffer.flip() + } + + override fun close() { + synchronized(lock) { + if (isClosed) return + isClosed = true + } + + Archive.readFree(archive) + } + + fun getNextEntry() = Archive.readNextHeader(archive).takeUnless { it == 0L }?.let { entry -> + val name = ArchiveEntry.pathnameUtf8(entry) ?: ArchiveEntry.pathname(entry)?.decodeToString() ?: return null + val isFile = ArchiveEntry.filetype(entry) == ArchiveEntry.AE_IFREG + // SY --> + val isEncrypted = ArchiveEntry.isEncrypted(entry) + // SY <-- + ArchiveEntry( + name, + isFile, + // SY --> + isEncrypted + // SY <-- + ) + } +} diff --git a/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveReader.kt b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveReader.kt new file mode 100644 index 000000000..f0312092f --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveReader.kt @@ -0,0 +1,94 @@ +package mihon.core.common.archive + +import android.content.Context +import android.os.ParcelFileDescriptor +import android.system.Os +import android.system.OsConstants +import com.hippo.unifile.UniFile +import me.zhanghai.android.libarchive.ArchiveException +import tachiyomi.core.common.storage.openFileDescriptor +import java.io.Closeable +import java.io.InputStream + +class ArchiveReader(pfd: ParcelFileDescriptor) : Closeable { + val size = pfd.statSize + val address = Os.mmap(0, size, OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, pfd.fileDescriptor, 0) + + // SY --> + var encrypted: Boolean = false + private set + var wrongPassword: Boolean? = null + private set + val archiveHashCode = pfd.hashCode() + + init { + checkEncryptionStatus() + } + // SY <-- + + inline fun useEntries(block: (Sequence) -> T): T = ArchiveInputStream( + address, + size, + // SY --> + encrypted, + // SY <-- + ).use { block(generateSequence { it.getNextEntry() }) } + + fun getInputStream(entryName: String): InputStream? { + val archive = ArchiveInputStream(address, size, /* SY --> */ encrypted /* SY <-- */) + try { + while (true) { + val entry = archive.getNextEntry() ?: break + if (entry.name == entryName) { + return archive + } + } + } catch (e: ArchiveException) { + archive.close() + throw e + } + archive.close() + return null + } + + // SY --> + private fun checkEncryptionStatus() { + val archive = ArchiveInputStream(address, size, false) + try { + while (true) { + val entry = archive.getNextEntry() ?: break + if (entry.isEncrypted) { + encrypted = true + isPasswordIncorrect(entry.name) + break + } + } + } catch (e: ArchiveException) { + archive.close() + throw e + } + archive.close() + } + + private fun isPasswordIncorrect(entryName: String) { + try { + getInputStream(entryName).use { stream -> + stream!!.read() + } + } catch (e: ArchiveException) { + if (e.message == "Incorrect passphrase") { + wrongPassword = true + return + } + throw e + } + wrongPassword = false + } + // SY <-- + + override fun close() { + Os.munmap(address, size) + } +} + +fun UniFile.archiveReader(context: Context) = openFileDescriptor(context, "r").use { ArchiveReader(it) } diff --git a/core/common/src/main/kotlin/mihon/core/common/archive/ZipWriter.kt b/core/common/src/main/kotlin/mihon/core/common/archive/ZipWriter.kt new file mode 100644 index 000000000..3b5406d54 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ZipWriter.kt @@ -0,0 +1,119 @@ +package mihon.core.common.archive + +import android.content.Context +import android.system.Os +import android.system.StructStat +import com.hippo.unifile.UniFile +import eu.kanade.tachiyomi.util.storage.CbzCrypto +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +import me.zhanghai.android.libarchive.ArchiveEntry.AE_IFREG +import me.zhanghai.android.libarchive.ArchiveException +import tachiyomi.core.common.storage.openFileDescriptor +import java.io.Closeable +import java.nio.ByteBuffer + +class ZipWriter( + val context: Context, + file: UniFile, + // SY --> + encrypt: Boolean = false, + // SY <-- +) : Closeable { + private val pfd = file.openFileDescriptor(context, "wt") + private val archive = Archive.writeNew() + private val entry = ArchiveEntry.new2(archive) + private val buffer = ByteBuffer.allocateDirect( + // SY --> + BUFFER_SIZE + // SY <-- + ) + + init { + try { + Archive.setCharset(archive, Charsets.UTF_8.name().toByteArray()) + Archive.writeSetFormatZip(archive) + Archive.writeZipSetCompressionStore(archive) + // SY --> + if (encrypt) { + Archive.writeSetOptions(archive, CbzCrypto.getPreferredEncryptionAlgo()) + Archive.writeSetPassphrase(archive, CbzCrypto.getDecryptedPasswordCbz()) + } + // SY <-- + Archive.writeOpenFd(archive, pfd.fd) + } catch (e: ArchiveException) { + close() + throw e + } + } + + fun write(file: UniFile) { + file.openFileDescriptor(context, "r").use { + val fd = it.fileDescriptor + ArchiveEntry.clear(entry) + ArchiveEntry.setPathnameUtf8(entry, file.name) + val stat = Os.fstat(fd) + ArchiveEntry.setStat(entry, stat.toArchiveStat()) + Archive.writeHeader(archive, entry) + while (true) { + buffer.clear() + Os.read(fd, buffer) + if (buffer.position() == 0) break + buffer.flip() + Archive.writeData(archive, buffer) + } + Archive.writeFinishEntry(archive) + } + } + + // SY --> + fun write(fileData: ByteArray, fileName: String) { + ArchiveEntry.clear(entry) + ArchiveEntry.setPathnameUtf8(entry, fileName) + ArchiveEntry.setSize(entry, fileData.size.toLong()) + ArchiveEntry.setFiletype(entry, AE_IFREG) + Archive.writeHeader(archive, entry) + + var position = 0 + while (position < fileData.size) { + val lengthToRead = minOf(BUFFER_SIZE, fileData.size - position) + buffer.clear() + buffer.put(fileData, position, lengthToRead) + buffer.flip() + Archive.writeData(archive, buffer) + position += lengthToRead + } + Archive.writeFinishEntry(archive) + } + // SY <-- + + override fun close() { + ArchiveEntry.free(entry) + Archive.writeFree(archive) + pfd.close() + } + + // SY --> + companion object { + private const val BUFFER_SIZE = 8192 + } + // SY <-- +} + +private fun StructStat.toArchiveStat() = ArchiveEntry.StructStat().apply { + stDev = st_dev + stMode = st_mode + stNlink = st_nlink.toInt() + stUid = st_uid + stGid = st_gid + stRdev = st_rdev + stSize = st_size + stBlksize = st_blksize + stBlocks = st_blocks + stAtim = timespec(st_atime) + stMtim = timespec(st_mtime) + stCtim = timespec(st_ctime) + stIno = st_ino +} + +private fun timespec(tvSec: Long) = ArchiveEntry.StructTimespec().also { it.tvSec = tvSec } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt b/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt index f99ad117c..4b04ff405 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt @@ -3,19 +3,6 @@ package tachiyomi.core.common.storage import android.content.Context import android.os.ParcelFileDescriptor import com.hippo.unifile.UniFile -import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import eu.kanade.tachiyomi.util.storage.CbzCrypto -import logcat.LogPriority -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 tachiyomi.core.common.util.system.ImageUtil -import tachiyomi.core.common.util.system.logcat -import java.io.File -import java.io.InputStream -import java.nio.channels.FileChannel val UniFile.extension: String? get() = name?.substringAfterLast('.') @@ -26,200 +13,5 @@ val UniFile.nameWithoutExtension: String? val UniFile.displayablePath: String get() = filePath ?: uri.toString() -fun UniFile.openReadOnlyChannel(context: Context): FileChannel { - return ParcelFileDescriptor.AutoCloseInputStream(context.contentResolver.openFileDescriptor(uri, "r")).channel -// SY --> -} - -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(CbzCrypto.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) CbzCrypto.setZipParametersEncrypted(zipParameters) - zipOutputStream.putNextEntry(zipParameters) - - zipOutputStream.use { output -> - inputStream.use { input -> - input.copyTo(output) - } - } -} - -/** - * Unzips encrypted or unencrypted zip files using zip4j. - * The caller is responsible to ensure, that the file this is called from is a zip archive - */ -fun UniFile.unzip(destination: File, onlyCopyImages: Boolean = false) { - destination.mkdirs() - if (!destination.isDirectory) return - - val zipInputStream = ZipInputStream(this.openInputStream()) - var fileHeader: LocalFileHeader? - - if (this.isEncryptedZip()) { - zipInputStream.setPassword(CbzCrypto.getDecryptedPasswordCbz()) - } - try { - while ( - run { - fileHeader = zipInputStream.nextEntry - fileHeader != null - } - ) { - val tmpFile = File("${destination.absolutePath}/${fileHeader!!.fileName}") - - if (onlyCopyImages) { - if (!fileHeader!!.isDirectory && ImageUtil.isImage(fileHeader!!.fileName)) { - tmpFile.createNewFile() - tmpFile.outputStream().buffered().use { tmpOut -> - zipInputStream.buffered().copyTo(tmpOut) - } - } - } else { - if (!fileHeader!!.isDirectory && ImageUtil.isImage(fileHeader!!.fileName)) { - tmpFile.createNewFile() - tmpFile - .outputStream() - .buffered() - .use { zipInputStream.buffered().copyTo(it) } - } - } - } - zipInputStream.close() - } 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 - } - } -} - -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) CbzCrypto.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(CbzCrypto.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(CbzCrypto.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 - } - } -} -// SY <-- +fun UniFile.openFileDescriptor(context: Context, mode: String): ParcelFileDescriptor = + context.contentResolver.openFileDescriptor(uri, mode) ?: error("Failed to open file descriptor: $displayablePath") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74be61c9f..3c4666c7e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,9 +32,7 @@ jsoup = "org.jsoup:jsoup:1.18.1" disklrucache = "com.jakewharton:disklrucache:2.0.2" unifile = "com.github.tachiyomiorg:unifile:e0def6b3dc" -common-compress = "org.apache.commons:commons-compress:1.26.2" -junrar = "com.github.junrar:junrar:7.5.5" -zip4j = "net.lingala.zip4j:zip4j:2.11.5" +libarchive = "me.zhanghai.android.libarchive:library:1.1.0" sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" } @@ -105,7 +103,6 @@ detekt-rules-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatt detekt-rules-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } [bundles] -archive = ["common-compress", "junrar"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] js-engine = ["quickjs-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] diff --git a/i18n-sy/src/commonMain/moko-resources/base/strings.xml b/i18n-sy/src/commonMain/moko-resources/base/strings.xml index 48231a8b5..05f6d333e 100644 --- a/i18n-sy/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/base/strings.xml @@ -279,7 +279,6 @@ Wrong CBZ archive password Encryption type AES 256 - AES 192 AES 128 Standard zip encryption (fast but insecure) diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 9168212fb..3f147ecc2 100755 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -793,7 +793,6 @@ Failed to load pages: %1$s No pages found Source not found - RARv5 format is not supported Updating library diff --git a/source-local/build.gradle.kts b/source-local/build.gradle.kts index 3db447835..1a34cd1a8 100644 --- a/source-local/build.gradle.kts +++ b/source-local/build.gradle.kts @@ -15,10 +15,6 @@ kotlin { // SY <-- implementation(libs.unifile) - implementation(libs.bundles.archive) - // SY --> - implementation(libs.zip4j) - // SY <-- } } val androidMain by getting { 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 aa00f4ee0..b0decf05e 100755 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -1,7 +1,6 @@ package tachiyomi.source.local import android.content.Context -import android.os.Build import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source @@ -12,22 +11,18 @@ import eu.kanade.tachiyomi.source.model.Page 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.EpubFile import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority +import mihon.core.common.archive.ZipWriter +import mihon.core.common.archive.archiveReader import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.serialization.XML import tachiyomi.core.common.i18n.stringResource -import tachiyomi.core.common.storage.UniFileTempFileManager -import tachiyomi.core.common.storage.addStreamToZip import tachiyomi.core.common.storage.extension -import tachiyomi.core.common.storage.getCoverStreamFromZip -import tachiyomi.core.common.storage.getZipInputStream -import tachiyomi.core.common.storage.isEncryptedZip import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.ImageUtil @@ -51,7 +46,6 @@ import uy.kohesive.injekt.injectLazy import java.io.InputStream import java.nio.charset.StandardCharsets import kotlin.time.Duration.Companion.days -import com.github.junrar.Archive as JunrarArchive import tachiyomi.domain.source.model.Source as DomainSource actual class LocalSource( @@ -65,7 +59,6 @@ actual class LocalSource( private val json: Json by injectLazy() private val xml: XML by injectLazy() - private val tempFileManager: UniFileTempFileManager by injectLazy() private val POPULAR_FILTERS = FilterList(OrderBy.Popular(context)) private val LATEST_FILTERS = FilterList(OrderBy.Latest(context)) @@ -161,13 +154,14 @@ actual class LocalSource( val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url) val existingFile = mangaDirFiles .firstOrNull { it.name == COMIC_INFO_FILE } - val comicInfoArchiveFile = mangaDirFiles - .firstOrNull { it.name == COMIC_INFO_ARCHIVE } - val existingComicInfo = (existingFile?.openInputStream() ?: comicInfoArchiveFile?.getZipInputStream(COMIC_INFO_FILE))?.use { - AndroidXmlReader(it, StandardCharsets.UTF_8.name()).use { - xml.decodeFromReader(it) + val comicInfoArchiveFile = mangaDirFiles.firstOrNull { it.name == COMIC_INFO_ARCHIVE } + val comicInfoArchiveReader = comicInfoArchiveFile?.archiveReader(context) + val existingComicInfo = + (existingFile?.openInputStream() ?: comicInfoArchiveReader?.getInputStream(COMIC_INFO_FILE))?.use { + AndroidXmlReader(it, StandardCharsets.UTF_8.name()).use { xmlReader -> + xml.decodeFromReader(xmlReader) + } } - } val newComicInfo = if (existingComicInfo != null) { manga.run { existingComicInfo.copy( @@ -188,8 +182,9 @@ actual class LocalSource( fileSystem.getMangaDirectory(manga.url)?.let { copyComicInfoFile( xml.encodeToString(ComicInfo.serializer(), newComicInfo).byteInputStream(), - it - ) + it, + comicInfoArchiveReader?.encrypted ?: false + ) } } // SY <-- @@ -202,8 +197,8 @@ actual class LocalSource( // Augment manga details based on metadata files try { - val mangaDir by lazy { fileSystem.getMangaDirectory(manga.url) } - val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url) + val mangaDir = fileSystem.getMangaDirectory(manga.url) ?: error("${manga.url} is not a valid directory") + val mangaDirFiles = mangaDir.listFiles().orEmpty() val comicInfoFile = mangaDirFiles .firstOrNull { it.name == COMIC_INFO_FILE } @@ -226,7 +221,7 @@ actual class LocalSource( comicInfoArchiveFile != null -> { noXmlFile?.delete() - comicInfoArchiveFile.getZipInputStream(COMIC_INFO_FILE) + comicInfoArchiveFile.archiveReader(context).getInputStream(COMIC_INFO_FILE) ?.let { setMangaDetailsFromComicInfoFile(it, manga) } } @@ -246,7 +241,7 @@ actual class LocalSource( // Replace with ComicInfo.xml file val comicInfo = manga.getComicInfo() mangaDir - ?.createFile(COMIC_INFO_FILE) + .createFile(COMIC_INFO_FILE) ?.openOutputStream() ?.use { val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo) @@ -257,21 +252,20 @@ actual class LocalSource( // Copy ComicInfo.xml from chapter archive to top level if found noXmlFile == null -> { - val chapterArchives = mangaDirFiles - .filter(Archive::isSupported) - .toList() + val chapterArchives = mangaDirFiles.filter(Archive::isSupported) - val copiedFile = mangaDir?.let { copyComicInfoFileFromArchive(chapterArchives, it) } + val copiedFile = copyComicInfoFileFromArchive(chapterArchives, mangaDir) // SY --> if (copiedFile != null && copiedFile.name != COMIC_INFO_ARCHIVE) { setMangaDetailsFromComicInfoFile(copiedFile.openInputStream(), manga) } else if (copiedFile != null && copiedFile.name == COMIC_INFO_ARCHIVE) { - copiedFile.getZipInputStream(COMIC_INFO_FILE)?.let { setMangaDetailsFromComicInfoFile(it, manga) } + copiedFile.archiveReader(context).getInputStream(COMIC_INFO_FILE) + ?.let { setMangaDetailsFromComicInfoFile(it, manga) } } // SY <-- else { // Avoid re-scanning - mangaDir?.createFile(".noxml") + mangaDir.createFile(".noxml") } } } @@ -284,44 +278,31 @@ actual class LocalSource( private fun copyComicInfoFileFromArchive(chapterArchives: List, folder: UniFile): UniFile? { for (chapter in chapterArchives) { - when (Format.valueOf(chapter)) { - is Format.Zip -> { - // SY --> - chapter.getZipInputStream(COMIC_INFO_FILE)?.buffered().use { stream -> - return stream?.let { copyComicInfoFile(it, folder) } - } + chapter.archiveReader(context).use { reader -> + reader.getInputStream(COMIC_INFO_FILE)?.use { stream -> + return copyComicInfoFile(stream, folder, /* SY --> */ reader.encrypted /* SY <-- */) } - is Format.Rar -> { - val archive = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - JunrarArchive(tempFileManager.createTempFile(chapter)) - } else { - JunrarArchive(chapter.openInputStream()) - } - - archive.use { rar -> - rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> - rar.getInputStream(comicInfoFile).buffered().use { stream -> - return copyComicInfoFile(stream, folder) - } - } - } - } - else -> {} } } return null } - private fun copyComicInfoFile(comicInfoFileStream: InputStream, folder: UniFile): UniFile? { + private fun copyComicInfoFile( + comicInfoFileStream: InputStream, + folder: UniFile, // SY --> - if ( - CbzCrypto.getPasswordProtectDlPref() && - CbzCrypto.isPasswordSet() - ) { - val comicInfoArchive = folder.createFile(COMIC_INFO_ARCHIVE) - comicInfoArchive?.addStreamToZip(comicInfoFileStream, COMIC_INFO_FILE, CbzCrypto.getDecryptedPasswordCbz()) - - return comicInfoArchive + encrypt: Boolean, + // SY <-- + ): UniFile? { + // SY --> + if (encrypt) { + val comicInfoArchiveFile = folder.createFile(COMIC_INFO_ARCHIVE) + comicInfoArchiveFile?.let { archive -> + ZipWriter(context, archive, encrypt = true).use { writer -> + writer.write(comicInfoFileStream.use { it.readBytes() }, COMIC_INFO_FILE) + } + } + return comicInfoArchiveFile } else { // SY <-- return folder.createFile(COMIC_INFO_FILE)?.apply { @@ -344,7 +325,7 @@ actual class LocalSource( override suspend fun getChapterList(manga: SManga): List = withIOContext { val chapters = fileSystem.getFilesInMangaDirectory(manga.url) // Only keep supported formats - .filter { it.isDirectory || Archive.isSupported(it) } + .filter { it.isDirectory || Archive.isSupported(it) || it.extension.equals("epub", true) } .map { chapterFile -> SChapter.create().apply { url = "${manga.url}/${chapterFile.name}" @@ -360,7 +341,7 @@ actual class LocalSource( val format = Format.valueOf(chapterFile) if (format is Format.Epub) { - EpubFile(format.file, context).use { epub -> + EpubFile(format.file.archiveReader(context)).use { epub -> epub.fillMetadata(manga, this) } } @@ -418,43 +399,25 @@ actual class LocalSource( entry?.let { coverManager.update(manga, it.openInputStream()) } } - is Format.Zip -> { - // SY --> - format.file.getCoverStreamFromZip()?.let { inputStream -> - coverManager.update( - manga, - inputStream, - format.file.isEncryptedZip() - ) - } - } - is Format.Rar -> { - val rarArchive = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - JunrarArchive(tempFileManager.createTempFile(format.file)) - } else { - JunrarArchive(format.file.openInputStream()) - } - rarArchive.use { archive -> - // SY <-- - val entry = archive.fileHeaders - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } + is Format.Archive -> { + format.file.archiveReader(context).use { reader -> + val entry = reader.useEntries { entries -> + entries + .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + .find { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } } + } - entry?.let { coverManager.update(manga, archive.getInputStream(it)) } + entry?.let { coverManager.update(manga, reader.getInputStream(it.name)!!, reader.encrypted) } } } is Format.Epub -> { - // SY --> - EpubFile(format.file, context).use { epub -> - // SY <-- - val entry = epub.getImagesFromPages() - .firstOrNull() - ?.let { epub.getEntry(it) } + EpubFile(format.file.archiveReader(context)).use { epub -> + val entry = epub.getImagesFromPages().firstOrNull() - entry?.let { coverManager.update(manga, epub.getInputStream(it)) } - } + entry?.let { coverManager.update(manga, epub.getInputStream(it)!!) } } } + } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" } null 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 390e901dc..645433625 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 @@ -3,9 +3,8 @@ package tachiyomi.source.local.image import android.content.Context import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.util.storage.CbzCrypto -import tachiyomi.core.common.storage.addStreamToZip import eu.kanade.tachiyomi.util.storage.DiskUtil +import mihon.core.common.archive.ZipWriter import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.source.local.io.LocalSourceFileSystem @@ -58,7 +57,9 @@ actual class LocalCoverManager( inputStream.use { input -> // SY --> if (encrypted) { - targetFile.addStreamToZip(inputStream, DEFAULT_COVER_NAME, CbzCrypto.getDecryptedPasswordCbz()) + ZipWriter(context, targetFile, encrypt = true ).use { writer -> + writer.write(inputStream.readBytes(), DEFAULT_COVER_NAME) + } DiskUtil.createNoMediaFile(directory, context) manga.thumbnail_url = targetFile.uri.toString() diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt index e968adc7d..ea18e9b53 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt @@ -5,9 +5,9 @@ import tachiyomi.core.common.storage.extension object Archive { - private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") + private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "7z", "cb7", "tar", "cbt") fun isSupported(file: UniFile): Boolean { - return file.extension in SUPPORTED_ARCHIVE_TYPES + return file.extension?.lowercase() in SUPPORTED_ARCHIVE_TYPES } } diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt index 5b22e41e2..ad53d407c 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt @@ -2,25 +2,22 @@ package tachiyomi.source.local.io import com.hippo.unifile.UniFile import tachiyomi.core.common.storage.extension +import tachiyomi.source.local.io.Archive.isSupported as isArchiveSupported sealed interface Format { data class Directory(val file: UniFile) : Format - data class Zip(val file: UniFile) : Format - data class Rar(val file: UniFile) : Format + data class Archive(val file: UniFile) : Format data class Epub(val file: UniFile) : Format class UnknownFormatException : Exception() companion object { - fun valueOf(file: UniFile) = with(file) { - when { - isDirectory -> Directory(this) - extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this) - extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this) - extension.equals("epub", true) -> Epub(this) - else -> throw UnknownFormatException() - } + fun valueOf(file: UniFile) = when { + file.isDirectory -> Directory(file) + file.extension.equals("epub", true) -> Epub(file) + isArchiveSupported(file) -> Archive(file) + else -> throw UnknownFormatException() } } }