Version 1.5.53
Cherry-pick: Treat byte order mark (BOM) as whitespace
CL: https://r8-review.googlesource.com/c/r8/+/39991
Cherry-pick: Support reading main dex list files starting with a byte order mark (BOM)
CL: https://r8-review.googlesource.com/c/r8/+/39980
Cherry-pick: Support reading proguard map files starting with a byte order mark (BOM)
CL: https://r8-review.googlesource.com/c/r8/+/39942
Cherry-pick: Support reading proguard configuration files starting with a byte order mark (BOM)
CL: https://r8-review.googlesource.com/c/r8/+/39898
Bug: 135898791
Change-Id: I431e9766c7e48c7b48a5874b48b4bb3b5a771aad
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 76c37ab..2c62373 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
// This field is accessed from release scripts using simple pattern matching.
// Therefore, changing this field could break our release scripts.
- public static final String LABEL = "1.5.52";
+ public static final String LABEL = "1.5.53";
private Version() {
}
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 ef9aedd..b2c58be 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -96,6 +96,7 @@
}
private char nextChar() {
+ assert hasNext();
try {
return line.charAt(lineOffset++);
} catch (ArrayIndexOutOfBoundsException e) {
@@ -110,7 +111,7 @@
return skipLine();
}
- private static boolean isEmptyOrCommentLine(String line) {
+ private boolean isEmptyOrCommentLine(String line) {
if (line == null) {
return true;
}
@@ -118,7 +119,7 @@
char c = line.charAt(i);
if (c == '#') {
return true;
- } else if (!Character.isWhitespace(c)) {
+ } else if (!StringUtils.isWhitespace(c)) {
return false;
}
}
@@ -140,26 +141,36 @@
// Helpers for common pattern
private void skipWhitespace() {
- while (Character.isWhitespace(peekCodePoint())) {
+ while (hasNext() && StringUtils.isWhitespace(peekCodePoint())) {
nextCodePoint();
}
}
- private char expect(char c) {
+ private void expectWhitespace() {
+ boolean seen = false;
+ while (hasNext() && StringUtils.isWhitespace(peekCodePoint())) {
+ seen = seen || !StringUtils.isBOM(peekCodePoint());
+ nextCodePoint();
+ }
+ if (!seen) {
+ throw new ParseException("Expected whitespace", true);
+ }
+ }
+
+ private void expect(char c) {
if (!hasNext()) {
throw new ParseException("Expected '" + c + "'", true);
}
if (nextChar() != c) {
throw new ParseException("Expected '" + c + "'");
}
- return c;
}
void parse(ProguardMap.Builder mapBuilder) throws IOException {
// Read the first line.
do {
- lineNo++;
line = reader.readLine();
+ lineNo++;
} while (hasLine() && isEmptyOrCommentLine(line));
parseClassMappings(mapBuilder);
}
@@ -168,6 +179,7 @@
private void parseClassMappings(ProguardMap.Builder mapBuilder) throws IOException {
while (hasLine()) {
+ skipWhitespace();
String before = parseType(false);
skipWhitespace();
// Workaround for proguard map files that contain entries for package-info.java files.
@@ -186,9 +198,11 @@
}
skipWhitespace();
String after = parseType(false);
+ skipWhitespace();
expect(':');
ClassNaming.Builder currentClassBuilder =
mapBuilder.classNamingBuilder(after, before, getPosition());
+ skipWhitespace();
if (nextLine()) {
parseMemberMappings(currentClassBuilder);
}
@@ -204,7 +218,7 @@
Range mappedRange = null;
// Parse the member line ' x:y:name:z:q -> renamedName'.
- if (!Character.isWhitespace(peekCodePoint())) {
+ if (!StringUtils.isWhitespace(peekCodePoint())) {
break;
}
skipWhitespace();
@@ -215,12 +229,16 @@
String.format("Invalid obfuscated line number range (%s).", maybeRangeOrInt));
}
mappedRange = (Range) maybeRangeOrInt;
+ skipWhitespace();
expect(':');
}
+ skipWhitespace();
Signature signature = parseSignature();
+ skipWhitespace();
if (peekChar(0) == ':') {
// This is a mapping or inlining definition
nextChar();
+ skipWhitespace();
originalRange = maybeParseRangeOrInt();
if (originalRange == null) {
throw new ParseException("No number follows the colon after the method signature.");
@@ -300,7 +318,8 @@
expect('>');
}
if (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
- throw new ParseException("End of identifier expected");
+ throw new ParseException(
+ "End of identifier expected (was 0x" + Integer.toHexString(peekCodePoint()) + ")");
}
}
@@ -345,19 +364,24 @@
private Signature parseSignature() {
String type = parseType(true);
- expect(' ');
+ expectWhitespace();
String name = parseMethodName();
+ skipWhitespace();
Signature signature;
if (peekChar(0) == '(') {
nextChar();
+ skipWhitespace();
String[] arguments;
if (peekChar(0) == ')') {
arguments = new String[0];
} else {
List<String> items = new LinkedList<>();
items.add(parseType(true));
+ skipWhitespace();
while (peekChar(0) != ')') {
+ skipWhitespace();
expect(',');
+ skipWhitespace();
items.add(parseType(true));
}
arguments = items.toArray(StringUtils.EMPTY_ARRAY);
@@ -405,10 +429,12 @@
return null;
}
int from = parseNumber();
+ skipWhitespace();
if (peekChar(0) != ':') {
return from;
}
expect(':');
+ skipWhitespace();
int to = parseNumber();
return new Range(from, to);
}
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 7134bae..00340e4 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -26,6 +26,7 @@
import com.android.tools.r8.utils.LongInterval;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.StringUtils;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -200,7 +201,8 @@
private final Origin origin;
ProguardConfigurationSourceParser(ProguardConfigurationSource source) throws IOException {
- contents = source.get();
+ // Strip any leading BOM here so it is not included in the text position.
+ contents = StringUtils.stripLeadingBOM(source.get());
baseDirectory = source.getBaseDirectory();
name = source.getName();
this.origin = source.getOrigin();
@@ -1333,7 +1335,7 @@
}
private void skipWhitespace() {
- while (!eof() && Character.isWhitespace(contents.charAt(position))) {
+ while (!eof() && StringUtils.isWhitespace(peekChar())) {
if (peekChar() == '\n') {
line++;
lineStartPosition = position + 1;
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 309f8d9..d1975c4 100644
--- a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
@@ -29,7 +29,8 @@
|| (0x00a1 <= cp && cp <= 0x1fff)
|| (0x2010 <= cp && cp <= 0x2027)
|| (0x2030 <= cp && cp <= 0xd7ff)
- || (0xe000 <= cp && cp <= 0xffef)
+ || (0xe000 <= cp && cp < 0xfeff) // Don't include BOM.
+ || (0xfeff < cp && cp <= 0xffef)
|| (0x10000 <= cp && cp <= 0x10ffff);
}
}
diff --git a/src/main/java/com/android/tools/r8/utils/MainDexList.java b/src/main/java/com/android/tools/r8/utils/MainDexList.java
index 74034ef..e323566 100644
--- a/src/main/java/com/android/tools/r8/utils/MainDexList.java
+++ b/src/main/java/com/android/tools/r8/utils/MainDexList.java
@@ -42,7 +42,7 @@
++lineNumber;
int newLineIndex = lines.indexOf('\n', offset);
int lineEnd = newLineIndex == -1 ? lines.length() : newLineIndex;
- String line = lines.substring(offset, lineEnd).trim();
+ String line = StringUtils.trim(lines.substring(offset, lineEnd));
if (!line.isEmpty()) {
try {
result.add(parseEntry(line, itemFactory));
diff --git a/src/main/java/com/android/tools/r8/utils/StringUtils.java b/src/main/java/com/android/tools/r8/utils/StringUtils.java
index 295de13..b9de978 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -16,6 +16,7 @@
public static char[] EMPTY_CHAR_ARRAY = {};
public static final String[] EMPTY_ARRAY = {};
public static final String LINE_SEPARATOR = System.getProperty("line.separator");
+ public static final char BOM = '\uFEFF';
public enum BraceType {
PARENS,
@@ -260,4 +261,36 @@
}
return builder.toString();
}
+
+ public static boolean isBOM(int codePoint) {
+ return codePoint == BOM;
+ }
+
+ public static boolean isWhitespace(int codePoint) {
+ return Character.isWhitespace(codePoint) || isBOM(codePoint);
+ }
+
+ public static String stripLeadingBOM(String s) {
+ if (s.length() > 0 && s.charAt(0) == StringUtils.BOM) {
+ return s.substring(1);
+ } else {
+ return s;
+ }
+ }
+
+ public static String trim(String s) {
+ int beginIndex = 0;
+ int endIndex = s.length();
+ while (beginIndex < endIndex && isWhitespace(s.charAt(beginIndex))) {
+ beginIndex++;
+ }
+ while (endIndex - 1 > beginIndex && isWhitespace(s.charAt(endIndex - 1))) {
+ endIndex--;
+ }
+ if (beginIndex > 0 || endIndex < s.length()) {
+ return s.substring(beginIndex, endIndex);
+ } else {
+ return s;
+ }
+ }
}
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
index d79aaf0..fb0f5b4 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
@@ -73,8 +73,12 @@
} else {
position = ((TextPosition) diagnostic.getPosition());
}
- assertEquals(lineStart, position.getLine());
- assertEquals(columnStart, position.getColumn());
+ if (lineStart > 0) {
+ assertEquals(lineStart, position.getLine());
+ }
+ if (columnStart > 0) {
+ assertEquals(columnStart, position.getColumn());
+ }
for (String part : messageParts) {
assertTrue(diagnostic.getDiagnosticMessage() + " doesn't contain \"" + part + "\"",
diagnostic.getDiagnosticMessage().contains(part));
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index bea4ea0..fe494e6 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -71,6 +71,7 @@
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.MainDexList;
import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -338,16 +339,12 @@
@Test
public void validEntries() throws IOException {
- List<String> list = ImmutableList.of(
- "A.class",
- "a/b/c/D.class",
- "a/b/c/D$E.class"
- );
+ List<String> lines = ImmutableList.of("A.class", "a/b/c/D.class", "a/b/c/D$E.class");
DexItemFactory factory = new DexItemFactory();
Path mainDexList = temp.getRoot().toPath().resolve("valid.txt");
- FileUtils.writeTextFile(mainDexList, list);
+ FileUtils.writeTextFile(mainDexList, lines);
Set<DexType> types = parse(mainDexList, factory);
- for (String entry : list) {
+ for (String entry : lines) {
DexType type = factory.createType("L" + entry.replace(".class", "") + ";");
assertTrue(types.contains(type));
assertSame(type, MainDexList.parseEntry(entry, factory));
@@ -355,6 +352,56 @@
}
@Test
+ public void leadingBOM() throws IOException {
+ List<String> lines =
+ ImmutableList.of(StringUtils.BOM + "A.class", "a/b/c/D.class", "a/b/c/D$E.class");
+ List<String> classes = ImmutableList.of("A", "a/b/c/D", "a/b/c/D$E");
+ DexItemFactory factory = new DexItemFactory();
+ Path mainDexList = temp.getRoot().toPath().resolve("valid.txt");
+ FileUtils.writeTextFile(mainDexList, lines);
+ Set<DexType> types = parse(mainDexList, factory);
+ assertEquals(types.size(), classes.size());
+ for (String clazz : classes) {
+ DexType type = factory.createType("L" + clazz + ";");
+ assertTrue(types.contains(type));
+ }
+ }
+
+ @Test
+ public void lotsOfWhitespace() throws IOException {
+ List<String> ws =
+ ImmutableList.of(
+ "",
+ " ",
+ " ",
+ "\t ",
+ " \t",
+ "" + StringUtils.BOM,
+ StringUtils.BOM + " " + StringUtils.BOM);
+ for (String before : ws) {
+ for (String after : ws) {
+ List<String> lines =
+ ImmutableList.of(
+ before + "A.class" + after,
+ before + "a/b/c/D.class" + after,
+ before + "a/b/c/D$E.class" + after,
+ before + after);
+
+ List<String> classes = ImmutableList.of("A", "a/b/c/D", "a/b/c/D$E");
+ DexItemFactory factory = new DexItemFactory();
+ Path mainDexList = temp.getRoot().toPath().resolve("valid.txt");
+ FileUtils.writeTextFile(mainDexList, lines);
+ Set<DexType> types = parse(mainDexList, factory);
+ assertEquals(types.size(), classes.size());
+ for (String clazz : classes) {
+ DexType type = factory.createType("L" + clazz + ";");
+ assertTrue(types.contains(type));
+ }
+ }
+ }
+ }
+
+ @Test
public void validList() throws IOException {
List<String> list = ImmutableList.of(
"A.class ",
@@ -376,18 +423,27 @@
parse(mainDexList, factory);
}
+ enum TestMode {
+ FROM_CLASS_NAMES,
+ FROM_FILE,
+ FROM_FILE_WITH_BOM
+ }
+
private Path runD8WithMainDexList(
- CompilationMode mode, Path input, List<String> mainDexClasses, boolean useFile)
+ CompilationMode mode, Path input, List<String> mainDexClasses, TestMode testMode)
throws Exception {
Path testDir = temp.newFolder().toPath();
Path listFile = testDir.resolve("main-dex-list.txt");
- if (mainDexClasses != null && useFile) {
- FileUtils.writeTextFile(
- listFile,
- mainDexClasses
- .stream()
+ if (mainDexClasses != null
+ && (testMode == TestMode.FROM_FILE || testMode == TestMode.FROM_FILE_WITH_BOM)) {
+ List<String> lines =
+ mainDexClasses.stream()
.map(clazz -> clazz.replace('.', '/') + ".class")
- .collect(Collectors.toList()));
+ .collect(Collectors.toList());
+ if (testMode == TestMode.FROM_FILE_WITH_BOM) {
+ lines.set(0, StringUtils.BOM + lines.get(0));
+ }
+ FileUtils.writeTextFile(listFile, lines);
}
D8Command.Builder builder =
@@ -396,7 +452,7 @@
.setMode(mode)
.setOutput(testDir, OutputMode.DexIndexed);
if (mainDexClasses != null) {
- if (useFile) {
+ if (testMode == TestMode.FROM_FILE) {
builder.addMainDexListFiles(listFile);
} else {
builder.addMainDexClasses(mainDexClasses);
@@ -416,21 +472,25 @@
if (allClasses) {
// If all classes are passed add a run without a main-dex list as well.
testDirs.put(
- runD8WithMainDexList(mode, input, null, true),
+ runD8WithMainDexList(mode, input, null, TestMode.FROM_CLASS_NAMES),
mode.toString() + ": without a main-dex list");
}
testDirs.put(
- runD8WithMainDexList(mode, input, mainDexClasses, true),
+ runD8WithMainDexList(mode, input, mainDexClasses, TestMode.FROM_FILE),
mode.toString() + ": main-dex list files");
testDirs.put(
- runD8WithMainDexList(mode, input, mainDexClasses, false),
+ runD8WithMainDexList(mode, input, mainDexClasses, TestMode.FROM_FILE_WITH_BOM),
+ mode.toString() + ": main-dex list files (with BOM)");
+ testDirs.put(
+ runD8WithMainDexList(mode, input, mainDexClasses, TestMode.FROM_CLASS_NAMES),
mode.toString() + ": main-dex classes");
if (mainDexClasses != null) {
testDirs.put(
- runD8WithMainDexList(mode, input, Lists.reverse(mainDexClasses), true),
+ runD8WithMainDexList(mode, input, Lists.reverse(mainDexClasses), TestMode.FROM_FILE),
mode.toString() + ": main-dex list files (reversed)");
testDirs.put(
- runD8WithMainDexList(mode, input, Lists.reverse(mainDexClasses), false),
+ runD8WithMainDexList(
+ mode, input, Lists.reverse(mainDexClasses), TestMode.FROM_CLASS_NAMES),
mode.toString() + ": main-dex classes (reversed)");
}
diff --git a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
index 841b6cd..1ab9fe1 100644
--- a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
+++ b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
@@ -3,16 +3,25 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.naming;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.position.Position;
+import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
-public class ProguardMapReaderTest {
+public class ProguardMapReaderTest extends TestBase {
private static final String ROOT = ToolHelper.EXAMPLES_BUILD_DIR;
private static final String EXAMPLE_MAP = "throwing/throwing.map";
@@ -49,6 +58,71 @@
}
@Test
+ public void roundTripTestWithLeadingBOM() throws IOException {
+ ClassNameMapper firstMapper = ClassNameMapper.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP));
+ assertTrue(firstMapper.toString().charAt(0) != StringUtils.BOM);
+ ClassNameMapper secondMapper =
+ ClassNameMapper.mapperFromString(StringUtils.BOM + firstMapper.toString());
+ assertTrue(secondMapper.toString().charAt(0) != StringUtils.BOM);
+ Assert.assertEquals(firstMapper, secondMapper);
+ byte[] bytes = Files.readAllBytes(Paths.get(ROOT, EXAMPLE_MAP));
+ assertNotEquals(0xef, Byte.toUnsignedLong(bytes[0]));
+ Path mapFileWithBOM = writeTextToTempFile(StringUtils.BOM + firstMapper.toString());
+ bytes = Files.readAllBytes(mapFileWithBOM);
+ assertEquals(0xef, Byte.toUnsignedLong(bytes[0]));
+ assertEquals(0xbb, Byte.toUnsignedLong(bytes[1]));
+ assertEquals(0xbf, Byte.toUnsignedLong(bytes[2]));
+ ClassNameMapper thirdMapper = ClassNameMapper.mapperFromFile(mapFileWithBOM);
+ assertTrue(thirdMapper.toString().charAt(0) != StringUtils.BOM);
+ Assert.assertEquals(firstMapper, thirdMapper);
+ }
+
+ @Test
+ public void roundTripTestWithMultipleBOMsAndWhitespace() throws IOException {
+ List<String> ws =
+ ImmutableList.of(
+ "",
+ " ",
+ " ",
+ "\t ",
+ " \t",
+ "" + StringUtils.BOM,
+ StringUtils.BOM + " " + StringUtils.BOM);
+ for (String whitespace : ws) {
+ ClassNameMapper firstMapper = ClassNameMapper.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP));
+ assertTrue(firstMapper.toString().charAt(0) != StringUtils.BOM);
+ StringBuilder buildWithWhitespace = new StringBuilder();
+ char prevChar = '\0';
+ for (char c : firstMapper.toString().toCharArray()) {
+ if (c == ':' || c == ' ') {
+ buildWithWhitespace.append(whitespace);
+ buildWithWhitespace.append(c);
+ buildWithWhitespace.append(whitespace);
+ } else if (c == '-' || c == '(') {
+ buildWithWhitespace.append(whitespace);
+ buildWithWhitespace.append(c);
+ } else if (c == '>' && prevChar == '-') {
+ buildWithWhitespace.append(c);
+ buildWithWhitespace.append(whitespace);
+ } else {
+ buildWithWhitespace.append(c);
+ }
+ prevChar = c;
+ }
+ ClassNameMapper secondMapper =
+ ClassNameMapper.mapperFromString(buildWithWhitespace.toString());
+ assertFalse(firstMapper.toString().contains("" + StringUtils.BOM));
+ Assert.assertEquals(firstMapper, secondMapper);
+ byte[] bytes = Files.readAllBytes(Paths.get(ROOT, EXAMPLE_MAP));
+ assertNotEquals(0xef, Byte.toUnsignedLong(bytes[0]));
+ Path mapFileWithBOM = writeTextToTempFile(StringUtils.BOM + firstMapper.toString());
+ ClassNameMapper thirdMapper = ClassNameMapper.mapperFromFile(mapFileWithBOM);
+ assertTrue(thirdMapper.toString().charAt(0) != StringUtils.BOM);
+ Assert.assertEquals(firstMapper, thirdMapper);
+ }
+ }
+
+ @Test
public void parseIdentifierArrowAmbiguity1() throws IOException {
ClassNameMapper mapper = ClassNameMapper.mapperFromString("a->b:");
ClassNameMapper.Builder builder = ClassNameMapper.builder();
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index 14bc814..dffead7 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -32,10 +32,12 @@
import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
import com.android.tools.r8.utils.KeepingDiagnosticHandler;
import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -2583,4 +2585,52 @@
assertEquals(1, handler.infos.size());
checkDiagnostics(handler.infos, proguardConfig, 1, 7, "Ignoring modifier", "includecode");
}
+
+ @Test
+ public void parseFileStartingWithBOM() throws Exception {
+ // Copied from test 'parseIncludeCode()' and added a BOM.
+ ProguardConfigurationParser parser;
+ parser = new ProguardConfigurationParser(new DexItemFactory(), reporter);
+ Path proguardConfig =
+ writeTextToTempFile(StringUtils.BOM + "-keep,includecode class A { method(); }");
+ byte[] bytes = Files.readAllBytes(proguardConfig);
+ assertEquals(0xef, Byte.toUnsignedLong(bytes[0]));
+ assertEquals(0xbb, Byte.toUnsignedLong(bytes[1]));
+ assertEquals(0xbf, Byte.toUnsignedLong(bytes[2]));
+ parser.parse(proguardConfig);
+ assertEquals(1, parser.getConfig().getRules().size());
+ assertEquals(1, handler.infos.size());
+ checkDiagnostics(handler.infos, proguardConfig, 1, 7, "Ignoring modifier", "includecode");
+ }
+
+ @Test
+ public void parseFileWithLotsOfWhitespace() throws Exception {
+ List<String> ws =
+ ImmutableList.of(
+ "",
+ " ",
+ " ",
+ "\t ",
+ " \t",
+ "" + StringUtils.BOM,
+ StringUtils.BOM + " " + StringUtils.BOM);
+ // Copied from test 'parseIncludeCode()' and added whitespace.
+ for (String whitespace1 : ws) {
+ for (String whitespace2 : ws) {
+ reset();
+ Path proguardConfig =
+ writeTextToTempFile(
+ whitespace1
+ + "-keep,includecode "
+ + whitespace2
+ + "class A { method(); }"
+ + whitespace1);
+ parser.parse(proguardConfig);
+ assertEquals(1, parser.getConfig().getRules().size());
+ assertEquals(1, handler.infos.size());
+ // All BOSs except the leading are counted as input characters.
+ checkDiagnostics(handler.infos, proguardConfig, 1, 0, "Ignoring modifier", "includecode");
+ }
+ }
+ }
}
diff --git a/src/test/java/com/android/tools/r8/utils/StringUtilsTest.java b/src/test/java/com/android/tools/r8/utils/StringUtilsTest.java
index 32682a3..cac07ed 100644
--- a/src/test/java/com/android/tools/r8/utils/StringUtilsTest.java
+++ b/src/test/java/com/android/tools/r8/utils/StringUtilsTest.java
@@ -4,8 +4,10 @@
package com.android.tools.r8.utils;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import java.util.List;
import org.junit.Test;
@@ -35,4 +37,41 @@
StringUtils.join(ys, ", ", BraceType.SQUARE, s -> '"' + StringUtils.toASCIIString(s) + '"')
);
}
+
+ @Test
+ public void testTrim() {
+ assertEquals("", StringUtils.trim(""));
+ String empty = "";
+ assertSame(empty, StringUtils.trim(empty));
+ assertSame(Strings.repeat("A", 1), StringUtils.trim(Strings.repeat("A", 1)));
+ String oneChar = "A";
+ assertSame(oneChar, StringUtils.trim(oneChar));
+ assertEquals(Strings.repeat("A", 2), Strings.repeat("A", 2));
+ String twoChar = "AB";
+ assertSame(twoChar, StringUtils.trim(twoChar));
+ assertEquals(Strings.repeat("A", 10), Strings.repeat("A", 10));
+ String manyChar = Strings.repeat("A", 10);
+ assertSame(manyChar, StringUtils.trim(manyChar));
+ }
+
+ @Test
+ public void testTrimWithWhitespace() {
+ List<String> ws =
+ ImmutableList.of(
+ "",
+ " ",
+ " ",
+ "\t ",
+ " \t",
+ "" + StringUtils.BOM,
+ StringUtils.BOM + " " + StringUtils.BOM);
+ List<String> strings = ImmutableList.of("", "A", "AB", Strings.repeat("A", 10));
+ for (String before : ws) {
+ for (String after : ws) {
+ for (String string : strings) {
+ assertEquals(string, StringUtils.trim(before + string + after));
+ }
+ }
+ }
+ }
}