Reland "Allow unicode range 0x10000..0x10ffff in identifiers."
Which includes:
- Iterating by code points instead of by chars where we're checking
whether it's a valid Dex SimpleNameChar.
- Extending the InvalidMethodNames test with more thorough JVM and Art
testing and additional characters.
Bug:
Change-Id: I87b908d5ca0e8ce6f7ca673f8522b06ce1456d10
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index ad187ec..9036fd7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -25,7 +25,7 @@
public DexString(String string) {
this.size = string.length();
- this.content = encode(string);
+ this.content = encodeToMutf8(string);
}
@Override
@@ -118,7 +118,7 @@
}
// Inspired from /dex/src/main/java/com/android/dex/Mutf8.java
- private static byte[] encode(String string) {
+ public static byte[] encodeToMutf8(String string) {
byte[] result = new byte[countBytes(string)];
int offset = 0;
for (int i = 0; i < string.length(); i++) {
@@ -207,15 +207,12 @@
if (string.charAt(1) == '/' || string.charAt(string.length() - 2) == '/') {
return false;
}
- for (int i = 1; i < string.length() - 1; i++) {
- char ch = string.charAt(i);
- if (ch == '/') {
- continue;
+ int cp;
+ for (int i = 1; i < string.length() - 1; i += Character.charCount(cp)) {
+ cp = string.codePointAt(i);
+ if (cp != '/' && !IdentifierUtils.isDexIdentifierPart(cp)) {
+ return false;
}
- if (IdentifierUtils.isDexIdentifierPart(ch)) {
- continue;
- }
- return false;
}
return true;
}
@@ -232,12 +229,12 @@
string.equals(Constants.CLASS_INITIALIZER_NAME))) {
return true;
}
- for (int i = 0; i < string.length(); i++) {
- char ch = string.charAt(i);
- if (IdentifierUtils.isDexIdentifierPart(ch)) {
- continue;
+ int cp;
+ for (int i = 0; i < string.length(); i += Character.charCount(cp)) {
+ cp = string.codePointAt(i);
+ if (!IdentifierUtils.isDexIdentifierPart(cp)) {
+ return false;
}
- return false;
}
return true;
}
@@ -249,18 +246,19 @@
int start = 0;
int end = string.length();
if (string.charAt(0) == '<') {
- if (string.charAt(string.length() - 1) == '>') {
+ if (string.charAt(end - 1) == '>') {
start = 1;
- end = string.length() - 1;
+ --end;
} else {
return false;
}
}
- for (int i = start; i < end; i++) {
- if (IdentifierUtils.isDexIdentifierPart(string.charAt(i))) {
- continue;
+ int cp;
+ for (int i = start; i < end; i += Character.charCount(cp)) {
+ cp = string.codePointAt(i);
+ if (!IdentifierUtils.isDexIdentifierPart(cp)) {
+ return false;
}
- return false;
}
return true;
}
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 23a92a0..deab69f 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -69,11 +69,11 @@
private int lineOffset = 0;
private String line;
- private char peek() {
- return peek(0);
+ private int peekCodePoint() {
+ return lineOffset < line.length() ? line.codePointAt(lineOffset) : '\n';
}
- private char peek(int distance) {
+ private char peekChar(int distance) {
return lineOffset + distance < line.length()
? line.charAt(lineOffset + distance)
: '\n';
@@ -83,7 +83,17 @@
return lineOffset < line.length();
}
- private char next() {
+ private int nextCodePoint() {
+ try {
+ int cp = line.codePointAt(lineOffset);
+ lineOffset += Character.charCount(cp);
+ return cp;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParseException("Unexpected end of line");
+ }
+ }
+
+ private char nextChar() {
try {
return line.charAt(lineOffset++);
} catch (ArrayIndexOutOfBoundsException e) {
@@ -128,8 +138,8 @@
// Helpers for common pattern
private void skipWhitespace() {
- while (Character.isWhitespace(peek())) {
- next();
+ while (Character.isWhitespace(peekCodePoint())) {
+ nextCodePoint();
}
}
@@ -137,7 +147,7 @@
if (!hasNext()) {
throw new ParseException("Expected '" + c + "'", true);
}
- if (next() != c) {
+ if (nextChar() != c) {
throw new ParseException("Expected '" + c + "'");
}
return c;
@@ -197,7 +207,7 @@
// In the last round we're only here to flush the last line read (which may trigger adding a
// new MemberNaming) and flush activeMemberNaming, so skip parsing.
if (!lastRound) {
- if (!Character.isWhitespace(peek())) {
+ if (!Character.isWhitespace(peekCodePoint())) {
lastRound = true;
continue;
}
@@ -212,9 +222,9 @@
expect(':');
}
signature = parseSignature();
- if (peek() == ':') {
+ if (peekChar(0) == ':') {
// This is a mapping or inlining definition
- next();
+ nextChar();
originalRange = maybeParseRangeOrInt();
if (originalRange == null) {
throw new ParseException("No number follows the colon after the method signature.");
@@ -294,22 +304,22 @@
private void skipIdentifier(boolean allowInit) {
boolean isInit = false;
- if (allowInit && peek() == '<') {
+ if (allowInit && peekChar(0) == '<') {
// swallow the leading < character
- next();
+ nextChar();
isInit = true;
}
- if (!IdentifierUtils.isDexIdentifierStart(peek())) {
+ if (!IdentifierUtils.isDexIdentifierStart(peekCodePoint())) {
throw new ParseException("Identifier expected");
}
- next();
- while (IdentifierUtils.isDexIdentifierPart(peek())) {
- next();
+ nextCodePoint();
+ while (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
+ nextCodePoint();
}
if (isInit) {
expect('>');
}
- if (IdentifierUtils.isDexIdentifierPart(peek())) {
+ if (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
throw new ParseException("End of identifier expected");
}
}
@@ -330,8 +340,8 @@
private String parseMethodName() {
int startPosition = lineOffset;
skipIdentifier(true);
- while (peek() == '.') {
- next();
+ while (peekChar(0) == '.') {
+ nextChar();
skipIdentifier(true);
}
return substring(startPosition);
@@ -340,13 +350,13 @@
private String parseType(boolean allowArray) {
int startPosition = lineOffset;
skipIdentifier(false);
- while (peek() == '.') {
- next();
+ while (peekChar(0) == '.') {
+ nextChar();
skipIdentifier(false);
}
if (allowArray) {
- while (peek() == '[') {
- next();
+ while (peekChar(0) == '[') {
+ nextChar();
expect(']');
}
}
@@ -358,15 +368,15 @@
expect(' ');
String name = parseMethodName();
Signature signature;
- if (peek() == '(') {
- next();
+ if (peekChar(0) == '(') {
+ nextChar();
String[] arguments;
- if (peek() == ')') {
+ if (peekChar(0) == ')') {
arguments = new String[0];
} else {
List<String> items = new LinkedList<>();
items.add(parseType(true));
- while (peek() != ')') {
+ while (peekChar(0) != ')') {
expect(',');
items.add(parseType(true));
}
@@ -386,9 +396,9 @@
}
private boolean acceptArrow() {
- if (peek() == '-' && peek(1) == '>') {
- next();
- next();
+ if (peekChar(0) == '-' && peekChar(1) == '>') {
+ nextChar();
+ nextChar();
return true;
}
return false;
@@ -396,22 +406,26 @@
private boolean acceptString(String s) {
for (int i = 0; i < s.length(); i++) {
- if (peek(i) != s.charAt(i)) {
+ if (peekChar(i) != s.charAt(i)) {
return false;
}
}
for (int i = 0; i < s.length(); i++) {
- next();
+ nextChar();
}
return true;
}
+ private boolean isSimpleDigit(char c) {
+ return '0' <= c && c <= '9';
+ }
+
private Object maybeParseRangeOrInt() {
- if (!Character.isDigit(peek())) {
+ if (!isSimpleDigit(peekChar(0))) {
return null;
}
int from = parseNumber();
- if (peek() != ':') {
+ if (peekChar(0) != ':') {
return from;
}
expect(':');
@@ -421,13 +435,13 @@
private int parseNumber() {
int result = 0;
- if (!Character.isDigit(peek())) {
+ if (!isSimpleDigit(peekChar(0))) {
throw new ParseException("Number expected");
}
do {
result *= 10;
- result += Character.getNumericValue(next());
- } while (Character.isDigit(peek()));
+ result += Character.getNumericValue(nextChar());
+ } while (isSimpleDigit(peekChar(0)));
return result;
}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 5cb4ce8..35c8e47 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -1104,14 +1104,15 @@
return Integer.parseInt(s);
}
- private final Predicate<Character> CLASS_NAME_PREDICATE =
- character -> IdentifierUtils.isDexIdentifierPart(character)
- || character == '.'
- || character == '*'
- || character == '?'
- || character == '%'
- || character == '['
- || character == ']';
+ private final Predicate<Integer> CLASS_NAME_PREDICATE =
+ codePoint ->
+ IdentifierUtils.isDexIdentifierPart(codePoint)
+ || codePoint == '.'
+ || codePoint == '*'
+ || codePoint == '?'
+ || codePoint == '%'
+ || codePoint == '['
+ || codePoint == ']';
private String acceptClassName() {
return acceptString(CLASS_NAME_PREDICATE);
@@ -1123,7 +1124,7 @@
int start = position;
int end = position;
while (!eof(end)) {
- char current = contents.charAt(end);
+ int current = contents.codePointAt(end);
if (currentBackreference != null) {
if (current == '>') {
try {
@@ -1138,10 +1139,10 @@
origin, getPosition()));
}
currentBackreference = null;
- } else if (Character.isDigit(current)
+ } else if (('0' <= current && current <= '9')
// Only collect integer literal for the backreference.
|| (current == '-' && currentBackreference.length() == 0)) {
- currentBackreference.append(current);
+ currentBackreference.append((char) current);
} else if (kind == IdentifierType.CLASS_NAME) {
throw reporter.fatalError(new StringDiagnostic(
"Use of generics not allowed for java type.", origin, getPosition()));
@@ -1150,12 +1151,12 @@
// collection of the backreference.
currentBackreference = null;
}
- end++;
+ end += Character.charCount(current);
} else if (CLASS_NAME_PREDICATE.test(current) || current == '>') {
- end++;
+ end += Character.charCount(current);
} else if (current == '<') {
currentBackreference = new StringBuilder();
- end++;
+ ++end;
} else {
break;
}
@@ -1177,14 +1178,15 @@
int start = position;
int end = position;
while (!eof(end)) {
- char current = contents.charAt(end);
+ int current = contents.codePointAt(end);
if (current == '.' && !eof(end + 1) && peekCharAt(end + 1) == '.') {
// The grammar is ambiguous. End accepting before .. token used in return ranges.
break;
}
- if ((start == end && IdentifierUtils.isDexIdentifierStart(current)) ||
- ((start < end) && (IdentifierUtils.isDexIdentifierPart(current) || current == '.'))) {
- end++;
+ if ((start == end && IdentifierUtils.isDexIdentifierStart(current))
+ || ((start < end)
+ && (IdentifierUtils.isDexIdentifierPart(current) || current == '.'))) {
+ end += Character.charCount(current);
} else {
break;
}
@@ -1216,18 +1218,21 @@
}
private String acceptPattern() {
- return acceptString(character ->
- IdentifierUtils.isDexIdentifierPart(character) || character == '!' || character == '*');
+ return acceptString(
+ codePoint ->
+ IdentifierUtils.isDexIdentifierPart(codePoint)
+ || codePoint == '!'
+ || codePoint == '*');
}
- private String acceptString(Predicate<Character> characterAcceptor) {
+ private String acceptString(Predicate<Integer> codepointAcceptor) {
skipWhitespace();
int start = position;
int end = position;
while (!eof(end)) {
- char current = contents.charAt(end);
- if (characterAcceptor.test(current)) {
- end++;
+ int current = contents.codePointAt(end);
+ if (codepointAcceptor.test(current)) {
+ end += Character.charCount(current);
} else {
break;
}
diff --git a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
index 3097a07..5fb617b 100644
--- a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
@@ -5,43 +5,27 @@
package com.android.tools.r8.utils;
public class IdentifierUtils {
- public static boolean isDexIdentifierStart(char ch) {
+ public static boolean isDexIdentifierStart(int cp) {
// Dex does not have special restrictions on the first char of an identifier.
- return isDexIdentifierPart(ch);
+ return isDexIdentifierPart(cp);
}
- public static boolean isDexIdentifierPart(char ch) {
- return isSimpleNameChar(ch);
+ public static boolean isDexIdentifierPart(int cp) {
+ return isSimpleNameChar(cp);
}
- private static boolean isSimpleNameChar(char ch) {
- if (ch >= 'A' && ch <= 'Z') {
- return true;
- }
- if (ch >= 'a' && ch <= 'z') {
- return true;
- }
- if (ch >= '0' && ch <= '9') {
- return true;
- }
- if (ch == '$' || ch == '-' || ch == '_') {
- return true;
- }
- if (ch >= 0x00a1 && ch <= 0x1fff) {
- return true;
- }
- if (ch >= 0x2010 && ch <= 0x2027) {
- return true;
- }
- if (ch >= 0x2030 && ch <= 0xd7ff) {
- return true;
- }
- if (ch >= 0xe000 && ch <= 0xffef) {
- return true;
- }
- if (ch >= 0x10000 && ch <= 0x10ffff) {
- return true;
- }
- return false;
+ private static boolean isSimpleNameChar(int cp) {
+ // See https://source.android.com/devices/tech/dalvik/dex-format#string-syntax.
+ return ('A' <= cp && cp <= 'Z')
+ || ('a' <= cp && cp <= 'z')
+ || ('0' <= cp && cp <= '9')
+ || cp == '$'
+ || cp == '-'
+ || cp == '_'
+ || (0x00a1 <= cp && cp <= 0x1fff)
+ || (0x2010 <= cp && cp <= 0x2027)
+ || (0x2030 <= cp && cp <= 0xd7ff)
+ || (0xe000 <= cp && cp <= 0xffef)
+ || (0x10000 <= cp && cp <= 0x10ffff);
}
}
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
index 7caac8e..bad64ac 100644
--- a/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
@@ -4,14 +4,25 @@
package com.android.tools.r8.jasmin;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.smali.SmaliBuilder;
+import com.android.tools.r8.smali.SmaliBuilder.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Bytes;
+import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
+import java.util.zip.Adler32;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -20,64 +31,183 @@
@RunWith(Parameterized.class)
public class InvalidFieldNames extends JasminTestBase {
- public boolean runsOnJVM;
- public String name;
+ private static final String CLASS_NAME = "Test";
+ private static String FIELD_VALUE = "42";
- public InvalidFieldNames(String name, boolean runsOnJVM) {
- this.name = name;
- this.runsOnJVM = runsOnJVM;
- }
-
- private void runTest(JasminBuilder builder, String main, String expected) throws Exception {
- if (runsOnJVM) {
- String javaResult = runOnJava(builder, main);
- assertEquals(expected, javaResult);
- }
- String artResult = null;
- try {
- artResult = runOnArtD8(builder, main);
- fail();
- } catch (CompilationError t) {
- assertTrue(t.getMessage().contains(name));
- }
- assertNull("Invalid dex field names should be rejected.", artResult);
- }
-
- @Parameters
+ @Parameters(name = "\"{0}\", jvm: {1}, art: {2}")
public static Collection<Object[]> data() {
- return Arrays.asList(new Object[][]{
- {"\u00a0", !ToolHelper.isJava9Runtime()},
- {"\u2000", !ToolHelper.isJava9Runtime()},
- {"\u200f", !ToolHelper.isJava9Runtime()},
- {"\u2028", !ToolHelper.isJava9Runtime()},
- {"\u202f", !ToolHelper.isJava9Runtime()},
- {"\ud800", !ToolHelper.isJava9Runtime()},
- {"\udfff", !ToolHelper.isJava9Runtime()},
- {"\ufff0", !ToolHelper.isJava9Runtime()},
- {"\uffff", !ToolHelper.isJava9Runtime()},
- {"a/b", false},
- {"<a", false},
- {"a>", !ToolHelper.isJava9Runtime()},
- {"a<b>", !ToolHelper.isJava9Runtime()},
- {"<a>b", !ToolHelper.isJava9Runtime()}
- });
+ return Arrays.asList(
+ new Object[][] {
+ {new TestStringParameter("azAZ09$_"), true, true},
+ {new TestStringParameter("_"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("a-b"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\u00a0"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\u00a1"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\u1fff"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\u2000"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\u200f"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\u2010"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\u2027"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\u2028"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\u202f"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\u2030"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\ud7ff"), !ToolHelper.isJava9Runtime(), true},
+
+ // Standalone high and low surrogates.
+ {new TestStringParameter("\ud800"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\udbff"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\udc00"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\udfff"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\ue000"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\uffef"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\ufff0"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("\uffff"), !ToolHelper.isJava9Runtime(), false},
+
+ // Single and double code points above 0x10000.
+ {new TestStringParameter("\ud800\udc00"), true, true},
+ {new TestStringParameter("\ud800\udcfa"), true, true},
+ {new TestStringParameter("\ud800\udcfb"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\udbff\udfff"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("\ud800\udc00\ud800\udcfa"), true, true},
+ {new TestStringParameter("\ud800\udc00\udbff\udfff"), !ToolHelper.isJava9Runtime(), true},
+ {new TestStringParameter("a/b"), false, false},
+ {new TestStringParameter("<a"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("a>"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("a<b>"), !ToolHelper.isJava9Runtime(), false},
+ {new TestStringParameter("<a>b"), !ToolHelper.isJava9Runtime(), false}
+ });
}
- @Test
- public void invalidFieldNames() throws Exception {
- JasminBuilder builder = new JasminBuilder();
- JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+ // TestStringParameter is a String with modified toString() which prints \\uXXXX for
+ // characters outside 0x20..0x7e.
+ static class TestStringParameter {
+ private final String value;
- clazz.addStaticField(name, "I", "42");
+ TestStringParameter(String value) {
+ this.value = value;
+ }
+
+ String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return StringUtils.toASCIIString(value);
+ }
+ }
+
+ private String name;
+ private boolean validForJVM;
+ private boolean validForArt;
+
+ public InvalidFieldNames(TestStringParameter name, boolean validForJVM, boolean validForArt) {
+ this.name = name.getValue();
+ this.validForJVM = validForJVM;
+ this.validForArt = validForArt;
+ }
+
+ private byte[] trimLastZeroByte(byte[] bytes) {
+ assert bytes.length > 0 && bytes[bytes.length - 1] == 0;
+ byte[] result = new byte[bytes.length - 1];
+ System.arraycopy(bytes, 0, result, 0, result.length);
+ return result;
+ }
+
+ private JasminBuilder createJasminBuilder() {
+ JasminBuilder builder = new JasminBuilder();
+ JasminBuilder.ClassBuilder clazz = builder.addClass(CLASS_NAME);
+
+ clazz.addStaticField(name, "I", FIELD_VALUE);
clazz.addMainMethod(
".limit stack 2",
".limit locals 1",
" getstatic java/lang/System/out Ljava/io/PrintStream;",
- " getstatic Test/" + name + " I",
+ " getstatic " + CLASS_NAME + "/" + name + " I",
" invokevirtual java/io/PrintStream.print(I)V",
" return");
+ return builder;
+ }
- runTest(builder, clazz.name, "42");
+ private AndroidApp createAppWithSmali() throws Exception {
+
+ SmaliBuilder smaliBuilder = new SmaliBuilder(CLASS_NAME);
+ String originalSourceFile = CLASS_NAME + FileUtils.JAVA_EXTENSION;
+ smaliBuilder.setSourceFile(originalSourceFile);
+
+ // We're using a valid placeholder string which will be replaced by the actual name.
+ byte[] nameMutf8 = trimLastZeroByte(DexString.encodeToMutf8(name));
+
+ String placeholderString = Strings.repeat("A", nameMutf8.length);
+
+ smaliBuilder.addStaticField(placeholderString, "I", FIELD_VALUE);
+ MethodSignature mainSignature =
+ smaliBuilder.addMainMethod(
+ 1,
+ "sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+ "sget v0, LTest;->" + placeholderString + ":I",
+ "invoke-virtual {p0, v0}, Ljava/io/PrintStream;->print(I)V",
+ "return-void");
+ byte[] dexCode = smaliBuilder.compile();
+
+ // Replace placeholder by mutf8-encoded name
+ byte[] placeholderBytes = trimLastZeroByte(DexString.encodeToMutf8(placeholderString));
+ assert placeholderBytes.length == nameMutf8.length;
+ int index = Bytes.indexOf(dexCode, placeholderBytes);
+ if (index >= 0) {
+ System.arraycopy(nameMutf8, 0, dexCode, index, nameMutf8.length);
+ }
+ assert Bytes.indexOf(dexCode, placeholderBytes) < 0;
+
+ // Update checksum
+ Adler32 adler = new Adler32();
+ adler.update(dexCode, Constants.SIGNATURE_OFFSET, dexCode.length - Constants.SIGNATURE_OFFSET);
+ int checksum = (int) adler.getValue();
+ for (int i = 0; i < 4; ++i) {
+ dexCode[Constants.CHECKSUM_OFFSET + i] = (byte) (checksum >> (8 * i) & 0xff);
+ }
+
+ return AndroidApp.builder()
+ .addDexProgramData(dexCode, new PathOrigin(Paths.get(originalSourceFile)))
+ .build();
+ }
+
+ @Test
+ public void invalidFieldNames() throws Exception {
+ JasminBuilder jasminBuilder = createJasminBuilder();
+
+ if (validForJVM) {
+ String javaResult = runOnJava(jasminBuilder, CLASS_NAME);
+ assertEquals(FIELD_VALUE, javaResult);
+ } else {
+ try {
+ runOnJava(jasminBuilder, CLASS_NAME);
+ fail("Should have failed on JVM.");
+ } catch (AssertionError e) {
+ // Silent on expected failure.
+ }
+ }
+
+ if (validForArt) {
+ String artResult = runOnArtD8(jasminBuilder, CLASS_NAME);
+ assertEquals(FIELD_VALUE, artResult);
+ } else {
+ // Make sure the compiler fails.
+ try {
+ runOnArtD8(jasminBuilder, CLASS_NAME);
+ fail("D8 should have rejected this case.");
+ } catch (CompilationError t) {
+ assertTrue(t.getMessage().contains(name));
+ }
+
+ // Make sure ART also fail, if D8 rejects it.
+ try {
+ runOnArt(createAppWithSmali(), CLASS_NAME);
+ fail("Art should have failed.");
+ } catch (AssertionError e) {
+ // Silent on expected failure.
+ }
+ }
}
}