Merge branch 'upstream'

# Conflicts:
#	brut.apktool/apktool-lib/src/main/java/brut/androlib/AaptInvoker.java
#	brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java
#	brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java
#	brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/MissingDiv9PatchTest.java
#	brut.j.util/src/main/java/brut/util/BrutIO.java
#	brut.j.util/src/main/java/brut/util/OSDetection.java
#	build.gradle.kts
This commit is contained in:
oSumAtrIX 2024-12-17 01:43:55 +01:00
commit 8f166d5125
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
209 changed files with 5171 additions and 4893 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Jar> {
manifest {
attributes["Main-Class"] = "brut.apktool.Main"
}
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
tasks.register<Delete>("cleanOutputDirectory") {
delete(fileTree("build/libs") {
exclude("apktool-cli-sources.jar")
exclude("apktool-cli-javadoc.jar")
exclude("apktool-cli-all.jar")
})
}
tasks.register<ProGuardTask>("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
dontobfuscate()
dontoptimize()
val dependencies = configurations
.runtimeClasspath
.get()
.map(::zipTree)
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("**")
val outPath = "build/libs/apktool-$apktoolVersion.jar"
outjars(outPath)
from(dependencies)
with(tasks.jar.get())
}
tasks.register<JavaExec>("proguard") {
dependsOn("shadowJar")
onlyIf {
JavaVersion.current().isJava11Compatible
}
val proguardRules = file("proguard-rules.pro")
val originalJar = shadowJar.outputs.files.singleFile
inputs.files(originalJar.toString(), proguardRules)
outputs.file("build/libs/apktool-$apktoolVersion.jar")
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<org.gradle.api.publish.maven.tasks.PublishToMavenRepository> {
dependsOn(tasks.named("shadowJar"))
}
tasks.withType<org.gradle.plugins.signing.Sign> {
dependsOn(tasks.named("shadowJar"))
}
tasks.withType<org.gradle.api.publish.tasks.GenerateModuleMetadata> {
dependsOn(tasks.named("shadowJar"))
}

View File

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

View File

@ -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")) {
switch (opt) {
case "d":
case "decode":
cmdDecode(commandLine, config);
cmdFound = true;
} else if (opt.equalsIgnoreCase("b") || opt.equalsIgnoreCase("build")) {
break;
case "b":
case "build":
cmdBuild(commandLine, config);
cmdFound = true;
} else if (opt.equalsIgnoreCase("if") || opt.equalsIgnoreCase("install-framework")) {
break;
case "if":
case "install-framework":
cmdInstallFramework(commandLine, config);
cmdFound = true;
} else if (opt.equalsIgnoreCase("empty-framework-dir")) {
break;
case "empty-framework-dir":
cmdEmptyFrameworkDirectory(commandLine, config);
cmdFound = true;
} else if (opt.equalsIgnoreCase("list-frameworks")) {
break;
case "list-frameworks":
cmdListFrameworks(commandLine, config);
cmdFound = true;
} else if (opt.equalsIgnoreCase("publicize-resources")) {
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")
@ -458,7 +490,7 @@ public class Main {
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 <tag>.")
@ -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();
}
}

View File

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

View File

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

View File

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

View File

@ -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 {
public void invoke(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include)
throws AndrolibException {
File aaptBinary = mConfig.aaptBinary;
List<String> cmd = new ArrayList<>();
String aaptPath;
boolean customAapt;
if (mConfig.aaptBinary != null) {
aaptPath = mConfig.aaptBinary.getPath();
customAapt = true;
} else {
try {
if (getAaptVersion() == 2) {
return AaptManager.getAapt2();
}
return AaptManager.getAapt1();
aaptPath = AaptManager.getAaptBinary(mConfig.aaptVersion).getPath();
customAapt = false;
} catch (BrutException ex) {
throw new AndrolibException(ex);
aaptPath = AaptManager.getAaptName(mConfig.aaptVersion);
customAapt = true;
LOGGER.warning(aaptPath + ": " + ex.getMessage() + " (defaulting to $PATH binary)");
}
}
private int getAaptVersion() {
return mConfig.isAapt2() ? 2 : 1;
}
cmd.add(aaptPath);
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();
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<String> cmd, boolean customAapt) throws AndrolibException {
List<String> 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<String> featureFlags = new ArrayList<>();
for (Map.Entry<String, Boolean> 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<String> 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<String> 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);
}
}

View File

@ -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<AndrolibException> 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());
public void build(File outApk) throws AndrolibException {
if (mConfig.jobs > 1) {
mWorker = new BackgroundWorker(mConfig.jobs - 1);
}
try {
mApkInfo = ApkInfo.load(mApkDir);
if (mApkInfo.getSdkInfo() != null && mApkInfo.getSdkInfo().get("minSdkVersion") != null) {
String minSdkVersion = mApkInfo.getSdkInfo().get("minSdkVersion");
String minSdkVersion = mApkInfo.getMinSdkVersion();
if (minSdkVersion != null) {
mMinSdkVersion = mApkInfo.getMinSdkVersionFromAndroidCodename(minSdkVersion);
}
if (outFile == null) {
if (outApk == null) {
String outFileName = mApkInfo.apkFileName;
outFile = new File(mApkDir, "dist" + File.separator + (outFileName == null ? "out.apk" : outFileName));
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
new File(mApkDir, APK_DIRNAME).mkdirs();
outDir.mkdirs();
File manifest = new File(mApkDir, "AndroidManifest.xml");
File manifestOriginal = new File(mApkDir, "AndroidManifest.xml.orig");
File manifestOrig = new File(mApkDir, "AndroidManifest.xml.orig");
buildSources();
buildNonDefaultSources();
buildManifestFile(manifest, manifestOriginal);
buildResources();
buildLibs();
buildCopyOriginalFiles();
buildApk(outFile);
LOGGER.info("Using Apktool " + ApktoolProperties.getVersion() + " on " + outApk.getName()
+ (mWorker != null ? " with " + mConfig.jobs + " threads" : ""));
// 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);
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() && manifest.exists() && manifestOriginal.isFile()) {
if (manifest.isFile() && manifestOrig.isFile()) {
try {
if (new File(mApkDir, "AndroidManifest.xml").delete()) {
FileUtils.moveFile(manifestOriginal, manifest);
if (manifest.delete()) {
FileUtils.moveFile(manifestOrig, manifest);
}
} catch (IOException ex) {
throw new AndrolibException(ex.getMessage());
throw new AndrolibException(ex);
}
}
} finally {
if (mWorker != null) {
mWorker.shutdownNow();
}
}
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;
private void buildSources(File outDir) throws AndrolibException {
if (!copySourcesRaw(outDir, "classes.dex")) {
buildSourcesSmali(outDir, "smali", "classes.dex");
}
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());
}
}
}
Directory in = mApkDir.getDirectory();
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<String, Directory> dirs = mApkDir.getDirectory().getDirs();
for (Map.Entry<String, Directory> directory : dirs.entrySet()) {
String name = directory.getKey();
if (name.startsWith("smali_")) {
String filename = name.substring(name.indexOf("_") + 1) + ".dex";
if (!buildSourcesRaw(filename) && !buildSourcesSmali(name, filename)) {
LOGGER.warning("Could not find sources");
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);
}
}
return true;
private void buildResources(File outDir, File manifest) throws AndrolibException {
if (!manifest.isFile()) {
LOGGER.fine("Could not find AndroidManifest.xml");
return;
}
private boolean buildSourcesSmali(String folder, String filename) throws AndrolibException {
ExtFile smaliDir = new ExtFile(mApkDir, folder);
if (!smaliDir.exists()) {
return false;
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);
}
File dex = new File(mApkDir, APK_DIRNAME + "/" + filename);
}
private void copyResourcesRaw(File outDir, File manifest) throws AndrolibException {
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");
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;
}
}
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;
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 boolean buildResourcesFull() throws AndrolibException {
try {
if (!new File(mApkDir, "res").exists()) {
return false;
}
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 has changed...");
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;
}
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...");
}
//noinspection ResultOfMethodCallIgnored
resourcesFile.delete();
try {
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"));
if (mConfig.aaptVersion == 2) {
LOGGER.info("Setting 'debuggable' attribute to 'true' in AndroidManifest.xml");
ResXmlUtils.setApplicationDebugTagTrue(manifest);
} else {
ResXmlPatcher.removeApplicationDebugTag(new File(mApkDir, "AndroidManifest.xml"));
ResXmlUtils.removeApplicationDebugTag(manifest);
}
}
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) {
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");
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"));
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);
}
File apkFile = File.createTempFile("APKTOOL", null);
ExtFile tmpFile;
try {
tmpFile = new ExtFile(File.createTempFile("APKTOOL", null));
} catch (IOException ex) {
throw new AndrolibException(ex);
}
//noinspection ResultOfMethodCallIgnored
apkFile.delete();
//noinspection ResultOfMethodCallIgnored
resourceFile.delete();
tmpFile.delete();
File resDir = new File(mApkDir, "res");
File ninePatch = new File(mApkDir, "9patch");
if (!ninePatch.exists()) {
if (!ninePatch.isDirectory()) {
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();
LOGGER.info("Building resources with " + AaptManager.getAaptName(mConfig.aaptVersion) + "...");
// 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);
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) {
LOGGER.warning(ex.getMessage());
throw new AndrolibException(ex);
} finally {
tmpExtFile.close();
}
// delete tmpDir
//noinspection ResultOfMethodCallIgnored
apkFile.delete();
}
return true;
} catch (IOException | BrutException | ParserConfigurationException | TransformerException | SAXException ex) {
throw new AndrolibException(ex);
tmpFile.delete();
}
}
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;
}
private void buildManifest(File outDir, File manifest) throws AndrolibException {
if (!mConfig.forceBuildAll) {
LOGGER.info("Checking whether resources has changed...");
LOGGER.info("Checking whether AndroidManifest.xml has changed...");
if (!isModified(manifest, new File(outDir, "AndroidManifest.xml"))) {
return;
}
}
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);
ExtFile tmpFile;
try {
tmpFile = new ExtFile(File.createTempFile("APKTOOL", null));
} catch (IOException ex) {
throw new AndrolibException(ex);
}
//noinspection ResultOfMethodCallIgnored
apkFile.delete();
tmpFile.delete();
File ninePatch = new File(mApkDir, "9patch");
if (!ninePatch.exists()) {
if (!ninePatch.isDirectory()) {
ninePatch = null;
}
LOGGER.info("Building AndroidManifest.xml with " + AaptManager.getAaptName(mConfig.aaptVersion) + "...");
try {
AaptInvoker invoker = new AaptInvoker(mConfig, mApkInfo);
invoker.invokeAapt(apkFile, new File(mApkDir, "AndroidManifest.xml"),
null, ninePatch, null, getIncludeFiles());
invoker.invoke(tmpFile, manifest, 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) {
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.");
return buildManifestRaw();
copyManifestRaw(outDir);
} finally {
//noinspection ResultOfMethodCallIgnored
tmpFile.delete();
}
}
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()) {
return;
}
File stored = new File(mApkDir, APK_DIRNAME + "/" + folder);
if (mConfig.forceBuildAll || isModified(working, stored)) {
LOGGER.info("Copying libs... (/" + folder + ")");
private void copyManifestRaw(File outDir) throws AndrolibException {
LOGGER.info("Copying raw manifest...");
try {
OS.rmdir(stored);
OS.cpdir(working, stored);
} catch (BrutException ex) {
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;
}
private void buildCopyOriginalFiles() throws AndrolibException {
if (mConfig.copyOriginalFiles) {
File originalDir = new File(mApkDir, "original");
if (originalDir.exists()) {
ExtFile originalDir = new ExtFile(mApkDir, "original");
if (!originalDir.isDirectory()) {
return;
}
LOGGER.info("Copying original files...");
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");
Directory in = originalDir.getDirectory();
for (String fileName : in.getFiles(true)) {
if (ApkInfo.ORIGINAL_FILENAMES_PATTERN.matcher(fileName).matches()) {
in.copyToDir(outDir, fileName);
}
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 importRawFiles(ZipOutputStream out) throws AndrolibException {
for (String dirName : ApkInfo.RAW_DIRNAMES) {
File rawDir = new File(mApkDir, dirName);
if (!rawDir.isDirectory()) {
continue;
}
private void buildUnknownFiles(File outFile) throws AndrolibException {
if (mApkInfo.unknownFiles != null) {
LOGGER.info("Copying unknown files/dir...");
Map<String, String> 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) {
LOGGER.info("Importing " + dirName + "...");
try {
ZipUtils.zipDir(mApkDir, dirName, out, mApkInfo.doNotCompress);
} catch (IOException 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<? extends ZipEntry> 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);
private void importUnknownFiles(ZipOutputStream out) throws AndrolibException {
File unknownDir = new File(mApkDir, "unknown");
if (!unknownDir.isDirectory()) {
return;
}
outputFile.closeEntry();
}
}
private void copyUnknownFiles(ZipOutputStream outputFile, Map<String, String> files)
throws BrutException, IOException {
File unknownFileDir = new File(mApkDir, UNK_DIRNAME);
// loop through unknown files
for (Map.Entry<String,String> unknownFileInfo : files.entrySet()) {
File inputFile;
LOGGER.info("Importing unknown files...");
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 {
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<String> 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");
}
}

View File

@ -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<AndrolibException> 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 {
public ApkDecoder(ExtFile apkFile, Config config) {
mApkFile = apkFile;
mConfig = config;
mBuildError = new AtomicReference<>(null);
}
public ApkInfo decode(File outDir) throws AndrolibException {
if (!mConfig.forceDelete && outDir.exists()) {
throw new OutDirExistsException();
}
if (!apkFile.isFile() || !apkFile.canRead()) {
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,33 +91,41 @@ 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);
copyOriginalFiles(outDir);
copyRawFiles(outDir);
copyUnknownFiles(outDir);
writeApkInfo(outDir);
return mApkInfo;
} finally {
if (mWorker != null) {
mWorker.shutdownNow();
}
try {
mApkFile.close();
} catch (IOException ignored) {}
}
}
private void decodeSources(File outDir) throws AndrolibException {
if (!mApkInfo.hasSources()) {
return;
}
if (mApkInfo.hasSources()) {
switch (mConfig.decodeSources) {
case Config.DECODE_SOURCES_NONE:
copySourcesRaw(outDir, "classes.dex");
@ -131,156 +135,154 @@ public class ApkDecoder {
decodeSourcesSmali(outDir, "classes.dex");
break;
}
}
if (mApkInfo.hasMultipleSources()) {
try {
Directory in = mApkFile.getDirectory();
// foreach unknown dex file in root, lets disassemble it
Set<String> files = apkFile.getDirectory().getFiles(true);
for (String file : files) {
if (file.endsWith(".dex")) {
if (!file.equalsIgnoreCase("classes.dex")) {
switch(mConfig.decodeSources) {
for (String fileName : in.getFiles(true)) {
if (fileName.endsWith(".dex") && !fileName.equals("classes.dex")) {
switch (mConfig.decodeSources) {
case Config.DECODE_SOURCES_NONE:
copySourcesRaw(outDir, file);
copySourcesRaw(outDir, fileName);
break;
case Config.DECODE_SOURCES_SMALI:
decodeSourcesSmali(outDir, file);
decodeSourcesSmali(outDir, fileName);
break;
case Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
if (file.startsWith("classes") && file.endsWith(".dex")) {
decodeSourcesSmali(outDir, file);
if (fileName.startsWith("classes")) {
decodeSourcesSmali(outDir, fileName);
} else {
copySourcesRaw(outDir, file);
copySourcesRaw(outDir, fileName);
}
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));
}
copyRawFiles(outDir);
copyUnknownFiles(outDir);
recordUncompressedFiles(resourcesDecoder.getResFileMapping());
copyOriginalFiles(outDir);
writeApkInfo(outDir);
return mApkInfo;
} finally {
try {
apkFile.close();
} catch (IOException ignored) {}
}
}
private void writeApkInfo(File outDir) throws AndrolibException {
mApkInfo.save(new File(outDir, "apktool.yml"));
}
private void copyManifestRaw(File outDir) throws AndrolibException {
try {
LOGGER.info("Copying raw manifest...");
mApkInfo.getApkFile().getDirectory().copyToDir(outDir, APK_MANIFEST_FILENAMES);
} catch (DirectoryException ex) {
throw new AndrolibException(ex);
}
}
private void copyResourcesRaw(File outDir) throws AndrolibException {
private void copySourcesRaw(File outDir, String fileName) throws AndrolibException {
LOGGER.info("Copying raw " + fileName + " file...");
try {
LOGGER.info("Copying raw resources...");
mApkInfo.getApkFile().getDirectory().copyToDir(outDir, APK_RESOURCES_FILENAMES);
Directory in = mApkFile.getDirectory();
in.copyToDir(outDir, fileName);
} catch (DirectoryException ex) {
throw new AndrolibException(ex);
}
}
private void copySourcesRaw(File outDir, String filename) throws AndrolibException {
private void decodeSourcesSmali(File outDir, String fileName) throws AndrolibException {
if (mWorker != null) {
mWorker.submit(() -> {
if (mBuildError.get() == null) {
try {
LOGGER.info("Copying raw " + filename + " file...");
mApkInfo.getApkFile().getDirectory().copyToDir(outDir, filename);
} catch (DirectoryException ex) {
throw new AndrolibException(ex);
decodeSourcesSmaliJob(outDir, fileName);
} catch (AndrolibException ex) {
mBuildError.compareAndSet(null, ex);
}
}
private void decodeSourcesSmali(File outDir, String filename) throws AndrolibException {
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(".")));
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 + "...");
DexFile dexFile = SmaliDecoder.decode(mApkInfo.getApkFile(), smaliDir, filename,
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;
}
} catch (BrutException ex) {
}
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 {
Directory in = mApkFile.getDirectory();
in.copyToDir(outDir, "resources.arsc");
in.copyToDir(outDir, ApkInfo.RESOURCES_DIRNAMES);
} 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 copyManifestRaw(File outDir) throws AndrolibException {
LOGGER.info("Copying raw manifest...");
try {
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");
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);
}
}
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<String> 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)));
}
}
} 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<String, String> resFileMapping = mResDecoder.getResFileMapping();
recordUncompressedFiles(resFileMapping);
// write apk info to file
mApkInfo.save(new File(outDir, "apktool.yml"));
}
public void recordUncompressedFiles(Map<String, String> resFileMapping) throws AndrolibException {
try {
List<String> uncompressedFilesOrExts = new ArrayList<>();
Directory unk = mApkInfo.getApkFile().getDirectory();
Set<String> files = unk.getFiles(true);
Set<String> uncompressedExts = new HashSet<>();
Set<String> 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);
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));
}
}
}
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);
// exclude files with an already recorded extenstion
if (!uncompressedExts.isEmpty() && !uncompressedFiles.isEmpty()) {
Iterator<String> 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<String> doNotCompress = new ArrayList<>(doNotCompressSize);
if (!uncompressedExts.isEmpty()) {
List<String> uncompressedExtsList = new ArrayList<>(uncompressedExts);
uncompressedExtsList.sort(null);
doNotCompress.addAll(uncompressedExtsList);
}
if (!uncompressedFiles.isEmpty()) {
List<String> uncompressedFilesList = new ArrayList<>(uncompressedFiles);
uncompressedFilesList.sort(null);
doNotCompress.addAll(uncompressedFilesList);
}
if (!doNotCompress.isEmpty()) {
mApkInfo.doNotCompress = doNotCompress;
}
}
} catch (DirectoryException ex) {
throw new AndrolibException(ex);

View File

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

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* 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<Future<?>> 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 <T> Future<T> submit(Callable<T> task) {
checkState();
Future<T> future = mExecutor.submit(task);
mWorkerFutures.add(future);
return future;
}
}

View File

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

View File

@ -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<String, String> sdkInfo = new LinkedHashMap<>();
public Map<String, String> sdkInfo = new LinkedHashMap<>();
public PackageInfo packageInfo = new PackageInfo();
public VersionInfo versionInfo = new VersionInfo();
public boolean resourcesAreCompressed;
public Map<String, Boolean> featureFlags = new LinkedHashMap<>();
public boolean sharedLibrary;
public boolean sparseResources;
public Map<String, String> unknownFiles = new LinkedHashMap<>();
public List<String> doNotCompress;
/** @Deprecated use {@link #resourcesAreCompressed} */
public boolean compressionType;
public boolean compactEntries;
public List<String> 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<String> 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<String, String> getSdkInfo() {
return sdkInfo;
}
public void setSdkInfo(Map<String, String> 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);
}
}
}

View File

@ -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 = "";

View File

@ -22,9 +22,8 @@ import java.io.InputStream;
import java.util.*;
public class YamlReader {
private ArrayList<YamlLine> 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<String, String> map) throws AndrolibException {
public void readStringMap(Map<String, String> 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<String, Boolean> map) throws AndrolibException {
readObject(map,
line -> line.hasColon,
(items, reader) -> {
YamlLine line = reader.getLine();
items.put(line.getKey(), line.getValueBool());
});
}
}

View File

@ -20,5 +20,6 @@ import brut.androlib.exceptions.AndrolibException;
public interface YamlSerializable {
void readItem(YamlReader reader) throws AndrolibException;
void write(YamlWriter writer);
}

View File

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

View File

@ -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);
}
}
public void writeStringMap(String key, Map<String, String> map) {
public <T> void writeMap(String key, Map<String, T> 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();
}

View File

@ -17,6 +17,7 @@
package brut.androlib.exceptions;
public class AXmlDecodingException extends AndrolibException {
public AXmlDecodingException(String message, Throwable cause) {
super(message, cause);
}

View File

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

View File

@ -17,6 +17,7 @@
package brut.androlib.exceptions;
public class CantFind9PatchChunkException extends AndrolibException {
public CantFind9PatchChunkException(String message, Throwable cause) {
super(message, cause);
}

View File

@ -17,8 +17,10 @@
package brut.androlib.exceptions;
public class CantFindFrameworkResException extends AndrolibException {
public CantFindFrameworkResException(int id) {
mPkgId = id;
private final int mPkgId;
public CantFindFrameworkResException(int pkgId) {
mPkgId = pkgId;
}
public int getPkgId() {
@ -27,8 +29,6 @@ public class CantFindFrameworkResException extends AndrolibException {
@Override
public String getMessage() {
return String.format("Can't find framework resources for package of id: %d", this.getPkgId());
return String.format("Could not find framework resources for package of id: %d", mPkgId);
}
private final int mPkgId;
}

View File

@ -17,6 +17,8 @@
package brut.androlib.exceptions;
public class InFileNotFoundException extends AndrolibException {
public InFileNotFoundException() {
super();
}
}

View File

@ -17,6 +17,8 @@
package brut.androlib.exceptions;
public class OutDirExistsException extends AndrolibException {
public OutDirExistsException() {
super();
}
}

View File

@ -17,6 +17,7 @@
package brut.androlib.exceptions;
public class RawXmlEncounteredException extends AndrolibException {
public RawXmlEncounteredException(String message, Throwable cause) {
super(message, cause);
}

View File

@ -17,6 +17,7 @@
package brut.androlib.exceptions;
public class UndefinedResObjectException extends AndrolibException {
public UndefinedResObjectException(String message) {
super(message);
}

View File

@ -30,32 +30,30 @@ 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 {
CommonTokenStream tokens;
smaliFlexLexer lexer;
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);
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);
if (printTokens) {
tokens.getTokens();
for (int i=0; i<tokens.size(); i++) {
for (int i = 0; i < tokens.size(); i++) {
Token token = tokens.get(i);
if (token.getChannel() == smaliParser.HIDDEN) {
continue;
}
if (token.getChannel() != smaliParser.HIDDEN) {
System.out.println(smaliParser.tokenNames[token.getType()] + ": " + token.getText());
}
}
}
smaliParser parser = new smaliParser(tokens);
parser.setApiLevel(apiLevel);
@ -64,8 +62,6 @@ public class SmaliMod {
smaliParser.smali_file_return result = parser.smali_file();
if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) {
is.close();
reader.close();
return false;
}
@ -80,9 +76,7 @@ public class SmaliMod {
dexGen.setDexBuilder(dexBuilder);
dexGen.smali_file();
is.close();
reader.close();
return dexGen.getNumberOfSyntaxErrors() == 0;
}
}
}

View File

@ -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,43 +34,36 @@ 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()));
try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(outFile.toPath()))) {
out.setMethod(ZipOutputStream.STORED);
CRC32 crc = new CRC32();
crc.update(data);
@ -83,11 +75,10 @@ public class Framework {
out.write(data);
out.closeEntry();
//Write fake AndroidManifest.xml file to support original aapt
// 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);
byte[] manifest = BrutIO.readAndClose(zip.getInputStream(entry));
CRC32 manifestCrc = new CRC32();
manifestCrc.update(manifest);
entry.setSize(manifest.length);
@ -97,14 +88,11 @@ public class Framework {
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");
}
}

View File

@ -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<String> 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<String, String> 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<String, String> mResFileMapping;
public ResourcesDecoder(Config config, ApkInfo apkInfo) {
mConfig = config;
mApkInfo = apkInfo;
mResTable = new ResTable(mConfig, mApkInfo);
mResFileMapping = new HashMap<>();
}
public ResTable getResTable() throws AndrolibException {
@ -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) {
File manifest = new File(apkDir, "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(outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml");
adjustPackageManifest(manifest);
ResXmlPatcher.removeManifestVersions(new File(
outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml"));
ResXmlUtils.removeManifestVersions(manifest);
// update apk info
mApkInfo.packageInfo.forcedPackageId = String.valueOf(mResTable.getPackageId());
}
// record feature flags
List<String> 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);
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);
}
}

View File

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

View File

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

View File

@ -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<ResID, ResResSpec> mResSpecs = new LinkedHashMap<>();
private final Map<ResConfigFlags, ResType> mConfigs = new LinkedHashMap<>();
private final Map<String, ResTypeSpec> mTypes = new LinkedHashMap<>();
private final Set<ResID> mSynthesizedRes = new HashSet<>();
private final Map<ResID, ResResSpec> mResSpecs;
private final Map<ResConfigFlags, ResType> mConfigs;
private final Map<String, ResTypeSpec> mTypes;
private final Set<ResID> 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<ResResSpec> 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());
}

