diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index 2fe6ed30..c1234dd9 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -17,12 +17,12 @@ jobs: uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: - languages: java + languages: java-kotlin - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34fc409f..ab0f1d73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,10 +76,16 @@ jobs: with: distribution: 'zulu' java-version: ${{ matrix.java }} - - name: Build and test - uses: gradle/gradle-build-action@v2.10.0 - with: - arguments: build shadowJar proguard + + - uses: gradle/actions/setup-gradle@v4.2.1 + + - name: Build (Linux/Mac) + if: runner.os != 'Windows' + run: ./gradlew build shadowJar proguard + + - name: Build (Windows) + if: runner.os == 'Windows' + run: ./gradlew.bat build shadowJar proguard upload-artifact: runs-on: ubuntu-latest @@ -91,17 +97,26 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - - name: Build - uses: gradle/gradle-build-action@v2.10.0 + + - uses: gradle/actions/setup-gradle@v4.2.1 with: dependency-graph: generate-and-submit - arguments: build shadowJar proguard + + - name: Build (Linux/Mac) + if: runner.os != 'Windows' + run: ./gradlew build shadowJar proguard + + - name: Build (Windows) + if: runner.os == 'Windows' + run: ./gradlew.bat build shadowJar proguard + - name: Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: apktool.jar path: brut.apktool/apktool-cli/build/libs/apktool-v* diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index bb7aef32..5e70b211 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -21,4 +21,4 @@ jobs: with: distribution: 'zulu' java-version: 17 - - uses: gradle/wrapper-validation-action@v1.1.0 + - uses: gradle/actions/wrapper-validation@v4.2.1 diff --git a/INTERNAL.md b/INTERNAL.md index 13be348e..2b28968d 100644 --- a/INTERNAL.md +++ b/INTERNAL.md @@ -10,11 +10,11 @@ _Currently broken after movement to kotlin dsl._ Inside `build.gradle` there are two lines. - apktoolversion_major - apktoolversion_minor + version + suffix -The major variable should be left unchanged. If done correctly, it will already be the version -you are about to release. In this case `2.2.2`. The minor variable should read `SNAPSHOT` as +The version variable should be left unchanged. If done correctly, it will already be the version +you are about to release. In this case `2.2.2`. The suffix variable should read `SNAPSHOT` as the `2.2.2` release up until this point was `SNAPSHOT` releases (Unofficial). We need to remove the `SNAPSHOT` portion and leave the minor version blank. An example can be @@ -26,11 +26,11 @@ with the commit message - `version bump (x.x.x)`. At this point we now have the commit of the release, but we need to tag it using the following message. - git tag -a vx.x.x -m "changed version to vx.x.x" + git tag -a vx.x.x -m "changed version to vx.x.x" -s For example for the `2.2.1` release. - git tag -a v2.2.1 -m "changed version to v2.2.1" + git tag -a v2.2.1 -m "changed version to v2.2.1" -s ### Prepare for publishing. @@ -219,9 +219,12 @@ where the release post was and send that link to Twitter, Google and whatever el Relax and watch the bug tracker. -# Building aapt binaries. +# Building aapt2 binaries. -The steps taken for building our modified aapt binaries for apktool. +> [!WARNING] +> aapt (aapt1) is deprecated and no longer receives updates. + +The steps taken for building our modified aapt2 binaries for apktool. ### Getting the modified `frameworks/base` repo. First step is using the [platform_frameworks_base](https://github.com/iBotPeaches/platform_frameworks_base) repo. @@ -249,10 +252,10 @@ Some optimization techniques for a smaller clone: After that, you need to build AOSP via this [documentation](https://source.android.com/source/building.html) guide. Now we aren't building the entire AOSP package, the initial build is to just see if you are capable of building it. -We check out a certain tag or branch. Currently we use +We check out a certain tag or branch. Currently, we use - * aapt2 - `android-14.0.0_r2`. - * aapt1 - `android-14.0.0_r2`. + * aapt2 - `android-14.0.0_r54` + * aapt1 - `android-14.0.0_r2` (deprecated) ### Including our modified `frameworks/base` package. @@ -277,7 +280,7 @@ The steps below are different per flavor and operating system. #### Linux / Windows 1. `source build/envsetup.sh` -2. `lunch sdk-eng` +2. `lunch aosp_arm64-trunk_staging-eng` 3. `m aapt` 4. `strip out/host/linux-x86/bin/aapt` 5. `strip out/host/linux-x86/bin/aapt_64` @@ -296,17 +299,20 @@ The steps below are different per flavor and operating system. The steps below are different per flavor and operating system. #### Linux / Windows +1. `source build/envsetup.sh` +1. `lunch aosp_arm64-trunk_staging-eng` 1. `m aapt2` -2. `strip out/host/linux-x86/bin/aapt2` -3. `strip out/host/linux-x86/bin/aapt2_64` -4. `strip out/host/windows-x86/bin/aapt2.exe` -5. `strip out/host/windows-x86/bin/aapt2_64.exe` +1. `strip out/host/linux-x86/bin/aapt2` +1. `strip out/host/linux-x86/bin/aapt2_64` +1. `strip out/host/windows-x86/bin/aapt2.exe` +1. `strip out/host/windows-x86/bin/aapt2_64.exe` #### Mac 1. `export ANDROID_JAVA_HOME=/Path/To/Jdk` -2. `source build/envsetup.sh` -3. `m aapt2` -4. `strip out/host/darwin-x86/bin/aapt2_64` +1. `source build/envsetup.sh` +1. `lunch aosp_arm64-trunk_staging-eng` +1. `m aapt2` +1. `strip out/host/darwin-x86/bin/aapt2_64` #### Confirming aapt/aapt2 builds are static diff --git a/README.md b/README.md index b60a497a..2ad21cd7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ ### Apktool -**This is the repository for Apktool. If you are looking for the Apktool website. Click [here](https://github.com/iBotPeaches/Apktool/tree/docs).** +**This is the repository for Apktool. The website is on the [Apktool docs branch](https://github.com/iBotPeaches/Apktool/tree/docs).** [![CI](https://github.com/iBotPeaches/Apktool/actions/workflows/build.yml/badge.svg)](https://github.com/iBotPeaches/Apktool/actions/workflows/test.yml) -[![Software License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/iBotPeaches/Apktool/blob/master/LICENSE) +[![Software License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/iBotPeaches/Apktool/blob/master/LICENSE.md) -It is a tool for reverse engineering 3rd party, closed, binary Android apps. It can decode resources to nearly original form and rebuild them after making some modifications; it makes possible to debug smali code step by step. Also it makes working with app easier because of project-like files structure and automation of some repetitive tasks like building apk, etc. +Apktool is a tool for reverse engineering third-party, closed, binary, Android apps. It can decode resources to nearly original form and rebuild them after making some modifications; it makes it possible to debug smali code step-by-step. It also makes working with apps easier thanks to project-like file structure and automation of some repetitive tasks such as building apk, etc. -It is NOT intended for piracy and other non-legal uses. It could be used for localizing, adding some features or support for custom platforms and other GOOD purposes. Just try to be fair with authors of an app, that you use and probably like. +Apktool is **NOT** intended for piracy and other non-legal uses. It could be used for localizing and adding features, adding support for custom platforms, and other GOOD purposes. Just try to be fair with the authors of an app, that you use and probably like. #### Support - [Project Page](https://ibotpeaches.github.io/Apktool/) diff --git a/brut.apktool/apktool-cli/build.gradle.kts b/brut.apktool/apktool-cli/build.gradle.kts index f9070836..707435e2 100644 --- a/brut.apktool/apktool-cli/build.gradle.kts +++ b/brut.apktool/apktool-cli/build.gradle.kts @@ -1,25 +1,15 @@ -import proguard.gradle.ProGuardTask - val apktoolVersion: String by rootProject.extra plugins { - alias(libs.plugins.shadow) application } -// Buildscript is deprecated, but the alternative approach does not support expanded properties -// https://github.com/gradle/gradle/issues/9830 -// So we must hard-code the version here. -buildscript { - dependencies { - // Proguard doesn't support plugin DSL - https://github.com/Guardsquare/proguard/issues/225 - classpath(libs.proguard) - } -} +val r8: Configuration by configurations.creating dependencies { implementation(libs.commons.cli) implementation(project(":brut.apktool:apktool-lib")) + r8(libs.r8) } application { @@ -28,46 +18,73 @@ application { tasks.run.get().workingDir = file(System.getProperty("user.dir")) } -tasks.withType { - manifest { - attributes["Main-Class"] = "brut.apktool.Main" - } +tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true } tasks.register("cleanOutputDirectory") { delete(fileTree("build/libs") { + exclude("apktool-cli-sources.jar") + exclude("apktool-cli-javadoc.jar") exclude("apktool-cli-all.jar") }) } -tasks.register("proguard") { +val shadowJar = tasks.create("shadowJar", Jar::class) { + dependsOn("build") dependsOn("cleanOutputDirectory") - dependsOn("shadowJar") - injars(tasks.named("shadowJar").get().outputs.files) - val javaHome = System.getProperty("java.home") - if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { - libraryjars("$javaHome/lib/jce.jar") - libraryjars("$javaHome/lib/rt.jar") - } else { - libraryjars(mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"), - { - "$javaHome/jmods/" - } - ) + group = "build" + description = "Creates a single executable JAR with all dependencies" + manifest.attributes["Main-Class"] = "brut.apktool.Main" + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + val dependencies = configurations + .runtimeClasspath + .get() + .map(::zipTree) + + from(dependencies) + with(tasks.jar.get()) +} + +tasks.register("proguard") { + dependsOn("shadowJar") + + onlyIf { + JavaVersion.current().isJava11Compatible } - dontobfuscate() - dontoptimize() + val proguardRules = file("proguard-rules.pro") + val originalJar = shadowJar.outputs.files.singleFile - keep("class brut.apktool.Main { public static void main(java.lang.String[]); }") - keepclassmembers("enum * { public static **[] values(); public static ** valueOf(java.lang.String); }") - dontwarn("com.google.common.base.**") - dontwarn("com.google.common.collect.**") - dontwarn("com.google.common.util.**") - dontwarn("javax.xml.xpath.**") - dontnote("**") + inputs.files(originalJar.toString(), proguardRules) + outputs.file("build/libs/apktool-$apktoolVersion.jar") - val outPath = "build/libs/apktool-$apktoolVersion.jar" - outjars(outPath) + classpath(r8) + mainClass.set("com.android.tools.r8.R8") + + args = mutableListOf( + "--release", + "--classfile", + "--no-minification", + "--map-diagnostics:UnusedProguardKeepRuleDiagnostic", "info", "none", + "--lib", javaLauncher.get().metadata.installationPath.toString(), + "--output", outputs.files.singleFile.toString(), + "--pg-conf", proguardRules.toString(), + originalJar.toString() + ) +} + +tasks.withType { + dependsOn(tasks.named("shadowJar")) +} + +tasks.withType { + dependsOn(tasks.named("shadowJar")) +} + +tasks.withType { + dependsOn(tasks.named("shadowJar")) } diff --git a/brut.apktool/apktool-cli/proguard-rules.pro b/brut.apktool/apktool-cli/proguard-rules.pro new file mode 100644 index 00000000..5b6b4e34 --- /dev/null +++ b/brut.apktool/apktool-cli/proguard-rules.pro @@ -0,0 +1,11 @@ +-keep class brut.apktool.Main { + public static void main(java.lang.String[]); +} +-keepclassmembers enum * { + static **[] values(); + static ** valueOf(java.lang.String); +} + +# https://github.com/iBotPeaches/Apktool/pull/3670#issuecomment-2296326878 +-dontwarn com.google.j2objc.annotations.Weak +-dontwarn com.google.j2objc.annotations.RetainedWith diff --git a/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java b/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java index e10ed961..eec8bd9c 100644 --- a/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java +++ b/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java @@ -36,6 +36,19 @@ import java.util.logging.*; * Main entry point of the apktool. */ public class Main { + private enum Verbosity { NORMAL, VERBOSE, QUIET } + + private static final Options normalOptions = new Options(); + private static final Options decodeOptions = new Options(); + private static final Options buildOptions = new Options(); + private static final Options frameOptions = new Options(); + private static final Options allOptions = new Options(); + private static final Options emptyOptions = new Options(); + private static final Options emptyFrameworkOptions = new Options(); + private static final Options listFrameworkOptions = new Options(); + + private static boolean advanceMode = false; + public static void main(String[] args) throws BrutException { // headless @@ -88,24 +101,34 @@ public class Main { boolean cmdFound = false; for (String opt : commandLine.getArgs()) { - if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")) { - cmdDecode(commandLine, config); - cmdFound = true; - } else if (opt.equalsIgnoreCase("b") || opt.equalsIgnoreCase("build")) { - cmdBuild(commandLine, config); - cmdFound = true; - } else if (opt.equalsIgnoreCase("if") || opt.equalsIgnoreCase("install-framework")) { - cmdInstallFramework(commandLine, config); - cmdFound = true; - } else if (opt.equalsIgnoreCase("empty-framework-dir")) { - cmdEmptyFrameworkDirectory(commandLine, config); - cmdFound = true; - } else if (opt.equalsIgnoreCase("list-frameworks")) { - cmdListFrameworks(commandLine, config); - cmdFound = true; - } else if (opt.equalsIgnoreCase("publicize-resources")) { - cmdPublicizeResources(commandLine, config); - cmdFound = true; + switch (opt) { + case "d": + case "decode": + cmdDecode(commandLine, config); + cmdFound = true; + break; + case "b": + case "build": + cmdBuild(commandLine, config); + cmdFound = true; + break; + case "if": + case "install-framework": + cmdInstallFramework(commandLine, config); + cmdFound = true; + break; + case "empty-framework-dir": + cmdEmptyFrameworkDirectory(commandLine, config); + cmdFound = true; + break; + case "list-frameworks": + cmdListFrameworks(commandLine, config); + cmdFound = true; + break; + case "publicize-resources": + cmdPublicizeResources(commandLine, config); + cmdFound = true; + break; } } @@ -130,6 +153,9 @@ public class Main { if (cli.hasOption("api") || cli.hasOption("api-level")) { config.apiLevel = Integer.parseInt(cli.getOptionValue("api")); } + if (cli.hasOption("j") || cli.hasOption("jobs")) { + config.jobs = Integer.parseInt(cli.getOptionValue("j")); + } } private static void cmdDecode(CommandLine cli, Config config) throws AndrolibException { @@ -211,8 +237,7 @@ public class Main { } ExtFile apkFile = new ExtFile(apkName); - ApkDecoder decoder = new ApkDecoder(config, apkFile); - + ApkDecoder decoder = new ApkDecoder(apkFile, config); try { decoder.decode(outDir); } catch (OutDirExistsException ex) { @@ -227,23 +252,17 @@ public class Main { System.exit(1); } catch (CantFindFrameworkResException ex) { System.err - .println("Can't find framework resources for package of id: " + .println("Could not find framework resources for package of id: " + ex.getPkgId() + ". You must install proper " + "framework files, see project website for more info."); System.exit(1); - } catch (IOException ex) { - System.err.println("Could not modify file. Please ensure you have permission."); - System.exit(1); - } catch (DirectoryException ex) { - System.err.println("Could not modify internal dex files. Please ensure you have permission."); - System.exit(1); } } - private static void cmdBuild(CommandLine cli, Config config) { + private static void cmdBuild(CommandLine cli, Config config) throws AndrolibException { String[] args = cli.getArgs(); - String appDirName = args.length < 2 ? "." : args[1]; + String apkDirName = args.length < 2 ? "." : args[1]; // check for build options if (cli.hasOption("f") || cli.hasOption("force-all")) { @@ -259,7 +278,25 @@ public class Main { config.verbose = true; } if (cli.hasOption("a") || cli.hasOption("aapt")) { - config.aaptPath = cli.getOptionValue("a"); + if (cli.hasOption("use-aapt1") || cli.hasOption("use-aapt2")) { + System.err.println("You can only use one of -a/--aapt or --use-aapt1 or --use-aapt2."); + System.exit(1); + } + + try { + config.aaptBinary = new File(cli.getOptionValue("a")); + config.aaptVersion = AaptManager.getAaptVersion(config.aaptBinary); + } catch (BrutException ex) { + System.err.println(ex.getMessage()); + System.exit(1); + } + } else if (cli.hasOption("use-aapt1")) { + if (cli.hasOption("use-aapt2")) { + System.err.println("You can only use one of --use-aapt1 or --use-aapt2."); + System.exit(1); + } + + config.aaptVersion = 1; } if (cli.hasOption("c") || cli.hasOption("copy-original")) { config.copyOriginalFiles = true; @@ -267,13 +304,8 @@ public class Main { if (cli.hasOption("nc") || cli.hasOption("no-crunch")) { config.noCrunch = true; } - if (cli.hasOption("use-aapt1")) { - config.useAapt2 = false; - } - - if (cli.hasOption("use-aapt1") && cli.hasOption("use-aapt2")) { - System.err.println("You can only use one of --use-aapt1 or --use-aapt2."); - System.exit(1); + if (cli.hasOption("na") || cli.hasOption("no-apk")) { + config.noApk = true; } File outFile; @@ -283,21 +315,14 @@ public class Main { outFile = null; } - if (config.netSecConf && !config.useAapt2) { - System.err.println("-n / --net-sec-conf is only supported with --use-aapt2."); + if (config.netSecConf && config.aaptVersion == 1) { + System.err.println("-n / --net-sec-conf is not supported with legacy aapt."); System.exit(1); } - // try and build apk - try { - if (cli.hasOption("a") || cli.hasOption("aapt")) { - config.aaptVersion = AaptManager.getAaptVersion(cli.getOptionValue("a")); - } - new ApkBuilder(config, new ExtFile(appDirName)).build(outFile); - } catch (BrutException ex) { - System.err.println(ex.getMessage()); - System.exit(1); - } + ExtFile apkDir = new ExtFile(apkDirName); + ApkBuilder builder = new ApkBuilder(apkDir, config); + builder.build(outFile); } private static void cmdInstallFramework(CommandLine cli, Config config) throws AndrolibException { @@ -341,6 +366,13 @@ public class Main { .desc("Print advanced information.") .build(); + Option jobsOption = Option.builder("j") + .longOpt("jobs") + .hasArg() + .type(Integer.class) + .desc("Sets the number of threads to use.") + .build(); + Option noSrcOption = Option.builder("s") .longOpt("no-src") .desc("Do not decode sources.") @@ -373,7 +405,7 @@ public class Main { Option analysisOption = Option.builder("m") .longOpt("match-original") - .desc("Keep files to closest to original as possible (prevents rebuild).") + .desc("Keep files closest to original as possible (prevents rebuild).") .build(); Option apiLevelOption = Option.builder("api") @@ -452,13 +484,13 @@ public class Main { .build(); Option aapt1Option = Option.builder() - .longOpt("use-aapt1") - .desc("Use aapt binary instead of aapt2 during the build step.") - .build(); + .longOpt("use-aapt1") + .desc("Use aapt binary instead of aapt2 during the build step.") + .build(); Option aapt2Option = Option.builder() .longOpt("use-aapt2") - .desc("Use aapt2 binary instead of aapt during the build step.") + .desc("Use aapt2 binary instead of aapt during the build step. (default)") .build(); Option originalOption = Option.builder("c") @@ -471,6 +503,11 @@ public class Main { .desc("Disable crunching of resource files during the build step.") .build(); + Option noApkOption = Option.builder("na") + .longOpt("no-apk") + .desc("Disable repacking of the built files into a new apk.") + .build(); + Option tagOption = Option.builder("t") .longOpt("tag") .desc("Tag frameworks using .") @@ -482,7 +519,7 @@ public class Main { .longOpt("output") .desc("The name of apk that gets written. (default: dist/name.apk)") .hasArg(true) - .argName("dir") + .argName("file") .build(); Option outputDecOption = Option.builder("o") @@ -502,6 +539,7 @@ public class Main { // check for advance mode if (isAdvanceMode()) { + decodeOptions.addOption(jobsOption); decodeOptions.addOption(noDbgOption); decodeOptions.addOption(keepResOption); decodeOptions.addOption(analysisOption); @@ -511,6 +549,7 @@ public class Main { decodeOptions.addOption(forceManOption); decodeOptions.addOption(resolveResModeOption); + buildOptions.addOption(jobsOption); buildOptions.addOption(apiLevelOption); buildOptions.addOption(debugBuiOption); buildOptions.addOption(netSecConfOption); @@ -518,6 +557,7 @@ public class Main { buildOptions.addOption(originalOption); buildOptions.addOption(aapt1Option); buildOptions.addOption(noCrunchOption); + buildOptions.addOption(noApkOption); } // add global options @@ -561,6 +601,7 @@ public class Main { for (Option op : frameOptions.getOptions()) { allOptions.addOption(op); } + allOptions.addOption(jobsOption); allOptions.addOption(apiLevelOption); allOptions.addOption(analysisOption); allOptions.addOption(debugDecOption); @@ -578,6 +619,7 @@ public class Main { allOptions.addOption(aapt1Option); allOptions.addOption(aapt2Option); allOptions.addOption(noCrunchOption); + allOptions.addOption(noApkOption); allOptions.addOption(onlyMainClassesOption); } @@ -662,8 +704,8 @@ public class Main { } } } - } catch (Exception exception) { - reportError(null, exception, ErrorManager.FORMAT_FAILURE); + } catch (Exception ex) { + reportError(null, ex, ErrorManager.FORMAT_FAILURE); } } @@ -688,31 +730,4 @@ public class Main { private static void setAdvanceMode() { Main.advanceMode = true; } - - private enum Verbosity { - NORMAL, VERBOSE, QUIET - } - - private static boolean advanceMode = false; - - private final static Options normalOptions; - private final static Options decodeOptions; - private final static Options buildOptions; - private final static Options frameOptions; - private final static Options allOptions; - private final static Options emptyOptions; - private final static Options emptyFrameworkOptions; - private final static Options listFrameworkOptions; - - static { - //normal and advance usage output - normalOptions = new Options(); - buildOptions = new Options(); - decodeOptions = new Options(); - frameOptions = new Options(); - allOptions = new Options(); - emptyOptions = new Options(); - emptyFrameworkOptions = new Options(); - listFrameworkOptions = new Options(); - } } diff --git a/brut.apktool/apktool-lib/build.gradle.kts b/brut.apktool/apktool-lib/build.gradle.kts index ab393934..ced180a5 100644 --- a/brut.apktool/apktool-lib/build.gradle.kts +++ b/brut.apktool/apktool-lib/build.gradle.kts @@ -1,25 +1,10 @@ val gitRevision: String by rootProject.extra val apktoolVersion: String by rootProject.extra -// region Determine Android SDK location - -val sdkRoot: String? = System.getenv("ANDROID_SDK_ROOT") -val androidJarPath: String = if (sdkRoot == null) { - GradleException("Missing ANDROID_SDK_ROOT").printStackTrace() - - "com.google.android:android:4.1.1.4" -} else { - val androidVersion = 33 - File("$sdkRoot/platforms/android-$androidVersion/android.jar").path -} - -// endregion - tasks { processResources { - from("src/main/resources/properties") { - include("**/*.properties") - into("properties") + from("src/main/resources") { + include("apktool.properties") expand("version" to apktoolVersion, "gitrev" to gitRevision) duplicatesStrategy = DuplicatesStrategy.INCLUDE } @@ -39,13 +24,13 @@ tasks { } dependencies { - api(project(":brut.j.dir")) - api(project(":brut.j.util")) api(project(":brut.j.common")) + api(project(":brut.j.util")) + api(project(":brut.j.dir")) + api(project(":brut.j.xml")) implementation(libs.baksmali) implementation(libs.smali) - implementation(libs.xmlpull) implementation(libs.guava) implementation(libs.commons.lang3) implementation(libs.commons.io) @@ -54,5 +39,15 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.xmlunit) - compileOnly(files(androidJarPath)) + val sdkRoot = System.getenv("ANDROID_HOME") + compileOnly( + if (sdkRoot == null) { + GradleException("Missing ANDROID_HOME").printStackTrace() + + "com.google.android:android:4.1.1.4" + } else { + val androidVersion = 33 + files("$sdkRoot/platforms/android-$androidVersion/android.jar") + } + ) } diff --git a/brut.apktool/apktool-lib/src/main/java/android/content/res/XmlResourceParser.java b/brut.apktool/apktool-lib/src/main/java/android/content/res/XmlResourceParser.java index 3e8d70f8..43685068 100644 --- a/brut.apktool/apktool-lib/src/main/java/android/content/res/XmlResourceParser.java +++ b/brut.apktool/apktool-lib/src/main/java/android/content/res/XmlResourceParser.java @@ -27,7 +27,7 @@ import org.xmlpull.v1.XmlPullParser; public interface XmlResourceParser extends XmlPullParser, AttributeSet { /** * Close this interface to the resource. Calls on the interface are no - * longer value after this call. + * longer valid after this call. */ void close(); } diff --git a/brut.apktool/apktool-lib/src/main/java/android/util/TypedValue.java b/brut.apktool/apktool-lib/src/main/java/android/util/TypedValue.java index 77d75bf6..03549609 100644 --- a/brut.apktool/apktool-lib/src/main/java/android/util/TypedValue.java +++ b/brut.apktool/apktool-lib/src/main/java/android/util/TypedValue.java @@ -119,7 +119,7 @@ public class TypedValue { /** Identifies the end of plain integer values. */ public static final int TYPE_LAST_INT = 0x1f; - /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ /** Complex data: bit location of unit information. */ public static final int COMPLEX_UNIT_SHIFT = 0; @@ -182,7 +182,7 @@ public class TypedValue { */ public static final int COMPLEX_MANTISSA_MASK = 0xffffff; - /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ /** * {@link #TYPE_NULL} data indicating the value was not specified. @@ -207,7 +207,7 @@ public class TypedValue { */ public static final int DENSITY_NONE = 0xffff; - /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ /** * The type held by this value, as defined by the constants here. This tells @@ -216,7 +216,7 @@ public class TypedValue { public int type; private static final float MANTISSA_MULT = 1.0f / (1 << TypedValue.COMPLEX_MANTISSA_SHIFT); - private static final float[] RADIX_MULTS = new float[] { + private static final float[] RADIX_MULTS = { MANTISSA_MULT, 1.0f / (1 << 7) * MANTISSA_MULT, 1.0f / (1 << 15) * MANTISSA_MULT, 1.0f / (1 << 23) * MANTISSA_MULT }; @@ -237,9 +237,10 @@ public class TypedValue { & TypedValue.COMPLEX_RADIX_MASK]; } - private static final String[] DIMENSION_UNIT_STRS = new String[] { "px", - "dip", "sp", "pt", "in", "mm" }; - private static final String[] FRACTION_UNIT_STRS = new String[] { "%", "%p" }; + private static final String[] DIMENSION_UNIT_STRS = { + "px", "dip", "sp", "pt", "in", "mm" + }; + private static final String[] FRACTION_UNIT_STRS = { "%", "%p" }; /** * Perform type conversion as per coerceToString on an explicitly diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/AaptInvoker.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/AaptInvoker.java index b8385c66..b9b4124a 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/AaptInvoker.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/AaptInvoker.java @@ -16,8 +16,8 @@ */ package brut.androlib; -import brut.androlib.exceptions.AndrolibException; import brut.androlib.apk.ApkInfo; +import brut.androlib.exceptions.AndrolibException; import brut.common.BrutException; import brut.util.AaptManager; import brut.util.OS; @@ -27,57 +27,52 @@ import java.util.*; import java.util.logging.Logger; public class AaptInvoker { + private static final Logger LOGGER = Logger.getLogger(AaptInvoker.class.getName()); + private final Config mConfig; private final ApkInfo mApkInfo; - private final static Logger LOGGER = Logger.getLogger(AaptInvoker.class.getName()); - public AaptInvoker(Config config, ApkInfo apkInfo) { mConfig = config; mApkInfo = apkInfo; } - private File getAaptBinaryFile() throws AndrolibException { - try { - if (getAaptVersion() == 2) { - return AaptManager.getAapt2(); + public void invoke(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include) + throws AndrolibException { + File aaptBinary = mConfig.aaptBinary; + + List cmd = new ArrayList<>(); + String aaptPath; + boolean customAapt; + + if (mConfig.aaptBinary != null) { + aaptPath = mConfig.aaptBinary.getPath(); + customAapt = true; + } else { + try { + aaptPath = AaptManager.getAaptBinary(mConfig.aaptVersion).getPath(); + customAapt = false; + } catch (BrutException ex) { + aaptPath = AaptManager.getAaptName(mConfig.aaptVersion); + customAapt = true; + LOGGER.warning(aaptPath + ": " + ex.getMessage() + " (defaulting to $PATH binary)"); } - return AaptManager.getAapt1(); - } catch (BrutException ex) { - throw new AndrolibException(ex); - } - } - - private int getAaptVersion() { - return mConfig.isAapt2() ? 2 : 1; - } - - private File createDoNotCompressExtensionsFile(ApkInfo apkInfo) throws AndrolibException { - if (apkInfo.doNotCompress == null || apkInfo.doNotCompress.isEmpty()) { - return null; } - File doNotCompressFile; - try { - doNotCompressFile = File.createTempFile("APKTOOL", null); - doNotCompressFile.deleteOnExit(); + cmd.add(aaptPath); - BufferedWriter fileWriter = new BufferedWriter(new FileWriter(doNotCompressFile)); - for (String extension : apkInfo.doNotCompress) { - fileWriter.write(extension); - fileWriter.newLine(); - } - fileWriter.close(); - - return doNotCompressFile; - } catch (IOException ex) { - throw new AndrolibException(ex); + switch (mConfig.aaptVersion) { + case 2: + invokeAapt2(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt); + break; + default: + invokeAapt1(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt); + break; } } private void invokeAapt2(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include, List cmd, boolean customAapt) throws AndrolibException { - List compileCommand = new ArrayList<>(cmd); File resourcesZip = null; @@ -91,7 +86,6 @@ public class AaptInvoker { } if (resDir != null && !resourcesZip.exists()) { - // Compile the files into flat arsc files cmd.add("compile"); @@ -135,7 +129,9 @@ public class AaptInvoker { cmd.add("-o"); cmd.add(apkFile.getAbsolutePath()); - if (mApkInfo.packageInfo.forcedPackageId != null && ! mApkInfo.sharedLibrary) { + if (mApkInfo.packageInfo.forcedPackageId != null && !mApkInfo.packageInfo.forcedPackageId.equals("1") + && !mApkInfo.sharedLibrary) { + cmd.add("--allow-reserved-package-id"); cmd.add("--package-id"); cmd.add(mApkInfo.packageInfo.forcedPackageId); } @@ -178,8 +174,6 @@ public class AaptInvoker { cmd.add("--no-version-transitions"); cmd.add("--no-resource-deduping"); - cmd.add("--allow-reserved-package-id"); - // TODO: Add this back, once AAPT2 from platform-tools 34.0.4 is stable // cmd.add("--no-compile-sdk-metadata"); @@ -190,27 +184,21 @@ public class AaptInvoker { cmd.add("--enable-sparse-encoding"); } + if (mApkInfo.compactEntries) { + cmd.add("--enable-compact-entries"); + } + if (mApkInfo.isFrameworkApk) { cmd.add("-x"); } - if (mApkInfo.doNotCompress != null && !customAapt) { - // Use custom -e option to avoid limits on commandline length. - // Can only be used when custom aapt binary is not used. - String extensionsFilePath = - Objects.requireNonNull(createDoNotCompressExtensionsFile(mApkInfo)).getAbsolutePath(); - cmd.add("-e"); - cmd.add(extensionsFilePath); - } else if (mApkInfo.doNotCompress != null) { - for (String file : mApkInfo.doNotCompress) { - cmd.add("-0"); - cmd.add(file); + if (!mApkInfo.featureFlags.isEmpty()) { + List featureFlags = new ArrayList<>(); + for (Map.Entry entry : mApkInfo.featureFlags.entrySet()) { + featureFlags.add(entry.getKey() + "=" + entry.getValue()); } - } - - if (!mApkInfo.resourcesAreCompressed) { - cmd.add("-0"); - cmd.add("arsc"); + cmd.add("--feature-flags"); + cmd.add(String.join(",", featureFlags)); } if (include != null) { @@ -252,7 +240,6 @@ public class AaptInvoker { private void invokeAapt1(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include, List cmd, boolean customAapt) throws AndrolibException { - cmd.add("p"); if (mConfig.verbose) { // output aapt verbose @@ -269,7 +256,7 @@ public class AaptInvoker { } // force package id so that some frameworks build with correct id // disable if user adds own aapt (can't know if they have this feature) - if (mApkInfo.packageInfo.forcedPackageId != null && ! customAapt && ! mApkInfo.sharedLibrary) { + if (mApkInfo.packageInfo.forcedPackageId != null && !mApkInfo.sharedLibrary && !customAapt) { cmd.add("--forced-package-id"); cmd.add(mApkInfo.packageInfo.forcedPackageId); } @@ -316,25 +303,6 @@ public class AaptInvoker { cmd.add("-x"); } - if (mApkInfo.doNotCompress != null && !customAapt) { - // Use custom -e option to avoid limits on commandline length. - // Can only be used when custom aapt binary is not used. - String extensionsFilePath = - Objects.requireNonNull(createDoNotCompressExtensionsFile(mApkInfo)).getAbsolutePath(); - cmd.add("-e"); - cmd.add(extensionsFilePath); - } else if (mApkInfo.doNotCompress != null) { - for (String file : mApkInfo.doNotCompress) { - cmd.add("-0"); - cmd.add(file); - } - } - - if (!mApkInfo.resourcesAreCompressed) { - cmd.add("-0"); - cmd.add("arsc"); - } - if (include != null) { for (File file : include) { cmd.add("-I"); @@ -364,36 +332,4 @@ public class AaptInvoker { throw new AndrolibException(ex); } } - - public void invokeAapt(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include) - throws AndrolibException { - - String aaptPath = mConfig.aaptPath; - // Mock using the included AAPT binary instead of a custom one. - // This is necessary, otherwise extension of every file from doNotCompress will be specified in the AAPT command - // which causes builds to fail. - boolean customAapt = false; // !aaptPath.isEmpty(); - List cmd = new ArrayList<>(); - - try { - // Instead of AaptManager.getAaptExecutionCommand(aaptPath, getAaptBinaryFile()); - // it is needed to use the following command, because getAaptBinaryFile() - // may throw BrutException even when not used by AaptManager.getAaptExecutionCommand - File aaptFile; - if (aaptPath.isEmpty() || !(aaptFile = new File(aaptPath)).exists()) - aaptFile = getAaptBinaryFile(); - String aaptCommand = aaptFile.getPath(); - - cmd.add(aaptCommand); - } catch (BrutException ex) { - LOGGER.warning("aapt: " + ex.getMessage() + " (defaulting to $PATH binary)"); - cmd.add(AaptManager.getAaptBinaryName(getAaptVersion())); - } - - if (mConfig.isAapt2()) { - invokeAapt2(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt); - return; - } - invokeAapt1(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt); - } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkBuilder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkBuilder.java index 34febb7f..914797f7 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkBuilder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkBuilder.java @@ -21,7 +21,7 @@ import brut.androlib.apk.ApkInfo; import brut.androlib.apk.UsesFramework; import brut.androlib.res.Framework; import brut.androlib.res.data.ResConfigFlags; -import brut.androlib.res.xml.ResXmlPatcher; +import brut.androlib.res.xml.ResXmlUtils; import brut.androlib.src.SmaliBuilder; import brut.common.BrutException; import brut.common.InvalidUnknownFileException; @@ -31,10 +31,10 @@ import brut.directory.Directory; import brut.directory.DirectoryException; import brut.directory.ExtFile; import brut.directory.ZipUtils; +import brut.util.AaptManager; import brut.util.BrutIO; import brut.util.OS; import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; @@ -42,134 +42,149 @@ import javax.xml.transform.TransformerException; import java.io.*; import java.nio.file.Files; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; public class ApkBuilder { - private final static Logger LOGGER = Logger.getLogger(ApkBuilder.class.getName()); + private static final Logger LOGGER = Logger.getLogger(ApkBuilder.class.getName()); - private final Config mConfig; private final ExtFile mApkDir; - private ApkInfo mApkInfo; - private int mMinSdkVersion = 0; + private final Config mConfig; + private final AtomicReference mBuildError; - private final static String APK_DIRNAME = "build/apk"; - private final static String UNK_DIRNAME = "unknown"; - private final static String[] APK_RESOURCES_FILENAMES = new String[] { - "resources.arsc", "AndroidManifest.xml", "res", "r", "R" }; - private final static String[] APK_RESOURCES_WITHOUT_RES_FILENAMES = new String[] { - "resources.arsc", "AndroidManifest.xml" }; - private final static String[] APP_RESOURCES_FILENAMES = new String[] { - "AndroidManifest.xml", "res" }; - private final static String[] APK_MANIFEST_FILENAMES = new String[] { - "AndroidManifest.xml" }; + private ApkInfo mApkInfo; + private int mMinSdkVersion; + private BackgroundWorker mWorker; public ApkBuilder(ExtFile apkDir) { - this(Config.getDefaultConfig(), apkDir); + this(apkDir, Config.getDefaultConfig()); } - public ApkBuilder(Config config, ExtFile apkDir) { - mConfig = config; + public ApkBuilder(ExtFile apkDir, Config config) { mApkDir = apkDir; + mConfig = config; + mBuildError = new AtomicReference<>(null); } - public void build(File outFile) throws BrutException { - LOGGER.info("Using Apktool " + ApktoolProperties.getVersion()); - - mApkInfo = ApkInfo.load(mApkDir); - - if (mApkInfo.getSdkInfo() != null && mApkInfo.getSdkInfo().get("minSdkVersion") != null) { - String minSdkVersion = mApkInfo.getSdkInfo().get("minSdkVersion"); - mMinSdkVersion = mApkInfo.getMinSdkVersionFromAndroidCodename(minSdkVersion); + public void build(File outApk) throws AndrolibException { + if (mConfig.jobs > 1) { + mWorker = new BackgroundWorker(mConfig.jobs - 1); } - - if (outFile == null) { - String outFileName = mApkInfo.apkFileName; - outFile = new File(mApkDir, "dist" + File.separator + (outFileName == null ? "out.apk" : outFileName)); - } - - //noinspection ResultOfMethodCallIgnored - new File(mApkDir, APK_DIRNAME).mkdirs(); - File manifest = new File(mApkDir, "AndroidManifest.xml"); - File manifestOriginal = new File(mApkDir, "AndroidManifest.xml.orig"); - - buildSources(); - buildNonDefaultSources(); - buildManifestFile(manifest, manifestOriginal); - buildResources(); - buildLibs(); - buildCopyOriginalFiles(); - buildApk(outFile); - - // we must go after the Apk is built, and copy the files in via Zip - // this is because Aapt won't add files it doesn't know (ex unknown files) - buildUnknownFiles(outFile); - - // we copied the AndroidManifest.xml to AndroidManifest.xml.orig so we can edit it - // lets restore the unedited one, to not change the original - if (manifest.isFile() && manifest.exists() && manifestOriginal.isFile()) { - try { - if (new File(mApkDir, "AndroidManifest.xml").delete()) { - FileUtils.moveFile(manifestOriginal, manifest); - } - } catch (IOException ex) { - throw new AndrolibException(ex.getMessage()); - } - } - LOGGER.info("Built apk into: " + outFile.getPath()); - } - - private void buildManifestFile(File manifest, File manifestOriginal) throws AndrolibException { - // If we decoded in "raw", we cannot patch AndroidManifest - if (new File(mApkDir, "resources.arsc").exists()) { - return; - } - if (manifest.isFile() && manifest.exists()) { - try { - if (manifestOriginal.exists()) { - //noinspection ResultOfMethodCallIgnored - manifestOriginal.delete(); - } - FileUtils.copyFile(manifest, manifestOriginal); - ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifest); - } catch (IOException ex) { - throw new AndrolibException(ex.getMessage()); - } - } - } - - private void buildSources() throws AndrolibException { - if (!buildSourcesRaw("classes.dex") && !buildSourcesSmali("smali", "classes.dex")) { - LOGGER.warning("Could not find sources"); - } - } - - private void buildNonDefaultSources() throws AndrolibException { try { - // loop through any smali_ directories for multi-dex apks - Map dirs = mApkDir.getDirectory().getDirs(); - for (Map.Entry directory : dirs.entrySet()) { - String name = directory.getKey(); - if (name.startsWith("smali_")) { - String filename = name.substring(name.indexOf("_") + 1) + ".dex"; + mApkInfo = ApkInfo.load(mApkDir); - if (!buildSourcesRaw(filename) && !buildSourcesSmali(name, filename)) { - LOGGER.warning("Could not find sources"); + String minSdkVersion = mApkInfo.getMinSdkVersion(); + if (minSdkVersion != null) { + mMinSdkVersion = mApkInfo.getMinSdkVersionFromAndroidCodename(minSdkVersion); + } + + if (outApk == null) { + String outFileName = mApkInfo.apkFileName; + if (outFileName == null) { + outFileName = "out.apk"; + } + outApk = new File(mApkDir, "dist" + File.separator + outFileName); + } + + File outDir = new File(mApkDir, "build" + File.separator + "apk"); + //noinspection ResultOfMethodCallIgnored + outDir.mkdirs(); + + File manifest = new File(mApkDir, "AndroidManifest.xml"); + File manifestOrig = new File(mApkDir, "AndroidManifest.xml.orig"); + + LOGGER.info("Using Apktool " + ApktoolProperties.getVersion() + " on " + outApk.getName() + + (mWorker != null ? " with " + mConfig.jobs + " threads" : "")); + + buildSources(outDir); + backupManifestFile(manifest, manifestOrig); + buildResources(outDir, manifest); + + if (mWorker != null) { + mWorker.waitForFinish(); + if (mBuildError.get() != null) { + throw mBuildError.get(); + } + } + + if (!mConfig.noApk) { + if (outApk.exists()) { + //noinspection ResultOfMethodCallIgnored + outApk.delete(); + } else { + File parentDir = outApk.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + //noinspection ResultOfMethodCallIgnored + parentDir.mkdirs(); + } + } + + copyOriginalFiles(outDir); + + LOGGER.info("Building apk file..."); + + try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(outApk.toPath()))) { + // zip aapt output files + try { + ZipUtils.zipDir(outDir, out, mApkInfo.doNotCompress); + } catch (IOException ex) { + throw new AndrolibException(ex); + } + + // zip remaining standard files + importRawFiles(out); + + // zip unknown files + importUnknownFiles(out); + } catch (IOException ex) { + throw new AndrolibException(ex); + } + + LOGGER.info("Built apk into: " + outApk.getPath()); + } + + // we copied the AndroidManifest.xml to AndroidManifest.xml.orig so we can edit it + // lets restore the unedited one, to not change the original + if (manifest.isFile() && manifestOrig.isFile()) { + try { + if (manifest.delete()) { + FileUtils.moveFile(manifestOrig, manifest); + } + } catch (IOException ex) { + throw new AndrolibException(ex); + } + } + } finally { + if (mWorker != null) { + mWorker.shutdownNow(); + } + } + } + + private void buildSources(File outDir) throws AndrolibException { + if (!copySourcesRaw(outDir, "classes.dex")) { + buildSourcesSmali(outDir, "smali", "classes.dex"); + } + + try { + Directory in = mApkDir.getDirectory(); + + // loop through any smali_ directories for multi-dex apks + for (String dirName : in.getDirs().keySet()) { + if (dirName.startsWith("smali_")) { + String fileName = dirName.substring(dirName.indexOf("_") + 1) + ".dex"; + if (!copySourcesRaw(outDir, fileName)) { + buildSourcesSmali(outDir, dirName, fileName); } } } // loop through any classes#.dex files for multi-dex apks - File[] dexFiles = mApkDir.listFiles(); - if (dexFiles != null) { - for (File dex : dexFiles) { - // skip classes.dex because we have handled it in buildSources() - if (dex.getName().endsWith(".dex") && !dex.getName().equalsIgnoreCase("classes.dex")) { - buildSourcesRaw(dex.getName()); - } + for (String fileName : in.getFiles()) { + // skip classes.dex because we have handled it + if (fileName.endsWith(".dex") && !fileName.equals("classes.dex")) { + copySourcesRaw(outDir, fileName); } } } catch (DirectoryException ex) { @@ -177,365 +192,302 @@ public class ApkBuilder { } } - private boolean buildSourcesRaw(String filename) throws AndrolibException { - File working = new File(mApkDir, filename); - if (!working.exists()) { + private boolean copySourcesRaw(File outDir, String fileName) throws AndrolibException { + File working = new File(mApkDir, fileName); + if (!working.isFile()) { return false; } - File stored = new File(mApkDir, APK_DIRNAME + "/" + filename); - if (mConfig.forceBuildAll || isModified(working, stored)) { - LOGGER.info("Copying " + mApkDir.toString() + " " + filename + " file..."); + + File stored = new File(outDir, fileName); + if (!mConfig.forceBuildAll && !isModified(working, stored)) { + return true; + } + + LOGGER.info("Copying raw " + fileName + " file..."); + try { + BrutIO.copyAndClose(Files.newInputStream(working.toPath()), Files.newOutputStream(stored.toPath())); + } catch (IOException ex) { + throw new AndrolibException(ex); + } + return true; + } + + private void buildSourcesSmali(File outDir, String dirName, String fileName) throws AndrolibException { + if (mWorker != null) { + mWorker.submit(() -> { + if (mBuildError.get() == null) { + try { + buildSourcesSmaliJob(outDir, dirName, fileName); + } catch (AndrolibException ex) { + mBuildError.compareAndSet(null, ex); + } + } + }); + } else { + buildSourcesSmaliJob(outDir, dirName, fileName); + } + } + + private void buildSourcesSmaliJob(File outDir, String dirName, String fileName) throws AndrolibException { + File smaliDir = new File(mApkDir, dirName); + if (!smaliDir.isDirectory()) { + return; + } + + File dex = new File(outDir, fileName); + if (!mConfig.forceBuildAll) { + LOGGER.info("Checking whether sources have changed..."); + if (!isModified(smaliDir, dex)) { + return; + } + } + //noinspection ResultOfMethodCallIgnored + dex.delete(); + + LOGGER.info("Smaling " + dirName + " folder into " + fileName + "..."); + int apiLevel = mConfig.apiLevel > 0 ? mConfig.apiLevel : mMinSdkVersion; + SmaliBuilder builder = new SmaliBuilder(smaliDir, apiLevel); + builder.build(dex); + } + + private void backupManifestFile(File manifest, File manifestOrig) throws AndrolibException { + // if we decoded in "raw", we cannot patch AndroidManifest + if (new File(mApkDir, "resources.arsc").isFile()) { + return; + } + + if (!manifest.isFile()) { + return; + } + + if (manifestOrig.exists()) { + //noinspection ResultOfMethodCallIgnored + manifestOrig.delete(); + } + + try { + FileUtils.copyFile(manifest, manifestOrig); + ResXmlUtils.fixingPublicAttrsInProviderAttributes(manifest); + } catch (IOException ex) { + throw new AndrolibException(ex); + } + } + + private void buildResources(File outDir, File manifest) throws AndrolibException { + if (!manifest.isFile()) { + LOGGER.fine("Could not find AndroidManifest.xml"); + return; + } + + if (new File(mApkDir, "resources.arsc").isFile()) { + copyResourcesRaw(outDir, manifest); + } else if (new File(mApkDir, "res").isDirectory()) { + buildResourcesFull(outDir, manifest); + } else { + LOGGER.fine("Could not find resources"); + buildManifest(outDir, manifest); + } + } + + private void copyResourcesRaw(File outDir, File manifest) throws AndrolibException { + if (!mConfig.forceBuildAll) { + LOGGER.info("Checking whether resources have changed..."); + if (!isModified(manifest, new File(outDir, "AndroidManifest.xml")) + && !isModified(new File(mApkDir, "resources.arsc"), new File(outDir, "resources.arsc")) + && !isModified(newFiles(mApkDir, ApkInfo.RESOURCES_DIRNAMES), + newFiles(outDir, ApkInfo.RESOURCES_DIRNAMES))) { + return; + } + } + + LOGGER.info("Copying raw resources..."); + try { + Directory in = mApkDir.getDirectory(); + + in.copyToDir(outDir, "AndroidManifest.xml"); + in.copyToDir(outDir, "resources.arsc"); + in.copyToDir(outDir, ApkInfo.RESOURCES_DIRNAMES); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + private void buildResourcesFull(File outDir, File manifest) throws AndrolibException { + File resourcesFile = new File(outDir.getParentFile(), "resources.zip"); + if (!mConfig.forceBuildAll) { + LOGGER.info("Checking whether resources have changed..."); + if (!isModified(manifest, new File(outDir, "AndroidManifest.xml")) + && !isModified(newFiles(mApkDir, ApkInfo.RESOURCES_DIRNAMES), + newFiles(outDir, ApkInfo.RESOURCES_DIRNAMES)) + && (mConfig.aaptVersion == 1 || resourcesFile.isFile())) { + return; + } + } + //noinspection ResultOfMethodCallIgnored + resourcesFile.delete(); + + try { + if (mConfig.debugMode) { + if (mConfig.aaptVersion == 2) { + LOGGER.info("Setting 'debuggable' attribute to 'true' in AndroidManifest.xml"); + ResXmlUtils.setApplicationDebugTagTrue(manifest); + } else { + ResXmlUtils.removeApplicationDebugTag(manifest); + } + } + + if (mConfig.netSecConf) { + String targetSdkVersion = mApkInfo.getTargetSdkVersion(); + if (targetSdkVersion != null) { + if (Integer.parseInt(targetSdkVersion) < ResConfigFlags.SDK_NOUGAT) { + LOGGER.warning("Target SDK version is lower than 24! Network Security Configuration might be ignored!"); + } + } + + File netSecConfOrig = new File(mApkDir, "res/xml/network_security_config.xml"); + ResXmlUtils.modNetworkSecurityConfig(netSecConfOrig); + ResXmlUtils.setNetworkSecurityConfig(manifest); + LOGGER.info("Added permissive network security config in manifest"); + } + } catch (IOException | ParserConfigurationException | TransformerException | SAXException ex) { + throw new AndrolibException(ex); + } + + ExtFile tmpFile; + try { + tmpFile = new ExtFile(File.createTempFile("APKTOOL", null)); + } catch (IOException ex) { + throw new AndrolibException(ex); + } + //noinspection ResultOfMethodCallIgnored + tmpFile.delete(); + + File resDir = new File(mApkDir, "res"); + File ninePatch = new File(mApkDir, "9patch"); + if (!ninePatch.isDirectory()) { + ninePatch = null; + } + + LOGGER.info("Building resources with " + AaptManager.getAaptName(mConfig.aaptVersion) + "..."); + + try { + AaptInvoker invoker = new AaptInvoker(mConfig, mApkInfo); + invoker.invoke(tmpFile, manifest, resDir, ninePatch, null, getIncludeFiles()); + + Directory tmpDir = tmpFile.getDirectory(); + tmpDir.copyToDir(outDir, "AndroidManifest.xml"); + tmpDir.copyToDir(outDir, "resources.arsc"); + tmpDir.copyToDir(outDir, ApkInfo.RESOURCES_DIRNAMES); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } finally { + //noinspection ResultOfMethodCallIgnored + tmpFile.delete(); + } + } + + private void buildManifest(File outDir, File manifest) throws AndrolibException { + if (!mConfig.forceBuildAll) { + LOGGER.info("Checking whether AndroidManifest.xml has changed..."); + if (!isModified(manifest, new File(outDir, "AndroidManifest.xml"))) { + return; + } + } + + ExtFile tmpFile; + try { + tmpFile = new ExtFile(File.createTempFile("APKTOOL", null)); + } catch (IOException ex) { + throw new AndrolibException(ex); + } + //noinspection ResultOfMethodCallIgnored + tmpFile.delete(); + + File ninePatch = new File(mApkDir, "9patch"); + if (!ninePatch.isDirectory()) { + ninePatch = null; + } + + LOGGER.info("Building AndroidManifest.xml with " + AaptManager.getAaptName(mConfig.aaptVersion) + "..."); + + try { + AaptInvoker invoker = new AaptInvoker(mConfig, mApkInfo); + invoker.invoke(tmpFile, manifest, null, ninePatch, null, getIncludeFiles()); + + Directory tmpDir = tmpFile.getDirectory(); + tmpDir.copyToDir(outDir, "AndroidManifest.xml"); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } catch (AndrolibException ex) { + LOGGER.warning("Parse AndroidManifest.xml failed, treat it as raw file."); + copyManifestRaw(outDir); + } finally { + //noinspection ResultOfMethodCallIgnored + tmpFile.delete(); + } + } + + private void copyManifestRaw(File outDir) throws AndrolibException { + LOGGER.info("Copying raw manifest..."); + try { + Directory in = mApkDir.getDirectory(); + + in.copyToDir(outDir, "AndroidManifest.xml"); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + private void copyOriginalFiles(File outDir) throws AndrolibException { + if (!mConfig.copyOriginalFiles) { + return; + } + + ExtFile originalDir = new ExtFile(mApkDir, "original"); + if (!originalDir.isDirectory()) { + return; + } + + LOGGER.info("Copying original files..."); + try { + Directory in = originalDir.getDirectory(); + + for (String fileName : in.getFiles(true)) { + if (ApkInfo.ORIGINAL_FILENAMES_PATTERN.matcher(fileName).matches()) { + in.copyToDir(outDir, fileName); + } + } + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + private void importRawFiles(ZipOutputStream out) throws AndrolibException { + for (String dirName : ApkInfo.RAW_DIRNAMES) { + File rawDir = new File(mApkDir, dirName); + if (!rawDir.isDirectory()) { + continue; + } + + LOGGER.info("Importing " + dirName + "..."); try { - BrutIO.copyAndClose(Files.newInputStream(working.toPath()), Files.newOutputStream(stored.toPath())); - return true; + ZipUtils.zipDir(mApkDir, dirName, out, mApkInfo.doNotCompress); } catch (IOException ex) { throw new AndrolibException(ex); } } - return true; } - private boolean buildSourcesSmali(String folder, String filename) throws AndrolibException { - ExtFile smaliDir = new ExtFile(mApkDir, folder); - if (!smaliDir.exists()) { - return false; - } - File dex = new File(mApkDir, APK_DIRNAME + "/" + filename); - if (!mConfig.forceBuildAll) { - LOGGER.info("Checking whether sources has changed..."); - } - if (mConfig.forceBuildAll || isModified(smaliDir, dex)) { - LOGGER.info("Smaling " + folder + " folder into " + filename + "..."); - //noinspection ResultOfMethodCallIgnored - dex.delete(); - SmaliBuilder.build(smaliDir, dex, mConfig.apiLevel > 0 ? mConfig.apiLevel : mMinSdkVersion); - } - return true; - } - - private void buildResources() throws BrutException { - if (!buildResourcesRaw() && !buildResourcesFull() && !buildManifest()) { - LOGGER.warning("Could not find resources"); - } - } - - private boolean buildResourcesRaw() throws AndrolibException { - try { - if (!new File(mApkDir, "resources.arsc").exists()) { - return false; - } - File apkDir = new File(mApkDir, APK_DIRNAME); - if (!mConfig.forceBuildAll) { - LOGGER.info("Checking whether resources has changed..."); - } - if (mConfig.forceBuildAll || isModified(newFiles(APK_RESOURCES_FILENAMES, mApkDir), - newFiles(APK_RESOURCES_FILENAMES, apkDir))) { - LOGGER.info("Copying raw resources..."); - mApkDir.getDirectory().copyToDir(apkDir, APK_RESOURCES_FILENAMES); - } - return true; - } catch (DirectoryException ex) { - throw new AndrolibException(ex); - } - } - - private boolean buildResourcesFull() throws AndrolibException { - try { - if (!new File(mApkDir, "res").exists()) { - return false; - } - if (!mConfig.forceBuildAll) { - LOGGER.info("Checking whether resources has changed..."); - } - File apkDir = new File(mApkDir, APK_DIRNAME); - File resourceFile = new File(apkDir.getParent(), "resources.zip"); - - if (mConfig.forceBuildAll || isModified(newFiles(APP_RESOURCES_FILENAMES, mApkDir), - newFiles(APK_RESOURCES_FILENAMES, apkDir)) || (mConfig.isAapt2() && !isFile(resourceFile))) { - LOGGER.info("Building resources..."); - - if (mConfig.debugMode) { - if (mConfig.isAapt2()) { - LOGGER.info("Using aapt2 - setting 'debuggable' attribute to 'true' in AndroidManifest.xml"); - ResXmlPatcher.setApplicationDebugTagTrue(new File(mApkDir, "AndroidManifest.xml")); - } else { - ResXmlPatcher.removeApplicationDebugTag(new File(mApkDir, "AndroidManifest.xml")); - } - } - - if (mConfig.netSecConf) { - ApkInfo meta = ApkInfo.load(new ExtFile(mApkDir)); - if (meta.getSdkInfo() != null && meta.getSdkInfo().get("targetSdkVersion") != null) { - if (Integer.parseInt(meta.getSdkInfo().get("targetSdkVersion")) < ResConfigFlags.SDK_NOUGAT) { - LOGGER.warning("Target SDK version is lower than 24! Network Security Configuration might be ignored!"); - } - } - File netSecConfOrig = new File(mApkDir, "res/xml/network_security_config.xml"); - if (netSecConfOrig.exists()) { - LOGGER.info("Replacing existing network_security_config.xml!"); - //noinspection ResultOfMethodCallIgnored - netSecConfOrig.delete(); - } - ResXmlPatcher.modNetworkSecurityConfig(netSecConfOrig); - ResXmlPatcher.setNetworkSecurityConfig(new File(mApkDir, "AndroidManifest.xml")); - LOGGER.info("Added permissive network security config in manifest"); - } - - File apkFile = File.createTempFile("APKTOOL", null); - //noinspection ResultOfMethodCallIgnored - apkFile.delete(); - //noinspection ResultOfMethodCallIgnored - resourceFile.delete(); - - File ninePatch = new File(mApkDir, "9patch"); - if (!ninePatch.exists()) { - ninePatch = null; - } - AaptInvoker invoker = new AaptInvoker(mConfig, mApkInfo); - invoker.invokeAapt(apkFile, new File(mApkDir, "AndroidManifest.xml"), - new File(mApkDir, "res"), ninePatch, null, getIncludeFiles()); - - ExtFile tmpExtFile = new ExtFile(apkFile); - Directory tmpDir = tmpExtFile.getDirectory(); - - // Sometimes an application is built with a resources.arsc file with no resources, - // Apktool assumes it will have a rebuilt arsc file, when it doesn't. So if we - // encounter a copy error, move to a warning and continue on. (#1730) - try { - tmpDir.copyToDir(apkDir, - tmpDir.containsDir("res") ? APK_RESOURCES_FILENAMES - : APK_RESOURCES_WITHOUT_RES_FILENAMES); - } catch (DirectoryException ex) { - LOGGER.warning(ex.getMessage()); - } finally { - tmpExtFile.close(); - } - - // delete tmpDir - //noinspection ResultOfMethodCallIgnored - apkFile.delete(); - } - return true; - } catch (IOException | BrutException | ParserConfigurationException | TransformerException | SAXException ex) { - throw new AndrolibException(ex); - } - } - - private boolean buildManifestRaw() throws AndrolibException { - try { - File apkDir = new File(mApkDir, APK_DIRNAME); - LOGGER.info("Copying raw AndroidManifest.xml..."); - mApkDir.getDirectory().copyToDir(apkDir, APK_MANIFEST_FILENAMES); - return true; - } catch (DirectoryException ex) { - throw new AndrolibException(ex); - } - } - - private boolean buildManifest() throws BrutException { - try { - if (!new File(mApkDir, "AndroidManifest.xml").exists()) { - return false; - } - if (!mConfig.forceBuildAll) { - LOGGER.info("Checking whether resources has changed..."); - } - - File apkDir = new File(mApkDir, APK_DIRNAME); - - if (mConfig.forceBuildAll || isModified(newFiles(APK_MANIFEST_FILENAMES, mApkDir), - newFiles(APK_MANIFEST_FILENAMES, apkDir))) { - LOGGER.info("Building AndroidManifest.xml..."); - - File apkFile = File.createTempFile("APKTOOL", null); - //noinspection ResultOfMethodCallIgnored - apkFile.delete(); - - File ninePatch = new File(mApkDir, "9patch"); - if (!ninePatch.exists()) { - ninePatch = null; - } - - AaptInvoker invoker = new AaptInvoker(mConfig, mApkInfo); - invoker.invokeAapt(apkFile, new File(mApkDir, "AndroidManifest.xml"), - null, ninePatch, null, getIncludeFiles()); - - Directory tmpDir = new ExtFile(apkFile).getDirectory(); - tmpDir.copyToDir(apkDir, APK_MANIFEST_FILENAMES); - - //noinspection ResultOfMethodCallIgnored - apkFile.delete(); - } - return true; - } catch (IOException | DirectoryException ex) { - throw new AndrolibException(ex); - } catch (AndrolibException ex) { - LOGGER.warning("Parse AndroidManifest.xml failed, treat it as raw file."); - return buildManifestRaw(); - } - } - - private void buildLibs() throws AndrolibException { - buildLibrary("lib"); - buildLibrary("libs"); - buildLibrary("kotlin"); - buildLibrary("META-INF/services"); - } - - private void buildLibrary(String folder) throws AndrolibException { - File working = new File(mApkDir, folder); - - if (!working.exists()) { + private void importUnknownFiles(ZipOutputStream out) throws AndrolibException { + File unknownDir = new File(mApkDir, "unknown"); + if (!unknownDir.isDirectory()) { return; } - File stored = new File(mApkDir, APK_DIRNAME + "/" + folder); - if (mConfig.forceBuildAll || isModified(working, stored)) { - LOGGER.info("Copying libs... (/" + folder + ")"); - try { - OS.rmdir(stored); - OS.cpdir(working, stored); - } catch (BrutException ex) { - throw new AndrolibException(ex); - } - } - } - - private void buildCopyOriginalFiles() throws AndrolibException { - if (mConfig.copyOriginalFiles) { - File originalDir = new File(mApkDir, "original"); - if (originalDir.exists()) { - try { - LOGGER.info("Copy original files..."); - Directory in = (new ExtFile(originalDir)).getDirectory(); - if (in.containsFile("AndroidManifest.xml")) { - LOGGER.info("Copy AndroidManifest.xml..."); - in.copyToDir(new File(mApkDir, APK_DIRNAME), "AndroidManifest.xml"); - } - if (in.containsFile("stamp-cert-sha256")) { - LOGGER.info("Copy stamp-cert-sha256..."); - in.copyToDir(new File(mApkDir, APK_DIRNAME), "stamp-cert-sha256"); - } - if (in.containsDir("META-INF")) { - LOGGER.info("Copy META-INF..."); - in.copyToDir(new File(mApkDir, APK_DIRNAME), "META-INF"); - } - } catch (DirectoryException ex) { - throw new AndrolibException(ex); - } - } - } - } - - private void buildUnknownFiles(File outFile) throws AndrolibException { - if (mApkInfo.unknownFiles != null) { - LOGGER.info("Copying unknown files/dir..."); - - Map files = mApkInfo.unknownFiles; - File tempFile = new File(outFile.getParent(), outFile.getName() + ".apktool_temp"); - boolean renamed = outFile.renameTo(tempFile); - if (!renamed) { - throw new AndrolibException("Unable to rename temporary file"); - } - - try ( - ZipFile inputFile = new ZipFile(tempFile); - ZipOutputStream actualOutput = new ZipOutputStream(Files.newOutputStream(outFile.toPath())) - ) { - copyExistingFiles(inputFile, actualOutput); - copyUnknownFiles(actualOutput, files); - } catch (IOException | BrutException ex) { - throw new AndrolibException(ex); - } - - // Remove our temporary file. - //noinspection ResultOfMethodCallIgnored - tempFile.delete(); - } - } - - private void copyExistingFiles(ZipFile inputFile, ZipOutputStream outputFile) throws IOException { - // First, copy the contents from the existing outFile: - Enumeration entries = inputFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = new ZipEntry(entries.nextElement()); - - // We can't reuse the compressed size because it depends on compression sizes. - entry.setCompressedSize(-1); - outputFile.putNextEntry(entry); - - // No need to create directory entries in the final apk - if (!entry.isDirectory()) { - BrutIO.copy(inputFile, outputFile, entry); - } - - outputFile.closeEntry(); - } - } - - private void copyUnknownFiles(ZipOutputStream outputFile, Map files) - throws BrutException, IOException { - File unknownFileDir = new File(mApkDir, UNK_DIRNAME); - - // loop through unknown files - for (Map.Entry unknownFileInfo : files.entrySet()) { - File inputFile; - - try { - inputFile = new File(unknownFileDir, BrutIO.sanitizeFilepath(unknownFileDir, unknownFileInfo.getKey())); - } catch (RootUnknownFileException | InvalidUnknownFileException | TraversalUnknownFileException exception) { - LOGGER.warning(String.format("Skipping file %s (%s)", unknownFileInfo.getKey(), exception.getMessage())); - continue; - } - - if (inputFile.isDirectory()) { - continue; - } - - ZipEntry newEntry = new ZipEntry(unknownFileInfo.getKey()); - int method = Integer.parseInt(unknownFileInfo.getValue()); - LOGGER.fine(String.format("Copying unknown file %s with method %d", unknownFileInfo.getKey(), method)); - if (method == ZipEntry.STORED) { - newEntry.setMethod(ZipEntry.STORED); - newEntry.setSize(inputFile.length()); - newEntry.setCompressedSize(-1); - BufferedInputStream unknownFile = new BufferedInputStream(Files.newInputStream(inputFile.toPath())); - CRC32 crc = BrutIO.calculateCrc(unknownFile); - newEntry.setCrc(crc.getValue()); - unknownFile.close(); - } else { - newEntry.setMethod(ZipEntry.DEFLATED); - } - outputFile.putNextEntry(newEntry); - - BrutIO.copy(inputFile, outputFile); - outputFile.closeEntry(); - } - } - - private void buildApk(File outApk) throws AndrolibException { - LOGGER.info("Building apk file..."); - if (outApk.exists()) { - //noinspection ResultOfMethodCallIgnored - outApk.delete(); - } else { - File outDir = outApk.getParentFile(); - if (outDir != null && !outDir.exists()) { - //noinspection ResultOfMethodCallIgnored - outDir.mkdirs(); - } - } - File assetDir = new File(mApkDir, "assets"); - if (!assetDir.exists()) { - assetDir = null; - } - zipPackage(outApk, new File(mApkDir, APK_DIRNAME), assetDir); - } - - private void zipPackage(File apkFile, File rawDir, File assetDir) throws AndrolibException { + LOGGER.info("Importing unknown files..."); try { - ZipUtils.zipFolders(rawDir, apkFile, assetDir, mApkInfo.doNotCompress); - } catch (IOException | BrutException ex) { + ZipUtils.zipDir(unknownDir, out, mApkInfo.doNotCompress); + } catch (IOException ex) { throw new AndrolibException(ex); } } @@ -562,11 +514,7 @@ public class ApkBuilder { } private boolean isModified(File working, File stored) { - return !stored.exists() || BrutIO.recursiveModifiedTime(working) > BrutIO .recursiveModifiedTime(stored); - } - - private boolean isFile(File working) { - return working.exists(); + return !stored.exists() || BrutIO.recursiveModifiedTime(working) > BrutIO.recursiveModifiedTime(stored); } private boolean isModified(File[] working, File[] stored) { @@ -578,29 +526,11 @@ public class ApkBuilder { return BrutIO.recursiveModifiedTime(working) > BrutIO.recursiveModifiedTime(stored); } - private File[] newFiles(String[] names, File dir) { + private File[] newFiles(File dir, String[] names) { File[] files = new File[names.length]; for (int i = 0; i < names.length; i++) { files[i] = new File(dir, names[i]); } return files; } - - public boolean detectWhetherAppIsFramework() throws AndrolibException { - File publicXml = new File(mApkDir, "res/values/public.xml"); - if (!publicXml.exists()) { - return false; - } - - Iterator it; - try { - it = IOUtils.lineIterator(new FileReader(new File(mApkDir, "res/values/public.xml"))); - } catch (FileNotFoundException ex) { - throw new AndrolibException( - "Could not detect whether app is framework one", ex); - } - it.next(); - it.next(); - return it.next().contains("0x01"); - } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java index 492d52f6..3f69db1b 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java @@ -32,60 +32,56 @@ import org.apache.commons.io.FilenameUtils; import java.io.*; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import java.util.regex.Pattern; public class ApkDecoder { - private final static Logger LOGGER = Logger.getLogger(ApkDecoder.class.getName()); + private static final Logger LOGGER = Logger.getLogger(ApkDecoder.class.getName()); + // extensions of files that are often packed uncompressed + private static final Pattern NO_COMPRESS_EXT_PATTERN = Pattern.compile( + "dex|arsc|so|jpg|jpeg|png|gif|wav|mp2|mp3|ogg|aac|mpg|mpeg|mid|midi|smf|jet|" + + "rtttl|imy|xmf|mp4|m4a|m4v|3gp|3gpp|3g2|3gpp2|amr|awb|wma|wmv|webm|webp|mkv"); + + private final ExtFile mApkFile; private final Config mConfig; - private final ApkInfo mApkInfo; - private int mMinSdkVersion = 0; + private final AtomicReference mBuildError; - private final static String SMALI_DIRNAME = "smali"; - private final static String UNK_DIRNAME = "unknown"; - private final static String[] APK_STANDARD_ALL_FILENAMES = new String[] { - "classes.dex", "AndroidManifest.xml", "resources.arsc", "res", "r", "R", - "lib", "libs", "assets", "META-INF", "kotlin" }; - private final static String[] APK_RESOURCES_FILENAMES = new String[] { - "resources.arsc", "res", "r", "R" }; - private final static String[] APK_MANIFEST_FILENAMES = new String[] { - "AndroidManifest.xml" }; - private final static Pattern NO_COMPRESS_PATTERN = Pattern.compile("(" + - "jpg|jpeg|png|gif|wav|mp2|mp3|ogg|aac|mpg|mpeg|mid|midi|smf|jet|rtttl|imy|xmf|mp4|" + - "m4a|m4v|3gp|3gpp|3g2|3gpp2|amr|awb|wma|wmv|webm|webp|mkv)$"); - - public ApkDecoder(File apkFile) { - this(Config.getDefaultConfig(), new ExtFile(apkFile)); - } + private ApkInfo mApkInfo; + private ResourcesDecoder mResDecoder; + private volatile int mMinSdkVersion; + private BackgroundWorker mWorker; public ApkDecoder(ExtFile apkFile) { - this(Config.getDefaultConfig(), apkFile); + this(apkFile, Config.getDefaultConfig()); } - public ApkDecoder(Config config, File apkFile) { - this(config, new ExtFile(apkFile)); - } + public ApkDecoder(ApkInfo apkInfo, Config config) { + this(apkInfo.getApkFile(), config); - public ApkDecoder(Config config, ExtFile apkFile) { - this(config, new ApkInfo(apkFile)); - } - - public ApkDecoder(Config config, ApkInfo apkInfo) { - mConfig = config; mApkInfo = apkInfo; } - public ApkInfo decode(File outDir) throws AndrolibException, IOException, DirectoryException { - ExtFile apkFile = mApkInfo.getApkFile(); - try { - if (!mConfig.forceDelete && outDir.exists()) { - throw new OutDirExistsException(); - } + public ApkDecoder(ExtFile apkFile, Config config) { + mApkFile = apkFile; + mConfig = config; + mBuildError = new AtomicReference<>(null); + } - if (!apkFile.isFile() || !apkFile.canRead()) { - throw new InFileNotFoundException(); - } + public ApkInfo decode(File outDir) throws AndrolibException { + if (!mConfig.forceDelete && outDir.exists()) { + throw new OutDirExistsException(); + } + if (!mApkFile.isFile() || !mApkFile.canRead()) { + throw new InFileNotFoundException(); + } + if (mConfig.jobs > 1) { + mWorker = new BackgroundWorker(mConfig.jobs - 1); + } + try { + if (mApkInfo == null) mApkInfo = new ApkInfo(mApkFile); + mResDecoder = new ResourcesDecoder(mConfig, mApkInfo); try { OS.rmdir(outDir); @@ -95,192 +91,198 @@ public class ApkDecoder { //noinspection ResultOfMethodCallIgnored outDir.mkdirs(); - LOGGER.info("Using Apktool " + ApktoolProperties.getVersion() + " on " + mApkInfo.apkFileName); + LOGGER.info("Using Apktool " + ApktoolProperties.getVersion() + " on " + mApkFile.getName() + + (mWorker != null ? " with " + mConfig.jobs + " threads" : "")); - ResourcesDecoder resourcesDecoder = new ResourcesDecoder(mConfig, mApkInfo); + decodeSources(outDir); + decodeResources(outDir); + decodeManifest(outDir); - if (mApkInfo.hasResources()) { - switch (mConfig.decodeResources) { - case Config.DECODE_RESOURCES_NONE: - copyResourcesRaw(outDir); - break; - case Config.DECODE_RESOURCES_FULL: - resourcesDecoder.decodeResources(outDir); - break; + if (mWorker != null) { + mWorker.waitForFinish(); + if (mBuildError.get() != null) { + throw mBuildError.get(); } } - if (mApkInfo.hasManifest()) { - if (mConfig.decodeResources == Config.DECODE_RESOURCES_FULL || - mConfig.forceDecodeManifest == Config.FORCE_DECODE_MANIFEST_FULL) { - resourcesDecoder.decodeManifest(outDir); - } - else { - copyManifestRaw(outDir); - } - } - resourcesDecoder.updateApkInfo(outDir); - - if (mApkInfo.hasSources()) { - switch (mConfig.decodeSources) { - case Config.DECODE_SOURCES_NONE: - copySourcesRaw(outDir, "classes.dex"); - break; - case Config.DECODE_SOURCES_SMALI: - case Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES: - decodeSourcesSmali(outDir, "classes.dex"); - break; - } - } - - if (mApkInfo.hasMultipleSources()) { - // foreach unknown dex file in root, lets disassemble it - Set files = apkFile.getDirectory().getFiles(true); - for (String file : files) { - if (file.endsWith(".dex")) { - if (!file.equalsIgnoreCase("classes.dex")) { - switch(mConfig.decodeSources) { - case Config.DECODE_SOURCES_NONE: - copySourcesRaw(outDir, file); - break; - case Config.DECODE_SOURCES_SMALI: - decodeSourcesSmali(outDir, file); - break; - case Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES: - if (file.startsWith("classes") && file.endsWith(".dex")) { - decodeSourcesSmali(outDir, file); - } else { - copySourcesRaw(outDir, file); - } - break; - } - } - } - } - } - - // In case we have no resources. We should store the minSdk we pulled from the source opcode api level - if (!mApkInfo.hasResources() && mMinSdkVersion > 0) { - mApkInfo.setSdkInfoField("minSdkVersion", Integer.toString(mMinSdkVersion)); - } - + copyOriginalFiles(outDir); copyRawFiles(outDir); copyUnknownFiles(outDir); - recordUncompressedFiles(resourcesDecoder.getResFileMapping()); - copyOriginalFiles(outDir); writeApkInfo(outDir); return mApkInfo; } finally { + if (mWorker != null) { + mWorker.shutdownNow(); + } try { - apkFile.close(); + mApkFile.close(); } catch (IOException ignored) {} } } - private void writeApkInfo(File outDir) throws AndrolibException { - mApkInfo.save(new File(outDir, "apktool.yml")); - } + private void decodeSources(File outDir) throws AndrolibException { + if (!mApkInfo.hasSources()) { + return; + } + + switch (mConfig.decodeSources) { + case Config.DECODE_SOURCES_NONE: + copySourcesRaw(outDir, "classes.dex"); + break; + case Config.DECODE_SOURCES_SMALI: + case Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES: + decodeSourcesSmali(outDir, "classes.dex"); + break; + } - private void copyManifestRaw(File outDir) throws AndrolibException { try { - LOGGER.info("Copying raw manifest..."); - mApkInfo.getApkFile().getDirectory().copyToDir(outDir, APK_MANIFEST_FILENAMES); + Directory in = mApkFile.getDirectory(); + + // foreach unknown dex file in root, lets disassemble it + for (String fileName : in.getFiles(true)) { + if (fileName.endsWith(".dex") && !fileName.equals("classes.dex")) { + switch (mConfig.decodeSources) { + case Config.DECODE_SOURCES_NONE: + copySourcesRaw(outDir, fileName); + break; + case Config.DECODE_SOURCES_SMALI: + decodeSourcesSmali(outDir, fileName); + break; + case Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES: + if (fileName.startsWith("classes")) { + decodeSourcesSmali(outDir, fileName); + } else { + copySourcesRaw(outDir, fileName); + } + break; + } + } + } } catch (DirectoryException ex) { throw new AndrolibException(ex); } } + private void copySourcesRaw(File outDir, String fileName) throws AndrolibException { + LOGGER.info("Copying raw " + fileName + " file..."); + try { + Directory in = mApkFile.getDirectory(); + + in.copyToDir(outDir, fileName); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + private void decodeSourcesSmali(File outDir, String fileName) throws AndrolibException { + if (mWorker != null) { + mWorker.submit(() -> { + if (mBuildError.get() == null) { + try { + decodeSourcesSmaliJob(outDir, fileName); + } catch (AndrolibException ex) { + mBuildError.compareAndSet(null, ex); + } + } + }); + } else { + decodeSourcesSmaliJob(outDir, fileName); + } + } + + private void decodeSourcesSmaliJob(File outDir, String fileName) throws AndrolibException { + File smaliDir; + if (fileName.equals("classes.dex")) { + smaliDir = new File(outDir, "smali"); + } else { + smaliDir = new File(outDir, "smali_" + fileName.substring(0, fileName.indexOf("."))); + } + try { + OS.rmdir(smaliDir); + } catch (BrutException ex) { + throw new AndrolibException(ex); + } + //noinspection ResultOfMethodCallIgnored + smaliDir.mkdirs(); + + LOGGER.info("Baksmaling " + fileName + "..."); + SmaliDecoder decoder = new SmaliDecoder(mApkFile, fileName, + mConfig.baksmaliDebugMode, mConfig.apiLevel); + DexFile dexFile = decoder.decode(smaliDir); + + // record minSdkVersion for jars + int minSdkVersion = dexFile.getOpcodes().api; + if (mMinSdkVersion == 0 || mMinSdkVersion > minSdkVersion) { + mMinSdkVersion = minSdkVersion; + } + } + + private void decodeResources(File outDir) throws AndrolibException { + if (!mApkInfo.hasResources()) { + return; + } + + switch (mConfig.decodeResources) { + case Config.DECODE_RESOURCES_NONE: + copyResourcesRaw(outDir); + break; + case Config.DECODE_RESOURCES_FULL: + mResDecoder.decodeResources(outDir); + break; + } + } + private void copyResourcesRaw(File outDir) throws AndrolibException { + LOGGER.info("Copying raw resources..."); try { - LOGGER.info("Copying raw resources..."); - mApkInfo.getApkFile().getDirectory().copyToDir(outDir, APK_RESOURCES_FILENAMES); + Directory in = mApkFile.getDirectory(); + + in.copyToDir(outDir, "resources.arsc"); + in.copyToDir(outDir, ApkInfo.RESOURCES_DIRNAMES); } catch (DirectoryException ex) { throw new AndrolibException(ex); } } - private void copySourcesRaw(File outDir, String filename) throws AndrolibException { - try { - LOGGER.info("Copying raw " + filename + " file..."); - mApkInfo.getApkFile().getDirectory().copyToDir(outDir, filename); - } catch (DirectoryException ex) { - throw new AndrolibException(ex); + private void decodeManifest(File outDir) throws AndrolibException { + if (!mApkInfo.hasManifest()) { + return; + } + + if (mConfig.decodeResources == Config.DECODE_RESOURCES_FULL + || mConfig.forceDecodeManifest == Config.FORCE_DECODE_MANIFEST_FULL) { + mResDecoder.decodeManifest(outDir); + } else { + copyManifestRaw(outDir); } } - private void decodeSourcesSmali(File outDir, String filename) throws AndrolibException { + private void copyManifestRaw(File outDir) throws AndrolibException { + LOGGER.info("Copying raw manifest..."); try { - File smaliDir; - if (filename.equalsIgnoreCase("classes.dex")) { - smaliDir = new File(outDir, SMALI_DIRNAME); - } else { - smaliDir = new File(outDir, SMALI_DIRNAME + "_" + filename.substring(0, filename.indexOf("."))); - } - OS.rmdir(smaliDir); - //noinspection ResultOfMethodCallIgnored - smaliDir.mkdirs(); - LOGGER.info("Baksmaling " + filename + "..."); - DexFile dexFile = SmaliDecoder.decode(mApkInfo.getApkFile(), smaliDir, filename, - mConfig.baksmaliDebugMode, mConfig.apiLevel); - int minSdkVersion = dexFile.getOpcodes().api; - if (mMinSdkVersion == 0 || mMinSdkVersion > minSdkVersion) { - mMinSdkVersion = minSdkVersion; - } - } catch (BrutException ex) { + Directory in = mApkFile.getDirectory(); + + in.copyToDir(outDir, "AndroidManifest.xml"); + } catch (DirectoryException ex) { throw new AndrolibException(ex); } } private void copyRawFiles(File outDir) throws AndrolibException { - LOGGER.info("Copying assets and libs..."); try { - Directory in = mApkInfo.getApkFile().getDirectory(); + Directory in = mApkFile.getDirectory(); - if (mConfig.decodeAssets == Config.DECODE_ASSETS_FULL) { - if (in.containsDir("assets")) { - in.copyToDir(outDir, "assets"); - } - } - if (in.containsDir("lib")) { - in.copyToDir(outDir, "lib"); - } - if (in.containsDir("libs")) { - in.copyToDir(outDir, "libs"); - } - if (in.containsDir("kotlin")) { - in.copyToDir(outDir, "kotlin"); - } - } catch (DirectoryException ex) { - throw new AndrolibException(ex); - } - } - - private boolean isAPKFileNames(String file) { - for (String apkFile : APK_STANDARD_ALL_FILENAMES) { - if (apkFile.equals(file) || file.startsWith(apkFile + "/")) { - return true; - } - } - return false; - } - - private void copyUnknownFiles(File outDir) throws AndrolibException { - LOGGER.info("Copying unknown files..."); - File unknownOut = new File(outDir, UNK_DIRNAME); - try { - Directory unk = mApkInfo.getApkFile().getDirectory(); - - // loop all items in container recursively, ignoring any that are pre-defined by aapt - Set files = unk.getFiles(true); - for (String file : files) { - if (!isAPKFileNames(file) && !file.endsWith(".dex")) { - - // copy file out of archive into special "unknown" folder - unk.copyToDir(unknownOut, file); - // let's record the name of the file, and its compression type - // so that we may re-include it the same way - mApkInfo.addUnknownFileInfo(file, String.valueOf(unk.getCompressionLevel(file))); + for (String dirName : ApkInfo.RAW_DIRNAMES) { + if ((mConfig.decodeAssets == Config.DECODE_ASSETS_FULL || !dirName.equals("assets")) + && in.containsDir(dirName)) { + LOGGER.info("Copying " + dirName + "..."); + for (String fileName : in.getDir(dirName).getFiles(true)) { + fileName = dirName + "/" + fileName; + if (!ApkInfo.ORIGINAL_FILENAMES_PATTERN.matcher(fileName).matches()) { + in.copyToDir(outDir, fileName); + } + } } } } catch (DirectoryException ex) { @@ -290,29 +292,13 @@ public class ApkDecoder { private void copyOriginalFiles(File outDir) throws AndrolibException { LOGGER.info("Copying original files..."); - File originalDir = new File(outDir, "original"); - if (!originalDir.exists()) { - //noinspection ResultOfMethodCallIgnored - originalDir.mkdirs(); - } - try { - Directory in = mApkInfo.getApkFile().getDirectory(); - if (in.containsFile("AndroidManifest.xml")) { - in.copyToDir(originalDir, "AndroidManifest.xml"); - } - if (in.containsFile("stamp-cert-sha256")) { - in.copyToDir(originalDir, "stamp-cert-sha256"); - } - if (in.containsDir("META-INF")) { - in.copyToDir(originalDir, "META-INF"); + Directory in = mApkFile.getDirectory(); + File originalDir = new File(outDir, "original"); - if (in.containsDir("META-INF/services")) { - // If the original APK contains the folder META-INF/services folder - // that is used for service locators (like coroutines on android), - // copy it to the destination folder, so it does not get dropped. - LOGGER.info("Copying META-INF/services directory"); - in.copyToDir(outDir, "META-INF/services"); + for (String fileName : in.getFiles(true)) { + if (ApkInfo.ORIGINAL_FILENAMES_PATTERN.matcher(fileName).matches()) { + in.copyToDir(originalDir, fileName); } } } catch (DirectoryException ex) { @@ -320,33 +306,86 @@ public class ApkDecoder { } } + private void copyUnknownFiles(File outDir) throws AndrolibException { + LOGGER.info("Copying unknown files..."); + try { + Directory in = mApkFile.getDirectory(); + File unknownDir = new File(outDir, "unknown"); + + for (String fileName : in.getFiles(true)) { + if (!ApkInfo.STANDARD_FILENAMES_PATTERN.matcher(fileName).matches()) { + in.copyToDir(unknownDir, fileName); + } + } + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + private void writeApkInfo(File outDir) throws AndrolibException { + mResDecoder.updateApkInfo(outDir); + + // in case we have no resources, we should store the minSdk we pulled from the source opcode api level + if (!mApkInfo.hasResources() && mMinSdkVersion > 0) { + mApkInfo.setMinSdkVersion(Integer.toString(mMinSdkVersion)); + } + + // record uncompressed files + Map resFileMapping = mResDecoder.getResFileMapping(); + recordUncompressedFiles(resFileMapping); + + // write apk info to file + mApkInfo.save(new File(outDir, "apktool.yml")); + } + public void recordUncompressedFiles(Map resFileMapping) throws AndrolibException { try { - List uncompressedFilesOrExts = new ArrayList<>(); - Directory unk = mApkInfo.getApkFile().getDirectory(); - Set files = unk.getFiles(true); + Set uncompressedExts = new HashSet<>(); + Set uncompressedFiles = new HashSet<>(); + Directory in = mApkFile.getDirectory(); - for (String file : files) { - if (isAPKFileNames(file) && unk.getCompressionLevel(file) == 0) { - String extOrFile = ""; - if (unk.getSize(file) != 0) { - extOrFile = FilenameUtils.getExtension(file); - } - - if (extOrFile.isEmpty() || !NO_COMPRESS_PATTERN.matcher(extOrFile).find()) { - extOrFile = file; - if (resFileMapping.containsKey(extOrFile)) { - extOrFile = resFileMapping.get(extOrFile); - } - } - if (!uncompressedFilesOrExts.contains(extOrFile)) { - uncompressedFilesOrExts.add(extOrFile); + for (String fileName : in.getFiles(true)) { + if (in.getCompressionLevel(fileName) == 0) { + String ext; + if (in.getSize(fileName) > 0 + && !(ext = FilenameUtils.getExtension(fileName)).isEmpty() + && NO_COMPRESS_EXT_PATTERN.matcher(ext).matches()) { + uncompressedExts.add(ext); + } else { + uncompressedFiles.add(resFileMapping.getOrDefault(fileName, fileName)); } } } + + // exclude files with an already recorded extenstion + if (!uncompressedExts.isEmpty() && !uncompressedFiles.isEmpty()) { + Iterator it = uncompressedFiles.iterator(); + while (it.hasNext()) { + String fileName = it.next(); + String ext = FilenameUtils.getExtension(fileName); + if (uncompressedExts.contains(ext)) { + it.remove(); + } + } + } + // update apk info - if (!uncompressedFilesOrExts.isEmpty()) { - mApkInfo.doNotCompress = uncompressedFilesOrExts; + int doNotCompressSize = uncompressedExts.size() + uncompressedFiles.size(); + if (doNotCompressSize > 0) { + List doNotCompress = new ArrayList<>(doNotCompressSize); + if (!uncompressedExts.isEmpty()) { + List uncompressedExtsList = new ArrayList<>(uncompressedExts); + uncompressedExtsList.sort(null); + doNotCompress.addAll(uncompressedExtsList); + } + if (!uncompressedFiles.isEmpty()) { + List uncompressedFilesList = new ArrayList<>(uncompressedFiles); + uncompressedFilesList.sort(null); + doNotCompress.addAll(uncompressedFilesList); + } + if (!doNotCompress.isEmpty()) { + mApkInfo.doNotCompress = doNotCompress; + } } } catch (DirectoryException ex) { throw new AndrolibException(ex); diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApktoolProperties.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApktoolProperties.java index 515e5ee1..f27d6579 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApktoolProperties.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApktoolProperties.java @@ -42,20 +42,20 @@ public class ApktoolProperties { } private static void loadProps() { - InputStream in = ApktoolProperties.class.getResourceAsStream("/properties/apktool.properties"); + InputStream in = ApktoolProperties.class.getResourceAsStream("/apktool.properties"); sProps = new Properties(); try { sProps.load(in); in.close(); - } catch (IOException ex) { - LOGGER.warning("Can't load properties."); + } catch (NullPointerException | IOException ex) { + LOGGER.warning("Could not load properties."); } InputStream templateStream = null; try { - templateStream = com.android.tools.smali.baksmali.Main.class.getClassLoader().getResourceAsStream("baksmali.properties"); + templateStream = com.android.tools.smali.baksmali.Main.class.getResourceAsStream("/baksmali.properties"); } catch(NoClassDefFoundError ex) { - LOGGER.warning("Can't load baksmali properties."); + LOGGER.warning("Could not load baksmali properties."); } Properties properties = new Properties(); String version = "(unknown)"; @@ -65,15 +65,15 @@ public class ApktoolProperties { properties.load(templateStream); version = properties.getProperty("application.version"); templateStream.close(); - } catch (IOException ignored) { } + } catch (IOException ignored) {} } sProps.put("baksmaliVersion", version); templateStream = null; try { - templateStream = com.android.tools.smali.smali.Main.class.getClassLoader().getResourceAsStream("smali.properties"); + templateStream = com.android.tools.smali.smali.Main.class.getResourceAsStream("/smali.properties"); } catch(NoClassDefFoundError ex) { - LOGGER.warning("Can't load smali properties."); + LOGGER.warning("Could not load smali properties."); } properties = new Properties(); version = "(unknown)"; @@ -83,7 +83,7 @@ public class ApktoolProperties { properties.load(templateStream); version = properties.getProperty("application.version"); templateStream.close(); - } catch (IOException ignored) { } + } catch (IOException ignored) {} } sProps.put("smaliVersion", version); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/BackgroundWorker.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/BackgroundWorker.java new file mode 100644 index 00000000..783ff74e --- /dev/null +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/BackgroundWorker.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package brut.androlib; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +public class BackgroundWorker { + private final ExecutorService mExecutor; + private final List> mWorkerFutures; + private volatile boolean mSubmitAllowed; + + public BackgroundWorker(int threads) { + mExecutor = Executors.newFixedThreadPool(threads); + mWorkerFutures = new ArrayList<>(); + mSubmitAllowed = true; + } + + public void waitForFinish() { + checkState(); + mSubmitAllowed = false; + for (Future future : mWorkerFutures) { + try { + future.get(); + } catch (InterruptedException | ExecutionException ex) { + throw new RuntimeException(ex); + } + } + mWorkerFutures.clear(); + mSubmitAllowed = true; + } + + public void clearFutures() { + mWorkerFutures.clear(); + } + + private void checkState() { + if (!mSubmitAllowed) { + throw new IllegalStateException("BackgroundWorker is not ready"); + } + } + + public void shutdownNow() { + mSubmitAllowed = false; + mExecutor.shutdownNow(); + } + + public ExecutorService getExecutor() { + return mExecutor; + } + + public void submit(Runnable task) { + checkState(); + mWorkerFutures.add(mExecutor.submit(task)); + } + + public Future submit(Callable task) { + checkState(); + Future future = mExecutor.submit(task); + mWorkerFutures.add(future); + return future; + } +} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/Config.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/Config.java index 5ac8b020..7ba3ed24 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/Config.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/Config.java @@ -22,26 +22,27 @@ import brut.util.OSDetection; import java.io.File; import java.util.logging.Logger; -public class Config { - private static Config instance = null; - private final static Logger LOGGER = Logger.getLogger(Config.class.getName()); +public final class Config { + private static final Logger LOGGER = Logger.getLogger(Config.class.getName()); - public final static short DECODE_SOURCES_NONE = 0x0000; - public final static short DECODE_SOURCES_SMALI = 0x0001; - public final static short DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES = 0x0010; + public static final short DECODE_SOURCES_NONE = 0x0000; + public static final short DECODE_SOURCES_SMALI = 0x0001; + public static final short DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES = 0x0010; - public final static short DECODE_RESOURCES_NONE = 0x0100; - public final static short DECODE_RESOURCES_FULL = 0x0101; + public static final short DECODE_RESOURCES_NONE = 0x0100; + public static final short DECODE_RESOURCES_FULL = 0x0101; - public final static short FORCE_DECODE_MANIFEST_NONE = 0x0000; - public final static short FORCE_DECODE_MANIFEST_FULL = 0x0001; + public static final short FORCE_DECODE_MANIFEST_NONE = 0x0000; + public static final short FORCE_DECODE_MANIFEST_FULL = 0x0001; - public final static short DECODE_ASSETS_NONE = 0x0000; - public final static short DECODE_ASSETS_FULL = 0x0001; + public static final short DECODE_ASSETS_NONE = 0x0000; + public static final short DECODE_ASSETS_FULL = 0x0001; - public final static short DECODE_RES_RESOLVE_REMOVE = 0x0000; - public final static short DECODE_RES_RESOLVE_DUMMY = 0x0001; - public final static short DECODE_RES_RESOLVE_RETAIN = 0x0002; + public static final short DECODE_RES_RESOLVE_REMOVE = 0x0000; + public static final short DECODE_RES_RESOLVE_DUMMY = 0x0001; + public static final short DECODE_RES_RESOLVE_RETAIN = 0x0002; + + private static Config sInstance; // Build options public boolean forceBuildAll = false; @@ -51,8 +52,8 @@ public class Config { public boolean verbose = false; public boolean copyOriginalFiles = false; public boolean updateFiles = false; - public boolean useAapt2 = true; public boolean noCrunch = false; + public boolean noApk = false; // Decode options public short decodeSources = DECODE_SOURCES_SMALI; @@ -67,16 +68,13 @@ public class Config { public boolean baksmaliDebugMode = true; // Common options + public int jobs = Math.min(Runtime.getRuntime().availableProcessors(), 8); public String frameworkDirectory = null; public String frameworkTag = null; - public String aaptPath = ""; - public int aaptVersion = 1; // default to v1 + public File aaptBinary = null; + public int aaptVersion = 2; // default to v2 // Utility functions - public boolean isAapt2() { - return this.useAapt2 || this.aaptVersion == 2; - } - public boolean isDecodeResolveModeUsingDummies() { return decodeResolveMode == DECODE_RES_RESOLVE_DUMMY; } @@ -86,14 +84,14 @@ public class Config { } private Config() { - instance = this; + sInstance = this; } public static Config getInstance() { - if (instance == null) { - instance = new Config(); + if (sInstance == null) { + sInstance = new Config(); } - return instance; + return sInstance; } private void setDefaultFrameworkDirectory() { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/ApkInfo.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/ApkInfo.java index e7866360..01fb9589 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/ApkInfo.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/ApkInfo.java @@ -21,36 +21,45 @@ import brut.androlib.exceptions.AndrolibException; import brut.androlib.res.data.ResConfigFlags; import brut.directory.DirectoryException; import brut.directory.ExtFile; -import brut.directory.FileDirectory; import java.io.*; +import java.nio.file.Files; import java.util.*; +import java.util.regex.Pattern; public class ApkInfo implements YamlSerializable { + public static final String[] RESOURCES_DIRNAMES = { "res", "r", "R" }; + public static final String[] RAW_DIRNAMES = { "assets", "lib", "libs", "kotlin", "META-INF/services" }; + + public static final Pattern ORIGINAL_FILENAMES_PATTERN = Pattern.compile( + "AndroidManifest\\.xml|META-INF/[^/]+\\.(RSA|SF|MF)|stamp-cert-sha256"); + + public static final Pattern STANDARD_FILENAMES_PATTERN = Pattern.compile( + "[^/]+\\.dex|resources\\.arsc|(" + String.join("|", RESOURCES_DIRNAMES) + "|" + + String.join("|", RAW_DIRNAMES) + ")/.*|" + ORIGINAL_FILENAMES_PATTERN.pattern()); + + // only set when loaded from a file (not a stream) private transient ExtFile mApkFile; public String version; public String apkFileName; public boolean isFrameworkApk; public UsesFramework usesFramework; - private Map sdkInfo = new LinkedHashMap<>(); + public Map sdkInfo = new LinkedHashMap<>(); public PackageInfo packageInfo = new PackageInfo(); public VersionInfo versionInfo = new VersionInfo(); - public boolean resourcesAreCompressed; + public Map featureFlags = new LinkedHashMap<>(); public boolean sharedLibrary; public boolean sparseResources; - public Map unknownFiles = new LinkedHashMap<>(); - public List doNotCompress; - - /** @Deprecated use {@link #resourcesAreCompressed} */ - public boolean compressionType; + public boolean compactEntries; + public List doNotCompress = new ArrayList<>(); public ApkInfo() { this(null); } public ApkInfo(ExtFile apkFile) { - this.version = ApktoolProperties.getVersion(); + version = ApktoolProperties.getVersion(); if (apkFile != null) { setApkFile(apkFile); } @@ -62,8 +71,19 @@ public class ApkInfo implements YamlSerializable { public void setApkFile(ExtFile apkFile) { mApkFile = apkFile; - if (this.apkFileName == null) { - this.apkFileName = apkFile.getName(); + if (apkFileName == null) { + apkFileName = apkFile.getName(); + } + } + + public boolean hasSources() throws AndrolibException { + if (mApkFile == null) { + return false; + } + try { + return mApkFile.getDirectory().containsFile("classes.dex"); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); } } @@ -89,41 +109,6 @@ public class ApkInfo implements YamlSerializable { } } - public boolean hasSources() throws AndrolibException { - if (mApkFile == null) { - return false; - } - try { - return mApkFile.getDirectory().containsFile("classes.dex"); - } catch (DirectoryException ex) { - throw new AndrolibException(ex); - } - } - - public boolean hasMultipleSources() throws AndrolibException { - if (mApkFile == null) { - return false; - } - try { - Set files = mApkFile.getDirectory().getFiles(false); - for (String file : files) { - if (file.endsWith(".dex")) { - if (!file.equalsIgnoreCase("classes.dex")) { - return true; - } - } - } - - return false; - } catch (DirectoryException ex) { - throw new AndrolibException(ex); - } - } - - public void addUnknownFileInfo(String file, String value) { - unknownFiles.put(file, value); - } - public String checkTargetSdkVersionBounds() { int target = mapSdkShorthandToVersion(getTargetSdkVersion()); @@ -135,35 +120,35 @@ public class ApkInfo implements YamlSerializable { return Integer.toString(target); } - public Map getSdkInfo() { - return sdkInfo; - } - - public void setSdkInfo(Map sdkInfo) { - this.sdkInfo = sdkInfo; - } - - public void setSdkInfoField(String key, String value) { - sdkInfo.put(key, value); - } - public String getMinSdkVersion() { return sdkInfo.get("minSdkVersion"); } + public void setMinSdkVersion(String minSdkVersion) { + sdkInfo.put("minSdkVersion", minSdkVersion); + } + public String getMaxSdkVersion() { return sdkInfo.get("maxSdkVersion"); } + public void setMaxSdkVersion(String maxSdkVersion) { + sdkInfo.put("maxSdkVersion", maxSdkVersion); + } + public String getTargetSdkVersion() { return sdkInfo.get("targetSdkVersion"); } + public void setTargetSdkVersion(String targetSdkVersion) { + sdkInfo.put("targetSdkVersion", targetSdkVersion); + } + public int getMinSdkVersionFromAndroidCodename(String sdkVersion) { int sdkNumber = mapSdkShorthandToVersion(sdkVersion); if (sdkNumber == ResConfigFlags.SDK_BASE) { - return Integer.parseInt(sdkInfo.get("minSdkVersion")); + return Integer.parseInt(getMinSdkVersion()); } return sdkNumber; } @@ -194,33 +179,41 @@ public class ApkInfo implements YamlSerializable { return ResConfigFlags.SDK_UPSIDEDOWN_CAKE; case "VANILLAICECREAM": case "VANILLA_ICE_CREAM": + return ResConfigFlags.SDK_VANILLA_ICE_CREAM; + case "BAKLAVA": + return ResConfigFlags.SDK_BAKLAVA; + case "SDK_CUR_DEVELOPMENT": return ResConfigFlags.SDK_DEVELOPMENT; default: return Integer.parseInt(sdkVersion); } } + public void addFeatureFlag(String flag, boolean value) { + featureFlags.put(flag, value); + } + public void save(File file) throws AndrolibException { - try (YamlWriter writer = new YamlWriter(new FileOutputStream(file))) { + try (YamlWriter writer = new YamlWriter(Files.newOutputStream(file.toPath()))) { write(writer); - } catch (FileNotFoundException e) { + } catch (FileNotFoundException ex) { throw new AndrolibException("File not found"); - } catch (Exception e) { - throw new AndrolibException(e); + } catch (Exception ex) { + throw new AndrolibException(ex); } } - public static ApkInfo load(InputStream is) throws AndrolibException { - YamlReader reader = new YamlReader(is); + public static ApkInfo load(InputStream in) throws AndrolibException { + YamlReader reader = new YamlReader(in); ApkInfo apkInfo = new ApkInfo(); reader.readRoot(apkInfo); return apkInfo; } - public static ApkInfo load(File appDir) throws AndrolibException { - try (InputStream in = new FileDirectory(appDir).getFileInput("apktool.yml")) { + public static ApkInfo load(ExtFile apkDir) throws AndrolibException { + try (InputStream in = apkDir.getDirectory().getFileInput("apktool.yml")) { ApkInfo apkInfo = ApkInfo.load(in); - apkInfo.setApkFile(new ExtFile(appDir)); + apkInfo.setApkFile(apkDir); return apkInfo; } catch (DirectoryException | IOException ex) { throw new AndrolibException(ex); @@ -232,56 +225,56 @@ public class ApkInfo implements YamlSerializable { YamlLine line = reader.getLine(); switch (line.getKey()) { case "version": { - this.version = line.getValue(); + version = line.getValue(); break; } case "apkFileName": { - this.apkFileName = line.getValue(); + apkFileName = line.getValue(); break; } case "isFrameworkApk": { - this.isFrameworkApk = line.getValueBool(); + isFrameworkApk = line.getValueBool(); break; } case "usesFramework": { - this.usesFramework = new UsesFramework(); + usesFramework = new UsesFramework(); reader.readObject(usesFramework); break; } case "sdkInfo": { - reader.readMap(sdkInfo); + sdkInfo.clear(); + reader.readStringMap(sdkInfo); break; } case "packageInfo": { - this.packageInfo = new PackageInfo(); + packageInfo = new PackageInfo(); reader.readObject(packageInfo); break; } case "versionInfo": { - this.versionInfo = new VersionInfo(); + versionInfo = new VersionInfo(); reader.readObject(versionInfo); break; } - case "compressionType": - case "resourcesAreCompressed": { - this.resourcesAreCompressed = line.getValueBool(); + case "featureFlags": { + featureFlags.clear(); + reader.readBoolMap(featureFlags); break; } case "sharedLibrary": { - this.sharedLibrary = line.getValueBool(); + sharedLibrary = line.getValueBool(); break; } case "sparseResources": { - this.sparseResources = line.getValueBool(); + sparseResources = line.getValueBool(); break; } - case "unknownFiles": { - this.unknownFiles = new LinkedHashMap<>(); - reader.readMap(unknownFiles); + case "compactEntries": { + compactEntries = line.getValueBool(); break; } case "doNotCompress": { - this.doNotCompress = new ArrayList<>(); + doNotCompress.clear(); reader.readStringList(doNotCompress); break; } @@ -294,15 +287,17 @@ public class ApkInfo implements YamlSerializable { writer.writeString("apkFileName", apkFileName); writer.writeBool("isFrameworkApk", isFrameworkApk); writer.writeObject("usesFramework", usesFramework); - writer.writeStringMap("sdkInfo", sdkInfo); + writer.writeMap("sdkInfo", sdkInfo); writer.writeObject("packageInfo", packageInfo); writer.writeObject("versionInfo", versionInfo); - writer.writeBool("resourcesAreCompressed", resourcesAreCompressed); + if (!featureFlags.isEmpty()) { + writer.writeMap("featureFlags", featureFlags); + } writer.writeBool("sharedLibrary", sharedLibrary); writer.writeBool("sparseResources", sparseResources); - if (unknownFiles.size() > 0) { - writer.writeStringMap("unknownFiles", unknownFiles); + writer.writeBool("compactEntries", compactEntries); + if (!doNotCompress.isEmpty()) { + writer.writeList("doNotCompress", doNotCompress); } - writer.writeList("doNotCompress", doNotCompress); } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlLine.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlLine.java index 50091f6a..d95828a7 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlLine.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlLine.java @@ -19,7 +19,6 @@ package brut.androlib.apk; import java.util.Objects; public class YamlLine { - public int indent = 0; private String key = ""; private String value = ""; @@ -62,7 +61,7 @@ public class YamlLine { if (isItem) { // array item line has only the value value = line.substring(1).trim(); - } else { + } else { // split line to key - value String[] parts = line.split(":"); if (parts.length > 0) { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlReader.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlReader.java index 75b1017f..8fd109e8 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlReader.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlReader.java @@ -22,9 +22,8 @@ import java.io.InputStream; import java.util.*; public class YamlReader { - private ArrayList mLines; - private int mCurrent = 0; + private int mCurrent; public YamlReader(InputStream in) { mLines = new ArrayList<>(); @@ -203,7 +202,7 @@ public class YamlReader { readList(list, (items, reader) -> items.add(reader.getLine().getValueInt())); } - public void readMap(Map map) throws AndrolibException { + public void readStringMap(Map map) throws AndrolibException { readObject(map, line -> line.hasColon, (items, reader) -> { @@ -211,4 +210,13 @@ public class YamlReader { items.put(line.getKey(), line.getValue()); }); } + + public void readBoolMap(Map map) throws AndrolibException { + readObject(map, + line -> line.hasColon, + (items, reader) -> { + YamlLine line = reader.getLine(); + items.put(line.getKey(), line.getValueBool()); + }); + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlSerializable.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlSerializable.java index f477bba5..2dc272a3 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlSerializable.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlSerializable.java @@ -20,5 +20,6 @@ import brut.androlib.exceptions.AndrolibException; public interface YamlSerializable { void readItem(YamlReader reader) throws AndrolibException; + void write(YamlWriter writer); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlStringEscapeUtils.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlStringEscapeUtils.java index 574409c3..fb947e82 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlStringEscapeUtils.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlStringEscapeUtils.java @@ -23,7 +23,11 @@ import java.io.IOException; import java.io.StringWriter; import java.io.Writer; -public class YamlStringEscapeUtils { +public final class YamlStringEscapeUtils { + + private YamlStringEscapeUtils() { + // Private constructor for utility class + } public static String escapeString(String str) { return escapeJavaStyleString(str); @@ -48,12 +52,12 @@ public class YamlStringEscapeUtils { } /** - * @param out write to receive the escaped string + * @param writer Writer to receive the escaped string * @param str String to escape values in, may be null * @throws IOException if an IOException occurs */ - private static void escapeJavaStyleString(Writer out, String str) throws IOException { - if (out == null) { + private static void escapeJavaStyleString(Writer writer, String str) throws IOException { + if (writer == null) { throw new IllegalArgumentException("The Writer must not be null"); } if (str == null) { @@ -66,57 +70,51 @@ public class YamlStringEscapeUtils { // "[^\t\n\r\u0020-\u007E\u0085\u00A0-\uD7FF\uE000-\uFFFD]" // handle unicode if (ch > 0xFFFD) { - out.write("\\u" + CharSequenceTranslator.hex(ch)); + writer.write("\\u" + CharSequenceTranslator.hex(ch)); } else if (ch > 0xD7FF && ch < 0xE000) { - out.write("\\u" + CharSequenceTranslator.hex(ch)); + writer.write("\\u" + CharSequenceTranslator.hex(ch)); } else if (ch > 0x7E && ch != 0x85 && ch < 0xA0) { - out.write("\\u00" + CharSequenceTranslator.hex(ch)); + writer.write("\\u00" + CharSequenceTranslator.hex(ch)); } else if (ch < 32) { switch (ch) { case '\t' : - out.write('\\'); - out.write('t'); + writer.write('\\'); + writer.write('t'); break; case '\n' : - out.write('\\'); - out.write('n'); + writer.write('\\'); + writer.write('n'); break; case '\r' : - out.write('\\'); - out.write('r'); + writer.write('\\'); + writer.write('r'); break; default : if (ch > 0xf) { - out.write("\\u00" + CharSequenceTranslator.hex(ch)); + writer.write("\\u00" + CharSequenceTranslator.hex(ch)); } else { - out.write("\\u000" + CharSequenceTranslator.hex(ch)); + writer.write("\\u000" + CharSequenceTranslator.hex(ch)); } break; } } else { switch (ch) { case '\'' : - if (false) { - out.write('\\'); - } - out.write('\''); + writer.write('\''); break; case '"' : - out.write('\\'); - out.write('"'); + writer.write('\\'); + writer.write('"'); break; case '\\' : - out.write('\\'); - out.write('\\'); + writer.write('\\'); + writer.write('\\'); break; case '/' : - if (false) { - out.write('\\'); - } - out.write('/'); + writer.write('/'); break; default : - out.write(ch); + writer.write(ch); break; } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlWriter.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlWriter.java index 902b96e8..133d6d38 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlWriter.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/YamlWriter.java @@ -21,10 +21,10 @@ import java.nio.charset.StandardCharsets; import java.util.*; public class YamlWriter implements Closeable { + private static final String QUOTE = "'"; - private int mIndent = 0; private final PrintWriter mWriter; - private final String QUOTE = "'"; + private int mIndent; public YamlWriter(OutputStream out) { mWriter = new PrintWriter(new BufferedWriter( @@ -89,21 +89,21 @@ public class YamlWriter implements Closeable { } writeIndent(); mWriter.println(escape(key) + ":"); - for (T item: list) { + for (T item : list) { writeIndent(); - mWriter.println("- " + item); + mWriter.println("- " + item); } } - public void writeStringMap(String key, Map map) { + public void writeMap(String key, Map map) { if (Objects.isNull(map)) { return; } writeIndent(); mWriter.println(escape(key) + ":"); nextIndent(); - for (String mapKey: map.keySet()) { - writeString(mapKey, map.get(mapKey)); + for (String mapKey : map.keySet()) { + writeString(mapKey, String.valueOf(map.get(mapKey))); } prevIndent(); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/AXmlDecodingException.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/AXmlDecodingException.java index 1e740b28..c094e8f6 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/AXmlDecodingException.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/AXmlDecodingException.java @@ -17,6 +17,7 @@ package brut.androlib.exceptions; public class AXmlDecodingException extends AndrolibException { + public AXmlDecodingException(String message, Throwable cause) { super(message, cause); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/AndrolibException.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/AndrolibException.java index e72da574..6c73b20f 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/AndrolibException.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/AndrolibException.java @@ -19,18 +19,20 @@ package brut.androlib.exceptions; import brut.common.BrutException; public class AndrolibException extends BrutException { + public AndrolibException() { + super(); } public AndrolibException(String message) { super(message); } - public AndrolibException(String message, Throwable cause) { - super(message, cause); - } - public AndrolibException(Throwable cause) { super(cause); } + + public AndrolibException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/CantFind9PatchChunkException.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/CantFind9PatchChunkException.java index 841f9823..d67f63d9 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/CantFind9PatchChunkException.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/CantFind9PatchChunkException.java @@ -17,7 +17,8 @@ package brut.androlib.exceptions; public class CantFind9PatchChunkException extends AndrolibException { - public CantFind9PatchChunkException(String message, Throwable cause) { - super(message, cause); - } + + public CantFind9PatchChunkException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/CantFindFrameworkResException.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/CantFindFrameworkResException.java index 1dc55231..cafa4341 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/CantFindFrameworkResException.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/CantFindFrameworkResException.java @@ -17,18 +17,18 @@ package brut.androlib.exceptions; public class CantFindFrameworkResException extends AndrolibException { - public CantFindFrameworkResException(int id) { - mPkgId = id; - } + private final int mPkgId; - public int getPkgId() { - return mPkgId; - } + public CantFindFrameworkResException(int pkgId) { + mPkgId = pkgId; + } - @Override - public String getMessage() { - return String.format("Can't find framework resources for package of id: %d", this.getPkgId()); - } + public int getPkgId() { + return mPkgId; + } - private final int mPkgId; + @Override + public String getMessage() { + return String.format("Could not find framework resources for package of id: %d", mPkgId); + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/InFileNotFoundException.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/InFileNotFoundException.java index 508682b2..07d3e63d 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/InFileNotFoundException.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/InFileNotFoundException.java @@ -17,6 +17,8 @@ package brut.androlib.exceptions; public class InFileNotFoundException extends AndrolibException { - public InFileNotFoundException() { - } + + public InFileNotFoundException() { + super(); + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/OutDirExistsException.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/OutDirExistsException.java index 2cd8346d..b6c3ec8d 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/OutDirExistsException.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/OutDirExistsException.java @@ -17,6 +17,8 @@ package brut.androlib.exceptions; public class OutDirExistsException extends AndrolibException { - public OutDirExistsException() { - } + + public OutDirExistsException() { + super(); + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/RawXmlEncounteredException.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/RawXmlEncounteredException.java index c8a4aabe..b44b1166 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/RawXmlEncounteredException.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/RawXmlEncounteredException.java @@ -17,6 +17,7 @@ package brut.androlib.exceptions; public class RawXmlEncounteredException extends AndrolibException { + public RawXmlEncounteredException(String message, Throwable cause) { super(message, cause); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/UndefinedResObjectException.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/UndefinedResObjectException.java index 14b61d84..8df878d2 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/UndefinedResObjectException.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions/UndefinedResObjectException.java @@ -17,7 +17,8 @@ package brut.androlib.exceptions; public class UndefinedResObjectException extends AndrolibException { - public UndefinedResObjectException(String message) { - super(message); - } + + public UndefinedResObjectException(String message) { + super(message); + } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/mod/SmaliMod.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/mod/SmaliMod.java index 26652e58..fadaee45 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/mod/SmaliMod.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/mod/SmaliMod.java @@ -30,59 +30,53 @@ import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -public class SmaliMod { +public final class SmaliMod { + + private SmaliMod() { + // Private constructor for utility class + } + public static boolean assembleSmaliFile(File smaliFile, DexBuilder dexBuilder, int apiLevel, boolean verboseErrors, boolean printTokens) throws IOException, RecognitionException { + try (InputStreamReader reader = new InputStreamReader( + Files.newInputStream(smaliFile.toPath()), StandardCharsets.UTF_8)) { + smaliFlexLexer lexer = new smaliFlexLexer(reader, apiLevel); + lexer.setSourceFile(smaliFile); + CommonTokenStream tokens = new CommonTokenStream(lexer); - CommonTokenStream tokens; - smaliFlexLexer lexer; + if (printTokens) { + tokens.getTokens(); - InputStream is = Files.newInputStream(smaliFile.toPath()); - InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8); - - lexer = new smaliFlexLexer(reader, apiLevel); - (lexer).setSourceFile(smaliFile); - tokens = new CommonTokenStream(lexer); - - if (printTokens) { - tokens.getTokens(); - - for (int i=0; i 0 || lexer.getNumberOfSyntaxErrors() > 0) { + return false; + } + + CommonTree t = result.getTree(); + + CommonTreeNodeStream treeStream = new CommonTreeNodeStream(t); + treeStream.setTokenStream(tokens); + + smaliTreeWalker dexGen = new smaliTreeWalker(treeStream); + dexGen.setApiLevel(apiLevel); + dexGen.setVerboseErrors(verboseErrors); + dexGen.setDexBuilder(dexBuilder); + dexGen.smali_file(); + + return dexGen.getNumberOfSyntaxErrors() == 0; } - - smaliParser parser = new smaliParser(tokens); - parser.setApiLevel(apiLevel); - parser.setVerboseErrors(verboseErrors); - - smaliParser.smali_file_return result = parser.smali_file(); - - if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) { - is.close(); - reader.close(); - return false; - } - - CommonTree t = result.getTree(); - - CommonTreeNodeStream treeStream = new CommonTreeNodeStream(t); - treeStream.setTokenStream(tokens); - - smaliTreeWalker dexGen = new smaliTreeWalker(treeStream); - dexGen.setApiLevel(apiLevel); - dexGen.setVerboseErrors(verboseErrors); - dexGen.setDexBuilder(dexBuilder); - dexGen.smali_file(); - - is.close(); - reader.close(); - - return dexGen.getNumberOfSyntaxErrors() == 0; } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/Framework.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/Framework.java index 87148180..bc15fd95 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/Framework.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/Framework.java @@ -22,8 +22,7 @@ import brut.androlib.exceptions.CantFindFrameworkResException; import brut.androlib.res.decoder.ARSCDecoder; import brut.androlib.res.data.arsc.ARSCData; import brut.androlib.res.data.arsc.FlagsOffset; -import brut.util.Jar; -import org.apache.commons.io.IOUtils; +import brut.util.BrutIO; import java.io.*; import java.nio.file.Files; @@ -35,76 +34,65 @@ import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; public class Framework { - private final Config config; + private static final Logger LOGGER = Logger.getLogger(Framework.class.getName()); - private File mFrameworkDirectory = null; - - private final static Logger LOGGER = Logger.getLogger(Framework.class.getName()); + private final Config mConfig; + private File mFrameworkDirectory; public Framework(Config config) { - this.config = config; + mConfig = config; } public void installFramework(File frameFile) throws AndrolibException { - installFramework(frameFile, config.frameworkTag); + installFramework(frameFile, mConfig.frameworkTag); } public void installFramework(File frameFile, String tag) throws AndrolibException { - InputStream in = null; - ZipOutputStream out = null; - try { - ZipFile zip = new ZipFile(frameFile); + try (ZipFile zip = new ZipFile(frameFile)) { ZipEntry entry = zip.getEntry("resources.arsc"); if (entry == null) { - throw new AndrolibException("Can't find resources.arsc file"); + throw new AndrolibException("Could not find resources.arsc file"); } - in = zip.getInputStream(entry); - byte[] data = IOUtils.toByteArray(in); - - ARSCData arsc = ARSCDecoder.decode(new ByteArrayInputStream(data), true, true); + byte[] data = BrutIO.readAndClose(zip.getInputStream(entry)); + ARSCDecoder decoder = new ARSCDecoder(new ByteArrayInputStream(data), null, true, true); + ARSCData arsc = decoder.decode(); publicizeResources(data, arsc.getFlagsOffsets()); - File outFile = new File(getFrameworkDirectory(), arsc - .getOnePackage().getId() - + (tag == null ? "" : '-' + tag) - + ".apk"); + File outFile = new File(getFrameworkDirectory(), + arsc.getOnePackage().getId() + (tag == null ? "" : '-' + tag) + ".apk"); - out = new ZipOutputStream(Files.newOutputStream(outFile.toPath())); - out.setMethod(ZipOutputStream.STORED); - CRC32 crc = new CRC32(); - crc.update(data); - entry = new ZipEntry("resources.arsc"); - entry.setSize(data.length); - entry.setMethod(ZipEntry.STORED); - entry.setCrc(crc.getValue()); - out.putNextEntry(entry); - out.write(data); - out.closeEntry(); - - //Write fake AndroidManifest.xml file to support original aapt - entry = zip.getEntry("AndroidManifest.xml"); - if (entry != null) { - in = zip.getInputStream(entry); - byte[] manifest = IOUtils.toByteArray(in); - CRC32 manifestCrc = new CRC32(); - manifestCrc.update(manifest); - entry.setSize(manifest.length); - entry.setCompressedSize(-1); - entry.setCrc(manifestCrc.getValue()); + try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(outFile.toPath()))) { + out.setMethod(ZipOutputStream.STORED); + CRC32 crc = new CRC32(); + crc.update(data); + entry = new ZipEntry("resources.arsc"); + entry.setSize(data.length); + entry.setMethod(ZipEntry.STORED); + entry.setCrc(crc.getValue()); out.putNextEntry(entry); - out.write(manifest); + out.write(data); out.closeEntry(); + + // write fake AndroidManifest.xml file to support original aapt + entry = zip.getEntry("AndroidManifest.xml"); + if (entry != null) { + byte[] manifest = BrutIO.readAndClose(zip.getInputStream(entry)); + CRC32 manifestCrc = new CRC32(); + manifestCrc.update(manifest); + entry.setSize(manifest.length); + entry.setCompressedSize(-1); + entry.setCrc(manifestCrc.getValue()); + out.putNextEntry(entry); + out.write(manifest); + out.closeEntry(); + } } - zip.close(); LOGGER.info("Framework installed to: " + outFile); } catch (IOException ex) { throw new AndrolibException(ex); - } finally { - IOUtils.closeQuietly(in); - IOUtils.closeQuietly(out); } } @@ -125,8 +113,10 @@ public class Framework { public void publicizeResources(File arscFile) throws AndrolibException { byte[] data = new byte[(int) arscFile.length()]; - try(InputStream in = Files.newInputStream(arscFile.toPath()); - OutputStream out = Files.newOutputStream(arscFile.toPath())) { + try ( + InputStream in = Files.newInputStream(arscFile.toPath()); + OutputStream out = Files.newOutputStream(arscFile.toPath()) + ) { //noinspection ResultOfMethodCallIgnored in.read(data); publicizeResources(data); @@ -136,16 +126,18 @@ public class Framework { } } - private void publicizeResources(byte[] arsc) throws AndrolibException { - publicizeResources(arsc, ARSCDecoder.decode(new ByteArrayInputStream(arsc), true, true).getFlagsOffsets()); + private void publicizeResources(byte[] data) throws AndrolibException { + ARSCDecoder decoder = new ARSCDecoder(new ByteArrayInputStream(data), null, true, true); + ARSCData arsc = decoder.decode(); + publicizeResources(data, arsc.getFlagsOffsets()); } - public void publicizeResources(byte[] arsc, FlagsOffset[] flagsOffsets) { + public void publicizeResources(byte[] data, FlagsOffset[] flagsOffsets) { for (FlagsOffset flags : flagsOffsets) { int offset = flags.offset + 3; int end = offset + 4 * flags.count; while (offset < end) { - arsc[offset] |= (byte) 0x40; + data[offset] |= (byte) 0x40; offset += 4; } } @@ -159,7 +151,7 @@ public class Framework { String path; // use default framework path or specified on the command line - path = config.frameworkDirectory; + path = mConfig.frameworkDirectory; File dir = new File(path); @@ -171,19 +163,19 @@ public class Framework { throw new AndrolibException("Please remove file at " + dir.getParentFile()); } - if (! dir.exists()) { - if (! dir.mkdirs()) { - if (config.frameworkDirectory != null) { - LOGGER.severe("Can't create Framework directory: " + dir); + if (!dir.exists()) { + if (!dir.mkdirs()) { + if (mConfig.frameworkDirectory != null) { + LOGGER.severe("Could not create Framework directory: " + dir); } throw new AndrolibException(String.format( - "Can't create directory: (%s). Pass a writable path with --frame-path {DIR}. ", dir + "Could not create directory: (%s). Pass a writable path with --frame-path {DIR}. ", dir )); } } - if (config.frameworkDirectory == null) { - if (! dir.canWrite()) { + if (mConfig.frameworkDirectory == null) { + if (!dir.canWrite()) { LOGGER.severe(String.format("WARNING: Could not write to (%1$s), using %2$s instead...", dir.getAbsolutePath(), System.getProperty("java.io.tmpdir"))); LOGGER.severe("Please be aware this is a volatile directory and frameworks could go missing, " + @@ -214,15 +206,12 @@ public class Framework { } if (id == 1) { - try ( - InputStream in = getAndroidFrameworkResourcesAsStream(); - OutputStream out = Files.newOutputStream(apk.toPath()) - ) { - IOUtils.copy(in, out); - return apk; + try { + BrutIO.copyAndClose(getAndroidFrameworkResourcesAsStream(), Files.newOutputStream(apk.toPath())); } catch (IOException ex) { throw new AndrolibException(ex); } + return apk; } throw new CantFindFrameworkResException(id); @@ -234,11 +223,11 @@ public class Framework { apk = new File(dir, "1.apk"); - if (! apk.exists()) { - LOGGER.warning("Can't empty framework directory, no file found at: " + apk.getAbsolutePath()); + if (!apk.exists()) { + LOGGER.warning("Could not empty framework directory, no file found at: " + apk.getAbsolutePath()); } else { try { - if (apk.exists() && Objects.requireNonNull(dir.listFiles()).length > 1 && ! config.forceDeleteFramework) { + if (apk.exists() && Objects.requireNonNull(dir.listFiles()).length > 1 && !mConfig.forceDeleteFramework) { LOGGER.warning("More than default framework detected. Please run command with `--force` parameter to wipe framework directory."); } else { for (File file : Objects.requireNonNull(dir.listFiles())) { @@ -249,13 +238,13 @@ public class Framework { } } } - } catch (NullPointerException e) { - throw new AndrolibException(e); + } catch (NullPointerException ex) { + throw new AndrolibException(ex); } } } private InputStream getAndroidFrameworkResourcesAsStream() { - return Jar.class.getResourceAsStream("/brut/androlib/android-framework.jar"); + return Framework.class.getResourceAsStream("/prebuilt/android-framework.jar"); } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java index 5ab890e6..98b75733 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java @@ -21,13 +21,13 @@ import brut.androlib.apk.ApkInfo; import brut.androlib.exceptions.AndrolibException; import brut.androlib.res.data.*; import brut.androlib.res.decoder.*; -import brut.androlib.res.util.ExtMXSerializer; -import brut.androlib.res.util.ExtXmlSerializer; import brut.androlib.res.xml.ResValuesXmlSerializable; -import brut.androlib.res.xml.ResXmlPatcher; +import brut.androlib.res.xml.ResXmlUtils; import brut.directory.Directory; import brut.directory.DirectoryException; -import brut.directory.FileDirectory; +import brut.directory.ExtFile; +import brut.xmlpull.MXSerializer; +import com.google.common.collect.Sets; import brut.util.OSDetection; import org.xmlpull.v1.XmlSerializer; @@ -36,21 +36,23 @@ import java.util.*; import java.util.logging.Logger; public class ResourcesDecoder { - private final static Logger LOGGER = Logger.getLogger(ResourcesDecoder.class.getName()); + private static final Logger LOGGER = Logger.getLogger(ResourcesDecoder.class.getName()); + + private static final Set IGNORED_PACKAGES = Sets.newHashSet( + "android", "com.htc", "com.lge", "com.lge.internal", "yi", "flyme", "air.com.adobe.appentry", + "FFFFFFFFFFFFFFFFFFFFFF" + ); private final Config mConfig; private final ApkInfo mApkInfo; private final ResTable mResTable; - private final Map mResFileMapping = new HashMap<>(); - - private final static String[] IGNORED_PACKAGES = new String[] { - "android", "com.htc", "com.lge", "com.lge.internal", "yi", "flyme", "air.com.adobe.appentry", - "FFFFFFFFFFFFFFFFFFFFFF" }; + private final Map mResFileMapping; public ResourcesDecoder(Config config, ApkInfo apkInfo) { mConfig = config; mApkInfo = apkInfo; mResTable = new ResTable(mConfig, mApkInfo); + mResFileMapping = new HashMap<>(); } public ResTable getResTable() throws AndrolibException { @@ -61,7 +63,7 @@ public class ResourcesDecoder { return mResTable; } - public Map getResFileMapping() { + public Map getResFileMapping() { return mResFileMapping; } @@ -69,56 +71,67 @@ public class ResourcesDecoder { mResTable.loadMainPkg(mApkInfo.getApkFile()); } - public void decodeManifest(File outDir) throws AndrolibException { + public void decodeManifest(File apkDir) throws AndrolibException { if (!mApkInfo.hasManifest()) { return; } AXmlResourceParser axmlParser = new AndroidManifestResourceParser(mResTable); - XmlPullStreamDecoder fileDecoder = new XmlPullStreamDecoder(axmlParser, getResXmlSerializer()); + XmlSerializer xmlSerializer = newXmlSerializer(); + ResStreamDecoder fileDecoder = new AndroidManifestPullStreamDecoder(axmlParser, xmlSerializer); - Directory inApk, out; + Directory inDir, outDir; try { - inApk = mApkInfo.getApkFile().getDirectory(); - out = new FileDirectory(outDir); + inDir = mApkInfo.getApkFile().getDirectory(); + outDir = new ExtFile(apkDir).getDirectory(); if (mApkInfo.hasResources()) { LOGGER.info("Decoding AndroidManifest.xml with resources..."); } else { LOGGER.info("Decoding AndroidManifest.xml with only framework resources..."); } - InputStream inputStream = inApk.getFileInput("AndroidManifest.xml"); - OutputStream outputStream = out.getFileOutput("AndroidManifest.xml"); - fileDecoder.decodeManifest(inputStream, outputStream); - } catch (DirectoryException ex) { + try ( + InputStream in = inDir.getFileInput("AndroidManifest.xml"); + OutputStream out = outDir.getFileOutput("AndroidManifest.xml") + ) { + fileDecoder.decode(in, out); + } + } catch (DirectoryException | IOException ex) { throw new AndrolibException(ex); } - if (mApkInfo.hasResources()) { - if (!mConfig.analysisMode) { - // Remove versionName / versionCode (aapt API 16) - // - // check for a mismatch between resources.arsc package and the package listed in AndroidManifest - // also remove the android::versionCode / versionName from manifest for rebuild - // this is a required change to prevent aapt warning about conflicting versions - // it will be passed as a parameter to aapt like "--min-sdk-version" via apktool.yml - adjustPackageManifest(outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml"); + File manifest = new File(apkDir, "AndroidManifest.xml"); - ResXmlPatcher.removeManifestVersions(new File( - outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml")); + if (mApkInfo.hasResources() && !mConfig.analysisMode) { + // Remove versionName / versionCode (aapt API 16) + // + // check for a mismatch between resources.arsc package and the package listed in AndroidManifest + // also remove the android::versionCode / versionName from manifest for rebuild + // this is a required change to prevent aapt warning about conflicting versions + // it will be passed as a parameter to aapt like "--min-sdk-version" via apktool.yml + adjustPackageManifest(manifest); - // update apk info - mApkInfo.packageInfo.forcedPackageId = String.valueOf(mResTable.getPackageId()); + ResXmlUtils.removeManifestVersions(manifest); + + // update apk info + mApkInfo.packageInfo.forcedPackageId = String.valueOf(mResTable.getPackageId()); + } + + // record feature flags + List featureFlags = ResXmlUtils.pullManifestFeatureFlags(manifest); + if (featureFlags != null) { + for (String flag : featureFlags) { + mApkInfo.addFeatureFlag(flag, true); } } } - public void updateApkInfo(File outDir) throws AndrolibException { - mResTable.initApkInfo(mApkInfo, outDir); + public void updateApkInfo(File apkDir) throws AndrolibException { + mResTable.initApkInfo(mApkInfo, apkDir); } - private void adjustPackageManifest(String filePath) throws AndrolibException { + private void adjustPackageManifest(File manifest) throws AndrolibException { // compare resources.arsc package name to the one present in AndroidManifest ResPackage resPackage = mResTable.getCurrentResPackage(); String pkgOriginal = resPackage.getName(); @@ -131,16 +144,16 @@ public class ResourcesDecoder { // 2) Check if pkgRenamed is null // 3) Check if pkgOriginal === mPackageRenamed // 4) Check if pkgOriginal is ignored via IGNORED_PACKAGES - if (pkgOriginal == null || pkgRenamed == null || pkgOriginal.equalsIgnoreCase(pkgRenamed) - || (Arrays.asList(IGNORED_PACKAGES).contains(pkgOriginal))) { + if (pkgOriginal == null || pkgRenamed == null || pkgOriginal.equals(pkgRenamed) + || IGNORED_PACKAGES.contains(pkgOriginal)) { LOGGER.info("Regular manifest package..."); } else { LOGGER.info("Renamed manifest package found! Replacing " + pkgRenamed + " with " + pkgOriginal); - ResXmlPatcher.renameManifestPackage(new File(filePath), pkgOriginal); + ResXmlUtils.renameManifestPackage(manifest, pkgOriginal); } } - public void decodeResources(File outDir) throws AndrolibException { + public void decodeResources(File apkDir) throws AndrolibException { if (!mApkInfo.hasResources()) { return; } @@ -156,32 +169,31 @@ public class ResourcesDecoder { ); AXmlResourceParser axmlParser = new AXmlResourceParser(mResTable); - decoders.setDecoder("xml", new XmlPullStreamDecoder(axmlParser, getResXmlSerializer())); + XmlSerializer xmlSerializer = newXmlSerializer(); + decoders.setDecoder("xml", new ResXmlPullStreamDecoder(axmlParser, xmlSerializer)); ResFileDecoder fileDecoder = new ResFileDecoder(decoders); - Directory in, out, outRes; + Directory inDir, outDir; try { - out = new FileDirectory(outDir); - in = mApkInfo.getApkFile().getDirectory(); - outRes = out.createDir("res"); + inDir = mApkInfo.getApkFile().getDirectory(); + outDir = new ExtFile(apkDir).getDirectory().createDir("res"); } catch (DirectoryException ex) { throw new AndrolibException(ex); } - ExtMXSerializer xmlSerializer = getResXmlSerializer(); for (ResPackage pkg : mResTable.listMainPackages()) { - LOGGER.info("Decoding file-resources..."); for (ResResource res : pkg.listFiles()) { - fileDecoder.decode(res, in, outRes, mResFileMapping); + fileDecoder.decode(res, inDir, outDir, mResFileMapping); } LOGGER.info("Decoding values */* XMLs..."); for (ResValuesFile valuesFile : pkg.listValuesFiles()) { - generateValuesFile(valuesFile, outRes, xmlSerializer); + generateValuesFile(valuesFile, outDir, xmlSerializer); } - generatePublicXml(pkg, outRes, xmlSerializer); + + generatePublicXml(pkg, outDir, xmlSerializer); } AndrolibException decodeError = axmlParser.getFirstError(); @@ -190,20 +202,23 @@ public class ResourcesDecoder { } } - public ExtMXSerializer getResXmlSerializer() { - ExtMXSerializer serial = new ExtMXSerializer(); - serial.setProperty(ExtXmlSerializer.PROPERTY_SERIALIZER_INDENTATION, " "); - serial.setProperty(ExtXmlSerializer.PROPERTY_SERIALIZER_LINE_SEPARATOR, System.getProperty("line.separator")); - serial.setProperty(ExtXmlSerializer.PROPERTY_DEFAULT_ENCODING, "utf-8"); - serial.setDisabledAttrEscape(true); - return serial; + public XmlSerializer newXmlSerializer() throws AndrolibException { + try { + XmlSerializer serial = new MXSerializer(); + serial.setFeature(MXSerializer.FEATURE_ATTR_VALUE_NO_ESCAPE, true); + serial.setProperty(MXSerializer.PROPERTY_DEFAULT_ENCODING, "utf-8"); + serial.setProperty(MXSerializer.PROPERTY_INDENTATION, " "); + serial.setProperty(MXSerializer.PROPERTY_LINE_SEPARATOR, System.getProperty("line.separator")); + return serial; + } catch (IllegalArgumentException | IllegalStateException ex) { + throw new AndrolibException(ex); + } } - private void generateValuesFile(ResValuesFile valuesFile, Directory out, - ExtXmlSerializer serial) throws AndrolibException { - try { - OutputStream outStream = out.getFileOutput(valuesFile.getPath()); - serial.setOutput((outStream), null); + private void generateValuesFile(ResValuesFile valuesFile, Directory resDir, XmlSerializer serial) + throws AndrolibException { + try (OutputStream out = resDir.getFileOutput(valuesFile.getPath())) { + serial.setOutput(out, null); serial.startDocument(null, null); serial.startTag(null, "resources"); @@ -215,20 +230,17 @@ public class ResourcesDecoder { } serial.endTag(null, "resources"); - serial.newLine(); serial.endDocument(); serial.flush(); - outStream.close(); - } catch (IOException | DirectoryException ex) { + } catch (DirectoryException | IOException ex) { throw new AndrolibException("Could not generate: " + valuesFile.getPath(), ex); } } - private void generatePublicXml(ResPackage pkg, Directory out, - XmlSerializer serial) throws AndrolibException { - try { - OutputStream outStream = out.getFileOutput("values/public.xml"); - serial.setOutput(outStream, null); + private void generatePublicXml(ResPackage pkg, Directory resDir, XmlSerializer serial) + throws AndrolibException { + try (OutputStream out = resDir.getFileOutput("values/public.xml")) { + serial.setOutput(out, null); serial.startDocument(null, null); serial.startTag(null, "resources"); @@ -243,8 +255,7 @@ public class ResourcesDecoder { serial.endTag(null, "resources"); serial.endDocument(); serial.flush(); - outStream.close(); - } catch (IOException | DirectoryException ex) { + } catch (DirectoryException | IOException ex) { throw new AndrolibException("Could not generate public.xml file", ex); } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java index 1d27b77e..b75aa0e9 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java @@ -19,6 +19,165 @@ package brut.androlib.res.data; import java.util.logging.Logger; public class ResConfigFlags { + private static final Logger LOGGER = Logger.getLogger(ResConfigFlags.class.getName()); + + public static final byte SDK_BASE = 1; + public static final byte SDK_BASE_1_1 = 2; + public static final byte SDK_CUPCAKE = 3; + public static final byte SDK_DONUT = 4; + public static final byte SDK_ECLAIR = 5; + public static final byte SDK_ECLAIR_0_1 = 6; + public static final byte SDK_ECLAIR_MR1 = 7; + public static final byte SDK_FROYO = 8; + public static final byte SDK_GINGERBREAD = 9; + public static final byte SDK_GINGERBREAD_MR1 = 10; + public static final byte SDK_HONEYCOMB = 11; + public static final byte SDK_HONEYCOMB_MR1 = 12; + public static final byte SDK_HONEYCOMB_MR2 = 13; + public static final byte SDK_ICE_CREAM_SANDWICH = 14; + public static final byte SDK_ICE_CREAM_SANDWICH_MR1 = 15; + public static final byte SDK_JELLY_BEAN = 16; + public static final byte SDK_JELLY_BEAN_MR1 = 17; + public static final byte SDK_JELLY_BEAN_MR2 = 18; + public static final byte SDK_KITKAT = 19; + public static final byte SDK_LOLLIPOP = 21; + public static final byte SDK_LOLLIPOP_MR1 = 22; + public static final byte SDK_MNC = 23; + public static final byte SDK_NOUGAT = 24; + public static final byte SDK_NOUGAT_MR1 = 25; + public static final byte SDK_OREO = 26; + public static final byte SDK_OREO_MR1 = 27; + public static final byte SDK_P = 28; + public static final byte SDK_Q = 29; + public static final byte SDK_R = 30; + public static final byte SDK_S = 31; + public static final byte SDK_S_V2 = 32; + public static final byte SDK_TIRAMISU = 33; + public static final byte SDK_UPSIDEDOWN_CAKE = 34; + public static final byte SDK_VANILLA_ICE_CREAM = 35; + + // AOSP changed Build IDs during QPR2 of API 34 (Upsidedown Cake), restarting at A. + // However, API 35 (Vanilla) took letter A (AP2A), so we start at B. + public static final byte SDK_BAKLAVA = 36; + + // AOSP has this as 10,000 for dev purposes. + // platform_frameworks_base/commit/c7a1109a1fe0771d4c9b572dcf178e2779fc4f2d + public static final int SDK_DEVELOPMENT = 10000; + + public static final byte ORIENTATION_ANY = 0; + public static final byte ORIENTATION_PORT = 1; + public static final byte ORIENTATION_LAND = 2; + public static final byte ORIENTATION_SQUARE = 3; + + public static final byte TOUCHSCREEN_ANY = 0; + public static final byte TOUCHSCREEN_NOTOUCH = 1; + public static final byte TOUCHSCREEN_STYLUS = 2; + public static final byte TOUCHSCREEN_FINGER = 3; + + public static final int DENSITY_DEFAULT = 0; + public static final int DENSITY_LOW = 120; + public static final int DENSITY_MEDIUM = 160; + public static final int DENSITY_400 = 190; + public static final int DENSITY_TV = 213; + public static final int DENSITY_HIGH = 240; + public static final int DENSITY_XHIGH = 320; + public static final int DENSITY_XXHIGH = 480; + public static final int DENSITY_XXXHIGH = 640; + public static final int DENSITY_ANY = 0xFFFE; + public static final int DENSITY_NONE = 0xFFFF; + + public static final int MNC_ZERO = -1; + + public static final short MASK_LAYOUTDIR = 0xc0; + public static final short SCREENLAYOUT_LAYOUTDIR_ANY = 0x00; + public static final short SCREENLAYOUT_LAYOUTDIR_LTR = 0x40; + public static final short SCREENLAYOUT_LAYOUTDIR_RTL = 0x80; + public static final short SCREENLAYOUT_LAYOUTDIR_SHIFT = 0x06; + + public static final short MASK_SCREENROUND = 0x03; + public static final short SCREENLAYOUT_ROUND_ANY = 0; + public static final short SCREENLAYOUT_ROUND_NO = 0x1; + public static final short SCREENLAYOUT_ROUND_YES = 0x2; + + public static final byte GRAMMATICAL_GENDER_ANY = 0; + public static final byte GRAMMATICAL_GENDER_NEUTER = 1; + public static final byte GRAMMATICAL_GENDER_FEMININE = 2; + public static final byte GRAMMATICAL_GENDER_MASCULINE = 3; + + public static final byte KEYBOARD_ANY = 0; + public static final byte KEYBOARD_NOKEYS = 1; + public static final byte KEYBOARD_QWERTY = 2; + public static final byte KEYBOARD_12KEY = 3; + + public static final byte NAVIGATION_ANY = 0; + public static final byte NAVIGATION_NONAV = 1; + public static final byte NAVIGATION_DPAD = 2; + public static final byte NAVIGATION_TRACKBALL = 3; + public static final byte NAVIGATION_WHEEL = 4; + + public static final byte MASK_KEYSHIDDEN = 0x3; + public static final byte KEYSHIDDEN_ANY = 0x0; + public static final byte KEYSHIDDEN_NO = 0x1; + public static final byte KEYSHIDDEN_YES = 0x2; + public static final byte KEYSHIDDEN_SOFT = 0x3; + + public static final byte MASK_NAVHIDDEN = 0xc; + public static final byte NAVHIDDEN_ANY = 0x0; + public static final byte NAVHIDDEN_NO = 0x4; + public static final byte NAVHIDDEN_YES = 0x8; + + public static final byte MASK_SCREENSIZE = 0x0f; + public static final byte SCREENSIZE_ANY = 0x00; + public static final byte SCREENSIZE_SMALL = 0x01; + public static final byte SCREENSIZE_NORMAL = 0x02; + public static final byte SCREENSIZE_LARGE = 0x03; + public static final byte SCREENSIZE_XLARGE = 0x04; + + public static final byte MASK_SCREENLONG = 0x30; + public static final byte SCREENLONG_ANY = 0x00; + public static final byte SCREENLONG_NO = 0x10; + public static final byte SCREENLONG_YES = 0x20; + + public static final byte MASK_UI_MODE_TYPE = 0x0f; + public static final byte UI_MODE_TYPE_ANY = 0x00; + public static final byte UI_MODE_TYPE_NORMAL = 0x01; + public static final byte UI_MODE_TYPE_DESK = 0x02; + public static final byte UI_MODE_TYPE_CAR = 0x03; + public static final byte UI_MODE_TYPE_TELEVISION = 0x04; + public static final byte UI_MODE_TYPE_APPLIANCE = 0x05; + public static final byte UI_MODE_TYPE_WATCH = 0x06; + public static final byte UI_MODE_TYPE_VR_HEADSET = 0x07; + + // start - miui + public static final byte UI_MODE_TYPE_GODZILLAUI = 0x0b; + public static final byte UI_MODE_TYPE_SMALLUI = 0x0c; + public static final byte UI_MODE_TYPE_MEDIUMUI = 0x0d; + public static final byte UI_MODE_TYPE_LARGEUI = 0x0e; + public static final byte UI_MODE_TYPE_HUGEUI = 0x0f; + // end - miui + + public static final byte MASK_UI_MODE_NIGHT = 0x30; + public static final byte UI_MODE_NIGHT_ANY = 0x00; + public static final byte UI_MODE_NIGHT_NO = 0x10; + public static final byte UI_MODE_NIGHT_YES = 0x20; + + public static final byte COLOR_HDR_MASK = 0xC; + public static final byte COLOR_HDR_NO = 0x4; + public static final byte COLOR_HDR_SHIFT = 0x2; + public static final byte COLOR_HDR_UNDEFINED = 0x0; + public static final byte COLOR_HDR_YES = 0x8; + + public static final byte COLOR_UNDEFINED = 0x0; + + public static final byte COLOR_WIDE_UNDEFINED = 0x0; + public static final byte COLOR_WIDE_NO = 0x1; + public static final byte COLOR_WIDE_YES = 0x2; + public static final byte COLOR_WIDE_MASK = 0x3; + + // TODO: Dirty static hack. This counter should be a part of ResPackage, + // but it would be hard right now and this feature is very rarely used. + private static int sErrCounter = 0; + public final short mcc; public final short mnc; @@ -137,7 +296,7 @@ public class ResConfigFlags { if (localeVariant[0] == '\00') { localeVariant = null; } - } else { + } else { localeVariant = null; } @@ -501,7 +660,7 @@ public class ResConfigFlags { private String toUpper(char[] character) { StringBuilder sb = new StringBuilder(); - for (char ch: character) { + for (char ch : character) { sb.append(Character.toUpperCase(ch)); } return sb.toString(); @@ -522,167 +681,13 @@ public class ResConfigFlags { return false; } final ResConfigFlags other = (ResConfigFlags) obj; - return this.mQualifiers.equals(other.mQualifiers); + return mQualifiers.equals(other.mQualifiers); } @Override public int hashCode() { int hash = 17; - hash = 31 * hash + this.mQualifiers.hashCode(); + hash = 31 * hash + mQualifiers.hashCode(); return hash; } - - // TODO: Dirty static hack. This counter should be a part of ResPackage, - // but it would be hard right now and this feature is very rarely used. - private static int sErrCounter = 0; - - public final static byte SDK_BASE = 1; - public final static byte SDK_BASE_1_1 = 2; - public final static byte SDK_CUPCAKE = 3; - public final static byte SDK_DONUT = 4; - public final static byte SDK_ECLAIR = 5; - public final static byte SDK_ECLAIR_0_1 = 6; - public final static byte SDK_ECLAIR_MR1 = 7; - public final static byte SDK_FROYO = 8; - public final static byte SDK_GINGERBREAD = 9; - public final static byte SDK_GINGERBREAD_MR1 = 10; - public final static byte SDK_HONEYCOMB = 11; - public final static byte SDK_HONEYCOMB_MR1 = 12; - public final static byte SDK_HONEYCOMB_MR2 = 13; - public final static byte SDK_ICE_CREAM_SANDWICH = 14; - public final static byte SDK_ICE_CREAM_SANDWICH_MR1 = 15; - public final static byte SDK_JELLY_BEAN = 16; - public final static byte SDK_JELLY_BEAN_MR1 = 17; - public final static byte SDK_JELLY_BEAN_MR2 = 18; - public final static byte SDK_KITKAT = 19; - public final static byte SDK_LOLLIPOP = 21; - public final static byte SDK_LOLLIPOP_MR1 = 22; - public final static byte SDK_MNC = 23; - public final static byte SDK_NOUGAT = 24; - public final static byte SDK_NOUGAT_MR1 = 25; - public final static byte SDK_OREO = 26; - public final static byte SDK_OREO_MR1 = 27; - public final static byte SDK_P = 28; - public final static byte SDK_Q = 29; - public final static byte SDK_R = 30; - public final static byte SDK_S = 31; - public final static byte SDK_S_V2 = 32; - public final static byte SDK_TIRAMISU = 33; - public final static byte SDK_UPSIDEDOWN_CAKE = 34; - - // AOSP has this as 10,000 for dev purposes. - // platform_frameworks_base/commit/c7a1109a1fe0771d4c9b572dcf178e2779fc4f2d - public final static int SDK_DEVELOPMENT = 10000; - - public final static byte ORIENTATION_ANY = 0; - public final static byte ORIENTATION_PORT = 1; - public final static byte ORIENTATION_LAND = 2; - public final static byte ORIENTATION_SQUARE = 3; - - public final static byte TOUCHSCREEN_ANY = 0; - public final static byte TOUCHSCREEN_NOTOUCH = 1; - public final static byte TOUCHSCREEN_STYLUS = 2; - public final static byte TOUCHSCREEN_FINGER = 3; - - public final static int DENSITY_DEFAULT = 0; - public final static int DENSITY_LOW = 120; - public final static int DENSITY_MEDIUM = 160; - public final static int DENSITY_400 = 190; - public final static int DENSITY_TV = 213; - public final static int DENSITY_HIGH = 240; - public final static int DENSITY_XHIGH = 320; - public final static int DENSITY_XXHIGH = 480; - public final static int DENSITY_XXXHIGH = 640; - public final static int DENSITY_ANY = 0xFFFE; - public final static int DENSITY_NONE = 0xFFFF; - - public final static int MNC_ZERO = -1; - - public final static short MASK_LAYOUTDIR = 0xc0; - public final static short SCREENLAYOUT_LAYOUTDIR_ANY = 0x00; - public final static short SCREENLAYOUT_LAYOUTDIR_LTR = 0x40; - public final static short SCREENLAYOUT_LAYOUTDIR_RTL = 0x80; - public final static short SCREENLAYOUT_LAYOUTDIR_SHIFT = 0x06; - - public final static short MASK_SCREENROUND = 0x03; - public final static short SCREENLAYOUT_ROUND_ANY = 0; - public final static short SCREENLAYOUT_ROUND_NO = 0x1; - public final static short SCREENLAYOUT_ROUND_YES = 0x2; - - public final static byte GRAMMATICAL_GENDER_ANY = 0; - public final static byte GRAMMATICAL_GENDER_NEUTER = 1; - public final static byte GRAMMATICAL_GENDER_FEMININE = 2; - public final static byte GRAMMATICAL_GENDER_MASCULINE = 3; - - public final static byte KEYBOARD_ANY = 0; - public final static byte KEYBOARD_NOKEYS = 1; - public final static byte KEYBOARD_QWERTY = 2; - public final static byte KEYBOARD_12KEY = 3; - - public final static byte NAVIGATION_ANY = 0; - public final static byte NAVIGATION_NONAV = 1; - public final static byte NAVIGATION_DPAD = 2; - public final static byte NAVIGATION_TRACKBALL = 3; - public final static byte NAVIGATION_WHEEL = 4; - - public final static byte MASK_KEYSHIDDEN = 0x3; - public final static byte KEYSHIDDEN_ANY = 0x0; - public final static byte KEYSHIDDEN_NO = 0x1; - public final static byte KEYSHIDDEN_YES = 0x2; - public final static byte KEYSHIDDEN_SOFT = 0x3; - - public final static byte MASK_NAVHIDDEN = 0xc; - public final static byte NAVHIDDEN_ANY = 0x0; - public final static byte NAVHIDDEN_NO = 0x4; - public final static byte NAVHIDDEN_YES = 0x8; - - public final static byte MASK_SCREENSIZE = 0x0f; - public final static byte SCREENSIZE_ANY = 0x00; - public final static byte SCREENSIZE_SMALL = 0x01; - public final static byte SCREENSIZE_NORMAL = 0x02; - public final static byte SCREENSIZE_LARGE = 0x03; - public final static byte SCREENSIZE_XLARGE = 0x04; - - public final static byte MASK_SCREENLONG = 0x30; - public final static byte SCREENLONG_ANY = 0x00; - public final static byte SCREENLONG_NO = 0x10; - public final static byte SCREENLONG_YES = 0x20; - - public final static byte MASK_UI_MODE_TYPE = 0x0f; - public final static byte UI_MODE_TYPE_ANY = 0x00; - public final static byte UI_MODE_TYPE_NORMAL = 0x01; - public final static byte UI_MODE_TYPE_DESK = 0x02; - public final static byte UI_MODE_TYPE_CAR = 0x03; - public final static byte UI_MODE_TYPE_TELEVISION = 0x04; - public final static byte UI_MODE_TYPE_APPLIANCE = 0x05; - public final static byte UI_MODE_TYPE_WATCH = 0x06; - public final static byte UI_MODE_TYPE_VR_HEADSET = 0x07; - - // start - miui - public final static byte UI_MODE_TYPE_GODZILLAUI = 0x0b; - public final static byte UI_MODE_TYPE_SMALLUI = 0x0c; - public final static byte UI_MODE_TYPE_MEDIUMUI = 0x0d; - public final static byte UI_MODE_TYPE_LARGEUI = 0x0e; - public final static byte UI_MODE_TYPE_HUGEUI = 0x0f; - // end - miui - - public final static byte MASK_UI_MODE_NIGHT = 0x30; - public final static byte UI_MODE_NIGHT_ANY = 0x00; - public final static byte UI_MODE_NIGHT_NO = 0x10; - public final static byte UI_MODE_NIGHT_YES = 0x20; - - public final static byte COLOR_HDR_MASK = 0xC; - public final static byte COLOR_HDR_NO = 0x4; - public final static byte COLOR_HDR_SHIFT = 0x2; - public final static byte COLOR_HDR_UNDEFINED = 0x0; - public final static byte COLOR_HDR_YES = 0x8; - - public final static byte COLOR_UNDEFINED = 0x0; - - public final static byte COLOR_WIDE_UNDEFINED = 0x0; - public final static byte COLOR_WIDE_NO = 0x1; - public final static byte COLOR_WIDE_YES = 0x2; - public final static byte COLOR_WIDE_MASK = 0x3; - - private static final Logger LOGGER = Logger.getLogger(ResConfigFlags.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResID.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResID.java index 6b118fec..eaa9ba55 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResID.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResID.java @@ -45,7 +45,7 @@ public class ResID { @Override public int hashCode() { int hash = 17; - hash = 31 * hash + this.id; + hash = 31 * hash + id; return hash; } @@ -58,6 +58,6 @@ public class ResID { return false; } final ResID other = (ResID) obj; - return this.id == other.id; + return id == other.id; } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResPackage.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResPackage.java index 90907c04..4c21a007 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResPackage.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResPackage.java @@ -26,20 +26,26 @@ import java.util.*; import java.util.logging.Logger; public class ResPackage { + private static final Logger LOGGER = Logger.getLogger(ResPackage.class.getName()); + private final ResTable mResTable; private final int mId; private final String mName; - private final Map mResSpecs = new LinkedHashMap<>(); - private final Map mConfigs = new LinkedHashMap<>(); - private final Map mTypes = new LinkedHashMap<>(); - private final Set mSynthesizedRes = new HashSet<>(); + private final Map mResSpecs; + private final Map mConfigs; + private final Map mTypes; + private final Set mSynthesizedRes; private ResValueFactory mValueFactory; public ResPackage(ResTable resTable, int id, String name) { - this.mResTable = resTable; - this.mId = id; - this.mName = name; + mResTable = resTable; + mId = id; + mName = name; + mResSpecs = new LinkedHashMap<>(); + mConfigs = new LinkedHashMap<>(); + mTypes = new LinkedHashMap<>(); + mSynthesizedRes = new HashSet<>(); } public List listResSpecs() { @@ -159,17 +165,17 @@ public class ResPackage { return false; } final ResPackage other = (ResPackage) obj; - if (!Objects.equals(this.mResTable, other.mResTable)) { + if (!Objects.equals(mResTable, other.mResTable)) { return false; } - return this.mId == other.mId; + return mId == other.mId; } @Override public int hashCode() { int hash = 17; - hash = 31 * hash + (this.mResTable != null ? this.mResTable.hashCode() : 0); - hash = 31 * hash + this.mId; + hash = 31 * hash + (mResTable != null ? mResTable.hashCode() : 0); + hash = 31 * hash + mId; return hash; } @@ -179,6 +185,4 @@ public class ResPackage { } return mValueFactory; } - - private final static Logger LOGGER = Logger.getLogger(ResPackage.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResSpec.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResSpec.java index c12b8240..9657877a 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResSpec.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResSpec.java @@ -18,42 +18,36 @@ package brut.androlib.res.data; import brut.androlib.exceptions.AndrolibException; import brut.androlib.exceptions.UndefinedResObjectException; +import com.google.common.collect.Sets; import org.apache.commons.lang3.StringUtils; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; public class ResResSpec { + private static final Set EMPTY_RESOURCE_NAMES = Sets.newHashSet( + "0_resource_name_obfuscated", "(name removed)" + ); + private final ResID mId; private final String mName; private final ResPackage mPackage; private final ResTypeSpec mType; - private final Map mResources = new LinkedHashMap<>(); - private static final Set EMPTY_RESOURCE_NAMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( - "0_resource_name_obfuscated", - "(name removed)" - ))); + private final Map mResources; public ResResSpec(ResID id, String name, ResPackage pkg, ResTypeSpec type) { - this.mId = id; - String cleanName; - name = EMPTY_RESOURCE_NAMES.contains(name) ? null : name; - - ResResSpec resResSpec = type.getResSpecUnsafe(name); - if (resResSpec != null) { - cleanName = String.format("APKTOOL_DUPLICATE_%s_%s", type, id.toString()); - } else { - cleanName = ((name == null || name.isEmpty()) ? ("APKTOOL_DUMMYVAL_" + id.toString()) : name); + mId = id; + if (name == null || name.isEmpty() || EMPTY_RESOURCE_NAMES.contains(name)) { + name = "APKTOOL_DUMMYVAL_" + id.toString(); + } else if (type.getResSpecUnsafe(name) != null) { + name = String.format("APKTOOL_DUPLICATE_%s_%s", type, id.toString()); } - - this.mName = cleanName; - this.mPackage = pkg; - this.mType = type; + mName = name; + mPackage = pkg; + mType = type; + mResources = new LinkedHashMap<>(); } public Set listResources() { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResource.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResource.java index f35aecff..ac4c45f2 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResource.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResResource.java @@ -25,9 +25,9 @@ public class ResResource { private final ResValue mValue; public ResResource(ResType config, ResResSpec spec, ResValue value) { - this.mConfig = config; - this.mResSpec = spec; - this.mValue = value; + mConfig = config; + mResSpec = spec; + mValue = value; } public String getFilePath() { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java index 1c29ef4f..c2c51dda 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java @@ -25,33 +25,30 @@ import brut.androlib.apk.UsesFramework; import brut.androlib.res.Framework; import brut.androlib.res.data.value.ResValue; import brut.androlib.res.decoder.ARSCDecoder; -import brut.androlib.res.xml.ResXmlPatcher; +import brut.androlib.res.xml.ResXmlUtils; import brut.directory.Directory; import brut.directory.DirectoryException; import brut.directory.ExtFile; import com.google.common.base.Strings; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.util.*; import java.util.logging.Logger; public class ResTable { - private final static Logger LOGGER = Logger.getLogger(ApkDecoder.class.getName()); + private static final Logger LOGGER = Logger.getLogger(ApkDecoder.class.getName()); private final Config mConfig; private final ApkInfo mApkInfo; - private final Map mPackagesById = new HashMap<>(); - private final Map mPackagesByName = new HashMap<>(); - private final Set mMainPackages = new LinkedHashSet<>(); - private final Set mFramePackages = new LinkedHashSet<>(); + private final Map mPackagesById; + private final Map mPackagesByName; + private final Set mMainPackages; + private final Set mFramePackages; private String mPackageRenamed; private String mPackageOriginal; private int mPackageId; - - private boolean mMainPkgLoaded = false; + private boolean mMainPkgLoaded; public ResTable() { this(Config.getDefaultConfig(), new ApkInfo()); @@ -64,6 +61,10 @@ public class ResTable { public ResTable(Config config, ApkInfo apkInfo) { mConfig = config; mApkInfo = apkInfo; + mPackagesById = new HashMap<>(); + mPackagesByName = new HashMap<>(); + mMainPackages = new LinkedHashSet<>(); + mFramePackages = new LinkedHashSet<>(); } public boolean getAnalysisMode() { @@ -118,7 +119,7 @@ public class ResTable { for (int i = 0; i < pkgs.length; i++) { ResPackage resPackage = pkgs[i]; - if (resPackage.getResSpecCount() > value && ! resPackage.getName().equalsIgnoreCase("android")) { + if (resPackage.getResSpecCount() > value && ! resPackage.getName().equals("android")) { value = resPackage.getResSpecCount(); id = resPackage.getId(); index = i; @@ -175,11 +176,13 @@ public class ResTable { return pkg; } - private ResPackage[] loadResPackagesFromApk(ExtFile apkFile, boolean keepBrokenResources) throws AndrolibException { + private ResPackage[] loadResPackagesFromApk(ExtFile apkFile, boolean keepBrokenResources) + throws AndrolibException { try { Directory dir = apkFile.getDirectory(); - try (BufferedInputStream bfi = new BufferedInputStream(dir.getFileInput("resources.arsc"))) { - return ARSCDecoder.decode(bfi, false, keepBrokenResources, this).getPackages(); + try (BufferedInputStream in = new BufferedInputStream(dir.getFileInput("resources.arsc"))) { + ARSCDecoder decoder = new ARSCDecoder(in, this, false, keepBrokenResources); + return decoder.decode().getPackages(); } } catch (DirectoryException | IOException ex) { throw new AndrolibException("Could not load resources.arsc from file: " + apkFile, ex); @@ -190,7 +193,7 @@ public class ResTable { int id = 0; int value = 0; for (ResPackage resPackage : mPackagesById.values()) { - if (resPackage.getResSpecCount() > value && !resPackage.getName().equalsIgnoreCase("android")) { + if (resPackage.getResSpecCount() > value && !resPackage.getName().equals("android")) { value = resPackage.getResSpecCount(); id = resPackage.getId(); } @@ -220,8 +223,8 @@ public class ResTable { return pkg; } - public ResValue getValue(String package_, String type, String name) throws AndrolibException { - return getPackage(package_).getType(type).getResSpec(name).getDefaultResource().getValue(); + public ResValue getValue(String pkg, String type, String name) throws AndrolibException { + return getPackage(pkg).getType(type).getResSpec(name).getDefaultResource().getValue(); } public void addPackage(ResPackage pkg, boolean main) throws AndrolibException { @@ -260,18 +263,15 @@ public class ResTable { } public void setSparseResources(boolean flag) { - if (mApkInfo.sparseResources != flag) { - LOGGER.info("Sparsely packed resources detected."); - } mApkInfo.sparseResources = flag; } - public void clearSdkInfo() { - mApkInfo.getSdkInfo().clear(); + public void setCompactEntries(boolean flag) { + mApkInfo.compactEntries = flag; } public void addSdkInfo(String key, String value) { - mApkInfo.getSdkInfo().put(key, value); + mApkInfo.sdkInfo.put(key, value); } public void setVersionName(String versionName) { @@ -307,14 +307,14 @@ public class ResTable { return false; } - public void initApkInfo(ApkInfo apkInfo, File outDir) throws AndrolibException { + public void initApkInfo(ApkInfo apkInfo, File apkDir) throws AndrolibException { apkInfo.isFrameworkApk = isFrameworkApk(); apkInfo.usesFramework = getUsesFramework(); - if (!mApkInfo.getSdkInfo().isEmpty()) { - updateSdkInfoFromResources(outDir); + if (!mApkInfo.sdkInfo.isEmpty()) { + updateSdkInfoFromResources(apkDir); } initPackageInfo(); - loadVersionName(outDir); + loadVersionName(apkDir); } private UsesFramework getUsesFramework() { @@ -330,25 +330,26 @@ public class ResTable { return info; } - private void updateSdkInfoFromResources(File outDir) { - String refValue; - Map sdkInfo = mApkInfo.getSdkInfo(); - if (sdkInfo.get("minSdkVersion") != null) { - refValue = ResXmlPatcher.pullValueFromIntegers(outDir, sdkInfo.get("minSdkVersion")); + private void updateSdkInfoFromResources(File apkDir) { + String minSdkVersion = mApkInfo.getMinSdkVersion(); + if (minSdkVersion != null) { + String refValue = ResXmlUtils.pullValueFromIntegers(apkDir, minSdkVersion); if (refValue != null) { - sdkInfo.put("minSdkVersion", refValue); + mApkInfo.setMinSdkVersion(refValue); } } - if (sdkInfo.get("targetSdkVersion") != null) { - refValue = ResXmlPatcher.pullValueFromIntegers(outDir, sdkInfo.get("targetSdkVersion")); + String targetSdkVersion = mApkInfo.getTargetSdkVersion(); + if (targetSdkVersion != null) { + String refValue = ResXmlUtils.pullValueFromIntegers(apkDir, targetSdkVersion); if (refValue != null) { - sdkInfo.put("targetSdkVersion", refValue); + mApkInfo.setTargetSdkVersion(refValue); } } - if (sdkInfo.get("maxSdkVersion") != null) { - refValue = ResXmlPatcher.pullValueFromIntegers(outDir, sdkInfo.get("maxSdkVersion")); + String maxSdkVersion = mApkInfo.getMaxSdkVersion(); + if (maxSdkVersion != null) { + String refValue = ResXmlUtils.pullValueFromIntegers(apkDir, maxSdkVersion); if (refValue != null) { - sdkInfo.put("maxSdkVersion", refValue); + mApkInfo.setMaxSdkVersion(refValue); } } } @@ -367,15 +368,15 @@ public class ResTable { } // only put rename-manifest-package into apktool.yml, if the change will be required - if (renamed != null && !renamed.equalsIgnoreCase(original)) { + if (renamed != null && !renamed.equals(original)) { mApkInfo.packageInfo.renameManifestPackage = renamed; } mApkInfo.packageInfo.forcedPackageId = String.valueOf(id); } - private void loadVersionName(File outDir) { + private void loadVersionName(File apkDir) { String versionName = mApkInfo.versionInfo.versionName; - String refValue = ResXmlPatcher.pullValueFromStrings(outDir, versionName); + String refValue = ResXmlUtils.pullValueFromStrings(apkDir, versionName); if (refValue != null) { mApkInfo.versionInfo.versionName = refValue; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResType.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResType.java index b93bf0fa..355174ac 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResType.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResType.java @@ -22,10 +22,11 @@ import java.util.*; public class ResType { private final ResConfigFlags mFlags; - private final Map mResources = new LinkedHashMap<>(); + private final Map mResources; public ResType(ResConfigFlags flags) { - this.mFlags = flags; + mFlags = flags; + mResources = new LinkedHashMap<>(); } public ResResource getResource(ResResSpec spec) throws AndrolibException { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTypeSpec.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTypeSpec.java index f1b0cd24..b82640fd 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTypeSpec.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResTypeSpec.java @@ -21,7 +21,6 @@ import brut.androlib.exceptions.UndefinedResObjectException; import java.util.*; public final class ResTypeSpec { - public static final String RES_TYPE_NAME_ARRAY = "array"; public static final String RES_TYPE_NAME_ATTR = "attr"; public static final String RES_TYPE_NAME_ATTR_PRIVATE = "^attr-private"; @@ -30,13 +29,13 @@ public final class ResTypeSpec { public static final String RES_TYPE_NAME_STYLES = "style"; private final String mName; - private final Map mResSpecs = new LinkedHashMap<>(); - private final int mId; + private final Map mResSpecs; public ResTypeSpec(String name, int id) { - this.mName = name; - this.mId = id; + mName = name; + mId = id; + mResSpecs = new LinkedHashMap<>(); } public String getName() { @@ -48,7 +47,7 @@ public final class ResTypeSpec { } public boolean isString() { - return mName.equalsIgnoreCase(RES_TYPE_NAME_STRING); + return mName.equals(RES_TYPE_NAME_STRING); } public ResResSpec getResSpec(String name) throws AndrolibException { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResValuesFile.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResValuesFile.java index dc860fb1..b1f967a6 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResValuesFile.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResValuesFile.java @@ -24,12 +24,13 @@ public class ResValuesFile { private final ResPackage mPackage; private final ResTypeSpec mType; private final ResType mConfig; - private final Set mResources = new LinkedHashSet<>(); + private final Set mResources; public ResValuesFile(ResPackage pkg, ResTypeSpec type, ResType config) { - this.mPackage = pkg; - this.mType = type; - this.mConfig = config; + mPackage = pkg; + mType = type; + mConfig = config; + mResources = new LinkedHashSet<>(); } public String getPath() { @@ -63,17 +64,17 @@ public class ResValuesFile { return false; } final ResValuesFile other = (ResValuesFile) obj; - if (!Objects.equals(this.mType, other.mType)) { + if (!Objects.equals(mType, other.mType)) { return false; } - return Objects.equals(this.mConfig, other.mConfig); + return Objects.equals(mConfig, other.mConfig); } @Override public int hashCode() { int hash = 17; - hash = 31 * hash + (this.mType != null ? this.mType.hashCode() : 0); - hash = 31 * hash + (this.mConfig != null ? this.mConfig.hashCode() : 0); + hash = 31 * hash + (mType != null ? mType.hashCode() : 0); + hash = 31 * hash + (mConfig != null ? mConfig.hashCode() : 0); return hash; } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/ARSCData.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/ARSCData.java index aad1a24c..e6c7a68d 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/ARSCData.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/ARSCData.java @@ -22,11 +22,13 @@ import brut.androlib.res.data.ResPackage; import java.util.logging.Logger; public class ARSCData { + private static final Logger LOGGER = Logger.getLogger(ARSCData.class.getName()); + private final ResPackage[] mPackages; private final FlagsOffset[] mFlagsOffsets; - public ARSCData(ResPackage[] packages, FlagsOffset[] flagsOffsets) { - mPackages = packages; + public ARSCData(ResPackage[] pkgs, FlagsOffset[] flagsOffsets) { + mPackages = pkgs; mFlagsOffsets = flagsOffsets; } @@ -63,6 +65,4 @@ public class ARSCData { } return id; } - - private static final Logger LOGGER = Logger.getLogger(ARSCData.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/ARSCHeader.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/ARSCHeader.java index 5fe443dd..88383d93 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/ARSCHeader.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/ARSCHeader.java @@ -16,7 +16,6 @@ */ package brut.androlib.res.data.arsc; -import brut.util.ExtCountingDataInput; import brut.util.ExtDataInput; import java.io.EOFException; @@ -25,13 +24,40 @@ import java.math.BigInteger; import java.util.logging.Logger; public class ARSCHeader { + private static final Logger LOGGER = Logger.getLogger(ARSCHeader.class.getName()); + + public static final short RES_NONE_TYPE = -1; + public static final short RES_NULL_TYPE = 0x0000; + public static final short RES_STRING_POOL_TYPE = 0x0001; + public static final short RES_TABLE_TYPE = 0x0002; + public static final short RES_XML_TYPE = 0x0003; + + // RES_TABLE_TYPE Chunks + public static final short XML_TYPE_PACKAGE = 0x0200; + public static final short XML_TYPE_TYPE = 0x0201; + public static final short XML_TYPE_SPEC_TYPE = 0x0202; + public static final short XML_TYPE_LIBRARY = 0x0203; + public static final short XML_TYPE_OVERLAY = 0x0204; + public static final short XML_TYPE_OVERLAY_POLICY = 0x0205; + public static final short XML_TYPE_STAGED_ALIAS = 0x0206; + + // RES_XML_TYPE Chunks + public static final short RES_XML_FIRST_CHUNK_TYPE = 0x0100; + public static final short RES_XML_START_NAMESPACE_TYPE = 0x0100; + public static final short RES_XML_END_NAMESPACE_TYPE = 0x0101; + public static final short RES_XML_START_ELEMENT_TYPE = 0x0102; + public static final short RES_XML_END_ELEMENT_TYPE = 0x0103; + public static final short RES_XML_CDATA_TYPE = 0x0104; + public static final short RES_XML_LAST_CHUNK_TYPE = 0x017f; + public static final short RES_XML_RESOURCE_MAP_TYPE = 0x0180; + public final short type; public final int headerSize; public final int chunkSize; - public final int startPosition; - public final int endPosition; + public final long startPosition; + public final long endPosition; - public ARSCHeader(short type, int headerSize, int chunkSize, int headerStart) { + public ARSCHeader(short type, int headerSize, int chunkSize, long headerStart) { this.type = type; this.headerSize = headerSize; this.chunkSize = chunkSize; @@ -39,9 +65,9 @@ public class ARSCHeader { this.endPosition = headerStart + chunkSize; } - public static ARSCHeader read(ExtCountingDataInput in) throws IOException { + public static ARSCHeader read(ExtDataInput in) throws IOException { short type; - int start = in.position(); + long start = in.position(); try { type = in.readShort(); } catch (EOFException ex) { @@ -50,13 +76,13 @@ public class ARSCHeader { return new ARSCHeader(type, in.readShort(), in.readInt(), start); } - public void checkForUnreadHeader(ExtCountingDataInput in) throws IOException { + public void checkForUnreadHeader(ExtDataInput in) throws IOException { // Some applications lie about the reported size of their chunk header. Trusting the chunkSize is misleading // So compare to what we actually read in the header vs reported and skip the rest. // However, this runs after each chunk and not every chunk reading has a specific distinction between the // header and the body. - int actualHeaderSize = in.position() - this.startPosition; - int exceedingSize = this.headerSize - actualHeaderSize; + int actualHeaderSize = (int) (in.position() - startPosition); + int exceedingSize = headerSize - actualHeaderSize; if (exceedingSize > 0) { byte[] buf = new byte[exceedingSize]; in.readFully(buf); @@ -64,11 +90,11 @@ public class ARSCHeader { if (exceedingBI.equals(BigInteger.ZERO)) { LOGGER.fine(String.format("Chunk header size (%d), read (%d), but exceeding bytes are all zero.", - this.headerSize, actualHeaderSize + headerSize, actualHeaderSize )); } else { LOGGER.warning(String.format("Chunk header size (%d), read (%d). Exceeding bytes: 0x%X.", - this.headerSize, actualHeaderSize, exceedingBI + headerSize, actualHeaderSize, exceedingBI )); } } @@ -77,31 +103,4 @@ public class ARSCHeader { public void skipChunk(ExtDataInput in) throws IOException { in.skipBytes(chunkSize - headerSize); } - - public final static short RES_NONE_TYPE = -1; - public final static short RES_NULL_TYPE = 0x0000; - public final static short RES_STRING_POOL_TYPE = 0x0001; - public final static short RES_TABLE_TYPE = 0x0002; - public final static short RES_XML_TYPE = 0x0003; - - // RES_TABLE_TYPE Chunks - public final static short XML_TYPE_PACKAGE = 0x0200; - public final static short XML_TYPE_TYPE = 0x0201; - public final static short XML_TYPE_SPEC_TYPE = 0x0202; - public final static short XML_TYPE_LIBRARY = 0x0203; - public final static short XML_TYPE_OVERLAY = 0x0204; - public final static short XML_TYPE_OVERLAY_POLICY = 0x0205; - public final static short XML_TYPE_STAGED_ALIAS = 0x0206; - - // RES_XML_TYPE Chunks - public final static short RES_XML_FIRST_CHUNK_TYPE = 0x0100; - public final static short RES_XML_START_NAMESPACE_TYPE = 0x0100; - public final static short RES_XML_END_NAMESPACE_TYPE = 0x0101; - public final static short RES_XML_START_ELEMENT_TYPE = 0x0102; - public final static short RES_XML_END_ELEMENT_TYPE = 0x0103; - public final static short RES_XML_CDATA_TYPE = 0x0104; - public final static short RES_XML_LAST_CHUNK_TYPE = 0x017f; - public final static short RES_XML_RESOURCE_MAP_TYPE = 0x0180; - - private static final Logger LOGGER = Logger.getLogger(ARSCHeader.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/EntryData.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/EntryData.java index 66fe78b6..6660bd72 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/EntryData.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/arsc/EntryData.java @@ -19,7 +19,7 @@ package brut.androlib.res.data.arsc; import brut.androlib.res.data.value.ResValue; public class EntryData { - public short mFlags; - public int mSpecNamesId; - public ResValue mValue; + public short flags; + public int specNamesId; + public ResValue value; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/axml/NamespaceStack.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/axml/NamespaceStack.java index c07731c7..893f13ff 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/axml/NamespaceStack.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/axml/NamespaceStack.java @@ -31,38 +31,38 @@ package brut.androlib.res.data.axml; * !! functions expect 'prefix'+'uri' pairs, not 'uri'+'prefix' !! */ public final class NamespaceStack { - private int[] m_data; - private int m_dataLength; - private int m_depth; + private int[] mData; + private int mDataLength; + private int mDepth; public NamespaceStack() { - m_data = new int[32]; + mData = new int[32]; } public void reset() { - m_dataLength = 0; - m_depth = 0; + mDataLength = 0; + mDepth = 0; } public int getCurrentCount() { - if (m_dataLength == 0) { + if (mDataLength == 0) { return 0; } - int offset = m_dataLength - 1; - return m_data[offset]; + int offset = mDataLength - 1; + return mData[offset]; } public int getAccumulatedCount(int depth) { - if (m_dataLength == 0 || depth < 0) { + if (mDataLength == 0 || depth < 0) { return 0; } - if (depth > m_depth) { - depth = m_depth; + if (depth > mDepth) { + depth = mDepth; } int accumulatedCount = 0; int offset = 0; for (; depth != 0; --depth) { - int count = m_data[offset]; + int count = mData[offset]; accumulatedCount += count; offset += (2 + count * 2); } @@ -70,34 +70,34 @@ public final class NamespaceStack { } public void push(int prefix, int uri) { - if (m_depth == 0) { + if (mDepth == 0) { increaseDepth(); } ensureDataCapacity(2); - int offset = m_dataLength - 1; - int count = m_data[offset]; - m_data[offset - 1 - count * 2] = count + 1; - m_data[offset] = prefix; - m_data[offset + 1] = uri; - m_data[offset + 2] = count + 1; - m_dataLength += 2; + int offset = mDataLength - 1; + int count = mData[offset]; + mData[offset - 1 - count * 2] = count + 1; + mData[offset] = prefix; + mData[offset + 1] = uri; + mData[offset + 2] = count + 1; + mDataLength += 2; } public boolean pop() { - if (m_dataLength == 0) { + if (mDataLength == 0) { return false; } - int offset = m_dataLength - 1; - int count = m_data[offset]; + int offset = mDataLength - 1; + int count = mData[offset]; if (count == 0) { return false; } count -= 1; offset -= 2; - m_data[offset] = count; + mData[offset] = count; offset -= (1 + count * 2); - m_data[offset] = count; - m_dataLength -= 2; + mData[offset] = count; + mDataLength -= 2; return true; } @@ -114,58 +114,58 @@ public final class NamespaceStack { } public int getDepth() { - return m_depth; + return mDepth; } public void increaseDepth() { ensureDataCapacity(2); - int offset = m_dataLength; - m_data[offset] = 0; - m_data[offset + 1] = 0; - m_dataLength += 2; - m_depth += 1; + int offset = mDataLength; + mData[offset] = 0; + mData[offset + 1] = 0; + mDataLength += 2; + mDepth += 1; } public void decreaseDepth() { - if (m_dataLength == 0) { + if (mDataLength == 0) { return; } - int offset = m_dataLength - 1; - int count = m_data[offset]; + int offset = mDataLength - 1; + int count = mData[offset]; if ((offset - 1 - count * 2) == 0) { return; } - m_dataLength -= 2 + count * 2; - m_depth -= 1; + mDataLength -= 2 + count * 2; + mDepth -= 1; } private void ensureDataCapacity(int capacity) { - int available = (m_data.length - m_dataLength); + int available = (mData.length - mDataLength); if (available > capacity) { return; } - int newLength = (m_data.length + available) * 2; + int newLength = (mData.length + available) * 2; int[] newData = new int[newLength]; - System.arraycopy(m_data, 0, newData, 0, m_dataLength); - m_data = newData; + System.arraycopy(mData, 0, newData, 0, mDataLength); + mData = newData; } private int find(int prefixOrUri, boolean prefix) { - if (m_dataLength == 0) { + if (mDataLength == 0) { return -1; } - int offset = m_dataLength - 1; - for (int i = m_depth; i != 0; --i) { - int count = m_data[offset]; + int offset = mDataLength - 1; + for (int i = mDepth; i != 0; --i) { + int count = mData[offset]; offset -= 2; for (; count != 0; --count) { if (prefix) { - if (m_data[offset] == prefixOrUri) { - return m_data[offset + 1]; + if (mData[offset] == prefixOrUri) { + return mData[offset + 1]; } } else { - if (m_data[offset + 1] == prefixOrUri) { - return m_data[offset]; + if (mData[offset + 1] == prefixOrUri) { + return mData[offset]; } } offset -= 2; @@ -175,12 +175,12 @@ public final class NamespaceStack { } private int get(int index, boolean prefix) { - if (m_dataLength == 0 || index < 0) { + if (mDataLength == 0 || index < 0) { return -1; } int offset = 0; - for (int i = m_depth; i != 0; --i) { - int count = m_data[offset]; + for (int i = mDepth; i != 0; --i) { + int count = mData[offset]; if (index >= count) { index -= count; offset += (2 + count * 2); @@ -190,7 +190,7 @@ public final class NamespaceStack { if (!prefix) { offset += 1; } - return m_data[offset]; + return mData[offset]; } return -1; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ninepatch/NinePatchData.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ninepatch/NinePatchData.java index 28d5f4f4..f2473155 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ninepatch/NinePatchData.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ninepatch/NinePatchData.java @@ -17,6 +17,7 @@ package brut.androlib.res.data.ninepatch; import brut.util.ExtDataInput; + import java.io.IOException; public class NinePatchData { @@ -32,19 +33,19 @@ public class NinePatchData { this.yDivs = yDivs; } - public static NinePatchData decode(ExtDataInput di) throws IOException { - di.skipBytes(1); // wasDeserialized - byte numXDivs = di.readByte(); - byte numYDivs = di.readByte(); - di.skipBytes(1); // numColors - di.skipBytes(8); // xDivs/yDivs offset - int padLeft = di.readInt(); - int padRight = di.readInt(); - int padTop = di.readInt(); - int padBottom = di.readInt(); - di.skipBytes(4); // colorsOffset - int[] xDivs = di.readIntArray(numXDivs); - int[] yDivs = di.readIntArray(numYDivs); + public static NinePatchData decode(ExtDataInput in) throws IOException { + in.skipBytes(1); // wasDeserialized + byte numXDivs = in.readByte(); + byte numYDivs = in.readByte(); + in.skipBytes(1); // numColors + in.skipBytes(8); // xDivs/yDivs offset + int padLeft = in.readInt(); + int padRight = in.readInt(); + int padTop = in.readInt(); + int padBottom = in.readInt(); + in.skipBytes(4); // colorsOffset + int[] xDivs = in.readIntArray(numXDivs); + int[] yDivs = in.readIntArray(numYDivs); return new NinePatchData(padLeft, padRight, padTop, padBottom, xDivs, yDivs); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ninepatch/OpticalInset.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ninepatch/OpticalInset.java index 49ddee8f..1836ddfa 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ninepatch/OpticalInset.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ninepatch/OpticalInset.java @@ -17,6 +17,7 @@ package brut.androlib.res.data.ninepatch; import brut.util.ExtDataInput; + import java.io.IOException; public class OpticalInset { @@ -29,11 +30,11 @@ public class OpticalInset { this.layoutBoundsBottom = layoutBoundsBottom; } - public static OpticalInset decode(ExtDataInput di) throws IOException { - int layoutBoundsLeft = Integer.reverseBytes(di.readInt()); - int layoutBoundsTop = Integer.reverseBytes(di.readInt()); - int layoutBoundsRight = Integer.reverseBytes(di.readInt()); - int layoutBoundsBottom = Integer.reverseBytes(di.readInt()); + public static OpticalInset decode(ExtDataInput in) throws IOException { + int layoutBoundsLeft = Integer.reverseBytes(in.readInt()); + int layoutBoundsTop = Integer.reverseBytes(in.readInt()); + int layoutBoundsRight = Integer.reverseBytes(in.readInt()); + int layoutBoundsBottom = Integer.reverseBytes(in.readInt()); return new OpticalInset(layoutBoundsLeft, layoutBoundsTop, layoutBoundsRight, layoutBoundsBottom); } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResArrayValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResArrayValue.java index 2b1e2046..06a14444 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResArrayValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResArrayValue.java @@ -20,15 +20,19 @@ import brut.androlib.exceptions.AndrolibException; import brut.androlib.res.data.ResResource; import brut.androlib.res.xml.ResValuesXmlSerializable; import brut.util.Duo; +import com.google.common.collect.Sets; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; -import java.util.Arrays; +import java.util.Set; public class ResArrayValue extends ResBagValue implements ResValuesXmlSerializable { + private static final Set ALLOWED_ARRAY_TYPES = Sets.newHashSet("string", "integer"); + + private final ResScalarValue[] mItems; + ResArrayValue(ResReferenceValue parent, Duo[] items) { super(parent); - mItems = new ResScalarValue[items.length]; for (int i = 0; i < items.length; i++) { mItems[i] = items[i].m2; @@ -41,8 +45,8 @@ public class ResArrayValue extends ResBagValue implements ResValuesXmlSerializab } @Override - public void serializeToResValuesXml(XmlSerializer serializer, - ResResource res) throws IOException, AndrolibException { + public void serializeToResValuesXml(XmlSerializer serializer, ResResource res) + throws IOException, AndrolibException { String type = getType(); type = (type == null ? "" : type + "-") + "array"; serializer.startTag(null, type); @@ -83,12 +87,9 @@ public class ResArrayValue extends ResBagValue implements ResValuesXmlSerializab return null; } } - if (!Arrays.asList(AllowedArrayTypes).contains(type)) { + if (!ALLOWED_ARRAY_TYPES.contains(type)) { return "string"; } return type; } - - private final ResScalarValue[] mItems; - private final String[] AllowedArrayTypes = {"string", "integer"}; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResAttr.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResAttr.java index 982a78c5..b2551b4f 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResAttr.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResAttr.java @@ -26,6 +26,28 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; public class ResAttr extends ResBagValue implements ResValuesXmlSerializable { + private static final int BAG_KEY_ATTR_MIN = 0x01000001; + private static final int BAG_KEY_ATTR_MAX = 0x01000002; + private static final int BAG_KEY_ATTR_L10N = 0x01000003; + + private static final int TYPE_REFERENCE = 0x01; + private static final int TYPE_STRING = 0x02; + private static final int TYPE_INT = 0x04; + private static final int TYPE_BOOL = 0x08; + private static final int TYPE_COLOR = 0x10; + private static final int TYPE_FLOAT = 0x20; + private static final int TYPE_DIMEN = 0x40; + private static final int TYPE_FRACTION = 0x80; + private static final int TYPE_ANY_STRING = 0xee; + + private static final int TYPE_ENUM = 0x00010000; + private static final int TYPE_FLAGS = 0x00020000; + + private final int mType; + private final Integer mMin; + private final Integer mMax; + private final Boolean mL10n; + ResAttr(ResReferenceValue parentVal, int type, Integer min, Integer max, Boolean l10n) { super(parentVal); mType = type; @@ -139,26 +161,4 @@ public class ResAttr extends ResBagValue implements ResValuesXmlSerializable { } return s.substring(1); } - - private final int mType; - private final Integer mMin; - private final Integer mMax; - private final Boolean mL10n; - - private static final int BAG_KEY_ATTR_MIN = 0x01000001; - private static final int BAG_KEY_ATTR_MAX = 0x01000002; - private static final int BAG_KEY_ATTR_L10N = 0x01000003; - - private final static int TYPE_REFERENCE = 0x01; - private final static int TYPE_STRING = 0x02; - private final static int TYPE_INT = 0x04; - private final static int TYPE_BOOL = 0x08; - private final static int TYPE_COLOR = 0x10; - private final static int TYPE_FLOAT = 0x20; - private final static int TYPE_DIMEN = 0x40; - private final static int TYPE_FRACTION = 0x80; - private final static int TYPE_ANY_STRING = 0xee; - - private static final int TYPE_ENUM = 0x00010000; - private static final int TYPE_FLAGS = 0x00020000; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResBagValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResBagValue.java index ea77c39e..8b71aedc 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResBagValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResBagValue.java @@ -28,7 +28,7 @@ public class ResBagValue extends ResValue implements ResValuesXmlSerializable { protected final ResReferenceValue mParent; public ResBagValue(ResReferenceValue parent) { - this.mParent = parent; + mParent = parent; } @Override diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResBoolValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResBoolValue.java index 831d422e..55f2d98b 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResBoolValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResBoolValue.java @@ -21,7 +21,7 @@ public class ResBoolValue extends ResScalarValue { public ResBoolValue(boolean value, int rawIntValue, String rawValue) { super("bool", rawIntValue, rawValue); - this.mValue = value; + mValue = value; } public boolean getValue() { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResColorValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResColorValue.java index bbee5ff6..7440309f 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResColorValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResColorValue.java @@ -17,6 +17,7 @@ package brut.androlib.res.data.value; public class ResColorValue extends ResIntValue { + public ResColorValue(int value, String rawValue) { super(value, rawValue, "color"); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResDimenValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResDimenValue.java index bfdb24f2..667cead4 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResDimenValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResDimenValue.java @@ -20,6 +20,7 @@ import android.util.TypedValue; import brut.androlib.exceptions.AndrolibException; public class ResDimenValue extends ResIntValue { + public ResDimenValue(int value, String rawValue) { super(value, rawValue, "dimen"); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEmptyValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEmptyValue.java index 3de842ec..8f2f23fa 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEmptyValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEmptyValue.java @@ -20,18 +20,18 @@ import brut.androlib.exceptions.AndrolibException; public class ResEmptyValue extends ResScalarValue { protected final int mValue; - protected int type; + protected int mType; public ResEmptyValue(int value, String rawValue, int type) { this(value, rawValue, "integer"); - this.type = type; + mType = type; } public ResEmptyValue(int value, String rawValue, String type) { super(type, value, rawValue); if (value != 1) throw new UnsupportedOperationException(); - this.mValue = value; + mValue = value; } public int getValue() { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEnumAttr.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEnumAttr.java index d8ffced4..f2bb5588 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEnumAttr.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResEnumAttr.java @@ -28,10 +28,16 @@ import java.util.Map; import java.util.logging.Logger; public class ResEnumAttr extends ResAttr { + private static final Logger LOGGER = Logger.getLogger(ResEnumAttr.class.getName()); + + private final Duo[] mItems; + private final Map mItemsCache; + ResEnumAttr(ResReferenceValue parent, int type, Integer min, Integer max, Boolean l10n, Duo[] items) { super(parent, type, min, max, l10n); mItems = items; + mItemsCache = new HashMap<>(); } @Override @@ -85,9 +91,4 @@ public class ResEnumAttr extends ResAttr { } return value2; } - - private final Duo[] mItems; - private final Map mItemsCache = new HashMap<>(); - - private static final Logger LOGGER = Logger.getLogger(ResEnumAttr.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFileValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFileValue.java index f4ea0ce5..f73f8f77 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFileValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFileValue.java @@ -23,7 +23,7 @@ public class ResFileValue extends ResIntBasedValue { public ResFileValue(String path, int rawIntValue) { super(rawIntValue); - this.mPath = path; + mPath = path; } public String getStrippedPath() throws AndrolibException { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFlagsAttr.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFlagsAttr.java index c37519f6..03b0708b 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFlagsAttr.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFlagsAttr.java @@ -28,6 +28,12 @@ import java.util.Arrays; import java.util.logging.Logger; public class ResFlagsAttr extends ResAttr { + private static final Logger LOGGER = Logger.getLogger(ResFlagsAttr.class.getName()); + + private final FlagItem[] mItems; + private FlagItem[] mZeroFlags; + private FlagItem[] mFlags; + ResFlagsAttr(ResReferenceValue parent, int type, Integer min, Integer max, Boolean l10n, Duo[] items) { super(parent, type, min, max, l10n); @@ -133,11 +139,4 @@ public class ResFlagsAttr extends ResAttr { Arrays.sort(mFlags, (o1, o2) -> Integer.compare(Integer.bitCount(o2.flag), Integer.bitCount(o1.flag))); } - - private final FlagItem[] mItems; - - private FlagItem[] mZeroFlags; - private FlagItem[] mFlags; - - private static final Logger LOGGER = Logger.getLogger(ResFlagsAttr.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFloatValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFloatValue.java index b61f2ccf..11c218d0 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFloatValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFloatValue.java @@ -21,7 +21,7 @@ public class ResFloatValue extends ResScalarValue { public ResFloatValue(float value, int rawIntValue, String rawValue) { super("float", rawIntValue, rawValue); - this.mValue = value; + mValue = value; } public float getValue() { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFractionValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFractionValue.java index a98c2bb0..8a0e9eda 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFractionValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResFractionValue.java @@ -20,6 +20,7 @@ import android.util.TypedValue; import brut.androlib.exceptions.AndrolibException; public class ResFractionValue extends ResIntValue { + public ResFractionValue(int value, String rawValue) { super(value, rawValue, "fraction"); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResIdValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResIdValue.java index dc4271ab..c6bd9632 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResIdValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResIdValue.java @@ -23,6 +23,7 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; public class ResIdValue extends ResValue implements ResValuesXmlSerializable { + @Override public void serializeToResValuesXml(XmlSerializer serializer, ResResource res) throws IOException { serializer.startTag(null, "item"); diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResIntValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResIntValue.java index 3a00b1b1..a0c87935 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResIntValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResIntValue.java @@ -21,16 +21,16 @@ import brut.androlib.exceptions.AndrolibException; public class ResIntValue extends ResScalarValue { protected final int mValue; - private int type; + private int mType; public ResIntValue(int value, String rawValue, int type) { this(value, rawValue, "integer"); - this.type = type; + mType = type; } public ResIntValue(int value, String rawValue, String type) { super(type, value, rawValue); - this.mValue = value; + mValue = value; } public int getValue() { @@ -39,6 +39,6 @@ public class ResIntValue extends ResScalarValue { @Override protected String encodeAsResXml() throws AndrolibException { - return TypedValue.coerceToString(type, mValue); + return TypedValue.coerceToString(mType, mValue); } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java index 34e90a42..727b3a61 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java @@ -26,9 +26,14 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; public class ResPluralsValue extends ResBagValue implements ResValuesXmlSerializable { + private static final String[] QUANTITY_MAP = { "other", "zero", "one", "two", "few", "many" }; + + private static final int BAG_KEY_PLURALS_START = 0x01000004; + + private final ResScalarValue[] mItems; + ResPluralsValue(ResReferenceValue parent, Duo[] items) { super(parent); - mItems = new ResScalarValue[6]; for (Duo item : items) { mItems[item.m1 - BAG_KEY_PLURALS_START] = item.m2; @@ -53,9 +58,4 @@ public class ResPluralsValue extends ResBagValue implements ResValuesXmlSerializ } serializer.endTag(null, "plurals"); } - - private final ResScalarValue[] mItems; - - public static final int BAG_KEY_PLURALS_START = 0x01000004; - private static final String[] QUANTITY_MAP = new String[] { "other", "zero", "one", "two", "few", "many" }; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResReferenceValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResReferenceValue.java index 4cf2c842..09b01613 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResReferenceValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResReferenceValue.java @@ -25,14 +25,13 @@ public class ResReferenceValue extends ResIntValue { private final ResPackage mPackage; private final boolean mTheme; - public ResReferenceValue(ResPackage package_, int value, String rawValue) { - this(package_, value, rawValue, false); + public ResReferenceValue(ResPackage pkg, int value, String rawValue) { + this(pkg, value, rawValue, false); } - public ResReferenceValue(ResPackage package_, int value, String rawValue, - boolean theme) { + public ResReferenceValue(ResPackage pkg, int value, String rawValue, boolean theme) { super(value, rawValue, "reference"); - mPackage = package_; + mPackage = pkg; mTheme = theme; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java index 30ee763b..55a02927 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java @@ -73,7 +73,7 @@ public abstract class ResScalarValue extends ResIntBasedValue implements String body = encodeAsResXmlValue(); // check for resource reference - if (!type.equalsIgnoreCase("color")) { + if (!type.equals("color")) { if (body.contains("@")) { if (!res.getFilePath().contains("string")) { item = true; @@ -89,7 +89,7 @@ public abstract class ResScalarValue extends ResIntBasedValue implements // Android does not allow values (false) for ids.xml anymore // https://issuetracker.google.com/issues/80475496 // But it decodes as a ResBoolean, which makes no sense. So force it to empty - if (type.equalsIgnoreCase("id") && !body.isEmpty()) { + if (type.equals("id") && !body.isEmpty()) { body = ""; } @@ -115,8 +115,9 @@ public abstract class ResScalarValue extends ResIntBasedValue implements return mType; } - protected void serializeExtraXmlAttrs(XmlSerializer serializer, - ResResource res) throws IOException { + protected void serializeExtraXmlAttrs(XmlSerializer serializer, ResResource res) + throws IOException { + // stub } protected abstract String encodeAsResXml() throws AndrolibException; diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResStringValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResStringValue.java index 0cbb49b4..beabb6a4 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResStringValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResStringValue.java @@ -25,6 +25,8 @@ import java.io.IOException; import java.util.regex.Pattern; public class ResStringValue extends ResScalarValue { + private static final Pattern ALL_DIGITS = Pattern.compile("\\d{9,}"); + public ResStringValue(String value, int rawValue) { this(value, rawValue, "string"); } @@ -64,8 +66,6 @@ public class ResStringValue extends ResScalarValue { if (val == null || val.isEmpty()) { return val; } - return allDigits.matcher(val).matches() ? "\\ " + val : val; + return ALL_DIGITS.matcher(val).matches() ? "\\ " + val : val; } - - private static final Pattern allDigits = Pattern.compile("\\d{9,}"); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResStyleValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResStyleValue.java index 9cf6d74d..a548e813 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResStyleValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResStyleValue.java @@ -29,9 +29,12 @@ import java.util.Set; import java.util.logging.Logger; public class ResStyleValue extends ResBagValue implements ResValuesXmlSerializable { + private static final Logger LOGGER = Logger.getLogger(ResStyleValue.class.getName()); + + private final Duo[] mItems; + ResStyleValue(ResReferenceValue parent, Duo[] items, ResValueFactory factory) { super(parent); - mItems = new Duo[items.length]; for (int i = 0; i < items.length; i++) { mItems[i] = new Duo<>( @@ -97,8 +100,4 @@ public class ResStyleValue extends ResBagValue implements ResValuesXmlSerializab serializer.endTag(null, "style"); processedNames.clear(); } - - private final Duo[] mItems; - - private static final Logger LOGGER = Logger.getLogger(ResStyleValue.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResValue.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResValue.java index 1b535e6c..47d91d4b 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResValue.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResValue.java @@ -19,6 +19,7 @@ package brut.androlib.res.data.value; import brut.androlib.Config; public class ResValue { + public boolean shouldRemoveUnknownRes() { return Config.getInstance().isDecodeResolveModeRemoving(); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResValueFactory.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResValueFactory.java index 1d20c79f..7364f5f2 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResValueFactory.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/value/ResValueFactory.java @@ -25,8 +25,8 @@ import brut.util.Duo; public class ResValueFactory { private final ResPackage mPackage; - public ResValueFactory(ResPackage package_) { - this.mPackage = package_; + public ResValueFactory(ResPackage pkg) { + mPackage = pkg; } public ResScalarValue factory(int type, int value, String rawValue) throws AndrolibException { @@ -79,7 +79,7 @@ public class ResValueFactory { } public ResBagValue bagFactory(int parent, Duo[] items, ResTypeSpec resTypeSpec) - throws AndrolibException { + throws AndrolibException { ResReferenceValue parentVal = newReference(parent, null); if (items.length == 0) { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java index 24bfe964..d6bf1882 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java @@ -22,8 +22,7 @@ import brut.androlib.res.data.*; import brut.androlib.res.data.arsc.*; import brut.androlib.res.data.value.*; import brut.util.Duo; -import brut.util.ExtCountingDataInput; -import com.google.common.io.LittleEndianDataInputStream; +import brut.util.ExtDataInputStream; import java.io.*; import java.math.BigInteger; @@ -31,37 +30,57 @@ import java.util.*; import java.util.logging.Logger; public class ARSCDecoder { - public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken) - throws AndrolibException { - return decode(arscStream, findFlagsOffsets, keepBroken, new ResTable()); + private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName()); + + private static final short ENTRY_FLAG_COMPLEX = 0x0001; + private static final short ENTRY_FLAG_PUBLIC = 0x0002; + private static final short ENTRY_FLAG_WEAK = 0x0004; + private static final short ENTRY_FLAG_COMPACT = 0x0008; + + private static final short TABLE_TYPE_FLAG_SPARSE = 0x01; + private static final short TABLE_TYPE_FLAG_OFFSET16 = 0x02; + + private static final int KNOWN_CONFIG_BYTES = 64; + + private static final int NO_ENTRY = 0xFFFFFFFF; + private static final int NO_ENTRY_OFFSET16 = 0xFFFF; + + private final ExtDataInputStream mIn; + private final ResTable mResTable; + private final List mFlagsOffsets; + private final boolean mKeepBroken; + private final HashMap mMissingResSpecMap; + private final HashMap mResTypeSpecs; + + private ARSCHeader mHeader; + private StringBlock mTableStrings; + private StringBlock mTypeNames; + private StringBlock mSpecNames; + private ResPackage mPkg; + private ResTypeSpec mTypeSpec; + private ResType mType; + private int mResId; + private int mTypeIdOffset; + + public ARSCDecoder(InputStream in, ResTable resTable, boolean storeFlagsOffsets, boolean keepBroken) { + mIn = ExtDataInputStream.littleEndian(in); + mResTable = resTable != null ? resTable : new ResTable(); + mFlagsOffsets = storeFlagsOffsets ? new ArrayList<>() : null; + mKeepBroken = keepBroken; + mMissingResSpecMap = new LinkedHashMap<>(); + mResTypeSpecs = new HashMap<>(); } - public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken, - ResTable resTable) - throws AndrolibException { + public ARSCData decode() throws AndrolibException { try { - ARSCDecoder decoder = new ARSCDecoder(arscStream, resTable, findFlagsOffsets, keepBroken); - ResPackage[] pkgs = decoder.readResourceTable(); - return new ARSCData(pkgs, decoder.mFlagsOffsets == null - ? null - : decoder.mFlagsOffsets.toArray(new FlagsOffset[0])); + ResPackage[] pkgs = readResourceTable(); + FlagsOffset[] flagsOffsets = mFlagsOffsets != null ? mFlagsOffsets.toArray(new FlagsOffset[0]) : null; + return new ARSCData(pkgs, flagsOffsets); } catch (IOException ex) { throw new AndrolibException("Could not decode arsc file", ex); } } - private ARSCDecoder(InputStream arscStream, ResTable resTable, boolean storeFlagsOffsets, boolean keepBroken) { - if (storeFlagsOffsets) { - mFlagsOffsets = new ArrayList<>(); - } else { - mFlagsOffsets = null; - } - mIn = new ExtCountingDataInput(new LittleEndianDataInputStream(arscStream)); - mResTable = resTable; - mKeepBroken = keepBroken; - mMissingResSpecMap = new LinkedHashMap<>(); - } - private ResPackage[] readResourceTable() throws IOException, AndrolibException { Set pkgs = new LinkedHashSet<>(); ResTypeSpec typeSpec; @@ -170,7 +189,7 @@ public class ARSCDecoder { // TypeIdOffset was added platform_frameworks_base/@f90f2f8dc36e7243b85e0b6a7fd5a590893c827e // which is only in split/new applications. - int splitHeaderSize = (2 + 2 + 4 + 4 + (2 * 128) + (4 * 5)); // short, short, int, int, char[128], int * 4 + int splitHeaderSize = 2 + 2 + 4 + 4 + (2 * 128) + (4 * 5); // short, short, int, int, char[128], int * 4 if (mHeader.headerSize == splitHeaderSize) { mTypeIdOffset = mIn.readInt(); } @@ -246,7 +265,7 @@ public class ARSCDecoder { int entryCount = mIn.readInt(); if (mFlagsOffsets != null) { - mFlagsOffsets.add(new FlagsOffset(mIn.position(), entryCount)); + mFlagsOffsets.add(new FlagsOffset((int) mIn.position(), entryCount)); } mHeader.checkForUnreadHeader(mIn); @@ -310,14 +329,14 @@ public class ARSCDecoder { } } - mType = flags.isInvalid && !mKeepBroken ? null : mPkg.getOrCreateConfig(flags); + mType = !flags.isInvalid || mKeepBroken ? mPkg.getOrCreateConfig(flags) : null; int noEntry = isOffset16 ? NO_ENTRY_OFFSET16 : NO_ENTRY; // #3428 - In some applications the res entries are padded for alignment. - int entriesStartAligned = mHeader.startPosition + entriesStart; + long entriesStartAligned = mHeader.startPosition + entriesStart; if (mIn.position() < entriesStartAligned) { long bytesSkipped = mIn.skip(entriesStartAligned - mIn.position()); - LOGGER.fine("Skipping: " + bytesSkipped + " byte(s) to align with ResTable_entry start."); + LOGGER.fine(String.format("Skipping: %d byte(s) to align with ResTable_entry start.", bytesSkipped)); } for (int i : entryOffsetMap.keySet()) { @@ -355,21 +374,23 @@ public class ARSCDecoder { } private EntryData readEntryData() throws IOException, AndrolibException { - short size = mIn.readShort(); + int size = mIn.readUnsignedShort(); short flags = mIn.readShort(); boolean isComplex = (flags & ENTRY_FLAG_COMPLEX) != 0; boolean isCompact = (flags & ENTRY_FLAG_COMPACT) != 0; - if (size < 0 && !isCompact) { - throw new AndrolibException("Entry size is under 0 bytes and not compactly packed."); - } - int specNamesId = mIn.readInt(); if (specNamesId == NO_ENTRY && !isCompact) { return null; } + // Be sure we don't poison mResTable by marking the application as compact + // Only flag the ResTable as compact if the main package is not loaded. + if (isCompact && !mResTable.isMainPkgLoaded()) { + mResTable.setCompactEntries(true); + } + // #3366 - In a compactly packed entry, the key index is the size & type is higher 8 bits on flags. // We assume a size of 8 bytes for compact entries and the specNamesId is the data itself encoded. ResValue value; @@ -377,7 +398,7 @@ public class ARSCDecoder { byte type = (byte) ((flags >> 8) & 0xFF); value = readCompactValue(type, specNamesId); - // To keep code below happy - we know if compact that the size has the key index encoded. + // To keep code below happy - we know if compact then the size has the key index encoded. specNamesId = size; } else if (isComplex) { value = readComplexEntry(); @@ -392,15 +413,15 @@ public class ARSCDecoder { } EntryData entryData = new EntryData(); - entryData.mFlags = flags; - entryData.mSpecNamesId = specNamesId; - entryData.mValue = value; + entryData.flags = flags; + entryData.specNamesId = specNamesId; + entryData.value = value; return entryData; } private void readEntry(EntryData entryData) throws AndrolibException { - int specNamesId = entryData.mSpecNamesId; - ResValue value = entryData.mValue; + int specNamesId = entryData.specNamesId; + ResValue value = entryData.value; if (mTypeSpec.isString() && value instanceof ResFileValue) { value = new ResStringValue(value.toString(), ((ResFileValue) value).getRawIntValue()); @@ -469,18 +490,18 @@ public class ARSCDecoder { } private ResIntBasedValue readValue() throws IOException, AndrolibException { - int size = mIn.readShort(); + short size = mIn.readShort(); if (size < 8) { return null; } - mIn.skipCheckByte((byte) 0); // zero + mIn.skipCheckByte((byte) 0); // zero byte type = mIn.readByte(); int data = mIn.readInt(); return type == TypedValue.TYPE_STRING - ? mPkg.getValueFactory().factory(mTableStrings.getHTML(data), data) - : mPkg.getValueFactory().factory(type, data, null); + ? mPkg.getValueFactory().factory(mTableStrings.getHTML(data), data) + : mPkg.getValueFactory().factory(type, data, null); } private ResConfigFlags readConfigFlags() throws IOException, AndrolibException { @@ -499,8 +520,8 @@ public class ARSCDecoder { char[] language = new char[0]; char[] country = new char[0]; if (size >= 12) { - language = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), 'a'); - country = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), '0'); + language = unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), 'a'); + country = unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), '0'); read = 12; } @@ -682,36 +703,4 @@ public class ARSCDecoder { expectedType, mHeader.type)); } } - - private final ExtCountingDataInput mIn; - private final ResTable mResTable; - private final List mFlagsOffsets; - private final boolean mKeepBroken; - - private ARSCHeader mHeader; - private StringBlock mTableStrings; - private StringBlock mTypeNames; - private StringBlock mSpecNames; - private ResPackage mPkg; - private ResTypeSpec mTypeSpec; - private ResType mType; - private int mResId; - private int mTypeIdOffset = 0; - private final HashMap mMissingResSpecMap; - private final HashMap mResTypeSpecs = new HashMap<>(); - - private final static short ENTRY_FLAG_COMPLEX = 0x0001; - private final static short ENTRY_FLAG_PUBLIC = 0x0002; - private final static short ENTRY_FLAG_WEAK = 0x0004; - private final static short ENTRY_FLAG_COMPACT = 0x0008; - - private final static short TABLE_TYPE_FLAG_SPARSE = 0x01; - private final static short TABLE_TYPE_FLAG_OFFSET16 = 0x02; - - private static final int KNOWN_CONFIG_BYTES = 64; - - private static final int NO_ENTRY = 0xFFFFFFFF; - private static final int NO_ENTRY_OFFSET16 = 0xFFFF; - - private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java index a2e01d40..e5d16483 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java @@ -29,8 +29,7 @@ import brut.androlib.res.data.axml.NamespaceStack; import brut.androlib.res.data.value.ResAttr; import brut.androlib.res.data.value.ResScalarValue; import brut.androlib.res.xml.ResXmlEncoders; -import brut.util.ExtCountingDataInput; -import com.google.common.io.LittleEndianDataInputStream; +import brut.util.ExtDataInputStream; import org.xmlpull.v1.XmlPullParserException; import java.io.*; @@ -47,9 +46,45 @@ import java.util.logging.Logger; * this state methods return invalid values or throw exceptions. */ public class AXmlResourceParser implements XmlResourceParser { + private static final Logger LOGGER = Logger.getLogger(AXmlResourceParser.class.getName()); + + private static final String E_NOT_SUPPORTED = "Method is not supported."; + private static final String ANDROID_RES_NS_AUTO = "http://schemas.android.com/apk/res-auto"; + public static final String ANDROID_RES_NS = "http://schemas.android.com/apk/res/android"; + + // ResXMLTree_attribute + private static final int ATTRIBUTE_IX_NAMESPACE_URI = 0; // ns + private static final int ATTRIBUTE_IX_NAME = 1; // name + private static final int ATTRIBUTE_IX_VALUE_STRING = 2; // rawValue + private static final int ATTRIBUTE_IX_VALUE_TYPE = 3; // (size/res0/dataType) + private static final int ATTRIBUTE_IX_VALUE_DATA = 4; // data + private static final int ATTRIBUTE_LENGTH = 5; + + private static final int PRIVATE_PKG_ID = 0x7F; + + private final ResTable mResTable; + private final NamespaceStack mNamespaces; + + private boolean mIsOperational; + private ExtDataInputStream mIn; + private StringBlock mStringBlock; + private int[] mResourceIds; + private boolean mDecreaseDepth; + private AndrolibException mFirstError; + + // All values are essentially indices, e.g. mNameIndex is an index of name in mStringBlock. + private int mEvent; + private int mLineNumber; + private int mNameIndex; + private int mNamespaceIndex; + private int[] mAttributes; + private int mIdIndex; + private int mClassIndex; + private int mStyleIndex; public AXmlResourceParser(ResTable resTable) { mResTable = resTable; + mNamespaces = new NamespaceStack(); resetEventInfo(); } @@ -64,16 +99,16 @@ public class AXmlResourceParser implements XmlResourceParser { public void open(InputStream stream) { close(); if (stream != null) { - mIn = new ExtCountingDataInput(new LittleEndianDataInputStream(stream)); + mIn = ExtDataInputStream.littleEndian(stream); } } @Override public void close() { - if (!isOperational) { + if (!mIsOperational) { return; } - isOperational = false; + mIsOperational = false; mIn = null; mStringBlock = null; mResourceIds = null; @@ -89,9 +124,9 @@ public class AXmlResourceParser implements XmlResourceParser { try { doNext(); return mEvent; - } catch (IOException e) { + } catch (IOException ex) { close(); - throw e; + throw ex; } } @@ -314,17 +349,16 @@ public class AXmlResourceParser implements XmlResourceParser { private String getNonDefaultNamespaceUri(int offset) { String prefix = mStringBlock.getString(mNamespaces.getPrefix(offset)); - if (prefix != null) { - return mStringBlock.getString(mNamespaces.getUri(offset)); + if (prefix == null) { + // If we are here. There is some clever obfuscation going on. Our reference points to the namespace are gone. + // Normally we could take the index * attributeCount to get an offset. + // That would point to the URI in the StringBlock table, but that is empty. + // We have the namespaces that can't be touched in the opening tag. + // Though no known way to correlate them at this time. + // So return the res-auto namespace. + return ANDROID_RES_NS_AUTO; } - - // If we are here. There is some clever obfuscation going on. Our reference points to the namespace are gone. - // Normally we could take the index * attributeCount to get an offset. - // That would point to the URI in the StringBlock table, but that is empty. - // We have the namespaces that can't be touched in the opening tag. - // Though no known way to correlate them at this time. - // So return the res-auto namespace. - return ANDROID_RES_NS_AUTO; + return mStringBlock.getString(mNamespaces.getUri(offset)); } @Override @@ -348,10 +382,10 @@ public class AXmlResourceParser implements XmlResourceParser { String resourceMapValue; String stringBlockValue = mStringBlock.getString(name); - int resourceId = getAttributeNameResource(index); + int attrResId = getAttributeNameResource(index); try { - resourceMapValue = decodeFromResourceId(resourceId); + resourceMapValue = decodeFromResourceId(attrResId); } catch (AndrolibException ignored) { resourceMapValue = null; } @@ -371,7 +405,7 @@ public class AXmlResourceParser implements XmlResourceParser { } // In this case we have a bogus resource. If it was not found in either. - return "APKTOOL_MISSING_" + Integer.toHexString(resourceId); + return "APKTOOL_MISSING_" + Integer.toHexString(attrResId); } @Override @@ -404,13 +438,17 @@ public class AXmlResourceParser implements XmlResourceParser { int valueRaw = mAttributes[offset + ATTRIBUTE_IX_VALUE_STRING]; try { - String stringBlockValue = valueRaw == -1 ? null : ResXmlEncoders.escapeXmlChars(mStringBlock.getString(valueRaw)); + String stringBlockValue = valueRaw != -1 + ? ResXmlEncoders.escapeXmlChars(mStringBlock.getString(valueRaw)) + : null; String resourceMapValue = null; // Ensure we only track down obfuscated values for reference/attribute type values. Otherwise, we might // spam lookups against resource table for invalid ids. - if (valueType == TypedValue.TYPE_REFERENCE || valueType == TypedValue.TYPE_DYNAMIC_REFERENCE || - valueType == TypedValue.TYPE_ATTRIBUTE || valueType == TypedValue.TYPE_DYNAMIC_ATTRIBUTE) { + if (valueType == TypedValue.TYPE_REFERENCE + || valueType == TypedValue.TYPE_DYNAMIC_REFERENCE + || valueType == TypedValue.TYPE_ATTRIBUTE + || valueType == TypedValue.TYPE_DYNAMIC_ATTRIBUTE) { resourceMapValue = decodeFromResourceId(valueData); } String value = getPreferredString(stringBlockValue, resourceMapValue); @@ -451,21 +489,20 @@ public class AXmlResourceParser implements XmlResourceParser { public float getAttributeFloatValue(int index, float defaultValue) { int offset = getAttributeOffset(index); int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE]; - if (valueType == TypedValue.TYPE_FLOAT) { - int valueData = mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA]; - return Float.intBitsToFloat(valueData); + if (valueType != TypedValue.TYPE_FLOAT) { + return defaultValue; } - return defaultValue; + return Float.intBitsToFloat(mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA]); } @Override public int getAttributeIntValue(int index, int defaultValue) { int offset = getAttributeOffset(index); int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE]; - if (valueType >= TypedValue.TYPE_FIRST_INT && valueType <= TypedValue.TYPE_LAST_INT) { - return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA]; + if (valueType < TypedValue.TYPE_FIRST_INT || valueType > TypedValue.TYPE_LAST_INT) { + return defaultValue; } - return defaultValue; + return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA]; } @Override @@ -477,10 +514,10 @@ public class AXmlResourceParser implements XmlResourceParser { public int getAttributeResourceValue(int index, int defaultValue) { int offset = getAttributeOffset(index); int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE]; - if (valueType == TypedValue.TYPE_REFERENCE) { - return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA]; + if (valueType != TypedValue.TYPE_REFERENCE) { + return defaultValue; } - return defaultValue; + return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA]; } @Override @@ -609,12 +646,12 @@ public class AXmlResourceParser implements XmlResourceParser { } @Override - public boolean getFeature(String feature) { + public boolean getFeature(String name) { return false; } @Override - public void setFeature(String name, boolean value) throws XmlPullParserException { + public void setFeature(String name, boolean state) throws XmlPullParserException { throw new XmlPullParserException(E_NOT_SUPPORTED); } @@ -685,7 +722,7 @@ public class AXmlResourceParser implements XmlResourceParser { mStringBlock = StringBlock.readWithChunk(mIn); mNamespaces.increaseDepth(); - isOperational = true; + mIsOperational = true; } if (mEvent == END_DOCUMENT) { @@ -696,8 +733,8 @@ public class AXmlResourceParser implements XmlResourceParser { resetEventInfo(); while (true) { - if (m_decreaseDepth) { - m_decreaseDepth = false; + if (mDecreaseDepth) { + mDecreaseDepth = false; mNamespaces.decreaseDepth(); } @@ -708,7 +745,7 @@ public class AXmlResourceParser implements XmlResourceParser { } // #2070 - Some applications have 2 start namespaces, but only 1 end namespace. - if (mIn.remaining() == 0) { + if (mIn.available() == 0) { LOGGER.warning(String.format("AXML hit unexpected end of file at byte: 0x%X", mIn.position())); mEvent = END_DOCUMENT; break; @@ -807,7 +844,7 @@ public class AXmlResourceParser implements XmlResourceParser { mNamespaceIndex = mIn.readInt(); mNameIndex = mIn.readInt(); mEvent = END_TAG; - m_decreaseDepth = true; + mDecreaseDepth = true; break; } @@ -826,40 +863,4 @@ public class AXmlResourceParser implements XmlResourceParser { mFirstError = error; } } - - private ExtCountingDataInput mIn; - private final ResTable mResTable; - private AndrolibException mFirstError; - - private boolean isOperational = false; - private StringBlock mStringBlock; - private int[] mResourceIds; - private final NamespaceStack mNamespaces = new NamespaceStack(); - private boolean m_decreaseDepth; - - // All values are essentially indices, e.g. mNameIndex is an index of name in mStringBlock. - private int mEvent; - private int mLineNumber; - private int mNameIndex; - private int mNamespaceIndex; - private int[] mAttributes; - private int mIdIndex; - private int mClassIndex; - private int mStyleIndex; - - private final static Logger LOGGER = Logger.getLogger(AXmlResourceParser.class.getName()); - private static final String E_NOT_SUPPORTED = "Method is not supported."; - - // ResXMLTree_attribute - private static final int ATTRIBUTE_IX_NAMESPACE_URI = 0; // ns - private static final int ATTRIBUTE_IX_NAME = 1; // name - private static final int ATTRIBUTE_IX_VALUE_STRING = 2; // rawValue - private static final int ATTRIBUTE_IX_VALUE_TYPE = 3; // (size/res0/dataType) - private static final int ATTRIBUTE_IX_VALUE_DATA = 4; // data - private static final int ATTRIBUTE_LENGTH = 5; - - private static final int PRIVATE_PKG_ID = 0x7F; - - private static final String ANDROID_RES_NS_AUTO = "http://schemas.android.com/apk/res-auto"; - private static final String ANDROID_RES_NS = "http://schemas.android.com/apk/res/android"; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestPullStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestPullStreamDecoder.java new file mode 100644 index 00000000..d3235015 --- /dev/null +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestPullStreamDecoder.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package brut.androlib.res.decoder; + +import brut.androlib.exceptions.AndrolibException; +import brut.androlib.exceptions.AXmlDecodingException; +import brut.androlib.exceptions.RawXmlEncounteredException; +import brut.androlib.res.data.ResTable; +import brut.xmlpull.XmlPullUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.*; + +public class AndroidManifestPullStreamDecoder implements ResStreamDecoder { + private final AXmlResourceParser mParser; + private final XmlSerializer mSerial; + + public AndroidManifestPullStreamDecoder(AXmlResourceParser parser, XmlSerializer serial) { + mParser = parser; + mSerial = serial; + } + + @Override + public void decode(InputStream in, OutputStream out) throws AndrolibException { + try { + mParser.setInput(in, null); + mSerial.setOutput(out, null); + XmlPullUtils.copy(mParser, mSerial, new EventHandler(mParser.getResTable())); + } catch (XmlPullParserException ex) { + throw new AXmlDecodingException("Could not decode XML", ex); + } catch (IOException ex) { + throw new RawXmlEncounteredException("Could not decode XML", ex); + } + } + + private static class EventHandler implements XmlPullUtils.EventHandler { + private final ResTable mResTable; + private final boolean mHideSdkInfo; + + public EventHandler(ResTable resTable) { + mResTable = resTable; + mHideSdkInfo = !resTable.getAnalysisMode(); + } + + @Override + public boolean onEvent(XmlPullParser in, XmlSerializer out) throws XmlPullParserException { + int type = in.getEventType(); + + if (type == XmlPullParser.START_TAG) { + String name = in.getName(); + + if (name.equals("manifest")) { + parseManifest(in); + } else if (name.equals("uses-sdk")) { + parseUsesSdk(in); + + if (mHideSdkInfo) { + return true; + } + } + } else if (type == XmlPullParser.END_TAG) { + String name = in.getName(); + + if (name.equals("uses-sdk")) { + if (mHideSdkInfo) { + return true; + } + } + } + + return false; + } + + private void parseManifest(XmlPullParser in) { + for (int i = 0; i < in.getAttributeCount(); i++) { + String ns = in.getAttributeNamespace(i); + String name = in.getAttributeName(i); + String value = in.getAttributeValue(i); + + if (value.isEmpty()) { + continue; + } + if (ns.isEmpty()) { + if (name.equals("package")) { + mResTable.setPackageRenamed(value); + } + } else if (ns.equals(AXmlResourceParser.ANDROID_RES_NS)) { + switch (name) { + case "versionCode": + mResTable.setVersionCode(value); + break; + case "versionName": + mResTable.setVersionName(value); + break; + } + } + } + } + + private void parseUsesSdk(XmlPullParser in) { + for (int i = 0; i < in.getAttributeCount(); i++) { + String ns = in.getAttributeNamespace(i); + String name = in.getAttributeName(i); + String value = in.getAttributeValue(i); + + if (value.isEmpty()) { + continue; + } + if (ns.equals(AXmlResourceParser.ANDROID_RES_NS)) { + switch (name) { + case "minSdkVersion": + case "targetSdkVersion": + case "maxSdkVersion": + case "compileSdkVersion": + mResTable.addSdkInfo(name, value); + break; + } + } + } + } + } +} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestResourceParser.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestResourceParser.java index 64562b4c..a79385d6 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestResourceParser.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/AndroidManifestResourceParser.java @@ -25,11 +25,6 @@ import java.util.regex.Pattern; * AXmlResourceParser specifically for parsing encoded AndroidManifest.xml. */ public class AndroidManifestResourceParser extends AXmlResourceParser { - - public AndroidManifestResourceParser(ResTable resTable) { - super(resTable); - } - /** * Pattern for matching numeric string meta-data values. aapt automatically infers the * type for a manifest meta-data value based on the string in the unencoded XML. However, @@ -39,6 +34,10 @@ public class AndroidManifestResourceParser extends AXmlResourceParser { */ private static final Pattern PATTERN_NUMERIC_STRING = Pattern.compile("\\s?\\d+"); + public AndroidManifestResourceParser(ResTable resTable) { + super(resTable); + } + @Override public String getAttributeValue(int index) { String value = super.getAttributeValue(index); @@ -58,9 +57,9 @@ public class AndroidManifestResourceParser extends AXmlResourceParser { } private boolean isNumericStringMetadataAttributeValue(int index, String value) { - return "meta-data".equalsIgnoreCase(super.getName()) - && "value".equalsIgnoreCase(super.getAttributeName(index)) - && super.getAttributeValueType(index) == TypedValue.TYPE_STRING - && PATTERN_NUMERIC_STRING.matcher(value).matches(); + return "meta-data".equals(super.getName()) + && "value".equals(super.getAttributeName(index)) + && super.getAttributeValueType(index) == TypedValue.TYPE_STRING + && PATTERN_NUMERIC_STRING.matcher(value).matches(); } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchAndroidStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchAndroidStreamDecoder.java index 801b05e7..794cafad 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchAndroidStreamDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchAndroidStreamDecoder.java @@ -7,6 +7,7 @@ import brut.androlib.exceptions.CantFind9PatchChunkException; import brut.androlib.res.data.ninepatch.NinePatchData; import brut.androlib.res.data.ninepatch.OpticalInset; import brut.util.ExtDataInput; +import brut.util.ExtDataInputStream; import org.apache.commons.io.IOUtils; import java.io.*; @@ -87,14 +88,14 @@ public class Res9patchAndroidStreamDecoder implements ResStreamDecoder { private NinePatchData getNinePatch(byte[] data) throws AndrolibException, IOException { - ExtDataInput di = new ExtDataInput(new ByteArrayInputStream(data)); + ExtDataInput di = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data)); find9patchChunk(di, NP_CHUNK_TYPE); return NinePatchData.decode(di); } private OpticalInset getOpticalInset(byte[] data) throws AndrolibException, IOException { - ExtDataInput di = new ExtDataInput(new ByteArrayInputStream(data)); + ExtDataInput di = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data)); find9patchChunk(di, OI_CHUNK_TYPE); return OpticalInset.decode(di); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchStreamDecoder.java index 1750ba78..20f7d349 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchStreamDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/Res9patchStreamDecoder.java @@ -21,6 +21,7 @@ import brut.androlib.exceptions.CantFind9PatchChunkException; import brut.androlib.res.data.ninepatch.NinePatchData; import brut.androlib.res.data.ninepatch.OpticalInset; import brut.util.ExtDataInput; +import brut.util.ExtDataInputStream; import org.apache.commons.io.IOUtils; import javax.imageio.ImageIO; @@ -30,6 +31,11 @@ import java.awt.image.WritableRaster; import java.io.*; public class Res9patchStreamDecoder implements ResStreamDecoder { + private static final int NP_CHUNK_TYPE = 0x6e705463; // npTc + private static final int OI_CHUNK_TYPE = 0x6e704c62; // npLb + private static final int NP_COLOR = 0xff000000; + private static final int OI_COLOR = 0xffff0000; + @Override public void decode(InputStream in, OutputStream out) throws AndrolibException { try { @@ -122,32 +128,32 @@ public class Res9patchStreamDecoder implements ResStreamDecoder { private NinePatchData getNinePatch(byte[] data) throws AndrolibException, IOException { - ExtDataInput di = new ExtDataInput(new ByteArrayInputStream(data)); - find9patchChunk(di, NP_CHUNK_TYPE); - return NinePatchData.decode(di); + ExtDataInput in = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data)); + find9patchChunk(in, NP_CHUNK_TYPE); + return NinePatchData.decode(in); } private OpticalInset getOpticalInset(byte[] data) throws AndrolibException, IOException { - ExtDataInput di = new ExtDataInput(new ByteArrayInputStream(data)); - find9patchChunk(di, OI_CHUNK_TYPE); - return OpticalInset.decode(di); + ExtDataInput in = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data)); + find9patchChunk(in, OI_CHUNK_TYPE); + return OpticalInset.decode(in); } - private void find9patchChunk(DataInput di, int magic) throws AndrolibException, + private void find9patchChunk(DataInput in, int magic) throws AndrolibException, IOException { - di.skipBytes(8); + in.skipBytes(8); while (true) { int size; try { - size = di.readInt(); + size = in.readInt(); } catch (IOException ex) { - throw new CantFind9PatchChunkException("Cant find nine patch chunk", ex); + throw new CantFind9PatchChunkException("Could not find nine patch chunk", ex); } - if (di.readInt() == magic) { + if (in.readInt() == magic) { return; } - di.skipBytes(size + 4); + in.skipBytes(size + 4); } } @@ -162,9 +168,4 @@ public class Res9patchStreamDecoder implements ResStreamDecoder { im.setRGB(x, y, NP_COLOR); } } - - private static final int NP_CHUNK_TYPE = 0x6e705463; // npTc - private static final int OI_CHUNK_TYPE = 0x6e704c62; // npLb - private static final int NP_COLOR = 0xff000000; - private static final int OI_COLOR = 0xffff0000; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java index 979d89a8..1c3112ac 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java @@ -22,9 +22,9 @@ import brut.androlib.exceptions.RawXmlEncounteredException; import brut.androlib.res.data.ResResource; import brut.androlib.res.data.value.ResBoolValue; import brut.androlib.res.data.value.ResFileValue; -import brut.directory.DirUtil; import brut.directory.Directory; import brut.directory.DirectoryException; +import brut.directory.DirUtils; import brut.util.BrutIO; import java.io.*; @@ -33,10 +33,21 @@ import java.util.logging.Level; import java.util.logging.Logger; public class ResFileDecoder { + private static final Logger LOGGER = Logger.getLogger(ResFileDecoder.class.getName()); + + private static final String[] RAW_IMAGE_EXTENSIONS = { + "m4a", // apple + "qmg", // samsung + }; + private static final String[] RAW_9PATCH_IMAGE_EXTENSIONS = { + "qmg", // samsung + "spi", // samsung + }; + private final ResStreamDecoderContainer mDecoders; public ResFileDecoder(ResStreamDecoderContainer decoders) { - this.mDecoders = decoders; + mDecoders = decoders; } public void decode(ResResource res, Directory inDir, Directory outDir, Map resFileMapping) @@ -109,7 +120,7 @@ public class ResFileDecoder { return; } catch (CantFind9PatchChunkException ex) { LOGGER.log(Level.WARNING, String.format( - "Cant find 9patch chunk in file: \"%s\". Renaming it to *.png.", inFileName + "Could not find 9patch chunk in file: \"%s\". Renaming it to *.png.", inFileName ), ex); outDir.removeFile(outFileName); outFileName = outResName + ext; @@ -156,24 +167,12 @@ public class ResFileDecoder { } } - public void copyRaw(Directory inDir, Directory outDir, String inFilename, - String outFilename) throws AndrolibException { + public void copyRaw(Directory inDir, Directory outDir, String inFileName, + String outFileName) throws AndrolibException { try { - DirUtil.copyToDir(inDir, outDir, inFilename, outFilename); + DirUtils.copyToDir(inDir, outDir, inFileName, outFileName); } catch (DirectoryException ex) { throw new AndrolibException(ex); } } - - private final static Logger LOGGER = Logger.getLogger(ResFileDecoder.class.getName()); - - private final static String[] RAW_IMAGE_EXTENSIONS = new String[] { - "m4a", // apple - "qmg", // samsung - }; - - private final static String[] RAW_9PATCH_IMAGE_EXTENSIONS = new String[] { - "qmg", // samsung - "spi", // samsung - }; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResRawStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResRawStreamDecoder.java index 38d81c82..c4c7119a 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResRawStreamDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResRawStreamDecoder.java @@ -22,9 +22,9 @@ import org.apache.commons.io.IOUtils; import java.io.*; public class ResRawStreamDecoder implements ResStreamDecoder { + @Override - public void decode(InputStream in, OutputStream out) - throws AndrolibException { + public void decode(InputStream in, OutputStream out) throws AndrolibException { try { IOUtils.copy(in, out); } catch (IOException ex) { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResStreamDecoder.java index d2f63ae0..d0825d1c 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResStreamDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResStreamDecoder.java @@ -20,6 +20,5 @@ import brut.androlib.exceptions.AndrolibException; import java.io.*; public interface ResStreamDecoder { - void decode(InputStream in, OutputStream out) - throws AndrolibException; + void decode(InputStream in, OutputStream out) throws AndrolibException; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResStreamDecoderContainer.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResStreamDecoderContainer.java index ca7a7227..f9fd0edd 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResStreamDecoderContainer.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResStreamDecoderContainer.java @@ -21,7 +21,11 @@ import java.io.*; import java.util.*; public class ResStreamDecoderContainer { - private final Map mDecoders = new HashMap<>(); + private final Map mDecoders; + + public ResStreamDecoderContainer() { + mDecoders = new HashMap<>(); + } public void decode(InputStream in, OutputStream out, String decoderName) throws AndrolibException { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResXmlPullStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResXmlPullStreamDecoder.java new file mode 100644 index 00000000..1e90d176 --- /dev/null +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResXmlPullStreamDecoder.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package brut.androlib.res.decoder; + +import brut.androlib.exceptions.AndrolibException; +import brut.androlib.exceptions.AXmlDecodingException; +import brut.androlib.exceptions.RawXmlEncounteredException; +import brut.xmlpull.XmlPullUtils; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.*; + +public class ResXmlPullStreamDecoder implements ResStreamDecoder { + private final AXmlResourceParser mParser; + private final XmlSerializer mSerial; + + public ResXmlPullStreamDecoder(AXmlResourceParser parser, XmlSerializer serial) { + mParser = parser; + mSerial = serial; + } + + @Override + public void decode(InputStream in, OutputStream out) throws AndrolibException { + try { + mParser.setInput(in, null); + mSerial.setOutput(out, null); + XmlPullUtils.copy(mParser, mSerial); + } catch (XmlPullParserException ex) { + throw new AXmlDecodingException("Could not decode XML", ex); + } catch (IOException ex) { + throw new RawXmlEncounteredException("Could not decode XML", ex); + } + } +} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java index 34a95598..831e741d 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java @@ -18,20 +18,34 @@ package brut.androlib.res.decoder; import brut.androlib.res.data.arsc.ARSCHeader; import brut.androlib.res.xml.ResXmlEncoders; -import brut.util.ExtCountingDataInput; +import brut.util.ExtDataInput; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.*; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.logging.Logger; public class StringBlock { - public static StringBlock readWithChunk(ExtCountingDataInput reader) throws IOException { - int startPosition = reader.position(); + private static final Logger LOGGER = Logger.getLogger(StringBlock.class.getName()); + + private static final CharsetDecoder UTF16LE_DECODER = StandardCharsets.UTF_16LE.newDecoder(); + private static final CharsetDecoder UTF8_DECODER = StandardCharsets.UTF_8.newDecoder(); + private static final CharsetDecoder CESU8_DECODER = Charset.forName("CESU8").newDecoder(); + + private static final int UTF8_FLAG = 0x00000100; + private static final int STRING_BLOCK_HEADER_SIZE = 28; + + private int[] mStringOffsets; + private byte[] mStrings; + private int[] mStyleOffsets; + private int[] mStyles; + private boolean mIsUtf8; + + public static StringBlock readWithChunk(ExtDataInput reader) throws IOException { + long startPosition = reader.position(); reader.skipCheckShort(ARSCHeader.RES_STRING_POOL_TYPE); int headerSize = reader.readShort(); int chunkSize = reader.readInt(); @@ -39,9 +53,8 @@ public class StringBlock { return readWithoutChunk(reader, startPosition, headerSize, chunkSize); } - public static StringBlock readWithoutChunk(ExtCountingDataInput reader, int startPosition, int headerSize, - int chunkSize) throws IOException - { + public static StringBlock readWithoutChunk(ExtDataInput reader, long startPosition, + int headerSize, int chunkSize) throws IOException { // ResStringPool_header int stringCount = reader.readInt(); int styleCount = reader.readInt(); @@ -91,6 +104,15 @@ public class StringBlock { return block; } + private StringBlock() { + } + + @VisibleForTesting + StringBlock(byte[] strings, boolean isUTF8) { + mStrings = strings; + mIsUtf8 = isUTF8; + } + /** * Returns raw string (without any styling information) at specified index. * @param index int @@ -112,6 +134,7 @@ public class StringBlock { offset += val[0]; } length = val[1]; + return decodeString(offset, length); } @@ -139,7 +162,7 @@ public class StringBlock { for (int i = 0; i < style.length; i += 3) { spans.add(new StyledString.Span(getString(style[i]), style[i + 1], style[i + 2])); } - Collections.sort(spans); + spans.sort(null); StyledString styledString = new StyledString(text, spans); return styledString.toString(); @@ -155,14 +178,14 @@ public class StringBlock { if (string == null) { return -1; } - for (int i = 0; i != mStringOffsets.length; ++i) { + for (int i = 0; i != mStringOffsets.length; i++) { int offset = mStringOffsets[i]; int length = getShort(mStrings, offset); if (length != string.length()) { continue; } int j = 0; - for (; j != length; ++j) { + for (; j != length; j++) { offset += 2; if (string.charAt(j) != getShort(mStrings, offset)) { break; @@ -175,15 +198,6 @@ public class StringBlock { return -1; } - private StringBlock() { - } - - @VisibleForTesting - StringBlock(byte[] strings, boolean isUTF8) { - mStrings = strings; - mIsUtf8 = isUTF8; - } - /** * Returns style information - array of int triplets, where in each triplet: * * first int is index of tag name ('b','i', etc.) * second int is tag @@ -197,7 +211,7 @@ public class StringBlock { int count = 0; int[] style; - for (int i = offset; i < mStyles.length; ++i) { + for (int i = offset; i < mStyles.length; i++) { if (mStyles[i] == -1) { break; } @@ -240,7 +254,7 @@ public class StringBlock { // in some places, Android uses 3-byte UTF-8 sequences instead of 4-bytes. // If decoding failed, we try to use CESU-8 decoder, which is closer to what Android actually uses. return CESU8_DECODER.decode(wrappedBufferRetry).toString(); - } catch (CharacterCodingException e) { + } catch (CharacterCodingException ex) { LOGGER.warning("Failed to decode a string with CESU-8 decoder."); return null; } @@ -263,39 +277,25 @@ public class StringBlock { val = array[offset]; offset += 1; if ((val & 0x80) != 0) { - int low = (array[offset] & 0xFF); - length = ((val & 0x7F) << 8) + low; + int low = array[offset] & 0xFF; + length = ((val & 0x7F) << 8) + low; offset += 1; } else { length = val; } - return new int[] { offset, length}; + return new int[] { offset, length }; } private static int[] getUtf16(byte[] array, int offset) { - int val = ((array[offset + 1] & 0xFF) << 8 | array[offset] & 0xFF); + int val = (array[offset + 1] & 0xFF) << 8 | array[offset] & 0xFF; if ((val & 0x8000) != 0) { int high = (array[offset + 3] & 0xFF) << 8; int low = (array[offset + 2] & 0xFF); int len_value = ((val & 0x7FFF) << 16) + (high + low); - return new int[] {4, len_value * 2}; + return new int[] { 4, len_value * 2 }; } - return new int[] {2, val * 2}; + return new int[] { 2, val * 2 }; } - - private int[] mStringOffsets; - private byte[] mStrings; - private int[] mStyleOffsets; - private int[] mStyles; - private boolean mIsUtf8; - - private final CharsetDecoder UTF16LE_DECODER = StandardCharsets.UTF_16LE.newDecoder(); - private final CharsetDecoder UTF8_DECODER = StandardCharsets.UTF_8.newDecoder(); - private final CharsetDecoder CESU8_DECODER = Charset.forName("CESU8").newDecoder(); - private static final Logger LOGGER = Logger.getLogger(StringBlock.class.getName()); - - private static final int UTF8_FLAG = 0x00000100; - private static final int STRING_BLOCK_HEADER_SIZE = 28; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StyledString.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StyledString.java index fc9affeb..454a8cff 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StyledString.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/StyledString.java @@ -27,12 +27,14 @@ import java.util.Map; import java.util.logging.Logger; public class StyledString { + private static final Logger LOGGER = Logger.getLogger(StyledString.class.getName()); + private final String mText; private final List mSpans; public StyledString(String text, List spans) { - this.mText = text; - this.mSpans = spans; + mText = text; + mSpans = spans; } String getText() { @@ -52,63 +54,63 @@ public class StyledString { private static final MapSplitter ATTRIBUTES_SPLITTER = Splitter.on(';').omitEmptyStrings().withKeyValueSeparator(Splitter.on('=').limit(2)); - private final String tag; - private final int firstChar; - private final int lastChar; + private final String mTag; + private final int mFirstChar; + private final int mLastChar; public Span(String tag, int firstChar, int lastChar) { - this.tag = tag; - this.firstChar = firstChar; - this.lastChar = lastChar; + mTag = tag; + mFirstChar = firstChar; + mLastChar = lastChar; } public String getTag() { - return tag; + return mTag; } public int getFirstChar() { - return firstChar; + return mFirstChar; } public int getLastChar() { - return lastChar; + return mLastChar; } public String getName() { - int separatorIdx = tag.indexOf(';'); - return separatorIdx == -1 ? tag : tag.substring(0, separatorIdx); + int separatorIdx = mTag.indexOf(';'); + return separatorIdx == -1 ? mTag : mTag.substring(0, separatorIdx); } public Map getAttributes() { - int separatorIdx = tag.indexOf(';'); - return separatorIdx == -1 ? null : ATTRIBUTES_SPLITTER.split( - tag.substring(separatorIdx + 1, tag.endsWith(";") ? tag.length() - 1 : tag.length()) - ); + int separatorIdx = mTag.indexOf(';'); + return separatorIdx != -1 ? ATTRIBUTES_SPLITTER.split( + mTag.substring(separatorIdx + 1, mTag.endsWith(";") ? mTag.length() - 1 : mTag.length()) + ) : null; } @Override public int compareTo(Span o) { - int res = Integer.compare(firstChar, o.firstChar); + int res = Integer.compare(mFirstChar, o.mFirstChar); if (res != 0) { return res; } - res = Integer.compare(lastChar, o.lastChar); + res = Integer.compare(mLastChar, o.mLastChar); if (res != 0) { return -res; } - return -tag.compareTo(o.tag); + return -mTag.compareTo(o.mTag); } } private static class Decoder { - private String text; - private StringBuilder xmlValue; - private int lastOffset; + private String mText; + private StringBuilder mXmlValue; + private int mLastOffset; - String decode(StyledString styledString) { - text = styledString.getText(); - xmlValue = new StringBuilder(text.length() * 2); - lastOffset = 0; + public String decode(StyledString styledString) { + mText = styledString.getText(); + mXmlValue = new StringBuilder(mText.length() * 2); + mLastOffset = 0; // recurse top-level tags PeekingIterator it = Iterators.peekingIterator(styledString.getSpans().iterator()); @@ -116,11 +118,11 @@ public class StyledString { decodeIterate(it); } - // write the remaining encoded raw text - if (lastOffset < text.length()) { - xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset))); + // write the remaining encoded raw mText + if (mLastOffset < mText.length()) { + mXmlValue.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset))); } - return xmlValue.toString(); + return mXmlValue.toString(); } private void decodeIterate(PeekingIterator it) { @@ -130,45 +132,43 @@ public class StyledString { int spanStart = span.getFirstChar(); int spanEnd = span.getLastChar() + 1; - // write encoded raw text preceding the opening tag - if (spanStart > lastOffset) { - xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset, spanStart))); + // write encoded raw mText preceding the opening tag + if (spanStart > mLastOffset) { + mXmlValue.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset, spanStart))); } - lastOffset = spanStart; + mLastOffset = spanStart; // write opening tag - xmlValue.append('<').append(name); + mXmlValue.append('<').append(name); if (attributes != null) { for (Map.Entry attrEntry : attributes.entrySet()) { - xmlValue.append(' ').append(attrEntry.getKey()).append("=\"") + mXmlValue.append(' ').append(attrEntry.getKey()).append("=\"") .append(ResXmlEncoders.escapeXmlChars(attrEntry.getValue())).append('"'); } } // if an opening tag is followed by a matching closing tag, write as an empty-element tag if (spanStart == spanEnd) { - xmlValue.append("/>"); + mXmlValue.append("/>"); return; } - xmlValue.append('>'); + mXmlValue.append('>'); // recurse nested tags while (it.hasNext() && it.peek().getFirstChar() < spanEnd) { decodeIterate(it); } - // write encoded raw text preceding the closing tag - if (spanEnd > lastOffset && text.length() >= spanEnd) { - xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset, spanEnd))); - } else if (text.length() >= lastOffset && text.length() < spanEnd) { - LOGGER.warning("Span (" + name + ") exceeds text length " + text.length()); - xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset))); + // write encoded raw mText preceding the closing tag + if (spanEnd > mLastOffset && mText.length() >= spanEnd) { + mXmlValue.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset, spanEnd))); + } else if (mText.length() >= mLastOffset && mText.length() < spanEnd) { + LOGGER.warning("Span (" + name + ") exceeds mText length " + mText.length()); + mXmlValue.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset))); } - lastOffset = spanEnd; + mLastOffset = spanEnd; // write closing tag - xmlValue.append("'); + mXmlValue.append("'); } } - - private static final Logger LOGGER = Logger.getLogger(StyledString.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java deleted file mode 100644 index de7aff98..00000000 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2010 Ryszard Wiśniewski - * Copyright (C) 2010 Connor Tumbleson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package brut.androlib.res.decoder; - -import brut.androlib.exceptions.AndrolibException; -import brut.androlib.exceptions.AXmlDecodingException; -import brut.androlib.exceptions.RawXmlEncounteredException; -import brut.androlib.res.data.ResTable; -import brut.androlib.res.util.ExtXmlSerializer; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.wrapper.XmlPullParserWrapper; -import org.xmlpull.v1.wrapper.XmlPullWrapperFactory; -import org.xmlpull.v1.wrapper.XmlSerializerWrapper; -import org.xmlpull.v1.wrapper.classic.StaticXmlSerializerWrapper; - -import java.io.*; - -public class XmlPullStreamDecoder implements ResStreamDecoder { - public XmlPullStreamDecoder(AXmlResourceParser parser, - ExtXmlSerializer serializer) { - this.mParser = parser; - this.mSerial = serializer; - } - - @Override - public void decode(InputStream in, OutputStream out) - throws AndrolibException { - try { - XmlPullWrapperFactory factory = XmlPullWrapperFactory.newInstance(); - XmlPullParserWrapper par = factory.newPullParserWrapper(mParser); - final ResTable resTable = mParser.getResTable(); - - XmlSerializerWrapper ser = new StaticXmlSerializerWrapper(mSerial, factory) { - boolean hideSdkInfo = false; - boolean hidePackageInfo = false; - - @Override - public void event(XmlPullParser pp) - throws XmlPullParserException, IOException { - int type = pp.getEventType(); - - if (type == XmlPullParser.START_TAG) { - if ("manifest".equalsIgnoreCase(pp.getName())) { - try { - hidePackageInfo = parseManifest(pp); - } catch (AndrolibException ignored) {} - } else if ("uses-sdk".equalsIgnoreCase(pp.getName())) { - try { - hideSdkInfo = parseAttr(pp); - if (hideSdkInfo) { - return; - } - } catch (AndrolibException ignored) {} - } - } else if (hideSdkInfo && type == XmlPullParser.END_TAG - && "uses-sdk".equalsIgnoreCase(pp.getName())) { - return; - } else if (hidePackageInfo && type == XmlPullParser.END_TAG - && "manifest".equalsIgnoreCase(pp.getName())) { - super.event(pp); - return; - } - super.event(pp); - } - - private boolean parseManifest(XmlPullParser pp) - throws AndrolibException { - String attr_name; - - // read for package: - for (int i = 0; i < pp.getAttributeCount(); i++) { - attr_name = pp.getAttributeName(i); - - if (attr_name.equalsIgnoreCase(("package"))) { - resTable.setPackageRenamed(pp.getAttributeValue(i)); - } else if (attr_name.equalsIgnoreCase("versionCode")) { - resTable.setVersionCode(pp.getAttributeValue(i)); - } else if (attr_name.equalsIgnoreCase("versionName")) { - resTable.setVersionName(pp.getAttributeValue(i)); - } - } - return true; - } - - private boolean parseAttr(XmlPullParser pp) - throws AndrolibException { - for (int i = 0; i < pp.getAttributeCount(); i++) { - final String a_ns = "http://schemas.android.com/apk/res/android"; - String ns = pp.getAttributeNamespace(i); - - if (a_ns.equalsIgnoreCase(ns)) { - String name = pp.getAttributeName(i); - String value = pp.getAttributeValue(i); - if (name != null && value != null) { - if (name.equalsIgnoreCase("minSdkVersion") - || name.equalsIgnoreCase("targetSdkVersion") - || name.equalsIgnoreCase("maxSdkVersion") - || name.equalsIgnoreCase("compileSdkVersion")) { - resTable.addSdkInfo(name, value); - } else { - resTable.clearSdkInfo(); - return false; // Found unknown flags - } - } - } else { - resTable.clearSdkInfo(); - - if (i >= pp.getAttributeCount()) { - return false; // Found unknown flags - } - } - } - - return ! resTable.getAnalysisMode(); - } - }; - - par.setInput(in, null); - ser.setOutput(out, null); - - while (par.nextToken() != XmlPullParser.END_DOCUMENT) { - ser.event(par); - } - ser.flush(); - } catch (XmlPullParserException ex) { - throw new AXmlDecodingException("Could not decode XML", ex); - } catch (IOException ex) { - throw new RawXmlEncounteredException("Could not decode XML", ex); - } - } - - public void decodeManifest(InputStream in, OutputStream out) - throws AndrolibException { - decode(in, out); - } - - private final AXmlResourceParser mParser; - private final ExtXmlSerializer mSerial; -} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java deleted file mode 100644 index 7114dae4..00000000 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2010 Ryszard Wiśniewski - * Copyright (C) 2010 Connor Tumbleson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package brut.androlib.res.util; - -import org.xmlpull.renamed.MXSerializer; - -import java.io.*; - -public class ExtMXSerializer extends MXSerializer implements ExtXmlSerializer { - @Override - public void startDocument(String encoding, Boolean standalone) - throws IOException, IllegalArgumentException, IllegalStateException { - super.startDocument(encoding != null ? encoding : mDefaultEncoding, standalone); - this.newLine(); - } - - @Override - protected void writeAttributeValue(String value, Writer out) throws IOException { - if (mIsDisabledAttrEscape) { - out.write(value == null ? "" : value); - return; - } - super.writeAttributeValue(value, out); - } - - @Override - public void setOutput(OutputStream os, String encoding) throws IOException { - super.setOutput(os, encoding != null ? encoding : mDefaultEncoding); - } - - @Override - public Object getProperty(String name) throws IllegalArgumentException { - if (PROPERTY_DEFAULT_ENCODING.equals(name)) { - return mDefaultEncoding; - } - return super.getProperty(name); - } - - @Override - public void setProperty(String name, Object value) throws IllegalArgumentException, IllegalStateException { - if (PROPERTY_DEFAULT_ENCODING.equals(name)) { - mDefaultEncoding = (String) value; - } else { - super.setProperty(name, value); - } - } - - @Override - public ExtXmlSerializer newLine() throws IOException { - super.out.write(lineSeparator); - return this; - } - - @Override - public void setDisabledAttrEscape(boolean disabled) { - mIsDisabledAttrEscape = disabled; - } - - private String mDefaultEncoding; - private boolean mIsDisabledAttrEscape = false; - -} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java deleted file mode 100644 index 3e4592ef..00000000 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2010 Ryszard Wiśniewski - * Copyright (C) 2010 Connor Tumbleson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package brut.androlib.res.util; - -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; - -public interface ExtXmlSerializer extends XmlSerializer { - - ExtXmlSerializer newLine() throws IOException; - - void setDisabledAttrEscape(boolean disabled); - - String PROPERTY_SERIALIZER_INDENTATION = "http://xmlpull.org/v1/doc/properties.html#serializer-indentation"; - String PROPERTY_SERIALIZER_LINE_SEPARATOR = "http://xmlpull.org/v1/doc/properties.html#serializer-line-separator"; - String PROPERTY_DEFAULT_ENCODING = "DEFAULT_ENCODING"; -} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResValuesXmlSerializable.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResValuesXmlSerializable.java index 0ee8c522..9c08626c 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResValuesXmlSerializable.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResValuesXmlSerializable.java @@ -23,6 +23,6 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; public interface ResValuesXmlSerializable { - void serializeToResValuesXml(XmlSerializer serializer, - ResResource res) throws IOException, AndrolibException; + void serializeToResValuesXml(XmlSerializer serializer, ResResource res) + throws IOException, AndrolibException; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlEncoders.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlEncoders.java index 74e679d3..1e64ec62 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlEncoders.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlEncoders.java @@ -25,6 +25,10 @@ import java.util.List; public final class ResXmlEncoders { + private ResXmlEncoders() { + // Private constructor for utility class + } + public static String escapeXmlChars(String str) { return StringUtils.replace(StringUtils.replace(str, "&", "&"), "<", "<"); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java deleted file mode 100644 index 0acd9328..00000000 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlPatcher.java +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Copyright (C) 2010 Ryszard Wiśniewski - * Copyright (C) 2010 Connor Tumbleson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package brut.androlib.res.xml; - -import brut.androlib.exceptions.AndrolibException; -import org.w3c.dom.*; -import org.xml.sax.SAXException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import javax.xml.xpath.*; -import java.io.*; -import java.util.logging.Logger; - -public final class ResXmlPatcher { - - /** - * Removes "debug" tag from file - * - * @param file AndroidManifest file - * @throws AndrolibException Error reading Manifest file - */ - public static void removeApplicationDebugTag(File file) throws AndrolibException { - if (file.exists()) { - try { - Document doc = loadDocument(file); - Node application = doc.getElementsByTagName("application").item(0); - - // load attr - NamedNodeMap attr = application.getAttributes(); - Node debugAttr = attr.getNamedItem("android:debuggable"); - - // remove application:debuggable - if (debugAttr != null) { - attr.removeNamedItem("android:debuggable"); - } - - saveDocument(file, doc); - - } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { - } - } - } - - /** - * Sets "debug" tag in the file to true - * - * @param file AndroidManifest file - */ - public static void setApplicationDebugTagTrue(File file) { - if (file.exists()) { - try { - Document doc = loadDocument(file); - Node application = doc.getElementsByTagName("application").item(0); - - // load attr - NamedNodeMap attr = application.getAttributes(); - Node debugAttr = attr.getNamedItem("android:debuggable"); - - if (debugAttr == null) { - debugAttr = doc.createAttribute("android:debuggable"); - attr.setNamedItem(debugAttr); - } - - // set application:debuggable to 'true - debugAttr.setNodeValue("true"); - - saveDocument(file, doc); - - } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { - } - } - } - - /** - * Sets the value of the network security config in the AndroidManifest file - * - * @param file AndroidManifest file - */ - public static void setNetworkSecurityConfig(File file) { - if (file.exists()) { - try { - Document doc = loadDocument(file); - Node application = doc.getElementsByTagName("application").item(0); - - // load attr - NamedNodeMap attr = application.getAttributes(); - Node netSecConfAttr = attr.getNamedItem("android:networkSecurityConfig"); - - if (netSecConfAttr == null) { - // there is not an already existing network security configuration - netSecConfAttr = doc.createAttribute("android:networkSecurityConfig"); - attr.setNamedItem(netSecConfAttr); - } - - // whether it already existed or it was created now set it to the proper value - netSecConfAttr.setNodeValue("@xml/network_security_config"); - - saveDocument(file, doc); - - } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { - } - } - } - - /** - * Creates a modified network security config file that is more permissive - * - * @param file network security config file - * @throws TransformerException XML file could not be edited - * @throws IOException XML file could not be located - * @throws SAXException XML file could not be read - * @throws ParserConfigurationException XML nodes could be written - */ - public static void modNetworkSecurityConfig(File file) - throws ParserConfigurationException, TransformerException, IOException, SAXException { - - DocumentBuilderFactory documentFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder documentBuilder = documentFactory.newDocumentBuilder(); - Document document = documentBuilder.newDocument(); - - Element root = document.createElement("network-security-config"); - document.appendChild(root); - Element baseConfig = document.createElement("base-config"); - root.appendChild(baseConfig); - Element trustAnchors = document.createElement("trust-anchors"); - baseConfig.appendChild(trustAnchors); - - Element certSystem = document.createElement("certificates"); - Attr attrSystem = document.createAttribute("src"); - attrSystem.setValue("system"); - certSystem.setAttributeNode(attrSystem); - trustAnchors.appendChild(certSystem); - - Element certUser = document.createElement("certificates"); - Attr attrUser = document.createAttribute("src"); - attrUser.setValue("user"); - certUser.setAttributeNode(attrUser); - trustAnchors.appendChild(certUser); - - saveDocument(file, document); - } - - /** - * Any @string reference in a provider value in AndroidManifest.xml will break on - * build, thus preventing the application from installing. This is from a bug/error - * in AOSP where public resources cannot be part of an authorities attribute within - * a provider tag. - *

- * This finds any reference and replaces it with the literal value found in the - * res/values/strings.xml file. - * - * @param file File for AndroidManifest.xml - */ - public static void fixingPublicAttrsInProviderAttributes(File file) { - boolean saved = false; - if (file.exists()) { - try { - Document doc = loadDocument(file); - XPath xPath = XPathFactory.newInstance().newXPath(); - XPathExpression expression = xPath.compile("/manifest/application/provider"); - - Object result = expression.evaluate(doc, XPathConstants.NODESET); - NodeList nodes = (NodeList) result; - - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attrs = node.getAttributes(); - Node provider = attrs.getNamedItem("android:authorities"); - - if (provider != null) { - saved = isSaved(file, saved, provider); - } - } - - // android:scheme - xPath = XPathFactory.newInstance().newXPath(); - expression = xPath.compile("/manifest/application/activity/intent-filter/data"); - - result = expression.evaluate(doc, XPathConstants.NODESET); - nodes = (NodeList) result; - - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attrs = node.getAttributes(); - Node provider = attrs.getNamedItem("android:scheme"); - - if (provider != null) { - saved = isSaved(file, saved, provider); - } - } - - if (saved) { - saveDocument(file, doc); - } - - } catch (SAXException | ParserConfigurationException | IOException | - XPathExpressionException | TransformerException ignored) { - } - } - } - - /** - * Checks if the replacement was properly made to a node. - * - * @param file File we are searching for value - * @param saved boolean on whether we need to save - * @param provider Node we are attempting to replace - * @return boolean - */ - private static boolean isSaved(File file, boolean saved, Node provider) { - String reference = provider.getNodeValue(); - String replacement = pullValueFromStrings(file.getParentFile(), reference); - - if (replacement != null) { - provider.setNodeValue(replacement); - saved = true; - } - return saved; - } - - /** - * Finds key in strings.xml file and returns text value - * - * @param directory Root directory of apk - * @param key String reference (ie @string/foo) - * @return String|null - */ - public static String pullValueFromStrings(File directory, String key) { - if (key == null || ! key.contains("@")) { - return null; - } - - File file = new File(directory, "/res/values/strings.xml"); - key = key.replace("@string/", ""); - - if (file.exists()) { - try { - Document doc = loadDocument(file); - XPath xPath = XPathFactory.newInstance().newXPath(); - XPathExpression expression = xPath.compile("/resources/string[@name=" + '"' + key + "\"]/text()"); - - Object result = expression.evaluate(doc, XPathConstants.STRING); - - if (result != null) { - return (String) result; - } - - } catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) { - } - } - - return null; - } - - /** - * Finds key in integers.xml file and returns text value - * - * @param directory Root directory of apk - * @param key Integer reference (ie @integer/foo) - * @return String|null - */ - public static String pullValueFromIntegers(File directory, String key) { - if (key == null || ! key.contains("@")) { - return null; - } - - File file = new File(directory, "/res/values/integers.xml"); - key = key.replace("@integer/", ""); - - if (file.exists()) { - try { - Document doc = loadDocument(file); - XPath xPath = XPathFactory.newInstance().newXPath(); - XPathExpression expression = xPath.compile("/resources/integer[@name=" + '"' + key + "\"]/text()"); - - Object result = expression.evaluate(doc, XPathConstants.STRING); - - if (result != null) { - return (String) result; - } - - } catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) { - } - } - - return null; - } - - /** - * Removes attributes like "versionCode" and "versionName" from file. - * - * @param file File representing AndroidManifest.xml - */ - public static void removeManifestVersions(File file) { - if (file.exists()) { - try { - Document doc = loadDocument(file); - Node manifest = doc.getFirstChild(); - NamedNodeMap attr = manifest.getAttributes(); - Node vCode = attr.getNamedItem("android:versionCode"); - Node vName = attr.getNamedItem("android:versionName"); - - if (vCode != null) { - attr.removeNamedItem("android:versionCode"); - } - if (vName != null) { - attr.removeNamedItem("android:versionName"); - } - saveDocument(file, doc); - - } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { - } - } - } - - /** - * Replaces package value with passed packageOriginal string - * - * @param file File for AndroidManifest.xml - * @param packageOriginal Package name to replace - */ - public static void renameManifestPackage(File file, String packageOriginal) { - try { - Document doc = loadDocument(file); - - // Get the manifest line - Node manifest = doc.getFirstChild(); - - // update package attribute - NamedNodeMap attr = manifest.getAttributes(); - Node nodeAttr = attr.getNamedItem("package"); - nodeAttr.setNodeValue(packageOriginal); - saveDocument(file, doc); - - } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { - } - } - - /** - * - * @param file File to load into Document - * @return Document - * @throws IOException - * @throws SAXException - * @throws ParserConfigurationException - */ - public static Document loadDocument(File file) - throws IOException, SAXException, ParserConfigurationException { - - DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); - docFactory.setFeature(FEATURE_DISABLE_DOCTYPE_DECL, true); - docFactory.setFeature(FEATURE_LOAD_DTD, false); - - try { - docFactory.setAttribute(ACCESS_EXTERNAL_DTD, " "); - docFactory.setAttribute(ACCESS_EXTERNAL_SCHEMA, " "); - } catch (IllegalArgumentException ex) { - LOGGER.warning("JAXP 1.5 Support is required to validate XML"); - } - - DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); - // Not using the parse(File) method on purpose, so that we can control when - // to close it. Somehow parse(File) does not seem to close the file in all cases. - try (FileInputStream inputStream = new FileInputStream(file)) { - return docBuilder.parse(inputStream); - } - } - - /** - * - * @param file File to save Document to (ie AndroidManifest.xml) - * @param doc Document being saved - * @throws IOException - * @throws SAXException - * @throws ParserConfigurationException - * @throws TransformerException - */ - private static void saveDocument(File file, Document doc) - throws IOException, SAXException, ParserConfigurationException, TransformerException { - - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = transformerFactory.newTransformer(); - DOMSource source = new DOMSource(doc); - StreamResult result = new StreamResult(file); - transformer.transform(source, result); - } - - private static final String ACCESS_EXTERNAL_DTD = "http://javax.xml.XMLConstants/property/accessExternalDTD"; - private static final String ACCESS_EXTERNAL_SCHEMA = "http://javax.xml.XMLConstants/property/accessExternalSchema"; - private static final String FEATURE_LOAD_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; - private static final String FEATURE_DISABLE_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl"; - - private static final Logger LOGGER = Logger.getLogger(ResXmlPatcher.class.getName()); -} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlUtils.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlUtils.java new file mode 100644 index 00000000..d9ee9a38 --- /dev/null +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlUtils.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package brut.androlib.res.xml; + +import brut.androlib.exceptions.AndrolibException; +import org.w3c.dom.*; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.*; +import java.util.logging.Logger; + +public final class ResXmlUtils { + private static final Logger LOGGER = Logger.getLogger(ResXmlUtils.class.getName()); + + private static final String FEATURE_LOAD_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; + private static final String FEATURE_DISABLE_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl"; + + private ResXmlUtils() { + // Private constructor for utility class + } + + /** + * Removes "debug" tag from file + * + * @param file AndroidManifest file + * @throws AndrolibException Error reading Manifest file + */ + public static void removeApplicationDebugTag(File file) throws AndrolibException { + try { + Document doc = loadDocument(file); + Node application = doc.getElementsByTagName("application").item(0); + + // load attr + NamedNodeMap attr = application.getAttributes(); + Node debugAttr = attr.getNamedItem("android:debuggable"); + + // remove application:debuggable + if (debugAttr != null) { + attr.removeNamedItem("android:debuggable"); + } + + saveDocument(file, doc); + } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { + } + } + + /** + * Sets "debug" tag in the file to true + * + * @param file AndroidManifest file + */ + public static void setApplicationDebugTagTrue(File file) { + try { + Document doc = loadDocument(file); + Node application = doc.getElementsByTagName("application").item(0); + + // load attr + NamedNodeMap attr = application.getAttributes(); + Node debugAttr = attr.getNamedItem("android:debuggable"); + + if (debugAttr == null) { + debugAttr = doc.createAttribute("android:debuggable"); + attr.setNamedItem(debugAttr); + } + + // set application:debuggable to 'true + debugAttr.setNodeValue("true"); + + saveDocument(file, doc); + } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { + } + } + + /** + * Sets the value of the network security config in the AndroidManifest file + * + * @param file AndroidManifest file + */ + public static void setNetworkSecurityConfig(File file) { + try { + Document doc = loadDocument(file); + Node application = doc.getElementsByTagName("application").item(0); + + // load attr + NamedNodeMap attr = application.getAttributes(); + Node netSecConfAttr = attr.getNamedItem("android:networkSecurityConfig"); + + if (netSecConfAttr == null) { + // there is not an already existing network security configuration + netSecConfAttr = doc.createAttribute("android:networkSecurityConfig"); + attr.setNamedItem(netSecConfAttr); + } + + // whether it already existed or it was created now set it to the proper value + netSecConfAttr.setNodeValue("@xml/network_security_config"); + + saveDocument(file, doc); + } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { + } + } + + /** + * Creates a modified network security config file that is more permissive + * + * @param file network security config file + * @throws TransformerException XML file could not be edited + * @throws IOException XML file could not be located + * @throws SAXException XML file could not be read + * @throws ParserConfigurationException XML nodes could be written + */ + public static void modNetworkSecurityConfig(File file) + throws ParserConfigurationException, TransformerException, IOException, SAXException { + DocumentBuilderFactory documentFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = documentFactory.newDocumentBuilder(); + Document document; + if (file.exists()) { + document = documentBuilder.parse(file); + document.getDocumentElement().normalize(); + } else { + document = documentBuilder.newDocument(); + } + + Element root = (Element) document.getElementsByTagName("network-security-config").item(0); + if (root == null) { + root = document.createElement("network-security-config"); + document.appendChild(root); + } + + Element baseConfig = (Element) document.getElementsByTagName("base-config").item(0); + if (baseConfig == null) { + baseConfig = document.createElement("base-config"); + root.appendChild(baseConfig); + } + + Element trustAnchors = (Element) document.getElementsByTagName("trust-anchors").item(0); + if (trustAnchors == null) { + trustAnchors = document.createElement("trust-anchors"); + baseConfig.appendChild(trustAnchors); + } + + NodeList certificates = document.getElementsByTagName("certificates"); + boolean hasSystemCert = false; + boolean hasUserCert = false; + for (int i = 0; i < certificates.getLength(); i++) { + Element cert = (Element) certificates.item(i); + String src = cert.getAttribute("src"); + if ("system".equals(src)) { + hasSystemCert = true; + } else if ("user".equals(src)) { + hasUserCert = true; + } + } + + if (!hasSystemCert) { + Element certSystem = document.createElement("certificates"); + certSystem.setAttribute("src", "system"); + trustAnchors.appendChild(certSystem); + } + + if (!hasUserCert) { + Element certUser = document.createElement("certificates"); + certUser.setAttribute("src", "user"); + trustAnchors.appendChild(certUser); + } + + saveDocument(file, document); + } + + /** + * Any @string reference in a provider value in AndroidManifest.xml will break on + * build, thus preventing the application from installing. This is from a bug/error + * in AOSP where public resources cannot be part of an authorities attribute within + * a provider tag. + *

+ * This finds any reference and replaces it with the literal value found in the + * res/values/strings.xml file. + * + * @param file File for AndroidManifest.xml + */ + public static void fixingPublicAttrsInProviderAttributes(File file) { + try { + Document doc = loadDocument(file); + XPath xPath = XPathFactory.newInstance().newXPath(); + XPathExpression expression = xPath.compile("/manifest/application/provider"); + + Object result = expression.evaluate(doc, XPathConstants.NODESET); + NodeList nodes = (NodeList) result; + + boolean saved = false; + + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + NamedNodeMap attrs = node.getAttributes(); + Node provider = attrs.getNamedItem("android:authorities"); + + if (provider != null) { + saved = isSaved(file, saved, provider); + } + } + + // android:scheme + xPath = XPathFactory.newInstance().newXPath(); + expression = xPath.compile("/manifest/application/activity/intent-filter/data"); + + result = expression.evaluate(doc, XPathConstants.NODESET); + nodes = (NodeList) result; + + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + NamedNodeMap attrs = node.getAttributes(); + Node provider = attrs.getNamedItem("android:scheme"); + + if (provider != null) { + saved = isSaved(file, saved, provider); + } + } + + if (saved) { + saveDocument(file, doc); + } + } catch (SAXException | ParserConfigurationException | IOException + | XPathExpressionException | TransformerException ignored) { + } + } + + /** + * Checks if the replacement was properly made to a node. + * + * @param file File we are searching for value + * @param saved boolean on whether we need to save + * @param provider Node we are attempting to replace + * @return boolean + */ + private static boolean isSaved(File file, boolean saved, Node provider) { + String reference = provider.getNodeValue(); + String replacement = pullValueFromStrings(file.getParentFile(), reference); + + if (replacement != null) { + provider.setNodeValue(replacement); + saved = true; + } + return saved; + } + + /** + * Finds key in strings.xml file and returns text value + * + * @param apkDir Root directory of apk + * @param key String reference (ie @string/foo) + * @return String|null + */ + public static String pullValueFromStrings(File apkDir, String key) { + if (key == null || ! key.contains("@")) { + return null; + } + + File file = new File(apkDir, "/res/values/strings.xml"); + key = key.replace("@string/", ""); + + if (!file.exists()) { + return null; + } + try { + Document doc = loadDocument(file); + XPath xPath = XPathFactory.newInstance().newXPath(); + XPathExpression expression = xPath.compile("/resources/string[@name=\"" + key + "\"]/text()"); + + Object result = expression.evaluate(doc, XPathConstants.STRING); + return result != null ? (String) result : null; + } catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) { + return null; + } + } + + /** + * Finds key in integers.xml file and returns text value + * + * @param apkDir Root directory of apk + * @param key Integer reference (ie @integer/foo) + * @return String|null + */ + public static String pullValueFromIntegers(File apkDir, String key) { + if (key == null || ! key.contains("@")) { + return null; + } + + File file = new File(apkDir, "/res/values/integers.xml"); + key = key.replace("@integer/", ""); + + if (!file.exists()) { + return null; + } + try { + Document doc = loadDocument(file); + XPath xPath = XPathFactory.newInstance().newXPath(); + XPathExpression expression = xPath.compile("/resources/integer[@name=\"" + key + "\"]/text()"); + + Object result = expression.evaluate(doc, XPathConstants.STRING); + return result != null ? (String) result : null; + + } catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) { + return null; + } + } + + /** + * Removes attributes like "versionCode" and "versionName" from file. + * + * @param file File representing AndroidManifest.xml + */ + public static void removeManifestVersions(File file) { + try { + Document doc = loadDocument(file); + Node manifest = doc.getFirstChild(); + NamedNodeMap attr = manifest.getAttributes(); + Node vCode = attr.getNamedItem("android:versionCode"); + Node vName = attr.getNamedItem("android:versionName"); + + if (vCode != null) { + attr.removeNamedItem("android:versionCode"); + } + if (vName != null) { + attr.removeNamedItem("android:versionName"); + } + saveDocument(file, doc); + + } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { + } + } + + /** + * Replaces package value with passed packageOriginal string + * + * @param file File for AndroidManifest.xml + * @param packageOriginal Package name to replace + */ + public static void renameManifestPackage(File file, String packageOriginal) { + try { + Document doc = loadDocument(file); + + // Get the manifest line + Node manifest = doc.getFirstChild(); + + // update package attribute + NamedNodeMap attr = manifest.getAttributes(); + Node nodeAttr = attr.getNamedItem("package"); + nodeAttr.setNodeValue(packageOriginal); + saveDocument(file, doc); + } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) { + } + } + + /** + * Finds all feature flags set on permissions in AndroidManifest.xml. + * + * @param file File for AndroidManifest.xml + */ + public static List pullManifestFeatureFlags(File file) { + try { + Document doc = loadDocument(file); + XPath xPath = XPathFactory.newInstance().newXPath(); + XPathExpression expression = xPath.compile("/manifest/permission"); + + Object result = expression.evaluate(doc, XPathConstants.NODESET); + NodeList nodes = (NodeList) result; + + List featureFlags = new ArrayList<>(); + + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + NamedNodeMap attrs = node.getAttributes(); + Node featureFlagAttr = attrs.getNamedItem("android:featureFlag"); + + if (featureFlagAttr != null) { + featureFlags.add(featureFlagAttr.getNodeValue()); + } + } + + return featureFlags; + } catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) { + return null; + } + } + + /** + * + * @param file File to load into Document + * @return Document + * @throws IOException + * @throws SAXException + * @throws ParserConfigurationException + */ + public static Document loadDocument(File file) + throws IOException, SAXException, ParserConfigurationException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(FEATURE_DISABLE_DOCTYPE_DECL, true); + factory.setFeature(FEATURE_LOAD_DTD, false); + + try { + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, " "); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, " "); + } catch (IllegalArgumentException ex) { + LOGGER.warning("JAXP 1.5 Support is required to validate XML"); + } + + DocumentBuilder builder = factory.newDocumentBuilder(); + // Not using the parse(File) method on purpose, so that we can control when + // to close it. Somehow parse(File) does not seem to close the file in all cases. + try (InputStream in = Files.newInputStream(file.toPath())) { + return builder.parse(in); + } + } + + /** + * + * @param file File to save Document to (ie AndroidManifest.xml) + * @param doc Document being saved + * @throws IOException + * @throws SAXException + * @throws ParserConfigurationException + * @throws TransformerException + */ + private static void saveDocument(File file, Document doc) + throws IOException, SAXException, ParserConfigurationException, TransformerException { + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer = factory.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + + byte[] xmlDecl = "".getBytes(StandardCharsets.US_ASCII); + byte[] newLine = System.getProperty("line.separator").getBytes(StandardCharsets.US_ASCII); + + try (OutputStream out = Files.newOutputStream(file.toPath())) { + out.write(xmlDecl); + out.write(newLine); + transformer.transform(new DOMSource(doc), new StreamResult(out)); + out.write(newLine); + } + } +} diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/src/SmaliBuilder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/src/SmaliBuilder.java index 6880c155..99c608fc 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/src/SmaliBuilder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/src/SmaliBuilder.java @@ -20,7 +20,6 @@ import brut.androlib.exceptions.AndrolibException; import brut.androlib.mod.SmaliMod; import brut.directory.DirectoryException; import brut.directory.ExtFile; -import org.antlr.runtime.RecognitionException; import com.android.tools.smali.dexlib2.Opcodes; import com.android.tools.smali.dexlib2.writer.builder.DexBuilder; import com.android.tools.smali.dexlib2.writer.io.FileDataStore; @@ -30,57 +29,54 @@ import java.nio.file.Files; import java.util.logging.Logger; public class SmaliBuilder { + private static final Logger LOGGER = Logger.getLogger(SmaliBuilder.class.getName()); - public static void build(ExtFile smaliDir, File dexFile, int apiLevel) throws AndrolibException { - new SmaliBuilder(smaliDir, dexFile, apiLevel).build(); - } + private final ExtFile mSmaliDir; + private final int mApiLevel; - private SmaliBuilder(ExtFile smaliDir, File dexFile, int apiLevel) { - mSmaliDir = smaliDir; - mDexFile = dexFile; + public SmaliBuilder(File smaliDir, int apiLevel) { + mSmaliDir = new ExtFile(smaliDir); mApiLevel = apiLevel; } - private void build() throws AndrolibException { + public void build(File dexFile) throws AndrolibException { try { - DexBuilder dexBuilder; - if (mApiLevel > 0) { - dexBuilder = new DexBuilder(Opcodes.forApi(mApiLevel)); - } else { - dexBuilder = new DexBuilder(Opcodes.getDefault()); - } + DexBuilder dexBuilder = mApiLevel > 0 + ? new DexBuilder(Opcodes.forApi(mApiLevel)) + : new DexBuilder(Opcodes.getDefault()); for (String fileName : mSmaliDir.getDirectory().getFiles(true)) { buildFile(fileName, dexBuilder); } - dexBuilder.writeTo(new FileDataStore( new File(mDexFile.getAbsolutePath()))); - } catch (IOException | DirectoryException ex) { - throw new AndrolibException(ex); + + dexBuilder.writeTo(new FileDataStore(dexFile)); + } catch (DirectoryException | IOException | RuntimeException ex) { + throw new AndrolibException("Could not smali folder: " + mSmaliDir.getName(), ex); } } - private void buildFile(String fileName, DexBuilder dexBuilder) - throws AndrolibException, IOException { - File inFile = new File(mSmaliDir, fileName); - InputStream inStream = Files.newInputStream(inFile.toPath()); + private void buildFile(String fileName, DexBuilder dexBuilder) throws AndrolibException { + if (!fileName.endsWith(".smali")) { + LOGGER.warning("Unknown file type, ignoring: " + fileName); + return; + } - if (fileName.endsWith(".smali")) { - try { - if (!SmaliMod.assembleSmaliFile(inFile, dexBuilder, mApiLevel, false, false)) { - throw new AndrolibException("Could not smali file: " + fileName); - } - } catch (IOException | RecognitionException ex) { - throw new AndrolibException(ex); + boolean success; + Exception cause; + try { + File inFile = new File(mSmaliDir, fileName); + success = SmaliMod.assembleSmaliFile(inFile, dexBuilder, mApiLevel, false, false); + cause = null; + } catch (Exception ex) { + success = false; + cause = ex; + } + if (!success) { + AndrolibException ex = new AndrolibException("Could not smali file: " + fileName); + if (cause != null) { + ex.initCause(cause); } - } else { - LOGGER.warning("Unknown file type, ignoring: " + inFile); + throw ex; } - inStream.close(); } - - private final ExtFile mSmaliDir; - private final File mDexFile; - private final int mApiLevel; - - private final static Logger LOGGER = Logger.getLogger(SmaliBuilder.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java index 688025b6..b2da1d8b 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java @@ -30,21 +30,19 @@ import com.android.tools.smali.dexlib2.iface.MultiDexContainer; import java.io.*; public class SmaliDecoder { + private final File mApkFile; + private final String mDexName; + private final boolean mBakDeb; + private final int mApiLevel; - public static DexFile decode(File apkFile, File outDir, String dexName, boolean bakDeb, int apiLevel) - throws AndrolibException { - return new SmaliDecoder(apkFile, outDir, dexName, bakDeb, apiLevel).decode(); - } - - private SmaliDecoder(File apkFile, File outDir, String dexName, boolean bakDeb, int apiLevel) { + public SmaliDecoder(File apkFile, String dexName, boolean bakDeb, int apiLevel) { mApkFile = apkFile; - mOutDir = outDir; - mDexFile = dexName; + mDexName = dexName; mBakDeb = bakDeb; mApiLevel = apiLevel; } - private DexFile decode() throws AndrolibException { + public DexFile decode(File outDir) throws AndrolibException { try { final BaksmaliOptions options = new BaksmaliOptions(); @@ -76,7 +74,7 @@ public class SmaliDecoder { if (container.getDexEntryNames().size() == 1) { dexEntry = container.getEntry(container.getDexEntryNames().get(0)); } else { - dexEntry = container.getEntry(mDexFile); + dexEntry = container.getEntry(mDexName); } // Double-check the passed param exists @@ -93,20 +91,14 @@ public class SmaliDecoder { if (dexFile instanceof DexBackedOdexFile) { options.inlineResolver = - InlineMethodResolver.createInlineMethodResolver(((DexBackedOdexFile)dexFile).getOdexVersion()); + InlineMethodResolver.createInlineMethodResolver(((DexBackedOdexFile) dexFile).getOdexVersion()); } - Baksmali.disassembleDexFile(dexFile, mOutDir, jobs, options); + Baksmali.disassembleDexFile(dexFile, outDir, jobs, options); return dexFile; } catch (IOException ex) { - throw new AndrolibException(ex); + throw new AndrolibException("Could not baksmali file: " + mDexName, ex); } } - - private final File mApkFile; - private final File mOutDir; - private final String mDexFile; - private final boolean mBakDeb; - private final int mApiLevel; } diff --git a/brut.apktool/apktool-lib/src/main/java/org/xmlpull/renamed/MXSerializer.java b/brut.apktool/apktool-lib/src/main/java/org/xmlpull/renamed/MXSerializer.java deleted file mode 100644 index 454c47b9..00000000 --- a/brut.apktool/apktool-lib/src/main/java/org/xmlpull/renamed/MXSerializer.java +++ /dev/null @@ -1,1062 +0,0 @@ -/* - * Copyright (C) 2010 Ryszard Wiśniewski - * Copyright (C) 2010 Connor Tumbleson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.xmlpull.renamed; - -import org.xmlpull.v1.XmlSerializer; - -import java.io.*; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -/** - * Implementation of XmlSerializer interface from XmlPull V1 API. This - * implementation is optimized for performance and low memory footprint. - * - *

- * Implemented features: - *

    - *
  • FEATURE_NAMES_INTERNED - when enabled all returned names (namespaces, - * prefixes) will be interned and it is required that all names passed as - * arguments MUST be interned - *
  • FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE - *
- *

- * Implemented properties: - *

    - *
  • PROPERTY_SERIALIZER_INDENTATION - *
  • PROPERTY_SERIALIZER_LINE_SEPARATOR - *
- * - */ -public class MXSerializer implements XmlSerializer { - protected final static String XML_URI = "http://www.w3.org/XML/1998/namespace"; - protected final static String XMLNS_URI = "http://www.w3.org/2000/xmlns/"; - private static final boolean TRACE_SIZING = false; - private static final boolean TRACE_ESCAPING = false; - - protected final String FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE = "http://xmlpull.org/v1/doc/features.html#serializer-attvalue-use-apostrophe"; - protected final String FEATURE_NAMES_INTERNED = "http://xmlpull.org/v1/doc/features.html#names-interned"; - protected final String PROPERTY_SERIALIZER_INDENTATION = "http://xmlpull.org/v1/doc/properties.html#serializer-indentation"; - protected final String PROPERTY_SERIALIZER_LINE_SEPARATOR = "http://xmlpull.org/v1/doc/properties.html#serializer-line-separator"; - protected final static String PROPERTY_LOCATION = "http://xmlpull.org/v1/doc/properties.html#location"; - - // properties/features - protected boolean namesInterned; - protected boolean attributeUseApostrophe; - protected String indentationString = null; // " "; - protected String lineSeparator = "\n"; - - protected String location; - protected Writer out; - - protected int autoDeclaredPrefixes; - - protected int depth = 0; - - // element stack - protected String[] elNamespace = new String[2]; - protected String[] elName = new String[elNamespace.length]; - protected String[] elPrefix = new String[elNamespace.length]; - protected int[] elNamespaceCount = new int[elNamespace.length]; - - // namespace stack - protected int namespaceEnd = 0; - protected String[] namespacePrefix = new String[8]; - protected String[] namespaceUri = new String[namespacePrefix.length]; - - protected boolean finished; - protected boolean pastRoot; - protected boolean setPrefixCalled; - protected boolean startTagIncomplete; - - protected boolean doIndent; - protected boolean seenTag; - - protected boolean seenBracket; - protected boolean seenBracketBracket; - - // buffer output if needed to write escaped String see text(String) - private static final int BUF_LEN = Runtime.getRuntime().freeMemory() > 1000000L ? 8 * 1024 : 256; - protected char[] buf = new char[BUF_LEN]; - - protected static final String[] precomputedPrefixes; - - static { - precomputedPrefixes = new String[32]; // arbitrary number ... - for (int i = 0; i < precomputedPrefixes.length; i++) { - precomputedPrefixes[i] = ("n" + i).intern(); - } - } - - private final boolean checkNamesInterned = false; - - private void checkInterning(String name) { - if (namesInterned && !Objects.equals(name, name.intern())) { - throw new IllegalArgumentException("all names passed as arguments must be interned" - + "when NAMES INTERNED feature is enabled"); - } - } - - protected void reset() { - location = null; - out = null; - autoDeclaredPrefixes = 0; - depth = 0; - - // nullify references on all levels to allow it to be GCed - for (int i = 0; i < elNamespaceCount.length; i++) { - elName[i] = null; - elPrefix[i] = null; - elNamespace[i] = null; - elNamespaceCount[i] = 2; - } - - namespaceEnd = 0; - - // TODO: how to prevent from reporting this namespace? - // this is special namespace declared for consistency with XML infoset - namespacePrefix[namespaceEnd] = "xmlns"; - namespaceUri[namespaceEnd] = XMLNS_URI; - ++namespaceEnd; - - namespacePrefix[namespaceEnd] = "xml"; - namespaceUri[namespaceEnd] = XML_URI; - ++namespaceEnd; - - finished = false; - pastRoot = false; - setPrefixCalled = false; - startTagIncomplete = false; - seenTag = false; - - seenBracket = false; - seenBracketBracket = false; - } - - protected void ensureElementsCapacity() { - final int elStackSize = elName.length; - final int newSize = (depth >= 7 ? 2 * depth : 8) + 2; - - if (TRACE_SIZING) { - System.err.println(getClass().getName() + " elStackSize " - + elStackSize + " ==> " + newSize); - } - final boolean needsCopying = elStackSize > 0; - String[] arr; - // reuse arr local variable slot - arr = new String[newSize]; - if (needsCopying) - System.arraycopy(elName, 0, arr, 0, elStackSize); - elName = arr; - - arr = new String[newSize]; - if (needsCopying) - System.arraycopy(elPrefix, 0, arr, 0, elStackSize); - elPrefix = arr; - - arr = new String[newSize]; - if (needsCopying) - System.arraycopy(elNamespace, 0, arr, 0, elStackSize); - elNamespace = arr; - - final int[] iarr = new int[newSize]; - if (needsCopying) { - System.arraycopy(elNamespaceCount, 0, iarr, 0, elStackSize); - } else { - // special initialization - iarr[0] = 0; - } - elNamespaceCount = iarr; - } - - protected void ensureNamespacesCapacity() { // int size) { - final int newSize = namespaceEnd > 7 ? 2 * namespaceEnd : 8; - if (TRACE_SIZING) { - System.err.println(getClass().getName() + " namespaceSize " + namespacePrefix.length + " ==> " + newSize); - } - final String[] newNamespacePrefix = new String[newSize]; - final String[] newNamespaceUri = new String[newSize]; - if (namespacePrefix != null) { - System.arraycopy(namespacePrefix, 0, newNamespacePrefix, 0, namespaceEnd); - System.arraycopy(namespaceUri, 0, newNamespaceUri, 0, namespaceEnd); - } - namespacePrefix = newNamespacePrefix; - namespaceUri = newNamespaceUri; - } - - @Override - public void setFeature(String name, boolean state) - throws IllegalArgumentException, IllegalStateException { - if (name == null) { - throw new IllegalArgumentException("feature name can not be null"); - } - if (FEATURE_NAMES_INTERNED.equals(name)) { - namesInterned = state; - } else if (FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals(name)) { - attributeUseApostrophe = state; - } else { - throw new IllegalStateException("unsupported feature " + name); - } - } - - @Override - public boolean getFeature(String name) throws IllegalArgumentException { - if (name == null) { - throw new IllegalArgumentException("feature name can not be null"); - } - if (FEATURE_NAMES_INTERNED.equals(name)) { - return namesInterned; - } else if (FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals(name)) { - return attributeUseApostrophe; - } else { - return false; - } - } - - // precomputed variables to simplify writing indentation - protected int offsetNewLine; - protected int indentationJump; - protected char[] indentationBuf; - protected int maxIndentLevel; - protected boolean writeLineSepartor; // should end-of-line be written - protected boolean writeIndentation; // is indentation used? - - /** - * For maximum efficiency when writing indents the required output is - * pre-computed This is internal function that recomputes buffer after user - * requested chnages. - */ - protected void rebuildIndentationBuf() { - if (!doIndent) - return; - final int maxIndent = 65; // hardcoded maximum indentation size in characters - int bufSize = 0; - offsetNewLine = 0; - if (writeLineSepartor) { - offsetNewLine = lineSeparator.length(); - bufSize += offsetNewLine; - } - maxIndentLevel = 0; - if (writeIndentation) { - indentationJump = indentationString.length(); - maxIndentLevel = maxIndent / indentationJump; - bufSize += maxIndentLevel * indentationJump; - } - if (indentationBuf == null || indentationBuf.length < bufSize) { - indentationBuf = new char[bufSize + 8]; - } - int bufPos = 0; - if (writeLineSepartor) { - for (int i = 0; i < lineSeparator.length(); i++) { - indentationBuf[bufPos++] = lineSeparator.charAt(i); - } - } - if (writeIndentation) { - for (int i = 0; i < maxIndentLevel; i++) { - for (int j = 0; j < indentationString.length(); j++) { - indentationBuf[bufPos++] = indentationString.charAt(j); - } - } - } - } - - protected void writeIndent() throws IOException { - final int start = writeLineSepartor ? 0 : offsetNewLine; - final int level = Math.min(depth, maxIndentLevel); - - out.write(indentationBuf, start, ((level - 1) * indentationJump) + offsetNewLine); - } - - @Override - public void setProperty(String name, Object value) - throws IllegalArgumentException, IllegalStateException { - if (name == null) { - throw new IllegalArgumentException("property name can not be null"); - } - switch (name) { - case PROPERTY_SERIALIZER_INDENTATION: - indentationString = (String) value; - break; - case PROPERTY_SERIALIZER_LINE_SEPARATOR: - lineSeparator = (String) value; - break; - case PROPERTY_LOCATION: - location = (String) value; - break; - default: - throw new IllegalStateException("unsupported property " + name); - } - writeLineSepartor = lineSeparator != null && lineSeparator.length() > 0; - writeIndentation = indentationString != null - && indentationString.length() > 0; - // optimize - do not write when nothing to write ... - doIndent = indentationString != null - && (writeLineSepartor || writeIndentation); - // NOTE: when indentationString == null there is no indentation - // (even though writeLineSeparator may be true ...) - rebuildIndentationBuf(); - seenTag = false; // for consistency - } - - @Override - public Object getProperty(String name) throws IllegalArgumentException { - if (name == null) { - throw new IllegalArgumentException("property name can not be null"); - } - switch (name) { - case PROPERTY_SERIALIZER_INDENTATION: - return indentationString; - case PROPERTY_SERIALIZER_LINE_SEPARATOR: - return lineSeparator; - case PROPERTY_LOCATION: - return location; - default: - return null; - } - } - - private String getLocation() { - return location != null ? " @" + location : ""; - } - - // this is special method that can be accessed directly to retrieve Writer - // serializer is using - public Writer getWriter() { - return out; - } - - @Override - public void setOutput(Writer writer) { - reset(); - out = writer; - } - - @Override - public void setOutput(OutputStream os, String encoding) throws IOException { - if (os == null) - throw new IllegalArgumentException("output stream can not be null"); - reset(); - if (encoding != null) { - out = new OutputStreamWriter(os, encoding); - } else { - out = new OutputStreamWriter(os); - } - } - - @Override - public void startDocument(String encoding, Boolean standalone) - throws IOException { - if (attributeUseApostrophe) { - out.write(""); - } - - @Override - public void endDocument() throws IOException { - // close all unclosed tag; - while (depth > 0) { - endTag(elNamespace[depth], elName[depth]); - } - finished = pastRoot = startTagIncomplete = true; - out.flush(); - } - - @Override - public void setPrefix(String prefix, String namespace) throws IOException { - if (startTagIncomplete) - closeStartTag(); - - if (prefix == null) { - prefix = ""; - } - if (!namesInterned) { - prefix = prefix.intern(); // will throw NPE if prefix==null - } else if (checkNamesInterned) { - checkInterning(prefix); - } else if (prefix == null) { - throw new IllegalArgumentException("prefix must be not null" + getLocation()); - } - - if (!namesInterned) { - namespace = namespace.intern(); - } else if (checkNamesInterned) { - checkInterning(namespace); - } else if (namespace == null) { - throw new IllegalArgumentException("namespace must be not null" + getLocation()); - } - - if (namespaceEnd >= namespacePrefix.length) { - ensureNamespacesCapacity(); - } - namespacePrefix[namespaceEnd] = prefix; - namespaceUri[namespaceEnd] = namespace; - ++namespaceEnd; - setPrefixCalled = true; - } - - protected String lookupOrDeclarePrefix(String namespace) { - return getPrefix(namespace, true); - } - - @Override - public String getPrefix(String namespace, boolean generatePrefix) { - return getPrefix(namespace, generatePrefix, false); - } - - protected String getPrefix(String namespace, boolean generatePrefix, - boolean nonEmpty) { - if (!namesInterned) { - // when String is interned we can do much faster namespace stack lookups ... - namespace = namespace.intern(); - } else if (checkNamesInterned) { - checkInterning(namespace); - } - if (namespace == null) { - throw new IllegalArgumentException("namespace must be not null" + getLocation()); - } else if (namespace.length() == 0) { - throw new IllegalArgumentException("default namespace cannot have prefix" + getLocation()); - } - - // first check if namespace is already in scope - for (int i = namespaceEnd - 1; i >= 0; --i) { - if (namespace.equals(namespaceUri[i])) { - final String prefix = namespacePrefix[i]; - if (nonEmpty && prefix.length() == 0) { - continue; - } - - return prefix; - } - } - - // so not found it ... - if (!generatePrefix) { - return null; - } - return generatePrefix(namespace); - } - - private String generatePrefix(String namespace) { - ++autoDeclaredPrefixes; - // fast lookup uses table that was pre-initialized in static{} .... - final String prefix = autoDeclaredPrefixes < precomputedPrefixes.length - ? precomputedPrefixes[autoDeclaredPrefixes] - : ("n" + autoDeclaredPrefixes).intern(); - - // declare prefix - if (namespaceEnd >= namespacePrefix.length) { - ensureNamespacesCapacity(); - } - namespacePrefix[namespaceEnd] = prefix; - namespaceUri[namespaceEnd] = namespace; - ++namespaceEnd; - - return prefix; - } - - @Override - public int getDepth() { - return depth; - } - - @Override - public String getNamespace() { - return elNamespace[depth]; - } - - @Override - public String getName() { - return elName[depth]; - } - - @Override - public XmlSerializer startTag(String namespace, String name) - throws IOException { - if (startTagIncomplete) { - closeStartTag(); - } - seenBracket = seenBracketBracket = false; - ++depth; - if (doIndent && depth > 0 && seenTag) { - writeIndent(); - } - seenTag = true; - setPrefixCalled = false; - startTagIncomplete = true; - if ((depth + 1) >= elName.length) { - ensureElementsCapacity(); - } - - if (checkNamesInterned && namesInterned) - checkInterning(namespace); - - elNamespace[depth] = (namesInterned || namespace == null) ? namespace : namespace.intern(); - if (checkNamesInterned && namesInterned) - checkInterning(name); - - elName[depth] = (namesInterned || name == null) ? name : name.intern(); - if (out == null) { - throw new IllegalStateException("setOutput() must called set before serialization can start"); - } - out.write('<'); - if (namespace != null) { - if (namespace.length() > 0) { - // in future make this algo a feature on serializer - String prefix = null; - if (depth > 0 && (namespaceEnd - elNamespaceCount[depth - 1]) == 1) { - // if only one prefix was declared un-declare it if the - // prefix is already declared on parent el with the same URI - String uri = namespaceUri[namespaceEnd - 1]; - if (uri == namespace || uri.equals(namespace)) { - String elPfx = namespacePrefix[namespaceEnd - 1]; - for (int pos = elNamespaceCount[depth - 1] - 1; pos >= 2; --pos) { - String pf = namespacePrefix[pos]; - if (pf == elPfx || pf.equals(elPfx)) { - String n = namespaceUri[pos]; - if (n == uri || n.equals(uri)) { - --namespaceEnd; // un-declare namespace: this is kludge! - prefix = elPfx; - } - break; - } - } - } - } - if (prefix == null) { - prefix = lookupOrDeclarePrefix(namespace); - } - // make sure that default ("") namespace to not print ":" - if (prefix.length() > 0) { - elPrefix[depth] = prefix; - out.write(prefix); - out.write(':'); - } else { - elPrefix[depth] = ""; - } - } else { - // make sure that default namespace can be declared - for (int i = namespaceEnd - 1; i >= 0; --i) { - if (namespacePrefix[i] == "") { - final String uri = namespaceUri[i]; - if (uri == null) { - setPrefix("", ""); - } else if (uri.length() > 0) { - throw new IllegalStateException("start tag can not be written in empty default namespace " - + "as default namespace is currently bound to '" - + uri + "'" + getLocation()); - } - break; - } - } - elPrefix[depth] = ""; - } - } else { - elPrefix[depth] = ""; - } - out.write(name); - return this; - } - - @Override - public XmlSerializer attribute(String namespace, String name, String value) - throws IOException { - if (!startTagIncomplete) { - throw new IllegalArgumentException("startTag() must be called before attribute()" + getLocation()); - } - out.write(' '); - if (namespace != null && namespace.length() > 0) { - if (!namesInterned) { - namespace = namespace.intern(); - } else if (checkNamesInterned) { - checkInterning(namespace); - } - String prefix = getPrefix(namespace, false, true); - if (prefix == null) { - // needs to declare prefix to hold default namespace - // NOTE: attributes such as a='b' are in NO namespace - prefix = generatePrefix(namespace); - } - out.write(prefix); - out.write(':'); - } - out.write(name); - out.write('='); - out.write(attributeUseApostrophe ? '\'' : '"'); - writeAttributeValue(value, out); - out.write(attributeUseApostrophe ? '\'' : '"'); - return this; - } - - protected void closeStartTag() throws IOException { - if (finished) { - throw new IllegalArgumentException("trying to write past already finished output" - + getLocation()); - } - if (seenBracket) { - seenBracket = seenBracketBracket = false; - } - if (startTagIncomplete || setPrefixCalled) { - if (setPrefixCalled) { - throw new IllegalArgumentException("startTag() must be called immediately after setPrefix()" - + getLocation()); - } - if (!startTagIncomplete) { - throw new IllegalArgumentException("trying to close start tag that is not opened" - + getLocation()); - } - - // write all namespace declarations! - writeNamespaceDeclarations(); - out.write('>'); - elNamespaceCount[depth] = namespaceEnd; - startTagIncomplete = false; - } - } - - protected void writeNamespaceDeclarations() throws IOException { - Set uniqueNamespaces = new HashSet<>(); - for (int i = elNamespaceCount[depth - 1]; i < namespaceEnd; i++) { - String prefix = namespacePrefix[i]; - String uri = namespaceUri[i]; - - // Some applications as seen in #2664 have duplicated namespaces. - // AOSP doesn't care, but the parser does. So we filter them out. - if (uniqueNamespaces.contains(prefix + uri)) { - continue; - } - - if (doIndent && uri.length() > 40) { - writeIndent(); - out.write(" "); - } - if (prefix != "") { - out.write(" xmlns:"); - out.write(prefix); - out.write('='); - } else { - out.write(" xmlns="); - } - out.write(attributeUseApostrophe ? '\'' : '"'); - - // NOTE: escaping of namespace value the same way as attributes!!!! - writeAttributeValue(uri, out); - out.write(attributeUseApostrophe ? '\'' : '"'); - - uniqueNamespaces.add(prefix + uri); - } - } - - @Override - public XmlSerializer endTag(String namespace, String name) - throws IOException { - seenBracket = seenBracketBracket = false; - if (namespace != null) { - if (!namesInterned) { - namespace = namespace.intern(); - } else if (checkNamesInterned) { - checkInterning(namespace); - } - } - - if (namespace != elNamespace[depth]) { - throw new IllegalArgumentException("expected namespace " + printable(elNamespace[depth]) + " and not " - + printable(namespace) + getLocation()); - } - if (name == null) { - throw new IllegalArgumentException("end tag name can not be null" + getLocation()); - } - if (checkNamesInterned && namesInterned) { - checkInterning(name); - } - String startTagName = elName[depth]; - if ((!namesInterned && !name.equals(startTagName)) || (namesInterned && name != startTagName)) { - throw new IllegalArgumentException("expected element name " - + printable(elName[depth]) + " and not " + printable(name) + getLocation()); - } - if (startTagIncomplete) { - writeNamespaceDeclarations(); - out.write(" />"); // space is added to make it easier to work in XHTML!!! - } else { - if (doIndent && seenTag) { - writeIndent(); - } - out.write(" 0) { - out.write(startTagPrefix); - out.write(':'); - } - out.write(name); - out.write('>'); - } - --depth; - namespaceEnd = elNamespaceCount[depth]; - startTagIncomplete = false; - seenTag = true; - return this; - } - - @Override - public XmlSerializer text(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled) - closeStartTag(); - if (doIndent && seenTag) - seenTag = false; - writeElementContent(text, out); - return this; - } - - @Override - public XmlSerializer text(char[] buf, int start, int len) - throws IOException { - if (startTagIncomplete || setPrefixCalled) - closeStartTag(); - if (doIndent && seenTag) - seenTag = false; - writeElementContent(buf, start, len, out); - return this; - } - - @Override - public void cdsect(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) - closeStartTag(); - if (doIndent && seenTag) - seenTag = false; - out.write(""); - } - - @Override - public void entityRef(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) - closeStartTag(); - if (doIndent && seenTag) - seenTag = false; - out.write('&'); - out.write(text); // escape? - out.write(';'); - } - - @Override - public void processingInstruction(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) - closeStartTag(); - if (doIndent && seenTag) - seenTag = false; - out.write(""); - } - - @Override - public void comment(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) - closeStartTag(); - if (doIndent && seenTag) - seenTag = false; - out.write(""); - } - - @Override - public void docdecl(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) - closeStartTag(); - if (doIndent && seenTag) - seenTag = false; - out.write(""); - } - - @Override - public void ignorableWhitespace(String text) throws IOException { - if (startTagIncomplete || setPrefixCalled || seenBracket) - closeStartTag(); - if (doIndent && seenTag) - seenTag = false; - if (text.length() == 0) { - throw new IllegalArgumentException("empty string is not allowed for ignorable whitespace" + getLocation()); - } - out.write(text); // no escape? - } - - @Override - public void flush() throws IOException { - if (!finished && startTagIncomplete) - closeStartTag(); - out.flush(); - } - - // --- utility methods - - protected void writeAttributeValue(String value, Writer out) - throws IOException { - // .[apostrophe and <, & escaped], - final char quot = attributeUseApostrophe ? '\'' : '"'; - final String quotEntity = attributeUseApostrophe ? "'" : """; - - int pos = 0; - for (int i = 0; i < value.length(); i++) { - char ch = value.charAt(i); - if (ch == '&') { - if (i > pos) - out.write(value.substring(pos, i)); - out.write("&"); - pos = i + 1; - } - if (ch == '<') { - if (i > pos) - out.write(value.substring(pos, i)); - out.write("<"); - pos = i + 1; - } else if (ch == quot) { - if (i > pos) - out.write(value.substring(pos, i)); - out.write(quotEntity); - pos = i + 1; - } else if (ch < 32) { - // in XML 1.0 only legal character are #x9 | #xA | #xD - // and they must be escaped otherwise in attribute value they - // are normalized to spaces - if (ch == 13 || ch == 10 || ch == 9) { - if (i > pos) - out.write(value.substring(pos, i)); - out.write("&#"); - out.write(Integer.toString(ch)); - out.write(';'); - pos = i + 1; - } else { - if (TRACE_ESCAPING) - System.err.println(getClass().getName() + " DEBUG ATTR value.len=" + value.length() - + " " + printable(value)); - - throw new IllegalStateException( - "character " + printable(ch) + " (" + Integer.toString(ch) + ") is not allowed in output" - + getLocation() + " (attr value=" - + printable(value) + ")"); - } - } - } - if (pos > 0) { - out.write(value.substring(pos)); - } else { - out.write(value); // this is shortcut to the most common case - } - } - - protected void writeElementContent(String text, Writer out) - throws IOException { - - // For some reason, some non-empty, empty characters are surviving this far and getting filtered out - // So we are left with null, which causes an NPE - if (text == null) { - return; - } - - // escape '<', '&', ']]>', <32 if necessary - int pos = 0; - for (int i = 0; i < text.length(); i++) { - // TODO: check if doing char[] text.getChars() would be faster than - // getCharAt(i) ... - char ch = text.charAt(i); - if (ch == ']') { - if (seenBracket) { - seenBracketBracket = true; - } else { - seenBracket = true; - } - } else { - if (ch == '&') { - if (!(i < text.length() - 3 && text.charAt(i+1) == 'l' - && text.charAt(i+2) == 't' && text.charAt(i+3) == ';')) { - if (i > pos) - out.write(text.substring(pos, i)); - out.write("&"); - pos = i + 1; - } - } else if (ch == '<') { - if (i > pos) - out.write(text.substring(pos, i)); - out.write("<"); - pos = i + 1; - } else if (seenBracketBracket && ch == '>') { - if (i > pos) - out.write(text.substring(pos, i)); - out.write(">"); - pos = i + 1; - } else if (ch < 32) { - // in XML 1.0 only legal character are #x9 | #xA | #xD - if (ch == 9 || ch == 10 || ch == 13) { - // pass through - } else { - if (TRACE_ESCAPING) - System.err.println(getClass().getName() + " DEBUG TEXT value.len=" + text.length() - + " " + printable(text)); - throw new IllegalStateException("character " + Integer.toString(ch) - + " is not allowed in output" + getLocation() - + " (text value=" + printable(text) + ")"); - } - } - if (seenBracket) { - seenBracketBracket = seenBracket = false; - } - - } - } - if (pos > 0) { - out.write(text.substring(pos)); - } else { - out.write(text); // this is shortcut to the most common case - } - - } - - protected void writeElementContent(char[] buf, int off, int len, Writer out) - throws IOException { - // escape '<', '&', ']]>' - final int end = off + len; - int pos = off; - for (int i = off; i < end; i++) { - final char ch = buf[i]; - if (ch == ']') { - if (seenBracket) { - seenBracketBracket = true; - } else { - seenBracket = true; - } - } else { - if (ch == '&') { - if (i > pos) { - out.write(buf, pos, i - pos); - } - out.write("&"); - pos = i + 1; - } else if (ch == '<') { - if (i > pos) { - out.write(buf, pos, i - pos); - } - out.write("<"); - pos = i + 1; - - } else if (seenBracketBracket && ch == '>') { - if (i > pos) { - out.write(buf, pos, i - pos); - } - out.write(">"); - pos = i + 1; - } else if (ch < 32) { - // in XML 1.0 only legal character are #x9 | #xA | #xD - if (ch == 9 || ch == 10 || ch == 13) { - // pass through - } else { - if (TRACE_ESCAPING) - System.err.println(getClass().getName() + " DEBUG TEXT value.len=" + len + " " - + printable(new String(buf, off, len))); - throw new IllegalStateException("character " - + printable(ch) + " (" + Integer.toString(ch) - + ") is not allowed in output" + getLocation()); - } - } - if (seenBracket) { - seenBracketBracket = seenBracket = false; - } - } - } - if (end > pos) { - out.write(buf, pos, end - pos); - } - } - - protected static String printable(String s) { - if (s == null) { - return "null"; - } - StringBuffer retval = new StringBuffer(s.length() + 16); - retval.append("'"); - for (int i = 0; i < s.length(); i++) { - addPrintable(retval, s.charAt(i)); - } - retval.append("'"); - return retval.toString(); - } - - protected static String printable(char ch) { - StringBuffer retval = new StringBuffer(); - addPrintable(retval, ch); - return retval.toString(); - } - - private static void addPrintable(StringBuffer retval, char ch) { - switch (ch) { - case '\b': - retval.append("\\b"); - break; - case '\t': - retval.append("\\t"); - break; - case '\n': - retval.append("\\n"); - break; - case '\f': - retval.append("\\f"); - break; - case '\r': - retval.append("\\r"); - break; - case '\"': - retval.append("\\\""); - break; - case '\'': - retval.append("\\'"); - break; - case '\\': - retval.append("\\\\"); - break; - default: - if (ch < 0x20 || ch > 0x7e) { - final String ss = "0000" + Integer.toString(ch, 16); - retval.append("\\u").append(ss.substring(ss.length() - 4)); - } else { - retval.append(ch); - } - } - } -} diff --git a/brut.apktool/apktool-lib/src/main/resources/properties/apktool.properties b/brut.apktool/apktool-lib/src/main/resources/apktool.properties similarity index 100% rename from brut.apktool/apktool-lib/src/main/resources/properties/apktool.properties rename to brut.apktool/apktool-lib/src/main/resources/apktool.properties diff --git a/brut.apktool/apktool-lib/src/main/resources/brut/androlib/android-framework.jar b/brut.apktool/apktool-lib/src/main/resources/brut/androlib/android-framework.jar deleted file mode 100644 index 76974147..00000000 Binary files a/brut.apktool/apktool-lib/src/main/resources/brut/androlib/android-framework.jar and /dev/null differ diff --git a/brut.apktool/apktool-lib/src/main/resources/prebuilt/android-framework.jar b/brut.apktool/apktool-lib/src/main/resources/prebuilt/android-framework.jar new file mode 100644 index 00000000..731454b1 Binary files /dev/null and b/brut.apktool/apktool-lib/src/main/resources/prebuilt/android-framework.jar differ diff --git a/brut.apktool/apktool-lib/src/main/resources/prebuilt/linux/aapt2 b/brut.apktool/apktool-lib/src/main/resources/prebuilt/linux/aapt2 index 8e11eba0..2e029401 100755 Binary files a/brut.apktool/apktool-lib/src/main/resources/prebuilt/linux/aapt2 and b/brut.apktool/apktool-lib/src/main/resources/prebuilt/linux/aapt2 differ diff --git a/brut.apktool/apktool-lib/src/main/resources/prebuilt/linux/aapt2_64 b/brut.apktool/apktool-lib/src/main/resources/prebuilt/linux/aapt2_64 index f0006f4c..27ddea78 100755 Binary files a/brut.apktool/apktool-lib/src/main/resources/prebuilt/linux/aapt2_64 and b/brut.apktool/apktool-lib/src/main/resources/prebuilt/linux/aapt2_64 differ diff --git a/brut.apktool/apktool-lib/src/main/resources/prebuilt/macosx/aapt2_64 b/brut.apktool/apktool-lib/src/main/resources/prebuilt/macosx/aapt2_64 index bd8655f0..371a6eb3 100755 Binary files a/brut.apktool/apktool-lib/src/main/resources/prebuilt/macosx/aapt2_64 and b/brut.apktool/apktool-lib/src/main/resources/prebuilt/macosx/aapt2_64 differ diff --git a/brut.apktool/apktool-lib/src/main/resources/prebuilt/windows/aapt2.exe b/brut.apktool/apktool-lib/src/main/resources/prebuilt/windows/aapt2.exe index 46f118db..6e8b608c 100755 Binary files a/brut.apktool/apktool-lib/src/main/resources/prebuilt/windows/aapt2.exe and b/brut.apktool/apktool-lib/src/main/resources/prebuilt/windows/aapt2.exe differ diff --git a/brut.apktool/apktool-lib/src/main/resources/prebuilt/windows/aapt2_64.exe b/brut.apktool/apktool-lib/src/main/resources/prebuilt/windows/aapt2_64.exe index 584e971d..c7db6fee 100755 Binary files a/brut.apktool/apktool-lib/src/main/resources/prebuilt/windows/aapt2_64.exe and b/brut.apktool/apktool-lib/src/main/resources/prebuilt/windows/aapt2_64.exe differ diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BaseTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BaseTest.java index 12d3a351..049354f1 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/BaseTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/BaseTest.java @@ -23,12 +23,15 @@ import brut.directory.FileDirectory; import org.custommonkey.xmlunit.*; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.*; +import java.nio.file.Files; import java.util.Map; import java.util.Set; import java.util.logging.Logger; @@ -37,22 +40,16 @@ import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; import static org.junit.Assert.*; public class BaseTest { + protected static final Logger LOGGER = Logger.getLogger(BaseTest.class.getName()); - protected void compareUnknownFiles() throws BrutException { - ApkInfo control = ApkInfo.load(sTestOrigDir); - ApkInfo test = ApkInfo.load(sTestNewDir); - assertNotNull(control.unknownFiles); - assertNotNull(test.unknownFiles); + private static final String ACCESS_EXTERNAL_DTD = "http://javax.xml.XMLConstants/property/accessExternalDTD"; + private static final String ACCESS_EXTERNAL_SCHEMA = "http://javax.xml.XMLConstants/property/accessExternalSchema"; + private static final String FEATURE_LOAD_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; + private static final String FEATURE_DISABLE_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl"; - Map controlFiles = control.unknownFiles; - Map testFiles = test.unknownFiles; - assertEquals(controlFiles.size(), testFiles.size()); - - // Make sure that the compression methods are still the same - for (Map.Entry controlEntry : controlFiles.entrySet()) { - assertEquals(controlEntry.getValue(), testFiles.get(controlEntry.getKey())); - } - } + protected static ExtFile sTmpDir; + protected static ExtFile sTestOrigDir; + protected static ExtFile sTestNewDir; protected void compareBinaryFolder(String path, boolean res) throws BrutException, IOException { boolean exists = true; @@ -67,10 +64,10 @@ public class BaseTest { FileDirectory fileDirectory = new FileDirectory(sTestOrigDir, location); Set files = fileDirectory.getFiles(true); - for (String filename : files) { + for (String fileName : files) { - File control = new File((sTestOrigDir + location), filename); - File test = new File((sTestNewDir + location), filename); + File control = new File((sTestOrigDir + location), fileName); + File test = new File((sTestNewDir + location), fileName); if (! test.isFile() || ! control.isFile()) { exists = false; @@ -92,6 +89,10 @@ public class BaseTest { compareBinaryFolder(File.separatorChar + "assets" + File.separatorChar + path, false); } + protected void compareUnknownFiles() throws BrutException, IOException { + compareBinaryFolder(File.separatorChar + "unknown", false); + } + protected void compareValuesFiles(String path) throws BrutException { compareXmlFiles("res/" + path, new ElementNameAndAttributeQualifier("name")); } @@ -135,24 +136,24 @@ public class BaseTest { assertTrue(path + ": " + diff.getAllDifferences().toString(), diff.similar()); } - protected static Document loadDocument(File file) throws IOException, SAXException, ParserConfigurationException { - - DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); - docFactory.setFeature(FEATURE_DISABLE_DOCTYPE_DECL, true); - docFactory.setFeature(FEATURE_LOAD_DTD, false); + protected static Document loadDocument(File file) + throws IOException, SAXException, ParserConfigurationException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(FEATURE_DISABLE_DOCTYPE_DECL, true); + factory.setFeature(FEATURE_LOAD_DTD, false); try { - docFactory.setAttribute(ACCESS_EXTERNAL_DTD, " "); - docFactory.setAttribute(ACCESS_EXTERNAL_SCHEMA, " "); + factory.setAttribute(ACCESS_EXTERNAL_DTD, " "); + factory.setAttribute(ACCESS_EXTERNAL_SCHEMA, " "); } catch (IllegalArgumentException ex) { LOGGER.warning("JAXP 1.5 Support is required to validate XML"); } - DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + DocumentBuilder builder = factory.newDocumentBuilder(); // Not using the parse(File) method on purpose, so that we can control when // to close it. Somehow parse(File) does not seem to close the file in all cases. - try (FileInputStream inputStream = new FileInputStream(file)) { - return docBuilder.parse(inputStream); + try (InputStream in = Files.newInputStream(file.toPath())) { + return builder.parse(in); } } @@ -167,14 +168,18 @@ public class BaseTest { return count; } - protected static ExtFile sTmpDir; - protected static ExtFile sTestOrigDir; - protected static ExtFile sTestNewDir; - - protected final static Logger LOGGER = Logger.getLogger(BaseTest.class.getName()); - - private static final String ACCESS_EXTERNAL_DTD = "http://javax.xml.XMLConstants/property/accessExternalDTD"; - private static final String ACCESS_EXTERNAL_SCHEMA = "http://javax.xml.XMLConstants/property/accessExternalSchema"; - private static final String FEATURE_LOAD_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; - private static final String FEATURE_DISABLE_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl"; + protected static boolean resourceNameContains(Element element, String name) { + if (element.hasAttribute("name") && element.getAttribute("name").contains(name)) { + return true; + } + NodeList children = element.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && resourceNameContains((Element) child, name)) { + return true; + } + } + return false; + } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java index 855326ff..cf94df09 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java @@ -18,19 +18,23 @@ package brut.androlib; import brut.androlib.exceptions.AndrolibException; import brut.androlib.res.Framework; -import brut.androlib.res.xml.ResXmlPatcher; +import brut.androlib.res.xml.ResXmlUtils; import brut.common.BrutException; -import brut.directory.DirUtil; +import brut.directory.DirUtils; import brut.directory.Directory; import brut.directory.FileDirectory; import brut.util.OS; import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.xml.sax.SAXException; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; import java.io.*; import java.net.URL; import java.net.URLDecoder; @@ -38,73 +42,58 @@ import java.nio.file.Files; import java.util.HashMap; import java.util.Map; -public abstract class TestUtils { +public final class TestUtils { - public static Map parseStringsXml(File file) - throws BrutException { + private TestUtils() { + // Private constructor for utility class + } + + public static Map parseStringsXml(File file) throws BrutException { try { - XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); - xpp.setInput(new FileReader(file)); + Document doc = getDocumentFromFile(file); + XPath xPath = XPathFactory.newInstance().newXPath(); + String expression = "/resources/string[@name]"; + NodeList nodes = (NodeList) xPath.evaluate(expression, doc, XPathConstants.NODESET); - int eventType; - String key = null; Map map = new HashMap<>(); - while ((eventType = xpp.next()) != XmlPullParser.END_DOCUMENT) { - switch (eventType) { - case XmlPullParser.START_TAG: - if ("string".equals(xpp.getName())) { - int attrCount = xpp.getAttributeCount(); - for (int i = 0; i < attrCount; i++) { - if ("name".equals(xpp.getAttributeName(i))) { - key = xpp.getAttributeValue(i); - break; - } - } - } - break; - case XmlPullParser.END_TAG: - if ("string".equals(xpp.getName())) { - key = null; - } - break; - case XmlPullParser.TEXT: - if (key != null) { - map.put(key, xpp.getText()); - } - break; - } + + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + NamedNodeMap attrs = node.getAttributes(); + Node nameAttr = attrs.getNamedItem("name"); + map.put(nameAttr.getNodeValue(), node.getTextContent()); } return map; - } catch (IOException | XmlPullParserException ex) { + } catch (XPathExpressionException ex) { throw new BrutException(ex); } } public static Document getDocumentFromFile(File file) throws BrutException { try { - return ResXmlPatcher.loadDocument(file); - } catch (ParserConfigurationException | SAXException | IOException ex) { + return ResXmlUtils.loadDocument(file); + } catch (IOException | SAXException | ParserConfigurationException ex) { throw new BrutException(ex); } } - public static void copyResourceDir(Class class_, String dirPath, File out) throws BrutException { + public static void copyResourceDir(Class clz, String dirPath, File out) throws BrutException { if (!out.exists()) { out.mkdirs(); } - copyResourceDir(class_, dirPath, new FileDirectory(out)); + copyResourceDir(clz, dirPath, new FileDirectory(out)); } - public static void copyResourceDir(Class class_, String dirPath, Directory out) throws BrutException { - if (class_ == null) { - class_ = Class.class; + public static void copyResourceDir(Class clz, String dirPath, Directory out) throws BrutException { + if (clz == null) { + clz = Class.class; } - URL dirURL = class_.getClassLoader().getResource(dirPath); + URL dirURL = clz.getClassLoader().getResource(dirPath); if (dirURL != null && dirURL.getProtocol().equals("file")) { try { - DirUtil.copyToDir(new FileDirectory(dirURL.getFile()), out); + DirUtils.copyToDir(new FileDirectory(dirURL.getFile()), out); } catch (UnsupportedEncodingException ex) { throw new BrutException(ex); } @@ -112,15 +101,15 @@ public abstract class TestUtils { } if (dirURL == null) { - String className = class_.getName().replace(".", "/") + ".class"; - dirURL = class_.getClassLoader().getResource(className); + String className = clz.getName().replace(".", "/") + ".class"; + dirURL = clz.getClassLoader().getResource(className); } if (dirURL.getProtocol().equals("jar")) { String jarPath; try { jarPath = URLDecoder.decode(dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")), "UTF-8"); - DirUtil.copyToDir(new FileDirectory(jarPath), out); + DirUtils.copyToDir(new FileDirectory(jarPath), out); } catch (UnsupportedEncodingException ex) { throw new BrutException(ex); } @@ -137,11 +126,12 @@ public abstract class TestUtils { public static byte[] readHeaderOfFile(File file, int size) throws IOException { byte[] buffer = new byte[size]; - InputStream inputStream = Files.newInputStream(file.toPath()); - if (inputStream.read(buffer) != buffer.length) { - throw new IOException("File size too small for buffer length: " + size); + + try (InputStream in = Files.newInputStream(file.toPath())) { + if (in.read(buffer) != buffer.length) { + throw new IOException("File size too small for buffer length: " + size); + } } - inputStream.close(); return buffer; } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/AndroidOreoNotSparseTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/AndroidOreoNotSparseTest.java index e13deccd..25f6d4cb 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/AndroidOreoNotSparseTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/AndroidOreoNotSparseTest.java @@ -30,6 +30,7 @@ import java.io.File; import static org.junit.Assert.assertTrue; public class AndroidOreoNotSparseTest extends BaseTest { + @BeforeClass public static void beforeClass() throws Exception { TestUtils.cleanFrameworkFile(); @@ -39,7 +40,7 @@ public class AndroidOreoNotSparseTest extends BaseTest { LOGGER.info("Unpacking not_sparse.apk..."); TestUtils.copyResourceDir(AndroidOreoNotSparseTest.class, "aapt1/issue1594", sTestOrigDir); - File testApk = new File(sTestOrigDir, "not_sparse.apk"); + ExtFile testApk = new ExtFile(sTestOrigDir, "not_sparse.apk"); LOGGER.info("Decoding not_sparse.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); @@ -47,8 +48,8 @@ public class AndroidOreoNotSparseTest extends BaseTest { LOGGER.info("Building not_sparse.apk..."); Config config = Config.getDefaultConfig(); - config.useAapt2 = false; - new ApkBuilder(config, sTestNewDir).build(testApk); + config.aaptVersion = 1; + new ApkBuilder(sTestNewDir, config).build(testApk); } @AfterClass diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/AndroidOreoSparseTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/AndroidOreoSparseTest.java index 94619167..07fe8c0a 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/AndroidOreoSparseTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/AndroidOreoSparseTest.java @@ -30,6 +30,7 @@ import java.io.File; import static org.junit.Assert.assertTrue; public class AndroidOreoSparseTest extends BaseTest { + @BeforeClass public static void beforeClass() throws Exception { TestUtils.cleanFrameworkFile(); @@ -39,7 +40,7 @@ public class AndroidOreoSparseTest extends BaseTest { LOGGER.info("Unpacking sparse.apk..."); TestUtils.copyResourceDir(AndroidOreoSparseTest.class, "aapt1/issue1594", sTestOrigDir); - File testApk = new File(sTestOrigDir, "sparse.apk"); + ExtFile testApk = new ExtFile(sTestOrigDir, "sparse.apk"); LOGGER.info("Decoding sparse.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); @@ -47,8 +48,8 @@ public class AndroidOreoSparseTest extends BaseTest { LOGGER.info("Building sparse.apk..."); Config config = Config.getDefaultConfig(); - config.useAapt2 = false; - new ApkBuilder(config, sTestNewDir).build(testApk); + config.aaptVersion = 1; + new ApkBuilder(sTestNewDir, config).build(testApk); } @AfterClass diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/BuildAndDecodeJarTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/BuildAndDecodeJarTest.java index 2de35510..812e74d8 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/BuildAndDecodeJarTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/BuildAndDecodeJarTest.java @@ -40,10 +40,10 @@ public class BuildAndDecodeJarTest extends BaseTest { TestUtils.copyResourceDir(BuildAndDecodeJarTest.class, "aapt1/testjar/", sTestOrigDir); LOGGER.info("Building testjar.jar..."); - File testJar = new File(sTmpDir, "testjar.jar"); + ExtFile testJar = new ExtFile(sTmpDir, "testjar.jar"); Config config = Config.getDefaultConfig(); - config.useAapt2 = false; - new ApkBuilder(config, sTestOrigDir).build(testJar); + config.aaptVersion = 1; + new ApkBuilder(sTestOrigDir, config).build(testJar); LOGGER.info("Decoding testjar.jar..."); ApkDecoder apkDecoder = new ApkDecoder(testJar); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/BuildAndDecodeTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/BuildAndDecodeTest.java index 2faf5cf6..49ab1b96 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/BuildAndDecodeTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/BuildAndDecodeTest.java @@ -48,10 +48,10 @@ public class BuildAndDecodeTest extends BaseTest { TestUtils.copyResourceDir(BuildAndDecodeTest.class, "aapt1/testapp/", sTestOrigDir); LOGGER.info("Building testapp.apk..."); - File testApk = new File(sTmpDir, "testapp.apk"); + ExtFile testApk = new ExtFile(sTmpDir, "testapp.apk"); Config config = Config.getDefaultConfig(); - config.useAapt2 = false; - new ApkBuilder(config, sTestOrigDir).build(testApk); + config.aaptVersion = 1; + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding testapp.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); @@ -139,6 +139,11 @@ public class BuildAndDecodeTest extends BaseTest { compareValuesFiles("values-mcc001/plurals.xml"); } + @Test + public void miuiRegressionTest() throws BrutException { + compareValuesFiles("values-godzillaui/strings.xml"); + } + @Test public void valuesStringsTest() throws BrutException { compareValuesFiles("values-mcc001/strings.xml"); @@ -535,7 +540,7 @@ public class BuildAndDecodeTest extends BaseTest { } @Test - public void unknownFolderTest() throws BrutException { + public void unknownFolderTest() throws BrutException, IOException { compareUnknownFiles(); } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/DebugTagRetainedTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/DebugTagRetainedTest.java index 9c63aa29..2aafb45e 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/DebugTagRetainedTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/DebugTagRetainedTest.java @@ -48,11 +48,11 @@ public class DebugTagRetainedTest extends BaseTest { LOGGER.info("Building issue1235.apk..."); Config config = Config.getDefaultConfig(); - config.useAapt2 = false; + config.aaptVersion = 1; config.debugMode = true; - File testApk = new File(sTmpDir, "issue1235.apk"); - new ApkBuilder(config, sTestOrigDir).build(testApk); + ExtFile testApk = new ExtFile(sTmpDir, "issue1235.apk"); + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding issue1235.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/DefaultBaksmaliVariableTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/DefaultBaksmaliVariableTest.java index ccdd9b1c..c3a63ed6 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/DefaultBaksmaliVariableTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/DefaultBaksmaliVariableTest.java @@ -42,10 +42,10 @@ public class DefaultBaksmaliVariableTest extends BaseTest { TestUtils.copyResourceDir(DefaultBaksmaliVariableTest.class, "aapt1/issue1481/", sTestOrigDir); LOGGER.info("Building issue1481.jar..."); - File testJar = new File(sTmpDir, "issue1481.jar"); + ExtFile testJar = new ExtFile(sTmpDir, "issue1481.jar"); Config config = Config.getDefaultConfig(); - config.useAapt2 = false; - new ApkBuilder(config, sTestOrigDir).build(testJar); + config.aaptVersion = 1; + new ApkBuilder(sTestOrigDir, config).build(testJar); LOGGER.info("Decoding issue1481.jar..."); ApkDecoder apkDecoder = new ApkDecoder(testJar); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/EmptyResourcesArscTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/EmptyResourcesArscTest.java index 240d3040..a8f5ddb3 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/EmptyResourcesArscTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/EmptyResourcesArscTest.java @@ -33,6 +33,12 @@ import java.util.logging.Logger; import static org.junit.Assert.assertTrue; public class EmptyResourcesArscTest { + private static final Logger LOGGER = Logger.getLogger(EmptyResourcesArscTest.class.getName()); + + private static ExtFile sTmpDir; + private static ExtFile sTestOrigDir; + private static ExtFile sTestNewDir; + @BeforeClass public static void beforeClass() throws Exception { TestUtils.cleanFrameworkFile(); @@ -42,7 +48,7 @@ public class EmptyResourcesArscTest { LOGGER.info("Unpacking issue1730.apk..."); TestUtils.copyResourceDir(EmptyResourcesArscTest.class, "aapt1/issue1730", sTestOrigDir); - File testApk = new File(sTestOrigDir, "issue1730.apk"); + ExtFile testApk = new ExtFile(sTestOrigDir, "issue1730.apk"); LOGGER.info("Decoding issue1730.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); @@ -50,8 +56,8 @@ public class EmptyResourcesArscTest { LOGGER.info("Building issue1730.apk..."); Config config = Config.getDefaultConfig(); - config.useAapt2 = false; - new ApkBuilder(config, sTestNewDir).build(testApk); + config.aaptVersion = 1; + new ApkBuilder(sTestNewDir, config).build(testApk); } @AfterClass @@ -64,10 +70,4 @@ public class EmptyResourcesArscTest { assertTrue(sTestNewDir.isDirectory()); assertTrue(sTestOrigDir.isDirectory()); } - - private static ExtFile sTmpDir; - private static ExtFile sTestOrigDir; - private static ExtFile sTestNewDir; - - private final static Logger LOGGER = Logger.getLogger(EmptyResourcesArscTest.class.getName()); } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/ExternalEntityTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/ExternalEntityTest.java index 4c673958..f2440097 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/ExternalEntityTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/ExternalEntityTest.java @@ -39,10 +39,10 @@ public class ExternalEntityTest extends BaseTest { TestUtils.copyResourceDir(ExternalEntityTest.class, "decode/doctype/", sTestOrigDir); LOGGER.info("Building doctype.apk..."); - File testApk = new File(sTestOrigDir, "doctype.apk"); + ExtFile testApk = new ExtFile(sTestOrigDir, "doctype.apk"); Config config = Config.getDefaultConfig(); - config.useAapt2 = false; - new ApkBuilder(config, sTestOrigDir).build(testApk); + config.aaptVersion = 1; + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding doctype.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/LargeIntsInManifestTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/LargeIntsInManifestTest.java index c8e4574c..e670b795 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/LargeIntsInManifestTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/LargeIntsInManifestTest.java @@ -44,7 +44,7 @@ public class LargeIntsInManifestTest extends BaseTest { String apk = "issue767.apk"; // decode issue767.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); @@ -52,13 +52,13 @@ public class LargeIntsInManifestTest extends BaseTest { // build issue767 Config config = Config.getDefaultConfig(); - config.useAapt2 = false; + config.aaptVersion = 1; ExtFile testApk = new ExtFile(sTmpDir, apk + ".out"); - new ApkBuilder(config, testApk).build(null); + new ApkBuilder(testApk, config).build(null); String newApk = apk + ".out" + File.separator + "dist" + File.separator + apk; // decode issue767 again - apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + newApk)); + apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + newApk)); sTestNewDir = new ExtFile(sTmpDir + File.separator + apk + ".out.two"); outDir = new File(sTmpDir + File.separator + apk + ".out.two"); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/ProviderAttributeTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/ProviderAttributeTest.java index 35a9ba9a..f618cdd2 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/ProviderAttributeTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/ProviderAttributeTest.java @@ -53,20 +53,20 @@ public class ProviderAttributeTest extends BaseTest { String apk = "issue636.apk"; // decode issue636.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); File outDir = new File(sTmpDir + File.separator + apk + ".out"); apkDecoder.decode(outDir); // build issue636 ExtFile testApk = new ExtFile(sTmpDir, apk + ".out"); Config config = Config.getDefaultConfig(); - config.useAapt2 = false; - new ApkBuilder(config, testApk).build(null); + config.aaptVersion = 1; + new ApkBuilder(testApk, config).build(null); String newApk = apk + ".out" + File.separator + "dist" + File.separator + apk; assertTrue(fileExists(newApk)); // decode issues636 again - apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + newApk)); + apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + newApk)); outDir = new File(sTmpDir + File.separator + apk + ".out.two"); apkDecoder.decode(outDir); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/SharedLibraryTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/SharedLibraryTest.java index bc7b44f2..fa5f08ab 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/SharedLibraryTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/SharedLibraryTest.java @@ -81,30 +81,30 @@ public class SharedLibraryTest extends BaseTest { Config config = Config.getDefaultConfig(); config.frameworkDirectory = sTmpDir.getAbsolutePath(); config.frameworkTag = "shared"; - config.useAapt2 = false; + config.aaptVersion = 1; // install library/framework new Framework(config).installFramework(new File(sTmpDir + File.separator + library)); assertTrue(fileExists("2-shared.apk")); // decode client.apk - ApkDecoder apkDecoder = new ApkDecoder(config, new ExtFile(sTmpDir + File.separator + client)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + client), config); File outDir = new File(sTmpDir + File.separator + client + ".out"); apkDecoder.decode(outDir); // decode library.apk - ApkDecoder libraryDecoder = new ApkDecoder(config, new ExtFile(sTmpDir + File.separator + library)); + ApkDecoder libraryDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + library), config); outDir = new File(sTmpDir + File.separator + library + ".out"); libraryDecoder.decode(outDir); // build client.apk ExtFile clientApk = new ExtFile(sTmpDir, client + ".out"); - new ApkBuilder(config, clientApk).build(null); + new ApkBuilder(clientApk, config).build(null); assertTrue(fileExists(client + ".out" + File.separator + "dist" + File.separator + client)); // build library.apk (shared library) ExtFile libraryApk = new ExtFile(sTmpDir, library + ".out"); - new ApkBuilder(config, libraryApk).build(null); + new ApkBuilder(libraryApk, config).build(null); assertTrue(fileExists(library + ".out" + File.separator + "dist" + File.separator + library)); } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/SkipAssetTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/SkipAssetTest.java index f6d02f3b..1e375304 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/SkipAssetTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/SkipAssetTest.java @@ -52,7 +52,7 @@ public class SkipAssetTest extends BaseTest { config.forceDelete = true; // decode issue1605.apk - ApkDecoder apkDecoder = new ApkDecoder(config, new ExtFile(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); apkDecoder.decode(sTestOrigDir); @@ -69,7 +69,7 @@ public class SkipAssetTest extends BaseTest { config.forceDelete = true; // decode issue1605.apk - ApkDecoder apkDecoder = new ApkDecoder(config, new ExtFile(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); apkDecoder.decode(sTestOrigDir); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/UnknownCompressionTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/UnknownCompressionTest.java index 5e087db2..930b41c4 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/UnknownCompressionTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt1/UnknownCompressionTest.java @@ -42,7 +42,7 @@ public class UnknownCompressionTest extends BaseTest { String apk = "deflated_unknowns.apk"; Config config = Config.getDefaultConfig(); config.frameworkDirectory = sTmpDir.getAbsolutePath(); - config.useAapt2 = false; + config.aaptVersion = 1; sTestOrigDir = new ExtFile(sTmpDir, apk); @@ -54,7 +54,7 @@ public class UnknownCompressionTest extends BaseTest { // build deflated_unknowns ExtFile clientApkFolder = new ExtFile(sTestOrigDir.getAbsolutePath() + ".out"); - new ApkBuilder(config, clientApkFolder).build(null); + new ApkBuilder(clientApkFolder, config).build(null); sTestNewDir = new ExtFile(clientApkFolder, "dist" + File.separator + apk); } @@ -71,7 +71,7 @@ public class UnknownCompressionTest extends BaseTest { // Check that control = rebuilt (both deflated) // Add extra check for checking not equal to 0, just in case control gets broken assertEquals(control, rebuilt); - assertNotSame(0, rebuilt); + assertNotSame(Integer.valueOf(0), rebuilt); } @Test @@ -95,11 +95,11 @@ public class UnknownCompressionTest extends BaseTest { } @Test - public void confirmPngFileIsCorrectlyDeflatedTest() throws BrutException, IOException { + public void confirmPngFileIsStoredTest() throws BrutException, IOException { Integer control = sTestOrigDir.getDirectory().getCompressionLevel("950x150.png"); Integer rebuilt = sTestNewDir.getDirectory().getCompressionLevel("950x150.png"); - assertEquals(control, rebuilt); - assertEquals(Integer.valueOf(8), rebuilt); + assertNotSame(control, rebuilt); + assertEquals(Integer.valueOf(0), rebuilt); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/BuildAndDecodeTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/BuildAndDecodeTest.java index 14014adc..ab008d42 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/BuildAndDecodeTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/BuildAndDecodeTest.java @@ -49,12 +49,11 @@ public class BuildAndDecodeTest extends BaseTest { TestUtils.copyResourceDir(BuildAndDecodeTest.class, "aapt2/testapp/", sTestOrigDir); Config config = Config.getDefaultConfig(); - config.useAapt2 = true; config.verbose = true; LOGGER.info("Building testapp.apk..."); - File testApk = new File(sTmpDir, "testapp.apk"); - new ApkBuilder(config, sTestOrigDir).build(testApk); + ExtFile testApk = new ExtFile(sTmpDir, "testapp.apk"); + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding testapp.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); @@ -135,7 +134,7 @@ public class BuildAndDecodeTest extends BaseTest { } @Test - public void samsungQmgFilesHandledTest() throws IOException, BrutException { + public void samsungQmgFilesHandledTest() throws BrutException, IOException { compareBinaryFolder("drawable-xhdpi", true); } @@ -175,7 +174,7 @@ public class BuildAndDecodeTest extends BaseTest { } @Test - public void unknownFolderTest() throws BrutException { + public void unknownFolderTest() throws BrutException, IOException { compareUnknownFiles(); } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableFalseChangeToTrueTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableFalseChangeToTrueTest.java index 8ca7f8d6..4c30f864 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableFalseChangeToTrueTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableFalseChangeToTrueTest.java @@ -49,11 +49,10 @@ public class DebuggableFalseChangeToTrueTest extends BaseTest { LOGGER.info("Building issue2328-debuggable-flase.apk..."); Config config = Config.getDefaultConfig(); config.debugMode = true; - config.useAapt2 = true; config.verbose = true; - File testApk = new File(sTmpDir, "issue2328-debuggable-flase.apk"); - new ApkBuilder(config, sTestOrigDir).build(testApk); + ExtFile testApk = new ExtFile(sTmpDir, "issue2328-debuggable-flase.apk"); + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding issue2328-debuggable-flase.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableTrueAddedTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableTrueAddedTest.java index 9cab0376..8cce9d82 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableTrueAddedTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableTrueAddedTest.java @@ -49,11 +49,10 @@ public class DebuggableTrueAddedTest extends BaseTest { LOGGER.info("Building issue2328-debuggable-missing.apk..."); Config config = Config.getDefaultConfig(); config.debugMode = true; - config.useAapt2 = true; config.verbose = true; - File testApk = new File(sTmpDir, "issue2328-debuggable-missing.apk"); - new ApkBuilder(config, sTestOrigDir).build(testApk); + ExtFile testApk = new ExtFile(sTmpDir, "issue2328-debuggable-missing.apk"); + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding issue2328-debuggable-missing.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableTrueRetainedTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableTrueRetainedTest.java index d85822f4..321129a0 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableTrueRetainedTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/DebuggableTrueRetainedTest.java @@ -49,11 +49,10 @@ public class DebuggableTrueRetainedTest extends BaseTest { LOGGER.info("Building issue2328-debuggable-true.apk..."); Config config = Config.getDefaultConfig(); config.debugMode = true; - config.useAapt2 = true; config.verbose = true; - File testApk = new File(sTmpDir, "issue2328-debuggable-true.apk"); - new ApkBuilder(config, sTestOrigDir).build(testApk); + ExtFile testApk = new ExtFile(sTmpDir, "issue2328-debuggable-true.apk"); + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding issue2328-debuggable-true.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NetworkConfigTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NetworkConfigTest.java index 51be3706..f483d279 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NetworkConfigTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NetworkConfigTest.java @@ -17,25 +17,30 @@ package brut.androlib.aapt2; import brut.androlib.*; -import brut.androlib.Config; import brut.common.BrutException; import brut.directory.ExtFile; import brut.util.OS; -import org.custommonkey.xmlunit.XMLUnit; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.xml.sax.SAXException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; -import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; import static org.junit.Assert.*; public class NetworkConfigTest extends BaseTest { @@ -53,9 +58,8 @@ public class NetworkConfigTest extends BaseTest { LOGGER.info("Building testapp.apk..."); Config config = Config.getDefaultConfig(); config.netSecConf = true; - config.useAapt2 = true; - File testApk = new File(sTmpDir, "testapp.apk"); - new ApkBuilder(config, sTestOrigDir).build(testApk); + ExtFile testApk = new ExtFile(sTmpDir, "testapp.apk"); + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding testapp.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); @@ -73,20 +77,30 @@ public class NetworkConfigTest extends BaseTest { } @Test - public void netSecConfGeneric() throws IOException, SAXException { - LOGGER.info("Comparing network security configuration file..."); - String expected = TestUtils.replaceNewlines("" + - ""); + public void netSecConfGeneric() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException { + LOGGER.info("Verifying network security configuration file contains user and system certificates..."); byte[] encoded = Files.readAllBytes(Paths.get(String.valueOf(sTestNewDir), "res/xml/network_security_config.xml")); - String obtained = TestUtils.replaceNewlines(new String(encoded)); + String obtained = new String(encoded); - XMLUnit.setIgnoreWhitespace(true); - XMLUnit.setIgnoreAttributeOrder(true); - XMLUnit.setCompareUnmatched(false); + // Load the XML document + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new ByteArrayInputStream(obtained.getBytes())); - assertXMLEqual(expected, obtained); + // XPath expression to check for user and system certificates + XPath xPath = XPathFactory.newInstance().newXPath(); + + // Check if 'system' certificate exists + XPathExpression systemCertExpr = xPath.compile("//certificates[@src='system']"); + NodeList systemCertNodes = (NodeList) systemCertExpr.evaluate(doc, XPathConstants.NODESET); + assertTrue(systemCertNodes.getLength() > 0); + + // Check if 'user' certificate exists + XPathExpression userCertExpr = xPath.compile("//certificates[@src='user']"); + NodeList userCertNodes = (NodeList) userCertExpr.evaluate(doc, XPathConstants.NODESET); + assertTrue(userCertNodes.getLength() > 0); } @Test diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NoNetworkConfigTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NoNetworkConfigTest.java index d19d3f1d..d483daba 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NoNetworkConfigTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NoNetworkConfigTest.java @@ -55,9 +55,8 @@ public class NoNetworkConfigTest extends BaseTest { LOGGER.info("Building testapp.apk..."); Config config = Config.getDefaultConfig(); config.netSecConf = true; - config.useAapt2 = true; - File testApk = new File(sTmpDir, "testapp.apk"); - new ApkBuilder(config, sTestOrigDir).build(testApk); + ExtFile testApk = new ExtFile(sTmpDir, "testapp.apk"); + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding testapp.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NonStandardPkgIdTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NonStandardPkgIdTest.java index 34ea0c6b..dfef5c36 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NonStandardPkgIdTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/aapt2/NonStandardPkgIdTest.java @@ -31,6 +31,7 @@ import org.junit.Test; import static org.junit.Assert.*; public class NonStandardPkgIdTest extends BaseTest { + private static ResTable mResTable; @BeforeClass public static void beforeClass() throws Exception { @@ -45,12 +46,11 @@ public class NonStandardPkgIdTest extends BaseTest { TestUtils.copyResourceDir(BuildAndDecodeTest.class, "aapt2/pkgid8/", sTestOrigDir); Config config = Config.getDefaultConfig(); - config.useAapt2 = true; config.verbose = true; LOGGER.info("Building pkgid8.apk..."); ExtFile testApk = new ExtFile(sTmpDir, "pkgid8.apk"); - new ApkBuilder(config, sTestOrigDir).build(testApk); + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding pkgid8.apk..."); ApkInfo testInfo = new ApkInfo(testApk); @@ -91,6 +91,4 @@ public class NonStandardPkgIdTest extends BaseTest { assertEquals(0x80, mResTable.getResSpec(0x80020001).getPackage().getId()); assertEquals(0x80, mResTable.getResSpec(0x80030000).getPackage().getId()); } - - private static ResTable mResTable; } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ApkInfoReaderTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ApkInfoReaderTest.java index 4e3b1c2e..f63b007a 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ApkInfoReaderTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ApkInfoReaderTest.java @@ -25,23 +25,22 @@ public class ApkInfoReaderTest { private void checkStandard(ApkInfo apkInfo) { assertEquals("standard.apk", apkInfo.apkFileName); - assertFalse(apkInfo.resourcesAreCompressed); assertEquals(1, apkInfo.doNotCompress.size()); - assertEquals("resources.arsc", apkInfo.doNotCompress.iterator().next()); + assertEquals("arsc", apkInfo.doNotCompress.iterator().next()); assertFalse(apkInfo.isFrameworkApk); assertNotNull(apkInfo.packageInfo); assertEquals("127", apkInfo.packageInfo.forcedPackageId); assertNull(apkInfo.packageInfo.renameManifestPackage); - assertNotNull(apkInfo.getSdkInfo()); - assertEquals(2, apkInfo.getSdkInfo().size()); - assertEquals("25", apkInfo.getSdkInfo().get("minSdkVersion")); - assertEquals("30", apkInfo.getSdkInfo().get("targetSdkVersion")); + assertNotNull(apkInfo.sdkInfo); + assertEquals(2, apkInfo.sdkInfo.size()); + assertEquals("25", apkInfo.sdkInfo.get("minSdkVersion")); + assertEquals("30", apkInfo.sdkInfo.get("targetSdkVersion")); assertFalse(apkInfo.sharedLibrary); assertFalse(apkInfo.sparseResources); assertNotNull(apkInfo.usesFramework); assertNotNull(apkInfo.usesFramework.ids); assertEquals(1, apkInfo.usesFramework.ids.size()); - assertEquals(1, (long)apkInfo.usesFramework.ids.get(0)); + assertEquals(1, (long) apkInfo.usesFramework.ids.get(0)); assertNull(apkInfo.usesFramework.tag); assertNotNull(apkInfo.versionInfo); assertNull(apkInfo.versionInfo.versionCode); @@ -51,7 +50,7 @@ public class ApkInfoReaderTest { @Test public void testStandard() throws AndrolibException { ApkInfo apkInfo = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/standard.yml")); + getClass().getResourceAsStream("/apk/standard.yml")); checkStandard(apkInfo); assertEquals("2.8.1", apkInfo.version); } @@ -59,7 +58,7 @@ public class ApkInfoReaderTest { @Test public void testUnknownFields() throws AndrolibException { ApkInfo apkInfo = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/unknown_fields.yml")); + getClass().getResourceAsStream("/apk/unknown_fields.yml")); checkStandard(apkInfo); assertEquals("2.8.1", apkInfo.version); } @@ -67,7 +66,7 @@ public class ApkInfoReaderTest { @Test public void testSkipIncorrectIndent() throws AndrolibException { ApkInfo apkInfo = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/skip_incorrect_indent.yml")); + getClass().getResourceAsStream("/apk/skip_incorrect_indent.yml")); checkStandard(apkInfo); assertNotEquals("2.0.0", apkInfo.version); } @@ -75,7 +74,7 @@ public class ApkInfoReaderTest { @Test public void testFirstIncorrectIndent() throws AndrolibException { ApkInfo apkInfo = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/first_incorrect_indent.yml")); + getClass().getResourceAsStream("/apk/first_incorrect_indent.yml")); checkStandard(apkInfo); assertNotEquals("2.0.0", apkInfo.version); } @@ -83,66 +82,54 @@ public class ApkInfoReaderTest { @Test public void testUnknownFiles() throws AndrolibException { ApkInfo apkInfo = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/unknown_files.yml")); + getClass().getResourceAsStream("/apk/unknown_files.yml")); assertEquals("2.0.0", apkInfo.version); assertEquals("testapp.apk", apkInfo.apkFileName); assertFalse(apkInfo.isFrameworkApk); assertNotNull(apkInfo.usesFramework); assertEquals(1, apkInfo.usesFramework.ids.size()); - assertEquals(1, (long)apkInfo.usesFramework.ids.get(0)); + assertEquals(1, (long) apkInfo.usesFramework.ids.get(0)); assertNotNull(apkInfo.packageInfo); assertEquals("127", apkInfo.packageInfo.forcedPackageId); assertNotNull(apkInfo.versionInfo); assertEquals("1", apkInfo.versionInfo.versionCode); assertEquals("1.0", apkInfo.versionInfo.versionName); - assertFalse(apkInfo.resourcesAreCompressed); assertNotNull(apkInfo.doNotCompress); - assertEquals(4, apkInfo.doNotCompress.size()); + assertEquals(5, apkInfo.doNotCompress.size()); assertEquals("assets/0byte_file.jpg", apkInfo.doNotCompress.get(0)); assertEquals("arsc", apkInfo.doNotCompress.get(1)); assertEquals("png", apkInfo.doNotCompress.get(2)); assertEquals("mp3", apkInfo.doNotCompress.get(3)); - assertNotNull(apkInfo.unknownFiles); - assertEquals(7, apkInfo.unknownFiles.size()); - assertEquals("8", apkInfo.unknownFiles.get("AssetBundle/assets/a.txt")); - assertEquals("8", apkInfo.unknownFiles.get("AssetBundle/b.txt")); - assertEquals("8", apkInfo.unknownFiles.get("hidden.file")); - assertEquals("8", apkInfo.unknownFiles.get("non\u007Fprintable.file")); - assertEquals("0", apkInfo.unknownFiles.get("stored.file")); - assertEquals("8", apkInfo.unknownFiles.get("unk_folder/unknown_file")); - assertEquals("8", apkInfo.unknownFiles.get("lib_bug603/bug603")); + assertEquals("stored.file", apkInfo.doNotCompress.get(4)); } @Test public void testUlist_with_indent() throws AndrolibException { ApkInfo apkInfo = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/list_with_indent.yml")); + getClass().getResourceAsStream("/apk/list_with_indent.yml")); assertEquals("2.8.0", apkInfo.version); assertEquals("basic.apk", apkInfo.apkFileName); assertFalse(apkInfo.isFrameworkApk); assertNotNull(apkInfo.usesFramework); assertEquals(1, apkInfo.usesFramework.ids.size()); - assertEquals(1, (long)apkInfo.usesFramework.ids.get(0)); + assertEquals(1, (long) apkInfo.usesFramework.ids.get(0)); assertEquals("tag", apkInfo.usesFramework.tag); assertNotNull(apkInfo.packageInfo); assertEquals("127", apkInfo.packageInfo.forcedPackageId); assertEquals("com.test.basic", apkInfo.packageInfo.renameManifestPackage); - assertNotNull(apkInfo.getSdkInfo()); - assertEquals(3, apkInfo.getSdkInfo().size()); - assertEquals("4", apkInfo.getSdkInfo().get("minSdkVersion")); - assertEquals("30", apkInfo.getSdkInfo().get("maxSdkVersion")); - assertEquals("22", apkInfo.getSdkInfo().get("targetSdkVersion")); + assertNotNull(apkInfo.sdkInfo); + assertEquals(3, apkInfo.sdkInfo.size()); + assertEquals("4", apkInfo.sdkInfo.get("minSdkVersion")); + assertEquals("30", apkInfo.sdkInfo.get("maxSdkVersion")); + assertEquals("22", apkInfo.sdkInfo.get("targetSdkVersion")); assertFalse(apkInfo.sharedLibrary); assertTrue(apkInfo.sparseResources); - assertNotNull(apkInfo.unknownFiles); - assertEquals(1, apkInfo.unknownFiles.size()); - assertEquals("1", apkInfo.unknownFiles.get("hidden.file")); assertNotNull(apkInfo.versionInfo); assertEquals("71", apkInfo.versionInfo.versionCode); assertEquals("1.0.70", apkInfo.versionInfo.versionName); assertNotNull(apkInfo.doNotCompress); assertEquals(2, apkInfo.doNotCompress.size()); - assertEquals("resources.arsc", apkInfo.doNotCompress.get(0)); + assertEquals("arsc", apkInfo.doNotCompress.get(0)); assertEquals("png", apkInfo.doNotCompress.get(1)); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ApkInfoSerializationTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ApkInfoSerializationTest.java index 295493a0..7e9275ad 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ApkInfoSerializationTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ApkInfoSerializationTest.java @@ -23,25 +23,24 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.*; +import java.nio.file.Files; import static org.junit.Assert.*; public class ApkInfoSerializationTest { - @Rule public TemporaryFolder folder = new TemporaryFolder(); @Test public void checkApkInfoSerialization() throws IOException, AndrolibException { ApkInfo control = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/unknown_files.yml")); + getClass().getResourceAsStream("/apk/unknown_files.yml")); check(control); - File savedApkInfo = folder.newFile( "saved.yml" ); + File savedApkInfo = folder.newFile("saved.yml"); control.save(savedApkInfo); - try (FileInputStream fis = new FileInputStream(savedApkInfo)) { - ApkInfo saved = ApkInfo.load(fis); - check(saved); + try (InputStream in = Files.newInputStream(savedApkInfo.toPath())) { + check(ApkInfo.load(in)); } } @@ -51,27 +50,18 @@ public class ApkInfoSerializationTest { assertFalse(apkInfo.isFrameworkApk); assertNotNull(apkInfo.usesFramework); assertEquals(1, apkInfo.usesFramework.ids.size()); - assertEquals(1, (long)apkInfo.usesFramework.ids.get(0)); + assertEquals(1, (long) apkInfo.usesFramework.ids.get(0)); assertNotNull(apkInfo.packageInfo); assertEquals("127", apkInfo.packageInfo.forcedPackageId); assertNotNull(apkInfo.versionInfo); assertEquals("1", apkInfo.versionInfo.versionCode); assertEquals("1.0", apkInfo.versionInfo.versionName); - assertFalse(apkInfo.resourcesAreCompressed); assertNotNull(apkInfo.doNotCompress); - assertEquals(4, apkInfo.doNotCompress.size()); + assertEquals(5, apkInfo.doNotCompress.size()); assertEquals("assets/0byte_file.jpg", apkInfo.doNotCompress.get(0)); assertEquals("arsc", apkInfo.doNotCompress.get(1)); assertEquals("png", apkInfo.doNotCompress.get(2)); assertEquals("mp3", apkInfo.doNotCompress.get(3)); - assertNotNull(apkInfo.unknownFiles); - assertEquals(7, apkInfo.unknownFiles.size()); - assertEquals("8", apkInfo.unknownFiles.get("AssetBundle/assets/a.txt")); - assertEquals("8", apkInfo.unknownFiles.get("AssetBundle/b.txt")); - assertEquals("8", apkInfo.unknownFiles.get("hidden.file")); - assertEquals("8", apkInfo.unknownFiles.get("non\u007Fprintable.file")); - assertEquals("0", apkInfo.unknownFiles.get("stored.file")); - assertEquals("8", apkInfo.unknownFiles.get("unk_folder/unknown_file")); - assertEquals("8", apkInfo.unknownFiles.get("lib_bug603/bug603")); + assertEquals("stored.file", apkInfo.doNotCompress.get(4)); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ConsistentPropertyTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ConsistentPropertyTest.java index bada5313..2ff2f3c3 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ConsistentPropertyTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/ConsistentPropertyTest.java @@ -26,7 +26,7 @@ public class ConsistentPropertyTest { @Test public void testAssertingAllKnownApkInfoProperties() throws AndrolibException { ApkInfo apkInfo = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/basic.yml")); + getClass().getResourceAsStream("/apk/basic.yml")); assertEquals("2.8.0", apkInfo.version); assertEquals("basic.apk", apkInfo.apkFileName); @@ -40,11 +40,8 @@ public class ConsistentPropertyTest { assertEquals("com.test.basic", apkInfo.packageInfo.renameManifestPackage); assertEquals("71", apkInfo.versionInfo.versionCode); assertEquals("1.0.70", apkInfo.versionInfo.versionName); - assertFalse(apkInfo.resourcesAreCompressed); assertFalse(apkInfo.sharedLibrary); assertTrue(apkInfo.sparseResources); - assertEquals(1, apkInfo.unknownFiles.size()); assertEquals(2, apkInfo.doNotCompress.size()); - assertFalse(apkInfo.compressionType); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/DoNotCompressHieroglyphTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/DoNotCompressHieroglyphTest.java index 9c9fbd3c..f3ae48bc 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/DoNotCompressHieroglyphTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/DoNotCompressHieroglyphTest.java @@ -26,7 +26,7 @@ public class DoNotCompressHieroglyphTest { @Test public void testHieroglyph() throws AndrolibException { ApkInfo apkInfo = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/donotcompress_with_hieroglyph.yml")); + getClass().getResourceAsStream("/apk/donotcompress_with_hieroglyph.yml")); assertEquals("2.0.0", apkInfo.version); assertEquals("testapp.apk", apkInfo.apkFileName); assertEquals(2, apkInfo.doNotCompress.size()); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/InvalidSdkBoundingTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/InvalidSdkBoundingTest.java index d3f4c850..7555cb7c 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/InvalidSdkBoundingTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/InvalidSdkBoundingTest.java @@ -34,7 +34,7 @@ public class InvalidSdkBoundingTest { sdkInfo.put("targetSdkVersion", "25"); sdkInfo.put("maxSdkVersion", "19"); - apkInfo.setSdkInfo(sdkInfo); + apkInfo.sdkInfo = sdkInfo; assertEquals("19", apkInfo.checkTargetSdkVersionBounds()); } @@ -46,7 +46,7 @@ public class InvalidSdkBoundingTest { sdkInfo.put("targetSdkVersion", "25"); sdkInfo.put("maxSdkVersion", "19"); - apkInfo.setSdkInfo(sdkInfo); + apkInfo.sdkInfo = sdkInfo; assertEquals("19", apkInfo.checkTargetSdkVersionBounds()); } @@ -58,7 +58,7 @@ public class InvalidSdkBoundingTest { sdkInfo.put("minSdkVersion", "15"); sdkInfo.put("targetSdkVersion", "25"); - apkInfo.setSdkInfo(sdkInfo); + apkInfo.sdkInfo = sdkInfo; assertEquals("25", apkInfo.checkTargetSdkVersionBounds()); } @@ -69,7 +69,7 @@ public class InvalidSdkBoundingTest { Map sdkInfo = new LinkedHashMap<>(); sdkInfo.put("targetSdkVersion", "25"); - apkInfo.setSdkInfo(sdkInfo); + apkInfo.sdkInfo = sdkInfo; assertEquals("25", apkInfo.checkTargetSdkVersionBounds()); } @@ -80,7 +80,7 @@ public class InvalidSdkBoundingTest { Map sdkInfo = new LinkedHashMap<>(); sdkInfo.put("targetSdkVersion", "S"); - apkInfo.setSdkInfo(sdkInfo); + apkInfo.sdkInfo = sdkInfo; assertEquals("31", apkInfo.checkTargetSdkVersionBounds()); } @@ -91,7 +91,7 @@ public class InvalidSdkBoundingTest { Map sdkInfo = new LinkedHashMap<>(); sdkInfo.put("targetSdkVersion", "O"); - apkInfo.setSdkInfo(sdkInfo); + apkInfo.sdkInfo = sdkInfo; assertEquals("26", apkInfo.checkTargetSdkVersionBounds()); } @@ -100,9 +100,9 @@ public class InvalidSdkBoundingTest { ApkInfo apkInfo = new ApkInfo(); Map sdkInfo = new LinkedHashMap<>(); - sdkInfo.put("targetSdkVersion", "VANILLAICECREAM"); + sdkInfo.put("targetSdkVersion", "SDK_CUR_DEVELOPMENT"); - apkInfo.setSdkInfo(sdkInfo); + apkInfo.sdkInfo = sdkInfo; assertEquals("10000", apkInfo.checkTargetSdkVersionBounds()); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/MaliciousYamlTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/MaliciousYamlTest.java index 66a53c6a..df6fc9a2 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/MaliciousYamlTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/apk/MaliciousYamlTest.java @@ -26,7 +26,7 @@ public class MaliciousYamlTest { @Test public void testMaliciousYaml() throws AndrolibException { ApkInfo apkInfo = ApkInfo.load( - this.getClass().getResourceAsStream("/apk/cve20220476.yml")); + getClass().getResourceAsStream("/apk/cve20220476.yml")); assertEquals("2.6.1-ddc4bb-SNAPSHOT", apkInfo.version); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/AndResGuardTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/AndResGuardTest.java index fe6d3dc3..39518bd4 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/AndResGuardTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/AndResGuardTest.java @@ -48,7 +48,7 @@ public class AndResGuardTest extends BaseTest { String apk = "issue1170.apk"; // decode issue1170.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); @@ -65,7 +65,7 @@ public class AndResGuardTest extends BaseTest { config.forceDelete = true; config.decodeResources = Config.DECODE_RESOURCES_NONE; String apk = "issue1170.apk"; - ApkDecoder apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".raw.out"); File outDir = new File(sTmpDir + File.separator + apk + ".raw.out"); apkDecoder.decode(outDir); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/CompactResourceTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/CompactResourceTest.java index 5b4ad679..c7f6a583 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/CompactResourceTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/CompactResourceTest.java @@ -48,7 +48,7 @@ public class CompactResourceTest extends BaseTest { @Test public void checkIfDecodeSucceeds() throws BrutException, IOException, ParserConfigurationException, SAXException { String apk = "issue3366.apk"; - File testApk = new File(sTmpDir, apk); + ExtFile testApk = new ExtFile(sTmpDir, apk); // decode issue3366.apk ApkDecoder apkDecoder = new ApkDecoder(testApk); @@ -62,6 +62,6 @@ public class CompactResourceTest extends BaseTest { Config config = Config.getDefaultConfig(); LOGGER.info("Building duplicatedex.apk..."); - new ApkBuilder(config, sTestOrigDir).build(testApk); + new ApkBuilder(sTestOrigDir, config).build(testApk); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DecodeKotlinCoroutinesTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DecodeKotlinCoroutinesTest.java index d52b3555..96561eac 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DecodeKotlinCoroutinesTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DecodeKotlinCoroutinesTest.java @@ -55,7 +55,7 @@ public class DecodeKotlinCoroutinesTest extends BaseTest { Config config = Config.getDefaultConfig(); config.forceDelete = true; // decode kotlin coroutines - ApkDecoder apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); File outDir = new File(sTmpDir + File.separator + apk + ".out"); apkDecoder.decode(outDir); File coroutinesExceptionHandler = new File(sTmpDir + File.separator + apk + ".out" + File.separator + "META-INF" + File.separator + "services", "kotlinx.coroutines.CoroutineExceptionHandler"); @@ -66,23 +66,23 @@ public class DecodeKotlinCoroutinesTest extends BaseTest { } @Test - public void kotlinCoroutinesEncodeAfterDecodeTest() throws IOException, BrutException { + public void kotlinCoroutinesEncodeAfterDecodeTest() throws BrutException, IOException { Config config = Config.getDefaultConfig(); config.forceDelete = true; // decode kotlin coroutines - ApkDecoder apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); File outDir = new File(sTmpDir + File.separator + apk + ".out"); apkDecoder.decode(outDir); // build kotlin coroutines ExtFile testApk = new ExtFile(sTmpDir, apk + ".out"); - new ApkBuilder(config, testApk).build(null); + new ApkBuilder(testApk, config).build(null); String newApk = apk + ".out" + File.separator + "dist" + File.separator + apk; assertTrue(fileExists(newApk)); // decode kotlin coroutines again - apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + newApk)); + apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + newApk), config); outDir = new File(sTmpDir + File.separator + apk + ".out.two"); apkDecoder.decode(outDir); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DecodeKotlinTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DecodeKotlinTest.java index 415138e7..2dc515ad 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DecodeKotlinTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DecodeKotlinTest.java @@ -43,7 +43,7 @@ public class DecodeKotlinTest extends BaseTest { String apk = "testkotlin.apk"; // decode testkotlin.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); sTestNewDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DoubleExtensionUnknownFileTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DoubleExtensionUnknownFileTest.java index 29c68a1b..b56dc48a 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DoubleExtensionUnknownFileTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DoubleExtensionUnknownFileTest.java @@ -52,7 +52,7 @@ public class DoubleExtensionUnknownFileTest extends BaseTest { String apk = "issue1244.apk"; // decode issue1244.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); ExtFile decodedApk = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); apkDecoder.decode(outDir); @@ -60,7 +60,7 @@ public class DoubleExtensionUnknownFileTest extends BaseTest { ApkInfo apkInfo = ApkInfo.load(decodedApk); for (String string : apkInfo.doNotCompress) { if (StringUtils.countMatches(string, ".") > 1) { - assertTrue(string.equalsIgnoreCase("assets/bin/Data/sharedassets1.assets.split0")); + assertTrue(string.equals("assets/bin/Data/sharedassets1.assets.split0")); } } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DuplicateDexTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DuplicateDexTest.java index 9af4216f..a4ab7491 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DuplicateDexTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/DuplicateDexTest.java @@ -45,7 +45,7 @@ public class DuplicateDexTest extends BaseTest { @Test(expected = AndrolibException.class) public void decodeAllSourcesShouldThrowException() throws BrutException, IOException { - File testApk = new File(sTestOrigDir, "duplicatedex.apk"); + ExtFile testApk = new ExtFile(sTestOrigDir, "duplicatedex.apk"); LOGGER.info("Decoding duplicatedex.apk..."); ApkDecoder apkDecoder = new ApkDecoder(testApk); @@ -53,22 +53,22 @@ public class DuplicateDexTest extends BaseTest { LOGGER.info("Building duplicatedex.apk..."); Config config = Config.getDefaultConfig(); - new ApkBuilder(config, sTestNewDir).build(testApk); + new ApkBuilder(sTestNewDir, config).build(testApk); } @Test public void decodeUsingOnlyMainClassesMode() throws BrutException, IOException { - File testApk = new File(sTestOrigDir, "duplicatedex.apk"); + ExtFile testApk = new ExtFile(sTestOrigDir, "duplicatedex.apk"); LOGGER.info("Decoding duplicatedex.apk..."); Config config = Config.getDefaultConfig(); config.decodeSources = Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES; - ApkDecoder apkDecoder = new ApkDecoder(config, testApk); + ApkDecoder apkDecoder = new ApkDecoder(testApk, config); apkDecoder.decode(sTestNewDir); LOGGER.info("Building duplicatedex.apk..."); - new ApkBuilder(config, sTestNewDir).build(testApk); + new ApkBuilder(sTestNewDir, config).build(testApk); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/Empty9PatchTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/Empty9PatchTest.java index d9789e29..da4d9f09 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/Empty9PatchTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/Empty9PatchTest.java @@ -51,7 +51,7 @@ public class Empty9PatchTest extends BaseTest { String apk = "empty9patch.apk"; // decode empty9patch.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/EmptyArscTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/EmptyArscTest.java index 4a7916d6..75de4d6b 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/EmptyArscTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/EmptyArscTest.java @@ -50,7 +50,7 @@ public class EmptyArscTest extends BaseTest { String apk = "test.apk"; // decode test.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ForceManifestDecodeNoResourcesTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ForceManifestDecodeNoResourcesTest.java index 3ce0491b..19a8cb19 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ForceManifestDecodeNoResourcesTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ForceManifestDecodeNoResourcesTest.java @@ -34,8 +34,7 @@ import java.util.Arrays; import static org.junit.Assert.*; public class ForceManifestDecodeNoResourcesTest extends BaseTest { - - private final byte[] xmlHeader = new byte[] { + private static final byte[] XML_HEADER = { 0x3C, // < 0x3F, // ? 0x78, // x @@ -68,7 +67,7 @@ public class ForceManifestDecodeNoResourcesTest extends BaseTest { // let's probe filetype of manifest, we should detect XML File manifestFile = new File(output + File.separator + "AndroidManifest.xml"); byte[] magic = TestUtils.readHeaderOfFile(manifestFile, 6); - assertArrayEquals(this.xmlHeader, magic); + assertArrayEquals(XML_HEADER, magic); // confirm resources.arsc still exists, as its raw File resourcesArsc = new File(output + File.separator + "resources.arsc"); @@ -87,7 +86,7 @@ public class ForceManifestDecodeNoResourcesTest extends BaseTest { // let's probe filetype of manifest, we should detect XML File manifestFile = new File(output + File.separator + "AndroidManifest.xml"); byte[] magic = TestUtils.readHeaderOfFile(manifestFile, 6); - assertArrayEquals(this.xmlHeader, magic); + assertArrayEquals(XML_HEADER, magic); // confirm resources.arsc does not exist File resourcesArsc = new File(output + File.separator + "resources.arsc"); @@ -106,7 +105,7 @@ public class ForceManifestDecodeNoResourcesTest extends BaseTest { // lets probe filetype of manifest, we should detect XML File manifestFile = new File(output + File.separator + "AndroidManifest.xml"); byte[] magic = TestUtils.readHeaderOfFile(manifestFile, 6); - assertArrayEquals(this.xmlHeader, magic); + assertArrayEquals(XML_HEADER, magic); // confirm resources.arsc does not exist File resourcesArsc = new File(output + File.separator + "resources.arsc"); @@ -125,7 +124,7 @@ public class ForceManifestDecodeNoResourcesTest extends BaseTest { // lets probe filetype of manifest, we should not detect XML File manifestFile = new File(output + File.separator + "AndroidManifest.xml"); byte[] magic = TestUtils.readHeaderOfFile(manifestFile, 6); - assertFalse(Arrays.equals(this.xmlHeader, magic)); + assertFalse(Arrays.equals(XML_HEADER, magic)); // confirm resources.arsc exists File resourcesArsc = new File(output + File.separator + "resources.arsc"); @@ -139,7 +138,7 @@ public class ForceManifestDecodeNoResourcesTest extends BaseTest { config.forceDelete = true; config.decodeResources = decodeResources; config.forceDecodeManifest = decodeManifest; - ApkDecoder apkDecoder = new ApkDecoder(config, new File(apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(apk), config); apkDecoder.decode(new File(output)); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/LargeCompactResourceTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/LargeCompactResourceTest.java new file mode 100644 index 00000000..644f1ffd --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/LargeCompactResourceTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package brut.androlib.decode; + +import brut.androlib.*; +import brut.directory.ExtFile; +import brut.common.BrutException; +import brut.util.OS; +import java.io.File; +import java.io.IOException; + +import org.junit.*; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; + +import static org.junit.Assert.*; + +public class LargeCompactResourceTest extends BaseTest { + + @BeforeClass + public static void beforeClass() throws Exception { + TestUtils.cleanFrameworkFile(); + sTmpDir = new ExtFile(OS.createTempDirectory()); + TestUtils.copyResourceDir(CompactResourceTest.class, "decode/issue3705/", sTmpDir); + } + + @AfterClass + public static void afterClass() throws BrutException { + OS.rmdir(sTmpDir); + } + + @Test + public void checkIfDecodeSucceeds() throws BrutException, IOException, ParserConfigurationException, SAXException { + String apk = "issue3705.apk"; + ExtFile testApk = new ExtFile(sTmpDir, apk); + + // decode issue3705.apk + ApkDecoder apkDecoder = new ApkDecoder(testApk); + sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); + + File outDir = new File(sTmpDir + File.separator + apk + ".out"); + apkDecoder.decode(outDir); + + Document doc = loadDocument(new File(sTestOrigDir + "/res/values/strings.xml")); + assertFalse(resourceNameContains(doc.getDocumentElement(), "APKTOOL")); + + Config config = Config.getDefaultConfig(); + LOGGER.info("Building issue3705.apk..."); + new ApkBuilder(sTestOrigDir, config).build(testApk); + } +} diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MinifiedArscTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MinifiedArscTest.java index f4e12077..168bc9b9 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MinifiedArscTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MinifiedArscTest.java @@ -48,7 +48,7 @@ public class MinifiedArscTest extends BaseTest { Config config = Config.getDefaultConfig(); config.forceDelete = true; // decode issue1157.apk - ApkDecoder apkDecoder = new ApkDecoder(config, new ExtFile(sTmpDir, apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir, apk), config); // this should not raise an exception: apkDecoder.decode(sTestNewDir); } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingDiv9PatchTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingDiv9PatchTest.java index 67eeea31..93525f59 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingDiv9PatchTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingDiv9PatchTest.java @@ -32,10 +32,12 @@ import org.junit.Test; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; +import java.nio.file.Files; import static org.junit.Assert.*; public class MissingDiv9PatchTest extends BaseTest { + private static final int NP_COLOR = 0xff000000; @BeforeClass public static void beforeClass() throws Exception { @@ -51,26 +53,22 @@ public class MissingDiv9PatchTest extends BaseTest { @Test public void assertMissingDivAdded() throws Exception { - InputStream inputStream = getFileInputStream(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + File file = new File(sTmpDir, "pip_dismiss_scrim.9.png"); + byte[] data; - ResStreamDecoder decoder = OSDetection.isAndroid() ? new Res9patchAndroidStreamDecoder() : new Res9patchStreamDecoder(); - decoder.decode(inputStream, outputStream); + try (InputStream in = Files.newInputStream(file.toPath())) { + ResStreamDecoder decoder = OSDetection.isAndroid() ? new Res9patchAndroidStreamDecoder() : new Res9patchStreamDecoder(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + decoder.decode(in, out); + data = out.toByteArray(); + } - BufferedImage image = ImageIO.read(new ByteArrayInputStream(outputStream.toByteArray())); + BufferedImage image = ImageIO.read(new ByteArrayInputStream(data)); int height = image.getHeight() - 1; // First and last pixel will be invisible, so lets check the first column and ensure its all black for (int y = 1; y < height; y++) { assertEquals("y coordinate failed at: " + y, NP_COLOR, image.getRGB(0, y)); } - } - - private FileInputStream getFileInputStream() throws IOException { - File file = new File(sTmpDir, "pip_dismiss_scrim.9.png"); - return new FileInputStream(file.toPath().toString()); - } - - private static final int NP_COLOR = 0xff000000; } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingVersionManifestTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingVersionManifestTest.java index 55eaf149..a6002f68 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingVersionManifestTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingVersionManifestTest.java @@ -51,7 +51,7 @@ public class MissingVersionManifestTest extends BaseTest { String apk = "issue1264.apk"; // decode issue1264.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); ExtFile decodedApk = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); apkDecoder.decode(outDir); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/OutsideOfDirectoryEntryTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/OutsideOfDirectoryEntryTest.java index 80b4c4f5..2978b3aa 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/OutsideOfDirectoryEntryTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/OutsideOfDirectoryEntryTest.java @@ -42,7 +42,7 @@ public class OutsideOfDirectoryEntryTest extends BaseTest { String apk = "issue1589.apk"; // decode issue1589.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); sTestNewDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ParentDirectoryTraversalTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ParentDirectoryTraversalTest.java index 1805a27e..f1bb0917 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ParentDirectoryTraversalTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ParentDirectoryTraversalTest.java @@ -52,7 +52,7 @@ public class ParentDirectoryTraversalTest extends BaseTest { config.forceDelete = true; config.decodeResources = Config.DECODE_RESOURCES_NONE; // decode issue1498.apk - ApkDecoder apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); File outDir = new File(sTmpDir + File.separator + apk + ".out"); // this should not raise an exception: apkDecoder.decode(outDir); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ProtectedApkTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ProtectedApkTest.java index 2c979766..c1f419bb 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ProtectedApkTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ProtectedApkTest.java @@ -47,7 +47,7 @@ public class ProtectedApkTest extends BaseTest { String apk = "protected-v1.apk"; // decode protected-v1.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ResourceDirectoryTraversalTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ResourceDirectoryTraversalTest.java index 44ec664c..a9245b66 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ResourceDirectoryTraversalTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ResourceDirectoryTraversalTest.java @@ -52,7 +52,7 @@ public class ResourceDirectoryTraversalTest extends BaseTest { Config config = Config.getDefaultConfig(); config.forceDelete = true; - ApkDecoder apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); File outDir = new File(sTmpDir + File.separator + apk + ".out"); apkDecoder.decode(outDir); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ResourceModeTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ResourceModeTest.java index 1fbe855d..e0af62f4 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ResourceModeTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ResourceModeTest.java @@ -53,7 +53,7 @@ public class ResourceModeTest extends BaseTest { config.setDecodeResolveMode(Config.DECODE_RES_RESOLVE_REMOVE); // decode issue2836.apk - ApkDecoder apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + "remove.out"); File outDir = new File(sTmpDir + File.separator + apk + "remove.out"); @@ -84,7 +84,7 @@ public class ResourceModeTest extends BaseTest { config.setDecodeResolveMode(Config.DECODE_RES_RESOLVE_DUMMY); // decode issue2836.apk - ApkDecoder apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + "dummies.out"); File outDir = new File(sTmpDir + File.separator + apk + "dummies.out"); @@ -115,7 +115,7 @@ public class ResourceModeTest extends BaseTest { config.setDecodeResolveMode(Config.DECODE_RES_RESOLVE_RETAIN); // decode issue2836.apk - ApkDecoder apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk), config); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + "leave.out"); File outDir = new File(sTmpDir + File.separator + apk + "leave.out"); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/SparseFlagTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/SparseFlagTest.java index c3959250..1667b280 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/SparseFlagTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/SparseFlagTest.java @@ -50,35 +50,35 @@ public class SparseFlagTest extends BaseTest { @Test public void decodeWithExpectationOfSparseResources() throws BrutException, IOException { - File testApk = new File(sTestOrigDir, "sparse.apk"); + ExtFile testApk = new ExtFile(sTestOrigDir, "sparse.apk"); LOGGER.info("Decoding sparse.apk..."); Config config = Config.getDefaultConfig(); config.frameworkTag = "issue-3298"; - ApkDecoder apkDecoder = new ApkDecoder(config, testApk); + ApkDecoder apkDecoder = new ApkDecoder(testApk, config); ApkInfo apkInfo = apkDecoder.decode(sTestNewDir); assertTrue("Expecting sparse resources", apkInfo.sparseResources); LOGGER.info("Building sparse.apk..."); - new ApkBuilder(config, sTestNewDir).build(testApk); + new ApkBuilder(sTestNewDir, config).build(testApk); } @Test public void decodeWithExpectationOfNoSparseResources() throws BrutException, IOException { - File testApk = new File(sTestOrigDir, "not-sparse.apk"); + ExtFile testApk = new ExtFile(sTestOrigDir, "not-sparse.apk"); LOGGER.info("Decoding not-sparse.apk..."); Config config = Config.getDefaultConfig(); config.frameworkTag = "issue-3298"; - ApkDecoder apkDecoder = new ApkDecoder(config, testApk); + ApkDecoder apkDecoder = new ApkDecoder(testApk, config); ApkInfo apkInfo = apkDecoder.decode(sTestNewDir); assertFalse("Expecting not-sparse resources", apkInfo.sparseResources); LOGGER.info("Building not-sparse.apk..."); - new ApkBuilder(config, sTestNewDir).build(testApk); + new ApkBuilder(sTestNewDir, config).build(testApk); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/VectorDrawableTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/VectorDrawableTest.java index 255775a8..5f6c0706 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/VectorDrawableTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/VectorDrawableTest.java @@ -50,7 +50,7 @@ public class VectorDrawableTest extends BaseTest { String apk = "issue1456.apk"; // decode issue1456.apk - ApkDecoder apkDecoder = new ApkDecoder(new File(sTmpDir + File.separator + apk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(sTmpDir + File.separator + apk)); sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out"); File outDir = new File(sTmpDir + File.separator + apk + ".out"); diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/res/decoder/StringBlockWithSurrogatePairInUtf8Test.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/res/decoder/StringBlockWithSurrogatePairInUtf8Test.java index 7d9a6075..634db3c9 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/res/decoder/StringBlockWithSurrogatePairInUtf8Test.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/res/decoder/StringBlockWithSurrogatePairInUtf8Test.java @@ -25,48 +25,55 @@ import static org.junit.Assert.assertEquals; public class StringBlockWithSurrogatePairInUtf8Test { @Test public void decodeSingleOctet() { - final String actual = new StringBlock("abcDEF123".getBytes(StandardCharsets.UTF_8), true).decodeString(0, 9); + final byte[] bytes = "abcDEF123".getBytes(StandardCharsets.UTF_8); + final String actual = new StringBlock(bytes, true).decodeString(0, 9); assertEquals("Incorrect decoding", "abcDEF123", actual); } @Test public void decodeTwoOctets() { - final String actual0 = new StringBlock(new byte[] { (byte) 0xC2, (byte) 0x80}, true).decodeString(0, 2); + final byte[] bytes0 = { (byte) 0xC2, (byte) 0x80 }; + final String actual0 = new StringBlock(bytes0, true).decodeString(0, 2); assertEquals("Incorrect decoding", "\u0080", actual0); - final String actual1 = new StringBlock(new byte[] { (byte) 0xDF, (byte) 0xBF}, true).decodeString(0, 2); + final byte[] bytes1 = { (byte) 0xDF, (byte) 0xBF }; + final String actual1 = new StringBlock(bytes1, true).decodeString(0, 2); assertEquals("Incorrect decoding", "\u07FF", actual1); } @Test public void decodeThreeOctets() { - final String actual0 = new StringBlock(new byte[] { (byte) 0xE0, (byte) 0xA0, (byte) 0x80}, true).decodeString(0, 3); + final byte[] bytes0 = { (byte) 0xE0, (byte) 0xA0, (byte) 0x80 }; + final String actual0 = new StringBlock(bytes0, true).decodeString(0, 3); assertEquals("Incorrect decoding", "\u0800", actual0); - final String actual1 = new StringBlock(new byte[] { (byte) 0xEF, (byte) 0xBF, (byte) 0xBF}, true).decodeString(0, 3); + final byte[] bytes1 = { (byte) 0xEF, (byte) 0xBF, (byte) 0xBF }; + final String actual1 = new StringBlock(bytes1, true).decodeString(0, 3); assertEquals("Incorrect decoding", "\uFFFF", actual1); } @Test public void decodeSurrogatePair_when_givesAsThreeOctetsFromInvalidRangeOfUtf8() { // See: https://github.com/iBotPeaches/Apktool/issues/2299 - final String actual = new StringBlock(new byte[] { (byte) 0xED, (byte) 0xA0, (byte) 0xBD, (byte) 0xED, (byte) 0xB4, (byte) 0x86}, true).decodeString(0, 6); - assertEquals("Incorrect decoding", "\uD83D\uDD06", actual); + final byte[] bytes0 = { (byte) 0xED, (byte) 0xA0, (byte) 0xBD, (byte) 0xED, (byte) 0xB4, (byte) 0x86 }; + final String actual0 = new StringBlock(bytes0, true).decodeString(0, 6); + assertEquals("Incorrect decoding", "\uD83D\uDD06", actual0); // See: https://github.com/iBotPeaches/Apktool/issues/2546 - final byte[] bytesWithCharactersBeforeSurrogatePair = {'G', 'o', 'o', 'd', ' ', 'm', 'o', 'r', 'n', 'i', 'n', 'g', '!', ' ', + // Bytes with characters before surrogate pair + final byte[] bytes1 = { + 'G', 'o', 'o', 'd', ' ', 'm', 'o', 'r', 'n', 'i', 'n', 'g', '!', ' ', (byte) 0xED, (byte) 0xA0, (byte) 0xBD, (byte) 0xED, (byte) 0xB1, (byte) 0x8B, ' ', 'S', 'u', 'n', ' ', (byte) 0xED, (byte) 0xA0, (byte) 0xBC, (byte) 0xED, (byte) 0xBC, (byte) 0x9E }; - final String actual2 = new StringBlock(bytesWithCharactersBeforeSurrogatePair, true).decodeString(0, 31); - + final String actual1 = new StringBlock(bytes1, true).decodeString(0, 31); // D83D -> 0xED 0xA0 0xBD // DC4B -> 0xED 0xB1 0x8B // D83C -> 0xED 0xA0 0xBC // DF1E -> 0xED 0xBC 0x9E assertEquals("Incorrect decoding when there are valid characters before the surrogate pair", - "Good morning! \uD83D\uDC4B Sun \uD83C\uDF1E", actual2); + "Good morning! \uD83D\uDC4B Sun \uD83C\uDF1E", actual1); } @Test @@ -74,7 +81,8 @@ public class StringBlockWithSurrogatePairInUtf8Test { // \u10FFFF is encoded in UTF-8 as "0xDBFF 0xDFFF" (4-byte encoding), // but when used in Android resources which are encoded in UTF-8, 3-byte encoding is used, // so each of these is encoded as 3-bytes - final String actual = new StringBlock(new byte[] { (byte) 0xED, (byte) 0xAF, (byte) 0xBF, (byte) 0xED, (byte) 0xBF, (byte) 0xBF}, true).decodeString(0, 6); + final byte[] bytes = { (byte) 0xED, (byte) 0xAF, (byte) 0xBF, (byte) 0xED, (byte) 0xBF, (byte) 0xBF }; + final String actual = new StringBlock(bytes, true).decodeString(0, 6); assertEquals("Incorrect decoding", "\uDBFF\uDFFF", actual); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/res/src/DexStaticFieldValueTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/res/src/DexStaticFieldValueTest.java index e8f8dd50..4007f580 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/res/src/DexStaticFieldValueTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/res/src/DexStaticFieldValueTest.java @@ -49,11 +49,11 @@ public class DexStaticFieldValueTest extends BaseTest { LOGGER.info("Building issue2543.apk..."); File testApk = new File(sTmpDir, "issue2543.apk"); - new ApkBuilder(config, sTestOrigDir).build(testApk); + new ApkBuilder(sTestOrigDir, config).build(testApk); LOGGER.info("Decoding issue2543.apk..."); config.baksmaliDebugMode = false; - ApkDecoder apkDecoder = new ApkDecoder(config, new ExtFile(testApk)); + ApkDecoder apkDecoder = new ApkDecoder(new ExtFile(testApk), config); apkDecoder.decode(sTestNewDir); } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/util/AaptVersionTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/util/AaptVersionTest.java index 675a62c7..41b3b6a8 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/util/AaptVersionTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/util/AaptVersionTest.java @@ -26,9 +26,9 @@ public class AaptVersionTest { @Test public void testAapt2Iterations() throws BrutException { - assertEquals(2, AaptManager.getAppVersionFromString("Android Asset Packaging Tool (aapt) 2:17")); - assertEquals(2, AaptManager.getAppVersionFromString("Android Asset Packaging Tool (aapt) 2.17")); - assertEquals(1, AaptManager.getAppVersionFromString("Android Asset Packaging Tool, v0.9")); - assertEquals(1, AaptManager.getAppVersionFromString("Android Asset Packaging Tool, v0.2-2679779")); + assertEquals(2, AaptManager.getAaptVersionFromString("Android Asset Packaging Tool (aapt) 2:17")); + assertEquals(2, AaptManager.getAaptVersionFromString("Android Asset Packaging Tool (aapt) 2.17")); + assertEquals(1, AaptManager.getAaptVersionFromString("Android Asset Packaging Tool, v0.9")); + assertEquals(1, AaptManager.getAaptVersionFromString("Android Asset Packaging Tool, v0.2-2679779")); } } diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/util/UnknownDirectoryTraversalTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/util/UnknownDirectoryTraversalTest.java index 269b3fc0..27ddc960 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/util/UnknownDirectoryTraversalTest.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/util/UnknownDirectoryTraversalTest.java @@ -50,32 +50,32 @@ public class UnknownDirectoryTraversalTest extends BaseTest { } @Test - public void validFileTest() throws IOException, BrutException { - String validFilename = BrutIO.sanitizeFilepath(sTmpDir, "file"); - assertEquals(validFilename, "file"); + public void validFileTest() throws BrutException, IOException { + String validFileName = BrutIO.sanitizePath(sTmpDir, "file"); + assertEquals(validFileName, "file"); - File validFile = new File(sTmpDir, validFilename); + File validFile = new File(sTmpDir, validFileName); assertTrue(validFile.isFile()); } @Test(expected = TraversalUnknownFileException.class) - public void invalidBackwardFileTest() throws IOException, BrutException { - BrutIO.sanitizeFilepath(sTmpDir, "../file"); + public void invalidBackwardFileTest() throws BrutException, IOException { + BrutIO.sanitizePath(sTmpDir, "../file"); } @Test(expected = RootUnknownFileException.class) - public void invalidRootFileTest() throws IOException, BrutException { + public void invalidRootFileTest() throws BrutException, IOException { String rootLocation = OSDetection.isWindows() ? "C:/" : File.separator; - BrutIO.sanitizeFilepath(sTmpDir, rootLocation + "file"); + BrutIO.sanitizePath(sTmpDir, rootLocation + "file"); } @Test(expected = InvalidUnknownFileException.class) - public void noFilePassedTest() throws IOException, BrutException { - BrutIO.sanitizeFilepath(sTmpDir, ""); + public void noFilePassedTest() throws BrutException, IOException { + BrutIO.sanitizePath(sTmpDir, ""); } @Test(expected = TraversalUnknownFileException.class) - public void invalidBackwardPathOnWindows() throws IOException, BrutException { + public void invalidBackwardPathOnWindows() throws BrutException, IOException { String invalidPath; if (! OSDetection.isWindows()) { invalidPath = "../../app"; @@ -83,12 +83,12 @@ public class UnknownDirectoryTraversalTest extends BaseTest { invalidPath = "..\\..\\app.exe"; } - BrutIO.sanitizeFilepath(sTmpDir, invalidPath); + BrutIO.sanitizePath(sTmpDir, invalidPath); } @Test - public void validDirectoryFileTest() throws IOException, BrutException { - String validFilename = BrutIO.sanitizeFilepath(sTmpDir, "dir" + File.separator + "file"); - assertEquals("dir" + File.separator + "file", validFilename); + public void validDirectoryFileTest() throws BrutException, IOException { + String validFileName = BrutIO.sanitizePath(sTmpDir, "dir" + File.separator + "file"); + assertEquals("dir" + File.separator + "file", validFileName); } } diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt1/issue1235/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/aapt1/issue1235/apktool.yml index 9daf7e74..96426c32 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt1/issue1235/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt1/issue1235/apktool.yml @@ -8,5 +8,4 @@ packageInfo: forcedPackageId: '127' versionInfo: versionCode: '1' - versionName: '1.0' -compressionType: false \ No newline at end of file + versionName: '1.0' \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt1/testapp/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/aapt1/testapp/apktool.yml index 8362ef05..87d8da18 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt1/testapp/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt1/testapp/apktool.yml @@ -9,17 +9,9 @@ packageInfo: versionInfo: versionCode: '1' versionName: '1.0' -compressionType: false doNotCompress: - assets/0byte_file.jpg - arsc - png - mp3 -unknownFiles: - AssetBundle/assets/a.txt: '8' - AssetBundle/b.txt: '8' - hidden.file: '8' - non\u007Fprintable.file: '8' - stored.file: '0' - unk_folder/unknown_file: '8' - lib_bug603/bug603: '8' +- stored.file diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt1/testapp/res/values-godzillaui/strings.xml b/brut.apktool/apktool-lib/src/test/resources/aapt1/testapp/res/values-godzillaui/strings.xml new file mode 100644 index 00000000..965a23a8 --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/resources/aapt1/testapp/res/values-godzillaui/strings.xml @@ -0,0 +1,4 @@ + + + test1 + diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-false/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-false/apktool.yml index 73522b62..ecc612a2 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-false/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-false/apktool.yml @@ -8,5 +8,4 @@ packageInfo: forcedPackageId: '127' versionInfo: versionCode: '1' - versionName: '1.0' -compressionType: false \ No newline at end of file + versionName: '1.0' \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-missing/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-missing/apktool.yml index 73522b62..ecc612a2 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-missing/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-missing/apktool.yml @@ -8,5 +8,4 @@ packageInfo: forcedPackageId: '127' versionInfo: versionCode: '1' - versionName: '1.0' -compressionType: false \ No newline at end of file + versionName: '1.0' \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-true/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-true/apktool.yml index 73522b62..ecc612a2 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-true/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt2/issue2328/debuggable-true/apktool.yml @@ -8,5 +8,4 @@ packageInfo: forcedPackageId: '127' versionInfo: versionCode: '1' - versionName: '1.0' -compressionType: false \ No newline at end of file + versionName: '1.0' \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt2/pkgid8/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/aapt2/pkgid8/apktool.yml index 74875006..dc7aef41 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt2/pkgid8/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt2/pkgid8/apktool.yml @@ -8,5 +8,4 @@ packageInfo: forcedPackageId: '128' versionInfo: versionCode: '1' - versionName: '1.0' -compressionType: false \ No newline at end of file + versionName: '1.0' \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml index 444556f3..b5020e46 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/AndroidManifest.xml @@ -1,7 +1,7 @@ - + @@ -11,4 +11,5 @@ + diff --git a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/apktool.yml index e83d2e54..4da39874 100644 --- a/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/aapt2/testapp/apktool.yml @@ -9,10 +9,8 @@ packageInfo: versionInfo: versionCode: '1' versionName: '1.0' -compressionType: false +featureFlags: + brut.feature.flag: true doNotCompress: - assets/0byte_file.jpg sparseResources: false -unknownFiles: - AssetBundle/assets/a.txt: '8' - AssetBundle/b.txt: '8' diff --git a/brut.apktool/apktool-lib/src/test/resources/apk/basic.yml b/brut.apktool/apktool-lib/src/test/resources/apk/basic.yml index b51612c6..c1ecc383 100644 --- a/brut.apktool/apktool-lib/src/test/resources/apk/basic.yml +++ b/brut.apktool/apktool-lib/src/test/resources/apk/basic.yml @@ -1,8 +1,7 @@ !!brut.androlib.meta.MetaInfo apkFileName: basic.apk -compressionType: false doNotCompress: - - resources.arsc + - arsc - png isFrameworkApk: false packageInfo: @@ -14,8 +13,6 @@ sdkInfo: targetSdkVersion: '22' sharedLibrary: false sparseResources: true -unknownFiles: - hidden.file: 1 usesFramework: ids: - 1 diff --git a/brut.apktool/apktool-lib/src/test/resources/apk/cve20220476.yml b/brut.apktool/apktool-lib/src/test/resources/apk/cve20220476.yml index 2006ae81..a705c8c7 100644 --- a/brut.apktool/apktool-lib/src/test/resources/apk/cve20220476.yml +++ b/brut.apktool/apktool-lib/src/test/resources/apk/cve20220476.yml @@ -1,9 +1,8 @@ !!brut.androlib.meta.MetaInfo apkFileName: cve20220476.apk -compressionType: false some_var: !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["https://127.0.0.1:8000"]]]] doNotCompress: -- resources.arsc +- arsc isFrameworkApk: false packageInfo: forcedPackageId: '127' diff --git a/brut.apktool/apktool-lib/src/test/resources/apk/first_incorrect_indent.yml b/brut.apktool/apktool-lib/src/test/resources/apk/first_incorrect_indent.yml index 49e9b669..08f35ab7 100644 --- a/brut.apktool/apktool-lib/src/test/resources/apk/first_incorrect_indent.yml +++ b/brut.apktool/apktool-lib/src/test/resources/apk/first_incorrect_indent.yml @@ -1,9 +1,8 @@ !!brut.androlib.meta.MetaInfo version: 2.0.0 apkFileName: standard.apk -compressionType: false doNotCompress: -- resources.arsc +- arsc isFrameworkApk: false packageInfo: forcedPackageId: '127' diff --git a/brut.apktool/apktool-lib/src/test/resources/apk/list_with_indent.yml b/brut.apktool/apktool-lib/src/test/resources/apk/list_with_indent.yml index b51612c6..c1ecc383 100644 --- a/brut.apktool/apktool-lib/src/test/resources/apk/list_with_indent.yml +++ b/brut.apktool/apktool-lib/src/test/resources/apk/list_with_indent.yml @@ -1,8 +1,7 @@ !!brut.androlib.meta.MetaInfo apkFileName: basic.apk -compressionType: false doNotCompress: - - resources.arsc + - arsc - png isFrameworkApk: false packageInfo: @@ -14,8 +13,6 @@ sdkInfo: targetSdkVersion: '22' sharedLibrary: false sparseResources: true -unknownFiles: - hidden.file: 1 usesFramework: ids: - 1 diff --git a/brut.apktool/apktool-lib/src/test/resources/apk/skip_incorrect_indent.yml b/brut.apktool/apktool-lib/src/test/resources/apk/skip_incorrect_indent.yml index cfdc2d78..f1356897 100644 --- a/brut.apktool/apktool-lib/src/test/resources/apk/skip_incorrect_indent.yml +++ b/brut.apktool/apktool-lib/src/test/resources/apk/skip_incorrect_indent.yml @@ -1,9 +1,8 @@ !!brut.androlib.meta.MetaInfo apkFileName: standard.apk version: 2.0.0 -compressionType: false doNotCompress: -- resources.arsc +- arsc isFrameworkApk: false packageInfo: forcedPackageId: '127' diff --git a/brut.apktool/apktool-lib/src/test/resources/apk/standard.yml b/brut.apktool/apktool-lib/src/test/resources/apk/standard.yml index 6aa78ecb..deb61b22 100644 --- a/brut.apktool/apktool-lib/src/test/resources/apk/standard.yml +++ b/brut.apktool/apktool-lib/src/test/resources/apk/standard.yml @@ -1,8 +1,7 @@ !!brut.androlib.meta.MetaInfo apkFileName: standard.apk -compressionType: false doNotCompress: -- resources.arsc +- arsc isFrameworkApk: false packageInfo: forcedPackageId: '127' diff --git a/brut.apktool/apktool-lib/src/test/resources/apk/unknown_fields.yml b/brut.apktool/apktool-lib/src/test/resources/apk/unknown_fields.yml index 876e9fca..dd90de40 100644 --- a/brut.apktool/apktool-lib/src/test/resources/apk/unknown_fields.yml +++ b/brut.apktool/apktool-lib/src/test/resources/apk/unknown_fields.yml @@ -1,9 +1,8 @@ !!brut.androlib.meta.MetaInfo apkFileName: standard.apk -compressionType: false test: test doNotCompress: -- resources.arsc +- arsc isFrameworkApk: false packageInfo: forcedPackageId: '127' diff --git a/brut.apktool/apktool-lib/src/test/resources/apk/unknown_files.yml b/brut.apktool/apktool-lib/src/test/resources/apk/unknown_files.yml index 8362ef05..87d8da18 100644 --- a/brut.apktool/apktool-lib/src/test/resources/apk/unknown_files.yml +++ b/brut.apktool/apktool-lib/src/test/resources/apk/unknown_files.yml @@ -9,17 +9,9 @@ packageInfo: versionInfo: versionCode: '1' versionName: '1.0' -compressionType: false doNotCompress: - assets/0byte_file.jpg - arsc - png - mp3 -unknownFiles: - AssetBundle/assets/a.txt: '8' - AssetBundle/b.txt: '8' - hidden.file: '8' - non\u007Fprintable.file: '8' - stored.file: '0' - unk_folder/unknown_file: '8' - lib_bug603/bug603: '8' +- stored.file diff --git a/brut.apktool/apktool-lib/src/test/resources/decode/doctype/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/decode/doctype/apktool.yml index 1e3c552c..fd49b569 100644 --- a/brut.apktool/apktool-lib/src/test/resources/decode/doctype/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/decode/doctype/apktool.yml @@ -8,5 +8,4 @@ packageInfo: forcedPackageId: '127' versionInfo: versionCode: '1' - versionName: '1.0' -compressionType: false \ No newline at end of file + versionName: '1.0' \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/decode/issue2543/apktool.yml b/brut.apktool/apktool-lib/src/test/resources/decode/issue2543/apktool.yml index 7e576b37..2fa938a1 100644 --- a/brut.apktool/apktool-lib/src/test/resources/decode/issue2543/apktool.yml +++ b/brut.apktool/apktool-lib/src/test/resources/decode/issue2543/apktool.yml @@ -8,5 +8,4 @@ packageInfo: forcedPackageId: '127' versionInfo: versionCode: '1' - versionName: '1.0' -compressionType: false \ No newline at end of file + versionName: '1.0' \ No newline at end of file diff --git a/brut.apktool/apktool-lib/src/test/resources/decode/issue3705/issue3705.apk b/brut.apktool/apktool-lib/src/test/resources/decode/issue3705/issue3705.apk new file mode 100644 index 00000000..19034fef Binary files /dev/null and b/brut.apktool/apktool-lib/src/test/resources/decode/issue3705/issue3705.apk differ diff --git a/brut.j.common/src/main/java/brut/common/BrutException.java b/brut.j.common/src/main/java/brut/common/BrutException.java index b85c7165..b5ed4479 100644 --- a/brut.j.common/src/main/java/brut/common/BrutException.java +++ b/brut.j.common/src/main/java/brut/common/BrutException.java @@ -17,6 +17,15 @@ package brut.common; public class BrutException extends Exception { + + public BrutException() { + super(); + } + + public BrutException(String message) { + super(message); + } + public BrutException(Throwable cause) { super(cause); } @@ -24,11 +33,4 @@ public class BrutException extends Exception { public BrutException(String message, Throwable cause) { super(message, cause); } - - public BrutException(String message) { - super(message); - } - - public BrutException() { - } } diff --git a/brut.j.common/src/main/java/brut/common/InvalidUnknownFileException.java b/brut.j.common/src/main/java/brut/common/InvalidUnknownFileException.java index 195cd9a5..a5853ce3 100644 --- a/brut.j.common/src/main/java/brut/common/InvalidUnknownFileException.java +++ b/brut.j.common/src/main/java/brut/common/InvalidUnknownFileException.java @@ -17,6 +17,7 @@ package brut.common; public class InvalidUnknownFileException extends BrutException { + public InvalidUnknownFileException(String message) { super(message); } diff --git a/brut.j.common/src/main/java/brut/common/RootUnknownFileException.java b/brut.j.common/src/main/java/brut/common/RootUnknownFileException.java index 1457268e..d372795a 100644 --- a/brut.j.common/src/main/java/brut/common/RootUnknownFileException.java +++ b/brut.j.common/src/main/java/brut/common/RootUnknownFileException.java @@ -17,6 +17,7 @@ package brut.common; public class RootUnknownFileException extends BrutException { + public RootUnknownFileException(String message) { super(message); } diff --git a/brut.j.common/src/main/java/brut/common/TraversalUnknownFileException.java b/brut.j.common/src/main/java/brut/common/TraversalUnknownFileException.java index a58116e1..8956ab01 100644 --- a/brut.j.common/src/main/java/brut/common/TraversalUnknownFileException.java +++ b/brut.j.common/src/main/java/brut/common/TraversalUnknownFileException.java @@ -17,6 +17,7 @@ package brut.common; public class TraversalUnknownFileException extends BrutException { + public TraversalUnknownFileException(String message) { super(message); } diff --git a/brut.j.dir/build.gradle.kts b/brut.j.dir/build.gradle.kts index d4865c80..d6157bc6 100644 --- a/brut.j.dir/build.gradle.kts +++ b/brut.j.dir/build.gradle.kts @@ -1,5 +1,5 @@ dependencies { - implementation(project(":brut.j.common")) - implementation(project(":brut.j.util")) - implementation(libs.commons.io) + implementation(project(":brut.j.common")) + implementation(project(":brut.j.util")) + implementation(libs.commons.io) } diff --git a/brut.j.dir/src/main/java/brut/directory/AbstractDirectory.java b/brut.j.dir/src/main/java/brut/directory/AbstractDirectory.java index ecd5da77..acf2dbaa 100644 --- a/brut.j.dir/src/main/java/brut/directory/AbstractDirectory.java +++ b/brut.j.dir/src/main/java/brut/directory/AbstractDirectory.java @@ -59,7 +59,7 @@ public abstract class AbstractDirectory implements Directory { SubPath subpath; try { subpath = getSubPath(path); - } catch (PathNotExist e) { + } catch (PathNotExist ex) { return false; } @@ -74,7 +74,7 @@ public abstract class AbstractDirectory implements Directory { SubPath subpath; try { subpath = getSubPath(path); - } catch (PathNotExist e) { + } catch (PathNotExist ex) { return false; } @@ -117,10 +117,9 @@ public abstract class AbstractDirectory implements Directory { } Directory dir; - // IMPOSSIBLE_EXCEPTION try { dir = createDir(parsed.dir); - } catch (PathAlreadyExists e) { + } catch (PathAlreadyExists ex) { dir = getAbstractDirs().get(parsed.dir); } return dir.getFileOutput(parsed.subpath); @@ -165,7 +164,7 @@ public abstract class AbstractDirectory implements Directory { SubPath subpath; try { subpath = getSubPath(path); - } catch (PathNotExist e) { + } catch (PathNotExist ex) { return false; } @@ -181,31 +180,31 @@ public abstract class AbstractDirectory implements Directory { } public void copyToDir(Directory out) throws DirectoryException { - DirUtil.copyToDir(out, out); + DirUtils.copyToDir(out, out); } public void copyToDir(Directory out, String[] fileNames) throws DirectoryException { - DirUtil.copyToDir(out, out, fileNames); + DirUtils.copyToDir(out, out, fileNames); } public void copyToDir(Directory out, String fileName) throws DirectoryException { - DirUtil.copyToDir(out, out, fileName); + DirUtils.copyToDir(out, out, fileName); } public void copyToDir(File out) throws DirectoryException { - DirUtil.copyToDir(this, out); + DirUtils.copyToDir(this, out); } public void copyToDir(File out, String[] fileNames) throws DirectoryException { - DirUtil.copyToDir(this, out, fileNames); + DirUtils.copyToDir(this, out, fileNames); } public void copyToDir(File out, String fileName) throws DirectoryException { - DirUtil.copyToDir(this, out, fileName); + DirUtils.copyToDir(this, out, fileName); } public int getCompressionLevel(String fileName) @@ -226,7 +225,7 @@ public abstract class AbstractDirectory implements Directory { } Map dirs = new LinkedHashMap<>(mDirs); - for (Map.Entry dir : getAbstractDirs().entrySet()) { + for (Map.Entry dir : mDirs.entrySet()) { for (Map.Entry subdir : dir.getValue().getAbstractDirs( true).entrySet()) { dirs.put(dir.getKey() + separator + subdir.getKey(), @@ -270,6 +269,7 @@ public abstract class AbstractDirectory implements Directory { private static class ParsedPath { public final String dir; public final String subpath; + public ParsedPath(String dir, String subpath) { this.dir = dir; this.subpath = subpath; diff --git a/brut.j.dir/src/main/java/brut/directory/DirUtil.java b/brut.j.dir/src/main/java/brut/directory/DirUtils.java similarity index 66% rename from brut.j.dir/src/main/java/brut/directory/DirUtil.java rename to brut.j.dir/src/main/java/brut/directory/DirUtils.java index 906fdac2..bdc36d8d 100644 --- a/brut.j.dir/src/main/java/brut/directory/DirUtil.java +++ b/brut.j.dir/src/main/java/brut/directory/DirUtils.java @@ -22,27 +22,26 @@ import brut.common.RootUnknownFileException; import brut.common.TraversalUnknownFileException; import brut.util.BrutIO; import brut.util.OS; + import java.io.*; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.util.logging.Logger; -public class DirUtil { +public final class DirUtils { private static final Logger LOGGER = Logger.getLogger(""); - private DirUtil() { + private DirUtils() { // Private constructor for utility class } - public static void copyToDir(Directory in, Directory out) - throws DirectoryException { + public static void copyToDir(Directory in, Directory out) throws DirectoryException { for (String fileName : in.getFiles(true)) { copyToDir(in, out, fileName); } } - public static void copyToDir(Directory in, Directory out, - String[] fileNames) throws DirectoryException { + public static void copyToDir(Directory in, Directory out, String[] fileNames) throws DirectoryException { for (String fileName : fileNames) { copyToDir(in, out, fileName); } @@ -53,16 +52,16 @@ public class DirUtil { copyToDir(in, out, fileName, fileName); } - public static void copyToDir(Directory in, Directory out, String inFile, String outFile) + public static void copyToDir(Directory in, Directory out, String inFileName, String outFileName) throws DirectoryException { try { - if (in.containsDir(inFile)) { - in.getDir(inFile).copyToDir(out.createDir(outFile)); + if (in.containsDir(inFileName)) { + in.getDir(inFileName).copyToDir(out.createDir(outFileName)); } else { - BrutIO.copyAndClose(in.getFileInput(inFile), out.getFileOutput(outFile)); + BrutIO.copyAndClose(in.getFileInput(inFileName), out.getFileOutput(outFileName)); } } catch (IOException ex) { - throw new DirectoryException("Error copying file: " + inFile, ex); + throw new DirectoryException("Error copying file: " + inFileName, ex); } } @@ -82,27 +81,33 @@ public class DirUtil { public static void copyToDir(Directory in, File out, String fileName) throws DirectoryException { + copyToDir(in, out, fileName, fileName); + } + + public static void copyToDir(Directory in, File out, String inFileName, String outFileName) + throws DirectoryException { try { - if (in.containsDir(fileName)) { - OS.rmdir(new File(out, fileName)); - in.getDir(fileName).copyToDir(new File(out, fileName)); - } else if (!in.containsDir(fileName) && !in.containsFile(fileName)) { - // Skip copies of directories/files not found. - } else { - String cleanedFilename = BrutIO.sanitizeFilepath(out, fileName); - if (! cleanedFilename.isEmpty()) { - File outFile = new File(out, cleanedFilename); + if (in.containsDir(inFileName)) { + File outDir = new File(out, outFileName); + OS.rmdir(outDir); + in.getDir(inFileName).copyToDir(outDir); + } else if (in.containsFile(inFileName)) { + outFileName = BrutIO.sanitizePath(out, outFileName); + if (!outFileName.isEmpty()) { + File outFile = new File(out, outFileName); //noinspection ResultOfMethodCallIgnored outFile.getParentFile().mkdirs(); - BrutIO.copyAndClose(in.getFileInput(fileName), Files.newOutputStream(outFile.toPath())); + BrutIO.copyAndClose(in.getFileInput(inFileName), Files.newOutputStream(outFile.toPath())); } + } else { + // Skip if directory/file not found } - } catch (FileSystemException exception) { - LOGGER.warning(String.format("Skipping file %s (%s)", fileName, exception.getReason())); - } catch (RootUnknownFileException | InvalidUnknownFileException | TraversalUnknownFileException | IOException exception) { - LOGGER.warning(String.format("Skipping file %s (%s)", fileName, exception.getMessage())); + } catch (FileSystemException ex) { + LOGGER.warning(String.format("Skipping file %s (%s)", inFileName, ex.getReason())); + } catch (RootUnknownFileException | InvalidUnknownFileException | TraversalUnknownFileException | IOException ex) { + LOGGER.warning(String.format("Skipping file %s (%s)", inFileName, ex.getMessage())); } catch (BrutException ex) { - throw new DirectoryException("Error copying file: " + fileName, ex); + throw new DirectoryException("Error copying file: " + inFileName, ex); } } } diff --git a/brut.j.dir/src/main/java/brut/directory/Directory.java b/brut.j.dir/src/main/java/brut/directory/Directory.java index c5ece158..26a358d8 100644 --- a/brut.j.dir/src/main/java/brut/directory/Directory.java +++ b/brut.j.dir/src/main/java/brut/directory/Directory.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.Set; public interface Directory { + char separator = '/'; + Set getFiles(); Set getFiles(boolean recursive); @@ -65,6 +67,4 @@ public interface Directory { int getCompressionLevel(String fileName) throws DirectoryException; void close() throws IOException; - - char separator = '/'; } diff --git a/brut.j.dir/src/main/java/brut/directory/DirectoryException.java b/brut.j.dir/src/main/java/brut/directory/DirectoryException.java index 86db4992..3b735593 100644 --- a/brut.j.dir/src/main/java/brut/directory/DirectoryException.java +++ b/brut.j.dir/src/main/java/brut/directory/DirectoryException.java @@ -21,19 +21,19 @@ import brut.common.BrutException; public class DirectoryException extends BrutException { private static final long serialVersionUID = -8871963042836625387L; - public DirectoryException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public DirectoryException(String detailMessage) { - super(detailMessage); - } - - public DirectoryException(Throwable throwable) { - super(throwable); - } - public DirectoryException() { super(); } + + public DirectoryException(String message) { + super(message); + } + + public DirectoryException(Throwable cause) { + super(cause); + } + + public DirectoryException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/brut.j.dir/src/main/java/brut/directory/ExtFile.java b/brut.j.dir/src/main/java/brut/directory/ExtFile.java index 575ea9b3..9f3c253b 100644 --- a/brut.j.dir/src/main/java/brut/directory/ExtFile.java +++ b/brut.j.dir/src/main/java/brut/directory/ExtFile.java @@ -21,6 +21,8 @@ import java.io.IOException; import java.net.URI; public class ExtFile extends File { + private Directory mDirectory; + public ExtFile(File file) { super(file.getPath()); } @@ -58,5 +60,12 @@ public class ExtFile extends File { } } - private Directory mDirectory; + @Override + public boolean delete() { + try { + close(); + } catch (IOException ignored) {} + + return super.delete(); + } } diff --git a/brut.j.dir/src/main/java/brut/directory/FileDirectory.java b/brut.j.dir/src/main/java/brut/directory/FileDirectory.java index 8a9f9915..f6815b29 100644 --- a/brut.j.dir/src/main/java/brut/directory/FileDirectory.java +++ b/brut.j.dir/src/main/java/brut/directory/FileDirectory.java @@ -18,6 +18,9 @@ package brut.directory; import java.io.*; import java.net.URLDecoder; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -33,26 +36,23 @@ public class FileDirectory extends AbstractDirectory { } public FileDirectory(File dir) throws DirectoryException { - super(); - if (! dir.isDirectory()) { + if (!dir.isDirectory()) { throw new DirectoryException("file must be a directory: " + dir); } mDir = dir; } @Override - public long getSize(String fileName) - throws DirectoryException { + public long getSize(String fileName) throws DirectoryException { File file = new File(generatePath(fileName)); - if (! file.isFile()) { + if (!file.isFile()) { throw new DirectoryException("file must be a file: " + file); } return file.length(); } @Override - public long getCompressedSize(String fileName) - throws DirectoryException { + public long getCompressedSize(String fileName) throws DirectoryException { return getSize(fileName); } @@ -67,18 +67,20 @@ public class FileDirectory extends AbstractDirectory { @Override protected InputStream getFileInputLocal(String name) throws DirectoryException { try { - return new FileInputStream(generatePath(name)); - } catch (FileNotFoundException e) { - throw new DirectoryException(e); + File file = new File(generatePath(name)); + return Files.newInputStream(file.toPath()); + } catch (IOException ex) { + throw new DirectoryException(ex); } } @Override protected OutputStream getFileOutputLocal(String name) throws DirectoryException { try { - return new FileOutputStream(generatePath(name)); - } catch (FileNotFoundException e) { - throw new DirectoryException(e); + File file = new File(generatePath(name)); + return Files.newOutputStream(file.toPath()); + } catch (IOException ex) { + throw new DirectoryException(ex); } } @@ -94,8 +96,9 @@ public class FileDirectory extends AbstractDirectory { @Override protected void removeFileLocal(String name) { + File file = new File(generatePath(name)); //noinspection ResultOfMethodCallIgnored - new File(generatePath(name)).delete(); + file.delete(); } private String generatePath(String name) { @@ -107,11 +110,12 @@ public class FileDirectory extends AbstractDirectory { mDirs = new LinkedHashMap<>(); File[] files = getDir().listFiles(); + Arrays.sort(files, Comparator.comparing(f -> f.getName())); + for (File file : files) { if (file.isFile()) { mFiles.add(file.getName()); } else { - // IMPOSSIBLE_EXCEPTION try { mDirs.put(file.getName(), new FileDirectory(file)); } catch (DirectoryException ignored) {} diff --git a/brut.j.dir/src/main/java/brut/directory/PathAlreadyExists.java b/brut.j.dir/src/main/java/brut/directory/PathAlreadyExists.java index 57e7d102..48ceafc8 100644 --- a/brut.j.dir/src/main/java/brut/directory/PathAlreadyExists.java +++ b/brut.j.dir/src/main/java/brut/directory/PathAlreadyExists.java @@ -17,20 +17,21 @@ package brut.directory; public class PathAlreadyExists extends DirectoryException { - public PathAlreadyExists() { - } - - public PathAlreadyExists(Throwable throwable) { - super(throwable); - } - - public PathAlreadyExists(String detailMessage) { - super(detailMessage); - } - - public PathAlreadyExists(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - private static final long serialVersionUID = 3776428251424428904L; + + public PathAlreadyExists() { + super(); + } + + public PathAlreadyExists(String message) { + super(message); + } + + public PathAlreadyExists(Throwable cause) { + super(cause); + } + + public PathAlreadyExists(String message, Throwable cause) { + super(message, cause); + } } diff --git a/brut.j.dir/src/main/java/brut/directory/PathNotExist.java b/brut.j.dir/src/main/java/brut/directory/PathNotExist.java index f31c8ab2..e4607e5d 100644 --- a/brut.j.dir/src/main/java/brut/directory/PathNotExist.java +++ b/brut.j.dir/src/main/java/brut/directory/PathNotExist.java @@ -17,21 +17,21 @@ package brut.directory; public class PathNotExist extends DirectoryException { + private static final long serialVersionUID = -6949242015506342032L; + public PathNotExist() { super(); } - public PathNotExist(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); + public PathNotExist(String message) { + super(message); } - public PathNotExist(String detailMessage) { - super(detailMessage); + public PathNotExist(Throwable cause) { + super(cause); } - public PathNotExist(Throwable throwable) { - super(throwable); + public PathNotExist(String message, Throwable cause) { + super(message, cause); } - - private static final long serialVersionUID = -6949242015506342032L; } diff --git a/brut.j.dir/src/main/java/brut/directory/ZipRODirectory.java b/brut.j.dir/src/main/java/brut/directory/ZipRODirectory.java index b6072abe..44426771 100644 --- a/brut.j.dir/src/main/java/brut/directory/ZipRODirectory.java +++ b/brut.j.dir/src/main/java/brut/directory/ZipRODirectory.java @@ -48,17 +48,15 @@ public class ZipRODirectory extends AbstractDirectory { } public ZipRODirectory(File zipFile, String path) throws DirectoryException { - super(); try { mZipFile = new ZipFile(zipFile); - } catch (IOException e) { - throw new DirectoryException(e); + } catch (IOException ex) { + throw new DirectoryException(ex); } mPath = path; } public ZipRODirectory(ZipFile zipFile, String path) { - super(); mZipFile = zipFile; mPath = path; } @@ -73,8 +71,8 @@ public class ZipRODirectory extends AbstractDirectory { throws DirectoryException { try { return getZipFile().getInputStream(new ZipEntry(getPath() + name)); - } catch (IOException e) { - throw new PathNotExist(name, e); + } catch (IOException ex) { + throw new PathNotExist(name, ex); } } diff --git a/brut.j.dir/src/main/java/brut/directory/ZipUtils.java b/brut.j.dir/src/main/java/brut/directory/ZipUtils.java index 446dee64..7772f60d 100644 --- a/brut.j.dir/src/main/java/brut/directory/ZipUtils.java +++ b/brut.j.dir/src/main/java/brut/directory/ZipUtils.java @@ -17,6 +17,9 @@ package brut.directory; import brut.common.BrutException; +import brut.common.InvalidUnknownFileException; +import brut.common.RootUnknownFileException; +import brut.common.TraversalUnknownFileException; import brut.util.BrutIO; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; @@ -24,66 +27,89 @@ import org.apache.commons.io.IOUtils; import java.io.*; import java.nio.file.Files; import java.util.Collection; +import java.util.function.Predicate; +import java.util.logging.Logger; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -public class ZipUtils { - - private static Collection mDoNotCompress; +public final class ZipUtils { + private static final Logger LOGGER = Logger.getLogger(""); private ZipUtils() { // Private constructor for utility class } - public static void zipFolders(final File folder, final File zip, final File assets, final Collection doNotCompress) - throws BrutException, IOException { + public static void zipDir(File dir, ZipOutputStream out, Collection doNotCompress) + throws IOException { + zipDir(dir, null, out, doNotCompress); + } - mDoNotCompress = doNotCompress; - ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zip.toPath())); - zipFolders(folder, zipOutputStream); - - // We manually set the assets because we need to retain the folder structure - if (assets != null) { - processFolder(assets, zipOutputStream, assets.getPath().length() - 6); + public static void zipDir(File baseDir, String dirName, ZipOutputStream out, Collection doNotCompress) + throws IOException { + File dir; + if (dirName == null || dirName.isEmpty()) { + dir = baseDir; + } else { + dir = new File(baseDir, dirName); + } + if (!dir.isDirectory()) { + return; } - zipOutputStream.close(); - } - private static void zipFolders(final File folder, final ZipOutputStream outputStream) - throws BrutException, IOException { - processFolder(folder, outputStream, folder.getPath().length() + 1); - } + for (File file : dir.listFiles()) { + String fileName = baseDir.toURI().relativize(file.toURI()).getPath(); - private static void processFolder(final File folder, final ZipOutputStream zipOutputStream, final int prefixLength) - throws BrutException, IOException { - for (final File file : folder.listFiles()) { - if (file.isFile()) { - final String cleanedPath = BrutIO.sanitizeFilepath(folder, file.getPath().substring(prefixLength)); - final ZipEntry zipEntry = new ZipEntry(BrutIO.adaptSeparatorToUnix(cleanedPath)); - - // aapt binary by default takes in parameters via -0 arsc to list extensions that shouldn't be - // compressed. We will replicate that behavior - final String extension = FilenameUtils.getExtension(file.getAbsolutePath()); - if (mDoNotCompress != null && (mDoNotCompress.contains(extension) || mDoNotCompress.contains(zipEntry.getName()))) { - zipEntry.setMethod(ZipEntry.STORED); - zipEntry.setSize(file.length()); - BufferedInputStream unknownFile = new BufferedInputStream(Files.newInputStream(file.toPath())); - CRC32 crc = BrutIO.calculateCrc(unknownFile); - zipEntry.setCrc(crc.getValue()); - unknownFile.close(); - } else { - zipEntry.setMethod(ZipEntry.DEFLATED); - } - - zipOutputStream.putNextEntry(zipEntry); - try (FileInputStream inputStream = new FileInputStream(file)) { - IOUtils.copy(inputStream, zipOutputStream); - } - zipOutputStream.closeEntry(); - } else if (file.isDirectory()) { - processFolder(file, zipOutputStream, prefixLength); + if (file.isDirectory()) { + zipDir(baseDir, fileName, out, doNotCompress); + } else if (file.isFile()) { + zipFile(baseDir, fileName, out, doNotCompress != null && !doNotCompress.isEmpty() + ? entryName -> doNotCompress.contains(entryName) + || doNotCompress.contains(FilenameUtils.getExtension(entryName)) + : entryName -> false); } } } + + public static void zipFile(File baseDir, String fileName, ZipOutputStream out, boolean doNotCompress) + throws IOException { + zipFile(baseDir, fileName, out, entryName -> doNotCompress); + } + + private static void zipFile(File baseDir, String fileName, ZipOutputStream out, Predicate doNotCompress) + throws IOException { + try { + String validFileName = BrutIO.sanitizePath(baseDir, fileName); + if (validFileName.isEmpty()) { + return; + } + + File file = new File(baseDir, validFileName); + if (!file.isFile()) { + return; + } + + String entryName = BrutIO.adaptSeparatorToUnix(validFileName); + ZipEntry zipEntry = new ZipEntry(entryName); + + if (doNotCompress.test(entryName)) { + zipEntry.setMethod(ZipEntry.STORED); + zipEntry.setSize(file.length()); + try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) { + CRC32 crc = BrutIO.calculateCrc(in); + zipEntry.setCrc(crc.getValue()); + } + } else { + zipEntry.setMethod(ZipEntry.DEFLATED); + } + + out.putNextEntry(zipEntry); + try (InputStream in = Files.newInputStream(file.toPath())) { + IOUtils.copy(in, out); + } + out.closeEntry(); + } catch (RootUnknownFileException | InvalidUnknownFileException | TraversalUnknownFileException ex) { + LOGGER.warning(String.format("Skipping file %s (%s)", fileName, ex.getMessage())); + } + } } diff --git a/brut.j.util/src/main/java/brut/util/AaptManager.java b/brut.j.util/src/main/java/brut/util/AaptManager.java index b1375b15..8ba66b9f 100644 --- a/brut.j.util/src/main/java/brut/util/AaptManager.java +++ b/brut.j.util/src/main/java/brut/util/AaptManager.java @@ -17,104 +17,90 @@ package brut.util; import brut.common.BrutException; + import java.io.File; import java.util.ArrayList; import java.util.List; -public class AaptManager { +public final class AaptManager { - public static File getAapt2() throws BrutException { - return getAapt(2); + private AaptManager() { + // Private constructor for utility class } - public static File getAapt1() throws BrutException { - return getAapt(1); + public static String getAaptName(int version) { + switch (version) { + case 2: + return "aapt2"; + default: + return "aapt"; + } } - private static File getAapt(Integer version) throws BrutException { - File aaptBinary; - String aaptVersion = getAaptBinaryName(version); + public static File getAaptBinary(int version) throws BrutException { + String aaptName = getAaptName(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 + " binary is 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(AaptManager.class, aaptPath.toString()); + setAaptBinaryExecutable(aaptBinary); + return aaptBinary; } - public static int getAaptVersion(String aaptLocation) throws BrutException { - return getAaptVersion(new File(aaptLocation)); + public static int getAaptVersion(File aaptBinary) throws BrutException { + setAaptBinaryExecutable(aaptBinary); + + List cmd = new ArrayList<>(); + cmd.add(aaptBinary.getPath()); + cmd.add("version"); + + String versionStr = OS.execAndReturn(cmd.toArray(new String[0])); + if (versionStr == null) { + throw new BrutException("Could not execute aapt binary at location: " + aaptBinary.getPath()); + } + + return getAaptVersionFromString(versionStr); } - public static String getAaptBinaryName(Integer version) { - return "aapt" + (version == 2 ? "2" : ""); - } - - public static int getAppVersionFromString(String version) throws BrutException { - if (version.startsWith("Android Asset Packaging Tool (aapt) 2:")) { + public static int getAaptVersionFromString(String versionStr) throws BrutException { + if (versionStr.startsWith("Android Asset Packaging Tool (aapt) 2:")) { return 2; - } else if (version.startsWith("Android Asset Packaging Tool (aapt) 2.")) { + } else if (versionStr.startsWith("Android Asset Packaging Tool (aapt) 2.")) { return 2; // Prior to Android SDK 26.0.2 - } else if (version.startsWith("Android Asset Packaging Tool, v0.")) { + } else if (versionStr.startsWith("Android Asset Packaging Tool, v0.")) { return 1; } - throw new BrutException("aapt version could not be identified: " + version); + throw new BrutException("aapt version could not be identified: " + versionStr); } - public static int getAaptVersion(File aapt) throws BrutException { - if (!aapt.isFile()) { - throw new BrutException("Could not identify aapt binary as executable."); + private static void setAaptBinaryExecutable(File aaptBinary) throws BrutException { + if (!aaptBinary.isFile() || !aaptBinary.canRead()) { + throw new BrutException("Could not read aapt binary: " + aaptBinary.getPath()); } - //noinspection ResultOfMethodCallIgnored - aapt.setExecutable(true); - - List 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()); + if (!aaptBinary.setExecutable(true)) { + throw new BrutException("Could not set aapt binary as executable: " + aaptBinary.getPath()); } - - return getAppVersionFromString(version); } } diff --git a/brut.j.util/src/main/java/brut/util/BrutIO.java b/brut.j.util/src/main/java/brut/util/BrutIO.java index e56867dc..9d81e680 100644 --- a/brut.j.util/src/main/java/brut/util/BrutIO.java +++ b/brut.j.util/src/main/java/brut/util/BrutIO.java @@ -24,13 +24,22 @@ 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 final class BrutIO { + + private BrutIO() { + // Private constructor for utility class + } + + public static byte[] readAndClose(InputStream in) throws IOException { + try { + return IOUtils.toByteArray(in); + } finally { + IOUtils.closeQuietly(in); + } + } + + public static void copyAndClose(InputStream in, OutputStream out) throws IOException { try { IOUtils.copy(in, out); } finally { @@ -64,27 +73,29 @@ public class BrutIO { return modified; } - public static CRC32 calculateCrc(InputStream input) throws IOException { + public static CRC32 calculateCrc(InputStream in) throws IOException { CRC32 crc = new CRC32(); int bytesRead; byte[] buffer = new byte[8192]; - while((bytesRead = input.read(buffer)) != -1) { + while ((bytesRead = in.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 +105,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 +121,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); - } - } - } diff --git a/brut.j.util/src/main/java/brut/util/DataInputDelegate.java b/brut.j.util/src/main/java/brut/util/DataInputDelegate.java deleted file mode 100644 index 0d5021d8..00000000 --- a/brut.j.util/src/main/java/brut/util/DataInputDelegate.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2010 Ryszard Wiśniewski - * Copyright (C) 2010 Connor Tumbleson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package brut.util; - -import java.io.DataInput; -import java.io.IOException; - -public abstract class DataInputDelegate implements DataInput { - protected final DataInput mDelegate; - - public DataInputDelegate(DataInput delegate) { - this.mDelegate = delegate; - } - - public int skipBytes(int n) throws IOException { - return mDelegate.skipBytes(n); - } - - public int readUnsignedShort() throws IOException { - return mDelegate.readUnsignedShort(); - } - - public int readUnsignedByte() throws IOException { - return mDelegate.readUnsignedByte(); - } - - public String readUTF() throws IOException { - return mDelegate.readUTF(); - } - - public short readShort() throws IOException { - return mDelegate.readShort(); - } - - public long readLong() throws IOException { - return mDelegate.readLong(); - } - - public String readLine() throws IOException { - return mDelegate.readLine(); - } - - public int readInt() throws IOException { - return mDelegate.readInt(); - } - - public void readFully(byte[] b, int off, int len) throws IOException { - mDelegate.readFully(b, off, len); - } - - public void readFully(byte[] b) throws IOException { - mDelegate.readFully(b); - } - - public float readFloat() throws IOException { - return mDelegate.readFloat(); - } - - public double readDouble() throws IOException { - return mDelegate.readDouble(); - } - - public char readChar() throws IOException { - return mDelegate.readChar(); - } - - public byte readByte() throws IOException { - return mDelegate.readByte(); - } - - public boolean readBoolean() throws IOException { - return mDelegate.readBoolean(); - } -} diff --git a/brut.j.util/src/main/java/brut/util/Duo.java b/brut.j.util/src/main/java/brut/util/Duo.java index 60aedacd..0b4654be 100644 --- a/brut.j.util/src/main/java/brut/util/Duo.java +++ b/brut.j.util/src/main/java/brut/util/Duo.java @@ -23,11 +23,10 @@ public class Duo { public final T2 m2; public Duo(T1 t1, T2 t2) { - this.m1 = t1; - this.m2 = t2; + m1 = t1; + m2 = t2; } - @SuppressWarnings("unchecked") @Override public boolean equals(Object obj) { if (obj == null) { @@ -37,17 +36,17 @@ public class Duo { return false; } final Duo other = (Duo) obj; - if (!Objects.equals(this.m1, other.m1)) { + if (!Objects.equals(m1, other.m1)) { return false; } - return Objects.equals(this.m2, other.m2); + return Objects.equals(m2, other.m2); } @Override public int hashCode() { int hash = 3; - hash = 71 * hash + (this.m1 != null ? this.m1.hashCode() : 0); - hash = 71 * hash + (this.m2 != null ? this.m2.hashCode() : 0); + hash = 71 * hash + (m1 != null ? m1.hashCode() : 0); + hash = 71 * hash + (m2 != null ? m2.hashCode() : 0); return hash; } } diff --git a/brut.j.util/src/main/java/brut/util/ExtCountingDataInput.java b/brut.j.util/src/main/java/brut/util/ExtCountingDataInput.java deleted file mode 100644 index 95e13182..00000000 --- a/brut.j.util/src/main/java/brut/util/ExtCountingDataInput.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2010 Ryszard Wiśniewski - * Copyright (C) 2010 Connor Tumbleson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package brut.util; - -import org.apache.commons.io.input.CountingInputStream; -import com.google.common.io.LittleEndianDataInputStream; - -import java.io.DataInput; -import java.io.IOException; -import java.util.logging.Logger; - -public class ExtCountingDataInput extends ExtDataInput { - private final CountingInputStream mCountIn; - - public ExtCountingDataInput(LittleEndianDataInputStream in) { - this(new CountingInputStream(in)); - } - - public ExtCountingDataInput(CountingInputStream countIn) { - // We need to explicitly cast to DataInput as otherwise the constructor is ambiguous. - // We choose DataInput instead of InputStream as ExtDataInput wraps an InputStream in - // a DataInputStream which is big-endian and ignores the little-endian behavior. - super((DataInput) new LittleEndianDataInputStream(countIn)); - mCountIn = countIn; - } - - public int position() { - return mCountIn.getCount(); - } - - public int remaining() throws IOException { - return mCountIn.available(); - } - - public long skip(int bytes) throws IOException { - return mCountIn.skip(bytes); - } - - public int[] readSafeIntArray(int length, long maxPosition) throws IOException { - int[] array = new int[length]; - - for (int i = 0; i < length; i++) { - // #3236 - In some applications we have more strings than fit into the block. This function takes - // an expected max position and if we are past it, we return early during processing. - if (position() >= maxPosition) { - LOGGER.warning(String.format("Bad string block: string entry is at %d, past end at %d", - position(), maxPosition) - ); - return array; - } - - array[i] = readInt(); - } - return array; - } - - private static final Logger LOGGER = Logger.getLogger(ExtCountingDataInput.class.getName()); -} diff --git a/brut.j.util/src/main/java/brut/util/ExtDataInput.java b/brut.j.util/src/main/java/brut/util/ExtDataInput.java index d7c852d4..c857bc36 100644 --- a/brut.j.util/src/main/java/brut/util/ExtDataInput.java +++ b/brut.j.util/src/main/java/brut/util/ExtDataInput.java @@ -16,76 +16,23 @@ */ package brut.util; -import java.io.*; +import java.io.DataInput; +import java.io.IOException; -public class ExtDataInput extends DataInputDelegate { - public ExtDataInput(InputStream in) { - this((DataInput) new DataInputStream(in)); - } +public interface ExtDataInput extends DataInput { + public long position(); - public ExtDataInput(DataInput delegate) { - super(delegate); - } + public void skipShort() throws IOException; - public int[] readIntArray(int length) throws IOException { - int[] array = new int[length]; - for (int i = 0; i < length; i++) { - array[i] = readInt(); - } - return array; - } + public void skipInt() throws IOException; - public void skipInt() throws IOException { - skipBytes(4); - } + public void skipCheckShort(short expected) throws IOException; - public void skipShort() throws IOException { - skipBytes(2); - } + public void skipCheckByte(byte expected) throws IOException; - public void skipCheckShort(short expected) throws IOException { - short got = readShort(); - if (got != expected) { - throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); - } - } + public int[] readIntArray(int len) throws IOException; - public void skipCheckByte(byte expected) throws IOException { - byte got = readByte(); - if (got != expected) { - throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); - } - } + public int[] readSafeIntArray(int len, long maxPosition) throws IOException; - /** - * The general contract of DataInput doesn't guarantee all the bytes requested will be skipped - * and failure can occur for many reasons. We override this to try harder to skip all the bytes - * requested (this is similar to DataInputStream's wrapper). - */ - public final int skipBytes(int n) throws IOException { - int total = 0; - int cur; - - while ((total < n) && ((cur = super.skipBytes(n - total)) > 0)) { - total += cur; - } - - return total; - } - - public String readNullEndedString(int length, boolean fixed) throws IOException { - StringBuilder string = new StringBuilder(16); - while (length-- != 0) { - short ch = readShort(); - if (ch == 0) { - break; - } - string.append((char) ch); - } - if (fixed) { - skipBytes(length * 2); - } - - return string.toString(); - } + public String readNullEndedString(int len, boolean fixed) throws IOException; } diff --git a/brut.j.util/src/main/java/brut/util/ExtDataInputStream.java b/brut.j.util/src/main/java/brut/util/ExtDataInputStream.java new file mode 100644 index 00000000..872c78d8 --- /dev/null +++ b/brut.j.util/src/main/java/brut/util/ExtDataInputStream.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package brut.util; + +import com.google.common.io.CountingInputStream; +import com.google.common.io.LittleEndianDataInputStream; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.util.logging.Logger; + +public class ExtDataInputStream extends FilterInputStream implements ExtDataInput { + private static final Logger LOGGER = Logger.getLogger(ExtDataInputStream.class.getName()); + + private final DataInput mDelegate; + private final CountingInputStream mCountIn; + + public static ExtDataInputStream bigEndian(InputStream in) { + CountingInputStream countIn = new CountingInputStream(in); + DataInput delegate = new DataInputStream(countIn); + return new ExtDataInputStream(delegate, countIn); + } + + public static ExtDataInputStream littleEndian(InputStream in) { + CountingInputStream countIn = new CountingInputStream(in); + DataInput delegate = new LittleEndianDataInputStream(countIn); + return new ExtDataInputStream(delegate, countIn); + } + + private ExtDataInputStream(DataInput delegate, CountingInputStream countIn) { + super((InputStream) delegate); + mDelegate = delegate; + mCountIn = countIn; + } + + // ExtDataInput + + @Override + public long position() { + return mCountIn.getCount(); + } + + @Override + public void skipShort() throws IOException { + //noinspection ResultOfMethodCallIgnored + readShort(); + } + + @Override + public void skipInt() throws IOException { + //noinspection ResultOfMethodCallIgnored + readInt(); + } + + @Override + public void skipCheckShort(short expected) throws IOException { + short got = readShort(); + if (got != expected) { + throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); + } + } + + @Override + public void skipCheckByte(byte expected) throws IOException { + byte got = readByte(); + if (got != expected) { + throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); + } + } + + @Override + public int[] readIntArray(int len) throws IOException { + int[] arr = new int[len]; + for (int i = 0; i < len; i++) { + arr[i] = readInt(); + } + return arr; + } + + @Override + public int[] readSafeIntArray(int len, long maxPosition) throws IOException { + int[] arr = new int[len]; + for (int i = 0; i < len; i++) { + // #3236 - In some applications we have more strings than fit into the block. This function takes + // an expected max position and if we are past it, we return early during processing. + if (position() >= maxPosition) { + LOGGER.warning(String.format("Bad string block: string entry is at %d, past end at %d", + position(), maxPosition) + ); + return arr; + } + + arr[i] = readInt(); + } + return arr; + } + + @Override + public String readNullEndedString(int len, boolean fixed) throws IOException { + StringBuilder str = new StringBuilder(16); + while (len-- != 0) { + short ch = readShort(); + if (ch == 0) { + break; + } + str.append((char) ch); + } + if (fixed) { + skipBytes(len * 2); + } + return str.toString(); + } + + // DataInput + + @Override + public void readFully(byte[] b) throws IOException { + mDelegate.readFully(b); + } + + @Override + public void readFully(byte[] b, int off, int len) throws IOException { + mDelegate.readFully(b, off, len); + } + + /** + * The general contract of DataInput doesn't guarantee all the bytes requested will be skipped + * and failure can occur for many reasons. We override this to try harder to skip all the bytes + * requested (this is similar to DataInputStream's wrapper). + */ + @Override + public int skipBytes(int n) throws IOException { + int total = 0; + int cur; + while (total < n && (cur = mDelegate.skipBytes(n - total)) > 0) { + total += cur; + } + return total; + } + + @Override + public boolean readBoolean() throws IOException { + return mDelegate.readBoolean(); + } + + @Override + public byte readByte() throws IOException { + return mDelegate.readByte(); + } + + @Override + public int readUnsignedByte() throws IOException { + return mDelegate.readUnsignedByte(); + } + + @Override + public short readShort() throws IOException { + return mDelegate.readShort(); + } + + @Override + public int readUnsignedShort() throws IOException { + return mDelegate.readUnsignedShort(); + } + + @Override + public char readChar() throws IOException { + return mDelegate.readChar(); + } + + @Override + public int readInt() throws IOException { + return mDelegate.readInt(); + } + + @Override + public long readLong() throws IOException { + return mDelegate.readLong(); + } + + @Override + public float readFloat() throws IOException { + return mDelegate.readFloat(); + } + + @Override + public double readDouble() throws IOException { + return mDelegate.readDouble(); + } + + @Override + public String readLine() throws IOException { + return mDelegate.readLine(); + } + + @Override + public String readUTF() throws IOException { + return mDelegate.readUTF(); + } +} diff --git a/brut.j.util/src/main/java/brut/util/Jar.java b/brut.j.util/src/main/java/brut/util/Jar.java index f72f4316..ad00a912 100644 --- a/brut.j.util/src/main/java/brut/util/Jar.java +++ b/brut.j.util/src/main/java/brut/util/Jar.java @@ -17,6 +17,7 @@ package brut.util; import brut.common.BrutException; +import brut.util.BrutIO; import org.apache.commons.io.IOUtils; import java.io.*; @@ -25,41 +26,45 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; -public abstract class Jar { - private static final Map mExtracted = new HashMap<>(); +public final class Jar { + private static final Map sExtracted = new HashMap<>(); - public static File getResourceAsFile(String name, Class clazz) throws BrutException { - File file = mExtracted.get(name); + private Jar() { + // Private constructor for utility class + } + + public static File getResourceAsFile(Class clz, String name) throws BrutException { + File file = sExtracted.get(name); if (file == null) { - file = extractToTmp(name, clazz); - mExtracted.put(name, file); + file = extractToTmp(clz, name); + sExtracted.put(name, file); } return file; } - public static File extractToTmp(String resourcePath, Class clazz) throws BrutException { - return extractToTmp(resourcePath, "brut_util_Jar_", clazz); + public static File extractToTmp(Class clz, String resourcePath) throws BrutException { + return extractToTmp(clz, resourcePath, "brut_util_Jar_"); } - public static File extractToTmp(String resourcePath, String tmpPrefix, Class clazz) throws BrutException { + public static File extractToTmp(Class clz, String resourcePath, String tmpPrefix) throws BrutException { + InputStream in = null; try { - InputStream in = clazz.getResourceAsStream(resourcePath); + in = clz.getResourceAsStream(resourcePath); if (in == null) { throw new FileNotFoundException(resourcePath); } long suffix = ThreadLocalRandom.current().nextLong(); - suffix = suffix == Long.MIN_VALUE ? 0 : Math.abs(suffix); + suffix = suffix > Long.MIN_VALUE ? Math.abs(suffix) : 0; File fileOut = File.createTempFile(tmpPrefix, suffix + ".tmp"); fileOut.deleteOnExit(); - OutputStream out = Files.newOutputStream(fileOut.toPath()); - IOUtils.copy(in, out); - in.close(); - out.close(); + BrutIO.copyAndClose(in, Files.newOutputStream(fileOut.toPath())); return fileOut; } catch (IOException ex) { throw new BrutException("Could not extract resource: " + resourcePath, ex); + } finally { + IOUtils.closeQuietly(in); } } } diff --git a/brut.j.util/src/main/java/brut/util/OS.java b/brut.j.util/src/main/java/brut/util/OS.java index 58f3c531..49d294a0 100644 --- a/brut.j.util/src/main/java/brut/util/OS.java +++ b/brut.j.util/src/main/java/brut/util/OS.java @@ -17,7 +17,7 @@ package brut.util; import brut.common.BrutException; -import org.apache.commons.io.IOUtils; +import brut.util.BrutIO; import java.io.*; import java.nio.file.Files; @@ -27,10 +27,13 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; -public class OS { - +public final class OS { private static final Logger LOGGER = Logger.getLogger(""); + private OS() { + // Private constructor for utility class + } + public static void rmdir(File dir) throws BrutException { if (! dir.exists()) { return; @@ -53,7 +56,7 @@ public class OS { } public static void rmfile(String file) { - File del = new File(file); + File del = new File(file); //noinspection ResultOfMethodCallIgnored del.delete(); } @@ -77,11 +80,7 @@ public class OS { continue; } try { - try (InputStream in = Files.newInputStream(file.toPath())) { - try (OutputStream out = Files.newOutputStream(destFile.toPath())) { - IOUtils.copy(in, out); - } - } + BrutIO.copyAndClose(Files.newInputStream(file.toPath()), Files.newOutputStream(destFile.toPath())); } catch (IOException ex) { throw new BrutException("Could not copy file: " + file, ex); } @@ -126,7 +125,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; } } @@ -147,19 +146,20 @@ public class OS { } } - static class StreamForwarder extends Thread { + private static class StreamForwarder extends Thread { + private final InputStream mIn; + private final String mType; - StreamForwarder(InputStream is, String type) { - mIn = is; + public StreamForwarder(InputStream in, String type) { + mIn = in; mType = type; } @Override public void run() { - try { - BufferedReader br = new BufferedReader(new InputStreamReader(mIn)); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(mIn))) { String line; - while ((line = br.readLine()) != null) { + while ((line = reader.readLine()) != null) { if (mType.equals("OUTPUT")) { LOGGER.info(line); } else { @@ -170,32 +170,29 @@ public class OS { ex.printStackTrace(); } } - - private final InputStream mIn; - private final String mType; } - static class StreamCollector implements Runnable { - private final StringBuilder buffer = new StringBuilder(); - private final InputStream inputStream; + private static class StreamCollector implements Runnable { + private final InputStream mIn; + private final StringBuilder mBuffer; - public StreamCollector(InputStream inputStream) { - super(); - this.inputStream = inputStream; + public StreamCollector(InputStream in) { + mIn = in; + mBuffer = new StringBuilder(); } @Override public void run() { - String line; - try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(mIn))) { + String line; while ((line = reader.readLine()) != null) { - buffer.append(line).append('\n'); + mBuffer.append(line).append('\n'); } } catch (IOException ignored) {} } public String get() { - return buffer.toString(); + return mBuffer.toString(); } } } diff --git a/brut.j.util/src/main/java/brut/util/OSDetection.java b/brut.j.util/src/main/java/brut/util/OSDetection.java index fcb31ef1..57bf79b8 100644 --- a/brut.j.util/src/main/java/brut/util/OSDetection.java +++ b/brut.j.util/src/main/java/brut/util/OSDetection.java @@ -16,10 +16,14 @@ */ package brut.util; -public class OSDetection { +public final class OSDetection { private static final String OS = System.getProperty("os.name").toLowerCase(); private static final String BIT = System.getProperty("sun.arch.data.model"); + private OSDetection() { + // Private constructor for utility class + } + public static boolean isWindows() { return (OS.contains("win")); } diff --git a/brut.j.xml/build.gradle.kts b/brut.j.xml/build.gradle.kts new file mode 100644 index 00000000..96159031 --- /dev/null +++ b/brut.j.xml/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + api(libs.xmlpull) +} diff --git a/brut.j.xml/src/main/java/brut/xmlpull/MXSerializer.java b/brut.j.xml/src/main/java/brut/xmlpull/MXSerializer.java new file mode 100644 index 00000000..68096dca --- /dev/null +++ b/brut.j.xml/src/main/java/brut/xmlpull/MXSerializer.java @@ -0,0 +1,1112 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package brut.xmlpull; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.*; +import java.util.*; + +/** + * Implementation of XmlSerializer interface from XmlPull V1 API. This + * implementation is optimized for performance and low memory footprint. + * + *

+ * Implemented features: + *

    + *
  • FEATURE_ATTR_VALUE_NO_ESCAPE + *
  • FEATURE_NAMES_INTERNED - when enabled all returned names (namespaces, + * prefixes) will be interned and it is required that all names passed as + * arguments MUST be interned + *
+ *

+ * Implemented properties: + *

    + *
  • PROPERTY_DEFAULT_ENCODING + *
  • PROPERTY_INDENTATION + *
  • PROPERTY_LINE_SEPARATOR + *
  • PROPERTY_LOCATION + *
+ * + */ +public class MXSerializer implements XmlSerializer { + public static final String FEATURE_ATTR_VALUE_NO_ESCAPE = "http://xmlpull.org/v1/doc/features.html#attr-value-no-escape"; + public static final String FEATURE_NAMES_INTERNED = "http://xmlpull.org/v1/doc/features.html#names-interned"; + public static final String PROPERTY_DEFAULT_ENCODING = "http://xmlpull.org/v1/doc/properties.html#default-encoding"; + public static final String PROPERTY_INDENTATION = "http://xmlpull.org/v1/doc/properties.html#indentation"; + public static final String PROPERTY_LINE_SEPARATOR = "http://xmlpull.org/v1/doc/properties.html#line-separator"; + public static final String PROPERTY_LOCATION = "http://xmlpull.org/v1/doc/properties.html#location"; + + private static final boolean TRACE_SIZING = false; + private static final boolean TRACE_ESCAPING = false; + private static final String XML_URI = "http://www.w3.org/XML/1998/namespace"; + private static final String XMLNS_URI = "http://www.w3.org/2000/xmlns/"; + + // properties/features + private boolean namesInterned; + private boolean attrValueNoEscape; + private String defaultEncoding; + private String indentationString; + private String lineSeparator; + + private String location; + private Writer writer; + + private int autoDeclaredPrefixes; + + private int depth = 0; + + // element stack + private String[] elNamespace = new String[2]; + private String[] elName = new String[elNamespace.length]; + private String[] elPrefix = new String[elNamespace.length]; + private int[] elNamespaceCount = new int[elNamespace.length]; + + // namespace stack + private int namespaceEnd = 0; + private String[] namespacePrefix = new String[8]; + private String[] namespaceUri = new String[namespacePrefix.length]; + + private boolean finished; + private boolean pastRoot; + private boolean setPrefixCalled; + private boolean startTagIncomplete; + + private boolean doIndent; + private boolean seenTag; + + private boolean seenBracket; + private boolean seenBracketBracket; + + private static final String[] precomputedPrefixes; + static { + precomputedPrefixes = new String[32]; // arbitrary number ... + for (int i = 0; i < precomputedPrefixes.length; i++) { + precomputedPrefixes[i] = ("n" + i).intern(); + } + } + + private final boolean checkNamesInterned = false; + + private void checkInterning(String name) { + if (namesInterned && !Objects.equals(name, name.intern())) { + throw new IllegalArgumentException("all names passed as arguments must be interned" + + "when NAMES INTERNED feature is enabled"); + } + } + + private String getLocation() { + return location != null ? " @" + location : ""; + } + + private void ensureElementsCapacity() { + int elStackSize = elName.length; + int newSize = (depth >= 7 ? 2 * depth : 8) + 2; + + if (TRACE_SIZING) { + System.err.println(getClass().getName() + " elStackSize " + + elStackSize + " ==> " + newSize); + } + boolean needsCopying = elStackSize > 0; + String[] arr; + // reuse arr local variable slot + arr = new String[newSize]; + if (needsCopying) { + System.arraycopy(elName, 0, arr, 0, elStackSize); + } + elName = arr; + + arr = new String[newSize]; + if (needsCopying) { + System.arraycopy(elPrefix, 0, arr, 0, elStackSize); + } + elPrefix = arr; + + arr = new String[newSize]; + if (needsCopying) { + System.arraycopy(elNamespace, 0, arr, 0, elStackSize); + } + elNamespace = arr; + + int[] iarr = new int[newSize]; + if (needsCopying) { + System.arraycopy(elNamespaceCount, 0, iarr, 0, elStackSize); + } else { + // special initialization + iarr[0] = 0; + } + elNamespaceCount = iarr; + } + + private void ensureNamespacesCapacity() { + int newSize = namespaceEnd > 7 ? 2 * namespaceEnd : 8; + if (TRACE_SIZING) { + System.err.println(getClass().getName() + " namespaceSize " + namespacePrefix.length + " ==> " + newSize); + } + String[] newNamespacePrefix = new String[newSize]; + String[] newNamespaceUri = new String[newSize]; + if (namespacePrefix != null) { + System.arraycopy(namespacePrefix, 0, newNamespacePrefix, 0, namespaceEnd); + System.arraycopy(namespaceUri, 0, newNamespaceUri, 0, namespaceEnd); + } + namespacePrefix = newNamespacePrefix; + namespaceUri = newNamespaceUri; + } + + // use buffer to optimize writing + private static final int BUFFER_LEN = 8192; + private final char[] buffer = new char[BUFFER_LEN]; + private int bufidx; + + private void flushBuffer() throws IOException { + if (bufidx > 0) { + writer.write(buffer, 0, bufidx); + writer.flush(); + bufidx = 0; + } + } + + private void write(char ch) throws IOException { + if (bufidx >= BUFFER_LEN) { + flushBuffer(); + } + buffer[bufidx++] = ch; + } + + private void write(char[] buf, int i, int length) throws IOException { + while (length > 0) { + if (bufidx == BUFFER_LEN) { + flushBuffer(); + } + int batch = BUFFER_LEN - bufidx; + if (batch > length) { + batch = length; + } + System.arraycopy(buf, i, buffer, bufidx, batch); + i += batch; + length -= batch; + bufidx += batch; + } + } + + private void write(String str) throws IOException { + write(str, 0, str.length()); + } + + private void write(String str, int i, int length) throws IOException { + while (length > 0) { + if (bufidx == BUFFER_LEN) { + flushBuffer(); + } + int batch = BUFFER_LEN - bufidx; + if (batch > length) { + batch = length; + } + str.getChars(i, i + batch, buffer, bufidx); + i += batch; + length -= batch; + bufidx += batch; + } + } + + // precomputed variables to simplify writing indentation + private static final int MAX_INDENT = 65; + private int offsetNewLine; + private int indentationJump; + private char[] indentationBuf; + private int maxIndentLevel; + private boolean writeLineSeparator; // should end-of-line be written + private boolean writeIndentation; // is indentation used? + + /** + * For maximum efficiency when writing indents the required output is + * pre-computed This is internal function that recomputes buffer after user + * requested changes. + */ + private void rebuildIndentationBuf() { + if (!doIndent) { + return; + } + int bufSize = 0; + offsetNewLine = 0; + if (writeLineSeparator) { + offsetNewLine = lineSeparator.length(); + bufSize += offsetNewLine; + } + maxIndentLevel = 0; + if (writeIndentation) { + indentationJump = indentationString.length(); + maxIndentLevel = MAX_INDENT / indentationJump; + bufSize += maxIndentLevel * indentationJump; + } + if (indentationBuf == null || indentationBuf.length < bufSize) { + indentationBuf = new char[bufSize + 8]; + } + int bufPos = 0; + if (writeLineSeparator) { + for (int i = 0; i < lineSeparator.length(); i++) { + indentationBuf[bufPos++] = lineSeparator.charAt(i); + } + } + if (writeIndentation) { + for (int i = 0; i < maxIndentLevel; i++) { + for (int j = 0; j < indentationString.length(); j++) { + indentationBuf[bufPos++] = indentationString.charAt(j); + } + } + } + } + + private void writeIndent() throws IOException { + int start = writeLineSeparator ? 0 : offsetNewLine; + int level = Math.min(depth, maxIndentLevel); + + write(indentationBuf, start, ((level - 1) * indentationJump) + offsetNewLine); + } + + // --- public API methods + + @Override + public void setFeature(String name, boolean state) + throws IllegalArgumentException, IllegalStateException { + if (name == null) { + throw new IllegalArgumentException("feature name can not be null"); + } + switch (name) { + case FEATURE_ATTR_VALUE_NO_ESCAPE: + attrValueNoEscape = state; + break; + case FEATURE_NAMES_INTERNED: + namesInterned = state; + break; + default: + throw new IllegalStateException("unsupported feature: " + name); + } + } + + @Override + public boolean getFeature(String name) throws IllegalArgumentException { + if (name == null) { + throw new IllegalArgumentException("feature name can not be null"); + } + switch (name) { + case FEATURE_ATTR_VALUE_NO_ESCAPE: + return attrValueNoEscape; + case FEATURE_NAMES_INTERNED: + return namesInterned; + default: + return false; + } + } + + @Override + public void setProperty(String name, Object value) + throws IllegalArgumentException, IllegalStateException { + if (name == null) { + throw new IllegalArgumentException("property name can not be null"); + } + switch (name) { + case PROPERTY_DEFAULT_ENCODING: + defaultEncoding = (String) value; + break; + case PROPERTY_INDENTATION: + indentationString = (String) value; + break; + case PROPERTY_LINE_SEPARATOR: + lineSeparator = (String) value; + break; + case PROPERTY_LOCATION: + location = (String) value; + break; + default: + throw new IllegalStateException("unsupported property: " + name); + } + writeLineSeparator = lineSeparator != null && !lineSeparator.isEmpty(); + writeIndentation = indentationString != null && !indentationString.isEmpty(); + // optimize - do not write when nothing to write ... + doIndent = indentationString != null && (writeLineSeparator || writeIndentation); + // NOTE: when indentationString == null there is no indentation + // (even though writeLineSeparator may be true ...) + rebuildIndentationBuf(); + seenTag = false; // for consistency + } + + @Override + public Object getProperty(String name) throws IllegalArgumentException { + if (name == null) { + throw new IllegalArgumentException("property name can not be null"); + } + switch (name) { + case PROPERTY_DEFAULT_ENCODING: + return defaultEncoding; + case PROPERTY_INDENTATION: + return indentationString; + case PROPERTY_LINE_SEPARATOR: + return lineSeparator; + case PROPERTY_LOCATION: + return location; + default: + return null; + } + } + + @Override + public void setOutput(Writer writer) { + this.writer = writer; + + // reset state + location = null; + autoDeclaredPrefixes = 0; + depth = 0; + + // nullify references on all levels to allow it to be GCed + for (int i = 0; i < elNamespaceCount.length; i++) { + elName[i] = null; + elPrefix[i] = null; + elNamespace[i] = null; + elNamespaceCount[i] = 2; + } + + namespaceEnd = 0; + + // TODO: how to prevent from reporting this namespace? + // this is special namespace declared for consistency with XML infoset + namespacePrefix[namespaceEnd] = "xmlns"; + namespaceUri[namespaceEnd] = XMLNS_URI; + ++namespaceEnd; + + namespacePrefix[namespaceEnd] = "xml"; + namespaceUri[namespaceEnd] = XML_URI; + ++namespaceEnd; + + finished = false; + pastRoot = false; + setPrefixCalled = false; + startTagIncomplete = false; + seenTag = false; + + seenBracket = false; + seenBracketBracket = false; + } + + @Override + public void setOutput(OutputStream os, String encoding) throws IOException { + if (os == null) { + throw new IllegalArgumentException("output stream can not be null"); + } + if (encoding == null) { + encoding = defaultEncoding; + } + setOutput(encoding != null + ? new OutputStreamWriter(os, encoding) + : new OutputStreamWriter(os)); + } + + @Override + public void startDocument(String encoding, Boolean standalone) throws IOException { + write(""); + if (writeLineSeparator) { + write(lineSeparator); + } + } + + @Override + public void endDocument() throws IOException { + // close all unclosed tag; + while (depth > 0) { + endTag(elNamespace[depth], elName[depth]); + } + if (writeLineSeparator) { + write(lineSeparator); + } + flushBuffer(); + finished = pastRoot = startTagIncomplete = true; + } + + @Override + public void setPrefix(String prefix, String namespace) throws IOException { + if (startTagIncomplete) { + closeStartTag(); + } + if (prefix == null) { + prefix = ""; + } + if (!namesInterned) { + prefix = prefix.intern(); // will throw NPE if prefix==null + } else if (checkNamesInterned) { + checkInterning(prefix); + } else if (prefix == null) { + throw new IllegalArgumentException("prefix must be not null" + getLocation()); + } + + if (!namesInterned) { + namespace = namespace.intern(); + } else if (checkNamesInterned) { + checkInterning(namespace); + } else if (namespace == null) { + throw new IllegalArgumentException("namespace must be not null" + getLocation()); + } + + if (namespaceEnd >= namespacePrefix.length) { + ensureNamespacesCapacity(); + } + namespacePrefix[namespaceEnd] = prefix; + namespaceUri[namespaceEnd] = namespace; + ++namespaceEnd; + setPrefixCalled = true; + } + + @Override + public String getPrefix(String namespace, boolean generatePrefix) { + return getPrefix(namespace, generatePrefix, false); + } + + private String getPrefix(String namespace, boolean generatePrefix, boolean nonEmpty) { + if (!namesInterned) { + // when String is interned we can do much faster namespace stack lookups ... + namespace = namespace.intern(); + } else if (checkNamesInterned) { + checkInterning(namespace); + } + if (namespace == null) { + throw new IllegalArgumentException("namespace must be not null" + getLocation()); + } else if (namespace.isEmpty()) { + throw new IllegalArgumentException("default namespace cannot have prefix" + getLocation()); + } + + // first check if namespace is already in scope + for (int i = namespaceEnd - 1; i >= 0; --i) { + if (namespace.equals(namespaceUri[i])) { + String prefix = namespacePrefix[i]; + if (nonEmpty && prefix.isEmpty()) { + continue; + } + + return prefix; + } + } + + // so not found it ... + if (!generatePrefix) { + return null; + } + return generatePrefix(namespace); + } + + @Override + public int getDepth() { + return depth; + } + + @Override + public String getNamespace() { + return elNamespace[depth]; + } + + @Override + public String getName() { + return elName[depth]; + } + + @Override + public XmlSerializer startTag(String namespace, String name) throws IOException { + if (startTagIncomplete) { + closeStartTag(); + } + seenBracket = seenBracketBracket = false; + ++depth; + if (doIndent && depth > 0 && seenTag) { + writeIndent(); + } + seenTag = true; + setPrefixCalled = false; + startTagIncomplete = true; + if ((depth + 1) >= elName.length) { + ensureElementsCapacity(); + } + + if (checkNamesInterned && namesInterned) { + checkInterning(namespace); + } + + elNamespace[depth] = (namesInterned || namespace == null) ? namespace : namespace.intern(); + if (checkNamesInterned && namesInterned) { + checkInterning(name); + } + + elName[depth] = (namesInterned || name == null) ? name : name.intern(); + if (writer == null) { + throw new IllegalStateException("setOutput() must called set before serialization can start"); + } + write('<'); + if (namespace != null) { + if (!namespace.isEmpty()) { + // in future make this algo a feature on serializer + String prefix = null; + if (depth > 0 && (namespaceEnd - elNamespaceCount[depth - 1]) == 1) { + // if only one prefix was declared un-declare it if the + // prefix is already declared on parent el with the same URI + String uri = namespaceUri[namespaceEnd - 1]; + if (uri == namespace || uri.equals(namespace)) { + String elPfx = namespacePrefix[namespaceEnd - 1]; + for (int pos = elNamespaceCount[depth - 1] - 1; pos >= 2; --pos) { + String pf = namespacePrefix[pos]; + if (pf == elPfx || pf.equals(elPfx)) { + String n = namespaceUri[pos]; + if (n == uri || n.equals(uri)) { + --namespaceEnd; // un-declare namespace: this is kludge! + prefix = elPfx; + } + break; + } + } + } + } + if (prefix == null) { + prefix = getPrefix(namespace, true, false); + } + // make sure that default ("") namespace to not print ":" + if (!prefix.isEmpty()) { + elPrefix[depth] = prefix; + write(prefix); + write(':'); + } else { + elPrefix[depth] = ""; + } + } else { + // make sure that default namespace can be declared + for (int i = namespaceEnd - 1; i >= 0; --i) { + if (namespacePrefix[i] == "") { + String uri = namespaceUri[i]; + if (uri == null) { + setPrefix("", ""); + } else if (!uri.isEmpty()) { + throw new IllegalStateException("start tag can not be written in empty default namespace " + + "as default namespace is currently bound to '" + + uri + "'" + getLocation()); + } + break; + } + } + elPrefix[depth] = ""; + } + } else { + elPrefix[depth] = ""; + } + write(name); + return this; + } + + private void closeStartTag() throws IOException { + if (finished) { + throw new IllegalArgumentException("trying to write past already finished output" + getLocation()); + } + if (seenBracket) { + seenBracket = seenBracketBracket = false; + } + if (startTagIncomplete || setPrefixCalled) { + if (setPrefixCalled) { + throw new IllegalArgumentException("startTag() must be called immediately after setPrefix()" + getLocation()); + } + if (!startTagIncomplete) { + throw new IllegalArgumentException("trying to close start tag that is not opened" + getLocation()); + } + + // write all namespace declarations! + writeNamespaceDeclarations(); + write('>'); + elNamespaceCount[depth] = namespaceEnd; + startTagIncomplete = false; + } + } + + @Override + public XmlSerializer attribute(String namespace, String name, String value) throws IOException { + if (!startTagIncomplete) { + throw new IllegalArgumentException("startTag() must be called before attribute()" + getLocation()); + } + write(' '); + if (namespace != null && !namespace.isEmpty()) { + if (!namesInterned) { + namespace = namespace.intern(); + } else if (checkNamesInterned) { + checkInterning(namespace); + } + String prefix = getPrefix(namespace, false, true); + if (prefix == null) { + // needs to declare prefix to hold default namespace + // NOTE: attributes such as a='b' are in NO namespace + prefix = generatePrefix(namespace); + } + write(prefix); + write(':'); + } + write(name); + write("=\""); + writeAttributeValue(value); + write('"'); + return this; + } + + @Override + public XmlSerializer endTag(String namespace, String name) throws IOException { + seenBracket = seenBracketBracket = false; + if (namespace != null) { + if (!namesInterned) { + namespace = namespace.intern(); + } else if (checkNamesInterned) { + checkInterning(namespace); + } + } + + if (name == null) { + throw new IllegalArgumentException("end tag name can not be null" + getLocation()); + } + if (checkNamesInterned && namesInterned) { + checkInterning(name); + } + if (startTagIncomplete) { + writeNamespaceDeclarations(); + write(" />"); // space is added to make it easier to work in XHTML!!! + } else { + if (doIndent && seenTag) { + writeIndent(); + } + write("'); + } + --depth; + namespaceEnd = elNamespaceCount[depth]; + startTagIncomplete = false; + seenTag = true; + return this; + } + + @Override + public XmlSerializer text(String text) throws IOException { + if (startTagIncomplete || setPrefixCalled) { + closeStartTag(); + } + if (doIndent && seenTag) { + seenTag = false; + } + writeElementContent(text); + return this; + } + + @Override + public XmlSerializer text(char[] buf, int start, int len) throws IOException { + if (startTagIncomplete || setPrefixCalled) { + closeStartTag(); + } + if (doIndent && seenTag) { + seenTag = false; + } + writeElementContent(buf, start, len); + return this; + } + + @Override + public void cdsect(String text) throws IOException { + if (startTagIncomplete || setPrefixCalled || seenBracket) { + closeStartTag(); + } + if (doIndent && seenTag) { + seenTag = false; + } + write(""); + } + + @Override + public void entityRef(String text) throws IOException { + if (startTagIncomplete || setPrefixCalled || seenBracket) { + closeStartTag(); + } + if (doIndent && seenTag) { + seenTag = false; + } + write('&'); + write(text); + write(';'); + } + + @Override + public void processingInstruction(String text) throws IOException { + if (startTagIncomplete || setPrefixCalled || seenBracket) { + closeStartTag(); + } + if (doIndent && seenTag) { + seenTag = false; + } + write(""); + } + + @Override + public void comment(String text) throws IOException { + if (startTagIncomplete || setPrefixCalled || seenBracket) { + closeStartTag(); + } + if (doIndent && seenTag) { + seenTag = false; + } + write(""); + } + + @Override + public void docdecl(String text) throws IOException { + if (startTagIncomplete || setPrefixCalled || seenBracket) { + closeStartTag(); + } + if (doIndent && seenTag) { + seenTag = false; + } + write(""); + } + + @Override + public void ignorableWhitespace(String text) throws IOException { + if (startTagIncomplete || setPrefixCalled || seenBracket) { + closeStartTag(); + } + if (doIndent && seenTag) { + seenTag = false; + } + if (text.isEmpty()) { + throw new IllegalArgumentException("empty string is not allowed for ignorable whitespace" + getLocation()); + } + write(text); + } + + @Override + public void flush() throws IOException { + if (!finished && startTagIncomplete) { + closeStartTag(); + } + flushBuffer(); + } + + // --- utility methods + + private String generatePrefix(String namespace) { + ++autoDeclaredPrefixes; + // fast lookup uses table that was pre-initialized in static{} .... + String prefix = autoDeclaredPrefixes < precomputedPrefixes.length + ? precomputedPrefixes[autoDeclaredPrefixes] + : ("n" + autoDeclaredPrefixes).intern(); + + // declare prefix + if (namespaceEnd >= namespacePrefix.length) { + ensureNamespacesCapacity(); + } + namespacePrefix[namespaceEnd] = prefix; + namespaceUri[namespaceEnd] = namespace; + ++namespaceEnd; + + return prefix; + } + + private void writeNamespaceDeclarations() throws IOException { + Set uniqueNamespaces = new HashSet<>(); + for (int i = elNamespaceCount[depth - 1]; i < namespaceEnd; i++) { + String prefix = namespacePrefix[i]; + String uri = namespaceUri[i]; + + // Some applications as seen in #2664 have duplicated namespaces. + // AOSP doesn't care, but the parser does. So we filter them writer. + if (uniqueNamespaces.contains(prefix + uri)) { + continue; + } + + if (doIndent && uri.length() > 40) { + writeIndent(); + write(' '); + } + write(" xmlns"); + if (prefix != "") { + write(':'); + write(prefix); + } + write("=\""); + writeAttributeValue(uri); + write('"'); + + uniqueNamespaces.add(prefix + uri); + } + } + + private void writeAttributeValue(String value) throws IOException { + if (attrValueNoEscape) { + write(value); + return; + } + // .[&, < and " escaped], + int pos = 0; + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if (ch == '&') { + if (i > pos) { + write(value.substring(pos, i)); + } + write("&"); + pos = i + 1; + } + if (ch == '<') { + if (i > pos) { + write(value.substring(pos, i)); + } + write("<"); + pos = i + 1; + } else if (ch == '"') { + if (i > pos) { + write(value.substring(pos, i)); + } + write("""); + pos = i + 1; + } else if (ch < 32) { + // in XML 1.0 only legal character are #x9 | #xA | #xD + // and they must be escaped otherwise in attribute value they + // are normalized to spaces + if (ch == 13 || ch == 10 || ch == 9) { + if (i > pos) { + write(value.substring(pos, i)); + } + write("&#"); + write(Integer.toString(ch)); + write(';'); + pos = i + 1; + } else { + if (TRACE_ESCAPING) { + System.err.println(getClass().getName() + " DEBUG ATTR value.len=" + value.length() + + " " + printable(value)); + } + throw new IllegalStateException( + "character " + printable(ch) + " (" + Integer.toString(ch) + ") is not allowed in output" + + getLocation() + " (attr value=" + + printable(value) + ")"); + } + } + } + write(pos > 0 ? value.substring(pos) : value); + } + + private void writeElementContent(String text) throws IOException { + // For some reason, some non-empty, empty characters are surviving this far and getting filtered writer + // So we are left with null, which causes an NPE + if (text == null) { + return; + } + + // escape '<', '&', ']]>', <32 if necessary + int pos = 0; + for (int i = 0; i < text.length(); i++) { + // TODO: check if doing char[] text.getChars() would be faster than + // getCharAt(i) ... + char ch = text.charAt(i); + if (ch == ']') { + if (seenBracket) { + seenBracketBracket = true; + } else { + seenBracket = true; + } + } else { + if (ch == '&') { + if (!(i < text.length() - 3 && text.charAt(i+1) == 'l' + && text.charAt(i+2) == 't' && text.charAt(i+3) == ';')) { + if (i > pos) { + write(text.substring(pos, i)); + } + write("&"); + pos = i + 1; + } + } else if (ch == '<') { + if (i > pos) { + write(text.substring(pos, i)); + } + write("<"); + pos = i + 1; + } else if (seenBracketBracket && ch == '>') { + if (i > pos) { + write(text.substring(pos, i)); + } + write(">"); + pos = i + 1; + } else if (ch < 32) { + // in XML 1.0 only legal character are #x9 | #xA | #xD + if (ch == 9 || ch == 10 || ch == 13) { + // pass through + } else { + if (TRACE_ESCAPING) { + System.err.println(getClass().getName() + " DEBUG TEXT value.len=" + text.length() + + " " + printable(text)); + } + throw new IllegalStateException("character " + Integer.toString(ch) + + " is not allowed in output" + getLocation() + + " (text value=" + printable(text) + ")"); + } + } + if (seenBracket) { + seenBracketBracket = seenBracket = false; + } + } + } + write(pos > 0 ? text.substring(pos) : text); + } + + private void writeElementContent(char[] buf, int off, int len) throws IOException { + // escape '<', '&', ']]>' + int end = off + len; + int pos = off; + for (int i = off; i < end; i++) { + char ch = buf[i]; + if (ch == ']') { + if (seenBracket) { + seenBracketBracket = true; + } else { + seenBracket = true; + } + } else { + if (ch == '&') { + if (i > pos) { + write(buf, pos, i - pos); + } + write("&"); + pos = i + 1; + } else if (ch == '<') { + if (i > pos) { + write(buf, pos, i - pos); + } + write("<"); + pos = i + 1; + + } else if (seenBracketBracket && ch == '>') { + if (i > pos) { + write(buf, pos, i - pos); + } + write(">"); + pos = i + 1; + } else if (ch < 32) { + // in XML 1.0 only legal character are #x9 | #xA | #xD + if (ch == 9 || ch == 10 || ch == 13) { + // pass through + } else { + if (TRACE_ESCAPING) { + System.err.println(getClass().getName() + " DEBUG TEXT value.len=" + len + " " + + printable(new String(buf, off, len))); + } + throw new IllegalStateException("character " + + printable(ch) + " (" + Integer.toString(ch) + + ") is not allowed in output" + getLocation()); + } + } + if (seenBracket) { + seenBracketBracket = seenBracket = false; + } + } + } + if (end > pos) { + write(buf, pos, end - pos); + } + } + + private static String printable(String str) { + if (str == null) { + return "null"; + } + StringBuffer retval = new StringBuffer(str.length() + 16); + retval.append("'"); + for (int i = 0; i < str.length(); i++) { + addPrintable(retval, str.charAt(i)); + } + retval.append("'"); + return retval.toString(); + } + + private static String printable(char ch) { + StringBuffer retval = new StringBuffer(); + addPrintable(retval, ch); + return retval.toString(); + } + + private static void addPrintable(StringBuffer retval, char ch) { + switch (ch) { + case '\b': + retval.append("\\b"); + break; + case '\t': + retval.append("\\t"); + break; + case '\n': + retval.append("\\n"); + break; + case '\f': + retval.append("\\f"); + break; + case '\r': + retval.append("\\r"); + break; + case '\"': + retval.append("\\\""); + break; + case '\'': + retval.append("\\'"); + break; + case '\\': + retval.append("\\\\"); + break; + default: + if (ch < 0x20 || ch > 0x7e) { + String str = "0000" + Integer.toString(ch, 16); + retval.append("\\u").append(str.substring(str.length() - 4)); + } else { + retval.append(ch); + } + break; + } + } +} diff --git a/brut.j.xml/src/main/java/brut/xmlpull/XmlPullUtils.java b/brut.j.xml/src/main/java/brut/xmlpull/XmlPullUtils.java new file mode 100644 index 00000000..47069147 --- /dev/null +++ b/brut.j.xml/src/main/java/brut/xmlpull/XmlPullUtils.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 Ryszard Wiśniewski + * Copyright (C) 2010 Connor Tumbleson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package brut.xmlpull; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.*; + +public final class XmlPullUtils { + private static final String PROPERTY_XMLDECL_STANDALONE + = "http://xmlpull.org/v1/doc/properties.html#xmldecl-standalone"; + + public interface EventHandler { + boolean onEvent(XmlPullParser in, XmlSerializer out) throws XmlPullParserException; + } + + private XmlPullUtils() { + // Private constructor for utility class + } + + public static void copy(XmlPullParser in, XmlSerializer out) + throws XmlPullParserException, IOException { + copy(in, out, null); + } + + public static void copy(XmlPullParser in, XmlSerializer out, EventHandler handler) + throws XmlPullParserException, IOException { + Boolean standalone = (Boolean) in.getProperty(PROPERTY_XMLDECL_STANDALONE); + + // Some parsers may have already consumed the event that starts the + // document, so we manually emit that event here for consistency + if (in.getEventType() == XmlPullParser.START_DOCUMENT) { + out.startDocument(in.getInputEncoding(), standalone); + } + + while (true) { + int event = in.nextToken(); + if (event == XmlPullParser.START_DOCUMENT) { + out.startDocument(in.getInputEncoding(), standalone); + continue; + } + if (event == XmlPullParser.END_DOCUMENT) { + out.endDocument(); + break; + } + if (handler != null && handler.onEvent(in, out)) { + continue; + } + switch (event) { + case XmlPullParser.START_TAG: + if (!in.getFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES)) { + int nsStart = in.getNamespaceCount(in.getDepth() - 1); + int nsEnd = in.getNamespaceCount(in.getDepth()); + for (int i = nsStart; i < nsEnd; i++) { + String prefix = in.getNamespacePrefix(i); + String ns = in.getNamespaceUri(i); + out.setPrefix(prefix, ns); + } + } + out.startTag(normalizeNamespace(in.getNamespace()), in.getName()); + for (int i = 0; i < in.getAttributeCount(); i++) { + String ns = normalizeNamespace(in.getAttributeNamespace(i)); + String name = in.getAttributeName(i); + String value = in.getAttributeValue(i); + out.attribute(ns, name, value); + } + break; + case XmlPullParser.END_TAG: + out.endTag(normalizeNamespace(in.getNamespace()), in.getName()); + break; + case XmlPullParser.TEXT: + out.text(in.getText()); + break; + case XmlPullParser.CDSECT: + out.cdsect(in.getText()); + break; + case XmlPullParser.ENTITY_REF: + out.entityRef(in.getName()); + break; + case XmlPullParser.IGNORABLE_WHITESPACE: + out.ignorableWhitespace(in.getText()); + break; + case XmlPullParser.PROCESSING_INSTRUCTION: + out.processingInstruction(in.getText()); + break; + case XmlPullParser.COMMENT: + out.comment(in.getText()); + break; + case XmlPullParser.DOCDECL: + out.docdecl(in.getText()); + break; + default: + throw new IllegalStateException("Unknown event: " + event); + } + } + } + + /** + * Some parsers may return an empty string when a namespace in unsupported, + * which can confuse serializers. This method normalizes empty strings to + * be null. + */ + private static String normalizeNamespace(String namespace) { + if (namespace == null || namespace.isEmpty()) { + return null; + } else { + return namespace; + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index b521e7a3..d34eee1f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import java.io.ByteArrayOutputStream -val version = "2.9.3" -val suffix = "" +val version = "2.10.1" +val suffix = "SNAPSHOT" // Strings embedded into the build. var gitRevision by extra("") @@ -55,7 +55,6 @@ if ("release" !in gradle.startParameter.taskNames) { } plugins { - alias(libs.plugins.shadow) `java-library` `maven-publish` signing @@ -84,7 +83,7 @@ subprojects { targetCompatibility = JavaVersion.VERSION_1_8 } - val mavenProjects = arrayOf("apktool-lib", "apktool-cli", "brut.j.common", "brut.j.util", "brut.j.dir") + val mavenProjects = arrayOf("brut.j.common", "brut.j.util", "brut.j.dir", "brut.j.xml", "apktool-lib", "apktool-cli") if (project.name in mavenProjects) { apply(plugin = "maven-publish") @@ -156,8 +155,10 @@ subprojects { } } -// Used for official releases. task("release") { - dependsOn("build") - finalizedBy("publish") + // Used for official releases. +} + +tasks.wrapper { + distributionType = Wrapper.DistributionType.ALL } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d08e532a..f1a3986d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,15 @@ [versions] -baksmali = "3.0.3" -commons_io = "2.15.1" -commons_cli = "1.6.0" -commons_lang3 = "3.14.0" -commons_text = "1.11.0" -guava = "32.0.1-jre" +baksmali = "3.0.8" +commons_io = "2.18.0" +commons_cli = "1.9.0" +commons_lang3 = "3.17.0" +commons_text = "1.12.0" +guava = "33.3.1-jre" junit = "4.13.2" -proguard = "7.4.1" -shadow = "8.1.1" -smali = "3.0.3" -xmlpull = "1.1.4c" -xmlunit = "2.9.1" +r8 = "8.5.35" +smali = "3.0.8" +xmlpull = "1.1.3.1" +xmlunit = "2.10.0" [libraries] baksmali = { module = "com.android.tools.smali:smali-baksmali", version.ref = "baksmali" } @@ -20,10 +19,7 @@ commons_lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "co commons_text = { module = "org.apache.commons:commons-text", version.ref = "commons_text" } guava = { module = "com.google.guava:guava", version.ref = "guava" } junit = { module = "junit:junit", version.ref = "junit" } -proguard = { module = "com.guardsquare:proguard-gradle", version.ref = "proguard" } +r8 = { module = "com.android.tools:r8", version.ref = "r8" } smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } -xmlpull = { module = "xpp3:xpp3", version.ref = "xmlpull" } +xmlpull = { module = "xmlpull:xmlpull", version.ref = "xmlpull" } xmlunit = { module = "org.xmlunit:xmlunit-legacy", version.ref = "xmlunit" } - -[plugins] -shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c..e6441136 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8838ba97..e7646dea 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..25da30db 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/scripts/linux/apktool b/scripts/linux/apktool index 5ef7d329..b89d4060 100755 --- a/scripts/linux/apktool +++ b/scripts/linux/apktool @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# This script is a wrapper for smali.jar, so you can simply call "smali", -# instead of java -jar smali.jar. It is heavily based on the "dx" script +# This script is a wrapper for apktool.jar, so you can simply call "apktool", +# instead of java -jar apktool.jar. It is heavily based on the "dx" script # from the Android SDK # Set up prog to be the path of this script, including following symlinks, @@ -41,10 +41,15 @@ cd "${oldwd}" jarfile=apktool.jar libdir="$progdir" -if [ ! -r "$libdir/$jarfile" ] -then - echo `basename "$prog"`": can't find $jarfile" - exit 1 +if [ ! -r "$libdir/$jarfile" ]; then + # Find the highest version of apktool_*.jar in the directory. + highest_jarfile=$(ls "$libdir"/apktool_*.jar 2>/dev/null | sort -V | tail -n 1) + if [ -n "$highest_jarfile" ]; then + jarfile=$(basename "$highest_jarfile") + else + echo `basename "$prog"`": can't find $jarfile" + exit 1 + fi fi javaOpts="" @@ -66,9 +71,9 @@ while expr "x$1" : 'x-J' >/dev/null; do done if [ "$OSTYPE" = "cygwin" ] ; then - jarpath=`cygpath -w "$libdir/$jarfile"` + jarpath=`cygpath -w "$libdir/$jarfile"` else - jarpath="$libdir/$jarfile" + jarpath="$libdir/$jarfile" fi # add current location to path for aapt diff --git a/scripts/osx/apktool b/scripts/osx/apktool index ce14198e..844d12a5 100755 --- a/scripts/osx/apktool +++ b/scripts/osx/apktool @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# This script is a wrapper for smali.jar, so you can simply call "smali", -# instead of java -jar smali.jar. It is heavily based on the "dx" script +# This script is a wrapper for apktool.jar, so you can simply call "apktool", +# instead of java -jar apktool.jar. It is heavily based on the "dx" script # from the Android SDK # Set up prog to be the path of this script, including following symlinks, @@ -43,8 +43,14 @@ jarfile=apktool.jar libdir="$progdir" if [ ! -r "$libdir/$jarfile" ] then - echo `basename "$prog"`": can't find $jarfile" - exit 1 + # Find the highest version of apktool_*.jar in the directory. + highest_jarfile=$(ls "$libdir"/apktool_*.jar 2>/dev/null | sort -V | tail -n 1) + if [ -n "$highest_jarfile" ]; then + jarfile=$(basename "$highest_jarfile") + else + echo `basename "$prog"`": can't find $jarfile" + exit 1 + fi fi javaOpts="" @@ -66,9 +72,9 @@ while expr "x$1" : 'x-J' >/dev/null; do done if [ "$OSTYPE" = "cygwin" ] ; then - jarpath=`cygpath -w "$libdir/$jarfile"` + jarpath=`cygpath -w "$libdir/$jarfile"` else - jarpath="$libdir/$jarfile" + jarpath="$libdir/$jarfile" fi # add current location to path for aapt diff --git a/settings.gradle.kts b/settings.gradle.kts index db82a26b..619f8094 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ rootProject.name = "apktool-cli" -include("brut.j.common", "brut.j.util", "brut.j.dir", "brut.apktool:apktool-lib", "brut.apktool:apktool-cli") +include("brut.j.common", "brut.j.util", "brut.j.dir", "brut.j.xml", "brut.apktool:apktool-lib", "brut.apktool:apktool-cli") dependencyResolutionManagement { versionCatalogs {