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));
+        }
+      }
+    }
+  }
 }