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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user