VYPR
High severity7.1NVD Advisory· Published Apr 21, 2026· Updated Apr 23, 2026

CVE-2026-39973

CVE-2026-39973

Description

Apktool is a tool for reverse engineering Android APK files. In versions 3.0.0 and 3.0.1, a path traversal vulnerability in brut/androlib/res/decoder/ResFileDecoder.java allows a maliciously crafted APK to write arbitrary files to the filesystem during standard decoding (apktool d). This is a security regression introduced in commit e10a045 (PR #4041, December 12, 2025), which removed the BrutIO.sanitizePath() call that previously prevented path traversal in resource file output paths. An attacker can embed ../ sequences in the resources.arsc Type String Pool to escape the output directory and write files to arbitrary locations, including ~/.ssh/config, ~/.bashrc, or Windows Startup folders, escalating to RCE. The fix in version 3.0.2 re-introduces BrutIO.sanitizePath() in ResFileDecoder.java before file write operations.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.apktool:apktool-libMaven
>= 3.0.0, < 3.0.23.0.2

Affected products

1
  • cpe:2.3:a:apktool:apktool:*:*:*:*:*:*:*:*
    Range: >=3.0.0,<3.0.2

Patches

1
e10a0450c7af

refactor: Improve renaming/injection of resources in stripped APKs (#4041)

https://github.com/iBotPeaches/ApktoolIgor EisbergDec 12, 2025via ghsa
24 files changed · +549 310
  • brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java+4 4 modified
    @@ -460,15 +460,15 @@ private static void cmdDecode(String[] args) throws AndrolibException {
                         case "remove":
                             config.setDecodeResolve(Config.DecodeResolve.REMOVE);
                             break;
    -                    case "dummy":
    -                        config.setDecodeResolve(Config.DecodeResolve.DUMMY);
    -                        break;
                         case "keep":
                             config.setDecodeResolve(Config.DecodeResolve.KEEP);
                             break;
    +                    case "dummy":
    +                        config.setDecodeResolve(Config.DecodeResolve.DUMMY);
    +                        break;
                         default:
                             System.err.println("Unknown resolve resources mode: " + mode);
    -                        System.err.println("Expect: 'remove', 'dummy' or 'keep'.");
    +                        System.err.println("Expect: 'remove', 'keep' or 'dummy'.");
                             System.exit(1);
                             return;
                     }
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/Config.java+2 2 modified
    @@ -19,7 +19,7 @@
     public class Config {
         public enum DecodeSources { NONE, FULL, ONLY_MAIN_CLASSES }
         public enum DecodeResources { NONE, FULL, ONLY_MANIFEST }
    -    public enum DecodeResolve { REMOVE, DUMMY, KEEP }
    +    public enum DecodeResolve { REMOVE, KEEP, DUMMY }
         public enum DecodeAssets { NONE, FULL }
     
         private final String mVersion;
    @@ -64,7 +64,7 @@ public Config(String version) {
             mDecodeSources = DecodeSources.FULL;
             mBaksmaliDebugMode = true;
             mDecodeResources = DecodeResources.FULL;
    -        mDecodeResolve = DecodeResolve.REMOVE;
    +        mDecodeResolve = DecodeResolve.KEEP;
             mKeepBrokenResources = false;
             mAnalysisMode = false;
             mDecodeAssets = DecodeAssets.FULL;
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/BinaryResourceParser.java+13 1 modified
    @@ -812,8 +812,20 @@ private void injectMissingEntrySpecs() throws AndrolibException {
             }
     
             for (ResId id : mMissingEntrySpecs) {
    +            ResTypeSpec typeSpec = mPackage.getTypeSpec(id.getTypeId());
    +            ResValue value;
    +            switch (typeSpec.getName()) {
    +                case "attr":
    +                case "^attr-private":
    +                    value = ResAttribute.DEFAULT;
    +                    break;
    +                default:
    +                    value = ResReference.NULL;
    +                    break;
    +            }
    +
                 mPackage.addEntrySpec(id, ResEntrySpec.DUMMY_PREFIX + id);
    -            mPackage.addEntry(id, ResConfig.DEFAULT, ResReference.NULL);
    +            mPackage.addEntry(id, ResConfig.DEFAULT, value);
             }
         }
     }
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/BinaryXmlResourceParser.java+82 49 modified
    @@ -18,6 +18,7 @@
     
     import android.content.res.XmlResourceParser;
     import android.util.TypedValue;
    +import brut.androlib.Config;
     import brut.androlib.exceptions.AndrolibException;
     import brut.androlib.exceptions.FrameworkNotFoundException;
     import brut.androlib.exceptions.UndefinedResObjectException;
    @@ -61,6 +62,7 @@ public class BinaryXmlResourceParser implements XmlResourceParser {
         private static final int ATTRIBUTE_IX_VALUE_DATA = 4; // data
         private static final int ATTRIBUTE_LENGTH = 5;
     
    +    private static final int ANDROID_PKG_ID = 0x01;
         private static final int PRIVATE_PKG_ID = 0x7F;
     
         private final ResTable mTable;
    @@ -215,10 +217,11 @@ public char[] getTextCharacters(int[] holderForStartAndLength) {
             if (text == null) {
                 return null;
             }
    +        int len = text.length();
             holderForStartAndLength[0] = 0;
    -        holderForStartAndLength[1] = text.length();
    -        char[] chars = new char[text.length()];
    -        text.getChars(0, text.length(), chars, 0);
    +        holderForStartAndLength[1] = len;
    +        char[] chars = new char[len];
    +        text.getChars(0, len, chars, 0);
             return chars;
         }
     
    @@ -310,20 +313,24 @@ public String getAttributeNamespace(int index) {
             int offset = getAttributeOffset(index);
             int namespace = mAttributes[offset + ATTRIBUTE_IX_NAMESPACE_URI];
     
    +        ResId nameId = ResId.of(getAttributeNameResource(index));
    +        int pkgId = nameId.getPackageId();
    +
             // #2972 - If the namespace index is -1, the attribute is not present, but if the attribute is from system
             // we can resolve it to the default namespace. This may prove to be too aggressive as we scope the entire
             // system namespace, but it is better than not resolving it at all.
    -        ResId id = ResId.of(getAttributeNameResource(index));
    -        int pkgId = id.getPackageId();
    -        if (namespace == -1 && pkgId == 1) {
    -            return ANDROID_RES_NS;
    -        }
             if (namespace == -1) {
    +            if (pkgId == PRIVATE_PKG_ID) {
    +                return getNonDefaultNamespaceUri(offset);
    +            }
    +            if (pkgId == ANDROID_PKG_ID) {
    +                return ANDROID_RES_NS;
    +            }
                 return "";
             }
     
             // Minifiers like removing the namespace, so we will default to default namespace
    -        // unless the pkgId of the resource is private. We will grab the non-standard one.
    +        // unless the package ID of the resource is private. We will grab the non-standard one.
             String value = mStringPool.getString(namespace);
             if (value != null && !value.isEmpty()) {
                 return value;
    @@ -375,23 +382,50 @@ public String getAttributeName(int index) {
             // Are improperly decoded when trusting the string pool.
             // Leveraging the resource map allows us to get the proper value.
             // <item android:state_enabled="true" app:d2="false" app:d3="true">
    -        String resourceMapValue;
             try {
    -            resourceMapValue = decodeFromResourceId(nameId);
    +            String resourceMapValue = decodeFromResourceId(nameId);
    +            if (resourceMapValue != null) {
    +                return resourceMapValue;
    +            }
             } catch (AndrolibException ignored) {
    -            resourceMapValue = null;
    -        }
    -        if (resourceMapValue != null) {
    -            return resourceMapValue;
             }
     
    +        // Couldn't decode from resource map, fall back to string pool.
             String stringPoolValue = mStringPool.getString(name);
    -        if (stringPoolValue != null) {
    -            return stringPoolValue;
    +        if (stringPoolValue == null) {
    +            stringPoolValue = "";
             }
     
    -        // If it was not found in either, then we have a bogus resource.
    -        return ResEntrySpec.MISSING_PREFIX + nameId;
    +        // In certain optimized apps, some attributes's specs are removed despite being used.
    +        // Inject a generic spec for the attribute, otherwise we can't rebuild.
    +        if (nameId != ResId.NULL) {
    +            Config config = mTable.getConfig();
    +            boolean removeUnresolved = config.getDecodeResolve() == Config.DecodeResolve.REMOVE;
    +            try {
    +                ResPackage pkg = mTable.getMainPackage();
    +
    +                // #2836 - Skip item if the resource cannot be identified.
    +                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;
    +                }
    +
    +                if (stringPoolValue.isEmpty()) {
    +                    stringPoolValue = ResEntrySpec.MISSING_PREFIX + nameId;
    +                }
    +                pkg.addEntrySpec(nameId, stringPoolValue);
    +                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));
    +            }
    +        }
    +
    +        return stringPoolValue;
         }
     
         private String decodeFromResourceId(ResId resId) throws AndrolibException {
    @@ -425,29 +459,48 @@ public String getAttributeValue(int index) {
             try {
                 String stringPoolValue = valueRaw != -1
                     ? ResXmlEncoders.escapeXmlChars(mStringPool.getString(valueRaw)) : null;
    -            String resourceMapValue = null;
    +            String rawValue = stringPoolValue;
     
    -            // Ensure we only track down obfuscated values for reference/attribute type values. Otherwise, we might
    -            // spam lookups against resource table for invalid IDs.
    +            boolean isExplicitType = valueType != TypedValue.TYPE_NULL;
                 if (valueType == TypedValue.TYPE_REFERENCE
                         || valueType == TypedValue.TYPE_DYNAMIC_REFERENCE
                         || valueType == TypedValue.TYPE_ATTRIBUTE
                         || valueType == TypedValue.TYPE_DYNAMIC_ATTRIBUTE) {
    -                resourceMapValue = decodeFromResourceId(ResId.of(valueData));
    +                // Explicit reference format is optional.
    +                isExplicitType = false;
    +                // Android prefers the resource map value over what the string pool has.
    +                // We only track down obfuscated values for reference/attribute type values.
    +                // Otherwise, we might spam lookups against resource table for invalid IDs.
    +                String resourceMapValue = decodeFromResourceId(ResId.of(valueData));
    +                if (stringPoolValue != null && resourceMapValue != null) {
    +                    // Handle a value with a format of "@yyy/xxx", but avoid "@yyy/zzz:xxx"
    +                    int slashPos = stringPoolValue.lastIndexOf('/');
    +                    if (slashPos != -1) {
    +                        int colonPos = stringPoolValue.lastIndexOf(':');
    +                        if (colonPos == -1) {
    +                            rawValue = stringPoolValue.substring(0, slashPos) + "/" + resourceMapValue;
    +                        }
    +                    } else if (!stringPoolValue.equals(resourceMapValue)) {
    +                        rawValue = resourceMapValue;
    +                    }
    +                }
                 }
     
                 // Try to decode from resource table.
                 ResId nameId = ResId.of(getAttributeNameResource(index));
    -            ResItem value = ResItem.parse(mTable.getCurrentPackage(), valueType, valueData,
    -                getPreferredString(stringPoolValue, resourceMapValue));
    +            ResItem value = ResItem.parse(mTable.getCurrentPackage(), valueType, valueData, rawValue);
                 if (nameId != ResId.NULL) {
                     try {
                         // We need the attribute entry's value to format this value.
                         ResEntrySpec nameSpec = mTable.getEntrySpec(nameId);
    -                    ResValue nameDefValue = mTable.getDefaultEntry(nameId).getValue();
    -
    -                    if (nameDefValue instanceof ResAttribute) {
    -                        decoded = ((ResAttribute) nameDefValue).formatValue(value, false);
    +                    ResValue nameValue = mTable.getDefaultEntry(nameId).getValue();
    +
    +                    if (nameValue instanceof ResAttribute) {
    +                        ResAttribute nameAttr = (ResAttribute) nameValue;
    +                        if (isExplicitType && !nameAttr.hasSymbolsForValue(value)) {
    +                            nameAttr.addType(valueType);
    +                        }
    +                        decoded = nameAttr.formatValue(value, false);
                         } else {
                             LOGGER.warning("Unexpected attribute name: " + nameSpec);
                         }
    @@ -678,26 +731,6 @@ private int findAttribute(String namespace, String attribute) {
             return -1;
         }
     
    -    private static String getPreferredString(String stringPoolValue, String resourceMapValue) {
    -        String value = stringPoolValue;
    -
    -        if (stringPoolValue != null && resourceMapValue != null) {
    -            int slashPos = stringPoolValue.lastIndexOf('/');
    -            int colonPos = stringPoolValue.lastIndexOf(':');
    -
    -            // Handle a value with a format of "@yyy/xxx", but avoid "@yyy/zzz:xxx"
    -            if (slashPos != -1) {
    -                if (colonPos == -1) {
    -                    String type = stringPoolValue.substring(0, slashPos);
    -                    value = type + "/" + resourceMapValue;
    -                }
    -            } else if (!stringPoolValue.equals(resourceMapValue)) {
    -                value = resourceMapValue;
    -            }
    -        }
    -        return value;
    -    }
    -
         private void resetEventInfo() {
             mEvent = -1;
             mLineNumber = -1;
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/data/StyledString.java+7 5 modified
    @@ -65,7 +65,8 @@ private String decode() {
                 return mDecodedText;
             }
     
    -        mBuffer = new StringBuilder(mText.length() * 2);
    +        int len = mText.length();
    +        mBuffer = new StringBuilder(len * 2);
             mLastOffset = 0;
     
             // Recurse top-level tags.
    @@ -75,7 +76,7 @@ private String decode() {
             }
     
             // Write the remaining encoded raw text.
    -        if (mLastOffset < mText.length()) {
    +        if (mLastOffset < len) {
                 mBuffer.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset)));
             }
     
    @@ -117,10 +118,11 @@ private void decodeIterate(PeekingIterator<Span> it) {
             }
     
             // Write encoded raw text preceding the closing tag.
    -        if (spanEnd > mLastOffset && mText.length() >= spanEnd) {
    +        int len = mText.length();
    +        if (spanEnd > mLastOffset && len >= spanEnd) {
                 mBuffer.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset, spanEnd)));
    -        } else if (mText.length() >= mLastOffset && mText.length() < spanEnd) {
    -            LOGGER.warning("Span (" + name + ") exceeds text length " + mText.length());
    +        } else if (len >= mLastOffset && len < spanEnd) {
    +            LOGGER.warning("Span (" + name + ") exceeds text length " + len);
                 mBuffer.append(ResXmlEncoders.escapeXmlChars(mText.substring(mLastOffset)));
             }
             mLastOffset = spanEnd;
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java+2 7 modified
    @@ -100,13 +100,8 @@ public void decode(ResEntry entry, Directory inDir, Directory outDir, Map<String
             }
     
             // Generate output file path from entry.
    -        String outResPath = entry.getTypeName() + entry.getConfig().getQualifiers() + "/" + entry.getName();
    -        if (BrutIO.detectPossibleDirectoryTraversal(outResPath)) {
    -            LOGGER.warning("Potentially malicious file path: " + outResPath + ", using instead: " + inResPath);
    -            outResPath = inResPath;
    -        } else if (!ext.isEmpty()) {
    -            outResPath += "." + ext;
    -        }
    +        String outResPath = entry.getTypeName() + entry.getConfig().getQualifiers() + "/" + entry.getName()
    +                + (ext.isEmpty() ? "" : "." + ext);
     
             // Map output path to original path if it's different.
             String outFileName = "res/" + outResPath;
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java+15 7 modified
    @@ -24,6 +24,7 @@
     import brut.androlib.meta.VersionInfo;
     import brut.androlib.res.decoder.*;
     import brut.androlib.res.table.*;
    +import brut.androlib.res.table.value.ResBag;
     import brut.androlib.res.table.value.ResFileReference;
     import brut.androlib.res.xml.ResXmlUtils;
     import brut.androlib.res.xml.ValuesXmlSerializable;
    @@ -86,15 +87,24 @@ public void decodeResources(File apkDir) throws AndrolibException {
             }
     
             ResPackage pkg = mTable.getMainPackage();
    -        Map<ResType, List<ResEntry>> valuesEntries = new HashMap<>();
     
    -        LOGGER.info("Decoding file-resources...");
    -        for (ResEntry entry : pkg.listEntries()) {
    +        LOGGER.info("Decoding value resources...");
    +        for (ResEntry entry : new ArrayList<>(pkg.listEntries())) {
    +            if (entry.getValue() instanceof ResBag) {
    +                ((ResBag) entry.getValue()).resolveKeys();
    +            }
    +        }
    +
    +        LOGGER.info("Decoding file resources...");
    +        for (ResEntry entry : new ArrayList<>(pkg.listEntries())) {
                 if (entry.getValue() instanceof ResFileReference) {
                     fileDecoder.decode(entry, inDir, outDir, mResFileMapping);
                 }
    -            // ResFileDecoder may have replaced an invalid file reference,
    -            // so we use "if" here rather than "else if".
    +        }
    +
    +        LOGGER.info("Generating values XMLs...");
    +        Map<ResType, List<ResEntry>> valuesEntries = new HashMap<>();
    +        for (ResEntry entry : pkg.listEntries()) {
                 if (entry.getValue() instanceof ValuesXmlSerializable) {
                     ResType type = entry.getType();
                     List<ResEntry> entries = valuesEntries.get(type);
    @@ -105,8 +115,6 @@ public void decodeResources(File apkDir) throws AndrolibException {
                     entries.add(entry);
                 }
             }
    -
    -        LOGGER.info("Decoding values */* XMLs...");
             for (Map.Entry<ResType, List<ResEntry>> entry : valuesEntries.entrySet()) {
                 generateValuesXml(pkg, entry.getKey(), entry.getValue(), outDir, serial);
             }
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResEntrySpec.java+26 14 modified
    @@ -16,17 +16,9 @@
      */
     package brut.androlib.res.table;
     
    -import com.google.common.collect.Sets;
    -
     import java.util.Objects;
    -import java.util.Set;
     
     public class ResEntrySpec {
    -    private static final Set<String> INVALID_ENTRY_NAMES = Sets.newHashSet(
    -        "0_resource_name_obfuscated", // #3067
    -        "(name removed)" // #2940
    -    );
    -
         public static final String DUMMY_PREFIX = "APKTOOL_DUMMY_";
         public static final String MISSING_PREFIX = "APKTOOL_MISSING_";
         public static final String RENAMED_PREFIX = "APKTOOL_RENAMED_";
    @@ -40,13 +32,33 @@ public ResEntrySpec(ResTypeSpec typeSpec, ResId id, String name) {
             assert typeSpec.getId() == id.getTypeId();
             mTypeSpec = typeSpec;
             mId = id;
    -        // Some apps had their entry names collapsed to a single value in
    -        // the key string pool. Rename to avoid duplicates.
    -        if (name == null || name.isEmpty() || INVALID_ENTRY_NAMES.contains(name)) {
    -            mName = RENAMED_PREFIX + id;
    -        } else {
    -            mName = name;
    +        // 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;
    +    }
    +
    +    private static boolean isValidEntryName(String name) {
    +        // Must not be empty.
    +        int len = name.length();
    +        if (len == 0) {
    +            return false;
    +        }
    +        // Must start with a valid Java identifier start character.
    +        if (!Character.isJavaIdentifierStart(name.charAt(0))) {
    +            return false;
    +        }
    +        // The rest must be valid Java identifier part characters.
    +        for (int i = 1; i < len; i++) {
    +            char ch = name.charAt(i);
    +            // Whitelisted special characters.
    +            if (ch == '.' || ch == '-') {
    +                continue;
    +            }
    +            if (!Character.isJavaIdentifierPart(ch)) {
    +                return false;
    +            }
             }
    +        return true;
         }
     
         public ResTypeSpec getTypeSpec() {
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResAttribute.java+47 1 modified
    @@ -16,6 +16,7 @@
      */
     package brut.androlib.res.table.value;
     
    +import android.util.TypedValue;
     import brut.androlib.exceptions.AndrolibException;
     import brut.androlib.res.table.ResEntry;
     import brut.androlib.res.table.ResId;
    @@ -62,7 +63,7 @@ public class ResAttribute extends ResBag implements ValuesXmlSerializable {
         public static final ResAttribute DEFAULT = new ResAttribute(
             null, TYPE_ANY, Integer.MIN_VALUE, Integer.MAX_VALUE, L10N_NOT_REQUIRED);
     
    -    protected final int mType;
    +    protected int mType;
         protected final int mMin;
         protected final int mMax;
         protected final int mL10n;
    @@ -149,6 +150,51 @@ public ResPrimitive getValue() {
             }
         }
     
    +    public void addType(int type) {
    +        if ((mType & TYPE_ANY) == TYPE_ANY) {
    +            return;
    +        }
    +        switch (type) {
    +            case TypedValue.TYPE_REFERENCE:
    +            case TypedValue.TYPE_DYNAMIC_REFERENCE:
    +            case TypedValue.TYPE_ATTRIBUTE:
    +            case TypedValue.TYPE_DYNAMIC_ATTRIBUTE:
    +                mType |= TYPE_REFERENCE;
    +                return;
    +            case TypedValue.TYPE_STRING:
    +                mType |= TYPE_STRING;
    +                return;
    +            case TypedValue.TYPE_FLOAT:
    +                mType |= TYPE_FLOAT;
    +                return;
    +            case TypedValue.TYPE_DIMENSION:
    +                mType |= TYPE_DIMEN;
    +                return;
    +            case TypedValue.TYPE_FRACTION:
    +                mType |= TYPE_FRACTION;
    +                return;
    +            case TypedValue.TYPE_INT_BOOLEAN:
    +                mType |= TYPE_BOOL;
    +                return;
    +            default:
    +                if (type >= TypedValue.TYPE_FIRST_COLOR_INT && type <= TypedValue.TYPE_LAST_COLOR_INT) {
    +                    mType |= TYPE_COLOR;
    +                } else if (type >= TypedValue.TYPE_FIRST_INT && type <= TypedValue.TYPE_LAST_INT) {
    +                    mType |= TYPE_INT;
    +                }
    +                return;
    +        }
    +    }
    +
    +    public boolean hasSymbolsForValue(ResItem value) throws AndrolibException {
    +        return getSymbolsForValue(value) != null;
    +    }
    +
    +    protected Symbol[] getSymbolsForValue(ResItem value) throws AndrolibException {
    +        // Stub for attribute types with symbols.
    +        return null;
    +    }
    +
         public String formatValue(ResItem value, boolean asTextNode) throws AndrolibException {
             if (!(value instanceof ResXmlEncodable)) {
                 return null;
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResBag.java+6 0 modified
    @@ -16,6 +16,8 @@
      */
     package brut.androlib.res.table.value;
     
    +import brut.androlib.exceptions.AndrolibException;
    +
     import java.util.logging.Logger;
     
     public abstract class ResBag extends ResValue {
    @@ -61,4 +63,8 @@ public ResItem getValue() {
                 return mValue;
             }
         }
    +
    +    public void resolveKeys() throws AndrolibException {
    +        // Stub for bags with resolvable keys.
    +    }
     }
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResEnum.java+100 35 modified
    @@ -16,13 +16,18 @@
      */
     package brut.androlib.res.table.value;
     
    +import android.util.TypedValue;
     import brut.androlib.Config;
     import brut.androlib.exceptions.AndrolibException;
    +import brut.androlib.res.table.ResConfig;
     import brut.androlib.res.table.ResEntry;
     import brut.androlib.res.table.ResEntrySpec;
    +import brut.androlib.res.table.ResId;
    +import brut.androlib.res.table.ResPackage;
     import org.xmlpull.v1.XmlSerializer;
     
     import java.io.IOException;
    +import java.util.Arrays;
     import java.util.HashMap;
     import java.util.Map;
     import java.util.Objects;
    @@ -32,77 +37,137 @@ public class ResEnum extends ResAttribute {
         private static final Logger LOGGER = Logger.getLogger(ResEnum.class.getName());
     
         private final Symbol[] mSymbols;
    -    private final Map<Integer, String> mFormatCache;
    +    private Map<Integer, Symbol[]> mSymbolsCache;
    +    private Map<Integer, String> mFormatsCache;
     
         public ResEnum(ResReference parent, int type, int min, int max, int l10n, Symbol[] symbols) {
             super(parent, type, min, max, l10n);
             mSymbols = symbols;
    -        mFormatCache = new HashMap<>();
         }
     
         @Override
    -    protected String formatValueToSymbols(ResItem value) throws AndrolibException {
    -        if (!(value instanceof ResPrimitive)) {
    +    public void resolveKeys() throws AndrolibException {
    +        ResPackage pkg = mParent.getPackage();
    +        Config config = pkg.getTable().getConfig();
    +        boolean removeUnresolved = config.getDecodeResolve() == Config.DecodeResolve.REMOVE;
    +
    +        for (Symbol symbol : mSymbols) {
    +            ResReference key = symbol.getKey();
    +            if (key.resolve() != null) {
    +                continue;
    +            }
    +
    +            ResId entryId = key.getId();
    +
    +            // #2836 - Skip item if the resource cannot be identified.
    +            if (removeUnresolved || entryId.getPackageId() != pkg.getId()) {
    +                LOGGER.warning(String.format(
    +                    "null enum reference: key=%s, value=%s", key, symbol.getValue()));
    +                continue;
    +            }
    +
    +            pkg.addEntrySpec(entryId, ResEntrySpec.MISSING_PREFIX + entryId);
    +            pkg.addEntry(entryId, ResConfig.DEFAULT, new ResCustom("id"));
    +        }
    +    }
    +
    +    @Override
    +    protected Symbol[] getSymbolsForValue(ResItem value) throws AndrolibException {
    +        if (!isSymbolValueType(value)) {
                 return null;
             }
     
             int data = ((ResPrimitive) value).getData();
    -        String formatted = mFormatCache.get(data);
    -        if (formatted != null) {
    -            return formatted;
    +        return getSymbols(data);
    +    }
    +
    +    private boolean isSymbolValueType(ResItem value) throws AndrolibException {
    +        if (!(value instanceof ResPrimitive)) {
    +            return false;
             }
     
    +        int type = ((ResPrimitive) value).getType();
    +        return type == TypedValue.TYPE_INT_DEC || type == TypedValue.TYPE_INT_HEX;
    +    }
    +
    +    private Symbol[] getSymbols(int data) throws AndrolibException {
    +        if (mSymbolsCache == null) {
    +            // Lazily establish a symbols cache for performance.
    +            mSymbolsCache = new HashMap<>();
    +        } else if (mSymbolsCache.containsKey(data)) {
    +            return mSymbolsCache.get(data);
    +        }
    +
    +        Symbol[] symbols = new Symbol[mSymbols.length];
    +        int symbolsCount = 0;
    +
             for (Symbol symbol : mSymbols) {
    -            if (symbol.getValue().getData() != data) {
    -                continue;
    +            if (symbol.getValue().getData() == data) {
    +                symbols[symbolsCount++] = symbol;
                 }
    +        }
    +
    +        if (symbolsCount == 0) {
    +            symbols = null;
    +        } else if (symbolsCount < symbols.length) {
    +            symbols = Arrays.copyOf(symbols, symbolsCount);
    +        }
    +
    +        mSymbolsCache.put(data, symbols);
    +        return symbols;
    +    }
    +
    +    @Override
    +    protected String formatValueToSymbols(ResItem value) throws AndrolibException {
    +        if (!isSymbolValueType(value)) {
    +            return null;
    +        }
    +
    +        int data = ((ResPrimitive) value).getData();
    +        if (mFormatsCache == null) {
    +            // Lazily establish a formats cache for performance.
    +            mFormatsCache = new HashMap<>();
    +        } else if (mFormatsCache.containsKey(data)) {
    +            return mFormatsCache.get(data);
    +        }
    +
    +        Symbol[] symbols = getSymbols(data);
    +        String formatted = null;
    +
    +        if (symbols != null) {
    +            for (Symbol symbol : symbols) {
    +                ResEntrySpec keySpec = symbol.getKey().resolve();
    +                if (keySpec == null) {
    +                    continue;
    +                }
     
    -            ResReference key = symbol.getKey();
    -            ResEntrySpec keySpec = key.resolve();
    -            if (keySpec != null) {
                     formatted = keySpec.getName();
    -                mFormatCache.put(data, formatted);
     
                     // fill_parent is deprecated since API 8 but appears first.
                     // Keep looking for match_parent and use it instead if found.
                     if (data == -1 && formatted.equals("fill_parent")) {
                         continue;
                     }
    +                break;
                 }
    -            break;
             }
     
    +        mFormatsCache.put(data, formatted);
             return formatted;
         }
     
         @Override
         protected void serializeSymbolsToValuesXml(XmlSerializer serial, ResEntry entry)
                 throws AndrolibException, IOException {
    -        Config config = mParent.getPackage().getTable().getConfig();
    -        boolean removeUnresolved = config.getDecodeResolve() == Config.DecodeResolve.REMOVE;
    -
             for (Symbol symbol : mSymbols) {
    -            ResReference key = symbol.getKey();
    -            ResEntrySpec keySpec = key.resolve();
    -            ResPrimitive value = symbol.getValue();
    -
    -            String name;
    -            if (keySpec != null) {
    -                name = keySpec.getName();
    -            } else {
    -                // #2836 - Support skipping items if the resource cannot be identified.
    -                if (removeUnresolved) {
    -                    LOGGER.warning(String.format(
    -                        "null enum reference: key=%s, value=%s", key, value));
    -                    continue;
    -                }
    -
    -                name = ResEntrySpec.MISSING_PREFIX + key.getId();
    +            ResEntrySpec keySpec = symbol.getKey().resolve();
    +            if (keySpec == null) {
    +                continue;
                 }
     
                 serial.startTag(null, "enum");
    -            serial.attribute(null, "name", name);
    -            serial.attribute(null, "value", value.encodeAsResXmlAttrValue());
    +            serial.attribute(null, "name", keySpec.getName());
    +            serial.attribute(null, "value", symbol.getValue().encodeAsResXmlAttrValue());
                 serial.endTag(null, "enum");
             }
         }
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResFlags.java+136 101 modified
    @@ -16,10 +16,14 @@
      */
     package brut.androlib.res.table.value;
     
    +import android.util.TypedValue;
     import brut.androlib.Config;
     import brut.androlib.exceptions.AndrolibException;
    +import brut.androlib.res.table.ResConfig;
     import brut.androlib.res.table.ResEntry;
     import brut.androlib.res.table.ResEntrySpec;
    +import brut.androlib.res.table.ResId;
    +import brut.androlib.res.table.ResPackage;
     import org.xmlpull.v1.XmlSerializer;
     
     import java.io.IOException;
    @@ -34,166 +38,197 @@ public class ResFlags extends ResAttribute {
         private static final Logger LOGGER = Logger.getLogger(ResFlags.class.getName());
     
         private final Symbol[] mSymbols;
    -    private final Symbol[] mZeroFlags;
    -    private final Symbol[] mFlags;
    -    private final Map<Integer, String> mFormatCache;
    +    private Map<Integer, Symbol[]> mSymbolsCache;
    +    private Map<Integer, String> mFormatsCache;
    +    private Symbol[] mSortedSymbols;
     
         public ResFlags(ResReference parent, int type, int min, int max, int l10n, Symbol[] symbols) {
             super(parent, type, min, max, l10n);
             mSymbols = symbols;
    -        mFormatCache = new HashMap<>();
    +    }
    +
    +    @Override
    +    public void resolveKeys() throws AndrolibException {
    +        ResPackage pkg = mParent.getPackage();
    +        Config config = pkg.getTable().getConfig();
    +        boolean removeUnresolved = config.getDecodeResolve() == Config.DecodeResolve.REMOVE;
     
    -        Symbol[] zeroFlags = new Symbol[symbols.length];
    -        int zeroFlagsCount = 0;
    -        Symbol[] flags = new Symbol[symbols.length];
    -        int flagsCount = 0;
    +        for (Symbol symbol : mSymbols) {
    +            ResReference key = symbol.getKey();
    +            if (key.resolve() != null) {
    +                continue;
    +            }
     
    -        for (Symbol symbol : symbols) {
    -            ResPrimitive value = symbol.getValue();
    +            ResId entryId = key.getId();
     
    -            if (value.getData() == 0) {
    -                zeroFlags[zeroFlagsCount++] = symbol;
    -            } else {
    -                flags[flagsCount++] = symbol;
    +            // #2836 - Skip item if the resource cannot be identified.
    +            if (removeUnresolved || entryId.getPackageId() != pkg.getId()) {
    +                LOGGER.warning(String.format(
    +                    "null flag reference: key=%s, value=%s", key, symbol.getValue()));
    +                continue;
                 }
    -        }
     
    -        mZeroFlags = zeroFlagsCount < zeroFlags.length
    -            ? Arrays.copyOf(zeroFlags, zeroFlagsCount) : zeroFlags;
    -        mFlags = flagsCount < flags.length ? Arrays.copyOf(flags, flagsCount) : flags;
    -
    -        // We establish a priority list for the flags. This can never be completely
    -        // accurate to the source, but it's a best-guess approach.
    -        Comparator<Symbol> byBitCount = Comparator.comparingInt(
    -            (Symbol symbol) -> Integer.bitCount(symbol.getValue().getData()));
    -        Comparator<Symbol> byRawValue = Comparator.comparingInt(
    -            (Symbol symbol) -> symbol.getValue().getData());
    -        Arrays.sort(mFlags, byBitCount.reversed().thenComparing(byRawValue));
    +            pkg.addEntrySpec(entryId, ResEntrySpec.MISSING_PREFIX + entryId);
    +            pkg.addEntry(entryId, ResConfig.DEFAULT, new ResCustom("id"));
    +        }
         }
     
         @Override
    -    protected String formatValueToSymbols(ResItem value) throws AndrolibException {
    -        if (!(value instanceof ResPrimitive)) {
    +    protected Symbol[] getSymbolsForValue(ResItem value) throws AndrolibException {
    +        if (!isSymbolValueType(value)) {
                 return null;
             }
     
             int data = ((ResPrimitive) value).getData();
    -        String formatted = mFormatCache.get(data);
    -        if (formatted != null) {
    -            return formatted;
    +        return getSymbols(data);
    +    }
    +
    +    private boolean isSymbolValueType(ResItem value) throws AndrolibException {
    +        if (!(value instanceof ResPrimitive)) {
    +            return false;
             }
     
    -        Symbol[] symbols;
    -        int count = 0;
    -        if (data == 0) {
    -            symbols = mZeroFlags;
    -            count = symbols.length;
    +        int type = ((ResPrimitive) value).getType();
    +        return type == TypedValue.TYPE_INT_DEC || type == TypedValue.TYPE_INT_HEX;
    +    }
     
    -            if (count == 0) {
    -                return null;
    +    private Symbol[] getSymbols(int data) throws AndrolibException {
    +        if (mSymbolsCache == null) {
    +            // Lazily establish a symbols cache for performance.
    +            mSymbolsCache = new HashMap<>();
    +        } else if (mSymbolsCache.containsKey(data)) {
    +            return mSymbolsCache.get(data);
    +        }
    +
    +        if (mSortedSymbols == null) {
    +            // Lazily establish a priority list for the flags. This can never be
    +            // completely accurate to the source, but it's a best-effort approach.
    +            mSortedSymbols = mSymbols.clone();
    +            Comparator<Symbol> byBitCount = Comparator.comparingInt(
    +                (Symbol symbol) -> Integer.bitCount(symbol.getValue().getData()));
    +            Comparator<Symbol> byRawValue = Comparator.comparingInt(
    +                (Symbol symbol) -> symbol.getValue().getData());
    +            Arrays.sort(mSortedSymbols, byBitCount.reversed().thenComparing(byRawValue));
    +        }
    +
    +        Symbol[] symbols = new Symbol[mSortedSymbols.length];
    +        int symbolsCount = 0;
    +
    +        if (data == 0) {
    +            for (Symbol symbol : mSortedSymbols) {
    +                if (symbol.getValue().getData() == 0) {
    +                    symbols[symbolsCount++] = symbol;
    +                }
                 }
             } else {
    -            symbols = new Symbol[mFlags.length];
                 int mask = 0;
     
    -            for (Symbol symbol : mFlags) {
    +            for (Symbol symbol : mSortedSymbols) {
                     int flag = symbol.getValue().getData();
    -
                     if ((data & flag) != flag || (mask & flag) == flag) {
                         continue;
                     }
     
    -                symbols[count++] = symbol;
    +                symbols[symbolsCount++] = symbol;
                     mask |= flag;
     
                     if (mask == data) {
                         break;
                     }
                 }
     
    -            if (count == 0) {
    -                LOGGER.warning("Invalid flags value: " + value);
    -                return null;
    -            }
    -        }
    +            // Filter out redundant flags.
    +            if (symbolsCount > 2) {
    +                Symbol[] filtered = new Symbol[symbolsCount];
    +                int filteredCount = 0;
     
    -        // Render the flags as a format.
    -        Config config = mParent.getPackage().getTable().getConfig();
    -        boolean removeUnresolved = config.getDecodeResolve() == Config.DecodeResolve.REMOVE;
    -        StringBuilder sb = new StringBuilder();
    -        for (int i = 0; i < count; i++) {
    -            Symbol symbol = symbols[i];
    +                for (int i = 0; i < symbolsCount; i++) {
    +                    Symbol symbol = symbols[i];
    +                    mask = 0;
     
    -            // Filter out redundant flags.
    -            if (count > 2) {
    -                int mask = 0;
    +                    // Combine the other flags.
    +                    for (int j = 0; j < symbolsCount; j++) {
    +                        Symbol other = symbols[j];
     
    -                // Combine the other flags.
    -                for (int j = 0; j < count; j++) {
    -                    Symbol other = symbols[j];
    +                        if (j != i) {
    +                            mask |= other.getValue().getData();
    +                        }
    +                    }
     
    -                    if (j != i) {
    -                        mask |= other.getValue().getData();
    +                    // Skip if it doesn't add at least one unique bit.
    +                    if ((symbol.getValue().getData() & ~mask) == 0) {
    +                        continue;
                         }
    -                }
     
    -                // Skip if it doesn't add at least one unique bit.
    -                if ((symbol.getValue().getData() & ~mask) == 0) {
    -                    continue;
    +                    filtered[filteredCount++] = symbol;
                     }
    -            }
     
    -            // Append the flag to the format.
    -            ResReference key = symbol.getKey();
    -            ResEntrySpec keySpec = key.resolve();
    -            if (sb.length() > 0) {
    -                sb.append('|');
    +                symbols = filtered;
    +                symbolsCount = filteredCount;
                 }
    -            if (keySpec != null) {
    -                sb.append(keySpec.getName());
    -            } else {
    -                // #2836 - Support skipping items if the resource cannot be identified.
    -                if (removeUnresolved) {
    -                    return null;
    +        }
    +
    +        if (symbolsCount == 0) {
    +            symbols = null;
    +        } else if (symbolsCount < symbols.length) {
    +            symbols = Arrays.copyOf(symbols, symbolsCount);
    +        }
    +
    +        mSymbolsCache.put(data, symbols);
    +        return symbols;
    +    }
    +
    +    @Override
    +    protected String formatValueToSymbols(ResItem value) throws AndrolibException {
    +        if (!isSymbolValueType(value)) {
    +            return null;
    +        }
    +
    +        int data = ((ResPrimitive) value).getData();
    +        if (mFormatsCache == null) {
    +            // Lazily establish a formats cache for performance.
    +            mFormatsCache = new HashMap<>();
    +        } else if (mFormatsCache.containsKey(data)) {
    +            return mFormatsCache.get(data);
    +        }
    +
    +        Symbol[] symbols = getSymbols(data);
    +        String formatted = null;
    +
    +        if (symbols != null) {
    +            StringBuilder sb = new StringBuilder();
    +
    +            for (Symbol symbol : symbols) {
    +                ResEntrySpec keySpec = symbol.getKey().resolve();
    +                if (keySpec == null) {
    +                    continue;
                     }
     
    -                sb.append(ResEntrySpec.MISSING_PREFIX + key.getId());
    +                if (sb.length() > 0) {
    +                    sb.append('|');
    +                }
    +                sb.append(keySpec.getName());
                 }
    +
    +            formatted = sb.toString();
             }
     
    -        formatted = sb.toString();
    -        mFormatCache.put(data, formatted);
    +        mFormatsCache.put(data, formatted);
             return formatted;
         }
     
         @Override
         protected void serializeSymbolsToValuesXml(XmlSerializer serial, ResEntry entry)
                 throws AndrolibException, IOException {
    -        Config config = mParent.getPackage().getTable().getConfig();
    -        boolean removeUnresolved = config.getDecodeResolve() == Config.DecodeResolve.REMOVE;
    -
             for (Symbol symbol : mSymbols) {
    -            ResReference key = symbol.getKey();
    -            ResEntrySpec keySpec = key.resolve();
    -            ResPrimitive value = symbol.getValue();
    -
    -            String name;
    -            if (keySpec != null) {
    -                name = keySpec.getName();
    -            } else {
    -                // #2836 - Support skipping items if the resource cannot be identified.
    -                if (removeUnresolved) {
    -                    LOGGER.warning(String.format(
    -                        "null flag reference: key=%s, value=%s", key, value));
    -                    continue;
    -                }
    -
    -                name = ResEntrySpec.MISSING_PREFIX + key.getId();
    +            ResEntrySpec keySpec = symbol.getKey().resolve();
    +            if (keySpec == null) {
    +                continue;
                 }
     
                 serial.startTag(null, "flag");
    -            serial.attribute(null, "name", name);
    -            serial.attribute(null, "value", value.encodeAsResXmlAttrValue());
    +            serial.attribute(null, "name", keySpec.getName());
    +            serial.attribute(null, "value", symbol.getValue().encodeAsResXmlAttrValue());
                 serial.endTag(null, "flag");
             }
         }
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResItem.java+2 2 modified
    @@ -41,8 +41,6 @@ public abstract class ResItem extends ResValue {
             STANDARD_TYPE_FORMATS.put("string", Sets.newHashSet("string"));
         }
     
    -    public abstract String getFormat();
    -
         public static ResItem parse(ResPackage pkg, int type, int data, String rawValue) {
             switch (type) {
                 case TypedValue.TYPE_NULL:
    @@ -72,4 +70,6 @@ public static ResItem parse(ResPackage pkg, int type, int data, String rawValue)
                     return null;
             }
         }
    +
    +    public abstract String getFormat();
     }
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResPlural.java+1 2 modified
    @@ -56,8 +56,6 @@ public void serializeToValuesXml(XmlSerializer serial, ResEntry entry)
     
             for (RawItem item : mItems) {
                 int key = item.getKey();
    -            ResItem value = item.getValue();
    -
                 String quantity;
                 switch (key) {
                     case ATTR_OTHER:
    @@ -83,6 +81,7 @@ public void serializeToValuesXml(XmlSerializer serial, ResEntry entry)
                         continue;
                 }
     
    +            ResItem value = item.getValue();
                 String body;
                 if (value instanceof ResString) {
                     body = ResXmlEncoders.enumerateNonPositionalSubstitutionsIfRequired(
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResStyle.java+53 26 modified
    @@ -19,6 +19,7 @@
     import brut.androlib.Config;
     import brut.androlib.exceptions.AndrolibException;
     import brut.androlib.exceptions.UndefinedResObjectException;
    +import brut.androlib.res.table.ResConfig;
     import brut.androlib.res.table.ResEntry;
     import brut.androlib.res.table.ResEntrySpec;
     import brut.androlib.res.table.ResId;
    @@ -78,12 +79,42 @@ public ResItem getValue() {
             }
         }
     
    +    @Override
    +    public void resolveKeys() throws AndrolibException {
    +        ResPackage pkg = mParent.getPackage();
    +        Config config = pkg.getTable().getConfig();
    +        boolean removeUnresolved = config.getDecodeResolve() == Config.DecodeResolve.REMOVE;
    +
    +        for (Item item : mItems) {
    +            ResReference key = item.getKey();
    +            ResEntrySpec keySpec = key.resolve();
    +            if (keySpec != null) {
    +                try {
    +                    keySpec.getPackage().getDefaultEntry(keySpec.getId());
    +                    continue;
    +                } catch (UndefinedResObjectException ignored) {
    +                }
    +            }
    +
    +            ResId entryId = key.getId();
    +
    +            // #2836 - Skip item if the resource cannot be identified.
    +            if (removeUnresolved || entryId.getPackageId() != pkg.getId()) {
    +                LOGGER.warning(String.format(
    +                    "null style reference: key=%s, value=%s", key, item.getValue()));
    +                continue;
    +            }
    +
    +            if (keySpec == null) {
    +                pkg.addEntrySpec(entryId, ResEntrySpec.MISSING_PREFIX + entryId);
    +            }
    +            pkg.addEntry(entryId, ResConfig.DEFAULT, ResAttribute.DEFAULT);
    +        }
    +    }
    +
         @Override
         public void serializeToValuesXml(XmlSerializer serial, ResEntry entry)
                 throws AndrolibException, IOException {
    -        Config config = mParent.getPackage().getTable().getConfig();
    -        boolean skipDuplicates = !config.isAnalysisMode();
    -
             serial.startTag(null, "style");
             serial.attribute(null, "name", entry.getName());
             if (mParent.resolve() != null) {
    @@ -92,52 +123,48 @@ public void serializeToValuesXml(XmlSerializer serial, ResEntry entry)
                 serial.attribute(null, "parent", "");
             }
     
    +        Config config = mParent.getPackage().getTable().getConfig();
    +        boolean skipDuplicates = !config.isAnalysisMode();
             Set<String> processedNames = new HashSet<>();
             for (Item item : mItems) {
    -            ResReference key = item.getKey();
    -            ResEntrySpec keySpec = key.resolve();
    -            ResItem value = item.getValue();
    -
    -            // #2836 - Support skipping items if the resource cannot be identified.
    +            ResEntrySpec keySpec = item.getKey().resolve();
                 if (keySpec == null) {
    -                LOGGER.warning(String.format("null style reference: key=%s, value=%s", key, value));
                     continue;
                 }
     
    -            String name = keySpec.getFullName(entry.getPackage(), true);
    +            String keyName = keySpec.getFullName(entry.getPackage(), true);
     
                 // #3400 - Skip duplicate items in styles.
    -            if (skipDuplicates && processedNames.contains(name)) {
    +            if (skipDuplicates && processedNames.contains(keyName)) {
                     continue;
                 }
     
    -            String body = null;
    +            // We need the attribute entry's value to format the item's value.
    +            ResValue keyValue;
                 try {
    -                // We need the attribute entry's value to format the item's value.
    -                ResValue keyDefValue =
    -                    keySpec.getPackage().getDefaultEntry(keySpec.getId()).getValue();
    -
    -                if (keyDefValue instanceof ResAttribute) {
    -                    body = ((ResAttribute) keyDefValue).formatValue(value, true);
    -                } else {
    -                    LOGGER.warning("Unexpected style item key: " + keySpec);
    -                }
    +                keyValue = keySpec.getPackage().getDefaultEntry(keySpec.getId()).getValue();
                 } catch (UndefinedResObjectException ignored) {
    +                continue;
    +            }
    +
    +            ResItem value = item.getValue();
    +            String body = null;
    +            if (keyValue instanceof ResAttribute) {
    +                body = ((ResAttribute) keyValue).formatValue(value, true);
    +            } else {
    +                LOGGER.warning("Unexpected style item key: " + keySpec);
                 }
                 if (body == null) {
                     // Fall back to default attribute.
                     body = ResAttribute.DEFAULT.formatValue(value, true);
                 }
    -            if (body == null) {
    -                continue;
    -            }
     
                 serial.startTag(null, "item");
    -            serial.attribute(null, "name", name);
    +            serial.attribute(null, "name", keyName);
                 serial.text(body);
                 serial.endTag(null, "item");
     
    -            processedNames.add(name);
    +            processedNames.add(keyName);
             }
     
             serial.endTag(null, "style");
    
  • brut.apktool/apktool-lib/src/main/java/brut/androlib/res/xml/ResXmlEncoders.java+13 13 modified
    @@ -91,7 +91,8 @@ public static String encodeAsXmlValue(String str) {
                 return str;
             }
     
    -        StringBuilder sb = new StringBuilder(str.length() + 10);
    +        int len = str.length();
    +        StringBuilder sb = new StringBuilder(len + 10);
     
             switch (str.charAt(0)) {
                 case '#':
    @@ -105,7 +106,7 @@ public static String encodeAsXmlValue(String str) {
             int startPos = 0;
             boolean enclose = false;
             boolean wasSpace = true;
    -        for (int i = 0; i < str.length(); i++) {
    +        for (int i = 0; i < len; i++) {
                 char ch = str.charAt(i);
                 if (isInStyleTag) {
                     if (ch == '>') {
    @@ -140,7 +141,7 @@ public static String encodeAsXmlValue(String str) {
                                 break;
                             }
                             // Skip writing trailing \u0000 if we are at end of string.
    -                        if ((sb.length() + 1) == str.length() && ch == '\u0000') {
    +                        if ((sb.length() + 1) == len && ch == '\u0000') {
                                 continue;
                             }
                             sb.append(String.format("\\u%04x", (int) ch));
    @@ -161,7 +162,8 @@ public static String encodeAsResXmlAttrValue(String str) {
                 return str;
             }
     
    -        StringBuilder sb = new StringBuilder(str.length() + 10);
    +        int len = str.length();
    +        StringBuilder sb = new StringBuilder(len + 10);
     
             switch (str.charAt(0)) {
                 case '#':
    @@ -171,7 +173,7 @@ public static String encodeAsResXmlAttrValue(String str) {
                     break;
             }
     
    -        for (int i = 0; i < str.length(); i++) {
    +        for (int i = 0; i < len; i++) {
                 char ch = str.charAt(i);
                 switch (ch) {
                     case '\\':
    @@ -239,29 +241,27 @@ private static Pair<List<Integer>, List<Integer>> findSubstitutions(String str,
             if (nonPosMax == -1) {
                 nonPosMax = Integer.MAX_VALUE;
             }
    -        int pos;
    -        int pos2 = 0;
    +
             List<Integer> nonPositional = new ArrayList<>();
             List<Integer> positional = new ArrayList<>();
    -
             if (str == null) {
                 return Pair.of(nonPositional, positional);
             }
     
    -        int length = str.length();
    -
    +        int len = str.length();
    +        int pos, pos2 = 0;
             while ((pos = str.indexOf('%', pos2)) != -1) {
                 pos2 = pos + 1;
    -            if (pos2 == length) {
    +            if (pos2 == len) {
                     nonPositional.add(pos);
                     break;
                 }
                 char ch = str.charAt(pos2++);
                 if (ch == '%') {
                     continue;
                 }
    -            if (ch >= '0' && ch <= '9' && pos2 < length) {
    -                while ((ch = str.charAt(pos2++)) >= '0' && ch <= '9' && pos2 < length);
    +            if (ch >= '0' && ch <= '9' && pos2 < len) {
    +                while ((ch = str.charAt(pos2++)) >= '0' && ch <= '9' && pos2 < len);
                     if (ch == '$') {
                         positional.add(pos);
                         continue;
    
  • brut.apktool/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeApkTest.java+1 1 modified
    @@ -438,7 +438,7 @@ public void drawableXhdpiTest() throws BrutException {
     
         @Test
         public void ninePatchImageColorTest() throws IOException {
    -        String fileName = "res/drawable-xhdpi/9patch.9.png";
    +        String fileName = "res/drawable-xhdpi/ninepatch.9.png";
     
             File control = new File(sTestOrigDir, fileName);
             File test = new File(sTestNewDir, fileName);
    
  • brut.apktool/apktool-lib/src/test/java/brut/androlib/DecodeResolveTest.java+18 18 modified
    @@ -58,47 +58,47 @@ public void decodeResolveRemoveTest() throws BrutException {
         }
     
         @Test
    -    public void decodeResolveDummyTest() throws BrutException {
    -        sConfig.setDecodeResolve(Config.DecodeResolve.DUMMY);
    +    public void decodeResolveKeepTest() throws BrutException {
    +        sConfig.setDecodeResolve(Config.DecodeResolve.KEEP);
     
             ExtFile testApk = new ExtFile(sTmpDir, TEST_APK);
    -        ExtFile testDir = new ExtFile(testApk + ".out.dummies");
    +        ExtFile testDir = new ExtFile(testApk + ".out.leave");
             new ApkDecoder(testApk, sConfig).decode(testDir);
     
             assertTrue(new File(testDir, "res/values/strings.xml").isFile());
     
    -        File attrXml = new File(testDir, "res/values/attrs.xml");
    -        Document attrDocument = loadDocument(attrXml);
    +        Document attrDocument = loadDocument(new File(testDir, "res/values/attrs.xml"));
             assertEquals(4, attrDocument.getElementsByTagName("enum").getLength());
     
    -        File colorXml = new File(testDir, "res/values/colors.xml");
    -        Document colorDocument = loadDocument(colorXml);
    +        Document colorDocument = loadDocument(new File(testDir, "res/values/colors.xml"));
             assertEquals(8, colorDocument.getElementsByTagName("color").getLength());
    -        assertEquals(1, colorDocument.getElementsByTagName("item").getLength());
    +        assertEquals(0, colorDocument.getElementsByTagName("item").getLength());
     
    -        File publicXml = new File(testDir, "res/values/public.xml");
    -        Document publicDocument = loadDocument(publicXml);
    +        Document publicDocument = loadDocument(new File(testDir, "res/values/public.xml"));
             assertEquals(22, publicDocument.getElementsByTagName("public").getLength());
         }
     
         @Test
    -    public void decodeResolveKeepTest() throws BrutException {
    -        sConfig.setDecodeResolve(Config.DecodeResolve.KEEP);
    +    public void decodeResolveDummyTest() throws BrutException {
    +        sConfig.setDecodeResolve(Config.DecodeResolve.DUMMY);
     
             ExtFile testApk = new ExtFile(sTmpDir, TEST_APK);
    -        ExtFile testDir = new ExtFile(testApk + ".out.leave");
    +        ExtFile testDir = new ExtFile(testApk + ".out.dummies");
             new ApkDecoder(testApk, sConfig).decode(testDir);
     
             assertTrue(new File(testDir, "res/values/strings.xml").isFile());
     
    -        Document attrDocument = loadDocument(new File(testDir, "res/values/attrs.xml"));
    +        File attrXml = new File(testDir, "res/values/attrs.xml");
    +        Document attrDocument = loadDocument(attrXml);
             assertEquals(4, attrDocument.getElementsByTagName("enum").getLength());
     
    -        Document colorDocument = loadDocument(new File(testDir, "res/values/colors.xml"));
    +        File colorXml = new File(testDir, "res/values/colors.xml");
    +        Document colorDocument = loadDocument(colorXml);
             assertEquals(8, colorDocument.getElementsByTagName("color").getLength());
    -        assertEquals(0, colorDocument.getElementsByTagName("item").getLength());
    +        assertEquals(1, colorDocument.getElementsByTagName("item").getLength());
     
    -        Document publicDocument = loadDocument(new File(testDir, "res/values/public.xml"));
    -        assertEquals(21, publicDocument.getElementsByTagName("public").getLength());
    +        File publicXml = new File(testDir, "res/values/public.xml");
    +        Document publicDocument = loadDocument(publicXml);
    +        assertEquals(23, publicDocument.getElementsByTagName("public").getLength());
         }
     }
    
  • brut.apktool/apktool-lib/src/test/java/brut/androlib/ResourceDirectoryTraversalTest.java+3 2 modified
    @@ -16,6 +16,7 @@
      */
     package brut.androlib;
     
    +import brut.androlib.res.table.ResEntrySpec;
     import brut.common.BrutException;
     import brut.directory.ExtFile;
     
    @@ -33,13 +34,13 @@ public static void beforeClass() throws Exception {
         }
     
         @Test
    -    public void checkIfMaliciousRawFileIsDisassembledProperly() throws BrutException {
    +    public void checkIfMaliciousRawFileRenamed() throws BrutException {
             sConfig.setForced(true);
     
             ExtFile testApk = new ExtFile(sTmpDir, TEST_APK);
             ExtFile testDir = new ExtFile(testApk + ".out");
             new ApkDecoder(testApk, sConfig).decode(testDir);
     
    -        assertTrue(new File(testDir, "res/raw/poc").exists());
    +        assertTrue(new File(testDir, "res/raw/" + ResEntrySpec.RENAMED_PREFIX + "0x7f040000").exists());
         }
     }
    
  • brut.apktool/apktool-lib/src/test/resources/testapp/res/drawable-xhdpi/ninepatch.9.png+0 0 renamed
  • brut.j.util/src/main/java/brut/util/BrutIO.java+0 5 modified
    @@ -103,9 +103,4 @@ public static String sanitizePath(File baseDir, String path) throws InvalidPathE
     
             return basePath.relativize(resolvedPath).toString();
         }
    -
    -    public static boolean detectPossibleDirectoryTraversal(String path) {
    -        return path.contains("../") || path.contains("/..")
    -                || path.contains("..\\") || path.contains("\\..");
    -    }
     }
    
  • brut.j.xml/src/main/java/brut/xmlpull/MXSerializer.java+14 11 modified
    @@ -266,13 +266,13 @@ private void rebuildIndentationBuf() {
             }
             int bufPos = 0;
             if (writeLineSeparator) {
    -            for (int i = 0; i < lineSeparator.length(); i++) {
    +            for (int i = 0, n = lineSeparator.length(); i < n; i++) {
                     indentationBuf[bufPos++] = lineSeparator.charAt(i);
                 }
             }
             if (writeIndentation) {
                 for (int i = 0; i < maxIndentLevel; i++) {
    -                for (int j = 0; j < indentationString.length(); j++) {
    +                for (int j = 0, n = indentationString.length(); j < n; j++) {
                         indentationBuf[bufPos++] = indentationString.charAt(j);
                     }
                 }
    @@ -894,8 +894,9 @@ private void writeAttributeValue(String value) throws IOException {
                 return;
             }
             // .[&, < and " escaped],
    +        int len = value.length();
             int pos = 0;
    -        for (int i = 0; i < value.length(); i++) {
    +        for (int i = 0; i < len; i++) {
                 char ch = value.charAt(i);
                 if (ch == '&') {
                     if (i > pos) {
    @@ -930,8 +931,8 @@ private void writeAttributeValue(String value) throws IOException {
                         pos = i + 1;
                     } else {
                         if (TRACE_ESCAPING) {
    -                        System.err.println(getClass().getName() + " DEBUG ATTR value.len=" + value.length()
    -                                + " " + printable(value));
    +                        System.err.println(getClass().getName() + " DEBUG ATTR value.len=" + len + " "
    +                                + printable(value));
                         }
                         throw new IllegalStateException(
                                 "character " + printable(ch) + " (" + Integer.toString(ch) + ") is not allowed in output"
    @@ -951,8 +952,9 @@ private void writeElementContent(String text) throws IOException {
             }
     
             // escape '<', '&', ']]>', <32 if necessary
    +        int len = text.length();
             int pos = 0;
    -        for (int i = 0; i < text.length(); i++) {
    +        for (int i = 0; i < len; i++) {
                 // TODO: check if doing char[] text.getChars() would be faster than
                 // getCharAt(i) ...
                 char ch = text.charAt(i);
    @@ -964,7 +966,7 @@ private void writeElementContent(String text) throws IOException {
                     }
                 } else {
                     if (ch == '&') {
    -                    if (!(i < text.length() - 3 && text.charAt(i + 1) == 'l'
    +                    if (!(i < len - 3 && text.charAt(i + 1) == 'l'
                                 && text.charAt(i + 2) == 't' && text.charAt(i + 3) == ';')) {
                             if (i > pos) {
                                 write(text.substring(pos, i));
    @@ -990,8 +992,8 @@ private void writeElementContent(String text) throws IOException {
                             // fallthrough
                         } else {
                             if (TRACE_ESCAPING) {
    -                            System.err.println(getClass().getName() + " DEBUG TEXT value.len=" + text.length()
    -                                    + " " + printable(text));
    +                            System.err.println(getClass().getName() + " DEBUG TEXT value.len=" + len + " "
    +                                    + printable(text));
                             }
                             throw new IllegalStateException("character " + Integer.toString(ch)
                                     + " is not allowed in output" + getLocation()
    @@ -1066,9 +1068,10 @@ private static String printable(String str) {
             if (str == null) {
                 return "null";
             }
    -        StringBuffer retval = new StringBuffer(str.length() + 16);
    +        int len = str.length();
    +        StringBuffer retval = new StringBuffer(len + 16);
             retval.append('\'');
    -        for (int i = 0; i < str.length(); i++) {
    +        for (int i = 0; i < len; i++) {
                 addPrintable(retval, str.charAt(i));
             }
             retval.append('\'');
    
  • brut.j.yaml/src/main/java/brut/yaml/YamlLine.java+2 1 modified
    @@ -39,7 +39,8 @@ public YamlLine(String line) {
                 return;
             }
             // count indent - space only
    -        for (int i = 0; i < line.length(); i++) {
    +        int len = line.length();
    +        for (int i = 0; i < len; i++) {
                 if (line.charAt(i) == ' ') {
                     indent++;
                 } else {
    
  • brut.j.yaml/src/main/java/brut/yaml/YamlStringEscapeUtils.java+2 3 modified
    @@ -63,9 +63,8 @@ private static void escapeJavaStyleString(Writer writer, String str) throws IOEx
             if (str == null) {
                 return;
             }
    -        int sz;
    -        sz = str.length();
    -        for (int i = 0; i < sz; i++) {
    +        int len = str.length();
    +        for (int i = 0; i < len; i++) {
                 char ch = str.charAt(i);
                 // "[^\t\n\r\u0020-\u007E\u0085\u00A0-\uD7FF\uE000-\uFFFD]"
                 // handle unicode
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.