| // Copyright (c) 2017, 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.MapIdEnvironment; | |
| import com.android.tools.r8.MapIdProvider; | |
| import com.android.tools.r8.StringConsumer; | |
| import com.android.tools.r8.Version; | |
| import com.android.tools.r8.errors.Unreachable; | |
| import com.android.tools.r8.naming.mappinginformation.MapVersionMappingInformation; | |
| 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_VALUE_COMPILER = "R8"; | |
| 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). | |
| public static class ProguardMapId { | |
| private final String id; | |
| private final String hash; | |
| private ProguardMapId(String id, String hash) { | |
| assert id != null; | |
| assert hash != null; | |
| this.id = id; | |
| this.hash = hash; | |
| } | |
| /** Id for the map file (user defined or a truncated prefix of the content hash). */ | |
| public String getId() { | |
| return id; | |
| } | |
| /** The actual content hash. */ | |
| public String getHash() { | |
| return hash; | |
| } | |
| } | |
| private final ClassNameMapper classNameMapper; | |
| private final StringConsumer consumer; | |
| private final InternalOptions options; | |
| private final Reporter reporter; | |
| private ProguardMapSupplier(ClassNameMapper classNameMapper, InternalOptions options) { | |
| assert classNameMapper != null; | |
| this.classNameMapper = classNameMapper.sorted(); | |
| this.consumer = | |
| InternalOptions.assertionsEnabled() | |
| ? new ProguardMapChecker(options.proguardMapConsumer) | |
| : options.proguardMapConsumer; | |
| this.options = options; | |
| this.reporter = options.reporter; | |
| } | |
| public static ProguardMapSupplier create( | |
| ClassNameMapper classNameMapper, InternalOptions options) { | |
| return new ProguardMapSupplier(classNameMapper, options); | |
| } | |
| public ProguardMapId writeProguardMap() { | |
| ProguardMapId id = computeProguardMapId(); | |
| writeMarker(id); | |
| writeBody(); | |
| ExceptionUtils.withFinishedResourceHandler(reporter, consumer); | |
| return id; | |
| } | |
| private ProguardMapId computeProguardMapId() { | |
| ProguardMapIdBuilder builder = new ProguardMapIdBuilder(); | |
| classNameMapper.write(builder); | |
| 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 | |
| + ": " | |
| + MARKER_VALUE_COMPILER | |
| + "\n" | |
| + "# " | |
| + MARKER_KEY_COMPILER_VERSION | |
| + ": " | |
| + Version.LABEL | |
| + "\n"); | |
| if (options.isGeneratingDex()) { | |
| builder.append("# " + MARKER_KEY_MIN_API + ": " + options.minApiLevel.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(new MapVersionMappingInformation(mapVersion).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 { | |
| private final Hasher hasher = Hashing.sha256().newHasher(); | |
| private MapIdProvider getProviderOrDefault(MapIdProvider provider) { | |
| return provider != null | |
| ? provider | |
| : environment -> environment.getMapHash().substring(0, PG_MAP_ID_LENGTH); | |
| } | |
| private MapIdEnvironment getEnvironment(String hash) { | |
| return new MapIdEnvironment() { | |
| @Override | |
| public String getMapHash() { | |
| return hash; | |
| } | |
| }; | |
| } | |
| @Override | |
| public ProguardMapIdBuilder accept(String string) { | |
| hasher.putString(string, StandardCharsets.UTF_8); | |
| return this; | |
| } | |
| public ProguardMapId build(MapIdProvider mapIdProvider) { | |
| String hash = hasher.hash().toString(); | |
| String id = getProviderOrDefault(mapIdProvider).get(getEnvironment(hash)); | |
| return new ProguardMapId(id, hash); | |
| } | |
| } | |
| 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 + "'"); | |
| } | |
| } | |
| } | |
| } | |
| } |