mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-18 10:13:33 -05:00
Install JARs directly
This commit is contained in:
@@ -110,6 +110,7 @@ dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex
|
|||||||
# APK
|
# APK
|
||||||
apk-parser = "net.dongliu:apk-parser:2.6.10"
|
apk-parser = "net.dongliu:apk-parser:2.6.10"
|
||||||
apksig = "com.android.tools.build:apksig:9.2.1"
|
apksig = "com.android.tools.build:apksig:9.2.1"
|
||||||
|
axml = "com.github.Aliucord:axml:ed26565eb0"
|
||||||
|
|
||||||
# Xml
|
# Xml
|
||||||
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
||||||
@@ -215,6 +216,7 @@ shared = [
|
|||||||
"dex2jar-translator",
|
"dex2jar-translator",
|
||||||
"dex2jar-tools",
|
"dex2jar-tools",
|
||||||
"apk-parser",
|
"apk-parser",
|
||||||
|
"axml",
|
||||||
"jackson-annotations",
|
"jackson-annotations",
|
||||||
"jcef",
|
"jcef",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ class ExtensionQuery {
|
|||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val pkgName: String? = null,
|
val pkgName: String? = null,
|
||||||
val apkUrl: String? = null,
|
val apkUrl: String? = null,
|
||||||
|
val jarUrl: String? = null,
|
||||||
val extensionLib: String? = null,
|
val extensionLib: String? = null,
|
||||||
val versionName: String? = null,
|
val versionName: String? = null,
|
||||||
val versionCode: Int? = null,
|
val versionCode: Int? = null,
|
||||||
@@ -122,6 +123,7 @@ class ExtensionQuery {
|
|||||||
opAnd.eq(apkName, ExtensionTable.apkName)
|
opAnd.eq(apkName, ExtensionTable.apkName)
|
||||||
opAnd.eq(iconUrl, ExtensionTable.iconUrl)
|
opAnd.eq(iconUrl, ExtensionTable.iconUrl)
|
||||||
opAnd.eq(apkUrl, ExtensionTable.apkUrl)
|
opAnd.eq(apkUrl, ExtensionTable.apkUrl)
|
||||||
|
opAnd.eq(jarUrl, ExtensionTable.jarUrl)
|
||||||
opAnd.eq(name, ExtensionTable.name)
|
opAnd.eq(name, ExtensionTable.name)
|
||||||
opAnd.eq(extensionLib, ExtensionTable.extensionLib)
|
opAnd.eq(extensionLib, ExtensionTable.extensionLib)
|
||||||
opAnd.eq(versionName, ExtensionTable.versionName)
|
opAnd.eq(versionName, ExtensionTable.versionName)
|
||||||
@@ -150,6 +152,7 @@ class ExtensionQuery {
|
|||||||
val name: StringFilter? = null,
|
val name: StringFilter? = null,
|
||||||
val pkgName: StringFilter? = null,
|
val pkgName: StringFilter? = null,
|
||||||
val apkUrl: StringFilter? = null,
|
val apkUrl: StringFilter? = null,
|
||||||
|
val jarUrl: StringFilter? = null,
|
||||||
val versionName: StringFilter? = null,
|
val versionName: StringFilter? = null,
|
||||||
val extensionLib: StringFilter? = null,
|
val extensionLib: StringFilter? = null,
|
||||||
@GraphQLDeprecated("", ReplaceWith("versionCodeLong"))
|
@GraphQLDeprecated("", ReplaceWith("versionCodeLong"))
|
||||||
@@ -175,6 +178,7 @@ class ExtensionQuery {
|
|||||||
andFilterWithCompareString(ExtensionTable.name, name),
|
andFilterWithCompareString(ExtensionTable.name, name),
|
||||||
andFilterWithCompareString(ExtensionTable.pkgName, pkgName),
|
andFilterWithCompareString(ExtensionTable.pkgName, pkgName),
|
||||||
andFilterWithCompareString(ExtensionTable.apkUrl, apkUrl),
|
andFilterWithCompareString(ExtensionTable.apkUrl, apkUrl),
|
||||||
|
andFilterWithCompareString(ExtensionTable.jarUrl, jarUrl),
|
||||||
andFilterWithCompareString(ExtensionTable.extensionLib, extensionLib),
|
andFilterWithCompareString(ExtensionTable.extensionLib, extensionLib),
|
||||||
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
||||||
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class ExtensionType(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val pkgName: String,
|
val pkgName: String,
|
||||||
val apkUrl: String?,
|
val apkUrl: String?,
|
||||||
|
val jarUrl: String?,
|
||||||
val extensionLib: String?,
|
val extensionLib: String?,
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
@GraphQLDeprecated(
|
@GraphQLDeprecated(
|
||||||
@@ -56,6 +57,7 @@ class ExtensionType(
|
|||||||
name = row[ExtensionTable.name],
|
name = row[ExtensionTable.name],
|
||||||
pkgName = row[ExtensionTable.pkgName],
|
pkgName = row[ExtensionTable.pkgName],
|
||||||
apkUrl = row[ExtensionTable.apkUrl],
|
apkUrl = row[ExtensionTable.apkUrl],
|
||||||
|
jarUrl = row[ExtensionTable.jarUrl],
|
||||||
extensionLib = row[ExtensionTable.extensionLib],
|
extensionLib = row[ExtensionTable.extensionLib],
|
||||||
versionName = row[ExtensionTable.versionName],
|
versionName = row[ExtensionTable.versionName],
|
||||||
versionCode = row[ExtensionTable.versionCode].toInt(),
|
versionCode = row[ExtensionTable.versionCode].toInt(),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import okhttp3.CacheControl
|
|||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import okio.source
|
import okio.source
|
||||||
|
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
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
@@ -27,6 +28,7 @@ import org.jetbrains.exposed.v1.jdbc.select
|
|||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.AndroidManifestParser
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools
|
import suwayomi.tachidesk.manga.impl.util.PackageTools
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
|
||||||
@@ -39,6 +41,7 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_SOURCE_CLASS
|
|||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.dex2jar
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.dex2jar
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
|
||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.ResourceArscIconParser
|
||||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||||
@@ -48,15 +51,30 @@ import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
|||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.nio.file.Path
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
import kotlin.collections.any
|
||||||
|
import kotlin.collections.orEmpty
|
||||||
|
import kotlin.io.inputStream
|
||||||
|
import kotlin.io.path.ExperimentalPathApi
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.copyTo
|
||||||
|
import kotlin.io.path.createDirectories
|
||||||
|
import kotlin.io.path.deleteIfExists
|
||||||
|
import kotlin.io.path.deleteRecursively
|
||||||
|
import kotlin.io.path.div
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
import kotlin.io.path.isRegularFile
|
||||||
|
import kotlin.io.path.name
|
||||||
|
import kotlin.io.path.nameWithoutExtension
|
||||||
import kotlin.io.path.outputStream
|
import kotlin.io.path.outputStream
|
||||||
|
import kotlin.io.path.relativeTo
|
||||||
|
import kotlin.io.path.walk
|
||||||
|
|
||||||
object Extension {
|
object Extension {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
@@ -64,37 +82,52 @@ object Extension {
|
|||||||
|
|
||||||
suspend fun installExtension(pkgName: String): Int {
|
suspend fun installExtension(pkgName: String): Int {
|
||||||
logger.debug { "Installing $pkgName" }
|
logger.debug { "Installing $pkgName" }
|
||||||
val apkUrl =
|
val extension =
|
||||||
transaction {
|
transaction {
|
||||||
ExtensionTable
|
ExtensionTable
|
||||||
.select(ExtensionTable.apkUrl)
|
.select(ExtensionTable.apkUrl, ExtensionTable.jarUrl)
|
||||||
.where { ExtensionTable.pkgName eq pkgName }
|
.where { ExtensionTable.pkgName eq pkgName }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.get(ExtensionTable.apkUrl)
|
} ?: throw NullPointerException("Could not find extension for $pkgName")
|
||||||
} ?: throw NullPointerException("Could not find extension $pkgName")
|
val jarUrl = extension[ExtensionTable.jarUrl]
|
||||||
|
val apkUrl = extension[ExtensionTable.apkUrl]
|
||||||
|
return when {
|
||||||
|
jarUrl != null -> {
|
||||||
|
installJAR {
|
||||||
|
val jarName = Uri.parse(jarUrl).lastPathSegment!!
|
||||||
|
val jarSavePath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||||
|
// download jar file
|
||||||
|
downloadExtension(jarUrl, jarSavePath)
|
||||||
|
|
||||||
return installAPK {
|
jarSavePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apkUrl != null -> {
|
||||||
|
installAPK {
|
||||||
val apkName = Uri.parse(apkUrl).lastPathSegment!!
|
val apkName = Uri.parse(apkUrl).lastPathSegment!!
|
||||||
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
||||||
// download apk file
|
// download apk file
|
||||||
downloadAPKFile(apkUrl, apkSavePath)
|
downloadExtension(apkUrl, apkSavePath)
|
||||||
|
|
||||||
apkSavePath
|
apkSavePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else -> throw NullPointerException("Could not find extension url for $pkgName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun installExternalExtension(
|
suspend fun installExternalExtension(
|
||||||
inputStream: InputStream,
|
inputStream: InputStream,
|
||||||
apkName: String,
|
extensionName: String,
|
||||||
): Int =
|
): Int {
|
||||||
installAPK(true) {
|
val copyToExtensionsRoot = {
|
||||||
val rootPath = Path(applicationDirs.extensionsRoot)
|
val rootPath = Path(applicationDirs.extensionsRoot)
|
||||||
val downloadedFile = rootPath.resolve(apkName).normalize()
|
val downloadedFile = rootPath.resolve(extensionName).normalize()
|
||||||
check(downloadedFile.startsWith(rootPath) && downloadedFile.parent == rootPath) {
|
check(downloadedFile.startsWith(rootPath) && downloadedFile.parent == rootPath) {
|
||||||
"File '$apkName' is not a valid extension file"
|
"File '$extensionName' is not a valid extension file"
|
||||||
}
|
}
|
||||||
logger.debug { "Saving apk at $apkName" }
|
logger.debug { "Saving jar at $extensionName" }
|
||||||
// download apk file
|
// download jar file
|
||||||
downloadedFile.outputStream().sink().buffer().use { sink ->
|
downloadedFile.outputStream().sink().buffer().use { sink ->
|
||||||
inputStream.source().use { source ->
|
inputStream.source().use { source ->
|
||||||
sink.writeAll(source)
|
sink.writeAll(source)
|
||||||
@@ -104,26 +137,30 @@ object Extension {
|
|||||||
downloadedFile.absolutePathString()
|
downloadedFile.absolutePathString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return when {
|
||||||
|
extensionName.endsWith(".jar") -> installJAR(true, copyToExtensionsRoot)
|
||||||
|
extensionName.endsWith(".apk") -> installAPK(true, copyToExtensionsRoot)
|
||||||
|
else -> throw NullPointerException("Could not find extension type for $extensionName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun installAPK(
|
suspend fun installAPK(
|
||||||
forceReinstall: Boolean = false,
|
forceReinstall: Boolean = false,
|
||||||
fetcher: suspend () -> String,
|
fetcher: suspend () -> String,
|
||||||
): Int {
|
): Int {
|
||||||
val apkFilePath = fetcher()
|
val apkFile = Path(fetcher())
|
||||||
val apkName = File(apkFilePath).name
|
|
||||||
|
|
||||||
// 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.selectAll().where { ExtensionTable.apkName eq apkName }.firstOrNull()
|
ExtensionTable.selectAll().where { ExtensionTable.apkName eq apkFile.name }.firstOrNull()
|
||||||
}?.get(ExtensionTable.isInstalled) ?: false
|
}?.get(ExtensionTable.isInstalled) ?: false
|
||||||
|
|
||||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/${apkFile.nameWithoutExtension}"
|
||||||
|
val jarFile = Path("$dirPathWithoutType.jar")
|
||||||
|
|
||||||
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
val packageInfo = getPackageInfo(apkFile)
|
||||||
val jarFilePath = "$dirPathWithoutType.jar"
|
|
||||||
|
|
||||||
val packageInfo = getPackageInfo(apkFilePath)
|
|
||||||
val pkgName = packageInfo.packageName
|
val pkgName = packageInfo.packageName
|
||||||
if (isInstalled && forceReinstall) {
|
if (isInstalled && forceReinstall) {
|
||||||
uninstallExtension(pkgName)
|
uninstallExtension(pkgName)
|
||||||
@@ -145,7 +182,7 @@ object Extension {
|
|||||||
|
|
||||||
// TODO: allow trusting keys
|
// TODO: allow trusting keys
|
||||||
// val signatureHash = getSignatureHash(packageInfo)
|
// val signatureHash = getSignatureHash(packageInfo)
|
||||||
|
//
|
||||||
// if (signatureHash == null) {
|
// if (signatureHash == null) {
|
||||||
// throw Exception("Package $pkgName isn't signed")
|
// throw Exception("Package $pkgName isn't signed")
|
||||||
// } else if (signatureHash !in trustedSignatures) {
|
// } else if (signatureHash !in trustedSignatures) {
|
||||||
@@ -180,16 +217,187 @@ object Extension {
|
|||||||
|
|
||||||
logger.debug { "Main class for extension is $className" }
|
logger.debug { "Main class for extension is $className" }
|
||||||
|
|
||||||
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
dex2jar(apkFile, jarFile)
|
||||||
extractAssetsFromApk(apkFilePath, jarFilePath)
|
extractAssetsFromApk(apkFile, jarFile)
|
||||||
extractAndCacheApkIcon(apkFilePath, packageInfo.packageName)
|
extractAndCacheApkIcon(apkFile, packageInfo.packageName)
|
||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
File(apkFilePath).delete()
|
apkFile.deleteIfExists()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
val extensionName =
|
||||||
|
packageInfo.applicationInfo.metaData.getString(METADATA_NAME)
|
||||||
|
?: packageInfo.applicationInfo.nonLocalizedLabel
|
||||||
|
.toString()
|
||||||
|
.substringAfter("Tachiyomi: ")
|
||||||
|
|
||||||
|
val extensionLibVersion =
|
||||||
|
packageInfo.applicationInfo.metaData
|
||||||
|
.getString(METADATA_EXTENSION_LIB)
|
||||||
|
.takeUnless { it == "0" }
|
||||||
|
?: packageInfo.versionName.substringBeforeLast('.')
|
||||||
|
|
||||||
|
setupJar(
|
||||||
|
jarFile,
|
||||||
|
className,
|
||||||
|
extensionName,
|
||||||
|
extensionLibVersion,
|
||||||
|
apkFile.name,
|
||||||
|
packageInfo.packageName,
|
||||||
|
packageInfo.versionName,
|
||||||
|
packageInfo.versionCode,
|
||||||
|
contentWarning
|
||||||
|
)
|
||||||
|
|
||||||
|
return 201 // we installed successfully
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// free up the file descriptor if exists
|
||||||
|
PackageTools.jarLoaderMap.remove(jarFile.absolutePathString())?.close()
|
||||||
|
jarFile.deleteIfExists()
|
||||||
|
|
||||||
|
try {
|
||||||
|
uninstallExtension(pkgName)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 302 // extension was already installed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun installJAR(
|
||||||
|
forceReinstall: Boolean = false,
|
||||||
|
fetcher: suspend () -> String,
|
||||||
|
): Int {
|
||||||
|
val jarFile = Path(fetcher())
|
||||||
|
|
||||||
|
// 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
|
||||||
|
val isInstalled =
|
||||||
|
transaction {
|
||||||
|
ExtensionTable.selectAll().where { ExtensionTable.apkName eq (jarFile.nameWithoutExtension + ".apk") }.firstOrNull()
|
||||||
|
}?.get(ExtensionTable.isInstalled) ?: false
|
||||||
|
|
||||||
|
val jarZip = ZipFile.builder()
|
||||||
|
.setPath(jarFile)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
return jarZip.use { jarZip ->
|
||||||
|
val manifest = jarZip.getInputStream(jarZip.getEntry("AndroidManifest.xml"))
|
||||||
|
.use {
|
||||||
|
AndroidManifestParser.parse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pkgName = manifest.packageName
|
||||||
|
if (isInstalled && forceReinstall) {
|
||||||
|
uninstallExtension(pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInstalled || forceReinstall) {
|
||||||
|
if (!manifest.usesFeatures.any { it.name == EXTENSION_FEATURE }) {
|
||||||
|
throw Exception("This apk is not a Tachiyomi extension")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate lib version
|
||||||
|
val libVersion = manifest.versionName!!.substringBeforeLast('.').toDouble()
|
||||||
|
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||||
|
throw Exception(
|
||||||
|
"Lib version is $libVersion, while only versions " +
|
||||||
|
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: allow trusting keys
|
||||||
|
// val signatureHash = getSignatureHash(packageInfo)
|
||||||
|
//
|
||||||
|
// if (signatureHash == null) {
|
||||||
|
// throw Exception("Package $pkgName isn't signed")
|
||||||
|
// } else if (signatureHash !in trustedSignatures) {
|
||||||
|
// throw Exception("This apk is not a signed with the official tachiyomi signature")
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun List<AndroidManifestParser.MetaData>.getString(name: String): String? {
|
||||||
|
return this.find { it.name == name }?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentWarning = manifest.application!!.metaData.getString(METADATA_CONTENT_WARNING)
|
||||||
|
?.toIntOrNull()
|
||||||
|
if (contentWarning == null) {
|
||||||
|
contentWarning = manifest.application.metaData.getString(METADATA_NSFW)
|
||||||
|
?.toIntOrNull()
|
||||||
|
?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceClass =
|
||||||
|
manifest.application.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
val className =
|
||||||
|
if (sourceClass.startsWith(".")) {
|
||||||
|
pkgName + sourceClass
|
||||||
|
} else {
|
||||||
|
sourceClass
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug { "Main class for extension is $className" }
|
||||||
|
|
||||||
|
extractAndCacheJarIcon(jarZip, pkgName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val extensionName =
|
||||||
|
manifest.application.metaData.getString(METADATA_NAME)
|
||||||
|
?: manifest.application.label!!
|
||||||
|
.substringAfter("Tachiyomi: ")
|
||||||
|
|
||||||
|
val extensionLibVersion =
|
||||||
|
manifest.application.metaData
|
||||||
|
.getString(METADATA_EXTENSION_LIB)
|
||||||
|
.takeUnless { it == "0" }
|
||||||
|
?: manifest.versionName.substringBeforeLast('.')
|
||||||
|
|
||||||
|
setupJar(
|
||||||
|
jarFile,
|
||||||
|
className,
|
||||||
|
extensionName,
|
||||||
|
extensionLibVersion,
|
||||||
|
jarFile.name.removeSuffix(".jar") + ".apk",
|
||||||
|
manifest.packageName,
|
||||||
|
manifest.versionName,
|
||||||
|
manifest.versionCode!!,
|
||||||
|
contentWarning
|
||||||
|
)
|
||||||
|
|
||||||
|
return@use 201 // we installed successfully
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// free up the file descriptor if exists
|
||||||
|
PackageTools.jarLoaderMap.remove(jarFile.absolutePathString())?.close()
|
||||||
|
jarFile.deleteIfExists()
|
||||||
|
|
||||||
|
try {
|
||||||
|
uninstallExtension(pkgName)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return@use 302 // extension was already installed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupJar(
|
||||||
|
jarFile: Path,
|
||||||
|
className: String,
|
||||||
|
extensionName: String,
|
||||||
|
extensionLibVersion: String,
|
||||||
|
apkName: String,
|
||||||
|
pkgName: String,
|
||||||
|
versionName: String,
|
||||||
|
versionCode: Int,
|
||||||
|
contentWarning: Int,
|
||||||
|
) {
|
||||||
// collect sources from the extension
|
// collect sources from the extension
|
||||||
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
val extensionMainClassInstance = loadExtensionSources(jarFile, className)
|
||||||
val sources: List<Source> =
|
val sources: List<Source> =
|
||||||
when (extensionMainClassInstance) {
|
when (extensionMainClassInstance) {
|
||||||
is Source -> listOf(extensionMainClassInstance)
|
is Source -> listOf(extensionMainClassInstance)
|
||||||
@@ -205,17 +413,6 @@ object Extension {
|
|||||||
else -> "all"
|
else -> "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionName =
|
|
||||||
packageInfo.applicationInfo.metaData.getString(METADATA_NAME)
|
|
||||||
?: packageInfo.applicationInfo.nonLocalizedLabel
|
|
||||||
.toString()
|
|
||||||
.substringAfter("Tachiyomi: ")
|
|
||||||
|
|
||||||
val extensionLibVersion =
|
|
||||||
packageInfo.applicationInfo.metaData
|
|
||||||
.getString(METADATA_EXTENSION_LIB)
|
|
||||||
.takeUnless { it == "0" }
|
|
||||||
?: packageInfo.versionName.substringBeforeLast('.')
|
|
||||||
|
|
||||||
// update extension info
|
// update extension info
|
||||||
transaction {
|
transaction {
|
||||||
@@ -223,9 +420,9 @@ object Extension {
|
|||||||
ExtensionTable.insert {
|
ExtensionTable.insert {
|
||||||
it[this.apkName] = apkName
|
it[this.apkName] = apkName
|
||||||
it[name] = extensionName
|
it[name] = extensionName
|
||||||
it[this.pkgName] = packageInfo.packageName
|
it[this.pkgName] = pkgName
|
||||||
it[versionName] = packageInfo.versionName
|
it[this.versionName] = versionName
|
||||||
it[versionCode] = packageInfo.versionCode.toLong()
|
it[this.versionCode] = versionCode.toLong()
|
||||||
it[extensionLib] = extensionLibVersion
|
it[extensionLib] = extensionLibVersion
|
||||||
it[lang] = extensionLang
|
it[lang] = extensionLang
|
||||||
it[this.contentWarning] = contentWarning
|
it[this.contentWarning] = contentWarning
|
||||||
@@ -236,8 +433,8 @@ object Extension {
|
|||||||
it[this.apkName] = apkName
|
it[this.apkName] = apkName
|
||||||
it[this.isInstalled] = true
|
it[this.isInstalled] = true
|
||||||
it[this.classFQName] = className
|
it[this.classFQName] = className
|
||||||
it[versionName] = packageInfo.versionName
|
it[this.versionName] = versionName
|
||||||
it[versionCode] = packageInfo.versionCode.toLong()
|
it[this.versionCode] = versionCode.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionId =
|
val extensionId =
|
||||||
@@ -258,31 +455,15 @@ object Extension {
|
|||||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 201 // we installed successfully
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// free up the file descriptor if exists
|
|
||||||
PackageTools.jarLoaderMap.remove(jarFilePath)?.close()
|
|
||||||
File(jarFilePath).delete()
|
|
||||||
|
|
||||||
try {
|
|
||||||
uninstallExtension(pkgName)
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return 302 // extension was already installed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractAndCacheApkIcon(
|
private fun extractAndCacheApkIcon(
|
||||||
apkFilePath: String,
|
apkFile: Path,
|
||||||
pkgName: String,
|
pkgName: String,
|
||||||
) {
|
) {
|
||||||
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
|
|
||||||
try {
|
try {
|
||||||
val iconData =
|
val iconData =
|
||||||
ApkFile(File(apkFilePath)).use { apk ->
|
ApkFile(apkFile.toFile()).use { apk ->
|
||||||
apk.allIcons
|
apk.allIcons
|
||||||
.filterIsInstance<Icon>()
|
.filterIsInstance<Icon>()
|
||||||
.mapNotNull { it.data?.let { data -> data to it.density } }
|
.mapNotNull { it.data?.let { data -> data to it.density } }
|
||||||
@@ -293,31 +474,48 @@ object Extension {
|
|||||||
logger.warn { "No icon found in APK $pkgName" }
|
logger.warn { "No icon found in APK $pkgName" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
cacheIcon(pkgName, iconData.inputStream())
|
||||||
File(iconCacheDir).mkdirs()
|
|
||||||
clearCachedImage(iconCacheDir, pkgName)
|
|
||||||
saveImage("$iconCacheDir/$pkgName", iconData.inputStream(), null)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn(e) { "Failed to extract icon from APK $pkgName" }
|
logger.warn(e) { "Failed to extract icon from APK $pkgName" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractAssetsFromApk(
|
private fun extractAndCacheJarIcon(
|
||||||
apkPath: String,
|
zipFile: ZipFile,
|
||||||
jarPath: String,
|
pkgName: String,
|
||||||
) {
|
) {
|
||||||
val apkFile = File(apkPath)
|
try {
|
||||||
val jarFile = File(jarPath)
|
val iconStream = ResourceArscIconParser.extractIcon(zipFile)
|
||||||
|
cacheIcon(pkgName, iconStream)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn(e) { "Failed to extract icon from JAR $pkgName" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val assetsFolder = File("${apkFile.parent}/${apkFile.nameWithoutExtension}_assets")
|
private fun cacheIcon(
|
||||||
assetsFolder.mkdir()
|
pkgName: String,
|
||||||
|
inputStream: InputStream,
|
||||||
|
) {
|
||||||
|
val iconCacheDir = Path("${applicationDirs.extensionsRoot}/icon")
|
||||||
|
iconCacheDir.createDirectories()
|
||||||
|
clearCachedImage(iconCacheDir.absolutePathString(), pkgName)
|
||||||
|
saveImage("$iconCacheDir/$pkgName", inputStream, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPathApi::class)
|
||||||
|
private fun extractAssetsFromApk(
|
||||||
|
apkFile: Path,
|
||||||
|
jarFile: Path,
|
||||||
|
) {
|
||||||
|
val assetsFolder = apkFile.parent / "${apkFile.nameWithoutExtension}_assets"
|
||||||
|
assetsFolder.createDirectories()
|
||||||
ZipInputStream(apkFile.inputStream()).use { zipInputStream ->
|
ZipInputStream(apkFile.inputStream()).use { zipInputStream ->
|
||||||
var zipEntry = zipInputStream.nextEntry
|
var zipEntry = zipInputStream.nextEntry
|
||||||
while (zipEntry != null) {
|
while (zipEntry != null) {
|
||||||
if (zipEntry.name.startsWith("assets/") && !zipEntry.isDirectory) {
|
if (zipEntry.name.startsWith("assets/") && !zipEntry.isDirectory) {
|
||||||
val assetFile = File(assetsFolder, zipEntry.name)
|
val assetFile = assetsFolder / zipEntry.name
|
||||||
assetFile.parentFile.mkdirs()
|
assetFile.parent.createDirectories()
|
||||||
FileOutputStream(assetFile).use { outputStream ->
|
assetFile.outputStream().use { outputStream ->
|
||||||
zipInputStream.copyTo(outputStream)
|
zipInputStream.copyTo(outputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,9 +523,9 @@ object Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val tempJarFile = File("${jarFile.parent}/${jarFile.nameWithoutExtension}_temp.jar")
|
val tempJarFile = jarFile.parent / "${jarFile.nameWithoutExtension}_temp.jar"
|
||||||
ZipInputStream(jarFile.inputStream()).use { jarZipInputStream ->
|
ZipInputStream(jarFile.inputStream()).use { jarZipInputStream ->
|
||||||
ZipOutputStream(FileOutputStream(tempJarFile)).use { jarZipOutputStream ->
|
ZipOutputStream(tempJarFile.outputStream()).use { jarZipOutputStream ->
|
||||||
var zipEntry = jarZipInputStream.nextEntry
|
var zipEntry = jarZipInputStream.nextEntry
|
||||||
while (zipEntry != null) {
|
while (zipEntry != null) {
|
||||||
if (!zipEntry.name.startsWith("META-INF/")) {
|
if (!zipEntry.name.startsWith("META-INF/")) {
|
||||||
@@ -336,8 +534,8 @@ object Extension {
|
|||||||
}
|
}
|
||||||
zipEntry = jarZipInputStream.nextEntry
|
zipEntry = jarZipInputStream.nextEntry
|
||||||
}
|
}
|
||||||
assetsFolder.walkTopDown().forEach { file ->
|
assetsFolder.walk().forEach { file ->
|
||||||
if (file.isFile) {
|
if (file.isRegularFile()) {
|
||||||
jarZipOutputStream.putNextEntry(ZipEntry(file.relativeTo(assetsFolder).toString().replace("\\", "/")))
|
jarZipOutputStream.putNextEntry(ZipEntry(file.relativeTo(assetsFolder).toString().replace("\\", "/")))
|
||||||
file.inputStream().use { inputStream ->
|
file.inputStream().use { inputStream ->
|
||||||
inputStream.copyTo(jarZipOutputStream)
|
inputStream.copyTo(jarZipOutputStream)
|
||||||
@@ -348,15 +546,16 @@ object Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jarFile.delete()
|
jarFile.deleteIfExists()
|
||||||
tempJarFile.renameTo(jarFile)
|
tempJarFile.copyTo(jarFile)
|
||||||
|
tempJarFile.deleteIfExists()
|
||||||
|
|
||||||
assetsFolder.deleteRecursively()
|
assetsFolder.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
private suspend fun downloadAPKFile(
|
private suspend fun downloadExtension(
|
||||||
url: String,
|
url: String,
|
||||||
savePath: String,
|
savePath: String,
|
||||||
) {
|
) {
|
||||||
@@ -366,11 +565,10 @@ object Extension {
|
|||||||
GET(url, cache = CacheControl.FORCE_NETWORK),
|
GET(url, cache = CacheControl.FORCE_NETWORK),
|
||||||
).await()
|
).await()
|
||||||
|
|
||||||
val downloadedFile = File(savePath)
|
val downloadedFile = Path(savePath)
|
||||||
downloadedFile.sink().buffer().use { sink ->
|
response.body.byteStream().use {
|
||||||
response.body.source().use { source ->
|
downloadedFile.outputStream().buffered().use { out ->
|
||||||
sink.writeAll(source)
|
it.copyTo(out)
|
||||||
sink.flush()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,7 +580,7 @@ object Extension {
|
|||||||
val fileNameWithoutType =
|
val fileNameWithoutType =
|
||||||
extensionRecord[ExtensionTable.apkName]?.substringBefore(".apk")
|
extensionRecord[ExtensionTable.apkName]?.substringBefore(".apk")
|
||||||
?: throw NullPointerException("Missing $pkgName apkName")
|
?: throw NullPointerException("Missing $pkgName apkName")
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
val jarPath = Path(applicationDirs.extensionsRoot) / "$fileNameWithoutType.jar"
|
||||||
val sources =
|
val sources =
|
||||||
transaction {
|
transaction {
|
||||||
val extensionId = extensionRecord[ExtensionTable.id].value
|
val extensionId = extensionRecord[ExtensionTable.id].value
|
||||||
@@ -404,14 +602,14 @@ object Extension {
|
|||||||
sources
|
sources
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File(jarPath).exists()) {
|
if (jarPath.exists()) {
|
||||||
// free up the file descriptor if exists
|
// free up the file descriptor if exists
|
||||||
PackageTools.jarLoaderMap.remove(jarPath)?.close()
|
PackageTools.jarLoaderMap.remove(jarPath.absolutePathString())?.close()
|
||||||
|
|
||||||
// clear all loaded sources
|
// clear all loaded sources
|
||||||
sources.forEach { GetSource.unregisterSource(it) }
|
sources.forEach { GetSource.unregisterSource(it) }
|
||||||
|
|
||||||
File(jarPath).delete()
|
jarPath.deleteIfExists()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ object ExtensionsList {
|
|||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
|
this[ExtensionTable.jarUrl] = foundExtension.jarUrl
|
||||||
|
|
||||||
// add these because batch updates need matching columns
|
// add these because batch updates need matching columns
|
||||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
||||||
@@ -176,6 +177,7 @@ object ExtensionsList {
|
|||||||
this[ExtensionTable.lang] = foundExtension.lang
|
this[ExtensionTable.lang] = foundExtension.lang
|
||||||
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
|
this[ExtensionTable.jarUrl] = foundExtension.jarUrl
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
}.toExecutable()
|
}.toExecutable()
|
||||||
@@ -193,6 +195,7 @@ object ExtensionsList {
|
|||||||
this[ExtensionTable.lang] = foundExtension.lang
|
this[ExtensionTable.lang] = foundExtension.lang
|
||||||
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
|
||||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||||
|
this[ExtensionTable.jarUrl] = foundExtension.jarUrl
|
||||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ data class NetworkExtensionStore(
|
|||||||
data class Resources(
|
data class Resources(
|
||||||
@ProtoNumber(1) val apkUrl: String,
|
@ProtoNumber(1) val apkUrl: String,
|
||||||
@ProtoNumber(2) val iconUrl: String,
|
@ProtoNumber(2) val iconUrl: String,
|
||||||
|
// Keiyoushi specific output
|
||||||
|
@ProtoNumber(501) val jarUrl: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -109,6 +111,7 @@ fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore):
|
|||||||
name = extension.name,
|
name = extension.name,
|
||||||
pkgName = extension.packageName,
|
pkgName = extension.packageName,
|
||||||
apkUrl = extension.resources.apkUrl,
|
apkUrl = extension.resources.apkUrl,
|
||||||
|
jarUrl = extension.resources.jarUrl,
|
||||||
iconUrl = extension.resources.iconUrl,
|
iconUrl = extension.resources.iconUrl,
|
||||||
extensionLib = extension.extensionLib,
|
extensionLib = extension.extensionLib,
|
||||||
versionCode = extension.versionCode,
|
versionCode = extension.versionCode,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
|||||||
name = name.substringAfter("Tachiyomi: "),
|
name = name.substringAfter("Tachiyomi: "),
|
||||||
pkgName = pkg,
|
pkgName = pkg,
|
||||||
apkUrl = "$storeBaseUrl/apk/$apk",
|
apkUrl = "$storeBaseUrl/apk/$apk",
|
||||||
|
jarUrl = null,
|
||||||
iconUrl = "$storeBaseUrl/icon/$pkg.png",
|
iconUrl = "$storeBaseUrl/icon/$pkg.png",
|
||||||
extensionLib = version.substringBeforeLast('.'),
|
extensionLib = version.substringBeforeLast('.'),
|
||||||
versionCode = code,
|
versionCode = code,
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.util
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi
|
||||||
|
import nl.adaptivity.xmlutil.QName
|
||||||
|
import nl.adaptivity.xmlutil.XmlDeclMode
|
||||||
|
import nl.adaptivity.xmlutil.XmlReader
|
||||||
|
import nl.adaptivity.xmlutil.core.KtXmlReader
|
||||||
|
import nl.adaptivity.xmlutil.serialization.InputKind
|
||||||
|
import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
|
||||||
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
|
import nl.adaptivity.xmlutil.serialization.XmlElement
|
||||||
|
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||||
|
import nl.adaptivity.xmlutil.serialization.structure.XmlDescriptor
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object AndroidManifestParser {
|
||||||
|
private const val ANDROID_NS = "http://schemas.android.com/apk/res/android"
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("manifest", "", "")
|
||||||
|
data class AndroidManifest(
|
||||||
|
@XmlSerialName("package", "", "")
|
||||||
|
val packageName: String,
|
||||||
|
|
||||||
|
@XmlSerialName("versionCode", ANDROID_NS, "android")
|
||||||
|
val versionCode: Int? = null,
|
||||||
|
|
||||||
|
@XmlSerialName("versionName", ANDROID_NS, "android")
|
||||||
|
val versionName: String? = null,
|
||||||
|
|
||||||
|
@XmlElement(true)
|
||||||
|
@XmlSerialName("uses-sdk", "", "")
|
||||||
|
val usesSdk: UsesSdk? = null,
|
||||||
|
|
||||||
|
@XmlElement(true)
|
||||||
|
@XmlSerialName("uses-feature", "", "")
|
||||||
|
val usesFeatures: List<UsesFeature> = emptyList(),
|
||||||
|
|
||||||
|
@XmlElement(true)
|
||||||
|
@XmlSerialName("application", "", "")
|
||||||
|
val application: Application? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("uses-sdk", "", "")
|
||||||
|
data class UsesSdk(
|
||||||
|
@XmlSerialName("minSdkVersion", ANDROID_NS, "android")
|
||||||
|
val minSdkVersion: Int? = null,
|
||||||
|
|
||||||
|
@XmlSerialName("targetSdkVersion", ANDROID_NS, "android")
|
||||||
|
val targetSdkVersion: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("uses-feature", "", "")
|
||||||
|
data class UsesFeature(
|
||||||
|
@XmlSerialName("name", ANDROID_NS, "android")
|
||||||
|
val name: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("application", "", "")
|
||||||
|
data class Application(
|
||||||
|
@XmlSerialName("label", ANDROID_NS, "android")
|
||||||
|
val label: String? = null,
|
||||||
|
|
||||||
|
@XmlSerialName("icon", ANDROID_NS, "android")
|
||||||
|
val icon: String? = null,
|
||||||
|
|
||||||
|
@XmlSerialName("allowBackup", ANDROID_NS, "android")
|
||||||
|
val allowBackup: Boolean? = null,
|
||||||
|
|
||||||
|
@XmlSerialName("extractNativeLibs", ANDROID_NS, "android")
|
||||||
|
val extractNativeLibs: Boolean? = null,
|
||||||
|
|
||||||
|
@XmlElement(true)
|
||||||
|
@XmlSerialName("meta-data", "", "")
|
||||||
|
val metaData: List<MetaData> = emptyList(),
|
||||||
|
|
||||||
|
@XmlElement(true)
|
||||||
|
@XmlSerialName("activity", "", "")
|
||||||
|
val activities: List<Activity> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("meta-data", "", "")
|
||||||
|
data class MetaData(
|
||||||
|
@XmlSerialName("name", ANDROID_NS, "android")
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
@XmlSerialName("value", ANDROID_NS, "android")
|
||||||
|
val value: String? = null,
|
||||||
|
|
||||||
|
@XmlSerialName("resource", ANDROID_NS, "android")
|
||||||
|
val resource: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("activity", "", "")
|
||||||
|
data class Activity(
|
||||||
|
@XmlSerialName("name", ANDROID_NS, "android")
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
@XmlSerialName("exported", ANDROID_NS, "android")
|
||||||
|
val exported: Boolean? = null,
|
||||||
|
|
||||||
|
@XmlSerialName("theme", ANDROID_NS, "android")
|
||||||
|
val theme: String? = null,
|
||||||
|
|
||||||
|
@XmlElement(true)
|
||||||
|
@XmlSerialName("intent-filter", "", "")
|
||||||
|
val intentFilters: List<IntentFilter> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("intent-filter", "", "")
|
||||||
|
data class IntentFilter(
|
||||||
|
@XmlElement(true)
|
||||||
|
@XmlSerialName("action", "", "")
|
||||||
|
val actions: List<Action> = emptyList(),
|
||||||
|
|
||||||
|
@XmlElement(true)
|
||||||
|
@XmlSerialName("category", "", "")
|
||||||
|
val categories: List<Category> = emptyList(),
|
||||||
|
|
||||||
|
@XmlElement(true)
|
||||||
|
@XmlSerialName("data", "", "")
|
||||||
|
val data: List<Data> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("action", "", "")
|
||||||
|
data class Action(
|
||||||
|
@XmlSerialName("name", ANDROID_NS, "android")
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("category", "", "")
|
||||||
|
data class Category(
|
||||||
|
@XmlSerialName("name", ANDROID_NS, "android")
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@XmlSerialName("data", "", "")
|
||||||
|
data class Data(
|
||||||
|
@XmlSerialName("scheme", ANDROID_NS, "android")
|
||||||
|
val scheme: String? = null,
|
||||||
|
|
||||||
|
@XmlSerialName("host", ANDROID_NS, "android")
|
||||||
|
val host: String? = null,
|
||||||
|
|
||||||
|
@XmlSerialName("pathPattern", ANDROID_NS, "android")
|
||||||
|
val pathPattern: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val xml = XML {
|
||||||
|
autoPolymorphic = false
|
||||||
|
repairNamespaces = true
|
||||||
|
xmlDeclMode = XmlDeclMode.Minimal
|
||||||
|
defaultPolicy {
|
||||||
|
unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalXmlUtilApi::class)
|
||||||
|
fun parse(input: InputStream): AndroidManifest =
|
||||||
|
xml.decodeFromReader(AndroidManifest.serializer(), KtXmlReader(input))
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ import java.nio.file.Files
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.nameWithoutExtension
|
||||||
import kotlin.io.path.relativeTo
|
import kotlin.io.path.relativeTo
|
||||||
|
|
||||||
object PackageTools {
|
object PackageTools {
|
||||||
@@ -52,15 +54,13 @@ object PackageTools {
|
|||||||
* Convert dex to jar, a wrapper for the dex2jar library
|
* Convert dex to jar, a wrapper for the dex2jar library
|
||||||
*/
|
*/
|
||||||
fun dex2jar(
|
fun dex2jar(
|
||||||
dexFile: String,
|
dexFile: Path,
|
||||||
jarFile: String,
|
jarFile: Path,
|
||||||
fileNameWithoutType: String,
|
|
||||||
) {
|
) {
|
||||||
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
||||||
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
||||||
|
|
||||||
val jarFilePath = File(jarFile).toPath()
|
val reader = MultiDexFileReader.open(Files.readAllBytes(dexFile))
|
||||||
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
|
|
||||||
val handler = BaksmaliBaseDexExceptionHandler()
|
val handler = BaksmaliBaseDexExceptionHandler()
|
||||||
Dex2jar
|
Dex2jar
|
||||||
.from(reader)
|
.from(reader)
|
||||||
@@ -73,10 +73,10 @@ object PackageTools {
|
|||||||
.noCode(false)
|
.noCode(false)
|
||||||
.skipExceptions(false)
|
.skipExceptions(false)
|
||||||
.dontSanitizeNames(true)
|
.dontSanitizeNames(true)
|
||||||
.to(jarFilePath)
|
.to(jarFile)
|
||||||
if (handler.hasException()) {
|
if (handler.hasException()) {
|
||||||
val rootPath = Path(applicationDirs.extensionsRoot)
|
val rootPath = Path(applicationDirs.extensionsRoot)
|
||||||
val errorFile: Path = rootPath.resolve("$fileNameWithoutType-error.txt")
|
val errorFile: Path = rootPath.resolve("${dexFile.nameWithoutExtension}-error.txt")
|
||||||
logger.error {
|
logger.error {
|
||||||
"""
|
"""
|
||||||
Detail Error Information in File ${errorFile.relativeTo(rootPath)}
|
Detail Error Information in File ${errorFile.relativeTo(rootPath)}
|
||||||
@@ -89,15 +89,14 @@ object PackageTools {
|
|||||||
}
|
}
|
||||||
handler.dump(errorFile, emptyArray<String>())
|
handler.dump(errorFile, emptyArray<String>())
|
||||||
} else {
|
} else {
|
||||||
BytecodeEditor.fixAndroidClasses(jarFilePath)
|
BytecodeEditor.fixAndroidClasses(jarFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
|
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
|
||||||
fun getPackageInfo(apkFilePath: String): PackageInfo {
|
fun getPackageInfo(apkFile: Path): PackageInfo {
|
||||||
val apk = File(apkFilePath)
|
return ApkParsers.getMetaInfo(apkFile.toFile()).toPackageInfo(apkFile.toFile()).apply {
|
||||||
return ApkParsers.getMetaInfo(apk).toPackageInfo(apk).apply {
|
val parsed = ApkFile(apkFile.toFile())
|
||||||
val parsed = ApkFile(apk)
|
|
||||||
val dbFactory = DocumentBuilderFactory.newInstance()
|
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||||
val dBuilder = dbFactory.newDocumentBuilder()
|
val dBuilder = dbFactory.newDocumentBuilder()
|
||||||
val doc =
|
val doc =
|
||||||
@@ -156,19 +155,19 @@ object PackageTools {
|
|||||||
* It may return an instance of HttpSource or SourceFactory depending on the extension.
|
* It may return an instance of HttpSource or SourceFactory depending on the extension.
|
||||||
*/
|
*/
|
||||||
fun loadExtensionSources(
|
fun loadExtensionSources(
|
||||||
jarPath: String,
|
jar: Path,
|
||||||
className: String,
|
className: String,
|
||||||
): Any {
|
): Any {
|
||||||
try {
|
try {
|
||||||
logger.debug { "loading jar with path: $jarPath" }
|
logger.debug { "loading jar with path: ${jar.absolutePathString()}" }
|
||||||
val classLoader = jarLoaderMap[jarPath] ?: ChildFirstURLClassLoader(arrayOf<URL>(Path(jarPath).toUri().toURL()))
|
val classLoader = jarLoaderMap[jar.absolutePathString()] ?: ChildFirstURLClassLoader(arrayOf<URL>(jar.toUri().toURL()))
|
||||||
val classToLoad = Class.forName(className, false, classLoader)
|
val classToLoad = Class.forName(className, false, classLoader)
|
||||||
|
|
||||||
jarLoaderMap[jarPath] = classLoader
|
jarLoaderMap[jar.absolutePathString()] = classLoader
|
||||||
|
|
||||||
return classToLoad.getDeclaredConstructor().newInstance()
|
return classToLoad.getDeclaredConstructor().newInstance()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "Failed to load jar with path: $jarPath" }
|
logger.error(e) { "Failed to load jar with path: ${jar.absolutePathString()}" }
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.util
|
||||||
|
|
||||||
|
import nl.adaptivity.xmlutil.core.impl.multiplatform.InputStream
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||||
|
import pxb.android.arsc.ArscParser
|
||||||
|
import pxb.android.arsc.Config
|
||||||
|
import pxb.android.arsc.Pkg
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
|
object ResourceArscIconParser {
|
||||||
|
private data class IconCandidate(
|
||||||
|
val density: Int,
|
||||||
|
val path: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun extractIcon(
|
||||||
|
jar: Path,
|
||||||
|
iconPath: Path
|
||||||
|
) {
|
||||||
|
ZipFile.builder()
|
||||||
|
.setPath(jar)
|
||||||
|
.get()
|
||||||
|
.use { zip ->
|
||||||
|
val packages = zip.getInputStream(zip.getEntry("resources.arsc"))
|
||||||
|
.use { ArscParser(it.readBytes()).parse() }
|
||||||
|
|
||||||
|
val icon = packages
|
||||||
|
.flatMap { it.iconCandidates() }
|
||||||
|
.maxByOrNull { it.density }
|
||||||
|
?: return
|
||||||
|
|
||||||
|
val entry = zip.getEntry(icon.path) ?: return
|
||||||
|
|
||||||
|
zip.getInputStream(entry).use {
|
||||||
|
iconPath.outputStream().use { out ->
|
||||||
|
it.copyTo(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractIcon(
|
||||||
|
zip: ZipFile,
|
||||||
|
): InputStream {
|
||||||
|
val packages = zip.getInputStream(zip.getEntry("resources.arsc"))
|
||||||
|
.use { ArscParser(it.readBytes()).parse() }
|
||||||
|
|
||||||
|
val icon = packages
|
||||||
|
.flatMap { it.iconCandidates() }
|
||||||
|
.maxByOrNull { it.density }
|
||||||
|
?: throw NullPointerException("No valid icons")
|
||||||
|
|
||||||
|
val entry = zip.getEntry(icon.path)
|
||||||
|
?: throw NullPointerException("Icon ${icon.path} missing")
|
||||||
|
|
||||||
|
return zip.getInputStream(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Pkg.iconCandidates(): List<IconCandidate> =
|
||||||
|
types.values
|
||||||
|
.filter { it.name == "mipmap" || it.name == "drawable" }
|
||||||
|
.flatMap {
|
||||||
|
it.configs.flatMap {
|
||||||
|
val density = it.density()
|
||||||
|
|
||||||
|
it.resources.values
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.spec.name == "ic_launcher" }
|
||||||
|
.map { it.value.toString() }
|
||||||
|
.filter(::isRasterImage)
|
||||||
|
.map { IconCandidate(density, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Config.density(): Int =
|
||||||
|
ByteBuffer.wrap(id)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
.getShort(14)
|
||||||
|
.toInt() and 0xffff
|
||||||
|
|
||||||
|
private val rasterExtensions = setOf(
|
||||||
|
"png",
|
||||||
|
"webp",
|
||||||
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun isRasterImage(path: String): Boolean =
|
||||||
|
path.substringAfterLast('.', "")
|
||||||
|
.lowercase() in rasterExtensions
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
|
|||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.div
|
||||||
|
|
||||||
object GetSource {
|
object GetSource {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
@@ -49,7 +51,7 @@ object GetSource {
|
|||||||
?: throw NullPointerException("Missing apkName")
|
?: throw NullPointerException("Missing apkName")
|
||||||
val className = extensionRecord[ExtensionTable.classFQName]
|
val className = extensionRecord[ExtensionTable.classFQName]
|
||||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
val jarPath = Path(applicationDirs.extensionsRoot) / jarName
|
||||||
|
|
||||||
when (val instance = loadExtensionSources(jarPath, className)) {
|
when (val instance = loadExtensionSources(jarPath, className)) {
|
||||||
is Source -> listOf(instance)
|
is Source -> listOf(instance)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ data class ExtensionInfo(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val pkgName: String,
|
val pkgName: String,
|
||||||
val apkUrl: String,
|
val apkUrl: String,
|
||||||
|
val jarUrl: String?,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val extensionLib: String,
|
val extensionLib: String,
|
||||||
val versionCode: Long,
|
val versionCode: Long,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ object ExtensionTable : IntIdTable() {
|
|||||||
val name = varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
val pkgName = varchar("pkg_name", 128)
|
val pkgName = varchar("pkg_name", 128)
|
||||||
val apkUrl = varchar("apk_url", 2048).nullable()
|
val apkUrl = varchar("apk_url", 2048).nullable()
|
||||||
|
val jarUrl = varchar("jar_url", 2048).nullable()
|
||||||
val extensionLib = varchar("extension_lib", 16).nullable()
|
val extensionLib = varchar("extension_lib", 16).nullable()
|
||||||
val versionName = varchar("version_name", 16)
|
val versionName = varchar("version_name", 16)
|
||||||
val versionCode = long("version_code")
|
val versionCode = long("version_code")
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package suwayomi.tachidesk.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import de.neonew.exposed.migrations.helpers.AddColumnMigration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0059_AddJarUrlToExtensionTable :
|
||||||
|
AddColumnMigration(
|
||||||
|
"extension",
|
||||||
|
"jar_url",
|
||||||
|
"VARCHAR(2048)",
|
||||||
|
"NULL",
|
||||||
|
)
|
||||||
111
server/src/test/kotlin/JarTest.kt
Normal file
111
server/src/test/kotlin/JarTest.kt
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||||
|
import pxb.android.arsc.ArscDumper
|
||||||
|
import pxb.android.arsc.ArscParser
|
||||||
|
import pxb.android.axml.AxmlParser
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.AndroidManifestParser
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.ResourceArscIconParser
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class JarTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun jarTest() {
|
||||||
|
println(Path("./").absolutePathString())
|
||||||
|
val jar = Path("./build/tachiyomi-all.ahottie-v1.4.3.jar")
|
||||||
|
|
||||||
|
val zip = ZipFile.builder()
|
||||||
|
.setPath(jar)
|
||||||
|
.get()
|
||||||
|
val manifest = zip.getInputStream(zip.getEntry("AndroidManifest.xml"))
|
||||||
|
.use {
|
||||||
|
AndroidManifestParser.parse(it)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// reader.parse()
|
||||||
|
// println(reader.resourceTable)
|
||||||
|
// val resourceTable = reader.resourceTable
|
||||||
|
// println(resourceTable.stringPool)
|
||||||
|
// //println(table.stringPool.get(1))
|
||||||
|
// // table::class.members.forEach {
|
||||||
|
// // if (it.name == "packageMap") {
|
||||||
|
// // it.isAccessible = true
|
||||||
|
// // val map = it.call(table) as Map<Short, ResourcePackage>
|
||||||
|
// // map.forEach { short, packagz ->
|
||||||
|
// // println(short)
|
||||||
|
// // println(packagz.name to packagz.id)
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// val apkTranslator = ApkMetaTranslator(resourceTable, null)
|
||||||
|
// val xmlTranslator = XmlTranslator()
|
||||||
|
// val xmlStreamer = CompositeXmlStreamer(xmlTranslator, apkTranslator)
|
||||||
|
// val data = zip.getInputStream(zip.getEntry("AndroidManifest.xml"))
|
||||||
|
// .use {
|
||||||
|
// it.readBytes()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// val buffer = ByteBuffer.wrap(data)
|
||||||
|
// val binaryXmlParser = BinaryXmlParser(buffer, resourceTable)
|
||||||
|
// //binaryXmlParser.locale = preferredLocale
|
||||||
|
// binaryXmlParser.xmlStreamer = xmlStreamer
|
||||||
|
// binaryXmlParser.parse()
|
||||||
|
// val manifestXml = xmlTranslator.xml
|
||||||
|
// println(manifestXml)
|
||||||
|
// val iconPaths = apkTranslator.iconPaths
|
||||||
|
// iconPaths.forEach {
|
||||||
|
// println(it.path)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// val parser = zip.getInputStream(zip.getEntry("resources.arsc"))
|
||||||
|
// .use {
|
||||||
|
// ArscParser(it.readBytes())
|
||||||
|
// }
|
||||||
|
// val parsed = parser.parse()
|
||||||
|
// ArscDumper.dump(parsed)
|
||||||
|
//
|
||||||
|
// parsed.forEach {
|
||||||
|
// print("pkg.name: ")
|
||||||
|
// println(it.name)
|
||||||
|
// it.types.toList().forEachIndexed { t, (i, type) ->
|
||||||
|
// print("pkg.types.$t.type.name: ")
|
||||||
|
// println(type.name)
|
||||||
|
// print("pkg.types.$t.type.id: ")
|
||||||
|
// println(type.id)
|
||||||
|
// type.specs.forEachIndexed { y, spec ->
|
||||||
|
// print("pkg.types.$t.type.specs.$y.id: ")
|
||||||
|
// println(spec.id)
|
||||||
|
// print("pkg.types.$t.type.specs.$y.name: ")
|
||||||
|
// println(spec.name)
|
||||||
|
// print("pkg.types.$t.type.specs.$y.flags: ")
|
||||||
|
// println(spec.flags)
|
||||||
|
// }
|
||||||
|
// type.configs.forEachIndexed { y, config ->
|
||||||
|
// print("pkg.types.$t.type.configs.$y.id: ")
|
||||||
|
// println(config.id.toHexString(HexFormat.UpperCase))
|
||||||
|
// print("pkg.types.$t.type.configs.$y.entryCount: ")
|
||||||
|
// println(config.entryCount)
|
||||||
|
// config.resources.forEach {
|
||||||
|
// print("pkg.types.$t.type.configs.$y.resources.${it.key}.flag: ")
|
||||||
|
// println(it.value.flag)
|
||||||
|
// print("pkg.types.$t.type.configs.$y.resources.${it.key}.value: ")
|
||||||
|
// println(it.value.value)
|
||||||
|
// print("pkg.types.$t.type.configs.$y.resources.${it.key}.spec.id: ")
|
||||||
|
// println(it.value.spec.id)
|
||||||
|
// print("pkg.types.$t.type.configs.$y.resources.${it.key}.spec.name: ")
|
||||||
|
// println(it.value.spec.name)
|
||||||
|
// print("pkg.types.$t.type.configs.$y.resources.${it.key}.spec.flags: ")
|
||||||
|
// println(it.value.spec.flags)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// val fileTest = Path("./build/icon.png")
|
||||||
|
// ResourceArscIconParser.extractIcon(jar, fileTest)
|
||||||
|
|
||||||
|
println(manifest.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user