// Copyright 2014 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.lib.exec; import static java.nio.charset.StandardCharsets.US_ASCII; import com.google.common.io.BaseEncoding; import com.google.devtools.build.lib.actions.cache.VirtualActionInput; import com.google.devtools.build.lib.util.Pair; import com.google.protobuf.ByteString; import com.google.protobuf.MessageLite; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * A utility class for obtaining MD5 digests. * Digests are represented as 32 characters in lowercase ASCII. */ public class Digest { public static final ByteString EMPTY_DIGEST = fromContent(new byte[]{}); private Digest() { } /** * Get the digest from the given byte array. * @param bytes the byte array. * @return a digest. */ public static ByteString fromContent(byte[] bytes) { MessageDigest md = newBuilder(); md.update(bytes, 0, bytes.length); return toByteString(BaseEncoding.base16().lowerCase().encode(md.digest())); } /** * Get the digest from the given ByteBuffer. * @param buffer the ByteBuffer. * @return a digest. */ public static ByteString fromBuffer(ByteBuffer buffer) { MessageDigest md = newBuilder(); md.update(buffer); return toByteString(BaseEncoding.base16().lowerCase().encode(md.digest())); } /** * Gets the digest of the given proto. * * @param proto a protocol buffer. * @return the digest. */ public static ByteString fromProto(MessageLite proto) { MD5OutputStream md5Stream = new MD5OutputStream(); try { proto.writeTo(md5Stream); } catch (IOException e) { throw new IllegalStateException("Unexpected IOException: ", e); } return toByteString(md5Stream.getDigest()); } /** * Gets the digest and size of a given VirtualActionInput. * * @param input the VirtualActionInput. * @return the digest and size. */ public static Pair<ByteString, Long> fromVirtualActionInput(VirtualActionInput input) throws IOException { CountingMD5OutputStream md5Stream = new CountingMD5OutputStream(); input.writeTo(md5Stream); ByteString digest = toByteString(md5Stream.getDigest()); return Pair.of(digest, md5Stream.getSize()); } /** * A Sink that does an online MD5 calculation, which avoids forcing us to keep the entire * proto message in memory. */ private static class MD5OutputStream extends OutputStream { private final MessageDigest md = newBuilder(); @Override public void write(int b) { md.update((byte) b); } @Override public void write(byte[] b, int off, int len) { md.update(b, off, len); } public String getDigest() { return BaseEncoding.base16().lowerCase().encode(md.digest()); } } private static final class CountingMD5OutputStream extends MD5OutputStream { private long size; @Override public void write(int b) { super.write(b); size++; } @Override public void write(byte[] b, int off, int len) { super.write(b, off, len); size += len; } public long getSize() { return size; } } /** * @param digest the digest to check. * @return true iff digest is a syntactically legal digest. It must be 32 * characters of hex with lowercase letters. */ public static boolean isDigest(ByteString digest) { if (digest == null || digest.size() != 32) { return false; } for (byte b : digest) { char c = (char) b; if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { continue; } return false; } return true; } /** * @param digest the digest. * @return true iff the digest is that of an empty file. */ public static boolean isEmpty(ByteString digest) { return digest.equals(EMPTY_DIGEST); } /** * @return a new MD5 digest builder. */ public static MessageDigest newBuilder() { try { return MessageDigest.getInstance("md5"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("MD5 not available", e); } } /** * Convert a String digest into a ByteString using ascii. * @param digest the digest in ascii. * @return the digest as a ByteString. */ public static ByteString toByteString(String digest) { return ByteString.copyFrom(digest.getBytes(US_ASCII)); } }