/* * Copyright 2013-present Facebook, Inc. * * 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.facebook.buck.util.sha1; import com.google.common.base.Preconditions; import com.google.common.hash.HashCode; import com.google.common.hash.Hasher; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.regex.Pattern; /** * A typesafe representation of a SHA-1 hash. It is safer to pass this around than a {@code byte[]}. */ public final class Sha1HashCode { private static final int NUM_BYTES_IN_HASH = 20; private static final int NUM_BYTES_IN_HEX_REPRESENTATION = 2 * NUM_BYTES_IN_HASH; private static final Pattern SHA1_PATTERN = Pattern.compile(String.format("[a-f0-9]{%d}", NUM_BYTES_IN_HEX_REPRESENTATION)); private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /** * Because Guava's AbstractStreamingHasher uses a ByteBuffer with ByteOrder.LITTLE_ENDIAN: * * <p>https://github.com/google/guava/blob/v19.0/guava/src/com/google/common/hash/AbstractStreamingHashFunction.java#L121 * * <p>and the contract for ByteBuffer#putInt(int) is: * * <p>"Writes four bytes containing the given int value, in the current byte order, into this * buffer at the given index." * * <p>The primitive int and long fields stored by this class must use the same ByteOrder as * Guava's hashing logic to facilitate the implementation of the {@link #update(Hasher)} method. */ private static final ByteOrder BYTE_ORDER_FOR_FIELDS = ByteOrder.LITTLE_ENDIAN; // Primitive fields are used for storage so the data is stored with this class instead of on the // heap in a byte[]. final int firstFourBytes; final long nextEightBytes; final long lastEightBytes; private Sha1HashCode(int firstFourBytes, long nextEightBytes, long lastEightBytes) { this.firstFourBytes = firstFourBytes; this.nextEightBytes = nextEightBytes; this.lastEightBytes = lastEightBytes; } /** Clones the specified bytes and uses the clone to create a new {@link Sha1HashCode}. */ public static Sha1HashCode fromBytes(byte[] bytes) { Preconditions.checkArgument(bytes.length == NUM_BYTES_IN_HASH); ByteBuffer buffer = ByteBuffer.wrap(bytes).order(BYTE_ORDER_FOR_FIELDS); return new Sha1HashCode(buffer.getInt(), buffer.getLong(), buffer.getLong()); } public static Sha1HashCode fromHashCode(HashCode hashCode) { // Note that hashCode.asBytes() does a clone of its internal byte[], but we cannot avoid it. return fromBytes(hashCode.asBytes()); } public static Sha1HashCode of(String hash) { Preconditions.checkArgument( SHA1_PATTERN.matcher(hash).matches(), "Should be 40 lowercase hex chars: %s.", hash); // Note that this could be done with less memory if we created the byte[20] ourselves and // walked the string and converted the hex chars into bytes as we went. byte[] bytes = HashCode.fromString(hash).asBytes(); return fromBytes(bytes); } /** * Updates the specified {@link Hasher} by putting the 20 bytes of this SHA-1 to it in order. * * @return The specified {@link Hasher}. */ public Hasher update(Hasher hasher) { hasher.putInt(firstFourBytes); hasher.putLong(nextEightBytes); hasher.putLong(lastEightBytes); return hasher; } /** * <strong>This method should be used sparingly as we are trying to favor {@link Sha1HashCode} * over {@link HashCode}, where appropriate.</strong> Currently, the {@code FileHashCache} API is * written in terms of {@code HashCode}, so conversions are common. As we migrate it to use {@link * Sha1HashCode}, this method should become unnecessary. * * @return a {@link HashCode} with an equivalent value */ public HashCode asHashCode() { return HashCode.fromString(getHash()); } /** @return the hash as a 40-character string from the alphabet [a-f0-9]. */ public String getHash() { StringBuilder sb = new StringBuilder(NUM_BYTES_IN_HEX_REPRESENTATION); appendInt(sb, firstFourBytes); appendLong(sb, nextEightBytes); appendLong(sb, lastEightBytes); return sb.toString(); } private static void appendLong(StringBuilder sb, long bytes) { appendInt(sb, (int) bytes); appendInt(sb, (int) (bytes >>> 32)); } private static void appendInt(StringBuilder sb, int bytes) { appendByte(sb, (byte) bytes); appendByte(sb, (byte) (bytes >>> 8)); appendByte(sb, (byte) (bytes >>> 16)); appendByte(sb, (byte) (bytes >>> 24)); } private static void appendByte(StringBuilder sb, byte b) { sb.append(HEX_DIGITS[(b >>> 4) & 0xF]); sb.append(HEX_DIGITS[b & 0xF]); } /** Same as {@link #getHash()}. */ @Override public String toString() { return getHash(); } @Override public boolean equals(Object obj) { if (obj instanceof Sha1HashCode) { Sha1HashCode that = (Sha1HashCode) obj; return this.firstFourBytes == that.firstFourBytes && this.nextEightBytes == that.nextEightBytes && this.lastEightBytes == that.lastEightBytes; } return false; } @Override public int hashCode() { return firstFourBytes; } }