Libarchive refactor (#1249)

* Refactor archive support with libarchive

* Refactor archive support with libarchive

* Revert string resource changs

* Only mark archive formats as supported

Comic book archives should not be compressed.

* Fixup

* Remove epub from archive format list

* Move to mihon package

* Format

* Cleanup

Co-authored-by: Shamicen <84282253+Shamicen@users.noreply.github.com>
(cherry picked from commit 239c38982c4fd55d4d86b37fd9c3c51c3b47d098)

* handle incorrect passwords

* lint

* fixed broken encryption detection + small tweaks

* Add safeguard to prevent ArchiveInputStream from being closed twice (#967)

* fix: Add safeguard to prevent ArchiveInputStream from being closed twice

* detekt

* lint: Make detekt happy

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>

(cherry picked from commit e620665dda9eb5cc39f09e6087ea4f60a3cbe150)

* fixed ArchiveReaderMode CACHE_TO_DISK

* Added some missing SY --> comments

---------

Co-authored-by: FooIbar <118464521+fooibar@users.noreply.github.com>
Co-authored-by: Ahmad Ansori Palembani <46041660+null2264@users.noreply.github.com>
This commit is contained in:
Shamicen
2024-08-18 02:25:25 +02:00
committed by GitHub
parent 71f2daf8f3
commit 95c834581b
27 changed files with 477 additions and 749 deletions
+1 -2
View File
@@ -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 <--
@@ -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),
}
@@ -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
@@ -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 {
"/"
@@ -0,0 +1,7 @@
package mihon.core.common.archive
class ArchiveEntry(
val name: String,
val isFile: Boolean,
val isEncrypted: Boolean,
)
@@ -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 <--
)
}
}
@@ -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 <T> useEntries(block: (Sequence<ArchiveEntry>) -> 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) }
@@ -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 }
@@ -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<UniFile>, 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<LocalFileHeader?> = 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")