diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt index 78c9ecb..05fd169 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt @@ -91,7 +91,7 @@ fun installExtension(pkgName: String): Int { // download apk file downloadAPKFile(apkToDownload, apkFilePath) - val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath) + val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath) logger.debug(className) // dex -> jar dex2jar(dexFilePath, jarFilePath, fileNameWithoutType) @@ -134,7 +134,6 @@ fun installExtension(pkgName: String): Int { it[this.lang] = httpSource.lang it[extension] = extensionId it[partOfFactorySource] = true - it[positionInFactorySource] = index } } logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}") diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/SourceList.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Source.kt similarity index 63% rename from server/src/main/kotlin/ir/armor/tachidesk/impl/SourceList.kt rename to server/src/main/kotlin/ir/armor/tachidesk/impl/Source.kt index b81bcbb..59e4eef 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/SourceList.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Source.kt @@ -19,24 +19,22 @@ import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import java.net.URL import java.net.URLClassLoader +import java.util.concurrent.ConcurrentHashMap private val logger = KotlinLogging.logger {} -private val sourceCache = mutableListOf>() -private val extensionCache = mutableListOf>() +private val sourceCache = ConcurrentHashMap() fun getHttpSource(sourceId: Long): HttpSource { - val sourceRecord = transaction { - SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! - } - - val cachedResult: Pair? = sourceCache.firstOrNull { it.first == sourceId } + val cachedResult: HttpSource? = sourceCache[sourceId] if (cachedResult != null) { - logger.debug("used cached HttpSource: ${cachedResult.second.name}") - return cachedResult.second + logger.debug("used cached HttpSource: ${cachedResult.name}") + return cachedResult } - val result: HttpSource = transaction { + transaction { + val sourceRecord = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! + val extensionId = sourceRecord[SourceTable.extension] val extensionRecord = ExtensionTable.select { ExtensionTable.id eq extensionId }.firstOrNull()!! val apkName = extensionRecord[ExtensionTable.apkName] @@ -44,37 +42,24 @@ fun getHttpSource(sourceId: Long): HttpSource { val jarName = apkName.substringBefore(".apk") + ".jar" val jarPath = "${applicationDirs.extensionsRoot}/$jarName" - val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath } - var usedCached = false - val instance = - if (cachedExtensionPair != null) { - usedCached = true - logger.debug("Used cached Extension") - cachedExtensionPair.second - } else { - logger.debug("No Extension cache") + val extensionInstance = + { val child = URLClassLoader(arrayOf(URL("file:$jarPath")), this::class.java.classLoader) val classToLoad = Class.forName(className, true, child) - classToLoad.newInstance() + classToLoad.getDeclaredConstructor().newInstance() } + if (sourceRecord[SourceTable.partOfFactorySource]) { - val positionInFactorySource = sourceRecord[SourceTable.positionInFactorySource]!! - return@transaction if (usedCached) { - @Suppress("UNCHECKED_CAST") - (instance as List)[positionInFactorySource] - } else { - val list = (instance as SourceFactory).createSources() - extensionCache.add(Pair(jarPath, list)) - list[positionInFactorySource] as HttpSource + (extensionInstance as SourceFactory).createSources().forEach{ + sourceCache[it.id] = it as HttpSource } } else { - if (!usedCached) - extensionCache.add(Pair(jarPath, instance)) - return@transaction instance as HttpSource + (extensionInstance as HttpSource).also { + sourceCache[it.id] = it + } } } - sourceCache.add(Pair(sourceId, result)) - return result + return sourceCache[sourceId]!! } fun getSourceList(): List { diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt index 6318c2e..f964c3e 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt @@ -7,6 +7,7 @@ package ir.armor.tachidesk.impl.util * 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 mu.KotlinLogging import org.w3c.dom.Document import org.xml.sax.InputSource import java.io.IOException @@ -17,16 +18,15 @@ import java.util.zip.ZipFile import javax.xml.parsers.DocumentBuilderFactory object APKExtractor { + private val logger = KotlinLogging.logger {} + // decompressXML -- Parse the 'compressed' binary form of Android XML docs // such as for AndroidManifest.xml in .apk files - var endDocTag = 0x00100101 - var startTag = 0x00100102 - var endTag = 0x00100103 - fun prt(str: String?) { - // System.err.print(str); - } + private const val endDocTag = 0x00100101 + private const val startTag = 0x00100102 + private const val endTag = 0x00100103 - fun decompressXML(xml: ByteArray): String { + private fun decompressXML(xml: ByteArray): String { val finalXML = StringBuilder() // Compressed XML file/bytes starts with 24x bytes of data, @@ -134,8 +134,8 @@ object APKExtractor { // AttrValue StrInd off += 5 * 4 // Skip over the 5 words of an attribute val attrName = compXmlString( - xml, sitOff, stOff, - attrNameSi + xml, sitOff, stOff, + attrNameSi ) val attrValue = if (attrValueSi != -1) compXmlString(xml, sitOff, stOff, attrValueSi) else "resourceID 0x ${Integer.toHexString(attrResId)}" @@ -151,26 +151,25 @@ object APKExtractor { val name = compXmlString(xml, sitOff, stOff, nameSi) finalXML.append("") prtIndent( - indent, - " (line " + startTagLineNo + - "-" + lineNo + ")" + indent, + " (line " + startTagLineNo + + "-" + lineNo + ")" ) // tr.parent(); // Step back up the NobTree } else if (tag0 == endDocTag) { // END OF XML DOC TAG break } else { - prt( - " Unrecognized tag code '" + Integer.toHexString(tag0) + - "' at offset " + off + logger.debug( + " Unrecognized tag code '${Integer.toHexString(tag0)}'' at offset $off" ) break } } // end of while loop scanning tags and attributes of XML tree - // prt(" end at offset " + off); + logger.debug(" end at offset $off"); return finalXML.toString() } // end of decompressXML - fun compXmlString(xml: ByteArray, sitOff: Int, stOff: Int, strInd: Int): String? { + private fun compXmlString(xml: ByteArray, sitOff: Int, stOff: Int, strInd: Int): String? { if (strInd < 0) return null val strOff = stOff + LEW(xml, sitOff + strInd * 4) return compXmlStringAt(xml, strOff) @@ -178,13 +177,13 @@ object APKExtractor { var spaces = " " fun prtIndent(indent: Int, str: String) { - prt(spaces.substring(0, Math.min(indent * 2, spaces.length)) + str) + logger.debug(spaces.substring(0, Math.min(indent * 2, spaces.length)) + str) } // compXmlStringAt -- Return the string stored in StringTable format at // offset strOff. This offset points to the 16 bit string length, which // is followed by that number of 16 bit (Unicode) chars. - fun compXmlStringAt(arr: ByteArray, strOff: Int): String { + private fun compXmlStringAt(arr: ByteArray, strOff: Int): String { val strLen: Int = arr[strOff + 1].toInt() shl 8 and 0xff00 or arr[strOff].toInt() and 0xff val chars = ByteArray(strLen) for (ii in 0 until strLen) { @@ -195,316 +194,57 @@ object APKExtractor { // LEW -- Return value of a Little Endian 32 bit word from the byte array // at offset off. - fun LEW(arr: ByteArray, off: Int): Int { + private fun LEW(arr: ByteArray, off: Int): Int { return (arr[off + 3].toInt() shl 24) and -0x1000000 or - (arr[off + 2].toInt() shl 16 and 0xff0000) or - (arr[off + 1].toInt() shl 8 and 0xff00) or - (arr[off].toInt() and 0xFF) + (arr[off + 2].toInt() shl 16 and 0xff0000) or + (arr[off + 1].toInt() shl 8 and 0xff00) or + (arr[off].toInt() and 0xFF) } // end of LEW @Throws(Exception::class) - fun loadXMLFromString(xml: String?): Document { - val docBuilderFactory = DocumentBuilderFactory.newInstance() - val docBuilder = docBuilderFactory.newDocumentBuilder() - return docBuilder.parse(InputSource(StringReader(xml))) + private fun loadXMLFromString(xml: String?): Document { + return DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(InputSource(StringReader(xml))) } @Throws(IOException::class) - fun extract_dex_and_read_className(filePath: String?, dexPath: String?): String { - var zip: ZipFile? = null - zip = ZipFile(filePath) - val androidManifest = zip.getEntry("AndroidManifest.xml") - val classesDex = zip.getEntry("classes.dex") + fun extractDexAndReadClassname(filePath: String, dexPath: String): String { + ZipFile(filePath).use { zip -> + val androidManifest = zip.getEntry("AndroidManifest.xml") + val classesDex = zip.getEntry("classes.dex") - // write dex file - val dexStream = zip.getInputStream(classesDex) - Files.newOutputStream(Paths.get(dexPath)).use { os -> - val buffer = ByteArray(1024) - var len: Int - while (dexStream.read(buffer).also { len = it } > 0) { - os.write(buffer, 0, len) + // write dex file + zip.getInputStream(classesDex).use { dexInputStream -> + Files.newOutputStream(Paths.get(dexPath)).use { fileOutputStream -> + dexInputStream.copyTo(fileOutputStream) + } } - } - // read xml file - val `is` = zip.getInputStream(androidManifest) - val buf = ByteArray(1024000) // 100 kb - `is`.read(buf) - `is`.close() - zip.close() - val xml = decompressXML(buf) - try { - val xmlDoc = loadXMLFromString(xml) - val pkg = xmlDoc.documentElement.getAttribute("package") - val nodes = xmlDoc.getElementsByTagName("meta-data") - for (i in 0 until nodes.length) { - val attributes = nodes.item(i).attributes - println(attributes.getNamedItem("name").nodeValue) - if (attributes.getNamedItem("name").nodeValue == "tachiyomi.extension.class") return pkg + attributes.getNamedItem("value").nodeValue + // read xml file + val xml = zip.getInputStream(androidManifest).use { inpStream -> + // 1024000 = 100 kb + ByteArray(1024000).let { + inpStream.read(it) + decompressXML(it) + } } - } catch (e: Exception) { - e.printStackTrace() + try { + val xmlDoc = loadXMLFromString(xml) + val pkg = xmlDoc.documentElement.getAttribute("package") + val nodes = xmlDoc.getElementsByTagName("meta-data") + for (i in 0 until nodes.length) { + val attributes = nodes.item(i).attributes + logger.debug(attributes.getNamedItem("name").nodeValue) + if (attributes.getNamedItem("name").nodeValue == "tachiyomi.extension.class") { + return pkg + attributes.getNamedItem("value").nodeValue + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" } - return "" } -} - -// original Java code - -// package ir.armor.tachidesk.impl.util; -// -// /* -// * 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 org.w3c.dom.Document; -// import org.w3c.dom.NamedNodeMap; -// import org.w3c.dom.NodeList; -// import org.xml.sax.InputSource; -// -// import javax.xml.parsers.DocumentBuilder; -// import javax.xml.parsers.DocumentBuilderFactory; -// import java.io.*; -// import java.nio.file.Files; -// import java.nio.file.Paths; -// import java.util.zip.ZipEntry; -// import java.util.zip.ZipFile; -// -// public class APKExtractor { -// // decompressXML -- Parse the 'compressed' binary form of Android XML docs -// // such as for AndroidManifest.xml in .apk files -// public static int endDocTag = 0x00100101; -// public static int startTag = 0x00100102; -// public static int endTag = 0x00100103; -// -// static void prt(String str) { -// //System.err.print(str); -// } -// -// public static String decompressXML(byte[] xml) { -// -// StringBuilder finalXML = new StringBuilder(); -// -// // Compressed XML file/bytes starts with 24x bytes of data, -// // 9 32 bit words in little endian order (LSB first): -// // 0th word is 03 00 08 00 -// // 3rd word SEEMS TO BE: Offset at then of StringTable -// // 4th word is: Number of strings in string table -// // WARNING: Sometime I indiscriminently display or refer to word in -// // little endian storage format, or in integer format (ie MSB first). -// int numbStrings = LEW(xml, 4 * 4); -// -// // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets -// // of the length/string data in the StringTable. -// int sitOff = 0x24; // Offset of start of StringIndexTable -// -// // StringTable, each string is represented with a 16 bit little endian -// // character count, followed by that number of 16 bit (LE) (Unicode) -// // chars. -// int stOff = sitOff + numbStrings * 4; // StringTable follows -// // StrIndexTable -// -// // XMLTags, The XML tag tree starts after some unknown content after the -// // StringTable. There is some unknown data after the StringTable, scan -// // forward from this point to the flag for the start of an XML start -// // tag. -// int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd -// // word. -// // Scan forward until we find the bytes: 0x02011000(x00100102 in normal -// // int) -// for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) { -// if (LEW(xml, ii) == startTag) { -// xmlTagOff = ii; -// break; -// } -// } // end of hack, scanning for start of first start tag -// -// // XML tags and attributes: -// // Every XML start and end tag consists of 6 32 bit words: -// // 0th word: 02011000 for startTag and 03011000 for endTag -// // 1st word: a flag?, like 38000000 -// // 2nd word: Line of where this tag appeared in the original source file -// // 3rd word: FFFFFFFF ?? -// // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS -// // 5th word: StringIndex of Element Name -// // (Note: 01011000 in 0th word means end of XML document, endDocTag) -// -// // Start tags (not end tags) contain 3 more words: -// // 6th word: 14001400 meaning?? -// // 7th word: Number of Attributes that follow this tag(follow word 8th) -// // 8th word: 00000000 meaning?? -// -// // Attributes consist of 5 words: -// // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF -// // 1st word: StringIndex of Attribute Name -// // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId -// // used -// // 3rd word: Flags? -// // 4th word: str ind of attr value again, or ResourceId of value -// -// // TMP, dump string table to tr for debugging -// // tr.addSelect("strings", null); -// // for (int ii=0; ii"); -// prtIndent(indent, "<" + name + sb + ">"); -// indent++; -// -// } else if (tag0 == endTag) { // XML END TAG -// indent--; -// off += 6 * 4; // Skip over 6 words of endTag data -// String name = compXmlString(xml, sitOff, stOff, nameSi); -// finalXML.append(""); -// prtIndent(indent, " (line " + startTagLineNo -// + "-" + lineNo + ")"); -// // tr.parent(); // Step back up the NobTree -// -// } else if (tag0 == endDocTag) { // END OF XML DOC TAG -// break; -// -// } else { -// prt(" Unrecognized tag code '" + Integer.toHexString(tag0) -// + "' at offset " + off); -// break; -// } -// } // end of while loop scanning tags and attributes of XML tree -// //prt(" end at offset " + off); -// return finalXML.toString(); -// } // end of decompressXML -// -// public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) { -// if (strInd < 0) -// return null; -// int strOff = stOff + LEW(xml, sitOff + strInd * 4); -// return compXmlStringAt(xml, strOff); -// } -// -// public static String spaces = " "; -// -// public static void prtIndent(int indent, String str) { -// prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str); -// } -// -// // compXmlStringAt -- Return the string stored in StringTable format at -// // offset strOff. This offset points to the 16 bit string length, which -// // is followed by that number of 16 bit (Unicode) chars. -// public static String compXmlStringAt(byte[] arr, int strOff) { -// int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff; -// byte[] chars = new byte[strLen]; -// for (int ii = 0; ii < strLen; ii++) { -// chars[ii] = arr[strOff + 2 + ii * 2]; -// } -// return new String(chars); // Hack, just use 8 byte chars -// } // end of compXmlStringAt -// -// // LEW -- Return value of a Little Endian 32 bit word from the byte array -// // at offset off. -// public static int LEW(byte[] arr, int off) { -// return ((arr[off + 3] << 24) & 0xff000000) | -// ((arr[off + 2] << 16) & 0xff0000) | -// ((arr[off + 1] << 8) & 0xff00) | -// (arr[off] & 0xFF); -// } // end of LEW -// -// public static Document loadXMLFromString(String xml) throws Exception { -// DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); -// DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); -// return docBuilder.parse(new InputSource(new StringReader(xml))); -// } -// -// public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException { -// ZipFile zip = null; -// -// zip = new ZipFile(filePath); -// ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml"); -// ZipEntry classesDex = zip.getEntry("classes.dex"); -// -// // write dex file -// InputStream dexStream = zip.getInputStream(classesDex); -// try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) { -// byte[] buffer = new byte[1024]; -// int len; -// while ((len = dexStream.read(buffer)) > 0) { -// os.write(buffer, 0, len); -// } -// } -// -// // read xml file -// InputStream is = zip.getInputStream(androidManifest); -// byte[] buf = new byte[1024000]; // 100 kb -// is.read(buf); -// is.close(); -// zip.close(); -// -// String xml = APKExtractor.decompressXML(buf); -// try { -// Document xmlDoc = loadXMLFromString(xml); -// String pkg = xmlDoc.getDocumentElement().getAttribute("package"); -// NodeList nodes = xmlDoc.getElementsByTagName("meta-data"); -// for (int i = 0; i < nodes.getLength(); i++) { -// NamedNodeMap attributes = nodes.item(i).getAttributes(); -// System.out.println(attributes.getNamedItem("name").getNodeValue()); -// if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class")) -// return pkg + attributes.getNamedItem("value").getNodeValue(); -// } -// } catch (Exception e) { -// e.printStackTrace(); -// } -// return ""; -// } -// } +} \ No newline at end of file