diff --git a/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java b/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java index 0f5f87fb9a..9d610a2592 100644 --- a/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java +++ b/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java @@ -457,18 +457,18 @@ private static void cmdDecode(String[] args) throws AndrolibException { } else { String mode = cli.getOptionValue(decodeResResolveModeOption); switch (mode) { - case "remove": - config.setDecodeResolve(Config.DecodeResolve.REMOVE); - break; case "keep": config.setDecodeResolve(Config.DecodeResolve.KEEP); break; + case "remove": + config.setDecodeResolve(Config.DecodeResolve.REMOVE); + break; case "dummy": config.setDecodeResolve(Config.DecodeResolve.DUMMY); break; default: System.err.println("Unknown resolve resources mode: " + mode); - System.err.println("Expect: 'remove', 'keep' or 'dummy'."); + System.err.println("Expect: 'keep', 'remove' or 'dummy'."); System.exit(1); return; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/BinaryXmlResourceParser.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/BinaryXmlResourceParser.java index 1a8a99bc40..9d2038349c 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/BinaryXmlResourceParser.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/BinaryXmlResourceParser.java @@ -382,18 +382,19 @@ public String getAttributeName(int index) { // Are improperly decoded when trusting the string pool. // Leveraging the resource map allows us to get the proper value. // + String nameStr; try { - String resourceMapValue = decodeFromResourceId(nameId); - if (resourceMapValue != null) { - return resourceMapValue; + nameStr = decodeFromResourceId(nameId); + if (nameStr != null) { + return nameStr; } } catch (AndrolibException ignored) { } // Couldn't decode from resource map, fall back to string pool. - String stringPoolValue = mStringPool.getString(name); - if (stringPoolValue == null) { - stringPoolValue = ""; + nameStr = mStringPool.getString(name); + if (nameStr == null) { + nameStr = ""; } // In certain optimized apps, some attributes's specs are removed despite being used. @@ -408,24 +409,24 @@ public String getAttributeName(int index) { if (removeUnresolved || nameId.getPackageId() != pkg.getId()) { LOGGER.warning(String.format( "null attr reference: ns=%s, name=%s, id=%s", - getAttributePrefix(index), stringPoolValue, nameId)); - return stringPoolValue; + getAttributePrefix(index), nameStr, nameId)); + return nameStr; } - if (stringPoolValue.isEmpty()) { - stringPoolValue = ResEntrySpec.MISSING_PREFIX + nameId; + if (nameStr.isEmpty()) { + nameStr = ResEntrySpec.MISSING_PREFIX + nameId; } - pkg.addEntrySpec(nameId, stringPoolValue); + nameStr = pkg.addEntrySpec(nameId, nameStr).getName(); pkg.addEntry(nameId, ResConfig.DEFAULT, ResAttribute.DEFAULT); } catch (AndrolibException ex) { setFirstError(ex); LOGGER.warning(String.format( "Could not add missing attr: ns=%s, name=%s, id=%s", - getAttributePrefix(index), stringPoolValue, nameId)); + getAttributePrefix(index), nameStr, nameId)); } } - return stringPoolValue; + return nameStr; } private String decodeFromResourceId(ResId resId) throws AndrolibException { diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResEntrySpec.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResEntrySpec.java index d297fe3811..78605cd922 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResEntrySpec.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResEntrySpec.java @@ -32,8 +32,8 @@ public ResEntrySpec(ResTypeSpec typeSpec, ResId id, String name) { assert typeSpec.getId() == id.getTypeId(); mTypeSpec = typeSpec; mId = id; - // Some apps had their entry names obfuscated or collapsed to - // a single value in the key string pool. + // Some apps had their entry names obfuscated or collapsed to a single + // value in the key string pool. mName = isValidEntryName(name) ? name : RENAMED_PREFIX + id; } @@ -47,14 +47,11 @@ private static boolean isValidEntryName(String name) { if (!Character.isJavaIdentifierStart(name.charAt(0))) { return false; } - // The rest must be valid Java identifier part characters. + // The rest must be valid Java identifier part characters or any of the + // whitelisted special characters. for (int i = 1; i < len; i++) { char ch = name.charAt(i); - // Whitelisted special characters. - if (ch == '.' || ch == '-') { - continue; - } - if (!Character.isJavaIdentifierPart(ch)) { + if (!Character.isJavaIdentifierPart(ch) && ch != '.' && ch != '-') { return false; } } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResPackage.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResPackage.java index cb3e600408..3622680b60 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResPackage.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResPackage.java @@ -23,8 +23,10 @@ import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.logging.Logger; public class ResPackage { @@ -36,6 +38,7 @@ public class ResPackage { private final Map mTypeSpecs; private final Map, ResType> mTypes; private final Map mEntrySpecs; + private final Map> mEntryNames; private final Map, ResEntry> mEntries; private final Map mOverlayables; @@ -46,6 +49,7 @@ public ResPackage(ResTable table, int id, String name) { mTypeSpecs = new HashMap<>(); mTypes = new HashMap<>(); mEntrySpecs = new HashMap<>(); + mEntryNames = new HashMap<>(); mEntries = new HashMap<>(); mOverlayables = new HashMap<>(); } @@ -152,8 +156,24 @@ public ResEntrySpec addEntrySpec(ResId id, String name) throws AndrolibException int typeId = id.getTypeId(); ResTypeSpec typeSpec = getTypeSpec(typeId); + + // Obfuscation can cause specs to be generated using existing names. + // Enforce uniqueness by renaming the spec when that happens. + Set entryNames = mEntryNames.get(typeId); + if (entryNames == null) { + entryNames = new HashSet<>(); + mEntryNames.put(typeId, entryNames); + } else if (entryNames.contains(name)) { + // Clear the name to force a rename. + name = ""; + } + entrySpec = new ResEntrySpec(typeSpec, id, name); mEntrySpecs.put(id, entrySpec); + + // Record the name to enforce uniqueness. + entryNames.add(entrySpec.getName()); + return entrySpec; } diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResItem.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResItem.java index 171f8b47d1..8e614cf7a2 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResItem.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResItem.java @@ -47,12 +47,12 @@ public static ResItem parse(ResPackage pkg, int type, int data, String rawValue) return data == TypedValue.DATA_NULL_EMPTY ? ResPrimitive.EMPTY : ResReference.NULL; case TypedValue.TYPE_REFERENCE: case TypedValue.TYPE_DYNAMIC_REFERENCE: - return data != 0 || rawValue != null + return (data != 0 || rawValue != null) ? new ResReference(pkg, ResId.of(data), rawValue) : ResReference.NULL; case TypedValue.TYPE_ATTRIBUTE: case TypedValue.TYPE_DYNAMIC_ATTRIBUTE: - return data != 0 || rawValue != null + return (data != 0 || rawValue != null) ? new ResReference(pkg, ResId.of(data), rawValue, ResReference.Type.ATTRIBUTE) : ResReference.NULL; case TypedValue.TYPE_STRING: diff --git a/brut.j.util/src/main/java/brut/util/ZipUtils.java b/brut.j.util/src/main/java/brut/util/ZipUtils.java index 28a4fe8115..0915a6c586 100644 --- a/brut.j.util/src/main/java/brut/util/ZipUtils.java +++ b/brut.j.util/src/main/java/brut/util/ZipUtils.java @@ -61,7 +61,7 @@ public static void zipDir(File baseDir, String dirName, ZipOutputStream out, Col if (file.isDirectory()) { zipDir(baseDir, fileName, out, doNotCompress); } else if (file.isFile()) { - zipFile(baseDir, fileName, out, doNotCompress != null && !doNotCompress.isEmpty() + zipFile(baseDir, fileName, out, (doNotCompress != null && !doNotCompress.isEmpty()) ? entryName -> doNotCompress.contains(entryName) || doNotCompress.contains(FilenameUtils.getExtension(entryName)) : entryName -> false);