mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-23 03:28:55 +02:00
Move archive related code to :core:archive
This commit is contained in:
2
core/archive/src/main/AndroidManifest.xml
Normal file
2
core/archive/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
@@ -0,0 +1,6 @@
|
||||
package mihon.core.archive
|
||||
|
||||
class ArchiveEntry(
|
||||
val name: String,
|
||||
val isFile: Boolean,
|
||||
)
|
@@ -0,0 +1,63 @@
|
||||
package mihon.core.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
|
||||
|
||||
internal 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)
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package mihon.core.archive
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import me.zhanghai.android.libarchive.ArchiveException
|
||||
import java.io.Closeable
|
||||
import java.io.InputStream
|
||||
|
||||
class ArchiveReader(pfd: ParcelFileDescriptor) : Closeable {
|
||||
private val size = pfd.statSize
|
||||
private val address = Os.mmap(0, size, OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, pfd.fileDescriptor, 0)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
135
core/archive/src/main/kotlin/mihon/core/archive/EpubReader.kt
Normal file
135
core/archive/src/main/kotlin/mihon/core/archive/EpubReader.kt
Normal file
@@ -0,0 +1,135 @@
|
||||
package mihon.core.archive
|
||||
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Wrapper over ArchiveReader to load files in epub format.
|
||||
*/
|
||||
class EpubReader(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 {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package mihon.core.archive
|
||||
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.hippo.unifile.UniFile
|
||||
|
||||
internal fun UniFile.openFileDescriptor(context: Context, mode: String): ParcelFileDescriptor =
|
||||
context.contentResolver.openFileDescriptor(uri, mode) ?: error("Failed to open file descriptor: ${filePath ?: uri.toString()}")
|
||||
|
||||
fun UniFile.archiveReader(context: Context) = openFileDescriptor(context, "r").use { ArchiveReader(it) }
|
||||
|
||||
fun UniFile.epubReader(context: Context) = EpubReader(archiveReader(context))
|
73
core/archive/src/main/kotlin/mihon/core/archive/ZipWriter.kt
Normal file
73
core/archive/src/main/kotlin/mihon/core/archive/ZipWriter.kt
Normal file
@@ -0,0 +1,73 @@
|
||||
package mihon.core.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 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 = st_atime.toTimespec()
|
||||
stMtim = st_mtime.toTimespec()
|
||||
stCtim = st_ctime.toTimespec()
|
||||
stIno = st_ino
|
||||
}
|
||||
|
||||
private fun Long.toTimespec() = ArchiveEntry.StructTimespec().also { it.tvSec = this }
|
Reference in New Issue
Block a user