mirror of
https://github.com/Hamza417/Inure.git
synced 2025-08-06 18:49:41 +08:00
feat: add Dex v41 resource support
- Update ARSC library for compatibility - Refactor code for resource handling - build beta version for testing (actions)
This commit is contained in:
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.10 (Inure)" />
|
||||
|
@ -102,9 +102,9 @@ android {
|
||||
|
||||
// Remove permissions for beta build
|
||||
manifestPlaceholders = [
|
||||
removePermission: "inure.terminal.permission.RUN_SCRIPT",
|
||||
removePermission2: "inure.terminal.permission.APPEND_TO_PATH",
|
||||
removePermission3: "inure.terminal.permission.PREPEND_TO_PATH"
|
||||
removePermission : "inure.terminal.permission.RUN_SCRIPT",
|
||||
removePermission2: "inure.terminal.permission.APPEND_TO_PATH",
|
||||
removePermission3: "inure.terminal.permission.PREPEND_TO_PATH"
|
||||
]
|
||||
|
||||
// Include all github flavor files
|
||||
@ -175,7 +175,11 @@ android {
|
||||
dependencies {
|
||||
// compileOnly project(':hidden-api-stub')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.3'
|
||||
// Android Tools
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||
implementation 'com.android.tools.build:apksig:4.0.2'
|
||||
|
||||
// Jar Libs, all included in the libs folder
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
// Test
|
||||
@ -231,7 +235,9 @@ dependencies {
|
||||
implementation 'net.lingala.zip4j:zip4j:2.11.5'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
|
||||
implementation 'io.github.reandroid:ARSCLib:1.2.4'
|
||||
// TODO : Update to latest version when available
|
||||
// https://github.com/REAndroid/ARSCLib
|
||||
implementation 'com.github.REAndroid:ARSCLib:95af206081'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
|
||||
githubImplementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
|
@ -38,6 +38,15 @@
|
||||
<li>Fixed <b>Batch Trackers</b> not working on not-root modes.</li>
|
||||
</ul>
|
||||
|
||||
<h4>Improvements</h4>
|
||||
|
||||
<ul>
|
||||
<li>APK Parser is now compatible with <b>Dex v41</b> or at least can parse XML files from these
|
||||
APK
|
||||
files now.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<br/>
|
||||
|
||||
<!-- //////////////////////////////////////////////////////////// Older Versions //////////////////////////////////////////////////////////// -->
|
||||
|
@ -8,7 +8,6 @@ import com.reandroid.arsc.chunk.PackageBlock;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
|
||||
import com.reandroid.arsc.chunk.xml.ResXmlPullParser;
|
||||
import com.reandroid.arsc.io.BlockReader;
|
||||
import com.reandroid.xml.XMLDocument;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
@ -29,14 +28,17 @@ import app.simple.inure.util.IntegerUtils;
|
||||
|
||||
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
|
||||
import static org.xmlpull.v1.XmlPullParser.END_TAG;
|
||||
import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
|
||||
import static org.xmlpull.v1.XmlPullParser.START_TAG;
|
||||
|
||||
public class XMLDecoder {
|
||||
|
||||
private static PackageBlock frameworkPackageBlock;
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024 * 50;
|
||||
private final ZipFile zipFile;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
public XMLDecoder(ZipFile zipFile) {
|
||||
this.zipFile = zipFile;
|
||||
}
|
||||
@ -82,18 +84,18 @@ public class XMLDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBinaryXml(@NonNull ByteBuffer buffer) {
|
||||
public static boolean isBinaryXml(@NonNull ByteBuffer buffer) {
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.mark();
|
||||
int version = IntegerUtils.getUInt16(buffer);
|
||||
int header = IntegerUtils.getUInt16(buffer);
|
||||
buffer.reset();
|
||||
// 0x0000 is NULL header. The only example of application using a NULL header is NP Manager
|
||||
// 0x0000 is NULL header
|
||||
return (version == 0x0003 || version == 0x0000) && header == 0x0008;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String decode(@NonNull byte[] data) throws IOException {
|
||||
public static String decode(@NonNull byte[] data) throws IOException {
|
||||
if (isBinaryXml(ByteBuffer.wrap(data))) {
|
||||
return decode(ByteBuffer.wrap(data));
|
||||
} else {
|
||||
@ -102,7 +104,7 @@ public class XMLDecoder {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String decode(@NonNull InputStream is) throws IOException {
|
||||
public static String decode(@NonNull InputStream is) throws IOException {
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int n;
|
||||
@ -113,7 +115,7 @@ public class XMLDecoder {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String decode(@NonNull ByteBuffer byteBuffer) throws IOException {
|
||||
public static String decode(@NonNull ByteBuffer byteBuffer) throws IOException {
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
decode(byteBuffer, bos);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
@ -130,17 +132,17 @@ public class XMLDecoder {
|
||||
PrintStream out = new PrintStream(os)) {
|
||||
ResXmlDocument resXmlDocument = new ResXmlDocument();
|
||||
resXmlDocument.readBytes(reader);
|
||||
try (ResXmlPullParser parser = new ResXmlPullParser()) {
|
||||
parser.setCurrentPackage(getFrameworkPackageBlock());
|
||||
parser.setResXmlDocument(resXmlDocument);
|
||||
resXmlDocument.setPackageBlock(getFrameworkPackageBlock());
|
||||
|
||||
try (ResXmlPullParser parser = new ResXmlPullParser(resXmlDocument)) {
|
||||
StringBuilder indent = new StringBuilder(10);
|
||||
final String indentStep = " ";
|
||||
out.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
|
||||
XML_BUILDER:
|
||||
|
||||
while (true) {
|
||||
int type = parser.next();
|
||||
switch (type) {
|
||||
case START_TAG: {
|
||||
case START_TAG:
|
||||
out.printf("%s<%s%s", indent, getNamespacePrefix(parser.getPrefix()), parser.getName());
|
||||
indent.append(indentStep);
|
||||
|
||||
@ -163,16 +165,13 @@ public class XMLDecoder {
|
||||
|
||||
out.println(">");
|
||||
break;
|
||||
}
|
||||
case END_TAG: {
|
||||
case END_TAG:
|
||||
indent.setLength(indent.length() - indentStep.length());
|
||||
out.printf("%s</%s%s>%n", indent, getNamespacePrefix(parser.getPrefix()), parser.getName());
|
||||
break;
|
||||
}
|
||||
case END_DOCUMENT:
|
||||
break XML_BUILDER;
|
||||
case START_DOCUMENT:
|
||||
// Unreachable statement
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -182,15 +181,6 @@ public class XMLDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public static XMLDocument decodeToXml(@NonNull ByteBuffer byteBuffer) throws IOException {
|
||||
ResXmlDocument xmlBlock = new ResXmlDocument();
|
||||
try (BlockReader reader = new BlockReader(byteBuffer.array())) {
|
||||
xmlBlock.readBytes(reader);
|
||||
xmlBlock.setPackageBlock(getFrameworkPackageBlock());
|
||||
return xmlBlock.decodeToXml();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String getNamespacePrefix(String prefix) {
|
||||
if (TextUtils.isEmpty(prefix)) {
|
||||
@ -200,13 +190,11 @@ public class XMLDecoder {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static PackageBlock getFrameworkPackageBlock() throws IOException {
|
||||
static PackageBlock getFrameworkPackageBlock() {
|
||||
if (frameworkPackageBlock != null) {
|
||||
return frameworkPackageBlock;
|
||||
}
|
||||
frameworkPackageBlock = AndroidFrameworks.getLatest().getTableBlock().getAllPackages().next();
|
||||
return frameworkPackageBlock;
|
||||
}
|
||||
|
||||
private static PackageBlock frameworkPackageBlock;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package app.simple.inure.apk.parsers
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import androidx.annotation.Nullable
|
||||
import app.simple.inure.R
|
||||
import app.simple.inure.apk.utils.PackageUtils.safeApplicationInfo
|
||||
import app.simple.inure.constants.Extensions.isExtrasFile
|
||||
@ -12,11 +13,23 @@ import app.simple.inure.exceptions.DexClassesNotFoundException
|
||||
import app.simple.inure.models.Extra
|
||||
import app.simple.inure.models.Graphic
|
||||
import app.simple.inure.preferences.SearchPreferences
|
||||
import app.simple.inure.util.FileUtils
|
||||
import com.android.apksig.apk.ApkFormatException
|
||||
import com.android.apksig.apk.ApkUtils
|
||||
import com.android.apksig.apk.ApkUtils.ZipSections
|
||||
import com.android.apksig.internal.zip.CentralDirectoryRecord
|
||||
import com.android.apksig.internal.zip.LocalFileRecord
|
||||
import com.android.apksig.util.DataSource
|
||||
import com.android.apksig.util.DataSources
|
||||
import com.android.apksig.zip.ZipFormatException
|
||||
import net.dongliu.apk.parser.ApkFile
|
||||
import net.dongliu.apk.parser.bean.ApkMeta
|
||||
import net.dongliu.apk.parser.bean.DexClass
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.Enumeration
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
@ -30,6 +43,7 @@ object APKParser {
|
||||
private const val MIPS = "mips"
|
||||
private const val x86 = "x86"
|
||||
private const val x86_64 = "x86_64"
|
||||
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
|
||||
/**
|
||||
* Fetch the install location of an APK file
|
||||
@ -397,4 +411,74 @@ object APKParser {
|
||||
|
||||
return extraFiles
|
||||
}
|
||||
|
||||
@Throws(ApkParserException::class, IOException::class)
|
||||
fun getManifestByteBuffer(file: File): ByteBuffer {
|
||||
RandomAccessFile(file, FileUtils.FILE_MODE_READ).use { randomAccessFile ->
|
||||
val source: DataSource = DataSources.asDataSource(randomAccessFile)
|
||||
val zipSections: ZipSections = ApkUtils.findZipSections(source)
|
||||
val centralDirectoryRecords: List<CentralDirectoryRecord> = parseZipCentralDirectory(source, zipSections)
|
||||
val slicedSource = source.slice(0, zipSections.zipCentralDirectoryOffset)
|
||||
return extractAndroidManifest(centralDirectoryRecords, slicedSource)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ApkFormatException::class)
|
||||
fun parseZipCentralDirectory(apk: DataSource, sections: ZipSections): List<CentralDirectoryRecord> {
|
||||
val sizeBytes: Long = sections.zipCentralDirectorySizeBytes.checkSizeOrThis()
|
||||
val offset = sections.zipCentralDirectoryOffset
|
||||
val buffer: ByteBuffer = apk.getByteBuffer(offset, sizeBytes.toInt())
|
||||
.order(ByteOrder.LITTLE_ENDIAN)
|
||||
val expectedCdRecordCount = sections.zipCentralDirectoryRecordCount
|
||||
val records: MutableList<CentralDirectoryRecord> = ArrayList(expectedCdRecordCount)
|
||||
|
||||
for (i in 0 until expectedCdRecordCount) {
|
||||
val record: CentralDirectoryRecord = CentralDirectoryRecord.getRecord(buffer)
|
||||
|
||||
/**
|
||||
* ZIP entry ending with '/' is a directory entry. We are not interested in
|
||||
* directory entries.
|
||||
*/
|
||||
if (record.name.endsWith("/")) {
|
||||
continue
|
||||
} else {
|
||||
records.add(record)
|
||||
}
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ApkFormatException::class, ZipFormatException::class)
|
||||
private fun extractAndroidManifest(records: List<CentralDirectoryRecord>, logicalHeaderFileSection: DataSource): ByteBuffer {
|
||||
val androidManifestCentralDirectoryRecord: CentralDirectoryRecord = findCentralDirectoryRecord(records)
|
||||
?: throw ApkFormatException("$ANDROID_MANIFEST not found in APK's Central Directory")
|
||||
return ByteBuffer.wrap(LocalFileRecord.getUncompressedData(
|
||||
logicalHeaderFileSection, androidManifestCentralDirectoryRecord, logicalHeaderFileSection.size()))
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private fun findCentralDirectoryRecord(records: List<CentralDirectoryRecord>): CentralDirectoryRecord? {
|
||||
for (record in records) {
|
||||
if (ANDROID_MANIFEST == record.name) {
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun Long.checkSizeOrThis(): Long {
|
||||
when {
|
||||
this > Int.MAX_VALUE -> {
|
||||
throw ApkFormatException("ZIP Central Directory too large: $this bytes")
|
||||
}
|
||||
this < 0 -> {
|
||||
throw ApkFormatException("ZIP Central Directory negative size: $this bytes")
|
||||
}
|
||||
else -> {
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import java.util.zip.ZipOutputStream
|
||||
|
||||
object FileUtils {
|
||||
|
||||
const val FILE_MODE_READ = "r"
|
||||
private const val BUFFER = 1024
|
||||
|
||||
fun openFolder(context: Context, location: String) {
|
||||
|
@ -9,6 +9,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.simple.inure.apk.decoders.XMLDecoder
|
||||
import app.simple.inure.apk.parsers.APKParser
|
||||
import app.simple.inure.apk.utils.PackageUtils.safeApplicationInfo
|
||||
import app.simple.inure.extensions.viewmodels.WrappedViewModel
|
||||
import app.simple.inure.util.FileUtils.toFile
|
||||
@ -19,6 +20,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.zip.ZipException
|
||||
|
||||
class XMLViewerViewModel(val packageInfo: PackageInfo,
|
||||
private val pathToXml: String,
|
||||
@ -50,13 +54,25 @@ class XMLViewerViewModel(val packageInfo: PackageInfo,
|
||||
private fun getSpannedXml() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
val code: String = if (raw) {
|
||||
FileInputStream(File(pathToXml)).use {
|
||||
it.readTextSafely()
|
||||
val code: String = when {
|
||||
raw -> {
|
||||
FileInputStream(File(pathToXml)).use {
|
||||
it.readTextSafely()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
try {
|
||||
XMLDecoder(packageInfo.safeApplicationInfo.sourceDir.toFile())
|
||||
.decode(pathToXml)
|
||||
} catch (e: ZipException) {
|
||||
Log.e(TAG, "Error decoding XML", e)
|
||||
val byteBuffer: ByteBuffer = APKParser
|
||||
.getManifestByteBuffer(packageInfo.safeApplicationInfo.sourceDir.toFile())
|
||||
.order(ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
XMLDecoder.decode(byteBuffer)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d("XMLViewerViewModel", "getSpannedXml: $pathToXml")
|
||||
XMLDecoder(packageInfo.safeApplicationInfo.sourceDir.toFile()).decode(pathToXml)
|
||||
}
|
||||
|
||||
spanned.postValue(code.formatXML().getPrettyXML())
|
||||
@ -85,4 +101,8 @@ class XMLViewerViewModel(val packageInfo: PackageInfo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "XMLViewerViewModel"
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user