View File

@ -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<String> 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<ResConfigFlags, ResResource> mResources = new LinkedHashMap<>();
private static final Set<String> EMPTY_RESOURCE_NAMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
"0_resource_name_obfuscated",
"(name removed)"
)));
private final Map<ResConfigFlags, ResResource> 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<ResResource> listResources() {

View File

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

View File

@ -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<Integer, ResPackage> mPackagesById = new HashMap<>();
private final Map<String, ResPackage> mPackagesByName = new HashMap<>();
private final Set<ResPackage> mMainPackages = new LinkedHashSet<>();
private final Set<ResPackage> mFramePackages = new LinkedHashSet<>();
private final Map<Integer, ResPackage> mPackagesById;
private final Map<String, ResPackage> mPackagesByName;
private final Set<ResPackage> mMainPackages;
private final Set<ResPackage> 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<String, String> 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;
}

View File

@ -22,10 +22,11 @@ import java.util.*;
public class ResType {
private final ResConfigFlags mFlags;
private final Map<ResResSpec, ResResource> mResources = new LinkedHashMap<>();
private final Map<ResResSpec, ResResource> mResources;
public ResType(ResConfigFlags flags) {
this.mFlags = flags;
mFlags = flags;
mResources = new LinkedHashMap<>();
}
public ResResource getResource(ResResSpec spec) throws AndrolibException {

View File

@ -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<String, ResResSpec> mResSpecs = new LinkedHashMap<>();
private final int mId;
private final Map<String, ResResSpec> 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 {

View File

@ -24,12 +24,13 @@ public class ResValuesFile {
private final ResPackage mPackage;
private final ResTypeSpec mType;
private final ResType mConfig;
private final Set<ResResource> mResources = new LinkedHashSet<>();
private final Set<ResResource> 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> ALLOWED_ARRAY_TYPES = Sets.newHashSet("string", "integer");
private final ResScalarValue[] mItems;
ResArrayValue(ResReferenceValue parent, Duo<Integer, ResScalarValue>[] 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"};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ResReferenceValue, ResScalarValue>[] mItems;
private final Map<Integer, String> mItemsCache;
ResEnumAttr(ResReferenceValue parent, int type, Integer min, Integer max,
Boolean l10n, Duo<ResReferenceValue, ResScalarValue>[] 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<ResReferenceValue, ResScalarValue>[] mItems;
private final Map<Integer, String> mItemsCache = new HashMap<>();
private static final Logger LOGGER = Logger.getLogger(ResEnumAttr.class.getName());
}

View File

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

View File

@ -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<ResReferenceValue, ResScalarValue>[] 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());
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Integer, ResScalarValue>[] items) {
super(parent);
mItems = new ResScalarValue[6];
for (Duo<Integer, ResScalarValue> 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" };
}

View File

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

View File

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

View File

@ -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,}");
}

View File

@ -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<ResReferenceValue, ResScalarValue>[] mItems;
ResStyleValue(ResReferenceValue parent, Duo<Integer, ResScalarValue>[] 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<ResReferenceValue, ResScalarValue>[] mItems;
private static final Logger LOGGER = Logger.getLogger(ResStyleValue.class.getName());
}

View File

@ -19,6 +19,7 @@ package brut.androlib.res.data.value;
import brut.androlib.Config;
public class ResValue {
public boolean shouldRemoveUnknownRes() {
return Config.getInstance().isDecodeResolveModeRemoving();
}

View File

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

View File

@ -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<FlagsOffset> mFlagsOffsets;
private final boolean mKeepBroken;
private final HashMap<Integer, Integer> mMissingResSpecMap;
private final HashMap<Integer, ResTypeSpec> 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<ResPackage> 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,7 +490,7 @@ public class ARSCDecoder {
}
private ResIntBasedValue readValue() throws IOException, AndrolibException {
int size = mIn.readShort();
short size = mIn.readShort();
if (size < 8) {
return null;
}
@ -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<FlagsOffset> 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<Integer, Integer> mMissingResSpecMap;
private final HashMap<Integer, ResTypeSpec> 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());
}

View File

@ -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,10 +349,7 @@ 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.
@ -326,6 +358,8 @@ public class AXmlResourceParser implements XmlResourceParser {
// So return the res-auto namespace.
return ANDROID_RES_NS_AUTO;
}
return mStringBlock.getString(mNamespaces.getUri(offset));
}
@Override
public String getAttributePrefix(int index) {
@ -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,22 +489,21 @@ 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 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 mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
}
@Override
public int getAttributeUnsignedIntValue(int index, int defaultValue) {
@ -477,11 +514,11 @@ 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 mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
}
@Override
public String getAttributeValue(String namespace, String attribute) {
@ -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";
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* 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;
}
}
}
}
}
}

