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