Change internal consumer of proguardmap to ProguardMapConsumer

We would use StringConsumer before but that is not compatible with partitioned output.

Change-Id: Id78ed07b0e06b4ff74d0fbac7f2e0cef60c445f8
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index d1495c2..1abfbd2 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.inspector.Inspector;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecification;
+import com.android.tools.r8.naming.ProguardMapStringConsumer;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.profile.art.ArtProfileForRewriting;
 import com.android.tools.r8.shaking.ProguardConfigurationParser;
@@ -665,7 +666,13 @@
     internal.setSyntheticInfoConsumer(syntheticInfoConsumer);
     internal.desugarGraphConsumer = desugarGraphConsumer;
     internal.mainDexKeepRules = mainDexKeepRules;
-    internal.proguardMapConsumer = proguardMapConsumer;
+    internal.proguardMapConsumer =
+        proguardMapConsumer == null
+            ? null
+            : ProguardMapStringConsumer.builder()
+                .setStringConsumer(proguardMapConsumer)
+                .setDiagnosticsHandler(getReporter())
+                .build();
     internal.lineNumberOptimization =
         !internal.debug && proguardMapConsumer != null
             ? LineNumberOptimization.ON
diff --git a/src/main/java/com/android/tools/r8/Finishable.java b/src/main/java/com/android/tools/r8/Finishable.java
new file mode 100644
index 0000000..2e114ef
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/Finishable.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8;
+
+public interface Finishable {
+
+  /**
+   * Callback when no further content will be provided for the resource.
+   *
+   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
+   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
+   * then the compiler guaranties to exit with an error.
+   *
+   * @param handler Diagnostics handler for reporting.
+   */
+  default void finished(DiagnosticsHandler handler) {}
+}
diff --git a/src/main/java/com/android/tools/r8/ProguardMapConsumer.java b/src/main/java/com/android/tools/r8/ProguardMapConsumer.java
new file mode 100644
index 0000000..f00b428
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ProguardMapConsumer.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8;
+
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.ProguardMapMarkerInfo;
+
+public abstract class ProguardMapConsumer implements Finishable {
+
+  public abstract void accept(ProguardMapMarkerInfo makerInfo, ClassNameMapper classNameMapper);
+}
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 6118b88..5fc02c3 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
+import com.android.tools.r8.naming.ProguardMapStringConsumer;
 import com.android.tools.r8.naming.SourceFileRewriter;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
@@ -1065,11 +1066,18 @@
     }
 
     // Amend the proguard-map consumer with options from the proguard configuration.
-    internal.proguardMapConsumer =
+    StringConsumer stringConsumer =
         wrapStringConsumer(
             proguardMapConsumer,
             proguardConfiguration.isPrintMapping(),
             proguardConfiguration.getPrintMappingFile());
+    internal.proguardMapConsumer =
+        stringConsumer == null
+            ? null
+            : ProguardMapStringConsumer.builder()
+                .setStringConsumer(stringConsumer)
+                .setDiagnosticsHandler(getReporter())
+                .build();
 
     // Amend the usage information consumer with options from the proguard configuration.
     internal.usageInformationConsumer =
diff --git a/src/main/java/com/android/tools/r8/StringConsumer.java b/src/main/java/com/android/tools/r8/StringConsumer.java
index d0e05cc..2583572 100644
--- a/src/main/java/com/android/tools/r8/StringConsumer.java
+++ b/src/main/java/com/android/tools/r8/StringConsumer.java
@@ -15,7 +15,7 @@
 
 /** Interface for receiving String resource. */
 @KeepForSubclassing
