Skip to content

Commit

Permalink
feat: add Dex v41 resource support
Browse files Browse the repository at this point in the history
- Update ARSC library for compatibility
- Refactor code for resource handling
- build beta version for testing (actions)
  • Loading branch information
Hamza417 committed Dec 27, 2024
1 parent ba0c594 commit d6d01d5
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 42 deletions.
1 change: 0 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 11 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/assets/html/changelogs.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ <h4>Bug Fixes</h4>
<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 //////////////////////////////////////////////////////////// -->
Expand Down
48 changes: 18 additions & 30 deletions app/src/main/java/app/simple/inure/apk/decoders/XMLDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
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;

Expand All @@ -29,14 +28,17 @@

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;
}
Expand Down Expand Up @@ -82,18 +84,18 @@ private byte[] getFileData(String path) throws IOException {
}
}

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 {
Expand All @@ -102,7 +104,7 @@ public String decode(@NonNull byte[] data) throws IOException {
}

@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;
Expand All @@ -113,7 +115,7 @@ public String decode(@NonNull InputStream is) throws IOException {
}

@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) {
Expand All @@ -130,17 +132,17 @@ public static void decode(@NonNull ByteBuffer byteBuffer, @NonNull OutputStream
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);

Expand All @@ -163,16 +165,13 @@ public static void decode(@NonNull ByteBuffer byteBuffer, @NonNull OutputStream

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;
}
}
Expand All @@ -182,15 +181,6 @@ public static void decode(@NonNull ByteBuffer byteBuffer, @NonNull OutputStream
}
}

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)) {
Expand All @@ -200,13 +190,11 @@ private static String getNamespacePrefix(String prefix) {
}

@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;
}
84 changes: 84 additions & 0 deletions app/src/main/java/app/simple/inure/apk/parsers/APKParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
}
}
1 change: 1 addition & 0 deletions app/src/main/java/app/simple/inure/util/FileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -85,4 +101,8 @@ class XMLViewerViewModel(val packageInfo: PackageInfo,
}
}
}

companion object {
private const val TAG = "XMLViewerViewModel"
}
}

0 comments on commit d6d01d5

Please sign in to comment.