mirror of
https://github.com/revanced/Apktool.git
synced 2025-04-29 21:54:25 +02:00
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:
commit
8f166d5125
8
.github/workflows/analyze.yml
vendored
8
.github/workflows/analyze.yml
vendored
@ -17,12 +17,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: java
|
languages: java-kotlin
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
|
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
@ -76,10 +76,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: ${{ matrix.java }}
|
java-version: ${{ matrix.java }}
|
||||||
- name: Build and test
|
|
||||||
uses: gradle/gradle-build-action@v2.10.0
|
- uses: gradle/actions/setup-gradle@v4.2.1
|
||||||
with:
|
|
||||||
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
|
||||||
|
|
||||||
upload-artifact:
|
upload-artifact:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -91,17 +97,26 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: 17
|
java-version: 17
|
||||||
- name: Build
|
|
||||||
uses: gradle/gradle-build-action@v2.10.0
|
- uses: gradle/actions/setup-gradle@v4.2.1
|
||||||
with:
|
with:
|
||||||
dependency-graph: generate-and-submit
|
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
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: apktool.jar
|
name: apktool.jar
|
||||||
path: brut.apktool/apktool-cli/build/libs/apktool-v*
|
path: brut.apktool/apktool-cli/build/libs/apktool-v*
|
||||||
|
2
.github/workflows/gradle.yml
vendored
2
.github/workflows/gradle.yml
vendored
@ -21,4 +21,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: 17
|
java-version: 17
|
||||||
- uses: gradle/wrapper-validation-action@v1.1.0
|
- uses: gradle/actions/wrapper-validation@v4.2.1
|
||||||
|
44
INTERNAL.md
44
INTERNAL.md
@ -10,11 +10,11 @@ _Currently broken after movement to kotlin dsl._
|
|||||||
|
|
||||||
Inside `build.gradle` there are two lines.
|
Inside `build.gradle` there are two lines.
|
||||||
|
|
||||||
apktoolversion_major
|
version
|
||||||
apktoolversion_minor
|
suffix
|
||||||
|
|
||||||
The major variable should be left unchanged. If done correctly, it will already be the version
|
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 minor variable should read `SNAPSHOT` as
|
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).
|
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
|
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.
|
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.
|
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.
|
### 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.
|
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.
|
### Getting the modified `frameworks/base` repo.
|
||||||
First step is using the [platform_frameworks_base](https://github.com/iBotPeaches/platform_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
|
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 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`.
|
* aapt2 - `android-14.0.0_r54`
|
||||||
* aapt1 - `android-14.0.0_r2`.
|
* aapt1 - `android-14.0.0_r2` (deprecated)
|
||||||
|
|
||||||
### Including our modified `frameworks/base` package.
|
### Including our modified `frameworks/base` package.
|
||||||
|
|
||||||
@ -277,7 +280,7 @@ The steps below are different per flavor and operating system.
|
|||||||
|
|
||||||
#### Linux / Windows
|
#### Linux / Windows
|
||||||
1. `source build/envsetup.sh`
|
1. `source build/envsetup.sh`
|
||||||
2. `lunch sdk-eng`
|
2. `lunch aosp_arm64-trunk_staging-eng`
|
||||||
3. `m aapt`
|
3. `m aapt`
|
||||||
4. `strip out/host/linux-x86/bin/aapt`
|
4. `strip out/host/linux-x86/bin/aapt`
|
||||||
5. `strip out/host/linux-x86/bin/aapt_64`
|
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.
|
The steps below are different per flavor and operating system.
|
||||||
|
|
||||||
#### Linux / Windows
|
#### Linux / Windows
|
||||||
|
1. `source build/envsetup.sh`
|
||||||
|
1. `lunch aosp_arm64-trunk_staging-eng`
|
||||||
1. `m aapt2`
|
1. `m aapt2`
|
||||||
2. `strip out/host/linux-x86/bin/aapt2`
|
1. `strip out/host/linux-x86/bin/aapt2`
|
||||||
3. `strip out/host/linux-x86/bin/aapt2_64`
|
1. `strip out/host/linux-x86/bin/aapt2_64`
|
||||||
4. `strip out/host/windows-x86/bin/aapt2.exe`
|
1. `strip out/host/windows-x86/bin/aapt2.exe`
|
||||||
5. `strip out/host/windows-x86/bin/aapt2_64.exe`
|
1. `strip out/host/windows-x86/bin/aapt2_64.exe`
|
||||||
|
|
||||||
#### Mac
|
#### Mac
|
||||||
1. `export ANDROID_JAVA_HOME=/Path/To/Jdk`
|
1. `export ANDROID_JAVA_HOME=/Path/To/Jdk`
|
||||||
2. `source build/envsetup.sh`
|
1. `source build/envsetup.sh`
|
||||||
3. `m aapt2`
|
1. `lunch aosp_arm64-trunk_staging-eng`
|
||||||
4. `strip out/host/darwin-x86/bin/aapt2_64`
|
1. `m aapt2`
|
||||||
|
1. `strip out/host/darwin-x86/bin/aapt2_64`
|
||||||
|
|
||||||
#### Confirming aapt/aapt2 builds are static
|
#### Confirming aapt/aapt2 builds are static
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
### Apktool
|
### 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).**
|
||||||
|
|
||||||
[](https://github.com/iBotPeaches/Apktool/actions/workflows/test.yml)
|
[](https://github.com/iBotPeaches/Apktool/actions/workflows/test.yml)
|
||||||
[](https://github.com/iBotPeaches/Apktool/blob/master/LICENSE)
|
[](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
|
#### Support
|
||||||
- [Project Page](https://ibotpeaches.github.io/Apktool/)
|
- [Project Page](https://ibotpeaches.github.io/Apktool/)
|
||||||
|
@ -1,25 +1,15 @@
|
|||||||
import proguard.gradle.ProGuardTask
|
|
||||||
|
|
||||||
val apktoolVersion: String by rootProject.extra
|
val apktoolVersion: String by rootProject.extra
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.shadow)
|
|
||||||
application
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buildscript is deprecated, but the alternative approach does not support expanded properties
|
val r8: Configuration by configurations.creating
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.commons.cli)
|
implementation(libs.commons.cli)
|
||||||
implementation(project(":brut.apktool:apktool-lib"))
|
implementation(project(":brut.apktool:apktool-lib"))
|
||||||
|
r8(libs.r8)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@ -28,46 +18,73 @@ application {
|
|||||||
tasks.run.get().workingDir = file(System.getProperty("user.dir"))
|
tasks.run.get().workingDir = file(System.getProperty("user.dir"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Jar> {
|
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||||
manifest {
|
isPreserveFileTimestamps = false
|
||||||
attributes["Main-Class"] = "brut.apktool.Main"
|
isReproducibleFileOrder = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Delete>("cleanOutputDirectory") {
|
tasks.register<Delete>("cleanOutputDirectory") {
|
||||||
delete(fileTree("build/libs") {
|
delete(fileTree("build/libs") {
|
||||||
|
exclude("apktool-cli-sources.jar")
|
||||||
|
exclude("apktool-cli-javadoc.jar")
|
||||||
exclude("apktool-cli-all.jar")
|
exclude("apktool-cli-all.jar")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<ProGuardTask>("proguard") {
|
val shadowJar = tasks.create("shadowJar", Jar::class) {
|
||||||
|
dependsOn("build")
|
||||||
dependsOn("cleanOutputDirectory")
|
dependsOn("cleanOutputDirectory")
|
||||||
dependsOn("shadowJar")
|
|
||||||
injars(tasks.named("shadowJar").get().outputs.files)
|
|
||||||
|
|
||||||
val javaHome = System.getProperty("java.home")
|
group = "build"
|
||||||
if (JavaVersion.current() <= JavaVersion.VERSION_1_8) {
|
description = "Creates a single executable JAR with all dependencies"
|
||||||
libraryjars("$javaHome/lib/jce.jar")
|
manifest.attributes["Main-Class"] = "brut.apktool.Main"
|
||||||
libraryjars("$javaHome/lib/rt.jar")
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
} else {
|
|
||||||
libraryjars(mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"),
|
val dependencies = configurations
|
||||||
{
|
.runtimeClasspath
|
||||||
"$javaHome/jmods/"
|
.get()
|
||||||
}
|
.map(::zipTree)
|
||||||
)
|
|
||||||
|
from(dependencies)
|
||||||
|
with(tasks.jar.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<JavaExec>("proguard") {
|
||||||
|
dependsOn("shadowJar")
|
||||||
|
|
||||||
|
onlyIf {
|
||||||
|
JavaVersion.current().isJava11Compatible
|
||||||
}
|
}
|
||||||
|
|
||||||
dontobfuscate()
|
val proguardRules = file("proguard-rules.pro")
|
||||||
dontoptimize()
|
val originalJar = shadowJar.outputs.files.singleFile
|
||||||
|
|
||||||
keep("class brut.apktool.Main { public static void main(java.lang.String[]); }")
|
inputs.files(originalJar.toString(), proguardRules)
|
||||||
keepclassmembers("enum * { public static **[] values(); public static ** valueOf(java.lang.String); }")
|
outputs.file("build/libs/apktool-$apktoolVersion.jar")
|
||||||
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"
|
classpath(r8)
|
||||||
outjars(outPath)
|
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"))
|
||||||
}
|
}
|
||||||
|
11
brut.apktool/apktool-cli/proguard-rules.pro
vendored
Normal file
11
brut.apktool/apktool-cli/proguard-rules.pro
vendored
Normal 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
|
@ -36,6 +36,19 @@ import java.util.logging.*;
|
|||||||
* Main entry point of the apktool.
|
* Main entry point of the apktool.
|
||||||
*/
|
*/
|
||||||
public class Main {
|
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 {
|
public static void main(String[] args) throws BrutException {
|
||||||
|
|
||||||
// headless
|
// headless
|
||||||
@ -88,24 +101,34 @@ public class Main {
|
|||||||
|
|
||||||
boolean cmdFound = false;
|
boolean cmdFound = false;
|
||||||
for (String opt : commandLine.getArgs()) {
|
for (String opt : commandLine.getArgs()) {
|
||||||
if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")) {
|
switch (opt) {
|
||||||
cmdDecode(commandLine, config);
|
case "d":
|
||||||
cmdFound = true;
|
case "decode":
|
||||||
} else if (opt.equalsIgnoreCase("b") || opt.equalsIgnoreCase("build")) {
|
cmdDecode(commandLine, config);
|
||||||
cmdBuild(commandLine, config);
|
cmdFound = true;
|
||||||
cmdFound = true;
|
break;
|
||||||
} else if (opt.equalsIgnoreCase("if") || opt.equalsIgnoreCase("install-framework")) {
|
case "b":
|
||||||
cmdInstallFramework(commandLine, config);
|
case "build":
|
||||||
cmdFound = true;
|
cmdBuild(commandLine, config);
|
||||||
} else if (opt.equalsIgnoreCase("empty-framework-dir")) {
|
cmdFound = true;
|
||||||
cmdEmptyFrameworkDirectory(commandLine, config);
|
break;
|
||||||
cmdFound = true;
|
case "if":
|
||||||
} else if (opt.equalsIgnoreCase("list-frameworks")) {
|
case "install-framework":
|
||||||
cmdListFrameworks(commandLine, config);
|
cmdInstallFramework(commandLine, config);
|
||||||
cmdFound = true;
|
cmdFound = true;
|
||||||
} else if (opt.equalsIgnoreCase("publicize-resources")) {
|
break;
|
||||||
cmdPublicizeResources(commandLine, config);
|
case "empty-framework-dir":
|
||||||
cmdFound = true;
|
cmdEmptyFrameworkDirectory(commandLine, config);
|
||||||
|
cmdFound = true;
|
||||||
|
break;
|
||||||
|
case "list-frameworks":
|
||||||
|
cmdListFrameworks(commandLine, config);
|
||||||
|
cmdFound = true;
|
||||||
|
break;
|
||||||
|
case "publicize-resources":
|
||||||
|
cmdPublicizeResources(commandLine, config);
|
||||||
|
cmdFound = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +153,9 @@ public class Main {
|
|||||||
if (cli.hasOption("api") || cli.hasOption("api-level")) {
|
if (cli.hasOption("api") || cli.hasOption("api-level")) {
|
||||||
config.apiLevel = Integer.parseInt(cli.getOptionValue("api"));
|
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 {
|
private static void cmdDecode(CommandLine cli, Config config) throws AndrolibException {
|
||||||
@ -211,8 +237,7 @@ public class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExtFile apkFile = new ExtFile(apkName);
|
ExtFile apkFile = new ExtFile(apkName);
|
||||||
ApkDecoder decoder = new ApkDecoder(config, apkFile);
|
ApkDecoder decoder = new ApkDecoder(apkFile, config);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
decoder.decode(outDir);
|
decoder.decode(outDir);
|
||||||
} catch (OutDirExistsException ex) {
|
} catch (OutDirExistsException ex) {
|
||||||
@ -227,23 +252,17 @@ public class Main {
|
|||||||
System.exit(1);
|
System.exit(1);
|
||||||
} catch (CantFindFrameworkResException ex) {
|
} catch (CantFindFrameworkResException ex) {
|
||||||
System.err
|
System.err
|
||||||
.println("Can't find framework resources for package of id: "
|
.println("Could not find framework resources for package of id: "
|
||||||
+ ex.getPkgId()
|
+ ex.getPkgId()
|
||||||
+ ". You must install proper "
|
+ ". You must install proper "
|
||||||
+ "framework files, see project website for more info.");
|
+ "framework files, see project website for more info.");
|
||||||
System.exit(1);
|
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[] args = cli.getArgs();
|
||||||
String appDirName = args.length < 2 ? "." : args[1];
|
String apkDirName = args.length < 2 ? "." : args[1];
|
||||||
|
|
||||||
// check for build options
|
// check for build options
|
||||||
if (cli.hasOption("f") || cli.hasOption("force-all")) {
|
if (cli.hasOption("f") || cli.hasOption("force-all")) {
|
||||||
@ -259,7 +278,25 @@ public class Main {
|
|||||||
config.verbose = true;
|
config.verbose = true;
|
||||||
}
|
}
|
||||||
if (cli.hasOption("a") || cli.hasOption("aapt")) {
|
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")) {
|
if (cli.hasOption("c") || cli.hasOption("copy-original")) {
|
||||||
config.copyOriginalFiles = true;
|
config.copyOriginalFiles = true;
|
||||||
@ -267,13 +304,8 @@ public class Main {
|
|||||||
if (cli.hasOption("nc") || cli.hasOption("no-crunch")) {
|
if (cli.hasOption("nc") || cli.hasOption("no-crunch")) {
|
||||||
config.noCrunch = true;
|
config.noCrunch = true;
|
||||||
}
|
}
|
||||||
if (cli.hasOption("use-aapt1")) {
|
if (cli.hasOption("na") || cli.hasOption("no-apk")) {
|
||||||
config.useAapt2 = false;
|
config.noApk = true;
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
File outFile;
|
File outFile;
|
||||||
@ -283,21 +315,14 @@ public class Main {
|
|||||||
outFile = null;
|
outFile = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.netSecConf && !config.useAapt2) {
|
if (config.netSecConf && config.aaptVersion == 1) {
|
||||||
System.err.println("-n / --net-sec-conf is only supported with --use-aapt2.");
|
System.err.println("-n / --net-sec-conf is not supported with legacy aapt.");
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// try and build apk
|
ExtFile apkDir = new ExtFile(apkDirName);
|
||||||
try {
|
ApkBuilder builder = new ApkBuilder(apkDir, config);
|
||||||
if (cli.hasOption("a") || cli.hasOption("aapt")) {
|
builder.build(outFile);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void cmdInstallFramework(CommandLine cli, Config config) throws AndrolibException {
|
private static void cmdInstallFramework(CommandLine cli, Config config) throws AndrolibException {
|
||||||
@ -341,6 +366,13 @@ public class Main {
|
|||||||
.desc("Print advanced information.")
|
.desc("Print advanced information.")
|
||||||
.build();
|
.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")
|
Option noSrcOption = Option.builder("s")
|
||||||
.longOpt("no-src")
|
.longOpt("no-src")
|
||||||
.desc("Do not decode sources.")
|
.desc("Do not decode sources.")
|
||||||
@ -373,7 +405,7 @@ public class Main {
|
|||||||
|
|
||||||
Option analysisOption = Option.builder("m")
|
Option analysisOption = Option.builder("m")
|
||||||
.longOpt("match-original")
|
.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();
|
.build();
|
||||||
|
|
||||||
Option apiLevelOption = Option.builder("api")
|
Option apiLevelOption = Option.builder("api")
|
||||||
@ -452,13 +484,13 @@ public class Main {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
Option aapt1Option = Option.builder()
|
Option aapt1Option = Option.builder()
|
||||||
.longOpt("use-aapt1")
|
.longOpt("use-aapt1")
|
||||||
.desc("Use aapt binary instead of aapt2 during the build step.")
|
.desc("Use aapt binary instead of aapt2 during the build step.")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Option aapt2Option = Option.builder()
|
Option aapt2Option = Option.builder()
|
||||||
.longOpt("use-aapt2")
|
.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();
|
.build();
|
||||||
|
|
||||||
Option originalOption = Option.builder("c")
|
Option originalOption = Option.builder("c")
|
||||||
@ -471,6 +503,11 @@ public class Main {
|
|||||||
.desc("Disable crunching of resource files during the build step.")
|
.desc("Disable crunching of resource files during the build step.")
|
||||||
.build();
|
.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")
|
Option tagOption = Option.builder("t")
|
||||||
.longOpt("tag")
|
.longOpt("tag")
|
||||||
.desc("Tag frameworks using <tag>.")
|
.desc("Tag frameworks using <tag>.")
|
||||||
@ -482,7 +519,7 @@ public class Main {
|
|||||||
.longOpt("output")
|
.longOpt("output")
|
||||||
.desc("The name of apk that gets written. (default: dist/name.apk)")
|
.desc("The name of apk that gets written. (default: dist/name.apk)")
|
||||||
.hasArg(true)
|
.hasArg(true)
|
||||||
.argName("dir")
|
.argName("file")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Option outputDecOption = Option.builder("o")
|
Option outputDecOption = Option.builder("o")
|
||||||
@ -502,6 +539,7 @@ public class Main {
|
|||||||
|
|
||||||
// check for advance mode
|
// check for advance mode
|
||||||
if (isAdvanceMode()) {
|
if (isAdvanceMode()) {
|
||||||
|
decodeOptions.addOption(jobsOption);
|
||||||
decodeOptions.addOption(noDbgOption);
|
decodeOptions.addOption(noDbgOption);
|
||||||
decodeOptions.addOption(keepResOption);
|
decodeOptions.addOption(keepResOption);
|
||||||
decodeOptions.addOption(analysisOption);
|
decodeOptions.addOption(analysisOption);
|
||||||
@ -511,6 +549,7 @@ public class Main {
|
|||||||
decodeOptions.addOption(forceManOption);
|
decodeOptions.addOption(forceManOption);
|
||||||
decodeOptions.addOption(resolveResModeOption);
|
decodeOptions.addOption(resolveResModeOption);
|
||||||
|
|
||||||
|
buildOptions.addOption(jobsOption);
|
||||||
buildOptions.addOption(apiLevelOption);
|
buildOptions.addOption(apiLevelOption);
|
||||||
buildOptions.addOption(debugBuiOption);
|
buildOptions.addOption(debugBuiOption);
|
||||||
buildOptions.addOption(netSecConfOption);
|
buildOptions.addOption(netSecConfOption);
|
||||||
@ -518,6 +557,7 @@ public class Main {
|
|||||||
buildOptions.addOption(originalOption);
|
buildOptions.addOption(originalOption);
|
||||||
buildOptions.addOption(aapt1Option);
|
buildOptions.addOption(aapt1Option);
|
||||||
buildOptions.addOption(noCrunchOption);
|
buildOptions.addOption(noCrunchOption);
|
||||||
|
buildOptions.addOption(noApkOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add global options
|
// add global options
|
||||||
@ -561,6 +601,7 @@ public class Main {
|
|||||||
for (Option op : frameOptions.getOptions()) {
|
for (Option op : frameOptions.getOptions()) {
|
||||||
allOptions.addOption(op);
|
allOptions.addOption(op);
|
||||||
}
|
}
|
||||||
|
allOptions.addOption(jobsOption);
|
||||||
allOptions.addOption(apiLevelOption);
|
allOptions.addOption(apiLevelOption);
|
||||||
allOptions.addOption(analysisOption);
|
allOptions.addOption(analysisOption);
|
||||||
allOptions.addOption(debugDecOption);
|
allOptions.addOption(debugDecOption);
|
||||||
@ -578,6 +619,7 @@ public class Main {
|
|||||||
allOptions.addOption(aapt1Option);
|
allOptions.addOption(aapt1Option);
|
||||||
allOptions.addOption(aapt2Option);
|
allOptions.addOption(aapt2Option);
|
||||||
allOptions.addOption(noCrunchOption);
|
allOptions.addOption(noCrunchOption);
|
||||||
|
allOptions.addOption(noApkOption);
|
||||||
allOptions.addOption(onlyMainClassesOption);
|
allOptions.addOption(onlyMainClassesOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,8 +704,8 @@ public class Main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception exception) {
|
} catch (Exception ex) {
|
||||||
reportError(null, exception, ErrorManager.FORMAT_FAILURE);
|
reportError(null, ex, ErrorManager.FORMAT_FAILURE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,31 +730,4 @@ public class Main {
|
|||||||
private static void setAdvanceMode() {
|
private static void setAdvanceMode() {
|
||||||
Main.advanceMode = true;
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,10 @@
|
|||||||
val gitRevision: String by rootProject.extra
|
val gitRevision: String by rootProject.extra
|
||||||
val apktoolVersion: 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 {
|
tasks {
|
||||||
processResources {
|
processResources {
|
||||||
from("src/main/resources/properties") {
|
from("src/main/resources") {
|
||||||
include("**/*.properties")
|
include("apktool.properties")
|
||||||
into("properties")
|
|
||||||
expand("version" to apktoolVersion, "gitrev" to gitRevision)
|
expand("version" to apktoolVersion, "gitrev" to gitRevision)
|
||||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
}
|
}
|
||||||
@ -39,13 +24,13 @@ tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":brut.j.dir"))
|
|
||||||
api(project(":brut.j.util"))
|
|
||||||
api(project(":brut.j.common"))
|
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.baksmali)
|
||||||
implementation(libs.smali)
|
implementation(libs.smali)
|
||||||
implementation(libs.xmlpull)
|
|
||||||
implementation(libs.guava)
|
implementation(libs.guava)
|
||||||
implementation(libs.commons.lang3)
|
implementation(libs.commons.lang3)
|
||||||
implementation(libs.commons.io)
|
implementation(libs.commons.io)
|
||||||
@ -54,5 +39,15 @@ dependencies {
|
|||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.xmlunit)
|
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")
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import org.xmlpull.v1.XmlPullParser;
|
|||||||
public interface XmlResourceParser extends XmlPullParser, AttributeSet {
|
public interface XmlResourceParser extends XmlPullParser, AttributeSet {
|
||||||
/**
|
/**
|
||||||
* Close this interface to the resource. Calls on the interface are no
|
* Close this interface to the resource. Calls on the interface are no
|
||||||
* longer value after this call.
|
* longer valid after this call.
|
||||||
*/
|
*/
|
||||||
void close();
|
void close();
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ public class TypedValue {
|
|||||||
/** Identifies the end of plain integer values. */
|
/** Identifies the end of plain integer values. */
|
||||||
public static final int TYPE_LAST_INT = 0x1f;
|
public static final int TYPE_LAST_INT = 0x1f;
|
||||||
|
|
||||||
/* ------------------------------------------------------------ */
|
/* ------------------------------------------------------------ */
|
||||||
|
|
||||||
/** Complex data: bit location of unit information. */
|
/** Complex data: bit location of unit information. */
|
||||||
public static final int COMPLEX_UNIT_SHIFT = 0;
|
public static final int COMPLEX_UNIT_SHIFT = 0;
|
||||||
@ -182,7 +182,7 @@ public class TypedValue {
|
|||||||
*/
|
*/
|
||||||
public static final int COMPLEX_MANTISSA_MASK = 0xffffff;
|
public static final int COMPLEX_MANTISSA_MASK = 0xffffff;
|
||||||
|
|
||||||
/* ------------------------------------------------------------ */
|
/* ------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link #TYPE_NULL} data indicating the value was not specified.
|
* {@link #TYPE_NULL} data indicating the value was not specified.
|
||||||
@ -207,7 +207,7 @@ public class TypedValue {
|
|||||||
*/
|
*/
|
||||||
public static final int DENSITY_NONE = 0xffff;
|
public static final int DENSITY_NONE = 0xffff;
|
||||||
|
|
||||||
/* ------------------------------------------------------------ */
|
/* ------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type held by this value, as defined by the constants here. This tells
|
* The type held by this value, as defined by the constants here. This tells
|
||||||
@ -216,7 +216,7 @@ public class TypedValue {
|
|||||||
public int type;
|
public int type;
|
||||||
|
|
||||||
private static final float MANTISSA_MULT = 1.0f / (1 << TypedValue.COMPLEX_MANTISSA_SHIFT);
|
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,
|
MANTISSA_MULT, 1.0f / (1 << 7) * MANTISSA_MULT,
|
||||||
1.0f / (1 << 15) * MANTISSA_MULT, 1.0f / (1 << 23) * 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];
|
& TypedValue.COMPLEX_RADIX_MASK];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String[] DIMENSION_UNIT_STRS = new String[] { "px",
|
private static final String[] DIMENSION_UNIT_STRS = {
|
||||||
"dip", "sp", "pt", "in", "mm" };
|
"px", "dip", "sp", "pt", "in", "mm"
|
||||||
private static final String[] FRACTION_UNIT_STRS = new String[] { "%", "%p" };
|
};
|
||||||
|
private static final String[] FRACTION_UNIT_STRS = { "%", "%p" };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform type conversion as per coerceToString on an explicitly
|
* Perform type conversion as per coerceToString on an explicitly
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
*/
|
*/
|
||||||
package brut.androlib;
|
package brut.androlib;
|
||||||
|
|
||||||
import brut.androlib.exceptions.AndrolibException;
|
|
||||||
import brut.androlib.apk.ApkInfo;
|
import brut.androlib.apk.ApkInfo;
|
||||||
|
import brut.androlib.exceptions.AndrolibException;
|
||||||
import brut.common.BrutException;
|
import brut.common.BrutException;
|
||||||
import brut.util.AaptManager;
|
import brut.util.AaptManager;
|
||||||
import brut.util.OS;
|
import brut.util.OS;
|
||||||
@ -27,57 +27,52 @@ import java.util.*;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class AaptInvoker {
|
public class AaptInvoker {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(AaptInvoker.class.getName());
|
||||||
|
|
||||||
private final Config mConfig;
|
private final Config mConfig;
|
||||||
private final ApkInfo mApkInfo;
|
private final ApkInfo mApkInfo;
|
||||||
|
|
||||||
private final static Logger LOGGER = Logger.getLogger(AaptInvoker.class.getName());
|
|
||||||
|
|
||||||
public AaptInvoker(Config config, ApkInfo apkInfo) {
|
public AaptInvoker(Config config, ApkInfo apkInfo) {
|
||||||
mConfig = config;
|
mConfig = config;
|
||||||
mApkInfo = apkInfo;
|
mApkInfo = apkInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getAaptBinaryFile() throws AndrolibException {
|
public void invoke(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include)
|
||||||
try {
|
throws AndrolibException {
|
||||||
if (getAaptVersion() == 2) {
|
File aaptBinary = mConfig.aaptBinary;
|
||||||
return AaptManager.getAapt2();
|
|
||||||
|
List<String> cmd = new ArrayList<>();
|
||||||
|
String aaptPath;
|
||||||
|
boolean customAapt;
|
||||||
|
|
||||||
|
if (mConfig.aaptBinary != null) {
|
||||||
|
aaptPath = mConfig.aaptBinary.getPath();
|
||||||
|
customAapt = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
aaptPath = AaptManager.getAaptBinary(mConfig.aaptVersion).getPath();
|
||||||
|
customAapt = false;
|
||||||
|
} catch (BrutException ex) {
|
||||||
|
aaptPath = AaptManager.getAaptName(mConfig.aaptVersion);
|
||||||
|
customAapt = true;
|
||||||
|
LOGGER.warning(aaptPath + ": " + ex.getMessage() + " (defaulting to $PATH binary)");
|
||||||
}
|
}
|
||||||
return AaptManager.getAapt1();
|
|
||||||
} catch (BrutException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getAaptVersion() {
|
|
||||||
return mConfig.isAapt2() ? 2 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private File createDoNotCompressExtensionsFile(ApkInfo apkInfo) throws AndrolibException {
|
|
||||||
if (apkInfo.doNotCompress == null || apkInfo.doNotCompress.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
File doNotCompressFile;
|
cmd.add(aaptPath);
|
||||||
try {
|
|
||||||
doNotCompressFile = File.createTempFile("APKTOOL", null);
|
|
||||||
doNotCompressFile.deleteOnExit();
|
|
||||||
|
|
||||||
BufferedWriter fileWriter = new BufferedWriter(new FileWriter(doNotCompressFile));
|
switch (mConfig.aaptVersion) {
|
||||||
for (String extension : apkInfo.doNotCompress) {
|
case 2:
|
||||||
fileWriter.write(extension);
|
invokeAapt2(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt);
|
||||||
fileWriter.newLine();
|
break;
|
||||||
}
|
default:
|
||||||
fileWriter.close();
|
invokeAapt1(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt);
|
||||||
|
break;
|
||||||
return doNotCompressFile;
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeAapt2(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include,
|
private void invokeAapt2(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include,
|
||||||
List<String> cmd, boolean customAapt) throws AndrolibException {
|
List<String> cmd, boolean customAapt) throws AndrolibException {
|
||||||
|
|
||||||
List<String> compileCommand = new ArrayList<>(cmd);
|
List<String> compileCommand = new ArrayList<>(cmd);
|
||||||
File resourcesZip = null;
|
File resourcesZip = null;
|
||||||
|
|
||||||
@ -91,7 +86,6 @@ public class AaptInvoker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resDir != null && !resourcesZip.exists()) {
|
if (resDir != null && !resourcesZip.exists()) {
|
||||||
|
|
||||||
// Compile the files into flat arsc files
|
// Compile the files into flat arsc files
|
||||||
cmd.add("compile");
|
cmd.add("compile");
|
||||||
|
|
||||||
@ -135,7 +129,9 @@ public class AaptInvoker {
|
|||||||
cmd.add("-o");
|
cmd.add("-o");
|
||||||
cmd.add(apkFile.getAbsolutePath());
|
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("--package-id");
|
||||||
cmd.add(mApkInfo.packageInfo.forcedPackageId);
|
cmd.add(mApkInfo.packageInfo.forcedPackageId);
|
||||||
}
|
}
|
||||||
@ -178,8 +174,6 @@ public class AaptInvoker {
|
|||||||
cmd.add("--no-version-transitions");
|
cmd.add("--no-version-transitions");
|
||||||
cmd.add("--no-resource-deduping");
|
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
|
// TODO: Add this back, once AAPT2 from platform-tools 34.0.4 is stable
|
||||||
// cmd.add("--no-compile-sdk-metadata");
|
// cmd.add("--no-compile-sdk-metadata");
|
||||||
|
|
||||||
@ -190,27 +184,21 @@ public class AaptInvoker {
|
|||||||
cmd.add("--enable-sparse-encoding");
|
cmd.add("--enable-sparse-encoding");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mApkInfo.compactEntries) {
|
||||||
|
cmd.add("--enable-compact-entries");
|
||||||
|
}
|
||||||
|
|
||||||
if (mApkInfo.isFrameworkApk) {
|
if (mApkInfo.isFrameworkApk) {
|
||||||
cmd.add("-x");
|
cmd.add("-x");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mApkInfo.doNotCompress != null && !customAapt) {
|
if (!mApkInfo.featureFlags.isEmpty()) {
|
||||||
// Use custom -e option to avoid limits on commandline length.
|
List<String> featureFlags = new ArrayList<>();
|
||||||
// Can only be used when custom aapt binary is not used.
|
for (Map.Entry<String, Boolean> entry : mApkInfo.featureFlags.entrySet()) {
|
||||||
String extensionsFilePath =
|
featureFlags.add(entry.getKey() + "=" + entry.getValue());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
cmd.add("--feature-flags");
|
||||||
|
cmd.add(String.join(",", featureFlags));
|
||||||
if (!mApkInfo.resourcesAreCompressed) {
|
|
||||||
cmd.add("-0");
|
|
||||||
cmd.add("arsc");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include != null) {
|
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,
|
private void invokeAapt1(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include,
|
||||||
List<String> cmd, boolean customAapt) throws AndrolibException {
|
List<String> cmd, boolean customAapt) throws AndrolibException {
|
||||||
|
|
||||||
cmd.add("p");
|
cmd.add("p");
|
||||||
|
|
||||||
if (mConfig.verbose) { // output aapt verbose
|
if (mConfig.verbose) { // output aapt verbose
|
||||||
@ -269,7 +256,7 @@ public class AaptInvoker {
|
|||||||
}
|
}
|
||||||
// force package id so that some frameworks build with correct id
|
// 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)
|
// 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("--forced-package-id");
|
||||||
cmd.add(mApkInfo.packageInfo.forcedPackageId);
|
cmd.add(mApkInfo.packageInfo.forcedPackageId);
|
||||||
}
|
}
|
||||||
@ -316,25 +303,6 @@ public class AaptInvoker {
|
|||||||
cmd.add("-x");
|
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) {
|
if (include != null) {
|
||||||
for (File file : include) {
|
for (File file : include) {
|
||||||
cmd.add("-I");
|
cmd.add("-I");
|
||||||
@ -364,36 +332,4 @@ public class AaptInvoker {
|
|||||||
throw new AndrolibException(ex);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import brut.androlib.apk.ApkInfo;
|
|||||||
import brut.androlib.apk.UsesFramework;
|
import brut.androlib.apk.UsesFramework;
|
||||||
import brut.androlib.res.Framework;
|
import brut.androlib.res.Framework;
|
||||||
import brut.androlib.res.data.ResConfigFlags;
|
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.androlib.src.SmaliBuilder;
|
||||||
import brut.common.BrutException;
|
import brut.common.BrutException;
|
||||||
import brut.common.InvalidUnknownFileException;
|
import brut.common.InvalidUnknownFileException;
|
||||||
@ -31,10 +31,10 @@ import brut.directory.Directory;
|
|||||||
import brut.directory.DirectoryException;
|
import brut.directory.DirectoryException;
|
||||||
import brut.directory.ExtFile;
|
import brut.directory.ExtFile;
|
||||||
import brut.directory.ZipUtils;
|
import brut.directory.ZipUtils;
|
||||||
|
import brut.util.AaptManager;
|
||||||
import brut.util.BrutIO;
|
import brut.util.BrutIO;
|
||||||
import brut.util.OS;
|
import brut.util.OS;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
@ -42,134 +42,149 @@ import javax.xml.transform.TransformerException;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.logging.Logger;
|
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;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
public class ApkBuilder {
|
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 final ExtFile mApkDir;
|
||||||
private ApkInfo mApkInfo;
|
private final Config mConfig;
|
||||||
private int mMinSdkVersion = 0;
|
private final AtomicReference<AndrolibException> mBuildError;
|
||||||
|
|
||||||
private final static String APK_DIRNAME = "build/apk";
|
private ApkInfo mApkInfo;
|
||||||
private final static String UNK_DIRNAME = "unknown";
|
private int mMinSdkVersion;
|
||||||
private final static String[] APK_RESOURCES_FILENAMES = new String[] {
|
private BackgroundWorker mWorker;
|
||||||
"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" };
|
|
||||||
|
|
||||||
public ApkBuilder(ExtFile apkDir) {
|
public ApkBuilder(ExtFile apkDir) {
|
||||||
this(Config.getDefaultConfig(), apkDir);
|
this(apkDir, Config.getDefaultConfig());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApkBuilder(Config config, ExtFile apkDir) {
|
public ApkBuilder(ExtFile apkDir, Config config) {
|
||||||
mConfig = config;
|
|
||||||
mApkDir = apkDir;
|
mApkDir = apkDir;
|
||||||
|
mConfig = config;
|
||||||
|
mBuildError = new AtomicReference<>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void build(File outFile) throws BrutException {
|
public void build(File outApk) throws AndrolibException {
|
||||||
LOGGER.info("Using Apktool " + ApktoolProperties.getVersion());
|
if (mConfig.jobs > 1) {
|
||||||
|
mWorker = new BackgroundWorker(mConfig.jobs - 1);
|
||||||
mApkInfo = ApkInfo.load(mApkDir);
|
|
||||||
|
|
||||||
if (mApkInfo.getSdkInfo() != null && mApkInfo.getSdkInfo().get("minSdkVersion") != null) {
|
|
||||||
String minSdkVersion = mApkInfo.getSdkInfo().get("minSdkVersion");
|
|
||||||
mMinSdkVersion = mApkInfo.getMinSdkVersionFromAndroidCodename(minSdkVersion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outFile == null) {
|
|
||||||
String outFileName = mApkInfo.apkFileName;
|
|
||||||
outFile = new File(mApkDir, "dist" + File.separator + (outFileName == null ? "out.apk" : outFileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
new File(mApkDir, APK_DIRNAME).mkdirs();
|
|
||||||
File manifest = new File(mApkDir, "AndroidManifest.xml");
|
|
||||||
File manifestOriginal = new File(mApkDir, "AndroidManifest.xml.orig");
|
|
||||||
|
|
||||||
buildSources();
|
|
||||||
buildNonDefaultSources();
|
|
||||||
buildManifestFile(manifest, manifestOriginal);
|
|
||||||
buildResources();
|
|
||||||
buildLibs();
|
|
||||||
buildCopyOriginalFiles();
|
|
||||||
buildApk(outFile);
|
|
||||||
|
|
||||||
// we must go after the Apk is built, and copy the files in via Zip
|
|
||||||
// this is because Aapt won't add files it doesn't know (ex unknown files)
|
|
||||||
buildUnknownFiles(outFile);
|
|
||||||
|
|
||||||
// we copied the AndroidManifest.xml to AndroidManifest.xml.orig so we can edit it
|
|
||||||
// lets restore the unedited one, to not change the original
|
|
||||||
if (manifest.isFile() && manifest.exists() && manifestOriginal.isFile()) {
|
|
||||||
try {
|
|
||||||
if (new File(mApkDir, "AndroidManifest.xml").delete()) {
|
|
||||||
FileUtils.moveFile(manifestOriginal, manifest);
|
|
||||||
}
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new AndrolibException(ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOGGER.info("Built apk into: " + outFile.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildManifestFile(File manifest, File manifestOriginal) throws AndrolibException {
|
|
||||||
// If we decoded in "raw", we cannot patch AndroidManifest
|
|
||||||
if (new File(mApkDir, "resources.arsc").exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (manifest.isFile() && manifest.exists()) {
|
|
||||||
try {
|
|
||||||
if (manifestOriginal.exists()) {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
manifestOriginal.delete();
|
|
||||||
}
|
|
||||||
FileUtils.copyFile(manifest, manifestOriginal);
|
|
||||||
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifest);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new AndrolibException(ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildSources() throws AndrolibException {
|
|
||||||
if (!buildSourcesRaw("classes.dex") && !buildSourcesSmali("smali", "classes.dex")) {
|
|
||||||
LOGGER.warning("Could not find sources");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildNonDefaultSources() throws AndrolibException {
|
|
||||||
try {
|
try {
|
||||||
// loop through any smali_ directories for multi-dex apks
|
mApkInfo = ApkInfo.load(mApkDir);
|
||||||
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)) {
|
String minSdkVersion = mApkInfo.getMinSdkVersion();
|
||||||
LOGGER.warning("Could not find sources");
|
if (minSdkVersion != null) {
|
||||||
|
mMinSdkVersion = mApkInfo.getMinSdkVersionFromAndroidCodename(minSdkVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outApk == null) {
|
||||||
|
String outFileName = mApkInfo.apkFileName;
|
||||||
|
if (outFileName == null) {
|
||||||
|
outFileName = "out.apk";
|
||||||
|
}
|
||||||
|
outApk = new File(mApkDir, "dist" + File.separator + outFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
File outDir = new File(mApkDir, "build" + File.separator + "apk");
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
outDir.mkdirs();
|
||||||
|
|
||||||
|
File manifest = new File(mApkDir, "AndroidManifest.xml");
|
||||||
|
File manifestOrig = new File(mApkDir, "AndroidManifest.xml.orig");
|
||||||
|
|
||||||
|
LOGGER.info("Using Apktool " + ApktoolProperties.getVersion() + " on " + outApk.getName()
|
||||||
|
+ (mWorker != null ? " with " + mConfig.jobs + " threads" : ""));
|
||||||
|
|
||||||
|
buildSources(outDir);
|
||||||
|
backupManifestFile(manifest, manifestOrig);
|
||||||
|
buildResources(outDir, manifest);
|
||||||
|
|
||||||
|
if (mWorker != null) {
|
||||||
|
mWorker.waitForFinish();
|
||||||
|
if (mBuildError.get() != null) {
|
||||||
|
throw mBuildError.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mConfig.noApk) {
|
||||||
|
if (outApk.exists()) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
outApk.delete();
|
||||||
|
} else {
|
||||||
|
File parentDir = outApk.getParentFile();
|
||||||
|
if (parentDir != null && !parentDir.exists()) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
parentDir.mkdirs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyOriginalFiles(outDir);
|
||||||
|
|
||||||
|
LOGGER.info("Building apk file...");
|
||||||
|
|
||||||
|
try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(outApk.toPath()))) {
|
||||||
|
// zip aapt output files
|
||||||
|
try {
|
||||||
|
ZipUtils.zipDir(outDir, out, mApkInfo.doNotCompress);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// zip remaining standard files
|
||||||
|
importRawFiles(out);
|
||||||
|
|
||||||
|
// zip unknown files
|
||||||
|
importUnknownFiles(out);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Built apk into: " + outApk.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
// we copied the AndroidManifest.xml to AndroidManifest.xml.orig so we can edit it
|
||||||
|
// lets restore the unedited one, to not change the original
|
||||||
|
if (manifest.isFile() && manifestOrig.isFile()) {
|
||||||
|
try {
|
||||||
|
if (manifest.delete()) {
|
||||||
|
FileUtils.moveFile(manifestOrig, manifest);
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mWorker != null) {
|
||||||
|
mWorker.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildSources(File outDir) throws AndrolibException {
|
||||||
|
if (!copySourcesRaw(outDir, "classes.dex")) {
|
||||||
|
buildSourcesSmali(outDir, "smali", "classes.dex");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Directory in = mApkDir.getDirectory();
|
||||||
|
|
||||||
|
// loop through any smali_ directories for multi-dex apks
|
||||||
|
for (String dirName : in.getDirs().keySet()) {
|
||||||
|
if (dirName.startsWith("smali_")) {
|
||||||
|
String fileName = dirName.substring(dirName.indexOf("_") + 1) + ".dex";
|
||||||
|
if (!copySourcesRaw(outDir, fileName)) {
|
||||||
|
buildSourcesSmali(outDir, dirName, fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// loop through any classes#.dex files for multi-dex apks
|
// loop through any classes#.dex files for multi-dex apks
|
||||||
File[] dexFiles = mApkDir.listFiles();
|
for (String fileName : in.getFiles()) {
|
||||||
if (dexFiles != null) {
|
// skip classes.dex because we have handled it
|
||||||
for (File dex : dexFiles) {
|
if (fileName.endsWith(".dex") && !fileName.equals("classes.dex")) {
|
||||||
// skip classes.dex because we have handled it in buildSources()
|
copySourcesRaw(outDir, fileName);
|
||||||
if (dex.getName().endsWith(".dex") && !dex.getName().equalsIgnoreCase("classes.dex")) {
|
|
||||||
buildSourcesRaw(dex.getName());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (DirectoryException ex) {
|
} catch (DirectoryException ex) {
|
||||||
@ -177,365 +192,302 @@ public class ApkBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean buildSourcesRaw(String filename) throws AndrolibException {
|
private boolean copySourcesRaw(File outDir, String fileName) throws AndrolibException {
|
||||||
File working = new File(mApkDir, filename);
|
File working = new File(mApkDir, fileName);
|
||||||
if (!working.exists()) {
|
if (!working.isFile()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
File stored = new File(mApkDir, APK_DIRNAME + "/" + filename);
|
|
||||||
if (mConfig.forceBuildAll || isModified(working, stored)) {
|
File stored = new File(outDir, fileName);
|
||||||
LOGGER.info("Copying " + mApkDir.toString() + " " + filename + " file...");
|
if (!mConfig.forceBuildAll && !isModified(working, stored)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Copying raw " + fileName + " file...");
|
||||||
|
try {
|
||||||
|
BrutIO.copyAndClose(Files.newInputStream(working.toPath()), Files.newOutputStream(stored.toPath()));
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildSourcesSmali(File outDir, String dirName, String fileName) throws AndrolibException {
|
||||||
|
if (mWorker != null) {
|
||||||
|
mWorker.submit(() -> {
|
||||||
|
if (mBuildError.get() == null) {
|
||||||
|
try {
|
||||||
|
buildSourcesSmaliJob(outDir, dirName, fileName);
|
||||||
|
} catch (AndrolibException ex) {
|
||||||
|
mBuildError.compareAndSet(null, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
buildSourcesSmaliJob(outDir, dirName, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildSourcesSmaliJob(File outDir, String dirName, String fileName) throws AndrolibException {
|
||||||
|
File smaliDir = new File(mApkDir, dirName);
|
||||||
|
if (!smaliDir.isDirectory()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File dex = new File(outDir, fileName);
|
||||||
|
if (!mConfig.forceBuildAll) {
|
||||||
|
LOGGER.info("Checking whether sources have changed...");
|
||||||
|
if (!isModified(smaliDir, dex)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
dex.delete();
|
||||||
|
|
||||||
|
LOGGER.info("Smaling " + dirName + " folder into " + fileName + "...");
|
||||||
|
int apiLevel = mConfig.apiLevel > 0 ? mConfig.apiLevel : mMinSdkVersion;
|
||||||
|
SmaliBuilder builder = new SmaliBuilder(smaliDir, apiLevel);
|
||||||
|
builder.build(dex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void backupManifestFile(File manifest, File manifestOrig) throws AndrolibException {
|
||||||
|
// if we decoded in "raw", we cannot patch AndroidManifest
|
||||||
|
if (new File(mApkDir, "resources.arsc").isFile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifest.isFile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifestOrig.exists()) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
manifestOrig.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileUtils.copyFile(manifest, manifestOrig);
|
||||||
|
ResXmlUtils.fixingPublicAttrsInProviderAttributes(manifest);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildResources(File outDir, File manifest) throws AndrolibException {
|
||||||
|
if (!manifest.isFile()) {
|
||||||
|
LOGGER.fine("Could not find AndroidManifest.xml");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new File(mApkDir, "resources.arsc").isFile()) {
|
||||||
|
copyResourcesRaw(outDir, manifest);
|
||||||
|
} else if (new File(mApkDir, "res").isDirectory()) {
|
||||||
|
buildResourcesFull(outDir, manifest);
|
||||||
|
} else {
|
||||||
|
LOGGER.fine("Could not find resources");
|
||||||
|
buildManifest(outDir, manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyResourcesRaw(File outDir, File manifest) throws AndrolibException {
|
||||||
|
if (!mConfig.forceBuildAll) {
|
||||||
|
LOGGER.info("Checking whether resources have changed...");
|
||||||
|
if (!isModified(manifest, new File(outDir, "AndroidManifest.xml"))
|
||||||
|
&& !isModified(new File(mApkDir, "resources.arsc"), new File(outDir, "resources.arsc"))
|
||||||
|
&& !isModified(newFiles(mApkDir, ApkInfo.RESOURCES_DIRNAMES),
|
||||||
|
newFiles(outDir, ApkInfo.RESOURCES_DIRNAMES))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Copying raw resources...");
|
||||||
|
try {
|
||||||
|
Directory in = mApkDir.getDirectory();
|
||||||
|
|
||||||
|
in.copyToDir(outDir, "AndroidManifest.xml");
|
||||||
|
in.copyToDir(outDir, "resources.arsc");
|
||||||
|
in.copyToDir(outDir, ApkInfo.RESOURCES_DIRNAMES);
|
||||||
|
} catch (DirectoryException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildResourcesFull(File outDir, File manifest) throws AndrolibException {
|
||||||
|
File resourcesFile = new File(outDir.getParentFile(), "resources.zip");
|
||||||
|
if (!mConfig.forceBuildAll) {
|
||||||
|
LOGGER.info("Checking whether resources have changed...");
|
||||||
|
if (!isModified(manifest, new File(outDir, "AndroidManifest.xml"))
|
||||||
|
&& !isModified(newFiles(mApkDir, ApkInfo.RESOURCES_DIRNAMES),
|
||||||
|
newFiles(outDir, ApkInfo.RESOURCES_DIRNAMES))
|
||||||
|
&& (mConfig.aaptVersion == 1 || resourcesFile.isFile())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
resourcesFile.delete();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mConfig.debugMode) {
|
||||||
|
if (mConfig.aaptVersion == 2) {
|
||||||
|
LOGGER.info("Setting 'debuggable' attribute to 'true' in AndroidManifest.xml");
|
||||||
|
ResXmlUtils.setApplicationDebugTagTrue(manifest);
|
||||||
|
} else {
|
||||||
|
ResXmlUtils.removeApplicationDebugTag(manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mConfig.netSecConf) {
|
||||||
|
String targetSdkVersion = mApkInfo.getTargetSdkVersion();
|
||||||
|
if (targetSdkVersion != null) {
|
||||||
|
if (Integer.parseInt(targetSdkVersion) < ResConfigFlags.SDK_NOUGAT) {
|
||||||
|
LOGGER.warning("Target SDK version is lower than 24! Network Security Configuration might be ignored!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File netSecConfOrig = new File(mApkDir, "res/xml/network_security_config.xml");
|
||||||
|
ResXmlUtils.modNetworkSecurityConfig(netSecConfOrig);
|
||||||
|
ResXmlUtils.setNetworkSecurityConfig(manifest);
|
||||||
|
LOGGER.info("Added permissive network security config in manifest");
|
||||||
|
}
|
||||||
|
} catch (IOException | ParserConfigurationException | TransformerException | SAXException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtFile tmpFile;
|
||||||
|
try {
|
||||||
|
tmpFile = new ExtFile(File.createTempFile("APKTOOL", null));
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
tmpFile.delete();
|
||||||
|
|
||||||
|
File resDir = new File(mApkDir, "res");
|
||||||
|
File ninePatch = new File(mApkDir, "9patch");
|
||||||
|
if (!ninePatch.isDirectory()) {
|
||||||
|
ninePatch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Building resources with " + AaptManager.getAaptName(mConfig.aaptVersion) + "...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
AaptInvoker invoker = new AaptInvoker(mConfig, mApkInfo);
|
||||||
|
invoker.invoke(tmpFile, manifest, resDir, ninePatch, null, getIncludeFiles());
|
||||||
|
|
||||||
|
Directory tmpDir = tmpFile.getDirectory();
|
||||||
|
tmpDir.copyToDir(outDir, "AndroidManifest.xml");
|
||||||
|
tmpDir.copyToDir(outDir, "resources.arsc");
|
||||||
|
tmpDir.copyToDir(outDir, ApkInfo.RESOURCES_DIRNAMES);
|
||||||
|
} catch (DirectoryException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
} finally {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
tmpFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildManifest(File outDir, File manifest) throws AndrolibException {
|
||||||
|
if (!mConfig.forceBuildAll) {
|
||||||
|
LOGGER.info("Checking whether AndroidManifest.xml has changed...");
|
||||||
|
if (!isModified(manifest, new File(outDir, "AndroidManifest.xml"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtFile tmpFile;
|
||||||
|
try {
|
||||||
|
tmpFile = new ExtFile(File.createTempFile("APKTOOL", null));
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
tmpFile.delete();
|
||||||
|
|
||||||
|
File ninePatch = new File(mApkDir, "9patch");
|
||||||
|
if (!ninePatch.isDirectory()) {
|
||||||
|
ninePatch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Building AndroidManifest.xml with " + AaptManager.getAaptName(mConfig.aaptVersion) + "...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
AaptInvoker invoker = new AaptInvoker(mConfig, mApkInfo);
|
||||||
|
invoker.invoke(tmpFile, manifest, null, ninePatch, null, getIncludeFiles());
|
||||||
|
|
||||||
|
Directory tmpDir = tmpFile.getDirectory();
|
||||||
|
tmpDir.copyToDir(outDir, "AndroidManifest.xml");
|
||||||
|
} catch (DirectoryException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
} catch (AndrolibException ex) {
|
||||||
|
LOGGER.warning("Parse AndroidManifest.xml failed, treat it as raw file.");
|
||||||
|
copyManifestRaw(outDir);
|
||||||
|
} finally {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
tmpFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyManifestRaw(File outDir) throws AndrolibException {
|
||||||
|
LOGGER.info("Copying raw manifest...");
|
||||||
|
try {
|
||||||
|
Directory in = mApkDir.getDirectory();
|
||||||
|
|
||||||
|
in.copyToDir(outDir, "AndroidManifest.xml");
|
||||||
|
} catch (DirectoryException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyOriginalFiles(File outDir) throws AndrolibException {
|
||||||
|
if (!mConfig.copyOriginalFiles) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtFile originalDir = new ExtFile(mApkDir, "original");
|
||||||
|
if (!originalDir.isDirectory()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Copying original files...");
|
||||||
|
try {
|
||||||
|
Directory in = originalDir.getDirectory();
|
||||||
|
|
||||||
|
for (String fileName : in.getFiles(true)) {
|
||||||
|
if (ApkInfo.ORIGINAL_FILENAMES_PATTERN.matcher(fileName).matches()) {
|
||||||
|
in.copyToDir(outDir, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (DirectoryException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importRawFiles(ZipOutputStream out) throws AndrolibException {
|
||||||
|
for (String dirName : ApkInfo.RAW_DIRNAMES) {
|
||||||
|
File rawDir = new File(mApkDir, dirName);
|
||||||
|
if (!rawDir.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Importing " + dirName + "...");
|
||||||
try {
|
try {
|
||||||
BrutIO.copyAndClose(Files.newInputStream(working.toPath()), Files.newOutputStream(stored.toPath()));
|
ZipUtils.zipDir(mApkDir, dirName, out, mApkInfo.doNotCompress);
|
||||||
return true;
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new AndrolibException(ex);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean buildSourcesSmali(String folder, String filename) throws AndrolibException {
|
private void importUnknownFiles(ZipOutputStream out) throws AndrolibException {
|
||||||
ExtFile smaliDir = new ExtFile(mApkDir, folder);
|
File unknownDir = new File(mApkDir, "unknown");
|
||||||
if (!smaliDir.exists()) {
|
if (!unknownDir.isDirectory()) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
File dex = new File(mApkDir, APK_DIRNAME + "/" + filename);
|
|
||||||
if (!mConfig.forceBuildAll) {
|
|
||||||
LOGGER.info("Checking whether sources has changed...");
|
|
||||||
}
|
|
||||||
if (mConfig.forceBuildAll || isModified(smaliDir, dex)) {
|
|
||||||
LOGGER.info("Smaling " + folder + " folder into " + filename + "...");
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
dex.delete();
|
|
||||||
SmaliBuilder.build(smaliDir, dex, mConfig.apiLevel > 0 ? mConfig.apiLevel : mMinSdkVersion);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildResources() throws BrutException {
|
|
||||||
if (!buildResourcesRaw() && !buildResourcesFull() && !buildManifest()) {
|
|
||||||
LOGGER.warning("Could not find resources");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean buildResourcesRaw() throws AndrolibException {
|
|
||||||
try {
|
|
||||||
if (!new File(mApkDir, "resources.arsc").exists()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
File apkDir = new File(mApkDir, APK_DIRNAME);
|
|
||||||
if (!mConfig.forceBuildAll) {
|
|
||||||
LOGGER.info("Checking whether resources has changed...");
|
|
||||||
}
|
|
||||||
if (mConfig.forceBuildAll || isModified(newFiles(APK_RESOURCES_FILENAMES, mApkDir),
|
|
||||||
newFiles(APK_RESOURCES_FILENAMES, apkDir))) {
|
|
||||||
LOGGER.info("Copying raw resources...");
|
|
||||||
mApkDir.getDirectory().copyToDir(apkDir, APK_RESOURCES_FILENAMES);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (DirectoryException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean buildResourcesFull() throws AndrolibException {
|
|
||||||
try {
|
|
||||||
if (!new File(mApkDir, "res").exists()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!mConfig.forceBuildAll) {
|
|
||||||
LOGGER.info("Checking whether resources has changed...");
|
|
||||||
}
|
|
||||||
File apkDir = new File(mApkDir, APK_DIRNAME);
|
|
||||||
File resourceFile = new File(apkDir.getParent(), "resources.zip");
|
|
||||||
|
|
||||||
if (mConfig.forceBuildAll || isModified(newFiles(APP_RESOURCES_FILENAMES, mApkDir),
|
|
||||||
newFiles(APK_RESOURCES_FILENAMES, apkDir)) || (mConfig.isAapt2() && !isFile(resourceFile))) {
|
|
||||||
LOGGER.info("Building resources...");
|
|
||||||
|
|
||||||
if (mConfig.debugMode) {
|
|
||||||
if (mConfig.isAapt2()) {
|
|
||||||
LOGGER.info("Using aapt2 - setting 'debuggable' attribute to 'true' in AndroidManifest.xml");
|
|
||||||
ResXmlPatcher.setApplicationDebugTagTrue(new File(mApkDir, "AndroidManifest.xml"));
|
|
||||||
} else {
|
|
||||||
ResXmlPatcher.removeApplicationDebugTag(new File(mApkDir, "AndroidManifest.xml"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mConfig.netSecConf) {
|
|
||||||
ApkInfo meta = ApkInfo.load(new ExtFile(mApkDir));
|
|
||||||
if (meta.getSdkInfo() != null && meta.getSdkInfo().get("targetSdkVersion") != null) {
|
|
||||||
if (Integer.parseInt(meta.getSdkInfo().get("targetSdkVersion")) < ResConfigFlags.SDK_NOUGAT) {
|
|
||||||
LOGGER.warning("Target SDK version is lower than 24! Network Security Configuration might be ignored!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File netSecConfOrig = new File(mApkDir, "res/xml/network_security_config.xml");
|
|
||||||
if (netSecConfOrig.exists()) {
|
|
||||||
LOGGER.info("Replacing existing network_security_config.xml!");
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
netSecConfOrig.delete();
|
|
||||||
}
|
|
||||||
ResXmlPatcher.modNetworkSecurityConfig(netSecConfOrig);
|
|
||||||
ResXmlPatcher.setNetworkSecurityConfig(new File(mApkDir, "AndroidManifest.xml"));
|
|
||||||
LOGGER.info("Added permissive network security config in manifest");
|
|
||||||
}
|
|
||||||
|
|
||||||
File apkFile = File.createTempFile("APKTOOL", null);
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
apkFile.delete();
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
resourceFile.delete();
|
|
||||||
|
|
||||||
File ninePatch = new File(mApkDir, "9patch");
|
|
||||||
if (!ninePatch.exists()) {
|
|
||||||
ninePatch = null;
|
|
||||||
}
|
|
||||||
AaptInvoker invoker = new AaptInvoker(mConfig, mApkInfo);
|
|
||||||
invoker.invokeAapt(apkFile, new File(mApkDir, "AndroidManifest.xml"),
|
|
||||||
new File(mApkDir, "res"), ninePatch, null, getIncludeFiles());
|
|
||||||
|
|
||||||
ExtFile tmpExtFile = new ExtFile(apkFile);
|
|
||||||
Directory tmpDir = tmpExtFile.getDirectory();
|
|
||||||
|
|
||||||
// Sometimes an application is built with a resources.arsc file with no resources,
|
|
||||||
// Apktool assumes it will have a rebuilt arsc file, when it doesn't. So if we
|
|
||||||
// encounter a copy error, move to a warning and continue on. (#1730)
|
|
||||||
try {
|
|
||||||
tmpDir.copyToDir(apkDir,
|
|
||||||
tmpDir.containsDir("res") ? APK_RESOURCES_FILENAMES
|
|
||||||
: APK_RESOURCES_WITHOUT_RES_FILENAMES);
|
|
||||||
} catch (DirectoryException ex) {
|
|
||||||
LOGGER.warning(ex.getMessage());
|
|
||||||
} finally {
|
|
||||||
tmpExtFile.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete tmpDir
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
apkFile.delete();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (IOException | BrutException | ParserConfigurationException | TransformerException | SAXException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean buildManifestRaw() throws AndrolibException {
|
|
||||||
try {
|
|
||||||
File apkDir = new File(mApkDir, APK_DIRNAME);
|
|
||||||
LOGGER.info("Copying raw AndroidManifest.xml...");
|
|
||||||
mApkDir.getDirectory().copyToDir(apkDir, APK_MANIFEST_FILENAMES);
|
|
||||||
return true;
|
|
||||||
} catch (DirectoryException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean buildManifest() throws BrutException {
|
|
||||||
try {
|
|
||||||
if (!new File(mApkDir, "AndroidManifest.xml").exists()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!mConfig.forceBuildAll) {
|
|
||||||
LOGGER.info("Checking whether resources has changed...");
|
|
||||||
}
|
|
||||||
|
|
||||||
File apkDir = new File(mApkDir, APK_DIRNAME);
|
|
||||||
|
|
||||||
if (mConfig.forceBuildAll || isModified(newFiles(APK_MANIFEST_FILENAMES, mApkDir),
|
|
||||||
newFiles(APK_MANIFEST_FILENAMES, apkDir))) {
|
|
||||||
LOGGER.info("Building AndroidManifest.xml...");
|
|
||||||
|
|
||||||
File apkFile = File.createTempFile("APKTOOL", null);
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
apkFile.delete();
|
|
||||||
|
|
||||||
File ninePatch = new File(mApkDir, "9patch");
|
|
||||||
if (!ninePatch.exists()) {
|
|
||||||
ninePatch = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
AaptInvoker invoker = new AaptInvoker(mConfig, mApkInfo);
|
|
||||||
invoker.invokeAapt(apkFile, new File(mApkDir, "AndroidManifest.xml"),
|
|
||||||
null, ninePatch, null, getIncludeFiles());
|
|
||||||
|
|
||||||
Directory tmpDir = new ExtFile(apkFile).getDirectory();
|
|
||||||
tmpDir.copyToDir(apkDir, APK_MANIFEST_FILENAMES);
|
|
||||||
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
apkFile.delete();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (IOException | DirectoryException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
|
||||||
} catch (AndrolibException ex) {
|
|
||||||
LOGGER.warning("Parse AndroidManifest.xml failed, treat it as raw file.");
|
|
||||||
return buildManifestRaw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildLibs() throws AndrolibException {
|
|
||||||
buildLibrary("lib");
|
|
||||||
buildLibrary("libs");
|
|
||||||
buildLibrary("kotlin");
|
|
||||||
buildLibrary("META-INF/services");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildLibrary(String folder) throws AndrolibException {
|
|
||||||
File working = new File(mApkDir, folder);
|
|
||||||
|
|
||||||
if (!working.exists()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
File stored = new File(mApkDir, APK_DIRNAME + "/" + folder);
|
LOGGER.info("Importing unknown files...");
|
||||||
if (mConfig.forceBuildAll || isModified(working, stored)) {
|
|
||||||
LOGGER.info("Copying libs... (/" + folder + ")");
|
|
||||||
try {
|
|
||||||
OS.rmdir(stored);
|
|
||||||
OS.cpdir(working, stored);
|
|
||||||
} catch (BrutException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildCopyOriginalFiles() throws AndrolibException {
|
|
||||||
if (mConfig.copyOriginalFiles) {
|
|
||||||
File originalDir = new File(mApkDir, "original");
|
|
||||||
if (originalDir.exists()) {
|
|
||||||
try {
|
|
||||||
LOGGER.info("Copy original files...");
|
|
||||||
Directory in = (new ExtFile(originalDir)).getDirectory();
|
|
||||||
if (in.containsFile("AndroidManifest.xml")) {
|
|
||||||
LOGGER.info("Copy AndroidManifest.xml...");
|
|
||||||
in.copyToDir(new File(mApkDir, APK_DIRNAME), "AndroidManifest.xml");
|
|
||||||
}
|
|
||||||
if (in.containsFile("stamp-cert-sha256")) {
|
|
||||||
LOGGER.info("Copy stamp-cert-sha256...");
|
|
||||||
in.copyToDir(new File(mApkDir, APK_DIRNAME), "stamp-cert-sha256");
|
|
||||||
}
|
|
||||||
if (in.containsDir("META-INF")) {
|
|
||||||
LOGGER.info("Copy META-INF...");
|
|
||||||
in.copyToDir(new File(mApkDir, APK_DIRNAME), "META-INF");
|
|
||||||
}
|
|
||||||
} catch (DirectoryException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildUnknownFiles(File outFile) throws AndrolibException {
|
|
||||||
if (mApkInfo.unknownFiles != null) {
|
|
||||||
LOGGER.info("Copying unknown files/dir...");
|
|
||||||
|
|
||||||
Map<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) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
ZipUtils.zipFolders(rawDir, apkFile, assetDir, mApkInfo.doNotCompress);
|
ZipUtils.zipDir(unknownDir, out, mApkInfo.doNotCompress);
|
||||||
} catch (IOException | BrutException ex) {
|
} catch (IOException ex) {
|
||||||
throw new AndrolibException(ex);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -562,11 +514,7 @@ public class ApkBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isModified(File working, File stored) {
|
private boolean isModified(File working, File stored) {
|
||||||
return !stored.exists() || BrutIO.recursiveModifiedTime(working) > BrutIO .recursiveModifiedTime(stored);
|
return !stored.exists() || BrutIO.recursiveModifiedTime(working) > BrutIO.recursiveModifiedTime(stored);
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFile(File working) {
|
|
||||||
return working.exists();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isModified(File[] working, File[] stored) {
|
private boolean isModified(File[] working, File[] stored) {
|
||||||
@ -578,29 +526,11 @@ public class ApkBuilder {
|
|||||||
return BrutIO.recursiveModifiedTime(working) > BrutIO.recursiveModifiedTime(stored);
|
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];
|
File[] files = new File[names.length];
|
||||||
for (int i = 0; i < names.length; i++) {
|
for (int i = 0; i < names.length; i++) {
|
||||||
files[i] = new File(dir, names[i]);
|
files[i] = new File(dir, names[i]);
|
||||||
}
|
}
|
||||||
return files;
|
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -32,60 +32,56 @@ import org.apache.commons.io.FilenameUtils;
|
|||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class ApkDecoder {
|
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 Config mConfig;
|
||||||
private final ApkInfo mApkInfo;
|
private final AtomicReference<AndrolibException> mBuildError;
|
||||||
private int mMinSdkVersion = 0;
|
|
||||||
|
|
||||||
private final static String SMALI_DIRNAME = "smali";
|
private ApkInfo mApkInfo;
|
||||||
private final static String UNK_DIRNAME = "unknown";
|
private ResourcesDecoder mResDecoder;
|
||||||
private final static String[] APK_STANDARD_ALL_FILENAMES = new String[] {
|
private volatile int mMinSdkVersion;
|
||||||
"classes.dex", "AndroidManifest.xml", "resources.arsc", "res", "r", "R",
|
private BackgroundWorker mWorker;
|
||||||
"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));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApkDecoder(ExtFile apkFile) {
|
public ApkDecoder(ExtFile apkFile) {
|
||||||
this(Config.getDefaultConfig(), apkFile);
|
this(apkFile, Config.getDefaultConfig());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApkDecoder(Config config, File apkFile) {
|
public ApkDecoder(ApkInfo apkInfo, Config config) {
|
||||||
this(config, new ExtFile(apkFile));
|
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;
|
mApkInfo = apkInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApkInfo decode(File outDir) throws AndrolibException, IOException, DirectoryException {
|
public ApkDecoder(ExtFile apkFile, Config config) {
|
||||||
ExtFile apkFile = mApkInfo.getApkFile();
|
mApkFile = apkFile;
|
||||||
try {
|
mConfig = config;
|
||||||
if (!mConfig.forceDelete && outDir.exists()) {
|
mBuildError = new AtomicReference<>(null);
|
||||||
throw new OutDirExistsException();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!apkFile.isFile() || !apkFile.canRead()) {
|
public ApkInfo decode(File outDir) throws AndrolibException {
|
||||||
throw new InFileNotFoundException();
|
if (!mConfig.forceDelete && outDir.exists()) {
|
||||||
}
|
throw new OutDirExistsException();
|
||||||
|
}
|
||||||
|
if (!mApkFile.isFile() || !mApkFile.canRead()) {
|
||||||
|
throw new InFileNotFoundException();
|
||||||
|
}
|
||||||
|
if (mConfig.jobs > 1) {
|
||||||
|
mWorker = new BackgroundWorker(mConfig.jobs - 1);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (mApkInfo == null) mApkInfo = new ApkInfo(mApkFile);
|
||||||
|
mResDecoder = new ResourcesDecoder(mConfig, mApkInfo);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OS.rmdir(outDir);
|
OS.rmdir(outDir);
|
||||||
@ -95,192 +91,198 @@ public class ApkDecoder {
|
|||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
outDir.mkdirs();
|
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()) {
|
if (mWorker != null) {
|
||||||
switch (mConfig.decodeResources) {
|
mWorker.waitForFinish();
|
||||||
case Config.DECODE_RESOURCES_NONE:
|
if (mBuildError.get() != null) {
|
||||||
copyResourcesRaw(outDir);
|
throw mBuildError.get();
|
||||||
break;
|
|
||||||
case Config.DECODE_RESOURCES_FULL:
|
|
||||||
resourcesDecoder.decodeResources(outDir);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mApkInfo.hasManifest()) {
|
copyOriginalFiles(outDir);
|
||||||
if (mConfig.decodeResources == Config.DECODE_RESOURCES_FULL ||
|
|
||||||
mConfig.forceDecodeManifest == Config.FORCE_DECODE_MANIFEST_FULL) {
|
|
||||||
resourcesDecoder.decodeManifest(outDir);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
copyManifestRaw(outDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resourcesDecoder.updateApkInfo(outDir);
|
|
||||||
|
|
||||||
if (mApkInfo.hasSources()) {
|
|
||||||
switch (mConfig.decodeSources) {
|
|
||||||
case Config.DECODE_SOURCES_NONE:
|
|
||||||
copySourcesRaw(outDir, "classes.dex");
|
|
||||||
break;
|
|
||||||
case Config.DECODE_SOURCES_SMALI:
|
|
||||||
case Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
|
|
||||||
decodeSourcesSmali(outDir, "classes.dex");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mApkInfo.hasMultipleSources()) {
|
|
||||||
// foreach unknown dex file in root, lets disassemble it
|
|
||||||
Set<String> files = apkFile.getDirectory().getFiles(true);
|
|
||||||
for (String file : files) {
|
|
||||||
if (file.endsWith(".dex")) {
|
|
||||||
if (!file.equalsIgnoreCase("classes.dex")) {
|
|
||||||
switch(mConfig.decodeSources) {
|
|
||||||
case Config.DECODE_SOURCES_NONE:
|
|
||||||
copySourcesRaw(outDir, file);
|
|
||||||
break;
|
|
||||||
case Config.DECODE_SOURCES_SMALI:
|
|
||||||
decodeSourcesSmali(outDir, file);
|
|
||||||
break;
|
|
||||||
case Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
|
|
||||||
if (file.startsWith("classes") && file.endsWith(".dex")) {
|
|
||||||
decodeSourcesSmali(outDir, file);
|
|
||||||
} else {
|
|
||||||
copySourcesRaw(outDir, file);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case we have no resources. We should store the minSdk we pulled from the source opcode api level
|
|
||||||
if (!mApkInfo.hasResources() && mMinSdkVersion > 0) {
|
|
||||||
mApkInfo.setSdkInfoField("minSdkVersion", Integer.toString(mMinSdkVersion));
|
|
||||||
}
|
|
||||||
|
|
||||||
copyRawFiles(outDir);
|
copyRawFiles(outDir);
|
||||||
copyUnknownFiles(outDir);
|
copyUnknownFiles(outDir);
|
||||||
recordUncompressedFiles(resourcesDecoder.getResFileMapping());
|
|
||||||
copyOriginalFiles(outDir);
|
|
||||||
writeApkInfo(outDir);
|
writeApkInfo(outDir);
|
||||||
|
|
||||||
return mApkInfo;
|
return mApkInfo;
|
||||||
} finally {
|
} finally {
|
||||||
|
if (mWorker != null) {
|
||||||
|
mWorker.shutdownNow();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
apkFile.close();
|
mApkFile.close();
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeApkInfo(File outDir) throws AndrolibException {
|
private void decodeSources(File outDir) throws AndrolibException {
|
||||||
mApkInfo.save(new File(outDir, "apktool.yml"));
|
if (!mApkInfo.hasSources()) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mConfig.decodeSources) {
|
||||||
|
case Config.DECODE_SOURCES_NONE:
|
||||||
|
copySourcesRaw(outDir, "classes.dex");
|
||||||
|
break;
|
||||||
|
case Config.DECODE_SOURCES_SMALI:
|
||||||
|
case Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
|
||||||
|
decodeSourcesSmali(outDir, "classes.dex");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
private void copyManifestRaw(File outDir) throws AndrolibException {
|
|
||||||
try {
|
try {
|
||||||
LOGGER.info("Copying raw manifest...");
|
Directory in = mApkFile.getDirectory();
|
||||||
mApkInfo.getApkFile().getDirectory().copyToDir(outDir, APK_MANIFEST_FILENAMES);
|
|
||||||
|
// foreach unknown dex file in root, lets disassemble it
|
||||||
|
for (String fileName : in.getFiles(true)) {
|
||||||
|
if (fileName.endsWith(".dex") && !fileName.equals("classes.dex")) {
|
||||||
|
switch (mConfig.decodeSources) {
|
||||||
|
case Config.DECODE_SOURCES_NONE:
|
||||||
|
copySourcesRaw(outDir, fileName);
|
||||||
|
break;
|
||||||
|
case Config.DECODE_SOURCES_SMALI:
|
||||||
|
decodeSourcesSmali(outDir, fileName);
|
||||||
|
break;
|
||||||
|
case Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
|
||||||
|
if (fileName.startsWith("classes")) {
|
||||||
|
decodeSourcesSmali(outDir, fileName);
|
||||||
|
} else {
|
||||||
|
copySourcesRaw(outDir, fileName);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (DirectoryException ex) {
|
} catch (DirectoryException ex) {
|
||||||
throw new AndrolibException(ex);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void copySourcesRaw(File outDir, String fileName) throws AndrolibException {
|
||||||
|
LOGGER.info("Copying raw " + fileName + " file...");
|
||||||
|
try {
|
||||||
|
Directory in = mApkFile.getDirectory();
|
||||||
|
|
||||||
|
in.copyToDir(outDir, fileName);
|
||||||
|
} catch (DirectoryException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decodeSourcesSmali(File outDir, String fileName) throws AndrolibException {
|
||||||
|
if (mWorker != null) {
|
||||||
|
mWorker.submit(() -> {
|
||||||
|
if (mBuildError.get() == null) {
|
||||||
|
try {
|
||||||
|
decodeSourcesSmaliJob(outDir, fileName);
|
||||||
|
} catch (AndrolibException ex) {
|
||||||
|
mBuildError.compareAndSet(null, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
decodeSourcesSmaliJob(outDir, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decodeSourcesSmaliJob(File outDir, String fileName) throws AndrolibException {
|
||||||
|
File smaliDir;
|
||||||
|
if (fileName.equals("classes.dex")) {
|
||||||
|
smaliDir = new File(outDir, "smali");
|
||||||
|
} else {
|
||||||
|
smaliDir = new File(outDir, "smali_" + fileName.substring(0, fileName.indexOf(".")));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
OS.rmdir(smaliDir);
|
||||||
|
} catch (BrutException ex) {
|
||||||
|
throw new AndrolibException(ex);
|
||||||
|
}
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
smaliDir.mkdirs();
|
||||||
|
|
||||||
|
LOGGER.info("Baksmaling " + fileName + "...");
|
||||||
|
SmaliDecoder decoder = new SmaliDecoder(mApkFile, fileName,
|
||||||
|
mConfig.baksmaliDebugMode, mConfig.apiLevel);
|
||||||
|
DexFile dexFile = decoder.decode(smaliDir);
|
||||||
|
|
||||||
|
// record minSdkVersion for jars
|
||||||
|
int minSdkVersion = dexFile.getOpcodes().api;
|
||||||
|
if (mMinSdkVersion == 0 || mMinSdkVersion > minSdkVersion) {
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decodeResources(File outDir) throws AndrolibException {
|
||||||
|
if (!mApkInfo.hasResources()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mConfig.decodeResources) {
|
||||||
|
case Config.DECODE_RESOURCES_NONE:
|
||||||
|
copyResourcesRaw(outDir);
|
||||||
|
break;
|
||||||
|
case Config.DECODE_RESOURCES_FULL:
|
||||||
|
mResDecoder.decodeResources(outDir);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void copyResourcesRaw(File outDir) throws AndrolibException {
|
private void copyResourcesRaw(File outDir) throws AndrolibException {
|
||||||
|
LOGGER.info("Copying raw resources...");
|
||||||
try {
|
try {
|
||||||
LOGGER.info("Copying raw resources...");
|
Directory in = mApkFile.getDirectory();
|
||||||
mApkInfo.getApkFile().getDirectory().copyToDir(outDir, APK_RESOURCES_FILENAMES);
|
|
||||||
|
in.copyToDir(outDir, "resources.arsc");
|
||||||
|
in.copyToDir(outDir, ApkInfo.RESOURCES_DIRNAMES);
|
||||||
} catch (DirectoryException ex) {
|
} catch (DirectoryException ex) {
|
||||||
throw new AndrolibException(ex);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copySourcesRaw(File outDir, String filename) throws AndrolibException {
|
private void decodeManifest(File outDir) throws AndrolibException {
|
||||||
try {
|
if (!mApkInfo.hasManifest()) {
|
||||||
LOGGER.info("Copying raw " + filename + " file...");
|
return;
|
||||||
mApkInfo.getApkFile().getDirectory().copyToDir(outDir, filename);
|
}
|
||||||
} catch (DirectoryException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
if (mConfig.decodeResources == Config.DECODE_RESOURCES_FULL
|
||||||
|
|| mConfig.forceDecodeManifest == Config.FORCE_DECODE_MANIFEST_FULL) {
|
||||||
|
mResDecoder.decodeManifest(outDir);
|
||||||
|
} else {
|
||||||
|
copyManifestRaw(outDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void decodeSourcesSmali(File outDir, String filename) throws AndrolibException {
|
private void copyManifestRaw(File outDir) throws AndrolibException {
|
||||||
|
LOGGER.info("Copying raw manifest...");
|
||||||
try {
|
try {
|
||||||
File smaliDir;
|
Directory in = mApkFile.getDirectory();
|
||||||
if (filename.equalsIgnoreCase("classes.dex")) {
|
|
||||||
smaliDir = new File(outDir, SMALI_DIRNAME);
|
in.copyToDir(outDir, "AndroidManifest.xml");
|
||||||
} else {
|
} catch (DirectoryException ex) {
|
||||||
smaliDir = new File(outDir, SMALI_DIRNAME + "_" + filename.substring(0, filename.indexOf(".")));
|
|
||||||
}
|
|
||||||
OS.rmdir(smaliDir);
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
smaliDir.mkdirs();
|
|
||||||
LOGGER.info("Baksmaling " + filename + "...");
|
|
||||||
DexFile dexFile = SmaliDecoder.decode(mApkInfo.getApkFile(), smaliDir, filename,
|
|
||||||
mConfig.baksmaliDebugMode, mConfig.apiLevel);
|
|
||||||
int minSdkVersion = dexFile.getOpcodes().api;
|
|
||||||
if (mMinSdkVersion == 0 || mMinSdkVersion > minSdkVersion) {
|
|
||||||
mMinSdkVersion = minSdkVersion;
|
|
||||||
}
|
|
||||||
} catch (BrutException ex) {
|
|
||||||
throw new AndrolibException(ex);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyRawFiles(File outDir) throws AndrolibException {
|
private void copyRawFiles(File outDir) throws AndrolibException {
|
||||||
LOGGER.info("Copying assets and libs...");
|
|
||||||
try {
|
try {
|
||||||
Directory in = mApkInfo.getApkFile().getDirectory();
|
Directory in = mApkFile.getDirectory();
|
||||||
|
|
||||||
if (mConfig.decodeAssets == Config.DECODE_ASSETS_FULL) {
|
for (String dirName : ApkInfo.RAW_DIRNAMES) {
|
||||||
if (in.containsDir("assets")) {
|
if ((mConfig.decodeAssets == Config.DECODE_ASSETS_FULL || !dirName.equals("assets"))
|
||||||
in.copyToDir(outDir, "assets");
|
&& in.containsDir(dirName)) {
|
||||||
}
|
LOGGER.info("Copying " + dirName + "...");
|
||||||
}
|
for (String fileName : in.getDir(dirName).getFiles(true)) {
|
||||||
if (in.containsDir("lib")) {
|
fileName = dirName + "/" + fileName;
|
||||||
in.copyToDir(outDir, "lib");
|
if (!ApkInfo.ORIGINAL_FILENAMES_PATTERN.matcher(fileName).matches()) {
|
||||||
}
|
in.copyToDir(outDir, fileName);
|
||||||
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) {
|
} catch (DirectoryException ex) {
|
||||||
@ -290,29 +292,13 @@ public class ApkDecoder {
|
|||||||
|
|
||||||
private void copyOriginalFiles(File outDir) throws AndrolibException {
|
private void copyOriginalFiles(File outDir) throws AndrolibException {
|
||||||
LOGGER.info("Copying original files...");
|
LOGGER.info("Copying original files...");
|
||||||
File originalDir = new File(outDir, "original");
|
|
||||||
if (!originalDir.exists()) {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
originalDir.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Directory in = mApkInfo.getApkFile().getDirectory();
|
Directory in = mApkFile.getDirectory();
|
||||||
if (in.containsFile("AndroidManifest.xml")) {
|
File originalDir = new File(outDir, "original");
|
||||||
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");
|
|
||||||
|
|
||||||
if (in.containsDir("META-INF/services")) {
|
for (String fileName : in.getFiles(true)) {
|
||||||
// If the original APK contains the folder META-INF/services folder
|
if (ApkInfo.ORIGINAL_FILENAMES_PATTERN.matcher(fileName).matches()) {
|
||||||
// that is used for service locators (like coroutines on android),
|
in.copyToDir(originalDir, fileName);
|
||||||
// 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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (DirectoryException ex) {
|
} 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 {
|
public void recordUncompressedFiles(Map<String, String> resFileMapping) throws AndrolibException {
|
||||||
try {
|
try {
|
||||||
List<String> uncompressedFilesOrExts = new ArrayList<>();
|
Set<String> uncompressedExts = new HashSet<>();
|
||||||
Directory unk = mApkInfo.getApkFile().getDirectory();
|
Set<String> uncompressedFiles = new HashSet<>();
|
||||||
Set<String> files = unk.getFiles(true);
|
Directory in = mApkFile.getDirectory();
|
||||||
|
|
||||||
for (String file : files) {
|
for (String fileName : in.getFiles(true)) {
|
||||||
if (isAPKFileNames(file) && unk.getCompressionLevel(file) == 0) {
|
if (in.getCompressionLevel(fileName) == 0) {
|
||||||
String extOrFile = "";
|
String ext;
|
||||||
if (unk.getSize(file) != 0) {
|
if (in.getSize(fileName) > 0
|
||||||
extOrFile = FilenameUtils.getExtension(file);
|
&& !(ext = FilenameUtils.getExtension(fileName)).isEmpty()
|
||||||
}
|
&& NO_COMPRESS_EXT_PATTERN.matcher(ext).matches()) {
|
||||||
|
uncompressedExts.add(ext);
|
||||||
if (extOrFile.isEmpty() || !NO_COMPRESS_PATTERN.matcher(extOrFile).find()) {
|
} else {
|
||||||
extOrFile = file;
|
uncompressedFiles.add(resFileMapping.getOrDefault(fileName, fileName));
|
||||||
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
|
// update apk info
|
||||||
if (!uncompressedFilesOrExts.isEmpty()) {
|
int doNotCompressSize = uncompressedExts.size() + uncompressedFiles.size();
|
||||||
mApkInfo.doNotCompress = uncompressedFilesOrExts;
|
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) {
|
} catch (DirectoryException ex) {
|
||||||
throw new AndrolibException(ex);
|
throw new AndrolibException(ex);
|
||||||
|
@ -42,20 +42,20 @@ public class ApktoolProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void loadProps() {
|
private static void loadProps() {
|
||||||
InputStream in = ApktoolProperties.class.getResourceAsStream("/properties/apktool.properties");
|
InputStream in = ApktoolProperties.class.getResourceAsStream("/apktool.properties");
|
||||||
sProps = new Properties();
|
sProps = new Properties();
|
||||||
try {
|
try {
|
||||||
sProps.load(in);
|
sProps.load(in);
|
||||||
in.close();
|
in.close();
|
||||||
} catch (IOException ex) {
|
} catch (NullPointerException | IOException ex) {
|
||||||
LOGGER.warning("Can't load properties.");
|
LOGGER.warning("Could not load properties.");
|
||||||
}
|
}
|
||||||
|
|
||||||
InputStream templateStream = null;
|
InputStream templateStream = null;
|
||||||
try {
|
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) {
|
} catch(NoClassDefFoundError ex) {
|
||||||
LOGGER.warning("Can't load baksmali properties.");
|
LOGGER.warning("Could not load baksmali properties.");
|
||||||
}
|
}
|
||||||
Properties properties = new Properties();
|
Properties properties = new Properties();
|
||||||
String version = "(unknown)";
|
String version = "(unknown)";
|
||||||
@ -65,15 +65,15 @@ public class ApktoolProperties {
|
|||||||
properties.load(templateStream);
|
properties.load(templateStream);
|
||||||
version = properties.getProperty("application.version");
|
version = properties.getProperty("application.version");
|
||||||
templateStream.close();
|
templateStream.close();
|
||||||
} catch (IOException ignored) { }
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
sProps.put("baksmaliVersion", version);
|
sProps.put("baksmaliVersion", version);
|
||||||
|
|
||||||
templateStream = null;
|
templateStream = null;
|
||||||
try {
|
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) {
|
} catch(NoClassDefFoundError ex) {
|
||||||
LOGGER.warning("Can't load smali properties.");
|
LOGGER.warning("Could not load smali properties.");
|
||||||
}
|
}
|
||||||
properties = new Properties();
|
properties = new Properties();
|
||||||
version = "(unknown)";
|
version = "(unknown)";
|
||||||
@ -83,7 +83,7 @@ public class ApktoolProperties {
|
|||||||
properties.load(templateStream);
|
properties.load(templateStream);
|
||||||
version = properties.getProperty("application.version");
|
version = properties.getProperty("application.version");
|
||||||
templateStream.close();
|
templateStream.close();
|
||||||
} catch (IOException ignored) { }
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
sProps.put("smaliVersion", version);
|
sProps.put("smaliVersion", version);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -22,26 +22,27 @@ import brut.util.OSDetection;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class Config {
|
public final class Config {
|
||||||
private static Config instance = null;
|
private static final Logger LOGGER = Logger.getLogger(Config.class.getName());
|
||||||
private final static Logger LOGGER = Logger.getLogger(Config.class.getName());
|
|
||||||
|
|
||||||
public final static short DECODE_SOURCES_NONE = 0x0000;
|
public static final short DECODE_SOURCES_NONE = 0x0000;
|
||||||
public final static short DECODE_SOURCES_SMALI = 0x0001;
|
public static final short DECODE_SOURCES_SMALI = 0x0001;
|
||||||
public final static short DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES = 0x0010;
|
public static final short DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES = 0x0010;
|
||||||
|
|
||||||
public final static short DECODE_RESOURCES_NONE = 0x0100;
|
public static final short DECODE_RESOURCES_NONE = 0x0100;
|
||||||
public final static short DECODE_RESOURCES_FULL = 0x0101;
|
public static final short DECODE_RESOURCES_FULL = 0x0101;
|
||||||
|
|
||||||
public final static short FORCE_DECODE_MANIFEST_NONE = 0x0000;
|
public static final short FORCE_DECODE_MANIFEST_NONE = 0x0000;
|
||||||
public final static short FORCE_DECODE_MANIFEST_FULL = 0x0001;
|
public static final short FORCE_DECODE_MANIFEST_FULL = 0x0001;
|
||||||
|
|
||||||
public final static short DECODE_ASSETS_NONE = 0x0000;
|
public static final short DECODE_ASSETS_NONE = 0x0000;
|
||||||
public final static short DECODE_ASSETS_FULL = 0x0001;
|
public static final short DECODE_ASSETS_FULL = 0x0001;
|
||||||
|
|
||||||
public final static short DECODE_RES_RESOLVE_REMOVE = 0x0000;
|
public static final short DECODE_RES_RESOLVE_REMOVE = 0x0000;
|
||||||
public final static short DECODE_RES_RESOLVE_DUMMY = 0x0001;
|
public static final short DECODE_RES_RESOLVE_DUMMY = 0x0001;
|
||||||
public final static short DECODE_RES_RESOLVE_RETAIN = 0x0002;
|
public static final short DECODE_RES_RESOLVE_RETAIN = 0x0002;
|
||||||
|
|
||||||
|
private static Config sInstance;
|
||||||
|
|
||||||
// Build options
|
// Build options
|
||||||
public boolean forceBuildAll = false;
|
public boolean forceBuildAll = false;
|
||||||
@ -51,8 +52,8 @@ public class Config {
|
|||||||
public boolean verbose = false;
|
public boolean verbose = false;
|
||||||
public boolean copyOriginalFiles = false;
|
public boolean copyOriginalFiles = false;
|
||||||
public boolean updateFiles = false;
|
public boolean updateFiles = false;
|
||||||
public boolean useAapt2 = true;
|
|
||||||
public boolean noCrunch = false;
|
public boolean noCrunch = false;
|
||||||
|
public boolean noApk = false;
|
||||||
|
|
||||||
// Decode options
|
// Decode options
|
||||||
public short decodeSources = DECODE_SOURCES_SMALI;
|
public short decodeSources = DECODE_SOURCES_SMALI;
|
||||||
@ -67,16 +68,13 @@ public class Config {
|
|||||||
public boolean baksmaliDebugMode = true;
|
public boolean baksmaliDebugMode = true;
|
||||||
|
|
||||||
// Common options
|
// Common options
|
||||||
|
public int jobs = Math.min(Runtime.getRuntime().availableProcessors(), 8);
|
||||||
public String frameworkDirectory = null;
|
public String frameworkDirectory = null;
|
||||||
public String frameworkTag = null;
|
public String frameworkTag = null;
|
||||||
public String aaptPath = "";
|
public File aaptBinary = null;
|
||||||
public int aaptVersion = 1; // default to v1
|
public int aaptVersion = 2; // default to v2
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
public boolean isAapt2() {
|
|
||||||
return this.useAapt2 || this.aaptVersion == 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDecodeResolveModeUsingDummies() {
|
public boolean isDecodeResolveModeUsingDummies() {
|
||||||
return decodeResolveMode == DECODE_RES_RESOLVE_DUMMY;
|
return decodeResolveMode == DECODE_RES_RESOLVE_DUMMY;
|
||||||
}
|
}
|
||||||
@ -86,14 +84,14 @@ public class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Config() {
|
private Config() {
|
||||||
instance = this;
|
sInstance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Config getInstance() {
|
public static Config getInstance() {
|
||||||
if (instance == null) {
|
if (sInstance == null) {
|
||||||
instance = new Config();
|
sInstance = new Config();
|
||||||
}
|
}
|
||||||
return instance;
|
return sInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDefaultFrameworkDirectory() {
|
private void setDefaultFrameworkDirectory() {
|
||||||
|
@ -21,36 +21,45 @@ import brut.androlib.exceptions.AndrolibException;
|
|||||||
import brut.androlib.res.data.ResConfigFlags;
|
import brut.androlib.res.data.ResConfigFlags;
|
||||||
import brut.directory.DirectoryException;
|
import brut.directory.DirectoryException;
|
||||||
import brut.directory.ExtFile;
|
import brut.directory.ExtFile;
|
||||||
import brut.directory.FileDirectory;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class ApkInfo implements YamlSerializable {
|
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;
|
private transient ExtFile mApkFile;
|
||||||
|
|
||||||
public String version;
|
public String version;
|
||||||
public String apkFileName;
|
public String apkFileName;
|
||||||
public boolean isFrameworkApk;
|
public boolean isFrameworkApk;
|
||||||
public UsesFramework usesFramework;
|
public UsesFramework usesFramework;
|
||||||
private Map<String, String> sdkInfo = new LinkedHashMap<>();
|
public Map<String, String> sdkInfo = new LinkedHashMap<>();
|
||||||
public PackageInfo packageInfo = new PackageInfo();
|
public PackageInfo packageInfo = new PackageInfo();
|
||||||
public VersionInfo versionInfo = new VersionInfo();
|
public VersionInfo versionInfo = new VersionInfo();
|
||||||
public boolean resourcesAreCompressed;
|
public Map<String, Boolean> featureFlags = new LinkedHashMap<>();
|
||||||
public boolean sharedLibrary;
|
public boolean sharedLibrary;
|
||||||
public boolean sparseResources;
|
public boolean sparseResources;
|
||||||
public Map<String, String> unknownFiles = new LinkedHashMap<>();
|
public boolean compactEntries;
|
||||||
public List<String> doNotCompress;
|
public List<String> doNotCompress = new ArrayList<>();
|
||||||
|
|
||||||
/** @Deprecated use {@link #resourcesAreCompressed} */
|
|
||||||
public boolean compressionType;
|
|
||||||
|
|
||||||
public ApkInfo() {
|
public ApkInfo() {
|
||||||
this(null);
|
this(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApkInfo(ExtFile apkFile) {
|
public ApkInfo(ExtFile apkFile) {
|
||||||
this.version = ApktoolProperties.getVersion();
|
version = ApktoolProperties.getVersion();
|
||||||
if (apkFile != null) {
|
if (apkFile != null) {
|
||||||
setApkFile(apkFile);
|
setApkFile(apkFile);
|
||||||
}
|
}
|
||||||
@ -62,8 +71,19 @@ public class ApkInfo implements YamlSerializable {
|
|||||||
|
|
||||||
public void setApkFile(ExtFile apkFile) {
|
public void setApkFile(ExtFile apkFile) {
|
||||||
mApkFile = apkFile;
|
mApkFile = apkFile;
|
||||||
if (this.apkFileName == null) {
|
if (apkFileName == null) {
|
||||||
this.apkFileName = apkFile.getName();
|
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() {
|
public String checkTargetSdkVersionBounds() {
|
||||||
int target = mapSdkShorthandToVersion(getTargetSdkVersion());
|
int target = mapSdkShorthandToVersion(getTargetSdkVersion());
|
||||||
|
|
||||||
@ -135,35 +120,35 @@ public class ApkInfo implements YamlSerializable {
|
|||||||
return Integer.toString(target);
|
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() {
|
public String getMinSdkVersion() {
|
||||||
return sdkInfo.get("minSdkVersion");
|
return sdkInfo.get("minSdkVersion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMinSdkVersion(String minSdkVersion) {
|
||||||
|
sdkInfo.put("minSdkVersion", minSdkVersion);
|
||||||
|
}
|
||||||
|
|
||||||
public String getMaxSdkVersion() {
|
public String getMaxSdkVersion() {
|
||||||
return sdkInfo.get("maxSdkVersion");
|
return sdkInfo.get("maxSdkVersion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMaxSdkVersion(String maxSdkVersion) {
|
||||||
|
sdkInfo.put("maxSdkVersion", maxSdkVersion);
|
||||||
|
}
|
||||||
|
|
||||||
public String getTargetSdkVersion() {
|
public String getTargetSdkVersion() {
|
||||||
return sdkInfo.get("targetSdkVersion");
|
return sdkInfo.get("targetSdkVersion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTargetSdkVersion(String targetSdkVersion) {
|
||||||
|
sdkInfo.put("targetSdkVersion", targetSdkVersion);
|
||||||
|
}
|
||||||
|
|
||||||
public int getMinSdkVersionFromAndroidCodename(String sdkVersion) {
|
public int getMinSdkVersionFromAndroidCodename(String sdkVersion) {
|
||||||
int sdkNumber = mapSdkShorthandToVersion(sdkVersion);
|
int sdkNumber = mapSdkShorthandToVersion(sdkVersion);
|
||||||
|
|
||||||
if (sdkNumber == ResConfigFlags.SDK_BASE) {
|
if (sdkNumber == ResConfigFlags.SDK_BASE) {
|
||||||
return Integer.parseInt(sdkInfo.get("minSdkVersion"));
|
return Integer.parseInt(getMinSdkVersion());
|
||||||
}
|
}
|
||||||
return sdkNumber;
|
return sdkNumber;
|
||||||
}
|
}
|
||||||
@ -194,33 +179,41 @@ public class ApkInfo implements YamlSerializable {
|
|||||||
return ResConfigFlags.SDK_UPSIDEDOWN_CAKE;
|
return ResConfigFlags.SDK_UPSIDEDOWN_CAKE;
|
||||||
case "VANILLAICECREAM":
|
case "VANILLAICECREAM":
|
||||||
case "VANILLA_ICE_CREAM":
|
case "VANILLA_ICE_CREAM":
|
||||||
|
return ResConfigFlags.SDK_VANILLA_ICE_CREAM;
|
||||||
|
case "BAKLAVA":
|
||||||
|
return ResConfigFlags.SDK_BAKLAVA;
|
||||||
|
case "SDK_CUR_DEVELOPMENT":
|
||||||
return ResConfigFlags.SDK_DEVELOPMENT;
|
return ResConfigFlags.SDK_DEVELOPMENT;
|
||||||
default:
|
default:
|
||||||
return Integer.parseInt(sdkVersion);
|
return Integer.parseInt(sdkVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addFeatureFlag(String flag, boolean value) {
|
||||||
|
featureFlags.put(flag, value);
|
||||||
|
}
|
||||||
|
|
||||||
public void save(File file) throws AndrolibException {
|
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);
|
write(writer);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException ex) {
|
||||||
throw new AndrolibException("File not found");
|
throw new AndrolibException("File not found");
|
||||||
} catch (Exception e) {
|
} catch (Exception ex) {
|
||||||
throw new AndrolibException(e);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ApkInfo load(InputStream is) throws AndrolibException {
|
public static ApkInfo load(InputStream in) throws AndrolibException {
|
||||||
YamlReader reader = new YamlReader(is);
|
YamlReader reader = new YamlReader(in);
|
||||||
ApkInfo apkInfo = new ApkInfo();
|
ApkInfo apkInfo = new ApkInfo();
|
||||||
reader.readRoot(apkInfo);
|
reader.readRoot(apkInfo);
|
||||||
return apkInfo;
|
return apkInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ApkInfo load(File appDir) throws AndrolibException {
|
public static ApkInfo load(ExtFile apkDir) throws AndrolibException {
|
||||||
try (InputStream in = new FileDirectory(appDir).getFileInput("apktool.yml")) {
|
try (InputStream in = apkDir.getDirectory().getFileInput("apktool.yml")) {
|
||||||
ApkInfo apkInfo = ApkInfo.load(in);
|
ApkInfo apkInfo = ApkInfo.load(in);
|
||||||
apkInfo.setApkFile(new ExtFile(appDir));
|
apkInfo.setApkFile(apkDir);
|
||||||
return apkInfo;
|
return apkInfo;
|
||||||
} catch (DirectoryException | IOException ex) {
|
} catch (DirectoryException | IOException ex) {
|
||||||
throw new AndrolibException(ex);
|
throw new AndrolibException(ex);
|
||||||
@ -232,56 +225,56 @@ public class ApkInfo implements YamlSerializable {
|
|||||||
YamlLine line = reader.getLine();
|
YamlLine line = reader.getLine();
|
||||||
switch (line.getKey()) {
|
switch (line.getKey()) {
|
||||||
case "version": {
|
case "version": {
|
||||||
this.version = line.getValue();
|
version = line.getValue();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "apkFileName": {
|
case "apkFileName": {
|
||||||
this.apkFileName = line.getValue();
|
apkFileName = line.getValue();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "isFrameworkApk": {
|
case "isFrameworkApk": {
|
||||||
this.isFrameworkApk = line.getValueBool();
|
isFrameworkApk = line.getValueBool();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "usesFramework": {
|
case "usesFramework": {
|
||||||
this.usesFramework = new UsesFramework();
|
usesFramework = new UsesFramework();
|
||||||
reader.readObject(usesFramework);
|
reader.readObject(usesFramework);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sdkInfo": {
|
case "sdkInfo": {
|
||||||
reader.readMap(sdkInfo);
|
sdkInfo.clear();
|
||||||
|
reader.readStringMap(sdkInfo);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "packageInfo": {
|
case "packageInfo": {
|
||||||
this.packageInfo = new PackageInfo();
|
packageInfo = new PackageInfo();
|
||||||
reader.readObject(packageInfo);
|
reader.readObject(packageInfo);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "versionInfo": {
|
case "versionInfo": {
|
||||||
this.versionInfo = new VersionInfo();
|
versionInfo = new VersionInfo();
|
||||||
reader.readObject(versionInfo);
|
reader.readObject(versionInfo);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "compressionType":
|
case "featureFlags": {
|
||||||
case "resourcesAreCompressed": {
|
featureFlags.clear();
|
||||||
this.resourcesAreCompressed = line.getValueBool();
|
reader.readBoolMap(featureFlags);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sharedLibrary": {
|
case "sharedLibrary": {
|
||||||
this.sharedLibrary = line.getValueBool();
|
sharedLibrary = line.getValueBool();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sparseResources": {
|
case "sparseResources": {
|
||||||
this.sparseResources = line.getValueBool();
|
sparseResources = line.getValueBool();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "unknownFiles": {
|
case "compactEntries": {
|
||||||
this.unknownFiles = new LinkedHashMap<>();
|
compactEntries = line.getValueBool();
|
||||||
reader.readMap(unknownFiles);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "doNotCompress": {
|
case "doNotCompress": {
|
||||||
this.doNotCompress = new ArrayList<>();
|
doNotCompress.clear();
|
||||||
reader.readStringList(doNotCompress);
|
reader.readStringList(doNotCompress);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -294,15 +287,17 @@ public class ApkInfo implements YamlSerializable {
|
|||||||
writer.writeString("apkFileName", apkFileName);
|
writer.writeString("apkFileName", apkFileName);
|
||||||
writer.writeBool("isFrameworkApk", isFrameworkApk);
|
writer.writeBool("isFrameworkApk", isFrameworkApk);
|
||||||
writer.writeObject("usesFramework", usesFramework);
|
writer.writeObject("usesFramework", usesFramework);
|
||||||
writer.writeStringMap("sdkInfo", sdkInfo);
|
writer.writeMap("sdkInfo", sdkInfo);
|
||||||
writer.writeObject("packageInfo", packageInfo);
|
writer.writeObject("packageInfo", packageInfo);
|
||||||
writer.writeObject("versionInfo", versionInfo);
|
writer.writeObject("versionInfo", versionInfo);
|
||||||
writer.writeBool("resourcesAreCompressed", resourcesAreCompressed);
|
if (!featureFlags.isEmpty()) {
|
||||||
|
writer.writeMap("featureFlags", featureFlags);
|
||||||
|
}
|
||||||
writer.writeBool("sharedLibrary", sharedLibrary);
|
writer.writeBool("sharedLibrary", sharedLibrary);
|
||||||
writer.writeBool("sparseResources", sparseResources);
|
writer.writeBool("sparseResources", sparseResources);
|
||||||
if (unknownFiles.size() > 0) {
|
writer.writeBool("compactEntries", compactEntries);
|
||||||
writer.writeStringMap("unknownFiles", unknownFiles);
|
if (!doNotCompress.isEmpty()) {
|
||||||
|
writer.writeList("doNotCompress", doNotCompress);
|
||||||
}
|
}
|
||||||
writer.writeList("doNotCompress", doNotCompress);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ package brut.androlib.apk;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class YamlLine {
|
public class YamlLine {
|
||||||
|
|
||||||
public int indent = 0;
|
public int indent = 0;
|
||||||
private String key = "";
|
private String key = "";
|
||||||
private String value = "";
|
private String value = "";
|
||||||
@ -62,7 +61,7 @@ public class YamlLine {
|
|||||||
if (isItem) {
|
if (isItem) {
|
||||||
// array item line has only the value
|
// array item line has only the value
|
||||||
value = line.substring(1).trim();
|
value = line.substring(1).trim();
|
||||||
} else {
|
} else {
|
||||||
// split line to key - value
|
// split line to key - value
|
||||||
String[] parts = line.split(":");
|
String[] parts = line.split(":");
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
|
@ -22,9 +22,8 @@ import java.io.InputStream;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class YamlReader {
|
public class YamlReader {
|
||||||
|
|
||||||
private ArrayList<YamlLine> mLines;
|
private ArrayList<YamlLine> mLines;
|
||||||
private int mCurrent = 0;
|
private int mCurrent;
|
||||||
|
|
||||||
public YamlReader(InputStream in) {
|
public YamlReader(InputStream in) {
|
||||||
mLines = new ArrayList<>();
|
mLines = new ArrayList<>();
|
||||||
@ -203,7 +202,7 @@ public class YamlReader {
|
|||||||
readList(list, (items, reader) -> items.add(reader.getLine().getValueInt()));
|
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,
|
readObject(map,
|
||||||
line -> line.hasColon,
|
line -> line.hasColon,
|
||||||
(items, reader) -> {
|
(items, reader) -> {
|
||||||
@ -211,4 +210,13 @@ public class YamlReader {
|
|||||||
items.put(line.getKey(), line.getValue());
|
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());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,5 +20,6 @@ import brut.androlib.exceptions.AndrolibException;
|
|||||||
|
|
||||||
public interface YamlSerializable {
|
public interface YamlSerializable {
|
||||||
void readItem(YamlReader reader) throws AndrolibException;
|
void readItem(YamlReader reader) throws AndrolibException;
|
||||||
|
|
||||||
void write(YamlWriter writer);
|
void write(YamlWriter writer);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,11 @@ import java.io.IOException;
|
|||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
|
||||||
public class YamlStringEscapeUtils {
|
public final class YamlStringEscapeUtils {
|
||||||
|
|
||||||
|
private YamlStringEscapeUtils() {
|
||||||
|
// Private constructor for utility class
|
||||||
|
}
|
||||||
|
|
||||||
public static String escapeString(String str) {
|
public static String escapeString(String str) {
|
||||||
return escapeJavaStyleString(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
|
* @param str String to escape values in, may be null
|
||||||
* @throws IOException if an IOException occurs
|
* @throws IOException if an IOException occurs
|
||||||
*/
|
*/
|
||||||
private static void escapeJavaStyleString(Writer out, String str) throws IOException {
|
private static void escapeJavaStyleString(Writer writer, String str) throws IOException {
|
||||||
if (out == null) {
|
if (writer == null) {
|
||||||
throw new IllegalArgumentException("The Writer must not be null");
|
throw new IllegalArgumentException("The Writer must not be null");
|
||||||
}
|
}
|
||||||
if (str == null) {
|
if (str == null) {
|
||||||
@ -66,57 +70,51 @@ public class YamlStringEscapeUtils {
|
|||||||
// "[^\t\n\r\u0020-\u007E\u0085\u00A0-\uD7FF\uE000-\uFFFD]"
|
// "[^\t\n\r\u0020-\u007E\u0085\u00A0-\uD7FF\uE000-\uFFFD]"
|
||||||
// handle unicode
|
// handle unicode
|
||||||
if (ch > 0xFFFD) {
|
if (ch > 0xFFFD) {
|
||||||
out.write("\\u" + CharSequenceTranslator.hex(ch));
|
writer.write("\\u" + CharSequenceTranslator.hex(ch));
|
||||||
} else if (ch > 0xD7FF && ch < 0xE000) {
|
} 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) {
|
} else if (ch > 0x7E && ch != 0x85 && ch < 0xA0) {
|
||||||
out.write("\\u00" + CharSequenceTranslator.hex(ch));
|
writer.write("\\u00" + CharSequenceTranslator.hex(ch));
|
||||||
} else if (ch < 32) {
|
} else if (ch < 32) {
|
||||||
switch (ch) {
|
switch (ch) {
|
||||||
case '\t' :
|
case '\t' :
|
||||||
out.write('\\');
|
writer.write('\\');
|
||||||
out.write('t');
|
writer.write('t');
|
||||||
break;
|
break;
|
||||||
case '\n' :
|
case '\n' :
|
||||||
out.write('\\');
|
writer.write('\\');
|
||||||
out.write('n');
|
writer.write('n');
|
||||||
break;
|
break;
|
||||||
case '\r' :
|
case '\r' :
|
||||||
out.write('\\');
|
writer.write('\\');
|
||||||
out.write('r');
|
writer.write('r');
|
||||||
break;
|
break;
|
||||||
default :
|
default :
|
||||||
if (ch > 0xf) {
|
if (ch > 0xf) {
|
||||||
out.write("\\u00" + CharSequenceTranslator.hex(ch));
|
writer.write("\\u00" + CharSequenceTranslator.hex(ch));
|
||||||
} else {
|
} else {
|
||||||
out.write("\\u000" + CharSequenceTranslator.hex(ch));
|
writer.write("\\u000" + CharSequenceTranslator.hex(ch));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (ch) {
|
switch (ch) {
|
||||||
case '\'' :
|
case '\'' :
|
||||||
if (false) {
|
writer.write('\'');
|
||||||
out.write('\\');
|
|
||||||
}
|
|
||||||
out.write('\'');
|
|
||||||
break;
|
break;
|
||||||
case '"' :
|
case '"' :
|
||||||
out.write('\\');
|
writer.write('\\');
|
||||||
out.write('"');
|
writer.write('"');
|
||||||
break;
|
break;
|
||||||
case '\\' :
|
case '\\' :
|
||||||
out.write('\\');
|
writer.write('\\');
|
||||||
out.write('\\');
|
writer.write('\\');
|
||||||
break;
|
break;
|
||||||
case '/' :
|
case '/' :
|
||||||
if (false) {
|
writer.write('/');
|
||||||
out.write('\\');
|
|
||||||
}
|
|
||||||
out.write('/');
|
|
||||||
break;
|
break;
|
||||||
default :
|
default :
|
||||||
out.write(ch);
|
writer.write(ch);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,10 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class YamlWriter implements Closeable {
|
public class YamlWriter implements Closeable {
|
||||||
|
private static final String QUOTE = "'";
|
||||||
|
|
||||||
private int mIndent = 0;
|
|
||||||
private final PrintWriter mWriter;
|
private final PrintWriter mWriter;
|
||||||
private final String QUOTE = "'";
|
private int mIndent;
|
||||||
|
|
||||||
public YamlWriter(OutputStream out) {
|
public YamlWriter(OutputStream out) {
|
||||||
mWriter = new PrintWriter(new BufferedWriter(
|
mWriter = new PrintWriter(new BufferedWriter(
|
||||||
@ -89,21 +89,21 @@ public class YamlWriter implements Closeable {
|
|||||||
}
|
}
|
||||||
writeIndent();
|
writeIndent();
|
||||||
mWriter.println(escape(key) + ":");
|
mWriter.println(escape(key) + ":");
|
||||||
for (T item: list) {
|
for (T item : list) {
|
||||||
writeIndent();
|
writeIndent();
|
||||||
mWriter.println("- " + item);
|
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)) {
|
if (Objects.isNull(map)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writeIndent();
|
writeIndent();
|
||||||
mWriter.println(escape(key) + ":");
|
mWriter.println(escape(key) + ":");
|
||||||
nextIndent();
|
nextIndent();
|
||||||
for (String mapKey: map.keySet()) {
|
for (String mapKey : map.keySet()) {
|
||||||
writeString(mapKey, map.get(mapKey));
|
writeString(mapKey, String.valueOf(map.get(mapKey)));
|
||||||
}
|
}
|
||||||
prevIndent();
|
prevIndent();
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package brut.androlib.exceptions;
|
package brut.androlib.exceptions;
|
||||||
|
|
||||||
public class AXmlDecodingException extends AndrolibException {
|
public class AXmlDecodingException extends AndrolibException {
|
||||||
|
|
||||||
public AXmlDecodingException(String message, Throwable cause) {
|
public AXmlDecodingException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
@ -19,18 +19,20 @@ package brut.androlib.exceptions;
|
|||||||
import brut.common.BrutException;
|
import brut.common.BrutException;
|
||||||
|
|
||||||
public class AndrolibException extends BrutException {
|
public class AndrolibException extends BrutException {
|
||||||
|
|
||||||
public AndrolibException() {
|
public AndrolibException() {
|
||||||
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public AndrolibException(String message) {
|
public AndrolibException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AndrolibException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AndrolibException(Throwable cause) {
|
public AndrolibException(Throwable cause) {
|
||||||
super(cause);
|
super(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AndrolibException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
package brut.androlib.exceptions;
|
package brut.androlib.exceptions;
|
||||||
|
|
||||||
public class CantFind9PatchChunkException extends AndrolibException {
|
public class CantFind9PatchChunkException extends AndrolibException {
|
||||||
public CantFind9PatchChunkException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
public CantFind9PatchChunkException(String message, Throwable cause) {
|
||||||
}
|
super(message, cause);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,18 +17,18 @@
|
|||||||
package brut.androlib.exceptions;
|
package brut.androlib.exceptions;
|
||||||
|
|
||||||
public class CantFindFrameworkResException extends AndrolibException {
|
public class CantFindFrameworkResException extends AndrolibException {
|
||||||
public CantFindFrameworkResException(int id) {
|
private final int mPkgId;
|
||||||
mPkgId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPkgId() {
|
public CantFindFrameworkResException(int pkgId) {
|
||||||
return mPkgId;
|
mPkgId = pkgId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public int getPkgId() {
|
||||||
public String getMessage() {
|
return mPkgId;
|
||||||
return String.format("Can't find framework resources for package of id: %d", this.getPkgId());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private final int mPkgId;
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return String.format("Could not find framework resources for package of id: %d", mPkgId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
package brut.androlib.exceptions;
|
package brut.androlib.exceptions;
|
||||||
|
|
||||||
public class InFileNotFoundException extends AndrolibException {
|
public class InFileNotFoundException extends AndrolibException {
|
||||||
public InFileNotFoundException() {
|
|
||||||
}
|
public InFileNotFoundException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
package brut.androlib.exceptions;
|
package brut.androlib.exceptions;
|
||||||
|
|
||||||
public class OutDirExistsException extends AndrolibException {
|
public class OutDirExistsException extends AndrolibException {
|
||||||
public OutDirExistsException() {
|
|
||||||
}
|
public OutDirExistsException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package brut.androlib.exceptions;
|
package brut.androlib.exceptions;
|
||||||
|
|
||||||
public class RawXmlEncounteredException extends AndrolibException {
|
public class RawXmlEncounteredException extends AndrolibException {
|
||||||
|
|
||||||
public RawXmlEncounteredException(String message, Throwable cause) {
|
public RawXmlEncounteredException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
package brut.androlib.exceptions;
|
package brut.androlib.exceptions;
|
||||||
|
|
||||||
public class UndefinedResObjectException extends AndrolibException {
|
public class UndefinedResObjectException extends AndrolibException {
|
||||||
public UndefinedResObjectException(String message) {
|
|
||||||
super(message);
|
public UndefinedResObjectException(String message) {
|
||||||
}
|
super(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,59 +30,53 @@ import java.io.*;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
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,
|
public static boolean assembleSmaliFile(File smaliFile, DexBuilder dexBuilder, int apiLevel, boolean verboseErrors,
|
||||||
boolean printTokens) throws IOException, RecognitionException {
|
boolean printTokens) throws IOException, RecognitionException {
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(
|
||||||
|
Files.newInputStream(smaliFile.toPath()), StandardCharsets.UTF_8)) {
|
||||||
|
smaliFlexLexer lexer = new smaliFlexLexer(reader, apiLevel);
|
||||||
|
lexer.setSourceFile(smaliFile);
|
||||||
|
CommonTokenStream tokens = new CommonTokenStream(lexer);
|
||||||
|
|
||||||
CommonTokenStream tokens;
|
if (printTokens) {
|
||||||
smaliFlexLexer lexer;
|
tokens.getTokens();
|
||||||
|
|
||||||
InputStream is = Files.newInputStream(smaliFile.toPath());
|
for (int i = 0; i < tokens.size(); i++) {
|
||||||
InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
|
Token token = tokens.get(i);
|
||||||
|
if (token.getChannel() != smaliParser.HIDDEN) {
|
||||||
lexer = new smaliFlexLexer(reader, apiLevel);
|
System.out.println(smaliParser.tokenNames[token.getType()] + ": " + token.getText());
|
||||||
(lexer).setSourceFile(smaliFile);
|
}
|
||||||
tokens = new CommonTokenStream(lexer);
|
|
||||||
|
|
||||||
if (printTokens) {
|
|
||||||
tokens.getTokens();
|
|
||||||
|
|
||||||
for (int i=0; i<tokens.size(); i++) {
|
|
||||||
Token token = tokens.get(i);
|
|
||||||
if (token.getChannel() == smaliParser.HIDDEN) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println(smaliParser.tokenNames[token.getType()] + ": " + token.getText());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
smaliParser parser = new smaliParser(tokens);
|
||||||
|
parser.setApiLevel(apiLevel);
|
||||||
|
parser.setVerboseErrors(verboseErrors);
|
||||||
|
|
||||||
|
smaliParser.smali_file_return result = parser.smali_file();
|
||||||
|
|
||||||
|
if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonTree t = result.getTree();
|
||||||
|
|
||||||
|
CommonTreeNodeStream treeStream = new CommonTreeNodeStream(t);
|
||||||
|
treeStream.setTokenStream(tokens);
|
||||||
|
|
||||||
|
smaliTreeWalker dexGen = new smaliTreeWalker(treeStream);
|
||||||
|
dexGen.setApiLevel(apiLevel);
|
||||||
|
dexGen.setVerboseErrors(verboseErrors);
|
||||||
|
dexGen.setDexBuilder(dexBuilder);
|
||||||
|
dexGen.smali_file();
|
||||||
|
|
||||||
|
return dexGen.getNumberOfSyntaxErrors() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
smaliParser parser = new smaliParser(tokens);
|
|
||||||
parser.setApiLevel(apiLevel);
|
|
||||||
parser.setVerboseErrors(verboseErrors);
|
|
||||||
|
|
||||||
smaliParser.smali_file_return result = parser.smali_file();
|
|
||||||
|
|
||||||
if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) {
|
|
||||||
is.close();
|
|
||||||
reader.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
CommonTree t = result.getTree();
|
|
||||||
|
|
||||||
CommonTreeNodeStream treeStream = new CommonTreeNodeStream(t);
|
|
||||||
treeStream.setTokenStream(tokens);
|
|
||||||
|
|
||||||
smaliTreeWalker dexGen = new smaliTreeWalker(treeStream);
|
|
||||||
dexGen.setApiLevel(apiLevel);
|
|
||||||
dexGen.setVerboseErrors(verboseErrors);
|
|
||||||
dexGen.setDexBuilder(dexBuilder);
|
|
||||||
dexGen.smali_file();
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
reader.close();
|
|
||||||
|
|
||||||
return dexGen.getNumberOfSyntaxErrors() == 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,7 @@ import brut.androlib.exceptions.CantFindFrameworkResException;
|
|||||||
import brut.androlib.res.decoder.ARSCDecoder;
|
import brut.androlib.res.decoder.ARSCDecoder;
|
||||||
import brut.androlib.res.data.arsc.ARSCData;
|
import brut.androlib.res.data.arsc.ARSCData;
|
||||||
import brut.androlib.res.data.arsc.FlagsOffset;
|
import brut.androlib.res.data.arsc.FlagsOffset;
|
||||||
import brut.util.Jar;
|
import brut.util.BrutIO;
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -35,76 +34,65 @@ import java.util.zip.ZipFile;
|
|||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
public class Framework {
|
public class Framework {
|
||||||
private final Config config;
|
private static final Logger LOGGER = Logger.getLogger(Framework.class.getName());
|
||||||
|
|
||||||
private File mFrameworkDirectory = null;
|
private final Config mConfig;
|
||||||
|
private File mFrameworkDirectory;
|
||||||
private final static Logger LOGGER = Logger.getLogger(Framework.class.getName());
|
|
||||||
|
|
||||||
public Framework(Config config) {
|
public Framework(Config config) {
|
||||||
this.config = config;
|
mConfig = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void installFramework(File frameFile) throws AndrolibException {
|
public void installFramework(File frameFile) throws AndrolibException {
|
||||||
installFramework(frameFile, config.frameworkTag);
|
installFramework(frameFile, mConfig.frameworkTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void installFramework(File frameFile, String tag) throws AndrolibException {
|
public void installFramework(File frameFile, String tag) throws AndrolibException {
|
||||||
InputStream in = null;
|
try (ZipFile zip = new ZipFile(frameFile)) {
|
||||||
ZipOutputStream out = null;
|
|
||||||
try {
|
|
||||||
ZipFile zip = new ZipFile(frameFile);
|
|
||||||
ZipEntry entry = zip.getEntry("resources.arsc");
|
ZipEntry entry = zip.getEntry("resources.arsc");
|
||||||
|
|
||||||
if (entry == null) {
|
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 = BrutIO.readAndClose(zip.getInputStream(entry));
|
||||||
byte[] data = IOUtils.toByteArray(in);
|
ARSCDecoder decoder = new ARSCDecoder(new ByteArrayInputStream(data), null, true, true);
|
||||||
|
ARSCData arsc = decoder.decode();
|
||||||
ARSCData arsc = ARSCDecoder.decode(new ByteArrayInputStream(data), true, true);
|
|
||||||
publicizeResources(data, arsc.getFlagsOffsets());
|
publicizeResources(data, arsc.getFlagsOffsets());
|
||||||
|
|
||||||
File outFile = new File(getFrameworkDirectory(), arsc
|
File outFile = new File(getFrameworkDirectory(),
|
||||||
.getOnePackage().getId()
|
arsc.getOnePackage().getId() + (tag == null ? "" : '-' + tag) + ".apk");
|
||||||
+ (tag == null ? "" : '-' + tag)
|
|
||||||
+ ".apk");
|
|
||||||
|
|
||||||
out = new ZipOutputStream(Files.newOutputStream(outFile.toPath()));
|
try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(outFile.toPath()))) {
|
||||||
out.setMethod(ZipOutputStream.STORED);
|
out.setMethod(ZipOutputStream.STORED);
|
||||||
CRC32 crc = new CRC32();
|
CRC32 crc = new CRC32();
|
||||||
crc.update(data);
|
crc.update(data);
|
||||||
entry = new ZipEntry("resources.arsc");
|
entry = new ZipEntry("resources.arsc");
|
||||||
entry.setSize(data.length);
|
entry.setSize(data.length);
|
||||||
entry.setMethod(ZipEntry.STORED);
|
entry.setMethod(ZipEntry.STORED);
|
||||||
entry.setCrc(crc.getValue());
|
entry.setCrc(crc.getValue());
|
||||||
out.putNextEntry(entry);
|
|
||||||
out.write(data);
|
|
||||||
out.closeEntry();
|
|
||||||
|
|
||||||
//Write fake AndroidManifest.xml file to support original aapt
|
|
||||||
entry = zip.getEntry("AndroidManifest.xml");
|
|
||||||
if (entry != null) {
|
|
||||||
in = zip.getInputStream(entry);
|
|
||||||
byte[] manifest = IOUtils.toByteArray(in);
|
|
||||||
CRC32 manifestCrc = new CRC32();
|
|
||||||
manifestCrc.update(manifest);
|
|
||||||
entry.setSize(manifest.length);
|
|
||||||
entry.setCompressedSize(-1);
|
|
||||||
entry.setCrc(manifestCrc.getValue());
|
|
||||||
out.putNextEntry(entry);
|
out.putNextEntry(entry);
|
||||||
out.write(manifest);
|
out.write(data);
|
||||||
out.closeEntry();
|
out.closeEntry();
|
||||||
|
|
||||||
|
// write fake AndroidManifest.xml file to support original aapt
|
||||||
|
entry = zip.getEntry("AndroidManifest.xml");
|
||||||
|
if (entry != null) {
|
||||||
|
byte[] manifest = BrutIO.readAndClose(zip.getInputStream(entry));
|
||||||
|
CRC32 manifestCrc = new CRC32();
|
||||||
|
manifestCrc.update(manifest);
|
||||||
|
entry.setSize(manifest.length);
|
||||||
|
entry.setCompressedSize(-1);
|
||||||
|
entry.setCrc(manifestCrc.getValue());
|
||||||
|
out.putNextEntry(entry);
|
||||||
|
out.write(manifest);
|
||||||
|
out.closeEntry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
zip.close();
|
|
||||||
LOGGER.info("Framework installed to: " + outFile);
|
LOGGER.info("Framework installed to: " + outFile);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new AndrolibException(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 {
|
public void publicizeResources(File arscFile) throws AndrolibException {
|
||||||
byte[] data = new byte[(int) arscFile.length()];
|
byte[] data = new byte[(int) arscFile.length()];
|
||||||
|
|
||||||
try(InputStream in = Files.newInputStream(arscFile.toPath());
|
try (
|
||||||
OutputStream out = Files.newOutputStream(arscFile.toPath())) {
|
InputStream in = Files.newInputStream(arscFile.toPath());
|
||||||
|
OutputStream out = Files.newOutputStream(arscFile.toPath())
|
||||||
|
) {
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
in.read(data);
|
in.read(data);
|
||||||
publicizeResources(data);
|
publicizeResources(data);
|
||||||
@ -136,16 +126,18 @@ public class Framework {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void publicizeResources(byte[] arsc) throws AndrolibException {
|
private void publicizeResources(byte[] data) throws AndrolibException {
|
||||||
publicizeResources(arsc, ARSCDecoder.decode(new ByteArrayInputStream(arsc), true, true).getFlagsOffsets());
|
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) {
|
for (FlagsOffset flags : flagsOffsets) {
|
||||||
int offset = flags.offset + 3;
|
int offset = flags.offset + 3;
|
||||||
int end = offset + 4 * flags.count;
|
int end = offset + 4 * flags.count;
|
||||||
while (offset < end) {
|
while (offset < end) {
|
||||||
arsc[offset] |= (byte) 0x40;
|
data[offset] |= (byte) 0x40;
|
||||||
offset += 4;
|
offset += 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,7 +151,7 @@ public class Framework {
|
|||||||
String path;
|
String path;
|
||||||
|
|
||||||
// use default framework path or specified on the command line
|
// use default framework path or specified on the command line
|
||||||
path = config.frameworkDirectory;
|
path = mConfig.frameworkDirectory;
|
||||||
|
|
||||||
File dir = new File(path);
|
File dir = new File(path);
|
||||||
|
|
||||||
@ -171,19 +163,19 @@ public class Framework {
|
|||||||
throw new AndrolibException("Please remove file at " + dir.getParentFile());
|
throw new AndrolibException("Please remove file at " + dir.getParentFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! dir.exists()) {
|
if (!dir.exists()) {
|
||||||
if (! dir.mkdirs()) {
|
if (!dir.mkdirs()) {
|
||||||
if (config.frameworkDirectory != null) {
|
if (mConfig.frameworkDirectory != null) {
|
||||||
LOGGER.severe("Can't create Framework directory: " + dir);
|
LOGGER.severe("Could not create Framework directory: " + dir);
|
||||||
}
|
}
|
||||||
throw new AndrolibException(String.format(
|
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 (mConfig.frameworkDirectory == null) {
|
||||||
if (! dir.canWrite()) {
|
if (!dir.canWrite()) {
|
||||||
LOGGER.severe(String.format("WARNING: Could not write to (%1$s), using %2$s instead...",
|
LOGGER.severe(String.format("WARNING: Could not write to (%1$s), using %2$s instead...",
|
||||||
dir.getAbsolutePath(), System.getProperty("java.io.tmpdir")));
|
dir.getAbsolutePath(), System.getProperty("java.io.tmpdir")));
|
||||||
LOGGER.severe("Please be aware this is a volatile directory and frameworks could go missing, " +
|
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) {
|
if (id == 1) {
|
||||||
try (
|
try {
|
||||||
InputStream in = getAndroidFrameworkResourcesAsStream();
|
BrutIO.copyAndClose(getAndroidFrameworkResourcesAsStream(), Files.newOutputStream(apk.toPath()));
|
||||||
OutputStream out = Files.newOutputStream(apk.toPath())
|
|
||||||
) {
|
|
||||||
IOUtils.copy(in, out);
|
|
||||||
return apk;
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new AndrolibException(ex);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
|
return apk;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new CantFindFrameworkResException(id);
|
throw new CantFindFrameworkResException(id);
|
||||||
@ -234,11 +223,11 @@ public class Framework {
|
|||||||
|
|
||||||
apk = new File(dir, "1.apk");
|
apk = new File(dir, "1.apk");
|
||||||
|
|
||||||
if (! apk.exists()) {
|
if (!apk.exists()) {
|
||||||
LOGGER.warning("Can't empty framework directory, no file found at: " + apk.getAbsolutePath());
|
LOGGER.warning("Could not empty framework directory, no file found at: " + apk.getAbsolutePath());
|
||||||
} else {
|
} else {
|
||||||
try {
|
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.");
|
LOGGER.warning("More than default framework detected. Please run command with `--force` parameter to wipe framework directory.");
|
||||||
} else {
|
} else {
|
||||||
for (File file : Objects.requireNonNull(dir.listFiles())) {
|
for (File file : Objects.requireNonNull(dir.listFiles())) {
|
||||||
@ -249,13 +238,13 @@ public class Framework {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (NullPointerException e) {
|
} catch (NullPointerException ex) {
|
||||||
throw new AndrolibException(e);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private InputStream getAndroidFrameworkResourcesAsStream() {
|
private InputStream getAndroidFrameworkResourcesAsStream() {
|
||||||
return Jar.class.getResourceAsStream("/brut/androlib/android-framework.jar");
|
return Framework.class.getResourceAsStream("/prebuilt/android-framework.jar");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,13 +21,13 @@ import brut.androlib.apk.ApkInfo;
|
|||||||
import brut.androlib.exceptions.AndrolibException;
|
import brut.androlib.exceptions.AndrolibException;
|
||||||
import brut.androlib.res.data.*;
|
import brut.androlib.res.data.*;
|
||||||
import brut.androlib.res.decoder.*;
|
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.ResValuesXmlSerializable;
|
||||||
import brut.androlib.res.xml.ResXmlPatcher;
|
import brut.androlib.res.xml.ResXmlUtils;
|
||||||
import brut.directory.Directory;
|
import brut.directory.Directory;
|
||||||
import brut.directory.DirectoryException;
|
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 brut.util.OSDetection;
|
||||||
import org.xmlpull.v1.XmlSerializer;
|
import org.xmlpull.v1.XmlSerializer;
|
||||||
|
|
||||||
@ -36,21 +36,23 @@ import java.util.*;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ResourcesDecoder {
|
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 Config mConfig;
|
||||||
private final ApkInfo mApkInfo;
|
private final ApkInfo mApkInfo;
|
||||||
private final ResTable mResTable;
|
private final ResTable mResTable;
|
||||||
private final Map<String, String> mResFileMapping = new HashMap<>();
|
private final Map<String, String> mResFileMapping;
|
||||||
|
|
||||||
private final static String[] IGNORED_PACKAGES = new String[] {
|
|
||||||
"android", "com.htc", "com.lge", "com.lge.internal", "yi", "flyme", "air.com.adobe.appentry",
|
|
||||||
"FFFFFFFFFFFFFFFFFFFFFF" };
|
|
||||||
|
|
||||||
public ResourcesDecoder(Config config, ApkInfo apkInfo) {
|
public ResourcesDecoder(Config config, ApkInfo apkInfo) {
|
||||||
mConfig = config;
|
mConfig = config;
|
||||||
mApkInfo = apkInfo;
|
mApkInfo = apkInfo;
|
||||||
mResTable = new ResTable(mConfig, mApkInfo);
|
mResTable = new ResTable(mConfig, mApkInfo);
|
||||||
|
mResFileMapping = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResTable getResTable() throws AndrolibException {
|
public ResTable getResTable() throws AndrolibException {
|
||||||
@ -61,7 +63,7 @@ public class ResourcesDecoder {
|
|||||||
return mResTable;
|
return mResTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, String> getResFileMapping() {
|
public Map<String, String> getResFileMapping() {
|
||||||
return mResFileMapping;
|
return mResFileMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,56 +71,67 @@ public class ResourcesDecoder {
|
|||||||
mResTable.loadMainPkg(mApkInfo.getApkFile());
|
mResTable.loadMainPkg(mApkInfo.getApkFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decodeManifest(File outDir) throws AndrolibException {
|
public void decodeManifest(File apkDir) throws AndrolibException {
|
||||||
if (!mApkInfo.hasManifest()) {
|
if (!mApkInfo.hasManifest()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AXmlResourceParser axmlParser = new AndroidManifestResourceParser(mResTable);
|
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 {
|
try {
|
||||||
inApk = mApkInfo.getApkFile().getDirectory();
|
inDir = mApkInfo.getApkFile().getDirectory();
|
||||||
out = new FileDirectory(outDir);
|
outDir = new ExtFile(apkDir).getDirectory();
|
||||||
|
|
||||||
if (mApkInfo.hasResources()) {
|
if (mApkInfo.hasResources()) {
|
||||||
LOGGER.info("Decoding AndroidManifest.xml with resources...");
|
LOGGER.info("Decoding AndroidManifest.xml with resources...");
|
||||||
} else {
|
} else {
|
||||||
LOGGER.info("Decoding AndroidManifest.xml with only framework resources...");
|
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);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mApkInfo.hasResources()) {
|
File manifest = new File(apkDir, "AndroidManifest.xml");
|
||||||
if (!mConfig.analysisMode) {
|
|
||||||
// Remove versionName / versionCode (aapt API 16)
|
|
||||||
//
|
|
||||||
// check for a mismatch between resources.arsc package and the package listed in AndroidManifest
|
|
||||||
// also remove the android::versionCode / versionName from manifest for rebuild
|
|
||||||
// this is a required change to prevent aapt warning about conflicting versions
|
|
||||||
// it will be passed as a parameter to aapt like "--min-sdk-version" via apktool.yml
|
|
||||||
adjustPackageManifest(outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml");
|
|
||||||
|
|
||||||
ResXmlPatcher.removeManifestVersions(new File(
|
if (mApkInfo.hasResources() && !mConfig.analysisMode) {
|
||||||
outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml"));
|
// Remove versionName / versionCode (aapt API 16)
|
||||||
|
//
|
||||||
|
// check for a mismatch between resources.arsc package and the package listed in AndroidManifest
|
||||||
|
// also remove the android::versionCode / versionName from manifest for rebuild
|
||||||
|
// this is a required change to prevent aapt warning about conflicting versions
|
||||||
|
// it will be passed as a parameter to aapt like "--min-sdk-version" via apktool.yml
|
||||||
|
adjustPackageManifest(manifest);
|
||||||
|
|
||||||
// update apk info
|
ResXmlUtils.removeManifestVersions(manifest);
|
||||||
mApkInfo.packageInfo.forcedPackageId = String.valueOf(mResTable.getPackageId());
|
|
||||||
|
// 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 {
|
public void updateApkInfo(File apkDir) throws AndrolibException {
|
||||||
mResTable.initApkInfo(mApkInfo, outDir);
|
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
|
// compare resources.arsc package name to the one present in AndroidManifest
|
||||||
ResPackage resPackage = mResTable.getCurrentResPackage();
|
ResPackage resPackage = mResTable.getCurrentResPackage();
|
||||||
String pkgOriginal = resPackage.getName();
|
String pkgOriginal = resPackage.getName();
|
||||||
@ -131,16 +144,16 @@ public class ResourcesDecoder {
|
|||||||
// 2) Check if pkgRenamed is null
|
// 2) Check if pkgRenamed is null
|
||||||
// 3) Check if pkgOriginal === mPackageRenamed
|
// 3) Check if pkgOriginal === mPackageRenamed
|
||||||
// 4) Check if pkgOriginal is ignored via IGNORED_PACKAGES
|
// 4) Check if pkgOriginal is ignored via IGNORED_PACKAGES
|
||||||
if (pkgOriginal == null || pkgRenamed == null || pkgOriginal.equalsIgnoreCase(pkgRenamed)
|
if (pkgOriginal == null || pkgRenamed == null || pkgOriginal.equals(pkgRenamed)
|
||||||
|| (Arrays.asList(IGNORED_PACKAGES).contains(pkgOriginal))) {
|
|| IGNORED_PACKAGES.contains(pkgOriginal)) {
|
||||||
LOGGER.info("Regular manifest package...");
|
LOGGER.info("Regular manifest package...");
|
||||||
} else {
|
} else {
|
||||||
LOGGER.info("Renamed manifest package found! Replacing " + pkgRenamed + " with " + pkgOriginal);
|
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()) {
|
if (!mApkInfo.hasResources()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -156,32 +169,31 @@ public class ResourcesDecoder {
|
|||||||
);
|
);
|
||||||
|
|
||||||
AXmlResourceParser axmlParser = new AXmlResourceParser(mResTable);
|
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);
|
ResFileDecoder fileDecoder = new ResFileDecoder(decoders);
|
||||||
Directory in, out, outRes;
|
Directory inDir, outDir;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
out = new FileDirectory(outDir);
|
inDir = mApkInfo.getApkFile().getDirectory();
|
||||||
in = mApkInfo.getApkFile().getDirectory();
|
outDir = new ExtFile(apkDir).getDirectory().createDir("res");
|
||||||
outRes = out.createDir("res");
|
|
||||||
} catch (DirectoryException ex) {
|
} catch (DirectoryException ex) {
|
||||||
throw new AndrolibException(ex);
|
throw new AndrolibException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtMXSerializer xmlSerializer = getResXmlSerializer();
|
|
||||||
for (ResPackage pkg : mResTable.listMainPackages()) {
|
for (ResPackage pkg : mResTable.listMainPackages()) {
|
||||||
|
|
||||||
LOGGER.info("Decoding file-resources...");
|
LOGGER.info("Decoding file-resources...");
|
||||||
for (ResResource res : pkg.listFiles()) {
|
for (ResResource res : pkg.listFiles()) {
|
||||||
fileDecoder.decode(res, in, outRes, mResFileMapping);
|
fileDecoder.decode(res, inDir, outDir, mResFileMapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Decoding values */* XMLs...");
|
LOGGER.info("Decoding values */* XMLs...");
|
||||||
for (ResValuesFile valuesFile : pkg.listValuesFiles()) {
|
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();
|
AndrolibException decodeError = axmlParser.getFirstError();
|
||||||
@ -190,20 +202,23 @@ public class ResourcesDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtMXSerializer getResXmlSerializer() {
|
public XmlSerializer newXmlSerializer() throws AndrolibException {
|
||||||
ExtMXSerializer serial = new ExtMXSerializer();
|
try {
|
||||||
serial.setProperty(ExtXmlSerializer.PROPERTY_SERIALIZER_INDENTATION, " ");
|
XmlSerializer serial = new MXSerializer();
|
||||||
serial.setProperty(ExtXmlSerializer.PROPERTY_SERIALIZER_LINE_SEPARATOR, System.getProperty("line.separator"));
|
serial.setFeature(MXSerializer.FEATURE_ATTR_VALUE_NO_ESCAPE, true);
|
||||||
serial.setProperty(ExtXmlSerializer.PROPERTY_DEFAULT_ENCODING, "utf-8");
|
serial.setProperty(MXSerializer.PROPERTY_DEFAULT_ENCODING, "utf-8");
|
||||||
serial.setDisabledAttrEscape(true);
|
serial.setProperty(MXSerializer.PROPERTY_INDENTATION, " ");
|
||||||
return serial;
|
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,
|
private void generateValuesFile(ResValuesFile valuesFile, Directory resDir, XmlSerializer serial)
|
||||||
ExtXmlSerializer serial) throws AndrolibException {
|
throws AndrolibException {
|
||||||
try {
|
try (OutputStream out = resDir.getFileOutput(valuesFile.getPath())) {
|
||||||
OutputStream outStream = out.getFileOutput(valuesFile.getPath());
|
serial.setOutput(out, null);
|
||||||
serial.setOutput((outStream), null);
|
|
||||||
serial.startDocument(null, null);
|
serial.startDocument(null, null);
|
||||||
serial.startTag(null, "resources");
|
serial.startTag(null, "resources");
|
||||||
|
|
||||||
@ -215,20 +230,17 @@ public class ResourcesDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serial.endTag(null, "resources");
|
serial.endTag(null, "resources");
|
||||||
serial.newLine();
|
|
||||||
serial.endDocument();
|
serial.endDocument();
|
||||||
serial.flush();
|
serial.flush();
|
||||||
outStream.close();
|
} catch (DirectoryException | IOException ex) {
|
||||||
} catch (IOException | DirectoryException ex) {
|
|
||||||
throw new AndrolibException("Could not generate: " + valuesFile.getPath(), ex);
|
throw new AndrolibException("Could not generate: " + valuesFile.getPath(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generatePublicXml(ResPackage pkg, Directory out,
|
private void generatePublicXml(ResPackage pkg, Directory resDir, XmlSerializer serial)
|
||||||
XmlSerializer serial) throws AndrolibException {
|
throws AndrolibException {
|
||||||
try {
|
try (OutputStream out = resDir.getFileOutput("values/public.xml")) {
|
||||||
OutputStream outStream = out.getFileOutput("values/public.xml");
|
serial.setOutput(out, null);
|
||||||
serial.setOutput(outStream, null);
|
|
||||||
serial.startDocument(null, null);
|
serial.startDocument(null, null);
|
||||||
serial.startTag(null, "resources");
|
serial.startTag(null, "resources");
|
||||||
|
|
||||||
@ -243,8 +255,7 @@ public class ResourcesDecoder {
|
|||||||
serial.endTag(null, "resources");
|
serial.endTag(null, "resources");
|
||||||
serial.endDocument();
|
serial.endDocument();
|
||||||
serial.flush();
|
serial.flush();
|
||||||
outStream.close();
|
} catch (DirectoryException | IOException ex) {
|
||||||
} catch (IOException | DirectoryException ex) {
|
|
||||||
throw new AndrolibException("Could not generate public.xml file", ex);
|
throw new AndrolibException("Could not generate public.xml file", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,165 @@ package brut.androlib.res.data;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ResConfigFlags {
|
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 mcc;
|
||||||
public final short mnc;
|
public final short mnc;
|
||||||
|
|
||||||
@ -137,7 +296,7 @@ public class ResConfigFlags {
|
|||||||
if (localeVariant[0] == '\00') {
|
if (localeVariant[0] == '\00') {
|
||||||
localeVariant = null;
|
localeVariant = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
localeVariant = null;
|
localeVariant = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,7 +660,7 @@ public class ResConfigFlags {
|
|||||||
|
|
||||||
private String toUpper(char[] character) {
|
private String toUpper(char[] character) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (char ch: character) {
|
for (char ch : character) {
|
||||||
sb.append(Character.toUpperCase(ch));
|
sb.append(Character.toUpperCase(ch));
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
@ -522,167 +681,13 @@ public class ResConfigFlags {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final ResConfigFlags other = (ResConfigFlags) obj;
|
final ResConfigFlags other = (ResConfigFlags) obj;
|
||||||
return this.mQualifiers.equals(other.mQualifiers);
|
return mQualifiers.equals(other.mQualifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int hash = 17;
|
int hash = 17;
|
||||||
hash = 31 * hash + this.mQualifiers.hashCode();
|
hash = 31 * hash + mQualifiers.hashCode();
|
||||||
return hash;
|
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());
|
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ public class ResID {
|
|||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int hash = 17;
|
int hash = 17;
|
||||||
hash = 31 * hash + this.id;
|
hash = 31 * hash + id;
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +58,6 @@ public class ResID {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final ResID other = (ResID) obj;
|
final ResID other = (ResID) obj;
|
||||||
return this.id == other.id;
|
return id == other.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,20 +26,26 @@ import java.util.*;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ResPackage {
|
public class ResPackage {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(ResPackage.class.getName());
|
||||||
|
|
||||||
private final ResTable mResTable;
|
private final ResTable mResTable;
|
||||||
private final int mId;
|
private final int mId;
|
||||||
private final String mName;
|
private final String mName;
|
||||||
private final Map<ResID, ResResSpec> mResSpecs = new LinkedHashMap<>();
|
private final Map<ResID, ResResSpec> mResSpecs;
|
||||||
private final Map<ResConfigFlags, ResType> mConfigs = new LinkedHashMap<>();
|
private final Map<ResConfigFlags, ResType> mConfigs;
|
||||||
private final Map<String, ResTypeSpec> mTypes = new LinkedHashMap<>();
|
private final Map<String, ResTypeSpec> mTypes;
|
||||||
private final Set<ResID> mSynthesizedRes = new HashSet<>();
|
private final Set<ResID> mSynthesizedRes;
|
||||||
|
|
||||||
private ResValueFactory mValueFactory;
|
private ResValueFactory mValueFactory;
|
||||||
|
|
||||||
public ResPackage(ResTable resTable, int id, String name) {
|
public ResPackage(ResTable resTable, int id, String name) {
|
||||||
this.mResTable = resTable;
|
mResTable = resTable;
|
||||||
this.mId = id;
|
mId = id;
|
||||||
this.mName = name;
|
mName = name;
|
||||||
|
mResSpecs = new LinkedHashMap<>();
|
||||||
|
mConfigs = new LinkedHashMap<>();
|
||||||
|
mTypes = new LinkedHashMap<>();
|
||||||
|
mSynthesizedRes = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ResResSpec> listResSpecs() {
|
public List<ResResSpec> listResSpecs() {
|
||||||
@ -159,17 +165,17 @@ public class ResPackage {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final ResPackage other = (ResPackage) obj;
|
final ResPackage other = (ResPackage) obj;
|
||||||
if (!Objects.equals(this.mResTable, other.mResTable)) {
|
if (!Objects.equals(mResTable, other.mResTable)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.mId == other.mId;
|
return mId == other.mId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int hash = 17;
|
int hash = 17;
|
||||||
hash = 31 * hash + (this.mResTable != null ? this.mResTable.hashCode() : 0);
|
hash = 31 * hash + (mResTable != null ? mResTable.hashCode() : 0);
|
||||||
hash = 31 * hash + this.mId;
|
hash = 31 * hash + mId;
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,6 +185,4 @@ public class ResPackage {
|
|||||||
}
|
}
|
||||||
return mValueFactory;
|
return mValueFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static Logger LOGGER = Logger.getLogger(ResPackage.class.getName());
|
|
||||||
}
|
}
|
||||||
|
@ -18,42 +18,36 @@ package brut.androlib.res.data;
|
|||||||
|
|
||||||
import brut.androlib.exceptions.AndrolibException;
|
import brut.androlib.exceptions.AndrolibException;
|
||||||
import brut.androlib.exceptions.UndefinedResObjectException;
|
import brut.androlib.exceptions.UndefinedResObjectException;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class ResResSpec {
|
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 ResID mId;
|
||||||
private final String mName;
|
private final String mName;
|
||||||
private final ResPackage mPackage;
|
private final ResPackage mPackage;
|
||||||
private final ResTypeSpec mType;
|
private final ResTypeSpec mType;
|
||||||
private final Map<ResConfigFlags, ResResource> mResources = new LinkedHashMap<>();
|
private final Map<ResConfigFlags, ResResource> mResources;
|
||||||
private static final Set<String> EMPTY_RESOURCE_NAMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
||||||
"0_resource_name_obfuscated",
|
|
||||||
"(name removed)"
|
|
||||||
)));
|
|
||||||
|
|
||||||
public ResResSpec(ResID id, String name, ResPackage pkg, ResTypeSpec type) {
|
public ResResSpec(ResID id, String name, ResPackage pkg, ResTypeSpec type) {
|
||||||
this.mId = id;
|
mId = id;
|
||||||
String cleanName;
|
if (name == null || name.isEmpty() || EMPTY_RESOURCE_NAMES.contains(name)) {
|
||||||
name = EMPTY_RESOURCE_NAMES.contains(name) ? null : name;
|
name = "APKTOOL_DUMMYVAL_" + id.toString();
|
||||||
|
} else if (type.getResSpecUnsafe(name) != null) {
|
||||||
ResResSpec resResSpec = type.getResSpecUnsafe(name);
|
name = String.format("APKTOOL_DUPLICATE_%s_%s", type, id.toString());
|
||||||
if (resResSpec != null) {
|
|
||||||
cleanName = String.format("APKTOOL_DUPLICATE_%s_%s", type, id.toString());
|
|
||||||
} else {
|
|
||||||
cleanName = ((name == null || name.isEmpty()) ? ("APKTOOL_DUMMYVAL_" + id.toString()) : name);
|
|
||||||
}
|
}
|
||||||
|
mName = name;
|
||||||
this.mName = cleanName;
|
mPackage = pkg;
|
||||||
this.mPackage = pkg;
|
mType = type;
|
||||||
this.mType = type;
|
mResources = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<ResResource> listResources() {
|
public Set<ResResource> listResources() {
|
||||||
|
@ -25,9 +25,9 @@ public class ResResource {
|
|||||||
private final ResValue mValue;
|
private final ResValue mValue;
|
||||||
|
|
||||||
public ResResource(ResType config, ResResSpec spec, ResValue value) {
|
public ResResource(ResType config, ResResSpec spec, ResValue value) {
|
||||||
this.mConfig = config;
|
mConfig = config;
|
||||||
this.mResSpec = spec;
|
mResSpec = spec;
|
||||||
this.mValue = value;
|
mValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFilePath() {
|
public String getFilePath() {
|
||||||
|
@ -25,33 +25,30 @@ import brut.androlib.apk.UsesFramework;
|
|||||||
import brut.androlib.res.Framework;
|
import brut.androlib.res.Framework;
|
||||||
import brut.androlib.res.data.value.ResValue;
|
import brut.androlib.res.data.value.ResValue;
|
||||||
import brut.androlib.res.decoder.ARSCDecoder;
|
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.Directory;
|
||||||
import brut.directory.DirectoryException;
|
import brut.directory.DirectoryException;
|
||||||
import brut.directory.ExtFile;
|
import brut.directory.ExtFile;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.*;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ResTable {
|
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 Config mConfig;
|
||||||
private final ApkInfo mApkInfo;
|
private final ApkInfo mApkInfo;
|
||||||
private final Map<Integer, ResPackage> mPackagesById = new HashMap<>();
|
private final Map<Integer, ResPackage> mPackagesById;
|
||||||
private final Map<String, ResPackage> mPackagesByName = new HashMap<>();
|
private final Map<String, ResPackage> mPackagesByName;
|
||||||
private final Set<ResPackage> mMainPackages = new LinkedHashSet<>();
|
private final Set<ResPackage> mMainPackages;
|
||||||
private final Set<ResPackage> mFramePackages = new LinkedHashSet<>();
|
private final Set<ResPackage> mFramePackages;
|
||||||
|
|
||||||
private String mPackageRenamed;
|
private String mPackageRenamed;
|
||||||
private String mPackageOriginal;
|
private String mPackageOriginal;
|
||||||
private int mPackageId;
|
private int mPackageId;
|
||||||
|
private boolean mMainPkgLoaded;
|
||||||
private boolean mMainPkgLoaded = false;
|
|
||||||
|
|
||||||
public ResTable() {
|
public ResTable() {
|
||||||
this(Config.getDefaultConfig(), new ApkInfo());
|
this(Config.getDefaultConfig(), new ApkInfo());
|
||||||
@ -64,6 +61,10 @@ public class ResTable {
|
|||||||
public ResTable(Config config, ApkInfo apkInfo) {
|
public ResTable(Config config, ApkInfo apkInfo) {
|
||||||
mConfig = config;
|
mConfig = config;
|
||||||
mApkInfo = apkInfo;
|
mApkInfo = apkInfo;
|
||||||
|
mPackagesById = new HashMap<>();
|
||||||
|
mPackagesByName = new HashMap<>();
|
||||||
|
mMainPackages = new LinkedHashSet<>();
|
||||||
|
mFramePackages = new LinkedHashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getAnalysisMode() {
|
public boolean getAnalysisMode() {
|
||||||
@ -118,7 +119,7 @@ public class ResTable {
|
|||||||
|
|
||||||
for (int i = 0; i < pkgs.length; i++) {
|
for (int i = 0; i < pkgs.length; i++) {
|
||||||
ResPackage resPackage = pkgs[i];
|
ResPackage resPackage = pkgs[i];
|
||||||
if (resPackage.getResSpecCount() > value && ! resPackage.getName().equalsIgnoreCase("android")) {
|
if (resPackage.getResSpecCount() > value && ! resPackage.getName().equals("android")) {
|
||||||
value = resPackage.getResSpecCount();
|
value = resPackage.getResSpecCount();
|
||||||
id = resPackage.getId();
|
id = resPackage.getId();
|
||||||
index = i;
|
index = i;
|
||||||
@ -175,11 +176,13 @@ public class ResTable {
|
|||||||
return pkg;
|
return pkg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResPackage[] loadResPackagesFromApk(ExtFile apkFile, boolean keepBrokenResources) throws AndrolibException {
|
private ResPackage[] loadResPackagesFromApk(ExtFile apkFile, boolean keepBrokenResources)
|
||||||
|
throws AndrolibException {
|
||||||
try {
|
try {
|
||||||
Directory dir = apkFile.getDirectory();
|
Directory dir = apkFile.getDirectory();
|
||||||
try (BufferedInputStream bfi = new BufferedInputStream(dir.getFileInput("resources.arsc"))) {
|
try (BufferedInputStream in = new BufferedInputStream(dir.getFileInput("resources.arsc"))) {
|
||||||
return ARSCDecoder.decode(bfi, false, keepBrokenResources, this).getPackages();
|
ARSCDecoder decoder = new ARSCDecoder(in, this, false, keepBrokenResources);
|
||||||
|
return decoder.decode().getPackages();
|
||||||
}
|
}
|
||||||
} catch (DirectoryException | IOException ex) {
|
} catch (DirectoryException | IOException ex) {
|
||||||
throw new AndrolibException("Could not load resources.arsc from file: " + apkFile, ex);
|
throw new AndrolibException("Could not load resources.arsc from file: " + apkFile, ex);
|
||||||
@ -190,7 +193,7 @@ public class ResTable {
|
|||||||
int id = 0;
|
int id = 0;
|
||||||
int value = 0;
|
int value = 0;
|
||||||
for (ResPackage resPackage : mPackagesById.values()) {
|
for (ResPackage resPackage : mPackagesById.values()) {
|
||||||
if (resPackage.getResSpecCount() > value && !resPackage.getName().equalsIgnoreCase("android")) {
|
if (resPackage.getResSpecCount() > value && !resPackage.getName().equals("android")) {
|
||||||
value = resPackage.getResSpecCount();
|
value = resPackage.getResSpecCount();
|
||||||
id = resPackage.getId();
|
id = resPackage.getId();
|
||||||
}
|
}
|
||||||
@ -220,8 +223,8 @@ public class ResTable {
|
|||||||
return pkg;
|
return pkg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResValue getValue(String package_, String type, String name) throws AndrolibException {
|
public ResValue getValue(String pkg, String type, String name) throws AndrolibException {
|
||||||
return getPackage(package_).getType(type).getResSpec(name).getDefaultResource().getValue();
|
return getPackage(pkg).getType(type).getResSpec(name).getDefaultResource().getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addPackage(ResPackage pkg, boolean main) throws AndrolibException {
|
public void addPackage(ResPackage pkg, boolean main) throws AndrolibException {
|
||||||
@ -260,18 +263,15 @@ public class ResTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setSparseResources(boolean flag) {
|
public void setSparseResources(boolean flag) {
|
||||||
if (mApkInfo.sparseResources != flag) {
|
|
||||||
LOGGER.info("Sparsely packed resources detected.");
|
|
||||||
}
|
|
||||||
mApkInfo.sparseResources = flag;
|
mApkInfo.sparseResources = flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearSdkInfo() {
|
public void setCompactEntries(boolean flag) {
|
||||||
mApkInfo.getSdkInfo().clear();
|
mApkInfo.compactEntries = flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSdkInfo(String key, String value) {
|
public void addSdkInfo(String key, String value) {
|
||||||
mApkInfo.getSdkInfo().put(key, value);
|
mApkInfo.sdkInfo.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setVersionName(String versionName) {
|
public void setVersionName(String versionName) {
|
||||||
@ -307,14 +307,14 @@ public class ResTable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initApkInfo(ApkInfo apkInfo, File outDir) throws AndrolibException {
|
public void initApkInfo(ApkInfo apkInfo, File apkDir) throws AndrolibException {
|
||||||
apkInfo.isFrameworkApk = isFrameworkApk();
|
apkInfo.isFrameworkApk = isFrameworkApk();
|
||||||
apkInfo.usesFramework = getUsesFramework();
|
apkInfo.usesFramework = getUsesFramework();
|
||||||
if (!mApkInfo.getSdkInfo().isEmpty()) {
|
if (!mApkInfo.sdkInfo.isEmpty()) {
|
||||||
updateSdkInfoFromResources(outDir);
|
updateSdkInfoFromResources(apkDir);
|
||||||
}
|
}
|
||||||
initPackageInfo();
|
initPackageInfo();
|
||||||
loadVersionName(outDir);
|
loadVersionName(apkDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UsesFramework getUsesFramework() {
|
private UsesFramework getUsesFramework() {
|
||||||
@ -330,25 +330,26 @@ public class ResTable {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSdkInfoFromResources(File outDir) {
|
private void updateSdkInfoFromResources(File apkDir) {
|
||||||
String refValue;
|
String minSdkVersion = mApkInfo.getMinSdkVersion();
|
||||||
Map<String, String> sdkInfo = mApkInfo.getSdkInfo();
|
if (minSdkVersion != null) {
|
||||||
if (sdkInfo.get("minSdkVersion") != null) {
|
String refValue = ResXmlUtils.pullValueFromIntegers(apkDir, minSdkVersion);
|
||||||
refValue = ResXmlPatcher.pullValueFromIntegers(outDir, sdkInfo.get("minSdkVersion"));
|
|
||||||
if (refValue != null) {
|
if (refValue != null) {
|
||||||
sdkInfo.put("minSdkVersion", refValue);
|
mApkInfo.setMinSdkVersion(refValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sdkInfo.get("targetSdkVersion") != null) {
|
String targetSdkVersion = mApkInfo.getTargetSdkVersion();
|
||||||
refValue = ResXmlPatcher.pullValueFromIntegers(outDir, sdkInfo.get("targetSdkVersion"));
|
if (targetSdkVersion != null) {
|
||||||
|
String refValue = ResXmlUtils.pullValueFromIntegers(apkDir, targetSdkVersion);
|
||||||
if (refValue != null) {
|
if (refValue != null) {
|
||||||
sdkInfo.put("targetSdkVersion", refValue);
|
mApkInfo.setTargetSdkVersion(refValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sdkInfo.get("maxSdkVersion") != null) {
|
String maxSdkVersion = mApkInfo.getMaxSdkVersion();
|
||||||
refValue = ResXmlPatcher.pullValueFromIntegers(outDir, sdkInfo.get("maxSdkVersion"));
|
if (maxSdkVersion != null) {
|
||||||
|
String refValue = ResXmlUtils.pullValueFromIntegers(apkDir, maxSdkVersion);
|
||||||
if (refValue != null) {
|
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
|
// 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.renameManifestPackage = renamed;
|
||||||
}
|
}
|
||||||
mApkInfo.packageInfo.forcedPackageId = String.valueOf(id);
|
mApkInfo.packageInfo.forcedPackageId = String.valueOf(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadVersionName(File outDir) {
|
private void loadVersionName(File apkDir) {
|
||||||
String versionName = mApkInfo.versionInfo.versionName;
|
String versionName = mApkInfo.versionInfo.versionName;
|
||||||
String refValue = ResXmlPatcher.pullValueFromStrings(outDir, versionName);
|
String refValue = ResXmlUtils.pullValueFromStrings(apkDir, versionName);
|
||||||
if (refValue != null) {
|
if (refValue != null) {
|
||||||
mApkInfo.versionInfo.versionName = refValue;
|
mApkInfo.versionInfo.versionName = refValue;
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,11 @@ import java.util.*;
|
|||||||
|
|
||||||
public class ResType {
|
public class ResType {
|
||||||
private final ResConfigFlags mFlags;
|
private final ResConfigFlags mFlags;
|
||||||
private final Map<ResResSpec, ResResource> mResources = new LinkedHashMap<>();
|
private final Map<ResResSpec, ResResource> mResources;
|
||||||
|
|
||||||
public ResType(ResConfigFlags flags) {
|
public ResType(ResConfigFlags flags) {
|
||||||
this.mFlags = flags;
|
mFlags = flags;
|
||||||
|
mResources = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResResource getResource(ResResSpec spec) throws AndrolibException {
|
public ResResource getResource(ResResSpec spec) throws AndrolibException {
|
||||||
|
@ -21,7 +21,6 @@ import brut.androlib.exceptions.UndefinedResObjectException;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public final class ResTypeSpec {
|
public final class ResTypeSpec {
|
||||||
|
|
||||||
public static final String RES_TYPE_NAME_ARRAY = "array";
|
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 = "attr";
|
||||||
public static final String RES_TYPE_NAME_ATTR_PRIVATE = "^attr-private";
|
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";
|
public static final String RES_TYPE_NAME_STYLES = "style";
|
||||||
|
|
||||||
private final String mName;
|
private final String mName;
|
||||||
private final Map<String, ResResSpec> mResSpecs = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
private final int mId;
|
private final int mId;
|
||||||
|
private final Map<String, ResResSpec> mResSpecs;
|
||||||
|
|
||||||
public ResTypeSpec(String name, int id) {
|
public ResTypeSpec(String name, int id) {
|
||||||
this.mName = name;
|
mName = name;
|
||||||
this.mId = id;
|
mId = id;
|
||||||
|
mResSpecs = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@ -48,7 +47,7 @@ public final class ResTypeSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isString() {
|
public boolean isString() {
|
||||||
return mName.equalsIgnoreCase(RES_TYPE_NAME_STRING);
|
return mName.equals(RES_TYPE_NAME_STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResResSpec getResSpec(String name) throws AndrolibException {
|
public ResResSpec getResSpec(String name) throws AndrolibException {
|
||||||
|
@ -24,12 +24,13 @@ public class ResValuesFile {
|
|||||||
private final ResPackage mPackage;
|
private final ResPackage mPackage;
|
||||||
private final ResTypeSpec mType;
|
private final ResTypeSpec mType;
|
||||||
private final ResType mConfig;
|
private final ResType mConfig;
|
||||||
private final Set<ResResource> mResources = new LinkedHashSet<>();
|
private final Set<ResResource> mResources;
|
||||||
|
|
||||||
public ResValuesFile(ResPackage pkg, ResTypeSpec type, ResType config) {
|
public ResValuesFile(ResPackage pkg, ResTypeSpec type, ResType config) {
|
||||||
this.mPackage = pkg;
|
mPackage = pkg;
|
||||||
this.mType = type;
|
mType = type;
|
||||||
this.mConfig = config;
|
mConfig = config;
|
||||||
|
mResources = new LinkedHashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPath() {
|
public String getPath() {
|
||||||
@ -63,17 +64,17 @@ public class ResValuesFile {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final ResValuesFile other = (ResValuesFile) obj;
|
final ResValuesFile other = (ResValuesFile) obj;
|
||||||
if (!Objects.equals(this.mType, other.mType)) {
|
if (!Objects.equals(mType, other.mType)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return Objects.equals(this.mConfig, other.mConfig);
|
return Objects.equals(mConfig, other.mConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int hash = 17;
|
int hash = 17;
|
||||||
hash = 31 * hash + (this.mType != null ? this.mType.hashCode() : 0);
|
hash = 31 * hash + (mType != null ? mType.hashCode() : 0);
|
||||||
hash = 31 * hash + (this.mConfig != null ? this.mConfig.hashCode() : 0);
|
hash = 31 * hash + (mConfig != null ? mConfig.hashCode() : 0);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,13 @@ import brut.androlib.res.data.ResPackage;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ARSCData {
|
public class ARSCData {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(ARSCData.class.getName());
|
||||||
|
|
||||||
private final ResPackage[] mPackages;
|
private final ResPackage[] mPackages;
|
||||||
private final FlagsOffset[] mFlagsOffsets;
|
private final FlagsOffset[] mFlagsOffsets;
|
||||||
|
|
||||||
public ARSCData(ResPackage[] packages, FlagsOffset[] flagsOffsets) {
|
public ARSCData(ResPackage[] pkgs, FlagsOffset[] flagsOffsets) {
|
||||||
mPackages = packages;
|
mPackages = pkgs;
|
||||||
mFlagsOffsets = flagsOffsets;
|
mFlagsOffsets = flagsOffsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +65,4 @@ public class ARSCData {
|
|||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(ARSCData.class.getName());
|
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
package brut.androlib.res.data.arsc;
|
package brut.androlib.res.data.arsc;
|
||||||
|
|
||||||
import brut.util.ExtCountingDataInput;
|
|
||||||
import brut.util.ExtDataInput;
|
import brut.util.ExtDataInput;
|
||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
@ -25,13 +24,40 @@ import java.math.BigInteger;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ARSCHeader {
|
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 short type;
|
||||||
public final int headerSize;
|
public final int headerSize;
|
||||||
public final int chunkSize;
|
public final int chunkSize;
|
||||||
public final int startPosition;
|
public final long startPosition;
|
||||||
public final int endPosition;
|
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.type = type;
|
||||||
this.headerSize = headerSize;
|
this.headerSize = headerSize;
|
||||||
this.chunkSize = chunkSize;
|
this.chunkSize = chunkSize;
|
||||||
@ -39,9 +65,9 @@ public class ARSCHeader {
|
|||||||
this.endPosition = headerStart + chunkSize;
|
this.endPosition = headerStart + chunkSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ARSCHeader read(ExtCountingDataInput in) throws IOException {
|
public static ARSCHeader read(ExtDataInput in) throws IOException {
|
||||||
short type;
|
short type;
|
||||||
int start = in.position();
|
long start = in.position();
|
||||||
try {
|
try {
|
||||||
type = in.readShort();
|
type = in.readShort();
|
||||||
} catch (EOFException ex) {
|
} catch (EOFException ex) {
|
||||||
@ -50,13 +76,13 @@ public class ARSCHeader {
|
|||||||
return new ARSCHeader(type, in.readShort(), in.readInt(), start);
|
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
|
// 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.
|
// 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
|
// However, this runs after each chunk and not every chunk reading has a specific distinction between the
|
||||||
// header and the body.
|
// header and the body.
|
||||||
int actualHeaderSize = in.position() - this.startPosition;
|
int actualHeaderSize = (int) (in.position() - startPosition);
|
||||||
int exceedingSize = this.headerSize - actualHeaderSize;
|
int exceedingSize = headerSize - actualHeaderSize;
|
||||||
if (exceedingSize > 0) {
|
if (exceedingSize > 0) {
|
||||||
byte[] buf = new byte[exceedingSize];
|
byte[] buf = new byte[exceedingSize];
|
||||||
in.readFully(buf);
|
in.readFully(buf);
|
||||||
@ -64,11 +90,11 @@ public class ARSCHeader {
|
|||||||
|
|
||||||
if (exceedingBI.equals(BigInteger.ZERO)) {
|
if (exceedingBI.equals(BigInteger.ZERO)) {
|
||||||
LOGGER.fine(String.format("Chunk header size (%d), read (%d), but exceeding bytes are all zero.",
|
LOGGER.fine(String.format("Chunk header size (%d), read (%d), but exceeding bytes are all zero.",
|
||||||
this.headerSize, actualHeaderSize
|
headerSize, actualHeaderSize
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
LOGGER.warning(String.format("Chunk header size (%d), read (%d). Exceeding bytes: 0x%X.",
|
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 {
|
public void skipChunk(ExtDataInput in) throws IOException {
|
||||||
in.skipBytes(chunkSize - headerSize);
|
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());
|
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ package brut.androlib.res.data.arsc;
|
|||||||
import brut.androlib.res.data.value.ResValue;
|
import brut.androlib.res.data.value.ResValue;
|
||||||
|
|
||||||
public class EntryData {
|
public class EntryData {
|
||||||
public short mFlags;
|
public short flags;
|
||||||
public int mSpecNamesId;
|
public int specNamesId;
|
||||||
public ResValue mValue;
|
public ResValue value;
|
||||||
}
|
}
|
||||||
|
@ -31,38 +31,38 @@ package brut.androlib.res.data.axml;
|
|||||||
* !! functions expect 'prefix'+'uri' pairs, not 'uri'+'prefix' !!
|
* !! functions expect 'prefix'+'uri' pairs, not 'uri'+'prefix' !!
|
||||||
*/
|
*/
|
||||||
public final class NamespaceStack {
|
public final class NamespaceStack {
|
||||||
private int[] m_data;
|
private int[] mData;
|
||||||
private int m_dataLength;
|
private int mDataLength;
|
||||||
private int m_depth;
|
private int mDepth;
|
||||||
|
|
||||||
public NamespaceStack() {
|
public NamespaceStack() {
|
||||||
m_data = new int[32];
|
mData = new int[32];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
m_dataLength = 0;
|
mDataLength = 0;
|
||||||
m_depth = 0;
|
mDepth = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCurrentCount() {
|
public int getCurrentCount() {
|
||||||
if (m_dataLength == 0) {
|
if (mDataLength == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int offset = m_dataLength - 1;
|
int offset = mDataLength - 1;
|
||||||
return m_data[offset];
|
return mData[offset];
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getAccumulatedCount(int depth) {
|
public int getAccumulatedCount(int depth) {
|
||||||
if (m_dataLength == 0 || depth < 0) {
|
if (mDataLength == 0 || depth < 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (depth > m_depth) {
|
if (depth > mDepth) {
|
||||||
depth = m_depth;
|
depth = mDepth;
|
||||||
}
|
}
|
||||||
int accumulatedCount = 0;
|
int accumulatedCount = 0;
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
for (; depth != 0; --depth) {
|
for (; depth != 0; --depth) {
|
||||||
int count = m_data[offset];
|
int count = mData[offset];
|
||||||
accumulatedCount += count;
|
accumulatedCount += count;
|
||||||
offset += (2 + count * 2);
|
offset += (2 + count * 2);
|
||||||
}
|
}
|
||||||
@ -70,34 +70,34 @@ public final class NamespaceStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void push(int prefix, int uri) {
|
public void push(int prefix, int uri) {
|
||||||
if (m_depth == 0) {
|
if (mDepth == 0) {
|
||||||
increaseDepth();
|
increaseDepth();
|
||||||
}
|
}
|
||||||
ensureDataCapacity(2);
|
ensureDataCapacity(2);
|
||||||
int offset = m_dataLength - 1;
|
int offset = mDataLength - 1;
|
||||||
int count = m_data[offset];
|
int count = mData[offset];
|
||||||
m_data[offset - 1 - count * 2] = count + 1;
|
mData[offset - 1 - count * 2] = count + 1;
|
||||||
m_data[offset] = prefix;
|
mData[offset] = prefix;
|
||||||
m_data[offset + 1] = uri;
|
mData[offset + 1] = uri;
|
||||||
m_data[offset + 2] = count + 1;
|
mData[offset + 2] = count + 1;
|
||||||
m_dataLength += 2;
|
mDataLength += 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean pop() {
|
public boolean pop() {
|
||||||
if (m_dataLength == 0) {
|
if (mDataLength == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int offset = m_dataLength - 1;
|
int offset = mDataLength - 1;
|
||||||
int count = m_data[offset];
|
int count = mData[offset];
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
count -= 1;
|
count -= 1;
|
||||||
offset -= 2;
|
offset -= 2;
|
||||||
m_data[offset] = count;
|
mData[offset] = count;
|
||||||
offset -= (1 + count * 2);
|
offset -= (1 + count * 2);
|
||||||
m_data[offset] = count;
|
mData[offset] = count;
|
||||||
m_dataLength -= 2;
|
mDataLength -= 2;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,58 +114,58 @@ public final class NamespaceStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getDepth() {
|
public int getDepth() {
|
||||||
return m_depth;
|
return mDepth;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void increaseDepth() {
|
public void increaseDepth() {
|
||||||
ensureDataCapacity(2);
|
ensureDataCapacity(2);
|
||||||
int offset = m_dataLength;
|
int offset = mDataLength;
|
||||||
m_data[offset] = 0;
|
mData[offset] = 0;
|
||||||
m_data[offset + 1] = 0;
|
mData[offset + 1] = 0;
|
||||||
m_dataLength += 2;
|
mDataLength += 2;
|
||||||
m_depth += 1;
|
mDepth += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decreaseDepth() {
|
public void decreaseDepth() {
|
||||||
if (m_dataLength == 0) {
|
if (mDataLength == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int offset = m_dataLength - 1;
|
int offset = mDataLength - 1;
|
||||||
int count = m_data[offset];
|
int count = mData[offset];
|
||||||
if ((offset - 1 - count * 2) == 0) {
|
if ((offset - 1 - count * 2) == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_dataLength -= 2 + count * 2;
|
mDataLength -= 2 + count * 2;
|
||||||
m_depth -= 1;
|
mDepth -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureDataCapacity(int capacity) {
|
private void ensureDataCapacity(int capacity) {
|
||||||
int available = (m_data.length - m_dataLength);
|
int available = (mData.length - mDataLength);
|
||||||
if (available > capacity) {
|
if (available > capacity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int newLength = (m_data.length + available) * 2;
|
int newLength = (mData.length + available) * 2;
|
||||||
int[] newData = new int[newLength];
|
int[] newData = new int[newLength];
|
||||||
System.arraycopy(m_data, 0, newData, 0, m_dataLength);
|
System.arraycopy(mData, 0, newData, 0, mDataLength);
|
||||||
m_data = newData;
|
mData = newData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int find(int prefixOrUri, boolean prefix) {
|
private int find(int prefixOrUri, boolean prefix) {
|
||||||
if (m_dataLength == 0) {
|
if (mDataLength == 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
int offset = m_dataLength - 1;
|
int offset = mDataLength - 1;
|
||||||
for (int i = m_depth; i != 0; --i) {
|
for (int i = mDepth; i != 0; --i) {
|
||||||
int count = m_data[offset];
|
int count = mData[offset];
|
||||||
offset -= 2;
|
offset -= 2;
|
||||||
for (; count != 0; --count) {
|
for (; count != 0; --count) {
|
||||||
if (prefix) {
|
if (prefix) {
|
||||||
if (m_data[offset] == prefixOrUri) {
|
if (mData[offset] == prefixOrUri) {
|
||||||
return m_data[offset + 1];
|
return mData[offset + 1];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (m_data[offset + 1] == prefixOrUri) {
|
if (mData[offset + 1] == prefixOrUri) {
|
||||||
return m_data[offset];
|
return mData[offset];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
offset -= 2;
|
offset -= 2;
|
||||||
@ -175,12 +175,12 @@ public final class NamespaceStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int get(int index, boolean prefix) {
|
private int get(int index, boolean prefix) {
|
||||||
if (m_dataLength == 0 || index < 0) {
|
if (mDataLength == 0 || index < 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
for (int i = m_depth; i != 0; --i) {
|
for (int i = mDepth; i != 0; --i) {
|
||||||
int count = m_data[offset];
|
int count = mData[offset];
|
||||||
if (index >= count) {
|
if (index >= count) {
|
||||||
index -= count;
|
index -= count;
|
||||||
offset += (2 + count * 2);
|
offset += (2 + count * 2);
|
||||||
@ -190,7 +190,7 @@ public final class NamespaceStack {
|
|||||||
if (!prefix) {
|
if (!prefix) {
|
||||||
offset += 1;
|
offset += 1;
|
||||||
}
|
}
|
||||||
return m_data[offset];
|
return mData[offset];
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package brut.androlib.res.data.ninepatch;
|
package brut.androlib.res.data.ninepatch;
|
||||||
|
|
||||||
import brut.util.ExtDataInput;
|
import brut.util.ExtDataInput;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class NinePatchData {
|
public class NinePatchData {
|
||||||
@ -32,19 +33,19 @@ public class NinePatchData {
|
|||||||
this.yDivs = yDivs;
|
this.yDivs = yDivs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NinePatchData decode(ExtDataInput di) throws IOException {
|
public static NinePatchData decode(ExtDataInput in) throws IOException {
|
||||||
di.skipBytes(1); // wasDeserialized
|
in.skipBytes(1); // wasDeserialized
|
||||||
byte numXDivs = di.readByte();
|
byte numXDivs = in.readByte();
|
||||||
byte numYDivs = di.readByte();
|
byte numYDivs = in.readByte();
|
||||||
di.skipBytes(1); // numColors
|
in.skipBytes(1); // numColors
|
||||||
di.skipBytes(8); // xDivs/yDivs offset
|
in.skipBytes(8); // xDivs/yDivs offset
|
||||||
int padLeft = di.readInt();
|
int padLeft = in.readInt();
|
||||||
int padRight = di.readInt();
|
int padRight = in.readInt();
|
||||||
int padTop = di.readInt();
|
int padTop = in.readInt();
|
||||||
int padBottom = di.readInt();
|
int padBottom = in.readInt();
|
||||||
di.skipBytes(4); // colorsOffset
|
in.skipBytes(4); // colorsOffset
|
||||||
int[] xDivs = di.readIntArray(numXDivs);
|
int[] xDivs = in.readIntArray(numXDivs);
|
||||||
int[] yDivs = di.readIntArray(numYDivs);
|
int[] yDivs = in.readIntArray(numYDivs);
|
||||||
|
|
||||||
return new NinePatchData(padLeft, padRight, padTop, padBottom, xDivs, yDivs);
|
return new NinePatchData(padLeft, padRight, padTop, padBottom, xDivs, yDivs);
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package brut.androlib.res.data.ninepatch;
|
package brut.androlib.res.data.ninepatch;
|
||||||
|
|
||||||
import brut.util.ExtDataInput;
|
import brut.util.ExtDataInput;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class OpticalInset {
|
public class OpticalInset {
|
||||||
@ -29,11 +30,11 @@ public class OpticalInset {
|
|||||||
this.layoutBoundsBottom = layoutBoundsBottom;
|
this.layoutBoundsBottom = layoutBoundsBottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OpticalInset decode(ExtDataInput di) throws IOException {
|
public static OpticalInset decode(ExtDataInput in) throws IOException {
|
||||||
int layoutBoundsLeft = Integer.reverseBytes(di.readInt());
|
int layoutBoundsLeft = Integer.reverseBytes(in.readInt());
|
||||||
int layoutBoundsTop = Integer.reverseBytes(di.readInt());
|
int layoutBoundsTop = Integer.reverseBytes(in.readInt());
|
||||||
int layoutBoundsRight = Integer.reverseBytes(di.readInt());
|
int layoutBoundsRight = Integer.reverseBytes(in.readInt());
|
||||||
int layoutBoundsBottom = Integer.reverseBytes(di.readInt());
|
int layoutBoundsBottom = Integer.reverseBytes(in.readInt());
|
||||||
return new OpticalInset(layoutBoundsLeft, layoutBoundsTop, layoutBoundsRight, layoutBoundsBottom);
|
return new OpticalInset(layoutBoundsLeft, layoutBoundsTop, layoutBoundsRight, layoutBoundsBottom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,15 +20,19 @@ import brut.androlib.exceptions.AndrolibException;
|
|||||||
import brut.androlib.res.data.ResResource;
|
import brut.androlib.res.data.ResResource;
|
||||||
import brut.androlib.res.xml.ResValuesXmlSerializable;
|
import brut.androlib.res.xml.ResValuesXmlSerializable;
|
||||||
import brut.util.Duo;
|
import brut.util.Duo;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
import org.xmlpull.v1.XmlSerializer;
|
import org.xmlpull.v1.XmlSerializer;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Set;
|
||||||
|
|
||||||
public class ResArrayValue extends ResBagValue implements ResValuesXmlSerializable {
|
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) {
|
ResArrayValue(ResReferenceValue parent, Duo<Integer, ResScalarValue>[] items) {
|
||||||
super(parent);
|
super(parent);
|
||||||
|
|
||||||
mItems = new ResScalarValue[items.length];
|
mItems = new ResScalarValue[items.length];
|
||||||
for (int i = 0; i < items.length; i++) {
|
for (int i = 0; i < items.length; i++) {
|
||||||
mItems[i] = items[i].m2;
|
mItems[i] = items[i].m2;
|
||||||
@ -41,8 +45,8 @@ public class ResArrayValue extends ResBagValue implements ResValuesXmlSerializab
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serializeToResValuesXml(XmlSerializer serializer,
|
public void serializeToResValuesXml(XmlSerializer serializer, ResResource res)
|
||||||
ResResource res) throws IOException, AndrolibException {
|
throws IOException, AndrolibException {
|
||||||
String type = getType();
|
String type = getType();
|
||||||
type = (type == null ? "" : type + "-") + "array";
|
type = (type == null ? "" : type + "-") + "array";
|
||||||
serializer.startTag(null, type);
|
serializer.startTag(null, type);
|
||||||
@ -83,12 +87,9 @@ public class ResArrayValue extends ResBagValue implements ResValuesXmlSerializab
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!Arrays.asList(AllowedArrayTypes).contains(type)) {
|
if (!ALLOWED_ARRAY_TYPES.contains(type)) {
|
||||||
return "string";
|
return "string";
|
||||||
}
|
}
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ResScalarValue[] mItems;
|
|
||||||
private final String[] AllowedArrayTypes = {"string", "integer"};
|
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,28 @@ import org.xmlpull.v1.XmlSerializer;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class ResAttr extends ResBagValue implements ResValuesXmlSerializable {
|
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) {
|
ResAttr(ResReferenceValue parentVal, int type, Integer min, Integer max, Boolean l10n) {
|
||||||
super(parentVal);
|
super(parentVal);
|
||||||
mType = type;
|
mType = type;
|
||||||
@ -139,26 +161,4 @@ public class ResAttr extends ResBagValue implements ResValuesXmlSerializable {
|
|||||||
}
|
}
|
||||||
return s.substring(1);
|
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;
|
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ public class ResBagValue extends ResValue implements ResValuesXmlSerializable {
|
|||||||
protected final ResReferenceValue mParent;
|
protected final ResReferenceValue mParent;
|
||||||
|
|
||||||
public ResBagValue(ResReferenceValue parent) {
|
public ResBagValue(ResReferenceValue parent) {
|
||||||
this.mParent = parent;
|
mParent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -21,7 +21,7 @@ public class ResBoolValue extends ResScalarValue {
|
|||||||
|
|
||||||
public ResBoolValue(boolean value, int rawIntValue, String rawValue) {
|
public ResBoolValue(boolean value, int rawIntValue, String rawValue) {
|
||||||
super("bool", rawIntValue, rawValue);
|
super("bool", rawIntValue, rawValue);
|
||||||
this.mValue = value;
|
mValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getValue() {
|
public boolean getValue() {
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package brut.androlib.res.data.value;
|
package brut.androlib.res.data.value;
|
||||||
|
|
||||||
public class ResColorValue extends ResIntValue {
|
public class ResColorValue extends ResIntValue {
|
||||||
|
|
||||||
public ResColorValue(int value, String rawValue) {
|
public ResColorValue(int value, String rawValue) {
|
||||||
super(value, rawValue, "color");
|
super(value, rawValue, "color");
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import android.util.TypedValue;
|
|||||||
import brut.androlib.exceptions.AndrolibException;
|
import brut.androlib.exceptions.AndrolibException;
|
||||||
|
|
||||||
public class ResDimenValue extends ResIntValue {
|
public class ResDimenValue extends ResIntValue {
|
||||||
|
|
||||||
public ResDimenValue(int value, String rawValue) {
|
public ResDimenValue(int value, String rawValue) {
|
||||||
super(value, rawValue, "dimen");
|
super(value, rawValue, "dimen");
|
||||||
}
|
}
|
||||||
|
@ -20,18 +20,18 @@ import brut.androlib.exceptions.AndrolibException;
|
|||||||
|
|
||||||
public class ResEmptyValue extends ResScalarValue {
|
public class ResEmptyValue extends ResScalarValue {
|
||||||
protected final int mValue;
|
protected final int mValue;
|
||||||
protected int type;
|
protected int mType;
|
||||||
|
|
||||||
public ResEmptyValue(int value, String rawValue, int type) {
|
public ResEmptyValue(int value, String rawValue, int type) {
|
||||||
this(value, rawValue, "integer");
|
this(value, rawValue, "integer");
|
||||||
this.type = type;
|
mType = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResEmptyValue(int value, String rawValue, String type) {
|
public ResEmptyValue(int value, String rawValue, String type) {
|
||||||
super(type, value, rawValue);
|
super(type, value, rawValue);
|
||||||
if (value != 1)
|
if (value != 1)
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
this.mValue = value;
|
mValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getValue() {
|
public int getValue() {
|
||||||
|
@ -28,10 +28,16 @@ import java.util.Map;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ResEnumAttr extends ResAttr {
|
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,
|
ResEnumAttr(ResReferenceValue parent, int type, Integer min, Integer max,
|
||||||
Boolean l10n, Duo<ResReferenceValue, ResScalarValue>[] items) {
|
Boolean l10n, Duo<ResReferenceValue, ResScalarValue>[] items) {
|
||||||
super(parent, type, min, max, l10n);
|
super(parent, type, min, max, l10n);
|
||||||
mItems = items;
|
mItems = items;
|
||||||
|
mItemsCache = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -85,9 +91,4 @@ public class ResEnumAttr extends ResAttr {
|
|||||||
}
|
}
|
||||||
return value2;
|
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());
|
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ public class ResFileValue extends ResIntBasedValue {
|
|||||||
|
|
||||||
public ResFileValue(String path, int rawIntValue) {
|
public ResFileValue(String path, int rawIntValue) {
|
||||||
super(rawIntValue);
|
super(rawIntValue);
|
||||||
this.mPath = path;
|
mPath = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStrippedPath() throws AndrolibException {
|
public String getStrippedPath() throws AndrolibException {
|
||||||
|
@ -28,6 +28,12 @@ import java.util.Arrays;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ResFlagsAttr extends ResAttr {
|
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,
|
ResFlagsAttr(ResReferenceValue parent, int type, Integer min, Integer max,
|
||||||
Boolean l10n, Duo<ResReferenceValue, ResScalarValue>[] items) {
|
Boolean l10n, Duo<ResReferenceValue, ResScalarValue>[] items) {
|
||||||
super(parent, type, min, max, l10n);
|
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)));
|
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());
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ public class ResFloatValue extends ResScalarValue {
|
|||||||
|
|
||||||
public ResFloatValue(float value, int rawIntValue, String rawValue) {
|
public ResFloatValue(float value, int rawIntValue, String rawValue) {
|
||||||
super("float", rawIntValue, rawValue);
|
super("float", rawIntValue, rawValue);
|
||||||
this.mValue = value;
|
mValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getValue() {
|
public float getValue() {
|
||||||
|
@ -20,6 +20,7 @@ import android.util.TypedValue;
|
|||||||
import brut.androlib.exceptions.AndrolibException;
|
import brut.androlib.exceptions.AndrolibException;
|
||||||
|
|
||||||
public class ResFractionValue extends ResIntValue {
|
public class ResFractionValue extends ResIntValue {
|
||||||
|
|
||||||
public ResFractionValue(int value, String rawValue) {
|
public ResFractionValue(int value, String rawValue) {
|
||||||
super(value, rawValue, "fraction");
|
super(value, rawValue, "fraction");
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import org.xmlpull.v1.XmlSerializer;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class ResIdValue extends ResValue implements ResValuesXmlSerializable {
|
public class ResIdValue extends ResValue implements ResValuesXmlSerializable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serializeToResValuesXml(XmlSerializer serializer, ResResource res) throws IOException {
|
public void serializeToResValuesXml(XmlSerializer serializer, ResResource res) throws IOException {
|
||||||
serializer.startTag(null, "item");
|
serializer.startTag(null, "item");
|
||||||
|
@ -21,16 +21,16 @@ import brut.androlib.exceptions.AndrolibException;
|
|||||||
|
|
||||||
public class ResIntValue extends ResScalarValue {
|
public class ResIntValue extends ResScalarValue {
|
||||||
protected final int mValue;
|
protected final int mValue;
|
||||||
private int type;
|
private int mType;
|
||||||
|
|
||||||
public ResIntValue(int value, String rawValue, int type) {
|
public ResIntValue(int value, String rawValue, int type) {
|
||||||
this(value, rawValue, "integer");
|
this(value, rawValue, "integer");
|
||||||
this.type = type;
|
mType = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResIntValue(int value, String rawValue, String type) {
|
public ResIntValue(int value, String rawValue, String type) {
|
||||||
super(type, value, rawValue);
|
super(type, value, rawValue);
|
||||||
this.mValue = value;
|
mValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getValue() {
|
public int getValue() {
|
||||||
@ -39,6 +39,6 @@ public class ResIntValue extends ResScalarValue {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String encodeAsResXml() throws AndrolibException {
|
protected String encodeAsResXml() throws AndrolibException {
|
||||||
return TypedValue.coerceToString(type, mValue);
|
return TypedValue.coerceToString(mType, mValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,14 @@ import org.xmlpull.v1.XmlSerializer;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class ResPluralsValue extends ResBagValue implements ResValuesXmlSerializable {
|
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) {
|
ResPluralsValue(ResReferenceValue parent, Duo<Integer, ResScalarValue>[] items) {
|
||||||
super(parent);
|
super(parent);
|
||||||
|
|
||||||
mItems = new ResScalarValue[6];
|
mItems = new ResScalarValue[6];
|
||||||
for (Duo<Integer, ResScalarValue> item : items) {
|
for (Duo<Integer, ResScalarValue> item : items) {
|
||||||
mItems[item.m1 - BAG_KEY_PLURALS_START] = item.m2;
|
mItems[item.m1 - BAG_KEY_PLURALS_START] = item.m2;
|
||||||
@ -53,9 +58,4 @@ public class ResPluralsValue extends ResBagValue implements ResValuesXmlSerializ
|
|||||||
}
|
}
|
||||||
serializer.endTag(null, "plurals");
|
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" };
|
|
||||||
}
|
}
|
||||||
|
@ -25,14 +25,13 @@ public class ResReferenceValue extends ResIntValue {
|
|||||||
private final ResPackage mPackage;
|
private final ResPackage mPackage;
|
||||||
private final boolean mTheme;
|
private final boolean mTheme;
|
||||||
|
|
||||||
public ResReferenceValue(ResPackage package_, int value, String rawValue) {
|
public ResReferenceValue(ResPackage pkg, int value, String rawValue) {
|
||||||
this(package_, value, rawValue, false);
|
this(pkg, value, rawValue, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResReferenceValue(ResPackage package_, int value, String rawValue,
|
public ResReferenceValue(ResPackage pkg, int value, String rawValue, boolean theme) {
|
||||||
boolean theme) {
|
|
||||||
super(value, rawValue, "reference");
|
super(value, rawValue, "reference");
|
||||||
mPackage = package_;
|
mPackage = pkg;
|
||||||
mTheme = theme;
|
mTheme = theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ public abstract class ResScalarValue extends ResIntBasedValue implements
|
|||||||
String body = encodeAsResXmlValue();
|
String body = encodeAsResXmlValue();
|
||||||
|
|
||||||
// check for resource reference
|
// check for resource reference
|
||||||
if (!type.equalsIgnoreCase("color")) {
|
if (!type.equals("color")) {
|
||||||
if (body.contains("@")) {
|
if (body.contains("@")) {
|
||||||
if (!res.getFilePath().contains("string")) {
|
if (!res.getFilePath().contains("string")) {
|
||||||
item = true;
|
item = true;
|
||||||
@ -89,7 +89,7 @@ public abstract class ResScalarValue extends ResIntBasedValue implements
|
|||||||
// Android does not allow values (false) for ids.xml anymore
|
// Android does not allow values (false) for ids.xml anymore
|
||||||
// https://issuetracker.google.com/issues/80475496
|
// https://issuetracker.google.com/issues/80475496
|
||||||
// But it decodes as a ResBoolean, which makes no sense. So force it to empty
|
// 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 = "";
|
body = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,8 +115,9 @@ public abstract class ResScalarValue extends ResIntBasedValue implements
|
|||||||
return mType;
|
return mType;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void serializeExtraXmlAttrs(XmlSerializer serializer,
|
protected void serializeExtraXmlAttrs(XmlSerializer serializer, ResResource res)
|
||||||
ResResource res) throws IOException {
|
throws IOException {
|
||||||
|
// stub
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract String encodeAsResXml() throws AndrolibException;
|
protected abstract String encodeAsResXml() throws AndrolibException;
|
||||||
|
@ -25,6 +25,8 @@ import java.io.IOException;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class ResStringValue extends ResScalarValue {
|
public class ResStringValue extends ResScalarValue {
|
||||||
|
private static final Pattern ALL_DIGITS = Pattern.compile("\\d{9,}");
|
||||||
|
|
||||||
public ResStringValue(String value, int rawValue) {
|
public ResStringValue(String value, int rawValue) {
|
||||||
this(value, rawValue, "string");
|
this(value, rawValue, "string");
|
||||||
}
|
}
|
||||||
@ -64,8 +66,6 @@ public class ResStringValue extends ResScalarValue {
|
|||||||
if (val == null || val.isEmpty()) {
|
if (val == null || val.isEmpty()) {
|
||||||
return val;
|
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,}");
|
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,12 @@ import java.util.Set;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ResStyleValue extends ResBagValue implements ResValuesXmlSerializable {
|
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) {
|
ResStyleValue(ResReferenceValue parent, Duo<Integer, ResScalarValue>[] items, ResValueFactory factory) {
|
||||||
super(parent);
|
super(parent);
|
||||||
|
|
||||||
mItems = new Duo[items.length];
|
mItems = new Duo[items.length];
|
||||||
for (int i = 0; i < items.length; i++) {
|
for (int i = 0; i < items.length; i++) {
|
||||||
mItems[i] = new Duo<>(
|
mItems[i] = new Duo<>(
|
||||||
@ -97,8 +100,4 @@ public class ResStyleValue extends ResBagValue implements ResValuesXmlSerializab
|
|||||||
serializer.endTag(null, "style");
|
serializer.endTag(null, "style");
|
||||||
processedNames.clear();
|
processedNames.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Duo<ResReferenceValue, ResScalarValue>[] mItems;
|
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(ResStyleValue.class.getName());
|
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package brut.androlib.res.data.value;
|
|||||||
import brut.androlib.Config;
|
import brut.androlib.Config;
|
||||||
|
|
||||||
public class ResValue {
|
public class ResValue {
|
||||||
|
|
||||||
public boolean shouldRemoveUnknownRes() {
|
public boolean shouldRemoveUnknownRes() {
|
||||||
return Config.getInstance().isDecodeResolveModeRemoving();
|
return Config.getInstance().isDecodeResolveModeRemoving();
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,8 @@ import brut.util.Duo;
|
|||||||
public class ResValueFactory {
|
public class ResValueFactory {
|
||||||
private final ResPackage mPackage;
|
private final ResPackage mPackage;
|
||||||
|
|
||||||
public ResValueFactory(ResPackage package_) {
|
public ResValueFactory(ResPackage pkg) {
|
||||||
this.mPackage = package_;
|
mPackage = pkg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResScalarValue factory(int type, int value, String rawValue) throws AndrolibException {
|
public ResScalarValue factory(int type, int value, String rawValue) throws AndrolibException {
|
||||||
@ -79,7 +79,7 @@ public class ResValueFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ResBagValue bagFactory(int parent, Duo<Integer, ResScalarValue>[] items, ResTypeSpec resTypeSpec)
|
public ResBagValue bagFactory(int parent, Duo<Integer, ResScalarValue>[] items, ResTypeSpec resTypeSpec)
|
||||||
throws AndrolibException {
|
throws AndrolibException {
|
||||||
ResReferenceValue parentVal = newReference(parent, null);
|
ResReferenceValue parentVal = newReference(parent, null);
|
||||||
|
|
||||||
if (items.length == 0) {
|
if (items.length == 0) {
|
||||||
|
@ -22,8 +22,7 @@ import brut.androlib.res.data.*;
|
|||||||
import brut.androlib.res.data.arsc.*;
|
import brut.androlib.res.data.arsc.*;
|
||||||
import brut.androlib.res.data.value.*;
|
import brut.androlib.res.data.value.*;
|
||||||
import brut.util.Duo;
|
import brut.util.Duo;
|
||||||
import brut.util.ExtCountingDataInput;
|
import brut.util.ExtDataInputStream;
|
||||||
import com.google.common.io.LittleEndianDataInputStream;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@ -31,37 +30,57 @@ import java.util.*;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ARSCDecoder {
|
public class ARSCDecoder {
|
||||||
public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken)
|
private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName());
|
||||||
throws AndrolibException {
|
|
||||||
return decode(arscStream, findFlagsOffsets, keepBroken, new ResTable());
|
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,
|
public ARSCData decode() throws AndrolibException {
|
||||||
ResTable resTable)
|
|
||||||
throws AndrolibException {
|
|
||||||
try {
|
try {
|
||||||
ARSCDecoder decoder = new ARSCDecoder(arscStream, resTable, findFlagsOffsets, keepBroken);
|
ResPackage[] pkgs = readResourceTable();
|
||||||
ResPackage[] pkgs = decoder.readResourceTable();
|
FlagsOffset[] flagsOffsets = mFlagsOffsets != null ? mFlagsOffsets.toArray(new FlagsOffset[0]) : null;
|
||||||
return new ARSCData(pkgs, decoder.mFlagsOffsets == null
|
return new ARSCData(pkgs, flagsOffsets);
|
||||||
? null
|
|
||||||
: decoder.mFlagsOffsets.toArray(new FlagsOffset[0]));
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new AndrolibException("Could not decode arsc file", 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 {
|
private ResPackage[] readResourceTable() throws IOException, AndrolibException {
|
||||||
Set<ResPackage> pkgs = new LinkedHashSet<>();
|
Set<ResPackage> pkgs = new LinkedHashSet<>();
|
||||||
ResTypeSpec typeSpec;
|
ResTypeSpec typeSpec;
|
||||||
@ -170,7 +189,7 @@ public class ARSCDecoder {
|
|||||||
|
|
||||||
// TypeIdOffset was added platform_frameworks_base/@f90f2f8dc36e7243b85e0b6a7fd5a590893c827e
|
// TypeIdOffset was added platform_frameworks_base/@f90f2f8dc36e7243b85e0b6a7fd5a590893c827e
|
||||||
// which is only in split/new applications.
|
// 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) {
|
if (mHeader.headerSize == splitHeaderSize) {
|
||||||
mTypeIdOffset = mIn.readInt();
|
mTypeIdOffset = mIn.readInt();
|
||||||
}
|
}
|
||||||
@ -246,7 +265,7 @@ public class ARSCDecoder {
|
|||||||
int entryCount = mIn.readInt();
|
int entryCount = mIn.readInt();
|
||||||
|
|
||||||
if (mFlagsOffsets != null) {
|
if (mFlagsOffsets != null) {
|
||||||
mFlagsOffsets.add(new FlagsOffset(mIn.position(), entryCount));
|
mFlagsOffsets.add(new FlagsOffset((int) mIn.position(), entryCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
mHeader.checkForUnreadHeader(mIn);
|
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;
|
int noEntry = isOffset16 ? NO_ENTRY_OFFSET16 : NO_ENTRY;
|
||||||
|
|
||||||
// #3428 - In some applications the res entries are padded for alignment.
|
// #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) {
|
if (mIn.position() < entriesStartAligned) {
|
||||||
long bytesSkipped = mIn.skip(entriesStartAligned - mIn.position());
|
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()) {
|
for (int i : entryOffsetMap.keySet()) {
|
||||||
@ -355,21 +374,23 @@ public class ARSCDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private EntryData readEntryData() throws IOException, AndrolibException {
|
private EntryData readEntryData() throws IOException, AndrolibException {
|
||||||
short size = mIn.readShort();
|
int size = mIn.readUnsignedShort();
|
||||||
short flags = mIn.readShort();
|
short flags = mIn.readShort();
|
||||||
|
|
||||||
boolean isComplex = (flags & ENTRY_FLAG_COMPLEX) != 0;
|
boolean isComplex = (flags & ENTRY_FLAG_COMPLEX) != 0;
|
||||||
boolean isCompact = (flags & ENTRY_FLAG_COMPACT) != 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();
|
int specNamesId = mIn.readInt();
|
||||||
if (specNamesId == NO_ENTRY && !isCompact) {
|
if (specNamesId == NO_ENTRY && !isCompact) {
|
||||||
return null;
|
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.
|
// #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.
|
// We assume a size of 8 bytes for compact entries and the specNamesId is the data itself encoded.
|
||||||
ResValue value;
|
ResValue value;
|
||||||
@ -377,7 +398,7 @@ public class ARSCDecoder {
|
|||||||
byte type = (byte) ((flags >> 8) & 0xFF);
|
byte type = (byte) ((flags >> 8) & 0xFF);
|
||||||
value = readCompactValue(type, specNamesId);
|
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;
|
specNamesId = size;
|
||||||
} else if (isComplex) {
|
} else if (isComplex) {
|
||||||
value = readComplexEntry();
|
value = readComplexEntry();
|
||||||
@ -392,15 +413,15 @@ public class ARSCDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EntryData entryData = new EntryData();
|
EntryData entryData = new EntryData();
|
||||||
entryData.mFlags = flags;
|
entryData.flags = flags;
|
||||||
entryData.mSpecNamesId = specNamesId;
|
entryData.specNamesId = specNamesId;
|
||||||
entryData.mValue = value;
|
entryData.value = value;
|
||||||
return entryData;
|
return entryData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readEntry(EntryData entryData) throws AndrolibException {
|
private void readEntry(EntryData entryData) throws AndrolibException {
|
||||||
int specNamesId = entryData.mSpecNamesId;
|
int specNamesId = entryData.specNamesId;
|
||||||
ResValue value = entryData.mValue;
|
ResValue value = entryData.value;
|
||||||
|
|
||||||
if (mTypeSpec.isString() && value instanceof ResFileValue) {
|
if (mTypeSpec.isString() && value instanceof ResFileValue) {
|
||||||
value = new ResStringValue(value.toString(), ((ResFileValue) value).getRawIntValue());
|
value = new ResStringValue(value.toString(), ((ResFileValue) value).getRawIntValue());
|
||||||
@ -469,18 +490,18 @@ public class ARSCDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ResIntBasedValue readValue() throws IOException, AndrolibException {
|
private ResIntBasedValue readValue() throws IOException, AndrolibException {
|
||||||
int size = mIn.readShort();
|
short size = mIn.readShort();
|
||||||
if (size < 8) {
|
if (size < 8) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mIn.skipCheckByte((byte) 0); // zero
|
mIn.skipCheckByte((byte) 0); // zero
|
||||||
byte type = mIn.readByte();
|
byte type = mIn.readByte();
|
||||||
int data = mIn.readInt();
|
int data = mIn.readInt();
|
||||||
|
|
||||||
return type == TypedValue.TYPE_STRING
|
return type == TypedValue.TYPE_STRING
|
||||||
? mPkg.getValueFactory().factory(mTableStrings.getHTML(data), data)
|
? mPkg.getValueFactory().factory(mTableStrings.getHTML(data), data)
|
||||||
: mPkg.getValueFactory().factory(type, data, null);
|
: mPkg.getValueFactory().factory(type, data, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResConfigFlags readConfigFlags() throws IOException, AndrolibException {
|
private ResConfigFlags readConfigFlags() throws IOException, AndrolibException {
|
||||||
@ -499,8 +520,8 @@ public class ARSCDecoder {
|
|||||||
char[] language = new char[0];
|
char[] language = new char[0];
|
||||||
char[] country = new char[0];
|
char[] country = new char[0];
|
||||||
if (size >= 12) {
|
if (size >= 12) {
|
||||||
language = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), 'a');
|
language = unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), 'a');
|
||||||
country = this.unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), '0');
|
country = unpackLanguageOrRegion(mIn.readByte(), mIn.readByte(), '0');
|
||||||
read = 12;
|
read = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -682,36 +703,4 @@ public class ARSCDecoder {
|
|||||||
expectedType, mHeader.type));
|
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());
|
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,7 @@ import brut.androlib.res.data.axml.NamespaceStack;
|
|||||||
import brut.androlib.res.data.value.ResAttr;
|
import brut.androlib.res.data.value.ResAttr;
|
||||||
import brut.androlib.res.data.value.ResScalarValue;
|
import brut.androlib.res.data.value.ResScalarValue;
|
||||||
import brut.androlib.res.xml.ResXmlEncoders;
|
import brut.androlib.res.xml.ResXmlEncoders;
|
||||||
import brut.util.ExtCountingDataInput;
|
import brut.util.ExtDataInputStream;
|
||||||
import com.google.common.io.LittleEndianDataInputStream;
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
@ -47,9 +46,45 @@ import java.util.logging.Logger;
|
|||||||
* this state methods return invalid values or throw exceptions.
|
* this state methods return invalid values or throw exceptions.
|
||||||
*/
|
*/
|
||||||
public class AXmlResourceParser implements XmlResourceParser {
|
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) {
|
public AXmlResourceParser(ResTable resTable) {
|
||||||
mResTable = resTable;
|
mResTable = resTable;
|
||||||
|
mNamespaces = new NamespaceStack();
|
||||||
resetEventInfo();
|
resetEventInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,16 +99,16 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
public void open(InputStream stream) {
|
public void open(InputStream stream) {
|
||||||
close();
|
close();
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
mIn = new ExtCountingDataInput(new LittleEndianDataInputStream(stream));
|
mIn = ExtDataInputStream.littleEndian(stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (!isOperational) {
|
if (!mIsOperational) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isOperational = false;
|
mIsOperational = false;
|
||||||
mIn = null;
|
mIn = null;
|
||||||
mStringBlock = null;
|
mStringBlock = null;
|
||||||
mResourceIds = null;
|
mResourceIds = null;
|
||||||
@ -89,9 +124,9 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
try {
|
try {
|
||||||
doNext();
|
doNext();
|
||||||
return mEvent;
|
return mEvent;
|
||||||
} catch (IOException e) {
|
} catch (IOException ex) {
|
||||||
close();
|
close();
|
||||||
throw e;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,17 +349,16 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
|
|
||||||
private String getNonDefaultNamespaceUri(int offset) {
|
private String getNonDefaultNamespaceUri(int offset) {
|
||||||
String prefix = mStringBlock.getString(mNamespaces.getPrefix(offset));
|
String prefix = mStringBlock.getString(mNamespaces.getPrefix(offset));
|
||||||
if (prefix != null) {
|
if (prefix == null) {
|
||||||
return mStringBlock.getString(mNamespaces.getUri(offset));
|
// If we are here. There is some clever obfuscation going on. Our reference points to the namespace are gone.
|
||||||
|
// Normally we could take the index * attributeCount to get an offset.
|
||||||
|
// That would point to the URI in the StringBlock table, but that is empty.
|
||||||
|
// We have the namespaces that can't be touched in the opening tag.
|
||||||
|
// Though no known way to correlate them at this time.
|
||||||
|
// So return the res-auto namespace.
|
||||||
|
return ANDROID_RES_NS_AUTO;
|
||||||
}
|
}
|
||||||
|
return mStringBlock.getString(mNamespaces.getUri(offset));
|
||||||
// If we are here. There is some clever obfuscation going on. Our reference points to the namespace are gone.
|
|
||||||
// Normally we could take the index * attributeCount to get an offset.
|
|
||||||
// That would point to the URI in the StringBlock table, but that is empty.
|
|
||||||
// We have the namespaces that can't be touched in the opening tag.
|
|
||||||
// Though no known way to correlate them at this time.
|
|
||||||
// So return the res-auto namespace.
|
|
||||||
return ANDROID_RES_NS_AUTO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -348,10 +382,10 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
|
|
||||||
String resourceMapValue;
|
String resourceMapValue;
|
||||||
String stringBlockValue = mStringBlock.getString(name);
|
String stringBlockValue = mStringBlock.getString(name);
|
||||||
int resourceId = getAttributeNameResource(index);
|
int attrResId = getAttributeNameResource(index);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resourceMapValue = decodeFromResourceId(resourceId);
|
resourceMapValue = decodeFromResourceId(attrResId);
|
||||||
} catch (AndrolibException ignored) {
|
} catch (AndrolibException ignored) {
|
||||||
resourceMapValue = null;
|
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.
|
// 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
|
@Override
|
||||||
@ -404,13 +438,17 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
int valueRaw = mAttributes[offset + ATTRIBUTE_IX_VALUE_STRING];
|
int valueRaw = mAttributes[offset + ATTRIBUTE_IX_VALUE_STRING];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String stringBlockValue = valueRaw == -1 ? null : ResXmlEncoders.escapeXmlChars(mStringBlock.getString(valueRaw));
|
String stringBlockValue = valueRaw != -1
|
||||||
|
? ResXmlEncoders.escapeXmlChars(mStringBlock.getString(valueRaw))
|
||||||
|
: null;
|
||||||
String resourceMapValue = null;
|
String resourceMapValue = null;
|
||||||
|
|
||||||
// Ensure we only track down obfuscated values for reference/attribute type values. Otherwise, we might
|
// Ensure we only track down obfuscated values for reference/attribute type values. Otherwise, we might
|
||||||
// spam lookups against resource table for invalid ids.
|
// spam lookups against resource table for invalid ids.
|
||||||
if (valueType == TypedValue.TYPE_REFERENCE || valueType == TypedValue.TYPE_DYNAMIC_REFERENCE ||
|
if (valueType == TypedValue.TYPE_REFERENCE
|
||||||
valueType == TypedValue.TYPE_ATTRIBUTE || valueType == TypedValue.TYPE_DYNAMIC_ATTRIBUTE) {
|
|| valueType == TypedValue.TYPE_DYNAMIC_REFERENCE
|
||||||
|
|| valueType == TypedValue.TYPE_ATTRIBUTE
|
||||||
|
|| valueType == TypedValue.TYPE_DYNAMIC_ATTRIBUTE) {
|
||||||
resourceMapValue = decodeFromResourceId(valueData);
|
resourceMapValue = decodeFromResourceId(valueData);
|
||||||
}
|
}
|
||||||
String value = getPreferredString(stringBlockValue, resourceMapValue);
|
String value = getPreferredString(stringBlockValue, resourceMapValue);
|
||||||
@ -451,21 +489,20 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
public float getAttributeFloatValue(int index, float defaultValue) {
|
public float getAttributeFloatValue(int index, float defaultValue) {
|
||||||
int offset = getAttributeOffset(index);
|
int offset = getAttributeOffset(index);
|
||||||
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
|
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
|
||||||
if (valueType == TypedValue.TYPE_FLOAT) {
|
if (valueType != TypedValue.TYPE_FLOAT) {
|
||||||
int valueData = mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
|
return defaultValue;
|
||||||
return Float.intBitsToFloat(valueData);
|
|
||||||
}
|
}
|
||||||
return defaultValue;
|
return Float.intBitsToFloat(mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAttributeIntValue(int index, int defaultValue) {
|
public int getAttributeIntValue(int index, int defaultValue) {
|
||||||
int offset = getAttributeOffset(index);
|
int offset = getAttributeOffset(index);
|
||||||
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
|
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
|
||||||
if (valueType >= TypedValue.TYPE_FIRST_INT && valueType <= TypedValue.TYPE_LAST_INT) {
|
if (valueType < TypedValue.TYPE_FIRST_INT || valueType > TypedValue.TYPE_LAST_INT) {
|
||||||
return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
|
return defaultValue;
|
||||||
}
|
}
|
||||||
return defaultValue;
|
return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -477,10 +514,10 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
public int getAttributeResourceValue(int index, int defaultValue) {
|
public int getAttributeResourceValue(int index, int defaultValue) {
|
||||||
int offset = getAttributeOffset(index);
|
int offset = getAttributeOffset(index);
|
||||||
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
|
int valueType = mAttributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
|
||||||
if (valueType == TypedValue.TYPE_REFERENCE) {
|
if (valueType != TypedValue.TYPE_REFERENCE) {
|
||||||
return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
|
return defaultValue;
|
||||||
}
|
}
|
||||||
return defaultValue;
|
return mAttributes[offset + ATTRIBUTE_IX_VALUE_DATA];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -609,12 +646,12 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean getFeature(String feature) {
|
public boolean getFeature(String name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
throw new XmlPullParserException(E_NOT_SUPPORTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,7 +722,7 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
|
|
||||||
mStringBlock = StringBlock.readWithChunk(mIn);
|
mStringBlock = StringBlock.readWithChunk(mIn);
|
||||||
mNamespaces.increaseDepth();
|
mNamespaces.increaseDepth();
|
||||||
isOperational = true;
|
mIsOperational = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mEvent == END_DOCUMENT) {
|
if (mEvent == END_DOCUMENT) {
|
||||||
@ -696,8 +733,8 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
resetEventInfo();
|
resetEventInfo();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (m_decreaseDepth) {
|
if (mDecreaseDepth) {
|
||||||
m_decreaseDepth = false;
|
mDecreaseDepth = false;
|
||||||
mNamespaces.decreaseDepth();
|
mNamespaces.decreaseDepth();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -708,7 +745,7 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// #2070 - Some applications have 2 start namespaces, but only 1 end namespace.
|
// #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()));
|
LOGGER.warning(String.format("AXML hit unexpected end of file at byte: 0x%X", mIn.position()));
|
||||||
mEvent = END_DOCUMENT;
|
mEvent = END_DOCUMENT;
|
||||||
break;
|
break;
|
||||||
@ -807,7 +844,7 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
mNamespaceIndex = mIn.readInt();
|
mNamespaceIndex = mIn.readInt();
|
||||||
mNameIndex = mIn.readInt();
|
mNameIndex = mIn.readInt();
|
||||||
mEvent = END_TAG;
|
mEvent = END_TAG;
|
||||||
m_decreaseDepth = true;
|
mDecreaseDepth = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -826,40 +863,4 @@ public class AXmlResourceParser implements XmlResourceParser {
|
|||||||
mFirstError = error;
|
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";
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,11 +25,6 @@ import java.util.regex.Pattern;
|
|||||||
* AXmlResourceParser specifically for parsing encoded AndroidManifest.xml.
|
* AXmlResourceParser specifically for parsing encoded AndroidManifest.xml.
|
||||||
*/
|
*/
|
||||||
public class AndroidManifestResourceParser extends AXmlResourceParser {
|
public class AndroidManifestResourceParser extends AXmlResourceParser {
|
||||||
|
|
||||||
public AndroidManifestResourceParser(ResTable resTable) {
|
|
||||||
super(resTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pattern for matching numeric string meta-data values. aapt automatically infers the
|
* 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,
|
* 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+");
|
private static final Pattern PATTERN_NUMERIC_STRING = Pattern.compile("\\s?\\d+");
|
||||||
|
|
||||||
|
public AndroidManifestResourceParser(ResTable resTable) {
|
||||||
|
super(resTable);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAttributeValue(int index) {
|
public String getAttributeValue(int index) {
|
||||||
String value = super.getAttributeValue(index);
|
String value = super.getAttributeValue(index);
|
||||||
@ -58,9 +57,9 @@ public class AndroidManifestResourceParser extends AXmlResourceParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isNumericStringMetadataAttributeValue(int index, String value) {
|
private boolean isNumericStringMetadataAttributeValue(int index, String value) {
|
||||||
return "meta-data".equalsIgnoreCase(super.getName())
|
return "meta-data".equals(super.getName())
|
||||||
&& "value".equalsIgnoreCase(super.getAttributeName(index))
|
&& "value".equals(super.getAttributeName(index))
|
||||||
&& super.getAttributeValueType(index) == TypedValue.TYPE_STRING
|
&& super.getAttributeValueType(index) == TypedValue.TYPE_STRING
|
||||||
&& PATTERN_NUMERIC_STRING.matcher(value).matches();
|
&& PATTERN_NUMERIC_STRING.matcher(value).matches();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import brut.androlib.exceptions.CantFind9PatchChunkException;
|
|||||||
import brut.androlib.res.data.ninepatch.NinePatchData;
|
import brut.androlib.res.data.ninepatch.NinePatchData;
|
||||||
import brut.androlib.res.data.ninepatch.OpticalInset;
|
import brut.androlib.res.data.ninepatch.OpticalInset;
|
||||||
import brut.util.ExtDataInput;
|
import brut.util.ExtDataInput;
|
||||||
|
import brut.util.ExtDataInputStream;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
@ -87,14 +88,14 @@ public class Res9patchAndroidStreamDecoder implements ResStreamDecoder {
|
|||||||
|
|
||||||
private NinePatchData getNinePatch(byte[] data) throws AndrolibException,
|
private NinePatchData getNinePatch(byte[] data) throws AndrolibException,
|
||||||
IOException {
|
IOException {
|
||||||
ExtDataInput di = new ExtDataInput(new ByteArrayInputStream(data));
|
ExtDataInput di = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data));
|
||||||
find9patchChunk(di, NP_CHUNK_TYPE);
|
find9patchChunk(di, NP_CHUNK_TYPE);
|
||||||
return NinePatchData.decode(di);
|
return NinePatchData.decode(di);
|
||||||
}
|
}
|
||||||
|
|
||||||
private OpticalInset getOpticalInset(byte[] data) throws AndrolibException,
|
private OpticalInset getOpticalInset(byte[] data) throws AndrolibException,
|
||||||
IOException {
|
IOException {
|
||||||
ExtDataInput di = new ExtDataInput(new ByteArrayInputStream(data));
|
ExtDataInput di = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data));
|
||||||
find9patchChunk(di, OI_CHUNK_TYPE);
|
find9patchChunk(di, OI_CHUNK_TYPE);
|
||||||
return OpticalInset.decode(di);
|
return OpticalInset.decode(di);
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import brut.androlib.exceptions.CantFind9PatchChunkException;
|
|||||||
import brut.androlib.res.data.ninepatch.NinePatchData;
|
import brut.androlib.res.data.ninepatch.NinePatchData;
|
||||||
import brut.androlib.res.data.ninepatch.OpticalInset;
|
import brut.androlib.res.data.ninepatch.OpticalInset;
|
||||||
import brut.util.ExtDataInput;
|
import brut.util.ExtDataInput;
|
||||||
|
import brut.util.ExtDataInputStream;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
@ -30,6 +31,11 @@ import java.awt.image.WritableRaster;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
|
||||||
public class Res9patchStreamDecoder implements ResStreamDecoder {
|
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
|
@Override
|
||||||
public void decode(InputStream in, OutputStream out) throws AndrolibException {
|
public void decode(InputStream in, OutputStream out) throws AndrolibException {
|
||||||
try {
|
try {
|
||||||
@ -122,32 +128,32 @@ public class Res9patchStreamDecoder implements ResStreamDecoder {
|
|||||||
|
|
||||||
private NinePatchData getNinePatch(byte[] data) throws AndrolibException,
|
private NinePatchData getNinePatch(byte[] data) throws AndrolibException,
|
||||||
IOException {
|
IOException {
|
||||||
ExtDataInput di = new ExtDataInput(new ByteArrayInputStream(data));
|
ExtDataInput in = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data));
|
||||||
find9patchChunk(di, NP_CHUNK_TYPE);
|
find9patchChunk(in, NP_CHUNK_TYPE);
|
||||||
return NinePatchData.decode(di);
|
return NinePatchData.decode(in);
|
||||||
}
|
}
|
||||||
|
|
||||||
private OpticalInset getOpticalInset(byte[] data) throws AndrolibException,
|
private OpticalInset getOpticalInset(byte[] data) throws AndrolibException,
|
||||||
IOException {
|
IOException {
|
||||||
ExtDataInput di = new ExtDataInput(new ByteArrayInputStream(data));
|
ExtDataInput in = ExtDataInputStream.bigEndian(new ByteArrayInputStream(data));
|
||||||
find9patchChunk(di, OI_CHUNK_TYPE);
|
find9patchChunk(in, OI_CHUNK_TYPE);
|
||||||
return OpticalInset.decode(di);
|
return OpticalInset.decode(in);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void find9patchChunk(DataInput di, int magic) throws AndrolibException,
|
private void find9patchChunk(DataInput in, int magic) throws AndrolibException,
|
||||||
IOException {
|
IOException {
|
||||||
di.skipBytes(8);
|
in.skipBytes(8);
|
||||||
while (true) {
|
while (true) {
|
||||||
int size;
|
int size;
|
||||||
try {
|
try {
|
||||||
size = di.readInt();
|
size = in.readInt();
|
||||||
} catch (IOException ex) {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
di.skipBytes(size + 4);
|
in.skipBytes(size + 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,9 +168,4 @@ public class Res9patchStreamDecoder implements ResStreamDecoder {
|
|||||||
im.setRGB(x, y, NP_COLOR);
|
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;
|
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,9 @@ import brut.androlib.exceptions.RawXmlEncounteredException;
|
|||||||
import brut.androlib.res.data.ResResource;
|
import brut.androlib.res.data.ResResource;
|
||||||
import brut.androlib.res.data.value.ResBoolValue;
|
import brut.androlib.res.data.value.ResBoolValue;
|
||||||
import brut.androlib.res.data.value.ResFileValue;
|
import brut.androlib.res.data.value.ResFileValue;
|
||||||
import brut.directory.DirUtil;
|
|
||||||
import brut.directory.Directory;
|
import brut.directory.Directory;
|
||||||
import brut.directory.DirectoryException;
|
import brut.directory.DirectoryException;
|
||||||
|
import brut.directory.DirUtils;
|
||||||
import brut.util.BrutIO;
|
import brut.util.BrutIO;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
@ -33,10 +33,21 @@ import java.util.logging.Level;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class ResFileDecoder {
|
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;
|
private final ResStreamDecoderContainer mDecoders;
|
||||||
|
|
||||||
public ResFileDecoder(ResStreamDecoderContainer decoders) {
|
public ResFileDecoder(ResStreamDecoderContainer decoders) {
|
||||||
this.mDecoders = decoders;
|
mDecoders = decoders;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decode(ResResource res, Directory inDir, Directory outDir, Map<String, String> resFileMapping)
|
public void decode(ResResource res, Directory inDir, Directory outDir, Map<String, String> resFileMapping)
|
||||||
@ -109,7 +120,7 @@ public class ResFileDecoder {
|
|||||||
return;
|
return;
|
||||||
} catch (CantFind9PatchChunkException ex) {
|
} catch (CantFind9PatchChunkException ex) {
|
||||||
LOGGER.log(Level.WARNING, String.format(
|
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);
|
), ex);
|
||||||
outDir.removeFile(outFileName);
|
outDir.removeFile(outFileName);
|
||||||
outFileName = outResName + ext;
|
outFileName = outResName + ext;
|
||||||
@ -156,24 +167,12 @@ public class ResFileDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void copyRaw(Directory inDir, Directory outDir, String inFilename,
|
public void copyRaw(Directory inDir, Directory outDir, String inFileName,
|
||||||
String outFilename) throws AndrolibException {
|
String outFileName) throws AndrolibException {
|
||||||
try {
|
try {
|
||||||
DirUtil.copyToDir(inDir, outDir, inFilename, outFilename);
|
DirUtils.copyToDir(inDir, outDir, inFileName, outFileName);
|
||||||
} catch (DirectoryException ex) {
|
} catch (DirectoryException ex) {
|
||||||
throw new AndrolibException(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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,9 @@ import org.apache.commons.io.IOUtils;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
|
||||||
public class ResRawStreamDecoder implements ResStreamDecoder {
|
public class ResRawStreamDecoder implements ResStreamDecoder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void decode(InputStream in, OutputStream out)
|
public void decode(InputStream in, OutputStream out) throws AndrolibException {
|
||||||
throws AndrolibException {
|
|
||||||
try {
|
try {
|
||||||
IOUtils.copy(in, out);
|
IOUtils.copy(in, out);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
|
@ -20,6 +20,5 @@ import brut.androlib.exceptions.AndrolibException;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
|
||||||
public interface ResStreamDecoder {
|
public interface ResStreamDecoder {
|
||||||
void decode(InputStream in, OutputStream out)
|
void decode(InputStream in, OutputStream out) throws AndrolibException;
|
||||||
throws AndrolibException;
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,11 @@ import java.io.*;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class ResStreamDecoderContainer {
|
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)
|
public void decode(InputStream in, OutputStream out, String decoderName)
|
||||||
throws AndrolibException {
|
throws AndrolibException {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,20 +18,34 @@ package brut.androlib.res.decoder;
|
|||||||
|
|
||||||
import brut.androlib.res.data.arsc.ARSCHeader;
|
import brut.androlib.res.data.arsc.ARSCHeader;
|
||||||
import brut.androlib.res.xml.ResXmlEncoders;
|
import brut.androlib.res.xml.ResXmlEncoders;
|
||||||
import brut.util.ExtCountingDataInput;
|
import brut.util.ExtDataInput;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.*;
|
import java.nio.charset.*;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class StringBlock {
|
public class StringBlock {
|
||||||
public static StringBlock readWithChunk(ExtCountingDataInput reader) throws IOException {
|
private static final Logger LOGGER = Logger.getLogger(StringBlock.class.getName());
|
||||||
int startPosition = reader.position();
|
|
||||||
|
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);
|
reader.skipCheckShort(ARSCHeader.RES_STRING_POOL_TYPE);
|
||||||
int headerSize = reader.readShort();
|
int headerSize = reader.readShort();
|
||||||
int chunkSize = reader.readInt();
|
int chunkSize = reader.readInt();
|
||||||
@ -39,9 +53,8 @@ public class StringBlock {
|
|||||||
return readWithoutChunk(reader, startPosition, headerSize, chunkSize);
|
return readWithoutChunk(reader, startPosition, headerSize, chunkSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StringBlock readWithoutChunk(ExtCountingDataInput reader, int startPosition, int headerSize,
|
public static StringBlock readWithoutChunk(ExtDataInput reader, long startPosition,
|
||||||
int chunkSize) throws IOException
|
int headerSize, int chunkSize) throws IOException {
|
||||||
{
|
|
||||||
// ResStringPool_header
|
// ResStringPool_header
|
||||||
int stringCount = reader.readInt();
|
int stringCount = reader.readInt();
|
||||||
int styleCount = reader.readInt();
|
int styleCount = reader.readInt();
|
||||||
@ -91,6 +104,15 @@ public class StringBlock {
|
|||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private StringBlock() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
StringBlock(byte[] strings, boolean isUTF8) {
|
||||||
|
mStrings = strings;
|
||||||
|
mIsUtf8 = isUTF8;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns raw string (without any styling information) at specified index.
|
* Returns raw string (without any styling information) at specified index.
|
||||||
* @param index int
|
* @param index int
|
||||||
@ -112,6 +134,7 @@ public class StringBlock {
|
|||||||
offset += val[0];
|
offset += val[0];
|
||||||
}
|
}
|
||||||
length = val[1];
|
length = val[1];
|
||||||
|
|
||||||
return decodeString(offset, length);
|
return decodeString(offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +162,7 @@ public class StringBlock {
|
|||||||
for (int i = 0; i < style.length; i += 3) {
|
for (int i = 0; i < style.length; i += 3) {
|
||||||
spans.add(new StyledString.Span(getString(style[i]), style[i + 1], style[i + 2]));
|
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);
|
StyledString styledString = new StyledString(text, spans);
|
||||||
return styledString.toString();
|
return styledString.toString();
|
||||||
@ -155,14 +178,14 @@ public class StringBlock {
|
|||||||
if (string == null) {
|
if (string == null) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
for (int i = 0; i != mStringOffsets.length; ++i) {
|
for (int i = 0; i != mStringOffsets.length; i++) {
|
||||||
int offset = mStringOffsets[i];
|
int offset = mStringOffsets[i];
|
||||||
int length = getShort(mStrings, offset);
|
int length = getShort(mStrings, offset);
|
||||||
if (length != string.length()) {
|
if (length != string.length()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int j = 0;
|
int j = 0;
|
||||||
for (; j != length; ++j) {
|
for (; j != length; j++) {
|
||||||
offset += 2;
|
offset += 2;
|
||||||
if (string.charAt(j) != getShort(mStrings, offset)) {
|
if (string.charAt(j) != getShort(mStrings, offset)) {
|
||||||
break;
|
break;
|
||||||
@ -175,15 +198,6 @@ public class StringBlock {
|
|||||||
return -1;
|
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:
|
* 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
|
* * 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 count = 0;
|
||||||
int[] style;
|
int[] style;
|
||||||
|
|
||||||
for (int i = offset; i < mStyles.length; ++i) {
|
for (int i = offset; i < mStyles.length; i++) {
|
||||||
if (mStyles[i] == -1) {
|
if (mStyles[i] == -1) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -240,7 +254,7 @@ public class StringBlock {
|
|||||||
// in some places, Android uses 3-byte UTF-8 sequences instead of 4-bytes.
|
// 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.
|
// If decoding failed, we try to use CESU-8 decoder, which is closer to what Android actually uses.
|
||||||
return CESU8_DECODER.decode(wrappedBufferRetry).toString();
|
return CESU8_DECODER.decode(wrappedBufferRetry).toString();
|
||||||
} catch (CharacterCodingException e) {
|
} catch (CharacterCodingException ex) {
|
||||||
LOGGER.warning("Failed to decode a string with CESU-8 decoder.");
|
LOGGER.warning("Failed to decode a string with CESU-8 decoder.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -263,39 +277,25 @@ public class StringBlock {
|
|||||||
val = array[offset];
|
val = array[offset];
|
||||||
offset += 1;
|
offset += 1;
|
||||||
if ((val & 0x80) != 0) {
|
if ((val & 0x80) != 0) {
|
||||||
int low = (array[offset] & 0xFF);
|
int low = array[offset] & 0xFF;
|
||||||
length = ((val & 0x7F) << 8) + low;
|
length = ((val & 0x7F) << 8) + low;
|
||||||
offset += 1;
|
offset += 1;
|
||||||
} else {
|
} else {
|
||||||
length = val;
|
length = val;
|
||||||
}
|
}
|
||||||
return new int[] { offset, length};
|
return new int[] { offset, length };
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int[] getUtf16(byte[] array, int offset) {
|
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) {
|
if ((val & 0x8000) != 0) {
|
||||||
int high = (array[offset + 3] & 0xFF) << 8;
|
int high = (array[offset + 3] & 0xFF) << 8;
|
||||||
int low = (array[offset + 2] & 0xFF);
|
int low = (array[offset + 2] & 0xFF);
|
||||||
int len_value = ((val & 0x7FFF) << 16) + (high + low);
|
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;
|
|
||||||
}
|
}
|
||||||
|
@ -27,12 +27,14 @@ import java.util.Map;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class StyledString {
|
public class StyledString {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(StyledString.class.getName());
|
||||||
|
|
||||||
private final String mText;
|
private final String mText;
|
||||||
private final List<Span> mSpans;
|
private final List<Span> mSpans;
|
||||||
|
|
||||||
public StyledString(String text, List<Span> spans) {
|
public StyledString(String text, List<Span> spans) {
|
||||||
this.mText = text;
|
mText = text;
|
||||||
this.mSpans = spans;
|
mSpans = spans;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getText() {
|
String getText() {
|
||||||
@ -52,63 +54,63 @@ public class StyledString {
|
|||||||
private static final MapSplitter ATTRIBUTES_SPLITTER =
|
private static final MapSplitter ATTRIBUTES_SPLITTER =
|
||||||
Splitter.on(';').omitEmptyStrings().withKeyValueSeparator(Splitter.on('=').limit(2));
|
Splitter.on(';').omitEmptyStrings().withKeyValueSeparator(Splitter.on('=').limit(2));
|
||||||
|
|
||||||
private final String tag;
|
private final String mTag;
|
||||||
private final int firstChar;
|
private final int mFirstChar;
|
||||||
private final int lastChar;
|
private final int mLastChar;
|
||||||
|
|
||||||
public Span(String tag, int firstChar, int lastChar) {
|
public Span(String tag, int firstChar, int lastChar) {
|
||||||
this.tag = tag;
|
mTag = tag;
|
||||||
this.firstChar = firstChar;
|
mFirstChar = firstChar;
|
||||||
this.lastChar = lastChar;
|
mLastChar = lastChar;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTag() {
|
public String getTag() {
|
||||||
return tag;
|
return mTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getFirstChar() {
|
public int getFirstChar() {
|
||||||
return firstChar;
|
return mFirstChar;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLastChar() {
|
public int getLastChar() {
|
||||||
return lastChar;
|
return mLastChar;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
int separatorIdx = tag.indexOf(';');
|
int separatorIdx = mTag.indexOf(';');
|
||||||
return separatorIdx == -1 ? tag : tag.substring(0, separatorIdx);
|
return separatorIdx == -1 ? mTag : mTag.substring(0, separatorIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, String> getAttributes() {
|
public Map<String, String> getAttributes() {
|
||||||
int separatorIdx = tag.indexOf(';');
|
int separatorIdx = mTag.indexOf(';');
|
||||||
return separatorIdx == -1 ? null : ATTRIBUTES_SPLITTER.split(
|
return separatorIdx != -1 ? ATTRIBUTES_SPLITTER.split(
|
||||||
tag.substring(separatorIdx + 1, tag.endsWith(";") ? tag.length() - 1 : tag.length())
|
mTag.substring(separatorIdx + 1, mTag.endsWith(";") ? mTag.length() - 1 : mTag.length())
|
||||||
);
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(Span o) {
|
public int compareTo(Span o) {
|
||||||
int res = Integer.compare(firstChar, o.firstChar);
|
int res = Integer.compare(mFirstChar, o.mFirstChar);
|
||||||
if (res != 0) {
|
if (res != 0) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
res = Integer.compare(lastChar, o.lastChar);
|
res = Integer.compare(mLastChar, o.mLastChar);
|
||||||
if (res != 0) {
|
if (res != 0) {
|
||||||
return -res;
|
return -res;
|
||||||
}
|
}
|
||||||
return -tag.compareTo(o.tag);
|
return -mTag.compareTo(o.mTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Decoder {
|
private static class Decoder {
|
||||||
private String text;
|
private String mText;
|
||||||
private StringBuilder xmlValue;
|
private StringBuilder mXmlValue;
|
||||||
private int lastOffset;
|
private int mLastOffset;
|
||||||
|
|
||||||
String decode(StyledString styledString) {
|
public String decode(StyledString styledString) {
|
||||||
text = styledString.getText();
|
mText = styledString.getText();
|
||||||
xmlValue = new StringBuilder(text.length() * 2);
|
mXmlValue = new StringBuilder(mText.length() * 2);
|
||||||
lastOffset = 0;
|
mLastOffset = 0;
|
||||||
|
|
||||||
// recurse top-level tags
|
// recurse top-level tags
|
||||||
PeekingIterator<Span> it = Iterators.peekingIterator(styledString.getSpans().iterator());
|
PeekingIterator<Span> it = Iterators.peekingIterator(styledString.getSpans().iterator());
|
||||||
@ -116,11 +118,11 @@ public class StyledString {
|
|||||||
decodeIterate(it);
|
decodeIterate(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the remaining encoded raw text
|
// write the remaining encoded raw mText
|
||||||
if (lastOffset < text.length()) {
|
if (mLastOffset < mText.length()) {
|
||||||
xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset)));
|
mXmlValue.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset)));
|
||||||
}
|
}
|
||||||
return xmlValue.toString();
|
return mXmlValue.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void decodeIterate(PeekingIterator<Span> it) {
|
private void decodeIterate(PeekingIterator<Span> it) {
|
||||||
@ -130,45 +132,43 @@ public class StyledString {
|
|||||||
int spanStart = span.getFirstChar();
|
int spanStart = span.getFirstChar();
|
||||||
int spanEnd = span.getLastChar() + 1;
|
int spanEnd = span.getLastChar() + 1;
|
||||||
|
|
||||||
// write encoded raw text preceding the opening tag
|
// write encoded raw mText preceding the opening tag
|
||||||
if (spanStart > lastOffset) {
|
if (spanStart > mLastOffset) {
|
||||||
xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset, spanStart)));
|
mXmlValue.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset, spanStart)));
|
||||||
}
|
}
|
||||||
lastOffset = spanStart;
|
mLastOffset = spanStart;
|
||||||
|
|
||||||
// write opening tag
|
// write opening tag
|
||||||
xmlValue.append('<').append(name);
|
mXmlValue.append('<').append(name);
|
||||||
if (attributes != null) {
|
if (attributes != null) {
|
||||||
for (Map.Entry<String, String> attrEntry : attributes.entrySet()) {
|
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('"');
|
.append(ResXmlEncoders.escapeXmlChars(attrEntry.getValue())).append('"');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if an opening tag is followed by a matching closing tag, write as an empty-element tag
|
// if an opening tag is followed by a matching closing tag, write as an empty-element tag
|
||||||
if (spanStart == spanEnd) {
|
if (spanStart == spanEnd) {
|
||||||
xmlValue.append("/>");
|
mXmlValue.append("/>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
xmlValue.append('>');
|
mXmlValue.append('>');
|
||||||
|
|
||||||
// recurse nested tags
|
// recurse nested tags
|
||||||
while (it.hasNext() && it.peek().getFirstChar() < spanEnd) {
|
while (it.hasNext() && it.peek().getFirstChar() < spanEnd) {
|
||||||
decodeIterate(it);
|
decodeIterate(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
// write encoded raw text preceding the closing tag
|
// write encoded raw mText preceding the closing tag
|
||||||
if (spanEnd > lastOffset && text.length() >= spanEnd) {
|
if (spanEnd > mLastOffset && mText.length() >= spanEnd) {
|
||||||
xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset, spanEnd)));
|
mXmlValue.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset, spanEnd)));
|
||||||
} else if (text.length() >= lastOffset && text.length() < spanEnd) {
|
} else if (mText.length() >= mLastOffset && mText.length() < spanEnd) {
|
||||||
LOGGER.warning("Span (" + name + ") exceeds text length " + text.length());
|
LOGGER.warning("Span (" + name + ") exceeds mText length " + mText.length());
|
||||||
xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset)));
|
mXmlValue.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset)));
|
||||||
}
|
}
|
||||||
lastOffset = spanEnd;
|
mLastOffset = spanEnd;
|
||||||
|
|
||||||
// write closing tag
|
// write closing tag
|
||||||
xmlValue.append("</").append(name).append('>');
|
mXmlValue.append("</").append(name).append('>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(StyledString.class.getName());
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
|
|
||||||
}
|
|
@ -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";
|
|
||||||
}
|
|
@ -23,6 +23,6 @@ import org.xmlpull.v1.XmlSerializer;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public interface ResValuesXmlSerializable {
|
public interface ResValuesXmlSerializable {
|
||||||
void serializeToResValuesXml(XmlSerializer serializer,
|
void serializeToResValuesXml(XmlSerializer serializer, ResResource res)
|
||||||
ResResource res) throws IOException, AndrolibException;
|
throws IOException, AndrolibException;
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,10 @@ import java.util.List;
|
|||||||
|
|
||||||
public final class ResXmlEncoders {
|
public final class ResXmlEncoders {
|
||||||
|
|
||||||
|
private ResXmlEncoders() {
|
||||||
|
// Private constructor for utility class
|
||||||
|
}
|
||||||
|
|
||||||
public static String escapeXmlChars(String str) {
|
public static String escapeXmlChars(String str) {
|
||||||
return StringUtils.replace(StringUtils.replace(str, "&", "&"), "<", "<");
|
return StringUtils.replace(StringUtils.replace(str, "&", "&"), "<", "<");
|
||||||
}
|
}
|
||||||
|
@ -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());
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,6 @@ import brut.androlib.exceptions.AndrolibException;
|
|||||||
import brut.androlib.mod.SmaliMod;
|
import brut.androlib.mod.SmaliMod;
|
||||||
import brut.directory.DirectoryException;
|
import brut.directory.DirectoryException;
|
||||||
import brut.directory.ExtFile;
|
import brut.directory.ExtFile;
|
||||||
import org.antlr.runtime.RecognitionException;
|
|
||||||
import com.android.tools.smali.dexlib2.Opcodes;
|
import com.android.tools.smali.dexlib2.Opcodes;
|
||||||
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder;
|
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder;
|
||||||
import com.android.tools.smali.dexlib2.writer.io.FileDataStore;
|
import com.android.tools.smali.dexlib2.writer.io.FileDataStore;
|
||||||
@ -30,57 +29,54 @@ import java.nio.file.Files;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class SmaliBuilder {
|
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 {
|
private final ExtFile mSmaliDir;
|
||||||
new SmaliBuilder(smaliDir, dexFile, apiLevel).build();
|
private final int mApiLevel;
|
||||||
}
|
|
||||||
|
|
||||||
private SmaliBuilder(ExtFile smaliDir, File dexFile, int apiLevel) {
|
public SmaliBuilder(File smaliDir, int apiLevel) {
|
||||||
mSmaliDir = smaliDir;
|
mSmaliDir = new ExtFile(smaliDir);
|
||||||
mDexFile = dexFile;
|
|
||||||
mApiLevel = apiLevel;
|
mApiLevel = apiLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void build() throws AndrolibException {
|
public void build(File dexFile) throws AndrolibException {
|
||||||
try {
|
try {
|
||||||
DexBuilder dexBuilder;
|
DexBuilder dexBuilder = mApiLevel > 0
|
||||||
if (mApiLevel > 0) {
|
? new DexBuilder(Opcodes.forApi(mApiLevel))
|
||||||
dexBuilder = new DexBuilder(Opcodes.forApi(mApiLevel));
|
: new DexBuilder(Opcodes.getDefault());
|
||||||
} else {
|
|
||||||
dexBuilder = new DexBuilder(Opcodes.getDefault());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String fileName : mSmaliDir.getDirectory().getFiles(true)) {
|
for (String fileName : mSmaliDir.getDirectory().getFiles(true)) {
|
||||||
buildFile(fileName, dexBuilder);
|
buildFile(fileName, dexBuilder);
|
||||||
}
|
}
|
||||||
dexBuilder.writeTo(new FileDataStore( new File(mDexFile.getAbsolutePath())));
|
|
||||||
} catch (IOException | DirectoryException ex) {
|
dexBuilder.writeTo(new FileDataStore(dexFile));
|
||||||
throw new AndrolibException(ex);
|
} catch (DirectoryException | IOException | RuntimeException ex) {
|
||||||
|
throw new AndrolibException("Could not smali folder: " + mSmaliDir.getName(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildFile(String fileName, DexBuilder dexBuilder)
|
private void buildFile(String fileName, DexBuilder dexBuilder) throws AndrolibException {
|
||||||
throws AndrolibException, IOException {
|
if (!fileName.endsWith(".smali")) {
|
||||||
File inFile = new File(mSmaliDir, fileName);
|
LOGGER.warning("Unknown file type, ignoring: " + fileName);
|
||||||
InputStream inStream = Files.newInputStream(inFile.toPath());
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (fileName.endsWith(".smali")) {
|
boolean success;
|
||||||
try {
|
Exception cause;
|
||||||
if (!SmaliMod.assembleSmaliFile(inFile, dexBuilder, mApiLevel, false, false)) {
|
try {
|
||||||
throw new AndrolibException("Could not smali file: " + fileName);
|
File inFile = new File(mSmaliDir, fileName);
|
||||||
}
|
success = SmaliMod.assembleSmaliFile(inFile, dexBuilder, mApiLevel, false, false);
|
||||||
} catch (IOException | RecognitionException ex) {
|
cause = null;
|
||||||
throw new AndrolibException(ex);
|
} catch (Exception ex) {
|
||||||
|
success = false;
|
||||||
|
cause = ex;
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
AndrolibException ex = new AndrolibException("Could not smali file: " + fileName);
|
||||||
|
if (cause != null) {
|
||||||
|
ex.initCause(cause);
|
||||||
}
|
}
|
||||||
} else {
|
throw ex;
|
||||||
LOGGER.warning("Unknown file type, ignoring: " + inFile);
|
|
||||||
}
|
}
|
||||||
inStream.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ExtFile mSmaliDir;
|
|
||||||
private final File mDexFile;
|
|
||||||
private final int mApiLevel;
|
|
||||||
|
|
||||||
private final static Logger LOGGER = Logger.getLogger(SmaliBuilder.class.getName());
|
|
||||||
}
|
}
|
||||||
|
@ -30,21 +30,19 @@ import com.android.tools.smali.dexlib2.iface.MultiDexContainer;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
|
||||||
public class SmaliDecoder {
|
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)
|
public SmaliDecoder(File apkFile, 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) {
|
|
||||||
mApkFile = apkFile;
|
mApkFile = apkFile;
|
||||||
mOutDir = outDir;
|
mDexName = dexName;
|
||||||
mDexFile = dexName;
|
|
||||||
mBakDeb = bakDeb;
|
mBakDeb = bakDeb;
|
||||||
mApiLevel = apiLevel;
|
mApiLevel = apiLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DexFile decode() throws AndrolibException {
|
public DexFile decode(File outDir) throws AndrolibException {
|
||||||
try {
|
try {
|
||||||
final BaksmaliOptions options = new BaksmaliOptions();
|
final BaksmaliOptions options = new BaksmaliOptions();
|
||||||
|
|
||||||
@ -76,7 +74,7 @@ public class SmaliDecoder {
|
|||||||
if (container.getDexEntryNames().size() == 1) {
|
if (container.getDexEntryNames().size() == 1) {
|
||||||
dexEntry = container.getEntry(container.getDexEntryNames().get(0));
|
dexEntry = container.getEntry(container.getDexEntryNames().get(0));
|
||||||
} else {
|
} else {
|
||||||
dexEntry = container.getEntry(mDexFile);
|
dexEntry = container.getEntry(mDexName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check the passed param exists
|
// Double-check the passed param exists
|
||||||
@ -93,20 +91,14 @@ public class SmaliDecoder {
|
|||||||
|
|
||||||
if (dexFile instanceof DexBackedOdexFile) {
|
if (dexFile instanceof DexBackedOdexFile) {
|
||||||
options.inlineResolver =
|
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;
|
return dexFile;
|
||||||
} catch (IOException ex) {
|
} 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;
|
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user