refactor: ApkDecoder & ApkBuilder overhaul (#3699)

* refactor: ApkDecoder & ApkBuilder overhaul

A major rewrite of ApkDecoder and ApkBuilder classes to make them managable.
Removed many instances of redundancy and improved syntaxed and indentation.

Modifying the stock Apktool source to our needs have become too difficult,
so I'm pushing the general (not specific to our needs) changes upstream.

I'd change a lot more, but I wanted to make sure all tests pass as expected,
despite some of them being wierd, outdated or unnecessary.

This also fixes certain files in META-INF being lost during recompile
when the -c/--copy-original option isn't used.

This has been tweaked and tested for several days and I vouch for its stablity.

* style: fix more redundancy

* style: fix more redundancy

* tweak: consistent case-sensitivity for cmd and options

* refactor: tracking unknownFiles via apkInfo is redundant

1) We take advantage of the fact that doNotCompress already tracks uncompressed files,
   including those separated into "unknown".
   With this change the "unknownFiles" is simply ignored, so it's backward-compatible
   with existing decoded APK dirs.
   Tweaked a few tests to match the removal of "unknownFiles".

2) Passing doNotCompress to AAPT is redundant, Apktool extracts the temp APK packed by
   AAPT to build/apk and then repackages it anyway, so it serves no purpose.

* refactor: fix minSdkVersion from baksmali + clean up more redundancy

* Regression: minSdkVersion inferred from baksmali was not stored properly.

* The arsc extension can be generalized for simplicity as seen in AOSP source.
https://cs.android.com/android/platform/superproject/main/+/main:external/deqp/scripts/android/build_apk.py;l=644?q=apk%20pack&ss=android%2Fplatform%2Fsuperproject%2Fmain:external%2F
  Note:
    NO_COMPRESS_EXT_PATTERN only collapses paths to a common extension.
    It does NOT force these extensions to be always uncompressed.
    doNotCompress is the one determining files/extensions that should be uncompressed.
  (no funcionality was changed)

* resourcesAreCompressed in apkInfo is redundant. It was only used in invokeAapt,
  but not ApkBuilder. Its value is also never set by Apktool, only read.
  Like with doNotCompress, passing any kind of compression rules to AAPT is pointless,
  since we don't use the temp APK packed by AAPT directly - it's extracted and repacked
  by ApkBuilder, where doNotCompress already determines whether resources.arsc should
  or should not be compressed in the final APK.
  (no funcionality was changed)

* style: optional args come after required args

* style: optional args come after required args

* style: sdkInfo as a normal field for consistency

* style: some formatting tweaks
This commit is contained in:
Igor Eisberg
2024-10-03 13:52:59 +03:00
committed by GitHub
parent c6bb75e540
commit 4de92a23ae
100 changed files with 1504 additions and 1598 deletions

View File

