Move archive related code to :core:archive

This commit is contained in:
AntsyLich
2024-09-05 16:00:46 +06:00
parent 70c1a842b2
commit bd7b354198
20 changed files with 70 additions and 52 deletions

View File

@ -1,136 +0,0 @@
package eu.kanade.tachiyomi.util.storage
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
/**
* Wrapper over ZipFile to load files in epub format.
*/
class EpubFile(private val reader: ArchiveReader) : Closeable by reader {
/**
* Path separator used by this epub.
*/
private val pathSeparator = getPathSeparator()
/**
* Returns an input stream for reading the contents of the specified zip file entry.
*/
fun getInputStream(entryName: String): InputStream? {
return reader.getInputStream(entryName)
}
/**
* Returns the path of all the images found in the epub file.
*/
fun getImagesFromPages(): List<String> {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val pages = getPagesFromDocument(doc)
return getImagesFromPages(pages, ref)
}
/**
* Returns the path to the package document.
*/
fun getPackageHref(): String {
val meta = getInputStream(resolveZipPath("META-INF", "container.xml"))
if (meta != null) {
val metaDoc = meta.use { Jsoup.parse(it, null, "") }
val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path")
if (path != null) {
return path
}
}
return resolveZipPath("OEBPS", "content.opf")
}
/**
* Returns the package document where all the files are listed.
*/
fun getPackageDocument(ref: String): Document {
return getInputStream(ref)!!.use { Jsoup.parse(it, null, "") }
}
/**
* Returns all the pages from the epub.
*/
private fun getPagesFromDocument(document: Document): List<String> {
val pages = document.select("manifest > item")
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
.associateBy { it.attr("id") }
val spine = document.select("spine > itemref").map { it.attr("idref") }
return spine.mapNotNull { pages[it] }.map { it.attr("href") }
}
/**
* Returns all the images contained in every page from the epub.
*/
private fun getImagesFromPages(pages: List<String>, packageHref: String): List<String> {
val result = mutableListOf<String>()
val basePath = getParentDirectory(packageHref)
pages.forEach { page ->
val entryPath = resolveZipPath(basePath, page)
val document = getInputStream(entryPath)!!.use { Jsoup.parse(it, null, "") }
val imageBasePath = getParentDirectory(entryPath)
document.allElements.forEach {
when (it.tagName()) {
"img" -> result.add(resolveZipPath(imageBasePath, it.attr("src")))
"image" -> result.add(resolveZipPath(imageBasePath, it.attr("xlink:href")))
}
}
}
return result
}
/**
* Returns the path separator used by the epub file.
*/
private fun getPathSeparator(): String {
val meta = getInputStream("META-INF\\container.xml")
return if (meta != null) {
meta.close()
"\\"
} else {
"/"
}
}
/**
* Resolves a zip path from base and relative components and a path separator.
*/
private fun resolveZipPath(basePath: String, relativePath: String): String {
if (relativePath.startsWith(pathSeparator)) {
// Path is absolute, so return as-is.
return relativePath
}
var fixedBasePath = basePath.replace(pathSeparator, File.separator)
if (!fixedBasePath.startsWith(File.separator)) {
fixedBasePath = "${File.separator}$fixedBasePath"
}
val fixedRelativePath = relativePath.replace(pathSeparator, File.separator)
val resolvedPath = File(fixedBasePath, fixedRelativePath).canonicalPath
return resolvedPath.replace(File.separator, pathSeparator).substring(1)
}
/**
* Gets the parent directory of a path.
*/
private fun getParentDirectory(path: String): String {
val separatorIndex = path.lastIndexOf(pathSeparator)
return if (separatorIndex >= 0) {
path.substring(0, separatorIndex)
} else {
""
}
}
}

View File

@ -1,6 +0,0 @@
package mihon.core.common.archive
class ArchiveEntry(
val name: String,
val isFile: Boolean,
)

View File

@ -1,63 +0,0 @@
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
import kotlin.concurrent.Volatile
class ArchiveInputStream(buffer: Long, size: Long) : InputStream() {
private val lock = Any()
@Volatile
private var isClosed = false
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() {
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
ArchiveEntry(name, isFile)
}
}

View File

@ -1,42 +0,0 @@
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 <T> useEntries(block: (Sequence<ArchiveEntry>) -> 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) }

View File

@ -1,74 +0,0 @@
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 }

View File

@ -1,7 +1,5 @@
package tachiyomi.core.common.storage
import android.content.Context
import android.os.ParcelFileDescriptor
import com.hippo.unifile.UniFile
val UniFile.extension: String?
@ -12,6 +10,3 @@ val UniFile.nameWithoutExtension: String?
val UniFile.displayablePath: String
get() = filePath ?: uri.toString()
fun UniFile.openFileDescriptor(context: Context, mode: String): ParcelFileDescriptor =
context.contentResolver.openFileDescriptor(uri, mode) ?: error("Failed to open file descriptor: $displayablePath")