Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
355dae9
chore: change publication group id
oSumAtrIX Aug 10, 2023
ad75203
fix: do not try to get existing AAPT binary if a path to it is provided
oSumAtrIX Aug 10, 2023
019b2e6
feat: allow recording uncompressed files manually
oSumAtrIX Aug 10, 2023
c0900e5
feat: allow instantiating with existing `ApkInfo`
oSumAtrIX Aug 10, 2023
4608df6
feat: allow using the default `XML` serializer by `ResourcesDecoder` …
oSumAtrIX Aug 10, 2023
72ffcbb
feat: decode `9patch` files on Android
oSumAtrIX Aug 10, 2023
b16e176
build: change publication repository
oSumAtrIX Aug 10, 2023
648cb8f
build: start new dev cycle (2.8.2-2)
oSumAtrIX Aug 10, 2023
9c6dffa
build: allow signing with using gpg-agent
oSumAtrIX Aug 10, 2023
adc9452
feat: create missing directories
oSumAtrIX Aug 11, 2023
e608041
build: start new dev cycle (2.8.2-3)
oSumAtrIX Aug 11, 2023
61bcd85
Merge branch 'upstream'
oSumAtrIX Aug 19, 2023
2c9e14c
fix: Make sure the property is not null
oSumAtrIX Aug 19, 2023
9ae8020
build: start new dev cycle (2.8.2-4)
oSumAtrIX Aug 19, 2023
452a620
feat: disable `customAapt` logic
oSumAtrIX Aug 21, 2023
863b738
Merge branch 'upstream'
oSumAtrIX Aug 22, 2023
24c0bd9
feat: remove AAPT2 argument unavailable in stable version of platform…
oSumAtrIX Aug 22, 2023
a377fde
build: start new dev cycle (2.8.2-5)
oSumAtrIX Aug 22, 2023
43edf04
Merge branch 'upstream'
oSumAtrIX Aug 27, 2023
538b8a8
build: start new dev cycle (2.8.2-6)
oSumAtrIX Aug 27, 2023
35e23a9
Merge branch 'upstream'
oSumAtrIX Oct 8, 2023
c68c74f
build: Publish on Jitpack
oSumAtrIX Oct 9, 2023
fc4a59f
Merge branch 'upstream'
oSumAtrIX Nov 2, 2023
1b1c7f8
fix: tighten up detectPossibleDirectoryTraversal for Windows
iBotPeaches Jan 17, 2024
bd82a53
test: run path traversal test on Windows
iBotPeaches Jan 17, 2024
3bb7888
build: version bump (2.9.3)
iBotPeaches Jan 20, 2024
0fd4443
build: Revert publishing on Jitpack
oSumAtrIX Feb 13, 2024
b0f3957
Merge upstream
oSumAtrIX Feb 13, 2024
8f166d5
Merge branch 'upstream'
oSumAtrIX Dec 17, 2024
49eeed6
build: start new dev cycle (2.10.1.1)
oSumAtrIX Dec 17, 2024
e47c889
feat: Add auxiliary resource table loading for split APK support
Aunali321 Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions brut.apktool/apktool-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,16 @@ dependencies {

testImplementation(libs.junit)
testImplementation(libs.xmlunit)

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")
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ private void invokeAapt2(File apkFile, File manifest, File resDir, File rawDir,

if (resDir != null) {
File buildDir = new File(resDir.getParent(), "build");

//noinspection ResultOfMethodCallIgnored
buildDir.mkdir();

resourcesZip = new File(buildDir, "resources.zip");
}

Expand Down Expand Up @@ -170,7 +174,8 @@ private void invokeAapt2(File apkFile, File manifest, File resDir, File rawDir,
cmd.add("--no-version-transitions");
cmd.add("--no-resource-deduping");

cmd.add("--no-compile-sdk-metadata");
// TODO: Add this back, once AAPT2 from platform-tools 34.0.4 is stable
// cmd.add("--no-compile-sdk-metadata");

// #3427 - Ignore stricter parsing during aapt2
cmd.add("--warn-manifest-validation");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public ApkDecoder(ExtFile apkFile) {
this(apkFile, Config.getDefaultConfig());
}

public ApkDecoder(ApkInfo apkInfo, Config config) {
this(apkInfo.getApkFile(), config);

mApkInfo = apkInfo;
}

public ApkDecoder(ExtFile apkFile, Config config) {
mApkFile = apkFile;
mConfig = config;
Expand All @@ -74,7 +80,7 @@ public ApkInfo decode(File outDir) throws AndrolibException {
mWorker = new BackgroundWorker(mConfig.jobs - 1);
}
try {
mApkInfo = new ApkInfo(mApkFile);
if (mApkInfo == null) mApkInfo = new ApkInfo(mApkFile);
mResDecoder = new ResourcesDecoder(mConfig, mApkInfo);

try {
Expand Down Expand Up @@ -325,8 +331,15 @@ private void writeApkInfo(File outDir) throws AndrolibException {
}

// record uncompressed files
Map<String, String> resFileMapping = mResDecoder.getResFileMapping();
recordUncompressedFiles(resFileMapping);

// write apk info to file
mApkInfo.save(new File(outDir, "apktool.yml"));
}

public void recordUncompressedFiles(Map<String, String> resFileMapping) throws AndrolibException {
try {
Map<String, String> resFileMapping = mResDecoder.getResFileMapping();
Set<String> uncompressedExts = new HashSet<>();
Set<String> uncompressedFiles = new HashSet<>();
Directory in = mApkFile.getDirectory();
Expand Down Expand Up @@ -377,8 +390,5 @@ private void writeApkInfo(File outDir) throws AndrolibException {
} catch (DirectoryException ex) {
throw new AndrolibException(ex);
}

// write apk info to file
mApkInfo.save(new File(outDir, "apktool.yml"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import brut.directory.ExtFile;
import brut.xmlpull.MXSerializer;
import com.google.common.collect.Sets;
import brut.util.OSDetection;
import org.xmlpull.v1.XmlSerializer;

import java.io.*;
Expand All @@ -46,6 +47,7 @@ public class ResourcesDecoder {
private final ApkInfo mApkInfo;
private final ResTable mResTable;
private final Map<String, String> mResFileMapping;
private boolean mIncludeAuxiliaryPublicXml;

public ResourcesDecoder(Config config, ApkInfo apkInfo) {
mConfig = config;
Expand All @@ -70,6 +72,18 @@ public void loadMainPkg() throws AndrolibException {
mResTable.loadMainPkg(mApkInfo.getApkFile());
}

public void loadAuxiliaryPkg(ExtFile apkFile) throws AndrolibException {
mResTable.loadAuxiliaryPkg(apkFile);
}

public void loadAuxiliaryPkgs(Collection<ExtFile> apkFiles) throws AndrolibException {
mResTable.loadAuxiliaryPkgs(apkFiles);
}

public void setIncludeAuxiliaryPublicXml(boolean includeAuxiliaryPublicXml) {
mIncludeAuxiliaryPublicXml = includeAuxiliaryPublicXml;
}

public void decodeManifest(File apkDir) throws AndrolibException {
if (!mApkInfo.hasManifest()) {
return;
Expand Down Expand Up @@ -157,11 +171,15 @@ public void decodeResources(File apkDir) throws AndrolibException {
return;
}

mResTable.loadMainPkg(mApkInfo.getApkFile());
loadMainPkg();

ResStreamDecoderContainer decoders = new ResStreamDecoderContainer();
decoders.setDecoder("raw", new ResRawStreamDecoder());
decoders.setDecoder("9patch", new Res9patchStreamDecoder());

decoders.setDecoder(
"9patch",
OSDetection.isAndroid() ? new Res9patchAndroidStreamDecoder() : new Res9patchStreamDecoder()
);

AXmlResourceParser axmlParser = new AXmlResourceParser(mResTable);
XmlSerializer xmlSerializer = newXmlSerializer();
Expand All @@ -188,7 +206,7 @@ public void decodeResources(File apkDir) throws AndrolibException {
generateValuesFile(valuesFile, outDir, xmlSerializer);
}

generatePublicXml(pkg, outDir, xmlSerializer);
generatePublicXml(pkg, outDir, xmlSerializer, mIncludeAuxiliaryPublicXml);
}

AndrolibException decodeError = axmlParser.getFirstError();
Expand All @@ -197,7 +215,7 @@ public void decodeResources(File apkDir) throws AndrolibException {
}
}

private XmlSerializer newXmlSerializer() throws AndrolibException {
public XmlSerializer newXmlSerializer() throws AndrolibException {
try {
XmlSerializer serial = new MXSerializer();
serial.setFeature(MXSerializer.FEATURE_ATTR_VALUE_NO_ESCAPE, true);
Expand Down Expand Up @@ -232,14 +250,14 @@ private void generateValuesFile(ResValuesFile valuesFile, Directory resDir, XmlS
}
}

private void generatePublicXml(ResPackage pkg, Directory resDir, XmlSerializer serial)
private void generatePublicXml(ResPackage pkg, Directory resDir, XmlSerializer serial, boolean includeAuxiliary)
throws AndrolibException {
try (OutputStream out = resDir.getFileOutput("values/public.xml")) {
serial.setOutput(out, null);
serial.startDocument(null, null);
serial.startTag(null, "resources");

for (ResResSpec spec : pkg.listResSpecs()) {
for (ResResSpec spec : mResTable.listResSpecs(pkg, includeAuxiliary)) {
serial.startTag(null, "public");
serial.attribute(null, "type", spec.getType().getName());
serial.attribute(null, "name", spec.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public class ResTable {
private final ApkInfo mApkInfo;
private final Map<Integer, ResPackage> mPackagesById;
private final Map<String, ResPackage> mPackagesByName;
private final Map<ResID, ResResSpec> mAuxiliaryResSpecs;
private final Map<String, ResResSpec> mAuxiliaryResSpecsByName;
private final Set<ResPackage> mMainPackages;
private final Set<ResPackage> mFramePackages;

Expand All @@ -63,6 +65,8 @@ public ResTable(Config config, ApkInfo apkInfo) {
mApkInfo = apkInfo;
mPackagesById = new HashMap<>();
mPackagesByName = new HashMap<>();
mAuxiliaryResSpecs = new LinkedHashMap<>();
mAuxiliaryResSpecsByName = new LinkedHashMap<>();
mMainPackages = new LinkedHashSet<>();
mFramePackages = new LinkedHashSet<>();
}
Expand Down Expand Up @@ -91,6 +95,16 @@ public ResResSpec getResSpec(int resID) throws AndrolibException {
}

public ResResSpec getResSpec(ResID resID) throws AndrolibException {
ResPackage pkg = mPackagesById.get(resID.pkgId);
if (pkg != null && pkg.hasResSpec(resID)) {
return pkg.getResSpec(resID);
}

ResResSpec auxiliarySpec = mAuxiliaryResSpecs.get(resID);
if (auxiliarySpec != null) {
return auxiliarySpec;
}

return getPackage(resID.pkgId).getResSpec(resID);
}

Expand Down Expand Up @@ -131,29 +145,27 @@ private ResPackage selectPkgWithMostResSpecs(ResPackage[] pkgs) {
}

public void loadMainPkg(ExtFile apkFile) throws AndrolibException {
LOGGER.info("Loading resource table...");
ResPackage[] pkgs = loadResPackagesFromApk(apkFile, mConfig.keepBrokenResources);
ResPackage pkg;

switch (pkgs.length) {
case 0:
pkg = new ResPackage(this, 0, null);
break;
case 1:
pkg = pkgs[0];
break;
case 2:
LOGGER.warning("Skipping package group: " + pkgs[0].getName());
pkg = pkgs[1];
break;
default:
pkg = selectPkgWithMostResSpecs(pkgs);
break;
if (mMainPkgLoaded) {
return;
}

LOGGER.info("Loading resource table...");
ResPackage pkg = selectPkg(loadResPackagesFromApk(apkFile, mConfig.keepBrokenResources));
addPackage(pkg, true);
mMainPkgLoaded = true;
}

public void loadAuxiliaryPkg(ExtFile apkFile) throws AndrolibException {
LOGGER.info("Loading auxiliary resource table from file: " + apkFile);
addAuxiliaryPackage(selectPkg(loadResPackagesFromApk(apkFile, mConfig.keepBrokenResources)));
}

public void loadAuxiliaryPkgs(Collection<ExtFile> apkFiles) throws AndrolibException {
for (ExtFile apkFile : apkFiles) {
loadAuxiliaryPkg(apkFile);
}
}

private ResPackage loadFrameworkPkg(int id) throws AndrolibException {
Framework framework = new Framework(mConfig);
File frameworkApk = framework.getFrameworkApk(id, mConfig.frameworkTag);
Expand Down Expand Up @@ -224,7 +236,32 @@ public ResPackage getPackage(String name) throws AndrolibException {
}

public ResValue getValue(String pkg, String type, String name) throws AndrolibException {
return getPackage(pkg).getType(type).getResSpec(name).getDefaultResource().getValue();
try {
return getPackage(pkg).getType(type).getResSpec(name).getDefaultResource().getValue();
} catch (UndefinedResObjectException ex) {
ResResSpec auxiliarySpec = mAuxiliaryResSpecsByName.get(getSpecNameKey(pkg, type, name));
if (auxiliarySpec == null) {
throw ex;
}
return auxiliarySpec.getDefaultResource().getValue();
}
}

public Collection<ResResSpec> listResSpecs(ResPackage pkg, boolean includeAuxiliary) {
if (!includeAuxiliary) {
return pkg.listResSpecs();
}

Map<ResID, ResResSpec> specs = new LinkedHashMap<>();
for (ResResSpec spec : pkg.listResSpecs()) {
specs.put(spec.getId(), spec);
}
for (ResResSpec spec : mAuxiliaryResSpecs.values()) {
if (spec.getId().pkgId == pkg.getId()) {
specs.putIfAbsent(spec.getId(), spec);
}
}
return specs.values();
}

public void addPackage(ResPackage pkg, boolean main) throws AndrolibException {
Expand All @@ -246,6 +283,12 @@ public void addPackage(ResPackage pkg, boolean main) throws AndrolibException {
}
}

void addAuxiliaryPackage(ResPackage pkg) {
for (ResResSpec spec : pkg.listResSpecs()) {
addAuxiliaryResSpec(spec);
}
}

public void setPackageRenamed(String pkg) {
mPackageRenamed = pkg;
}
Expand Down Expand Up @@ -381,4 +424,44 @@ private void loadVersionName(File apkDir) {
mApkInfo.versionInfo.versionName = refValue;
}
}

private ResPackage selectPkg(ResPackage[] pkgs) {
switch (pkgs.length) {
case 0:
return new ResPackage(this, 0, null);
case 1:
return pkgs[0];
case 2:
LOGGER.warning("Skipping package group: " + pkgs[0].getName());
return pkgs[1];
default:
return selectPkgWithMostResSpecs(pkgs);
}
}

private void addAuxiliaryResSpec(ResResSpec spec) {
ResResSpec existingSpec = mAuxiliaryResSpecs.get(spec.getId());
if (existingSpec != null) {
if (!existingSpec.getName().equals(spec.getName())
|| !existingSpec.getType().getName().equals(spec.getType().getName())) {
LOGGER.warning("Ignoring conflicting auxiliary resource spec: " + spec);
}
return;
}

mAuxiliaryResSpecs.put(spec.getId(), spec);

String key = getSpecNameKey(spec.getPackage().getName(), spec.getType().getName(), spec.getName());
ResResSpec existingNamedSpec = mAuxiliaryResSpecsByName.get(key);
if (existingNamedSpec != null && !existingNamedSpec.getId().equals(spec.getId())) {
LOGGER.warning("Ignoring conflicting auxiliary resource name: " + key);
return;
}

mAuxiliaryResSpecsByName.putIfAbsent(key, spec);
}

private String getSpecNameKey(String pkg, String type, String name) {
return pkg + ":" + type + "/" + name;
}
}
Loading