@ -22,6 +22,8 @@ import java.util.ArrayList;
import java.util.List;
public class AaptManager {
public static final int AAPT_VERSION_MIN = 1;
public static final int AAPT_VERSION_MAX = 2;
public static File getAapt2() throws BrutException {
return getAapt(2);
@ -31,59 +33,71 @@ public class AaptManager {
return getAapt(1);
}
private static File getAapt(Integer version) throws BrutException {
File aaptBinary;
String aaptVersion = getAaptBinaryName(version);
private static File getAapt(int version) throws BrutException {
String aaptName = getAaptBinaryName(version);
if (! OSDetection.is64Bit() && OSDetection.isMacOSX()) {
throw new BrutException("32 bit OS detected. No 32 bit binaries available.");
if (!OSDetection.is64Bit() && OSDetection.isMacOSX()) {
throw new BrutException(aaptName + " binaries are not available for 32-bit platform: " + OSDetection.returnOS());
}
// Set the 64 bit flag
aaptVersion += OSDetection.is64Bit() ? "_64" : "";
try {
if (OSDetection.isMacOSX()) {
aaptBinary = Jar.getResourceAsFile("/prebuilt/macosx/" + aaptVersion, AaptManager.class);
} else if (OSDetection.isUnix()) {
aaptBinary = Jar.getResourceAsFile("/prebuilt/linux/" + aaptVersion, AaptManager.class);
} else if (OSDetection.isWindows()) {
aaptBinary = Jar.getResourceAsFile("/prebuilt/windows/" + aaptVersion + ".exe", AaptManager.class);
} else {
throw new BrutException("Could not identify platform: " + OSDetection.returnOS());
}
} catch (BrutException ex) {
throw new BrutException(ex);
}
if (aaptBinary.setExecutable(true)) {
return aaptBinary;
}
throw new BrutException("Can't set aapt binary as executable");
}
public static String getAaptExecutionCommand(String aaptPath, File aapt) throws BrutException {
if (! aaptPath.isEmpty()) {
File aaptFile = new File(aaptPath);
if (aaptFile.canRead() && aaptFile.exists()) {
//noinspection ResultOfMethodCallIgnored
aaptFile.setExecutable(true);
return aaptFile.getPath();
} else {
throw new BrutException("binary could not be read: " + aaptFile.getAbsolutePath());
}
StringBuilder aaptPath = new StringBuilder("/prebuilt/");
if (OSDetection.isUnix()) {
aaptPath.append("linux");
} else if (OSDetection.isMacOSX()) {
aaptPath.append("macosx");
} else if (OSDetection.isWindows()) {
aaptPath.append("windows");
} else {
return aapt.getAbsolutePath();
throw new BrutException("Could not identify platform: " + OSDetection.returnOS());
}
aaptPath.append("/");
aaptPath.append(aaptName);
if (OSDetection.is64Bit()) {
aaptPath.append("_64");
}
if (OSDetection.isWindows()) {
aaptPath.append(".exe");
}
File aaptBinary = Jar.getResourceAsFile(aaptPath.toString(), AaptManager.class);
if (!aaptBinary.setExecutable(true)) {
throw new BrutException("Can't set aapt binary as executable");
}
return aaptBinary;
}
public static String getAaptBinaryName(int version) {
switch (version) {
case 2:
return "aapt2";
default:
return "aapt";
}
}
public static int getAaptVersion(String aaptLocation) throws BrutException {
return getAaptVersion(new File(aaptLocation));
public static int getAaptVersion(String aaptPath) throws BrutException {
return getAaptVersion(new File(aaptPath));
}
public static String getAaptBinaryName(Integer version) {
return "aapt" + (version == 2 ? "2" : "");
public static int getAaptVersion(File aaptBinary) throws BrutException {
if (!aaptBinary.isFile() || !aaptBinary.canRead()) {
throw new BrutException("Can't read aapt binary: " + aaptBinary.getAbsolutePath());
}
if (!aaptBinary.setExecutable(true)) {
throw new BrutException("Can't set aapt binary as executable: " + aaptBinary.getAbsolutePath());
}
List<String> cmd = new ArrayList<>();
cmd.add(aaptBinary.getAbsolutePath());
cmd.add("version");
String version = OS.execAndReturn(cmd.toArray(new String[0]));
if (version == null) {
throw new BrutException("Could not execute aapt binary at location: " + aaptBinary.getAbsolutePath());
}
return getAppVersionFromString(version);
}
public static int getAppVersionFromString(String version) throws BrutException {
@ -98,23 +112,19 @@ public class AaptManager {
throw new BrutException("aapt version could not be identified: " + version);
}
public static int getAaptVersion(File aapt) throws BrutException {
if (!aapt.isFile()) {
throw new BrutException("Could not identify aapt binary as executable.");
}
//noinspection ResultOfMethodCallIgnored
aapt.setExecutable(true);
List<String> cmd = new ArrayList<>();
cmd.add(aapt.getAbsolutePath());
cmd.add("version");
String version = OS.execAndReturn(cmd.toArray(new String[0]));
if (version == null) {
throw new BrutException("Could not execute aapt binary at location: " + aapt.getAbsolutePath());
public static String getAaptExecutionCommand(String aaptPath, File aaptBinary) throws BrutException {
if (aaptPath.isEmpty()) {
return aaptBinary.getAbsolutePath();
}
return getAppVersionFromString(version);
aaptBinary = new File(aaptPath);
if (!aaptBinary.isFile() || !aaptBinary.canRead()) {
throw new BrutException("Can't read aapt binary: " + aaptBinary.getAbsolutePath());
}
if (!aaptBinary.setExecutable(true)) {
throw new BrutException("Can't set aapt binary as executable: " + aaptBinary.getAbsolutePath());
}
return aaptBinary.getPath();
}
}

View File

@ -24,13 +24,9 @@ import org.apache.commons.io.IOUtils;
import java.io.*;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public class BrutIO {
public static void copyAndClose(InputStream in, OutputStream out)
throws IOException {
public static void copyAndClose(InputStream in, OutputStream out) throws IOException {
try {
IOUtils.copy(in, out);
} finally {
@ -68,23 +64,25 @@ public class BrutIO {
CRC32 crc = new CRC32();
int bytesRead;
byte[] buffer = new byte[8192];
while((bytesRead = input.read(buffer)) != -1) {
while ((bytesRead = input.read(buffer)) != -1) {
crc.update(buffer, 0, bytesRead);
}
return crc;
}
public static String sanitizeFilepath(final File directory, final String entry) throws IOException, BrutException {
if (entry.isEmpty()) {
public static String sanitizePath(File baseDir, String path)
throws InvalidUnknownFileException, RootUnknownFileException,
TraversalUnknownFileException, IOException {
if (path.isEmpty()) {
throw new InvalidUnknownFileException("Invalid Unknown File");
}
if (new File(entry).isAbsolute()) {
if (new File(path).isAbsolute()) {
throw new RootUnknownFileException("Absolute Unknown Files is not allowed");
}
final String canonicalDirPath = directory.getCanonicalPath() + File.separator;
final String canonicalEntryPath = new File(directory, entry).getCanonicalPath();
String canonicalDirPath = baseDir.getCanonicalPath() + File.separator;
String canonicalEntryPath = new File(baseDir, path).getCanonicalPath();
if (!canonicalEntryPath.startsWith(canonicalDirPath)) {
throw new TraversalUnknownFileException("Directory Traversal is not allowed");
@ -94,8 +92,11 @@ public class BrutIO {
return canonicalEntryPath.substring(canonicalDirPath.length());
}
public static boolean detectPossibleDirectoryTraversal(String entry) {
return entry.contains("../") || entry.contains("/..") || entry.contains("..\\") || entry.contains("\\..");
public static boolean detectPossibleDirectoryTraversal(String path) {
return path.contains("../")
|| path.contains("/..")
|| path.contains("..\\")
|| path.contains("\\..");
}
public static String adaptSeparatorToUnix(String path) {
@ -107,17 +108,4 @@ public class BrutIO {
return path;
}
public static void copy(File inputFile, ZipOutputStream outputFile) throws IOException {
try (FileInputStream fis = new FileInputStream(inputFile)) {
IOUtils.copy(fis, outputFile);
}
}
public static void copy(ZipFile inputFile, ZipOutputStream outputFile, ZipEntry entry) throws IOException {
try (InputStream is = inputFile.getInputStream(entry)) {
IOUtils.copy(is, outputFile);
}
}
}

View File

@ -126,7 +126,7 @@ public class OS {
System.err.println("Stream collector did not terminate.");
}
return collector.get();
} catch (IOException | InterruptedException e) {
} catch (IOException | InterruptedException ex) {
return null;
}
}
@ -149,8 +149,8 @@ public class OS {
static class StreamForwarder extends Thread {
StreamForwarder(InputStream is, String type) {
mIn = is;
StreamForwarder(InputStream in, String type) {
mIn = in;
mType = type;
}

View File

@ -39,7 +39,7 @@ public class OSDetection {
return arch != null && arch.endsWith("64") || wow64Arch != null && wow64Arch.endsWith("64");
}
return BIT.equalsIgnoreCase("64");
return BIT.equals("64");
}
public static String returnOS() {