diff --git a/build.gradle b/build.gradle index c58ae6fcc..2f80e0794 100644 --- a/build.gradle +++ b/build.gradle @@ -31,32 +31,6 @@ apply plugin: 'idea' version = '3.0.9' -def jcommanderVersion = '' - -if (!('release' in gradle.startParameter.taskNames)) { - // we compile against 1.48 normally, to match what's in AOSP, but switch to a newer version - // for release, because it has some fixes required when running on Android - jcommanderVersion = 'com.beust:jcommander:1.48' - - def versionSuffix - try { - def git = org.eclipse.jgit.api.Git.open(file('.')) - def head = git.getRepository().getRef('HEAD') - versionSuffix = head.getObjectId().abbreviate(8).name() - - if (!git.status().call().clean) { - versionSuffix += '-dirty' - } - } catch (Exception) { - // In case we can't get the commit for some reason, - // just use -dev - versionSuffix = 'dev' - } - - version += "-${versionSuffix}" -} else { - jcommanderVersion = 'com.beust:jcommander:1.64' -} // Note: please don't use this. This is strictly for the official releases // that are posted on, e.g. the bitbucket download page. @@ -108,7 +82,7 @@ subprojects { proguard_gradle: 'com.guardsquare:proguard-gradle:7.1.0', dx: 'com.google.android.tools:dx:1.7', gson: 'com.google.code.gson:gson:2.3.1', - jcommander: jcommanderVersion + jcommander: 'org.jcommander:jcommander:1.85' ] } diff --git a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/CDexBackedMethodImplementation.java b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/CDexBackedMethodImplementation.java index 99df58e1e..5167caee1 100644 --- a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/CDexBackedMethodImplementation.java +++ b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/CDexBackedMethodImplementation.java @@ -110,7 +110,7 @@ protected int getTriesSize() { } @Override - protected int getDebugOffset() { + public int getDebugOffset() { CDexBackedDexFile cdexFile = ((CDexBackedDexFile) dexFile); int debugTableItemOffset = (method.methodIndex / 16) * 4; diff --git a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/DexBackedDexFile.java b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/DexBackedDexFile.java index b3d24bc9e..4772f3063 100644 --- a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/DexBackedDexFile.java +++ b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/DexBackedDexFile.java @@ -104,13 +104,14 @@ protected DexBackedDexFile(@Nullable Opcodes opcodes, boolean verifyMagic, int header_offset) { if (offset < 0 || offset > buf.length) { - throw new IndexOutOfBoundsException( + throw new DexUtil.InvalidFile( String.format("Invalid offset %d for buffer of length %d", offset, buf.length)); } if (header_offset < 0 || buf.length - offset < header_offset) { - throw new IndexOutOfBoundsException(String.format( - "Invalid header_offset %d for buffer of length %d at offset %d", - header_offset, buf.length, offset)); + throw new DexUtil.InvalidFile( + String.format( + "Invalid header_offset %d for buffer of length %d at offset %d", + header_offset, buf.length, offset)); } dexBuffer = new DexBuffer(buf, offset); @@ -179,6 +180,16 @@ protected DexBackedDexFile(@Nullable Opcodes opcodes, if (container_off != header_offset) { throw new DexUtil.InvalidFile(String.format("Unexpected container offset in header")); } + + if (mapOffset < 0 || mapOffset > containerSize - 4) { + throw new DexUtil.InvalidFile(String.format("Invalid mapOffset %d", mapOffset)); + } + + // Eagerly read mapSize to validate map bounds + int mapSize = dexBuffer.readSmallUint(mapOffset); + if (mapOffset + 4 + mapSize * MapItem.ITEM_SIZE > containerSize) { + throw new DexUtil.InvalidFile("Map extends beyond container bounds"); + } } /** diff --git a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/DexBackedMethodImplementation.java b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/DexBackedMethodImplementation.java index a231cfb2b..497511063 100644 --- a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/DexBackedMethodImplementation.java +++ b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/DexBackedMethodImplementation.java @@ -137,7 +137,7 @@ public int size() { return Collections.emptyList(); } - protected int getDebugOffset() { + public int getDebugOffset() { return dexFile.getDataBuffer().readInt(codeOffset + CodeItem.DEBUG_INFO_OFFSET); } diff --git a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/OatFile.java b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/OatFile.java index c16990eaa..3e0573210 100644 --- a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/OatFile.java +++ b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/OatFile.java @@ -330,18 +330,19 @@ public String getKeyValue(@Nonnull String key) { int keyEndOffset = offset; String k = new String(buf, keyStartOffset, keyEndOffset - keyStartOffset); + int valueStartOffset = ++offset; + while (offset < endOffset && buf[offset] != '\0') { + offset++; + } + if (offset >= endOffset) { + throw new InvalidOatFileException("Oat file contains truncated key value store"); + } + if (k.equals(key)) { - int valueStartOffset = ++offset; - while (offset < endOffset && buf[offset] != '\0') { - offset++; - } - if (offset >= endOffset) { - throw new InvalidOatFileException("Oat file contains truncated key value store"); - } - int valueEndOffset = offset; - return new String(buf, valueStartOffset, valueEndOffset - valueStartOffset); + return new String(buf, valueStartOffset, offset - valueStartOffset); } - offset++; + + offset++; // Move past the null terminator of the value } return null; } diff --git a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/ZipDexContainer.java b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/ZipDexContainer.java index 4d2454863..99e492b8c 100644 --- a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/ZipDexContainer.java +++ b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/dexbacked/ZipDexContainer.java @@ -31,7 +31,7 @@ package com.android.tools.smali.dexlib2.dexbacked; import com.android.tools.smali.dexlib2.Opcodes; -import com.android.tools.smali.dexlib2.iface.DexFile; +import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; import com.android.tools.smali.dexlib2.iface.MultiDexContainer; import com.android.tools.smali.dexlib2.util.DexUtil; import com.android.tools.smali.dexlib2.util.DexUtil.InvalidFile; @@ -76,6 +76,7 @@ public interface ZipDexEntry extends DexEntry { private final File zipFilePath; @Nullable private final Opcodes opcodes; + private final boolean ignoreInvalid; private Map entries; /** @@ -84,8 +85,20 @@ public interface ZipDexEntry extends DexEntry { * @param zipFilePath The path to the zip file */ public ZipDexContainer(@Nonnull File zipFilePath, @Nullable Opcodes opcodes) { + this(zipFilePath, opcodes, true); + } + + /** + * Constructs a new ZipDexContainer for the given zip file + * + * @param zipFilePath The path to the zip file + * @param ignoreInvalid If true, invalid dex entries will be silently ignored + */ + public ZipDexContainer( + @Nonnull File zipFilePath, @Nullable Opcodes opcodes, boolean ignoreInvalid) { this.zipFilePath = zipFilePath; this.opcodes = opcodes; + this.ignoreInvalid = ignoreInvalid; } /** @@ -144,40 +157,54 @@ private void loadDexFilesFromZipEntry(@Nonnull ZipFile zipFile, @Nonnull ZipEntr try (InputStream inputStream = zipFile.getInputStream(entry)) { byte[] buf = InputStreamUtil.toByteArray(inputStream); for (int offset = 0, i = 0; offset < buf.length; i++) { - DexBackedDexFile dex = new DexBackedDexFile(opcodes, buf, 0, true, offset); - boolean hasSingleEntry = dex.isDexContainerFirstEntry() && - dex.isDexContainerLastEntry(); - String entryName = entry.getName() + (hasSingleEntry ? "" : ("/" + i)); - ZipDexEntry dexEntry = new ZipDexEntry() { - @Nonnull - @Override - public String getEntryName() { - return entryName; - } - - @Nonnull - @Override - public DexBackedDexFile getDexFile() { - return dex; - } - - @Nonnull - @Override - public MultiDexContainer getContainer() { - return ZipDexContainer.this; - } - - @Override - public long getCrc() { - return entryCrc; - } - }; - entries.put(entryName, dexEntry); - offset += dex.getFileSize(); - if (dex.isDexContainerLastEntry()) { - break; - } - }; + final int headerOffset = offset; + boolean isLast; + int fileSize; + try { + DexBackedDexFile dex = new DexBackedDexFile(opcodes, buf, 0, true, headerOffset); + isLast = dex.isDexContainerLastEntry(); + fileSize = dex.getFileSize(); + } catch (NotADexFile | InvalidFile | UnsupportedFile ex) { + if (ignoreInvalid) { + return; + } + isLast = true; + fileSize = buf.length - offset; + } + final String entryName = entry.getName() + (i == 0 && isLast ? "" : ("/" + i)); + ZipDexEntry dexEntry = + new ZipDexEntry() { + @Nonnull + @Override + public String getEntryName() { + return entryName; + } + + // throws InvalidFile if the dex file has an invalid header. + // throws UnsupportedFile if the dex file version is not supported. + @Nonnull + @Override + public DexBackedDexFile getDexFile() { + return new DexBackedDexFile(opcodes, buf, 0, true, headerOffset); + } + + @Nonnull + @Override + public MultiDexContainer getContainer() { + return ZipDexContainer.this; + } + + @Override + public long getCrc() { + return entryCrc; + } + }; + entries.put(entryName, dexEntry); + offset += fileSize; + if (isLast) { + return; + } + } } } diff --git a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/writer/builder/BuilderClassDef.java b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/writer/builder/BuilderClassDef.java index b6d0c29af..ff4b6f5d3 100644 --- a/dexlib2/src/main/java/com/android/tools/smali/dexlib2/writer/builder/BuilderClassDef.java +++ b/dexlib2/src/main/java/com/android/tools/smali/dexlib2/writer/builder/BuilderClassDef.java @@ -80,10 +80,10 @@ public class BuilderClassDef extends BaseTypeReference implements ClassDef { methods = Collections.emptyList(); } if (staticFields == null) { - staticFields = Collections.emptySortedSet(); + staticFields = Collections.unmodifiableSortedSet(new java.util.TreeSet<>()); } if (instanceFields == null) { - instanceFields = Collections.emptySortedSet(); + instanceFields = Collections.unmodifiableSortedSet(new java.util.TreeSet<>()); } this.type = type; diff --git a/dexlib2/src/main/java/com/android/tools/smali/util/ImmutableConverter.java b/dexlib2/src/main/java/com/android/tools/smali/util/ImmutableConverter.java index 5d48d705a..1ff9be12d 100644 --- a/dexlib2/src/main/java/com/android/tools/smali/util/ImmutableConverter.java +++ b/dexlib2/src/main/java/com/android/tools/smali/util/ImmutableConverter.java @@ -117,7 +117,7 @@ public Set toSet(@Nullable final Iterable iterabl public SortedSet toSortedSet(@Nonnull Comparator comparator, @Nullable final Iterable iterable) { if (iterable == null) { - return Collections.emptySortedSet(); + return unmodifiableSortedSet(new java.util.TreeSet(comparator)); } boolean needsCopy = false; diff --git a/dexlib2/src/main/java/com/android/tools/smali/util/ImmutableUtils.java b/dexlib2/src/main/java/com/android/tools/smali/util/ImmutableUtils.java index 0107f856a..233ced416 100644 --- a/dexlib2/src/main/java/com/android/tools/smali/util/ImmutableUtils.java +++ b/dexlib2/src/main/java/com/android/tools/smali/util/ImmutableUtils.java @@ -58,7 +58,7 @@ public class ImmutableUtils { @Nonnull public static SortedSet nullToEmptySortedSet(@Nullable SortedSet set) { if (set == null) { - return Collections.emptySortedSet(); + return unmodifiableSortedSet(new java.util.TreeSet()); } return unmodifiableSortedSet(set); } diff --git a/dexlib2/src/main/java/com/android/tools/smali/util/StringUtils.java b/dexlib2/src/main/java/com/android/tools/smali/util/StringUtils.java index 6aa63a4b7..8e7d60085 100644 --- a/dexlib2/src/main/java/com/android/tools/smali/util/StringUtils.java +++ b/dexlib2/src/main/java/com/android/tools/smali/util/StringUtils.java @@ -166,7 +166,7 @@ public static String repeat(String string, int count) { } if (count <= 1) { - if (count >= 0) { + if (count < 0) { throw new IllegalArgumentException("invalid count: " + count); } return (count == 0) ? "" : string; diff --git a/dexlib2/src/test/java/com/android/tools/smali/util/StringUtilsTest.java b/dexlib2/src/test/java/com/android/tools/smali/util/StringUtilsTest.java new file mode 100644 index 000000000..8303b6662 --- /dev/null +++ b/dexlib2/src/test/java/com/android/tools/smali/util/StringUtilsTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2026, Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.tools.smali.util; + +import org.junit.Assert; +import org.junit.Test; + +public class StringUtilsTest { + @Test + public void testRepeat() { + Assert.assertEquals("", StringUtils.repeat("a", 0)); + Assert.assertEquals("a", StringUtils.repeat("a", 1)); + Assert.assertEquals("aa", StringUtils.repeat("a", 2)); + Assert.assertEquals("aaaaa", StringUtils.repeat("a", 5)); + + Assert.assertEquals("", StringUtils.repeat("", 0)); + Assert.assertEquals("", StringUtils.repeat("", 1)); + Assert.assertEquals("", StringUtils.repeat("", 5)); + } + + @Test(expected = IllegalArgumentException.class) + public void testRepeatNegative() { + StringUtils.repeat("a", -1); + } +} diff --git a/smali/run_antlr.sh b/smali/run_antlr.sh index 1b5f000cc..e7127fc74 100755 --- a/smali/run_antlr.sh +++ b/smali/run_antlr.sh @@ -46,6 +46,9 @@ if [[ -z "$ANTLR_JARS" ]]; then exit 1 fi +# cd to the script directory so we can use relative paths +cd "$SCRIPT_DIR" + function run_antlr() { CLASSPATH="$ANTLR_JARS" java 2>&1 "$ANTLR_MAIN" "$@" } @@ -100,12 +103,12 @@ function cleanup_tokens { [[ -f "$TOKEN_FILE" ]] && rm "$TOKEN_FILE" } -generate_file "$SCRIPT_DIR/src/main/java/com/android/tools/smali/smali/smaliParser.java" "$SCRIPT_DIR/src/main/antlr/smaliParser.g" -generate_file "$SCRIPT_DIR/src/main/java/com/android/tools/smali/smali/smaliTreeWalker.java" "$SCRIPT_DIR/src/main/antlr/smaliTreeWalker.g" +generate_file "src/main/java/com/android/tools/smali/smali/smaliParser.java" "src/main/antlr/smaliParser.g" +generate_file "src/main/java/com/android/tools/smali/smali/smaliTreeWalker.java" "src/main/antlr/smaliTreeWalker.g" # Clean up the tokens, no longer necessary once the tree walker is generated -cleanup_tokens "$SCRIPT_DIR/src/main/java/com/android/tools/smali/smali/smaliParser.java" -cleanup_tokens "$SCRIPT_DIR/src/main/java/com/android/tools/smali/smali/smaliTreeWalker.java" +cleanup_tokens "src/main/java/com/android/tools/smali/smali/smaliParser.java" +cleanup_tokens "src/main/java/com/android/tools/smali/smali/smaliTreeWalker.java" # Uncomment to run interactively #run_antlr "$@" \ No newline at end of file diff --git a/smali/run_jflex.sh b/smali/run_jflex.sh index 79f1acaf1..65c9cb8b5 100755 --- a/smali/run_jflex.sh +++ b/smali/run_jflex.sh @@ -18,7 +18,10 @@ EXPECTED_JFLEX_VERSION_STR="This is JFlex $EXPECTED_JFLEX_VERSION" SCRIPT_DIR=`dirname $0` echo $SCRIPT_DIR -TOP_PATH="$SCRIPT_DIR/../../.." +# cd to the script directory so we can use relative paths +cd "$SCRIPT_DIR" + +TOP_PATH="../../.." # Run the java jar when 'JFLEX' is not in the environment function exec_jar_jflex { @@ -52,10 +55,10 @@ then exit 1 fi -JAVA_FILE=$SCRIPT_DIR/src/main/java/com/android/tools/smali/smali/smaliFlexLexer.java +JAVA_FILE=src/main/java/com/android/tools/smali/smali/smaliFlexLexer.java rm -f "$JAVA_FILE" -"$JFLEX" --nobak -d "$SCRIPT_DIR/src/main/java/com/android/tools/smali/smali" "$SCRIPT_DIR/src/main/jflex/smaliLexer.jflex" +"$JFLEX" --nobak -d "src/main/java/com/android/tools/smali/smali" "src/main/jflex/smaliLexer.jflex" # delete trailing space from end of each line to make gerrit happy sed 's/[ ]*$//' "$JAVA_FILE" > "$JAVA_FILE.tmp" diff --git a/util/src/main/java/com/android/tools/smali/util/jcommander/HelpFormatter.java b/util/src/main/java/com/android/tools/smali/util/jcommander/HelpFormatter.java index 5b580dcfc..2d050f5c4 100644 --- a/util/src/main/java/com/android/tools/smali/util/jcommander/HelpFormatter.java +++ b/util/src/main/java/com/android/tools/smali/util/jcommander/HelpFormatter.java @@ -162,7 +162,7 @@ public String format(@Nonnull List commandHierarchy) { } if (leafJc.getMainParameter() != null) { - String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameter()); + String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameterValue()); if (argumentNames.length == 0) { writer.write(" "); } else { @@ -239,7 +239,7 @@ public String format(@Nonnull List commandHierarchy) { } if (leafJc.getMainParameter() != null) { - String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameter()); + String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameterValue()); writer.write("\n"); writer.indent(4); if (argumentNames.length > 0) { @@ -288,7 +288,7 @@ public String format(@Nonnull List commandHierarchy) { writer.write(")"); } - String commandDesc = leafJc.getCommandDescription(commandName); + String commandDesc = leafJc.getUsageFormatter().getCommandDescription(commandName); if (commandDesc != null) { writer.write(" - "); writer.write(commandDesc);