| // Copyright 2018 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.vfs; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.hash.HashFunction; |
| import com.google.common.hash.Hashing; |
| import com.google.devtools.build.lib.vfs.DigestHashFunction.DigestLength.DigestLengthImpl; |
| import com.google.devtools.common.options.Converter; |
| import com.google.devtools.common.options.OptionsParsingException; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Map.Entry; |
| |
| /** |
| * Type of hash function to use for digesting files. |
| * |
| * <p>This tracks parallel {@link java.security.MessageDigest} and {@link HashFunction} interfaces |
| * for each provided hash, as Bazel uses both - MessageDigest where performance is critical and |
| * HashFunctions where ease-of-use wins over. |
| */ |
| // The underlying HashFunctions are immutable and thread safe. |
| public class DigestHashFunction { |
| // This map must be declared first to make sure that calls to register() have it ready. |
| private static final HashMap<String, DigestHashFunction> hashFunctionRegistry = new HashMap<>(); |
| |
| /** Describes the length of a digest. */ |
| public interface DigestLength { |
| /** Returns the length of a digest by inspecting its bytes. Used for variable-length digests. */ |
| default int getDigestLength(byte[] bytes, int offset) { |
| return getDigestMaximumLength(); |
| } |
| |
| /** Returns the maximum length a digest can turn into. */ |
| int getDigestMaximumLength(); |
| |
| /** Default implementation that simply returns a fixed length. */ |
| class DigestLengthImpl implements DigestLength { |
| private final int length; |
| |
| DigestLengthImpl(HashFunction hashFunction) { |
| this.length = hashFunction.bits() / 8; |
| } |
| |
| @Override |
| public int getDigestMaximumLength() { |
| return length; |
| } |
| } |
| } |
| |
| public static final DigestHashFunction MD5 = register(Hashing.md5(), "MD5"); |
| public static final DigestHashFunction SHA1 = register(Hashing.sha1(), "SHA-1", "SHA1"); |
| public static final DigestHashFunction SHA256 = register(Hashing.sha256(), "SHA-256", "SHA256"); |
| |
| private static DigestHashFunction defaultHash; |
| private static boolean defaultHasBeenSet = false; |
| |
| private final HashFunction hashFunction; |
| private final DigestLength digestLength; |
| private final String name; |
| private final MessageDigest messageDigestPrototype; |
| private final boolean messageDigestPrototypeSupportsClone; |
| private final ImmutableList<String> names; |
| |
| private DigestHashFunction( |
| HashFunction hashFunction, DigestLength digestLength, ImmutableList<String> names) { |
| this.hashFunction = hashFunction; |
| this.digestLength = digestLength; |
| checkArgument(!names.isEmpty()); |
| this.name = names.get(0); |
| this.names = names; |
| this.messageDigestPrototype = getMessageDigestInstance(); |
| this.messageDigestPrototypeSupportsClone = supportsClone(messageDigestPrototype); |
| } |
| |
| public static DigestHashFunction register( |
| HashFunction hash, String hashName, String... altNames) { |
| return register(hash, new DigestLengthImpl(hash), hashName, altNames); |
| } |
| |
| /** |
| * Creates a new DigestHashFunction that is registered to be recognized by its name in {@link |
| * DigestFunctionConverter}. |
| * |
| * @param hashName the canonical name for this hash function - and the name that can be used to |
| * uncover the MessageDigest. |
| * @param altNames alternative names that will be mapped to this function by the converter but |
| * will not serve as the canonical name for the DigestHashFunction. |
| * @param hash The {@link HashFunction} to register. |
| * @throws IllegalArgumentException if the name is already registered. |
| */ |
| public static DigestHashFunction register( |
| HashFunction hash, DigestLength digestLength, String hashName, String... altNames) { |
| try { |
| MessageDigest.getInstance(hashName); |
| } catch (NoSuchAlgorithmException e) { |
| throw new IllegalArgumentException( |
| "The hash function name provided does not correspond to a valid MessageDigest: " |
| + hashName, |
| e); |
| } |
| |
| ImmutableList<String> names = |
| ImmutableList.<String>builder().add(hashName).add(altNames).build(); |
| DigestHashFunction hashFunction = new DigestHashFunction(hash, digestLength, names); |
| synchronized (hashFunctionRegistry) { |
| for (String name : names) { |
| if (hashFunctionRegistry.containsKey(name)) { |
| throw new IllegalArgumentException("Hash function " + name + " is already registered."); |
| } |
| hashFunctionRegistry.put(name, hashFunction); |
| } |
| } |
| return hashFunction; |
| } |
| |
| /** |
| * Returns the default DigestHashFunction for this instance of Bazel. |
| * |
| * <p>Note: This is a synchronized function, to make sure it does not occur concurrently with |
| * {@link #setDefault(DigestHashFunction)}. Once this value is set, it's a constant, so to prevent |
| * blocking calls, users should cache this value if needed. |
| * |
| * @throws DefaultHashFunctionNotSetException if the default has not yet been set by a previous |
| * call to {@link #setDefault}. |
| */ |
| public static synchronized DigestHashFunction getDefault() |
| throws DefaultHashFunctionNotSetException { |
| if (!defaultHasBeenSet) { |
| throw new DefaultHashFunctionNotSetException("DigestHashFunction default has not been set"); |
| } |
| return defaultHash; |
| } |
| |
| /** |
| * Returns the default DigestHashFunction, or the testing default if unset. |
| */ |
| public static DigestHashFunction getDefaultUnchecked() { |
| try { |
| return getDefault(); |
| } catch (DefaultHashFunctionNotSetException e) { |
| // Some tests use this class without calling GoogleUnixFileSystemModule.globalInit(). |
| Preconditions.checkState( |
| System.getenv("TEST_TMPDIR") != null, "Default hash function has not been set"); |
| return DEFAULT_HASH_FOR_TESTS; |
| } |
| } |
| |
| |
| /** Indicates that the default has not been initialized. */ |
| public static final class DefaultHashFunctionNotSetException extends Exception { |
| DefaultHashFunctionNotSetException(String message) { |
| super(message); |
| } |
| } |
| |
| /** |
| * Sets the default DigestHashFunction for this instance of Bazel - can only be set once to |
| * prevent incongruities. |
| * |
| * @throws DefaultAlreadySetException if it was already set. |
| */ |
| public static synchronized void setDefault(DigestHashFunction hash) |
| throws DefaultAlreadySetException { |
| if (defaultHasBeenSet) { |
| throw new DefaultAlreadySetException( |
| String.format( |
| "setDefault(%s) failed. The default has already been set to %s, you cannot reset it.", |
| hash.name, defaultHash.name)); |
| } |
| defaultHash = hash; |
| defaultHasBeenSet = true; |
| } |
| |
| /** Failure to set the default if the default already being set. */ |
| public static final class DefaultAlreadySetException extends Exception { |
| DefaultAlreadySetException(String message) { |
| super(message); |
| } |
| } |
| |
| /** Converts a string to its registered {@link DigestHashFunction}. */ |
| public static class DigestFunctionConverter implements Converter<DigestHashFunction> { |
| @Override |
| public DigestHashFunction convert(String input) throws OptionsParsingException { |
| for (Entry<String, DigestHashFunction> possibleFunctions : hashFunctionRegistry.entrySet()) { |
| if (possibleFunctions.getKey().equalsIgnoreCase(input)) { |
| return possibleFunctions.getValue(); |
| } |
| } |
| throw new OptionsParsingException("Not a valid hash function."); |
| } |
| |
| @Override |
| public String getTypeDescription() { |
| return "hash function"; |
| } |
| } |
| |
| public HashFunction getHashFunction() { |
| return hashFunction; |
| } |
| |
| public MessageDigest cloneOrCreateMessageDigest() { |
| if (messageDigestPrototypeSupportsClone) { |
| try { |
| return (MessageDigest) messageDigestPrototype.clone(); |
| } catch (CloneNotSupportedException e) { |
| // We checked at initialization that this could be cloned, so this should never happen. |
| throw new IllegalStateException("Could not clone message digest", e); |
| } |
| } else { |
| return getMessageDigestInstance(); |
| } |
| } |
| |
| public DigestLength getDigestLength() { |
| return digestLength; |
| } |
| |
| public ImmutableList<String> getNames() { |
| return names; |
| } |
| |
| @Override |
| public String toString() { |
| return name; |
| } |
| |
| private MessageDigest getMessageDigestInstance() { |
| try { |
| return MessageDigest.getInstance(name); |
| } catch (NoSuchAlgorithmException e) { |
| // We check when we register() this digest function that the message digest exists. This |
| // should never happen. |
| throw new IllegalStateException("message digest " + name + " not available", e); |
| } |
| } |
| |
| private static boolean supportsClone(MessageDigest toCheck) { |
| try { |
| toCheck.clone(); |
| return true; |
| } catch (CloneNotSupportedException e) { |
| return false; |
| } |
| } |
| |
| @VisibleForTesting |
| static Collection<DigestHashFunction> getPossibleHashFunctions() { |
| return hashFunctionRegistry.values(); |
| } |
| |
| /** |
| * For tests that are testing the FileSystems themselves, those test should be parametrized and |
| * run with all the standard hash functions that Bazel supports using {@link |
| * #getPossibleHashFunctions} above. For tests that just need a FileSystem to test some adjacent |
| * behavior, though, we use this default. |
| */ |
| @VisibleForTesting |
| public static final DigestHashFunction DEFAULT_HASH_FOR_TESTS = DigestHashFunction.MD5; |
| } |