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:
Hamza417
2024-12-27 22:18:13 +05:30
parent ba0c594f40
commit d6d01d5f9b
7 changed files with 149 additions and 42 deletions

1
.idea/misc.xml generated
View File

@ -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)" />

View File

@ -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'

View File

@ -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 //////////////////////////////////////////////////////////// -->

View File

@ -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;
}

View File

@ -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
}
}
}
}

View File

@ -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) {

View File

@ -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"
}
}