diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 622715259..9ea8f6ccf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -203,7 +203,6 @@ dependencies { // Disk implementation(libs.disklrucache) implementation(libs.unifile) - implementation(libs.bundles.archive) // Preferences implementation(libs.preferencektx) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f4451efda..b2971f1b7 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -77,9 +77,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/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 11bfe28d1..ebf9df65e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -38,6 +38,7 @@ 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 @@ -58,12 +59,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. @@ -594,25 +591,9 @@ class Downloader( tmpDir: UniFile, ) { 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).use { writer -> + tmpDir.listFiles()?.forEach { file -> + writer.write(file) } } zip.renameTo("$dirname.cbz") 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/ArchivePageLoader.kt similarity index 57% rename from app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt index 89856bf22..397ac51bc 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/ArchivePageLoader.kt @@ -3,26 +3,22 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import mihon.core.common.extensions.toZipFile +import mihon.core.common.archive.ArchiveReader import tachiyomi.core.common.util.system.ImageUtil -import java.nio.channels.SeekableByteChannel /** - * Loader used to load a chapter from a .zip or .cbz file. + * Loader used to load a chapter from an archive file. */ -internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() { - - private val zip = channel.toZipFile() - +internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() { override var isLocal: Boolean = true - override suspend fun getPages(): List { - return zip.entries.asSequence() - .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } + override suspend fun getPages(): List = reader.useEntries { entries -> + entries + .filter { it.isFile && ImageUtil.isImage(it.name) { reader.getInputStream(it.name)!! } } .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .mapIndexed { i, entry -> ReaderPage(i).apply { - stream = { zip.getInputStream(entry) } + stream = { reader.getInputStream(entry.name)!! } status = Page.State.READY } } @@ -35,6 +31,6 @@ internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() { override fun recycle() { super.recycle() - zip.close() + reader.close() } } 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 1cd18bceb..3c1b34d6c 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,14 +1,13 @@ 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 import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter +import mihon.core.common.archive.archiveReader import tachiyomi.core.common.i18n.stringResource -import tachiyomi.core.common.storage.openReadOnlyChannel import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.manga.model.Manga @@ -95,13 +94,8 @@ 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.openReadOnlyChannel(context)) - is Format.Rar -> try { - RarPageLoader(format.file.openInputStream()) - } catch (e: UnsupportedRarV5Exception) { - error(context.stringResource(MR.strings.loader_rar5_error)) - } - is Format.Epub -> EpubPageLoader(format.file.openReadOnlyChannel(context)) + 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 abef28540..7b0f5c36c 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,7 +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 tachiyomi.core.common.storage.openReadOnlyChannel +import mihon.core.common.archive.archiveReader import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.injectLazy @@ -27,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 @@ -43,11 +43,11 @@ internal class DownloadPageLoader( override fun recycle() { super.recycle() - zipPageLoader?.recycle() + archivePageLoader?.recycle() } private suspend fun getPagesFromArchive(file: UniFile): List { - val loader = ZipPageLoader(file.openReadOnlyChannel(context)).also { zipPageLoader = it } + val loader = ArchivePageLoader(file.archiveReader(context)).also { archivePageLoader = it } return loader.getPages() } @@ -63,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 baf65324b..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 @@ -3,21 +3,21 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.storage.EpubFile -import java.nio.channels.SeekableByteChannel +import mihon.core.common.archive.ArchiveReader /** * Loader used to load a chapter from a .epub file. */ -internal class EpubPageLoader(channel: SeekableByteChannel) : PageLoader() { +internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() { - private val epub = EpubFile(channel) + 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/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt deleted file mode 100644 index b1db3300d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ /dev/null @@ -1,67 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.loader - -import com.github.junrar.Archive -import com.github.junrar.rarfile.FileHeader -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import tachiyomi.core.common.util.system.ImageUtil -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. - */ -internal class RarPageLoader(inputStream: InputStream) : PageLoader() { - - private val rar = Archive(inputStream) - - 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 { - 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 -> - ReaderPage(i).apply { - stream = { getStream(header) } - status = Page.State.READY - } - } - .toList() - } - - override suspend fun loadPage(page: ReaderPage) { - check(!isRecycled) - } - - override fun recycle() { - super.recycle() - rar.close() - 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/core/common/build.gradle.kts b/core/common/build.gradle.kts index e31015dc4..d00fec682 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { implementation(libs.image.decoder) implementation(libs.unifile) - implementation(libs.bundles.archive) + implementation(libs.libarchive) api(kotlinx.coroutines.core) api(kotlinx.serialization.json) 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 29cea5824..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,48 +1,27 @@ package eu.kanade.tachiyomi.util.storage -import mihon.core.common.extensions.toZipFile -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import mihon.core.common.archive.ArchiveReader import org.jsoup.Jsoup import org.jsoup.nodes.Document import java.io.Closeable import java.io.File import java.io.InputStream -import java.nio.channels.SeekableByteChannel /** * Wrapper over ZipFile to load files in epub format. */ -class EpubFile(channel: SeekableByteChannel) : Closeable { - - /** - * Zip file of this epub. - */ - private val zip = channel.toZipFile() +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) } /** @@ -59,9 +38,9 @@ class EpubFile(channel: SeekableByteChannel) : 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 @@ -74,8 +53,7 @@ class EpubFile(channel: SeekableByteChannel) : 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, "") } } /** @@ -98,8 +76,7 @@ class EpubFile(channel: SeekableByteChannel) : 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 { @@ -117,8 +94,9 @@ class EpubFile(channel: SeekableByteChannel) : 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..26240de00 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveEntry.kt @@ -0,0 +1,6 @@ +package mihon.core.common.archive + +class ArchiveEntry( + val name: String, + val isFile: 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..a9bb87879 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveInputStream.kt @@ -0,0 +1,52 @@ +package mihon.core.common.archive + +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 + +class ArchiveInputStream(buffer: Long, size: Long) : InputStream() { + private val archive = Archive.readNew() + + init { + try { + 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() { + 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 + ArchiveEntry(name, isFile) + } +} 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..28467d0fe --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ArchiveReader.kt @@ -0,0 +1,42 @@ +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) + + inline fun useEntries(block: (Sequence) -> T): T = + ArchiveInputStream(address, size).use { block(generateSequence { it.getNextEntry() }) } + + fun getInputStream(entryName: String): InputStream? { + val archive = ArchiveInputStream(address, size) + 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 + } + + 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..b5d201516 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/archive/ZipWriter.kt @@ -0,0 +1,74 @@ +package mihon.core.common.archive + +import android.content.Context +import android.system.Os +import android.system.StructStat +import com.hippo.unifile.UniFile +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +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) : Closeable { + private val pfd = file.openFileDescriptor(context, "wt") + private val archive = Archive.writeNew() + private val entry = ArchiveEntry.new2(archive) + private val buffer = ByteBuffer.allocateDirect(8192) + + init { + try { + Archive.setCharset(archive, Charsets.UTF_8.name().toByteArray()) + Archive.writeSetFormatZip(archive) + Archive.writeZipSetCompressionStore(archive) + 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) + } + } + + override fun close() { + ArchiveEntry.free(entry) + Archive.writeFree(archive) + pfd.close() + } +} + +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/mihon/core/common/extensions/SeekableByteChannel.kt b/core/common/src/main/kotlin/mihon/core/common/extensions/SeekableByteChannel.kt deleted file mode 100644 index 69e2d7201..000000000 --- a/core/common/src/main/kotlin/mihon/core/common/extensions/SeekableByteChannel.kt +++ /dev/null @@ -1,8 +0,0 @@ -package mihon.core.common.extensions - -import org.apache.commons.compress.archivers.zip.ZipFile -import java.nio.channels.SeekableByteChannel - -fun SeekableByteChannel.toZipFile(): ZipFile { - return ZipFile.Builder().setSeekableByteChannel(this).get() -} 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 257fe210d..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,7 +3,6 @@ package tachiyomi.core.common.storage import android.content.Context import android.os.ParcelFileDescriptor import com.hippo.unifile.UniFile -import java.nio.channels.FileChannel val UniFile.extension: String? get() = name?.substringAfterLast('.') @@ -14,6 +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 -} +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 3942f468f..36bf03900 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,8 +32,7 @@ jsoup = "org.jsoup:jsoup:1.17.2" 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" +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" } @@ -104,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/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index b0a397b1d..afbdf686c 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -781,7 +781,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 b0a720b97..25c268a34 100644 --- a/source-local/build.gradle.kts +++ b/source-local/build.gradle.kts @@ -12,7 +12,6 @@ kotlin { api(projects.i18n) implementation(libs.unifile) - implementation(libs.bundles.archive) } } 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 8efea5fd2..2d9725ad8 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -17,13 +17,12 @@ import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority -import mihon.core.common.extensions.toZipFile +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.extension import tachiyomi.core.common.storage.nameWithoutExtension -import tachiyomi.core.common.storage.openReadOnlyChannel import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.logcat @@ -45,7 +44,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( @@ -187,9 +185,7 @@ 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 = copyComicInfoFileFromArchive(chapterArchives, mangaDir) if (copiedFile != null) { @@ -209,26 +205,10 @@ actual class LocalSource( private fun copyComicInfoFileFromArchive(chapterArchives: List, folder: UniFile): UniFile? { for (chapter in chapterArchives) { - when (Format.valueOf(chapter)) { - is Format.Zip -> { - chapter.openReadOnlyChannel(context).toZipFile().use { zip -> - zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> - zip.getInputStream(comicInfoFile).buffered().use { stream -> - return copyComicInfoFile(stream, folder) - } - } - } + chapter.archiveReader(context).use { reader -> + reader.getInputStream(COMIC_INFO_FILE)?.use { stream -> + return copyComicInfoFile(stream, folder) } - is Format.Rar -> { - JunrarArchive(chapter.openInputStream()).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 @@ -254,7 +234,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}" @@ -270,7 +250,7 @@ actual class LocalSource( val format = Format.valueOf(chapterFile) if (format is Format.Epub) { - EpubFile(format.file.openReadOnlyChannel(context)).use { epub -> + EpubFile(format.file.archiveReader(context)).use { epub -> epub.fillMetadata(manga, this) } } @@ -328,31 +308,22 @@ actual class LocalSource( entry?.let { coverManager.update(manga, it.openInputStream()) } } - is Format.Zip -> { - format.file.openReadOnlyChannel(context).toZipFile().use { zip -> - val entry = zip.entries.toList() - .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.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, zip.getInputStream(it)) } - } - } - is Format.Rar -> { - JunrarArchive(format.file.openInputStream()).use { archive -> - val entry = archive.fileHeaders - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } - - entry?.let { coverManager.update(manga, archive.getInputStream(it)) } + entry?.let { coverManager.update(manga, reader.getInputStream(it.name)!!) } } } is Format.Epub -> { - EpubFile(format.file.openReadOnlyChannel(context)).use { epub -> - 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)!!) } } } } 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() } } }