View File

@ -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,8 +57,8 @@ public class AndroidManifestResourceParser extends AXmlResourceParser {
}
private boolean isNumericStringMetadataAttributeValue(int index, String value) {
return "meta-data".equalsIgnoreCase(super.getName())
&& "value".equalsIgnoreCase(super.getAttributeName(index))
return "meta-data".equals(super.getName())
&& "value".equals(super.getAttributeName(index))
&& super.getAttributeValueType(index) == TypedValue.TYPE_STRING
&& PATTERN_NUMERIC_STRING.matcher(value).matches();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,11 @@ import java.io.*;
import java.util.*;
public class ResStreamDecoderContainer {
private final Map<String, ResStreamDecoder> mDecoders = new HashMap<>();
private final Map<String, ResStreamDecoder> mDecoders;
public ResStreamDecoderContainer() {
mDecoders = new HashMap<>();
}
public void decode(InputStream in, OutputStream out, String decoderName)
throws AndrolibException {

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* 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);
}
}
}

View File

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

View File

@ -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<Span> mSpans;
public StyledString(String text, List<Span> 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<String, String> 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<Span> 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<Span> 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<String, String> 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("</").append(name).append('>');
mXmlValue.append("</").append(name).append('>');
}
}
private static final Logger LOGGER = Logger.getLogger(StyledString.class.getName());
}

View File

@ -1,154 +0,0 @@
/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* 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 <manifest> 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;
}

View File

@ -1,76 +0,0 @@
/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* 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;
}

View File

@ -1,32 +0,0 @@
/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* 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";
}

View File

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

View File

@ -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, "&", "&amp;"), "<", "&lt;");
}

View File

@ -1,415 +0,0 @@
/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* 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.
* <p>
* 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());
}

View File

@ -0,0 +1,471 @@
/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* 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.
* <p>
* 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<String> 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<String> 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 = "<?xml version=\"1.0\" encoding=\"utf-8\"?>".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);
}
}
}

View File

@ -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")) {
boolean success;
Exception cause;
try {
if (!SmaliMod.assembleSmaliFile(inFile, dexBuilder, mApiLevel, false, false)) {
throw new AndrolibException("Could not smali file: " + fileName);
File inFile = new File(mSmaliDir, fileName);
success = SmaliMod.assembleSmaliFile(inFile, dexBuilder, mApiLevel, false, false);
cause = null;
} catch (Exception ex) {
success = false;
cause = ex;
}
} catch (IOException | RecognitionException ex) {
throw new AndrolibException(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());
}

View File

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

Some files were not shown because too many files have changed in this diff Show More