diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/Adler32.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/Adler32.java new file mode 100644 index 0000000..6d30ac6 --- /dev/null +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/Adler32.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.hash; + +import java.util.zip.Checksum; + +final class Adler32 extends ChecksumHashFunction { + static final Adler32 INSTANCE = new Adler32(); + + @Override + protected Checksum getHasher() { + return new java.util.zip.Adler32(); + } + + @Override + public String extension() { + return "alder32"; + } +} diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/CRC32.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/CRC32.java new file mode 100644 index 0000000..dba0e96 --- /dev/null +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/CRC32.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.hash; + +import java.util.zip.Checksum; + +final class CRC32 extends ChecksumHashFunction { + static final CRC32 INSTANCE = new CRC32(); + + @Override + protected Checksum getHasher() { + return new java.util.zip.CRC32(); + } + + @Override + public String extension() { + return "crc32"; + } +} diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/ChecksumHashFunction.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/ChecksumHashFunction.java new file mode 100644 index 0000000..c015175 --- /dev/null +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/ChecksumHashFunction.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.hash; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.zip.Checksum; + +abstract class ChecksumHashFunction extends HashFunction { + private static final String PADDING = String.format(Locale.ENGLISH, "%032d", 0); + + protected abstract Checksum getHasher(); + + @Override + public final String hash(Iterable files) throws IOException { + Checksum hasher = getHasher(); + byte[] buffer = new byte[8192]; + + for (File file : files) { + if (!file.exists()) + continue; + + try (FileInputStream fin = new FileInputStream(file)) { + int count = -1; + while ((count = fin.read(buffer)) != -1) + hasher.update(buffer, 0, count); + } + } + return pad(Long.toHexString(hasher.getValue())); + } + + @Override + public final String hash(InputStream inputStream) throws IOException { + Checksum hasher = getHasher(); + byte[] buffer = new byte[8192]; + int count = -1; + while ((count = inputStream.read(buffer)) != -1) + hasher.update(buffer, 0, count); + return pad(Long.toHexString(hasher.getValue())); + } + + @Override + public final String hash(byte[] bytes) { + Checksum hasher = getHasher(); + hasher.update(bytes, 0, bytes.length); + return pad(Long.toHexString(hasher.getValue())); + } + + @Override + public final String pad(String hash) { + return (PADDING + hash).substring(hash.length()); + } +} diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/HashFunction.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/HashFunction.java index f5b2a79..cb9165d 100644 --- a/hash-utils/src/main/java/net/minecraftforge/util/hash/HashFunction.java +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/HashFunction.java @@ -8,150 +8,89 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.Iterator; import java.util.Locale; -public enum HashFunction { - MD5 ("md5", 32), - SHA1 ("SHA-1", 40), - SHA256("SHA-256", 64), - SHA512("SHA-512", 128); - - private final String algo; - private final String pad; - private final String ext; - private Boolean supported; - - HashFunction(String algo, int length) { - this.algo = algo; - this.pad = String.format(Locale.ENGLISH, "%0" + length + "d", 0); - this.ext = this.name().toLowerCase(Locale.ENGLISH); +public abstract class HashFunction implements Iterable { + public abstract String extension(); + + public final String hash(File file) throws IOException { + try (FileInputStream fin = new FileInputStream(file)) { + return hash(fin); + } } - public String extension() { - return this.ext; + public final String hash(File... files) throws IOException { + return hash(Arrays.asList(files)); } - public static HashFunction find(String name) { - String cleaned = name.toUpperCase(Locale.ENGLISH); - for (HashFunction func : values()) { - if (cleaned.equals(func.name())) - return func; - } + public abstract String hash(Iterable files) throws IOException; - return null; + public final String hash(String data) { + return hash(data == null ? new byte[0] : data.getBytes()); } - public static HashFunction findByHash(String hash) { - int len = hash.length(); - for (HashFunction func : values()) { - if (func.pad.length() == len) - return func; - } + public abstract String hash(InputStream inputStream) throws IOException; + public abstract String hash(byte[] bytes); - return null; - } + public abstract String pad(String hash); - public boolean supported() { - if (supported == null) { - try { - MessageDigest.getInstance(algo); - supported = true; - } catch (NoSuchAlgorithmException e) { - supported = false; - } - } - return supported; + public static HashFunction alder32() { + return Adler32.INSTANCE; } - public MessageDigest get() { - try { - return MessageDigest.getInstance(algo); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + public static HashFunction crc32() { + return CRC32.INSTANCE; } - public String hash(File file) throws IOException { - try (FileInputStream fin = new FileInputStream(file)) { - return hash(fin); - } + public static HashFunction md5() { + return MD5.INSTANCE; } - public String sneakyHash(File file) { - try { - return hash(file); - } catch (IOException e) { - return HashUtils.sneak(e); - } + public static HashFunction sha1() { + return SHA1.INSTANCE; } - public String hash(File... files) throws IOException { - return hash(Arrays.asList(files)); + public static HashFunction sha256() { + return SHA256.INSTANCE; } - public String sneakyHash(File... files) { - try { - return hash(Arrays.asList(files)); - } catch (IOException e) { - return HashUtils.sneak(e); - } + public static HashFunction sha512() { + return SHA512.INSTANCE; } - public String hash(Iterable files) throws IOException { - MessageDigest hash = get(); - byte[] buf = new byte[1024]; - - for (File file : files) { - if (!file.exists()) - continue; - - try (FileInputStream fin = new FileInputStream(file)) { - int count = -1; - while ((count = fin.read(buf)) != -1) - hash.update(buf, 0, count); - } + public static HashFunction byName(String extension) { + switch (extension.toLowerCase(Locale.ROOT)) { + case "adler32": return Adler32.INSTANCE; + case "crc32": return CRC32.INSTANCE; + case "md5": return MD5.INSTANCE; + case "sha1": return SHA1.INSTANCE; + case "sha256": return SHA256.INSTANCE; + case "sha512": return SHA512.INSTANCE; + default: throw new UnsupportedOperationException("Unknown hash extension: " + extension); } - return pad(new BigInteger(1, hash.digest()).toString(16)); } - public String hash(String data) { - return hash(data == null ? new byte[0] : data.getBytes(StandardCharsets.UTF_8)); + public static HashFunction[] values() { + return new HashFunction[] { Adler32.INSTANCE, CRC32.INSTANCE, MD5.INSTANCE, SHA1.INSTANCE, SHA256.INSTANCE, SHA512.INSTANCE }; } - public String hash(InputStream stream) throws IOException { - MessageDigest hash = get(); - byte[] buf = new byte[1024]; - int count = -1; - while ((count = stream.read(buf)) != -1) - hash.update(buf, 0, count); - return pad(new BigInteger(1, hash.digest()).toString(16)); - } + @Override + public Iterator iterator() { + return new Iterator() { + private final HashFunction[] values = values(); + private int index = 0; - public String hash(byte[] data) { - return pad(new BigInteger(1, get().digest(data)).toString(16)); - } - - public String pad(String hash) { - return (pad + hash).substring(hash.length()); - } + @Override + public boolean hasNext() { + return values.length > index; + } - private static final byte[] HEX_ARRAY = "0123456789abcdef".getBytes(StandardCharsets.UTF_8); - public static String bytesToHex(byte[] bytes) { - if (bytes == null || bytes.length == 0) - return ""; - byte[] hexChars = new byte[bytes.length * 3 - 1]; - for (int j = 0, k = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[k++] = HEX_ARRAY[v >>> 4]; - hexChars[k++] = HEX_ARRAY[v & 0x0F]; - if (j < bytes.length - 1) - hexChars[k++] = ' '; - } - return new String(hexChars, StandardCharsets.UTF_8); + @Override + public HashFunction next() { + return values[index++]; + } + }; } } diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/HashStore.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/HashStore.java index ef3f7a2..106af98 100644 --- a/hash-utils/src/main/java/net/minecraftforge/util/hash/HashStore.java +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/HashStore.java @@ -13,20 +13,17 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.Map; import java.util.Objects; import static net.minecraftforge.util.hash.HashUtils.sneak; @NotNullByDefault public class HashStore { - private static final HashFunction HASH = HashFunction.SHA1; - + private static final HashFunction HASH = HashFunction.sha1(); private final String root; - private final Map oldHashes = new HashMap<>(); - private final Map newHashes = new HashMap<>(); + private final HashMap oldHashes = new HashMap<>(); + private final HashMap newHashes = new HashMap<>(); private @Nullable File target; private boolean saved; @@ -97,7 +94,7 @@ public HashStore load(File file) { try { for (String line : Files.readAllLines(file.toPath())) { - String[] split = line.split("="); + String[] split = line.split("=", 2); oldHashes.put(split[0], split[1]); } } catch (IOException e) { @@ -142,7 +139,7 @@ public HashStore add(@Nullable String key, File file) { if (file.isDirectory()) { String prefix = getPath(file); - for (File f : HashUtils.listFiles(file)) { + for (File f : listFiles(file)) { String suffix = getPath(f).substring(prefix.length()); this.newHashes.put(key + " - " + suffix, HASH.hash(f)); } @@ -189,9 +186,9 @@ public void save() { } public void save(File file) { - StringBuilder buf = new StringBuilder(); ArrayList keys = new ArrayList<>(this.newHashes.keySet()); - Collections.sort(keys); + keys.sort(null); + StringBuilder buf = new StringBuilder((keys.size() + 2) * 64); // rough estimate of size for (String key : keys) buf.append(key).append('=').append(this.newHashes.get(key)).append('\n'); @@ -208,6 +205,28 @@ public boolean isSaved() { return this.saved; } + private static ArrayList listFiles(File path) { + return listFiles(path, new ArrayList<>()); + } + + private static ArrayList listFiles(File dir, ArrayList files) { + if (!dir.exists()) + return files; + + if (!dir.isDirectory()) + throw new IllegalArgumentException("Path must be directory: " + dir.getAbsolutePath()); + + //noinspection DataFlowIssue - checked by File#isDirectory + for (File file : dir.listFiles()) { + if (file.isDirectory()) + files = listFiles(file, files); + else + files.add(file); + } + + return files; + } + private String getPath(File file) { String path = file.getAbsolutePath(); diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/HashUtils.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/HashUtils.java index 07852c3..7788bf4 100644 --- a/hash-utils/src/main/java/net/minecraftforge/util/hash/HashUtils.java +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/HashUtils.java @@ -5,13 +5,8 @@ package net.minecraftforge.util.hash; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.math.BigInteger; import java.nio.file.Files; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.List; public final class HashUtils { /** @@ -27,24 +22,9 @@ public static String[] bulkHash(File file, HashFunction... functions) throws IOE if (file == null || !file.exists()) return null; - MessageDigest[] digests = new MessageDigest[functions.length]; - for (int x = 0; x < functions.length; x++) - digests[x] = functions[x].get(); - - byte[] buf = new byte[1024]; - int count = -1; - try (FileInputStream stream = new FileInputStream(file)) { - while ((count = stream.read(buf)) != -1) { - for (MessageDigest digest : digests) - digest.update(buf, 0, count); - } - } - String[] ret = new String[functions.length]; for (int x = 0; x < functions.length; x++) { - HashFunction func = functions[x]; - MessageDigest digest = digests[x]; - ret[x] = func.pad(new BigInteger(1, digest.digest()).toString(16)); + ret[x] = functions[x].hash(file); } return ret; } @@ -84,28 +64,6 @@ public static void updateHash(File target, HashFunction... functions) throws IOE } } - static List listFiles(File path) { - return listFiles(path, new ArrayList<>()); - } - - private static List listFiles(File dir, List files) { - if (!dir.exists()) - return files; - - if (!dir.isDirectory()) - throw new IllegalArgumentException("Path must be directory: " + dir.getAbsolutePath()); - - //noinspection DataFlowIssue - checked by File#isDirectory - for (File file : dir.listFiles()) { - if (file.isDirectory()) - files = listFiles(file, files); - else - files.add(file); - } - - return files; - } - /** * Allows the given {@link Throwable} to be thrown without needing to declare it in the method signature or * arbitrarily checked at compile time. @@ -116,7 +74,7 @@ private static List listFiles(File dir, List files) { * @throws E Unconditionally thrown */ @SuppressWarnings("unchecked") - static R sneak(Throwable t) throws E { + public static R sneak(Throwable t) throws E { throw (E) t; } diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/MD5.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/MD5.java new file mode 100644 index 0000000..06cce35 --- /dev/null +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/MD5.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.hash; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; + +final class MD5 extends MessageDigestHashFunction { + private static final String PADDING = String.format(Locale.ENGLISH, "%032d", 0); + static final MD5 INSTANCE = new MD5(); + + @Override + protected MessageDigest getHasher() { + try { + return MessageDigest.getInstance("md5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public String extension() { + return "md5"; + } + + @Override + public String pad(String hash) { + return (PADDING + hash).substring(hash.length()); + } +} diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/MessageDigestHashFunction.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/MessageDigestHashFunction.java new file mode 100644 index 0000000..ba36ef1 --- /dev/null +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/MessageDigestHashFunction.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.hash; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; + +abstract class MessageDigestHashFunction extends HashFunction { + private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray(); + + protected abstract MessageDigest getHasher(); + + @Override + public final String hash(Iterable files) throws IOException { + MessageDigest hasher = getHasher(); + byte[] buffer = new byte[8192]; + + for (File file : files) { + if (!file.exists()) + continue; + + try (FileInputStream fin = new FileInputStream(file)) { + int count = -1; + while ((count = fin.read(buffer)) != -1) + hasher.update(buffer, 0, count); + } + } + return pad(bytesToHexString(hasher.digest())); + } + + @Override + public final String hash(InputStream inputStream) throws IOException { + MessageDigest hasher = getHasher(); + byte[] buffer = new byte[8192]; + int count = -1; + while ((count = inputStream.read(buffer)) != -1) + hasher.update(buffer, 0, count); + return pad(bytesToHexString(hasher.digest())); + } + + @Override + public final String hash(byte[] bytes) { + return pad(bytesToHexString(getHasher().digest(bytes))); + } + + private static String bytesToHexString(byte[] bytes) { + char[] chars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + chars[j * 2] = HEX_CHARS[v >>> 4]; + chars[j * 2 + 1] = HEX_CHARS[v & 0x0F]; + } + return new String(chars); + } +} diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/SHA1.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/SHA1.java new file mode 100644 index 0000000..c6101bd --- /dev/null +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/SHA1.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.hash; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; + +final class SHA1 extends MessageDigestHashFunction { + private static final String PADDING = String.format(Locale.ENGLISH, "%040d", 0); + static final SHA1 INSTANCE = new SHA1(); + + @Override + protected MessageDigest getHasher() { + try { + return MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public String extension() { + return "sha1"; + } + + @Override + public String pad(String hash) { + return (PADDING + hash).substring(hash.length()); + } +} diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/SHA256.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/SHA256.java new file mode 100644 index 0000000..7c8f1b4 --- /dev/null +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/SHA256.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.hash; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; + +final class SHA256 extends MessageDigestHashFunction { + private static final String PADDING = String.format(Locale.ENGLISH, "%064d", 0); + static final SHA256 INSTANCE = new SHA256(); + + @Override + protected MessageDigest getHasher() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public String extension() { + return "sha256"; + } + + @Override + public String pad(String hash) { + return (PADDING + hash).substring(hash.length()); + } +} diff --git a/hash-utils/src/main/java/net/minecraftforge/util/hash/SHA512.java b/hash-utils/src/main/java/net/minecraftforge/util/hash/SHA512.java new file mode 100644 index 0000000..e0fcc1a --- /dev/null +++ b/hash-utils/src/main/java/net/minecraftforge/util/hash/SHA512.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.hash; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; + +final class SHA512 extends MessageDigestHashFunction { + private static final String PADDING = String.format(Locale.ENGLISH, "%0128d", 0); + static final SHA512 INSTANCE = new SHA512(); + + @Override + protected MessageDigest getHasher() { + try { + return MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public String extension() { + return "sha512"; + } + + @Override + public String pad(String hash) { + return (PADDING + hash).substring(hash.length()); + } +}