-public interface StringConsumer {
+public interface StringConsumer extends Finishable {
 
   /**
    * Callback to receive part of a string resource.
@@ -32,17 +32,6 @@
    */
   void accept(String string, DiagnosticsHandler handler);
 
-  /**
-   * Callback when no further content will be provided for the string resource.
-   *
-   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
-   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
-   * then the compiler guaranties to exit with an error.
-   *
-   * @param handler Diagnostics handler for reporting.
-   */
-  default void finished(DiagnosticsHandler handler) {}
-
   static EmptyConsumer emptyConsumer() {
     return EmptyConsumer.EMPTY_CONSUMER;
   }
diff --git a/src/main/java/com/android/tools/r8/bisect/Bisect.java b/src/main/java/com/android/tools/r8/bisect/Bisect.java
index 8a196d9..caa1c27 100644
--- a/src/main/java/com/android/tools/r8/bisect/Bisect.java
+++ b/src/main/java/com/android/tools/r8/bisect/Bisect.java
@@ -5,7 +5,7 @@
 
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.ProgramConsumer;
-import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.ProguardMapConsumer;
 import com.android.tools.r8.bisect.BisectOptions.Result;
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.dex.ApplicationWriter;
@@ -182,9 +182,9 @@
   private static void writeApp(DexApplication app, Path output, ExecutorService executor)
       throws IOException, ExecutionException {
     InternalOptions options = app.options;
-    // Save the original consumers so they can be unwrapped after write.
+    // Save the original consumers, so they can be unwrapped after write.
     ProgramConsumer programConsumer = options.programConsumer;
-    StringConsumer proguardMapConsumer = options.proguardMapConsumer;
+    ProguardMapConsumer proguardMapConsumer = options.proguardMapConsumer;
     AndroidAppConsumers compatSink = new AndroidAppConsumers(options);
     ApplicationWriter writer =
         ApplicationWriter.create(
diff --git a/src/main/java/com/android/tools/r8/naming/MultiProguardMapConsumer.java b/src/main/java/com/android/tools/r8/naming/MultiProguardMapConsumer.java
new file mode 100644
index 0000000..5901fd3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/MultiProguardMapConsumer.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ProguardMapConsumer;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MultiProguardMapConsumer extends ProguardMapConsumer {
+
+  private final List<ProguardMapConsumer> proguardMapConsumers;
+
+  public MultiProguardMapConsumer(List<ProguardMapConsumer> proguardMapConsumers) {
+    this.proguardMapConsumers = proguardMapConsumers;
+  }
+
+  @Override
+  public void finished(DiagnosticsHandler handler) {
+    proguardMapConsumers.forEach(consumer -> consumer.finished(handler));
+  }
+
+  @Override
+  public void accept(ProguardMapMarkerInfo markerInfo, ClassNameMapper classNameMapper) {
+    proguardMapConsumers.forEach(consumer -> consumer.accept(markerInfo, classNameMapper));
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private final List<ProguardMapConsumer> proguardMapConsumers = new ArrayList<>();
+
+    public Builder addProguardMapConsumer(ProguardMapConsumer consumer) {
+      proguardMapConsumers.add(consumer);
+      return this;
+    }
+
+    public MultiProguardMapConsumer build() {
+      return new MultiProguardMapConsumer(proguardMapConsumers);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapChecker.java b/src/main/java/com/android/tools/r8/naming/ProguardMapChecker.java
new file mode 100644
index 0000000..be80509
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapChecker.java
@@ -0,0 +1,129 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming;
+
+import static com.android.tools.r8.naming.ProguardMapMarkerInfo.MARKER_KEY_PG_MAP_HASH;
+import static com.android.tools.r8.naming.ProguardMapMarkerInfo.SHA_256_KEY;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+public class ProguardMapChecker implements StringConsumer {
+
+  private final StringConsumer inner;
+  private final StringBuilder contents = new StringBuilder();
+
+  ProguardMapChecker(StringConsumer inner) {
+    if (!InternalOptions.assertionsEnabled()) {
+      // Make sure we never get here without assertions enabled.
+      throw new Unreachable();
+    }
+    this.inner = inner;
+  }
+
+  @Override
+  public void accept(String string, DiagnosticsHandler handler) {
+    inner.accept(string, handler);
+    contents.append(string);
+  }
+
+  @Override
+  public void finished(DiagnosticsHandler handler) {
+    inner.finished(handler);
+    String stringContent = contents.toString();
+    assert validateProguardMapParses(stringContent);
+    assert validateProguardMapHash(stringContent).isOk();
+  }
+
+  private static boolean validateProguardMapParses(String content) {
+    try {
+      ClassNameMapper.mapperFromString(content);
+    } catch (IOException e) {
+      e.printStackTrace();
+      return false;
+    }
+    return true;
+  }
+
+  public static class VerifyMappingFileHashResult {
+
+    private final boolean error;
+    private final String message;
+
+    public static VerifyMappingFileHashResult createOk() {
+      return new VerifyMappingFileHashResult(false, null);
+    }
+
+    public static VerifyMappingFileHashResult createInfo(String message) {
+      return new VerifyMappingFileHashResult(false, message);
+    }
+
+    public static VerifyMappingFileHashResult createError(String message) {
+      return new VerifyMappingFileHashResult(true, message);
+    }
+
+    private VerifyMappingFileHashResult(boolean error, String message) {
+      this.error = error;
+      this.message = message;
+    }
+
+    public boolean isOk() {
+      return !error && message == null;
+    }
+
+    public boolean isError() {
+      return error;
+    }
+
+    public String getMessage() {
+      assert message != null;
+      return message;
+    }
+  }
+
+  public static VerifyMappingFileHashResult validateProguardMapHash(String content) {
+    int lineEnd = -1;
+    while (true) {
+      int lineStart = lineEnd + 1;
+      lineEnd = content.indexOf('\n', lineStart);
+      if (lineEnd < 0) {
+        return VerifyMappingFileHashResult.createInfo("Failure to find map hash");
+      }
+      String line = content.substring(lineStart, lineEnd).trim();
+      if (line.isEmpty()) {
+        // Ignore empty lines in the header.
+        continue;
+      }
+      if (line.charAt(0) != '#') {
+        // At the first non-empty non-comment line we assume that the file has no hash marker.
+        return VerifyMappingFileHashResult.createInfo("Failure to find map hash in header");
+      }
+      String headerLine = line.substring(1).trim();
+      if (headerLine.startsWith(MARKER_KEY_PG_MAP_HASH)) {
+        int shaIndex = headerLine.indexOf(SHA_256_KEY + " ", MARKER_KEY_PG_MAP_HASH.length());
+        if (shaIndex < 0) {
+          return VerifyMappingFileHashResult.createError(
+              "Unknown map hash function: '" + headerLine + "'");
+        }
+        String headerHash = headerLine.substring(shaIndex + SHA_256_KEY.length()).trim();
+        // We are on the hash line. Everything past this line is the hashed content.
+        Hasher hasher = Hashing.sha256().newHasher();
+        String hashedContent = content.substring(lineEnd + 1);
+        hasher.putString(hashedContent, StandardCharsets.UTF_8);
+        String computedHash = hasher.hash().toString();
+        return headerHash.equals(computedHash)
+            ? VerifyMappingFileHashResult.createOk()
+            : VerifyMappingFileHashResult.createError(
+                "Mismatching map hash: '" + headerHash + "' != '" + computedHash + "'");
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapMarkerInfo.java b/src/main/java/com/android/tools/r8/naming/ProguardMapMarkerInfo.java
new file mode 100644
index 0000000..8394ecf
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapMarkerInfo.java
@@ -0,0 +1,112 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming;
+
+import com.android.tools.r8.Version;
+import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapId;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.VersionProperties;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ProguardMapMarkerInfo {
+
+  private static final String MARKER_KEY_COMPILER = "compiler";
+  private static final String MARKER_KEY_COMPILER_VERSION = "compiler_version";
+  private static final String MARKER_KEY_COMPILER_HASH = "compiler_hash";
+  private static final String MARKER_KEY_MIN_API = "min_api";
+  private static final String MARKER_KEY_PG_MAP_ID = "pg_map_id";
+  public static final String MARKER_KEY_PG_MAP_HASH = "pg_map_hash";
+  public static final String SHA_256_KEY = "SHA-256";
+
+  private final String compilerName;
+  private final boolean isGeneratingDex;
+  private final AndroidApiLevel apiLevel;
+  private final MapVersion mapVersion;
+  private final ProguardMapId proguardMapId;
+
+  private ProguardMapMarkerInfo(
+      String compilerName,
+      boolean isGeneratingDex,
+      AndroidApiLevel apiLevel,
+      MapVersion mapVersion,
+      ProguardMapId proguardMapId) {
+    this.compilerName = compilerName;
+    this.isGeneratingDex = isGeneratingDex;
+    this.apiLevel = apiLevel;
+    this.mapVersion = mapVersion;
+    this.proguardMapId = proguardMapId;
+  }
+
+  public List<String> toPreamble() {
+    List<String> preamble = new ArrayList<>();
+    preamble.add("# " + MARKER_KEY_COMPILER + ": " + compilerName);
+    preamble.add("# " + MARKER_KEY_COMPILER_VERSION + ": " + Version.LABEL);
+    if (isGeneratingDex) {
+      preamble.add("# " + MARKER_KEY_MIN_API + ": " + apiLevel.getLevel());
+    }
+    if (Version.isDevelopmentVersion()) {
+      preamble.add("# " + MARKER_KEY_COMPILER_HASH + ": " + VersionProperties.INSTANCE.getSha());
+    }
+    // Turn off linting of the mapping file in some build systems.
+    preamble.add("# common_typos_disable");
+    // Emit the R8 specific map-file version.
+    if (mapVersion.isGreaterThan(MapVersion.MAP_VERSION_NONE)) {
+      preamble.add("# " + mapVersion.toMapVersionMappingInformation().serialize());
+    }
+    preamble.add("# " + MARKER_KEY_PG_MAP_ID + ": " + proguardMapId.getId());
+    preamble.add(
+        "# " + MARKER_KEY_PG_MAP_HASH + ": " + SHA_256_KEY + " " + proguardMapId.getHash());
+    return preamble;
+  }
+
+  public String serializeToString() {
+    return StringUtils.unixLines(toPreamble());
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private String compilerName;
+    private boolean isGeneratingDex;
+    private AndroidApiLevel apiLevel;
+    private MapVersion mapVersion;
+    private ProguardMapId proguardMapId;
+
+    public Builder setCompilerName(String compilerName) {
+      this.compilerName = compilerName;
+      return this;
+    }
+
+    public Builder setApiLevel(AndroidApiLevel apiLevel) {
+      this.apiLevel = apiLevel;
+      return this;
+    }
+
+    public Builder setGeneratingDex(boolean generatingDex) {
+      isGeneratingDex = generatingDex;
+      return this;
+    }
+
+    public Builder setMapVersion(MapVersion mapVersion) {
+      this.mapVersion = mapVersion;
+      return this;
+    }
+
+    public Builder setProguardMapId(ProguardMapId proguardMapId) {
+      this.proguardMapId = proguardMapId;
+      return this;
+    }
+
+    public ProguardMapMarkerInfo build() {
+      return new ProguardMapMarkerInfo(
+          compilerName, isGeneratingDex, apiLevel, mapVersion, proguardMapId);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapStringConsumer.java b/src/main/java/com/android/tools/r8/naming/ProguardMapStringConsumer.java
new file mode 100644
index 0000000..95bbd38
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapStringConsumer.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ProguardMapConsumer;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.utils.ChainableStringConsumer;
+
+/***
+ * Default implementation of a ProguardMapConsumer that wraps around a string consumer for streamed
+ * string output.
+ */
+public class ProguardMapStringConsumer extends ProguardMapConsumer
+    implements ChainableStringConsumer {
+
+  private final StringConsumer stringConsumer;
+  private final DiagnosticsHandler diagnosticsHandler;
+
+  private ProguardMapStringConsumer(
+      StringConsumer stringConsumer, DiagnosticsHandler diagnosticsHandler) {
+    assert stringConsumer != null;
+    assert diagnosticsHandler != null;
+    this.stringConsumer = stringConsumer;
+    this.diagnosticsHandler = diagnosticsHandler;
+  }
+
+  @Override
+  public void accept(ProguardMapMarkerInfo markerInfo, ClassNameMapper classNameMapper) {
+    accept(markerInfo.serializeToString());
+    classNameMapper.write(this);
+  }
+
+  @Override
+  public ChainableStringConsumer accept(String string) {
+    stringConsumer.accept(string, diagnosticsHandler);
+    return this;
+  }
+
+  public StringConsumer getStringConsumer() {
+    return stringConsumer;
+  }
+
+  @Override
+  public void finished(DiagnosticsHandler handler) {
+    stringConsumer.finished(handler);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private StringConsumer stringConsumer;
+    private DiagnosticsHandler diagnosticsHandler;
+
+    public Builder setStringConsumer(StringConsumer stringConsumer) {
+      this.stringConsumer = stringConsumer;
+      return this;
+    }
+
+    public Builder setDiagnosticsHandler(DiagnosticsHandler diagnosticsHandler) {
+      this.diagnosticsHandler = diagnosticsHandler;
+      return this;
+    }
+
+    public ProguardMapStringConsumer build() {
+      return new ProguardMapStringConsumer(stringConsumer, diagnosticsHandler);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
index a7f62dd..c3fc071 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
@@ -3,33 +3,20 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
-import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.MapIdEnvironment;
 import com.android.tools.r8.MapIdProvider;
-import com.android.tools.r8.StringConsumer;
-import com.android.tools.r8.Version;
+import com.android.tools.r8.ProguardMapConsumer;
 import com.android.tools.r8.dex.Marker.Tool;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.ChainableStringConsumer;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
-import com.android.tools.r8.utils.VersionProperties;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
-import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 
 public class ProguardMapSupplier {
 
-  public static final String MARKER_KEY_COMPILER = "compiler";
-  public static final String MARKER_KEY_COMPILER_VERSION = "compiler_version";
-  public static final String MARKER_KEY_COMPILER_HASH = "compiler_hash";
-  public static final String MARKER_KEY_MIN_API = "min_api";
-  public static final String MARKER_KEY_PG_MAP_ID = "pg_map_id";
-  public static final String MARKER_KEY_PG_MAP_HASH = "pg_map_hash";
-  public static final String SHA_256_KEY = "SHA-256";
-
   public static int PG_MAP_ID_LENGTH = 7;
 
   // Hash of the Proguard map (excluding the header up to and including the hash marker).
@@ -56,8 +43,8 @@
   }
 
   private final ClassNameMapper classNameMapper;
-  private final StringConsumer consumer;
   private final InternalOptions options;
+  private final ProguardMapConsumer consumer;
   private final Reporter reporter;
   private final Tool compiler;
 
@@ -79,11 +66,18 @@
   }
 
   public ProguardMapId writeProguardMap() {
-    ProguardMapId id = computeProguardMapId();
-    writeMarker(id);
-    writeBody();
-    ExceptionUtils.withFinishedResourceHandler(reporter, consumer);
-    return id;
+    ProguardMapId proguardMapId = computeProguardMapId();
+    consumer.accept(
+        ProguardMapMarkerInfo.builder()
+            .setCompilerName(compiler.name())
+            .setProguardMapId(proguardMapId)
+            .setGeneratingDex(options.isGeneratingDex())
+            .setApiLevel(options.getMinApiLevel())
+            .setMapVersion(options.getMapFileVersion())
+            .build(),
+        classNameMapper);
+    ExceptionUtils.withConsumeResourceHandler(reporter, this.consumer::finished);
+    return proguardMapId;
   }
 
   private ProguardMapId computeProguardMapId() {
@@ -92,52 +86,6 @@
     return builder.build(options.mapIdProvider);
   }
 
-  private void writeBody() {
-    classNameMapper.write(new ProguardMapWriter());
-  }
-
-  private void writeMarker(ProguardMapId id) {
-    StringBuilder builder = new StringBuilder();
-    builder.append(
-        "# "
-            + MARKER_KEY_COMPILER
-            + ": "
-            + compiler.name()
-            + "\n"
-            + "# "
-            + MARKER_KEY_COMPILER_VERSION
-            + ": "
-            + Version.LABEL
-            + "\n");
-    if (options.isGeneratingDex()) {
-      builder.append("# " + MARKER_KEY_MIN_API + ": " + options.getMinApiLevel().getLevel() + "\n");
-    }
-    if (Version.isDevelopmentVersion()) {
-      builder.append(
-          "# " + MARKER_KEY_COMPILER_HASH + ": " + VersionProperties.INSTANCE.getSha() + "\n");
-    }
-    // Turn off linting of the mapping file in some build systems.
-    builder.append("# common_typos_disable" + "\n");
-    // Emit the R8 specific map-file version.
-    MapVersion mapVersion = options.getMapFileVersion();
-    if (mapVersion.isGreaterThan(MapVersion.MAP_VERSION_NONE)) {
-      builder
-          .append("# ")
-          .append(mapVersion.toMapVersionMappingInformation().serialize())
-          .append("\n");
-    }
-    builder.append("# " + MARKER_KEY_PG_MAP_ID + ": " + id.getId() + "\n");
-    // Place the map hash as the last header item. Everything past this line is the hashed content.
-    builder
-        .append("# ")
-        .append(MARKER_KEY_PG_MAP_HASH)
-        .append(": ")
-        .append(SHA_256_KEY)
-        .append(" ")
-        .append(id.getHash())
-        .append("\n");
-    consumer.accept(builder.toString(), reporter);
-  }
 
   static class ProguardMapIdBuilder implements ChainableStringConsumer {
 
@@ -171,123 +119,4 @@
     }
   }
 
-  class ProguardMapWriter implements ChainableStringConsumer {
-
-    @Override
-    public ProguardMapWriter accept(String string) {
-      consumer.accept(string, reporter);
-      return this;
-    }
-  }
-
-  public static class ProguardMapChecker implements StringConsumer {
-
-    private final StringConsumer inner;
-    private final StringBuilder contents = new StringBuilder();
-
-    ProguardMapChecker(StringConsumer inner) {
-      if (!InternalOptions.assertionsEnabled()) {
-        // Make sure we never get here without assertions enabled.
-        throw new Unreachable();
-      }
-      this.inner = inner;
-    }
-
-    @Override
-    public void accept(String string, DiagnosticsHandler handler) {
-      inner.accept(string, handler);
-      contents.append(string);
-    }
-
-    @Override
-    public void finished(DiagnosticsHandler handler) {
-      inner.finished(handler);
-      String stringContent = contents.toString();
-      assert validateProguardMapParses(stringContent);
-      assert validateProguardMapHash(stringContent).isOk();
-    }
-
-    private static boolean validateProguardMapParses(String content) {
-      try {
-        ClassNameMapper.mapperFromString(content);
-      } catch (IOException e) {
-        e.printStackTrace();
-        return false;
-      }
-      return true;
-    }
-
-    public static class VerifyMappingFileHashResult {
-      private final boolean error;
-      private final String message;
-
-      public static VerifyMappingFileHashResult createOk() {
-        return new VerifyMappingFileHashResult(false, null);
-      }
-
-      public static VerifyMappingFileHashResult createInfo(String message) {
-        return new VerifyMappingFileHashResult(false, message);
-      }
-
-      public static VerifyMappingFileHashResult createError(String message) {
-        return new VerifyMappingFileHashResult(true, message);
-      }
-
-      private VerifyMappingFileHashResult(boolean error, String message) {
-        this.error = error;
-        this.message = message;
-      }
-
-      public boolean isOk() {
-        return !error && message == null;
-      }
-
-      public boolean isError() {
-        return error;
-      }
-
-      public String getMessage() {
-        assert message != null;
-        return message;
-      }
-    }
-
-    public static VerifyMappingFileHashResult validateProguardMapHash(String content) {
-      int lineEnd = -1;
-      while (true) {
-        int lineStart = lineEnd + 1;
-        lineEnd = content.indexOf('\n', lineStart);
-        if (lineEnd < 0) {
-          return VerifyMappingFileHashResult.createInfo("Failure to find map hash");
-        }
-        String line = content.substring(lineStart, lineEnd).trim();
-        if (line.isEmpty()) {
-          // Ignore empty lines in the header.
-          continue;
-        }
-        if (line.charAt(0) != '#') {
-          // At the first non-empty non-comment line we assume that the file has no hash marker.
-          return VerifyMappingFileHashResult.createInfo("Failure to find map hash in header");
-        }
-        String headerLine = line.substring(1).trim();
-        if (headerLine.startsWith(MARKER_KEY_PG_MAP_HASH)) {
-          int shaIndex = headerLine.indexOf(SHA_256_KEY + " ", MARKER_KEY_PG_MAP_HASH.length());
-          if (shaIndex < 0) {
-            return VerifyMappingFileHashResult.createError(
-                "Unknown map hash function: '" + headerLine + "'");
-          }
-          String headerHash = headerLine.substring(shaIndex + SHA_256_KEY.length()).trim();
-          // We are on the hash line. Everything past this line is the hashed content.
-          Hasher hasher = Hashing.sha256().newHasher();
-          String hashedContent = content.substring(lineEnd + 1);
-          hasher.putString(hashedContent, StandardCharsets.UTF_8);
-          String computedHash = hasher.hash().toString();
-          return headerHash.equals(computedHash)
-              ? VerifyMappingFileHashResult.createOk()
-              : VerifyMappingFileHashResult.createError(
-                  "Mismatching map hash: '" + headerHash + "' != '" + computedHash + "'");
-        }
-      }
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionMetadataInternal.java b/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionMetadataInternal.java
index ec9449a..8c8ee27 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionMetadataInternal.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/MappingPartitionMetadataInternal.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.naming.MapVersion;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.retrace.MappingPartitionMetadata;
-import com.android.tools.r8.retrace.RetracePartitionException;
 import com.android.tools.r8.retrace.internal.MetadataPartitionCollection.LazyMetadataPartitionCollection;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.google.common.primitives.Ints;
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java
index 98ac266..0298816 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java
@@ -18,7 +18,6 @@
 import com.android.tools.r8.retrace.ProguardMapPartitioner;
 import com.android.tools.r8.retrace.ProguardMapPartitionerBuilder;
 import com.android.tools.r8.retrace.ProguardMapProducer;
-import com.android.tools.r8.retrace.RetracePartitionException;
 import com.android.tools.r8.retrace.internal.MappingPartitionMetadataInternal.ObfuscatedTypeNameAsKeyMetadata;
 import com.android.tools.r8.retrace.internal.MappingPartitionMetadataInternal.ObfuscatedTypeNameAsKeyMetadataWithPartitionNames;
 import com.android.tools.r8.retrace.internal.ProguardMapReaderWithFiltering.ProguardMapReaderWithFilteringInputBuffer;
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMappingSupplierImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMappingSupplierImpl.java
index 14f41ed..d791a5c 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMappingSupplierImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMappingSupplierImpl.java
@@ -8,8 +8,8 @@
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.LineReader;
 import com.android.tools.r8.naming.MapVersion;
-import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapChecker;
-import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapChecker.VerifyMappingFileHashResult;
+import com.android.tools.r8.naming.ProguardMapChecker;
+import com.android.tools.r8.naming.ProguardMapChecker.VerifyMappingFileHashResult;
 import com.android.tools.r8.naming.mappinginformation.MapVersionMappingInformation;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.retrace.InvalidMappingFileException;
diff --git a/src/main/java/com/android/tools/r8/retrace/RetracePartitionException.java b/src/main/java/com/android/tools/r8/retrace/internal/RetracePartitionException.java
similarity index 89%
rename from src/main/java/com/android/tools/r8/retrace/RetracePartitionException.java
rename to src/main/java/com/android/tools/r8/retrace/internal/RetracePartitionException.java
index 7751302..dd183f2 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetracePartitionException.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetracePartitionException.java
@@ -2,7 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-package com.android.tools.r8.retrace;
+package com.android.tools.r8.retrace.internal;
 
 import com.android.tools.r8.Keep;
 
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java b/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
index 7a77e25..653e567 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
@@ -14,8 +14,11 @@
 import com.android.tools.r8.DexIndexedConsumer.ForwardingConsumer;
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.ProguardMapConsumer;
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.naming.MultiProguardMapConsumer;
+import com.android.tools.r8.naming.ProguardMapStringConsumer;
 import com.android.tools.r8.origin.Origin;
 import com.google.common.io.ByteStreams;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
@@ -33,7 +36,7 @@
   private boolean closed = false;
 
   private ProgramConsumer programConsumer = null;
-  private StringConsumer proguardMapConsumer = null;
+  private ProguardMapConsumer proguardMapConsumer = null;
 
   public AndroidAppConsumers() {
     // Nothing to do.
@@ -45,7 +48,8 @@
 
   public AndroidAppConsumers(InternalOptions options) {
     options.programConsumer = wrapProgramConsumer(options.programConsumer);
-    options.proguardMapConsumer = wrapProguardMapConsumer(options.proguardMapConsumer);
+    options.proguardMapConsumer =
+        wrapProguardMapConsumer(options.proguardMapConsumer, options.reporter);
   }
 
   public ProgramConsumer wrapProgramConsumer(ProgramConsumer consumer) {
@@ -65,30 +69,37 @@
     return programConsumer;
   }
 
-  public StringConsumer wrapProguardMapConsumer(StringConsumer consumer) {
+  public ProguardMapConsumer wrapProguardMapConsumer(
+      ProguardMapConsumer consumer, DiagnosticsHandler diagnosticsHandler) {
     assert proguardMapConsumer == null;
     if (consumer != null) {
       proguardMapConsumer =
-          new StringConsumer.ForwardingConsumer(consumer) {
-            StringBuilder stringBuilder = null;
+          MultiProguardMapConsumer.builder()
+              .addProguardMapConsumer(consumer)
+              .addProguardMapConsumer(
+                  ProguardMapStringConsumer.builder()
+                      .setStringConsumer(
+                          new StringConsumer() {
+                            StringBuilder stringBuilder = null;
 
-            @Override
-            public void accept(String string, DiagnosticsHandler handler) {
-              super.accept(string, handler);
-              if (stringBuilder == null) {
-                stringBuilder = new StringBuilder();
-              }
-              stringBuilder.append(string);
-            }
+                            @Override
+                            public void accept(String string, DiagnosticsHandler handler) {
+                              if (stringBuilder == null) {
+                                stringBuilder = new StringBuilder();
+                              }
+                              stringBuilder.append(string);
+                            }
 
-            @Override
-            public void finished(DiagnosticsHandler handler) {
-              super.finished(handler);
-              if (stringBuilder != null) {
-                builder.setProguardMapOutputData(stringBuilder.toString());
-              }
-            }
-          };
+                            @Override
+                            public void finished(DiagnosticsHandler handler) {
+                              if (stringBuilder != null) {
+                                builder.setProguardMapOutputData(stringBuilder.toString());
+                              }
+                            }
+                          })
+                      .setDiagnosticsHandler(diagnosticsHandler)
+                      .build())
+              .build();
     }
     return proguardMapConsumer;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 3a51f8b..6c09eb1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.GlobalSyntheticsConsumer;
 import com.android.tools.r8.MapIdProvider;
 import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.ProguardMapConsumer;
 import com.android.tools.r8.SourceFileProvider;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.SyntheticInfoConsumer;
@@ -1025,7 +1026,7 @@
 
   // If null, no proguard map needs to be computed.
   // If non null it must be and passed to the consumer.
-  public StringConsumer proguardMapConsumer = null;
+  public ProguardMapConsumer proguardMapConsumer = null;
 
   // If null, no usage information needs to be computed.
   // If non-null, it must be and is passed to the consumer.
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 a561718..9885ab6 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -248,7 +248,11 @@
   }
 
   public static String unixLines(String... lines) {
-    return lines(Arrays.asList(lines), "\n");
+    return unixLines(Arrays.asList(lines));
+  }
+
+  public static String unixLines(List<String> lines) {
+    return lines(lines, "\n");
   }
 
   public static String withNativeLineSeparator(String s) {
diff --git a/src/test/java/com/android/tools/r8/L8CommandTest.java b/src/test/java/com/android/tools/r8/L8CommandTest.java
index e2d8b20..4b42d31 100644
--- a/src/test/java/com/android/tools/r8/L8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/L8CommandTest.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.StringConsumer.FileConsumer;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
 import com.android.tools.r8.dex.Marker.Tool;
+import com.android.tools.r8.naming.ProguardMapStringConsumer;
 import com.android.tools.r8.origin.EmbeddedOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.references.Reference;
@@ -186,8 +187,10 @@
     assertNotNull(parsedCommand.getR8Command());
     InternalOptions internalOptions = parsedCommand.getR8Command().getInternalOptions();
     assertNotNull(internalOptions);
-    assertTrue(internalOptions.proguardMapConsumer instanceof StringConsumer.FileConsumer);
-    FileConsumer proguardMapConsumer = (FileConsumer) internalOptions.proguardMapConsumer;
+    assertTrue(internalOptions.proguardMapConsumer instanceof ProguardMapStringConsumer);
+    ProguardMapStringConsumer mapStringConsumer =
+        (ProguardMapStringConsumer) internalOptions.proguardMapConsumer;
+    FileConsumer proguardMapConsumer = (FileConsumer) mapStringConsumer.getStringConsumer();
     assertEquals(pgMap, proguardMapConsumer.getOutputPath());
   }
 
diff --git a/src/test/java/com/android/tools/r8/ProguardMapMarkerTest.java b/src/test/java/com/android/tools/r8/ProguardMapMarkerTest.java
index f827134..e38ecf7 100644
--- a/src/test/java/com/android/tools/r8/ProguardMapMarkerTest.java
+++ b/src/test/java/com/android/tools/r8/ProguardMapMarkerTest.java
@@ -25,6 +25,7 @@
 
 @RunWith(Parameterized.class)
 public class ProguardMapMarkerTest extends TestBase {
+
   private static final int EXPECTED_NUMBER_OF_KEYS_DEX = 6;
   private static final int EXPECTED_NUMBER_OF_KEYS_CF = 5;
   private static final String CLASS_FILE =
@@ -85,11 +86,10 @@
             .setMinApiLevel(minApiLevel.getLevel())
             .setProguardMapConsumer(
                 ToolHelper.consumeString(
-                    proguardMap -> {
-                      proguardMapIds.fromMap =
-                          verifyMarkersGetPgMapId(
-                              proguardMap, minApiLevel.getLevel(), EXPECTED_NUMBER_OF_KEYS_DEX);
-                    }))
+                    proguardMap ->
+                        proguardMapIds.fromMap =
+                            verifyMarkersGetPgMapId(
+                                proguardMap, minApiLevel.getLevel(), EXPECTED_NUMBER_OF_KEYS_DEX)))
             .build());
     verifyProguardMapIds(proguardMapIds);
   }
@@ -154,21 +154,28 @@
       }
       String key = comment.substring(0, colonIndex).trim();
       String value = comment.substring(colonIndex + 1).trim();
-      if (key.equals(ProguardMapSupplier.MARKER_KEY_COMPILER)) {
-        assertEquals("R8", value);
-      } else if (key.equals(ProguardMapSupplier.MARKER_KEY_COMPILER_VERSION)) {
-        assertEquals(Version.LABEL, value);
-      } else if (key.equals(ProguardMapSupplier.MARKER_KEY_MIN_API)) {
-        assertNotNull(minApiLevel);
-        assertEquals(minApiLevel.intValue(), Integer.parseInt(value));
-      } else if (key.equals(ProguardMapSupplier.MARKER_KEY_COMPILER_HASH)) {
-        assertEquals(VersionProperties.INSTANCE.getSha(), value);
-      } else if (key.equals(ProguardMapSupplier.MARKER_KEY_PG_MAP_ID)) {
-        proguardMapId = value;
-      } else if (key.equals(ProguardMapSupplier.MARKER_KEY_PG_MAP_HASH)) {
-        proguardMapHash = value;
-      } else {
-        continue;
+      switch (key) {
+        case "compiler":
+          assertEquals("R8", value);
+          break;
+        case "compiler_version":
+          assertEquals(Version.LABEL, value);
+          break;
+        case "min_api":
+          assertNotNull(minApiLevel);
+          assertEquals(minApiLevel.intValue(), Integer.parseInt(value));
+          break;
+        case "compiler_hash":
+          assertEquals(VersionProperties.INSTANCE.getSha(), value);
+          break;
+        case "pg_map_id":
+          proguardMapId = value;
+          break;
+        case "pg_map_hash":
+          proguardMapHash = value;
+          break;
+        default:
+          continue;
       }
       assertFalse(keysFound.contains(key));
       keysFound.add(key);
diff --git a/src/test/java/com/android/tools/r8/naming/MappingFileHashTest.java b/src/test/java/com/android/tools/r8/naming/MappingFileHashTest.java
index efc6cf7..1468bd8 100644
--- a/src/test/java/com/android/tools/r8/naming/MappingFileHashTest.java
+++ b/src/test/java/com/android/tools/r8/naming/MappingFileHashTest.java
@@ -8,8 +8,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapChecker;
-import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapChecker.VerifyMappingFileHashResult;
+import com.android.tools.r8.naming.ProguardMapChecker.VerifyMappingFileHashResult;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
diff --git a/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataUnknownTest.java b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataUnknownTest.java
index 345566b..01159fa 100644
--- a/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataUnknownTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionMetadataUnknownTest.java
@@ -12,8 +12,8 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.naming.MapVersion;
-import com.android.tools.r8.retrace.RetracePartitionException;
 import com.android.tools.r8.retrace.internal.MappingPartitionMetadataInternal;
+import com.android.tools.r8.retrace.internal.RetracePartitionException;
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
 import org.junit.Test;