Improvements and fixes

This commit is contained in:
Syer10
2026-07-12 23:43:11 -04:00
parent c0586e7f36
commit a830782b4b

View File

@@ -17,9 +17,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import net.dongliu.apk.parser.ApkFile import net.dongliu.apk.parser.ApkFile
import net.dongliu.apk.parser.bean.Icon import net.dongliu.apk.parser.bean.Icon
import okhttp3.CacheControl import okhttp3.CacheControl
import okio.buffer
import okio.sink
import okio.source
import org.apache.commons.compress.archivers.zip.ZipFile import org.apache.commons.compress.archivers.zip.ZipFile
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.deleteWhere import org.jetbrains.exposed.v1.jdbc.deleteWhere
@@ -61,6 +58,8 @@ import kotlin.io.path.Path
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.io.path.copyTo import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories import kotlin.io.path.createDirectories
import kotlin.io.path.createParentDirectories
import kotlin.io.path.deleteExisting
import kotlin.io.path.deleteIfExists import kotlin.io.path.deleteIfExists
import kotlin.io.path.deleteRecursively import kotlin.io.path.deleteRecursively
import kotlin.io.path.div import kotlin.io.path.div
@@ -92,7 +91,7 @@ object Extension {
jarUrl != null -> { jarUrl != null -> {
installJAR { installJAR {
val jarName = Uri.parse(jarUrl).lastPathSegment!! val jarName = Uri.parse(jarUrl).lastPathSegment!!
val jarSavePath = "${applicationDirs.extensionsRoot}/$jarName" val jarSavePath = Path(applicationDirs.tempRoot) / "extensions" / jarName
// download jar file // download jar file
downloadExtension(jarUrl, jarSavePath) downloadExtension(jarUrl, jarSavePath)
@@ -103,7 +102,7 @@ object Extension {
apkUrl != null -> { apkUrl != null -> {
installAPK { installAPK {
val apkName = Uri.parse(apkUrl).lastPathSegment!! val apkName = Uri.parse(apkUrl).lastPathSegment!!
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName" val apkSavePath = Path(applicationDirs.tempRoot) / "extensions" / apkName
// download apk file // download apk file
downloadExtension(apkUrl, apkSavePath) downloadExtension(apkUrl, apkSavePath)
@@ -122,20 +121,20 @@ object Extension {
extensionName: String, extensionName: String,
): String { ): String {
val copyToExtensionsRoot = { val copyToExtensionsRoot = {
val rootPath = Path(applicationDirs.extensionsRoot) val rootPath = Path(applicationDirs.tempRoot) / "extensions"
val downloadedFile = rootPath.resolve(extensionName).normalize() val downloadedFile = rootPath.resolve(extensionName).normalize()
check(downloadedFile.startsWith(rootPath) && downloadedFile.parent == rootPath) { check(downloadedFile.startsWith(rootPath) && downloadedFile.parent == rootPath) {
"File '$extensionName' is not a valid extension file" "File '$extensionName' is not a valid extension file"
} }
logger.debug { "Saving jar at $extensionName" } logger.debug { "Saving jar at $extensionName" }
// download jar file // download jar file
downloadedFile.outputStream().sink().buffer().use { sink -> downloadedFile.createParentDirectories()
inputStream.source().use { source -> downloadedFile.outputStream().buffered().use { out ->
sink.writeAll(source) inputStream.use {
sink.flush() it.copyTo(out)
} }
} }
downloadedFile.absolutePathString() downloadedFile
} }
return when { return when {
@@ -147,9 +146,9 @@ object Extension {
suspend fun installAPK( suspend fun installAPK(
forceReinstall: Boolean = false, forceReinstall: Boolean = false,
fetcher: suspend () -> String, fetcher: suspend () -> Path,
): String { ): String {
val apkFile = Path(fetcher()) val apkFile = fetcher()
val packageInfo = getPackageInfo(apkFile) val packageInfo = getPackageInfo(apkFile)
val pkgName = packageInfo.packageName val pkgName = packageInfo.packageName
@@ -161,14 +160,16 @@ object Extension {
ExtensionTable.select(ExtensionTable.isInstalled).where { ExtensionTable.pkgName eq pkgName }.firstOrNull() ExtensionTable.select(ExtensionTable.isInstalled).where { ExtensionTable.pkgName eq pkgName }.firstOrNull()
}?.get(ExtensionTable.isInstalled) ?: false }?.get(ExtensionTable.isInstalled) ?: false
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/${apkFile.nameWithoutExtension}" val extensionsRoot = Path(applicationDirs.extensionsRoot)
val jarFile = Path("$dirPathWithoutType.jar") val jarFile = extensionsRoot / (apkFile.nameWithoutExtension + ".jar")
if (isInstalled && forceReinstall) { if (isInstalled && forceReinstall) {
uninstallExtension(pkgName) uninstallExtension(pkgName)
} else if (isInstalled) {
apkFile.deleteExisting()
return pkgName
} }
if (!isInstalled || forceReinstall) {
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) { if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
throw Exception("This apk is not a Tachiyomi extension") throw Exception("This apk is not a Tachiyomi extension")
} }
@@ -224,7 +225,7 @@ object Extension {
extractAndCacheApkIcon(apkFile, pkgName) extractAndCacheApkIcon(apkFile, pkgName)
// clean up // clean up
apkFile.deleteIfExists() apkFile.deleteExisting()
try { try {
val extensionName = val extensionName =
@@ -261,43 +262,62 @@ object Extension {
} }
throw e throw e
} }
}
return pkgName return pkgName
} }
suspend fun installJAR( suspend fun installJAR(
forceReinstall: Boolean = false, forceReinstall: Boolean = false,
fetcher: suspend () -> String, fetcher: suspend () -> Path,
): String { ): String {
val jarFile = Path(fetcher()) val tmpJarFile = fetcher()
val jarFile = Path(applicationDirs.extensionsRoot) / tmpJarFile.name
val tmpIcon = jarFile.parent / (jarFile.nameWithoutExtension + ".icon")
val jarZip = val jarZip =
ZipFile ZipFile
.builder() .builder()
.setPath(jarFile) .setPath(tmpJarFile)
.get() .get()
return jarZip.use { jarZip -> val manifest: AndroidManifestParser.AndroidManifest
val manifest = jarZip.use { jarZip ->
manifest =
jarZip jarZip
.getInputStream(jarZip.getEntry("AndroidManifest.xml")) .getInputStream(jarZip.getEntry("AndroidManifest.xml"))
.use { .use {
AndroidManifestParser.parse(it) AndroidManifestParser.parse(it)
} }
val pkgName = manifest.packageName try {
ResourceArscIconParser.extractIcon(jarZip).use {
tmpIcon.outputStream().use { out ->
it.copyTo(out)
}
}
} catch (e: Exception) {
logger.warn(e) { "Failed to extract icon from JAR ${manifest.packageName}" }
}
}
val pkgName: String = manifest.packageName
// check if we don't have the extension already installed // check if we don't have the extension already installed
// if it's installed and we want to update, it first has to be uninstalled // if it's installed and we want to update, it first has to be uninstalled
val isInstalled = val isInstalled =
transaction { transaction {
ExtensionTable.select(ExtensionTable.isInstalled).where { ExtensionTable.pkgName eq pkgName }.firstOrNull() ExtensionTable
.select(ExtensionTable.isInstalled)
.where { ExtensionTable.pkgName eq pkgName }
.firstOrNull()
}?.get(ExtensionTable.isInstalled) ?: false }?.get(ExtensionTable.isInstalled) ?: false
if (isInstalled && forceReinstall) { if (isInstalled && forceReinstall) {
uninstallExtension(pkgName) uninstallExtension(pkgName)
} else if (isInstalled) {
tmpJarFile.deleteExisting()
tmpIcon.deleteIfExists()
return pkgName
} }
if (!isInstalled || forceReinstall) {
if (!manifest.usesFeatures.any { it.name == EXTENSION_FEATURE }) { if (!manifest.usesFeatures.any { it.name == EXTENSION_FEATURE }) {
throw Exception("This apk is not a Tachiyomi extension") throw Exception("This apk is not a Tachiyomi extension")
} }
@@ -311,6 +331,18 @@ object Extension {
) )
} }
if (tmpIcon.exists()) {
try {
cacheIcon(pkgName, tmpIcon.inputStream())
} catch (e: Exception) {
logger.warn(e) { "Failed to copy icon from JAR ${manifest.packageName}" }
}
}
tmpIcon.deleteIfExists()
tmpJarFile.copyTo(jarFile)
tmpJarFile.deleteExisting()
// TODO: allow trusting keys // TODO: allow trusting keys
// val signatureHash = getSignatureHash(packageInfo) // val signatureHash = getSignatureHash(packageInfo)
// //
@@ -348,8 +380,6 @@ object Extension {
logger.debug { "Main class for extension is $className" } logger.debug { "Main class for extension is $className" }
extractAndCacheJarIcon(jarZip, pkgName)
try { try {
val extensionName = val extensionName =
manifest.application.metaData.getString(METADATA_NAME) manifest.application.metaData.getString(METADATA_NAME)
@@ -360,7 +390,7 @@ object Extension {
manifest.application.metaData manifest.application.metaData
.getString(METADATA_EXTENSION_LIB) .getString(METADATA_EXTENSION_LIB)
.takeUnless { it == "0" } .takeUnless { it == "0" }
?: manifest.versionName.substringBeforeLast('.') ?: manifest.versionName!!.substringBeforeLast('.')
setupJar( setupJar(
jarFile, jarFile,
@@ -369,7 +399,7 @@ object Extension {
extensionLibVersion, extensionLibVersion,
jarFile.name.removeSuffix(".jar") + ".apk", jarFile.name.removeSuffix(".jar") + ".apk",
pkgName, pkgName,
manifest.versionName, manifest.versionName!!,
manifest.versionCode!!, manifest.versionCode!!,
contentWarning, contentWarning,
) )
@@ -384,9 +414,7 @@ object Extension {
} }
throw e throw e
} }
} return pkgName
return@use pkgName
}
} }
private fun setupJar( private fun setupJar(
@@ -499,7 +527,7 @@ object Extension {
pkgName: String, pkgName: String,
inputStream: InputStream, inputStream: InputStream,
) { ) {
val iconCacheDir = Path("${applicationDirs.extensionsRoot}/icon") val iconCacheDir = Path(applicationDirs.extensionsRoot) / "icon"
iconCacheDir.createDirectories() iconCacheDir.createDirectories()
clearCachedImage(iconCacheDir.absolutePathString(), pkgName) clearCachedImage(iconCacheDir.absolutePathString(), pkgName)
saveImage("$iconCacheDir/$pkgName", inputStream, null) saveImage("$iconCacheDir/$pkgName", inputStream, null)
@@ -517,7 +545,7 @@ object Extension {
while (zipEntry != null) { while (zipEntry != null) {
if (zipEntry.name.startsWith("assets/") && !zipEntry.isDirectory) { if (zipEntry.name.startsWith("assets/") && !zipEntry.isDirectory) {
val assetFile = assetsFolder / zipEntry.name val assetFile = assetsFolder / zipEntry.name
assetFile.parent.createDirectories() assetFile.createParentDirectories()
assetFile.outputStream().use { outputStream -> assetFile.outputStream().use { outputStream ->
zipInputStream.copyTo(outputStream) zipInputStream.copyTo(outputStream)
} }
@@ -551,7 +579,7 @@ object Extension {
jarFile.deleteIfExists() jarFile.deleteIfExists()
tempJarFile.copyTo(jarFile) tempJarFile.copyTo(jarFile)
tempJarFile.deleteIfExists() tempJarFile.deleteExisting()
assetsFolder.deleteRecursively() assetsFolder.deleteRecursively()
} }
@@ -560,7 +588,7 @@ object Extension {
private suspend fun downloadExtension( private suspend fun downloadExtension(
url: String, url: String,
savePath: String, savePath: Path,
) { ) {
val response = val response =
network.client network.client
@@ -568,9 +596,9 @@ object Extension {
GET(url, cache = CacheControl.FORCE_NETWORK), GET(url, cache = CacheControl.FORCE_NETWORK),
).await() ).await()
val downloadedFile = Path(savePath) savePath.createParentDirectories()
response.body.byteStream().use { response.body.byteStream().use {
downloadedFile.outputStream().buffered().use { out -> savePath.outputStream().buffered().use { out ->
it.copyTo(out) it.copyTo(out)
} }
} }
@@ -612,7 +640,7 @@ object Extension {
// clear all loaded sources // clear all loaded sources
sources.forEach { GetSource.unregisterSource(it) } sources.forEach { GetSource.unregisterSource(it) }
jarPath.deleteIfExists() jarPath.deleteExisting()
} }
} }