Skip to content

Commit 9a0dce6

Browse files
PaintNinjaLexManos
authored andcommitted
Rework HashUtils to support Alder32 and CRC32 and be faster
1 parent 994635d commit 9a0dce6

15 files changed

Lines changed: 625 additions & 159 deletions

File tree

hash-utils/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ java {
2323

2424
dependencies {
2525
compileOnly libs.nulls
26+
27+
testImplementation libs.junit.api
28+
testRuntimeOnly libs.bundles.junit.runtime
29+
}
30+
31+
test {
32+
useJUnitPlatform()
2633
}
2734

2835
tasks.named('jar', Jar) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) Forge Development LLC
3+
* SPDX-License-Identifier: LGPL-2.1-only
4+
*/
5+
package net.minecraftforge.util.hash;
6+
7+
import java.util.zip.Checksum;
8+
9+
final class Adler32 extends ChecksumHashFunction {
10+
static final Adler32 INSTANCE = new Adler32();
11+
12+
@Override
13+
protected Checksum getHasher() {
14+
return new java.util.zip.Adler32();
15+
}
16+
17+
@Override
18+
public String extension() {
19+
return "alder32";
20+
}
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) Forge Development LLC
3+
* SPDX-License-Identifier: LGPL-2.1-only
4+
*/
5+
package net.minecraftforge.util.hash;
6+
7+
import java.util.zip.Checksum;
8+
9+
final class CRC32 extends ChecksumHashFunction {
10+
static final CRC32 INSTANCE = new CRC32();
11+
12+
@Override
13+
protected Checksum getHasher() {
14+
return new java.util.zip.CRC32();
15+
}
16+
17+
@Override
18+
public String extension() {
19+
return "crc32";
20+
}
21+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright (c) Forge Development LLC
3+
* SPDX-License-Identifier: LGPL-2.1-only
4+
*/
5+
package net.minecraftforge.util.hash;
6+
7+
import java.io.File;
8+
import java.io.FileInputStream;
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
import java.util.Locale;
12+
import java.util.zip.Checksum;
13+
14+
abstract class ChecksumHashFunction extends HashFunction {
15+
private static final String PADDING = String.format(Locale.ENGLISH, "%08d", 0); // Both adler and crc are 32-bit
16+
17+
protected abstract Checksum getHasher();
18+
19+
@Override
20+
public HashInstance instance() {
21+
return new Instance(getHasher());
22+
}
23+
24+
@Override
25+
public final String hash(Iterable<File> files) throws IOException {
26+
Checksum hasher = getHasher();
27+
byte[] buffer = new byte[8192];
28+
29+
for (File file : files) {
30+
if (!file.exists())
31+
continue;
32+
33+
try (FileInputStream fin = new FileInputStream(file)) {
34+
int count = -1;
35+
while ((count = fin.read(buffer)) != -1)
36+
hasher.update(buffer, 0, count);
37+
}
38+
}
39+
return pad(Long.toHexString(hasher.getValue()));
40+
}
41+
42+
@Override
43+
public final String hash(InputStream inputStream) throws IOException {
44+
Checksum hasher = getHasher();
45+
byte[] buffer = new byte[8192];
46+
int count = -1;
47+
while ((count = inputStream.read(buffer)) != -1)
48+
hasher.update(buffer, 0, count);
49+
return pad(Long.toHexString(hasher.getValue()));
50+
}
51+
52+
@Override
53+
public final String hash(byte[] bytes) {
54+
Checksum hasher = getHasher();
55+
hasher.update(bytes, 0, bytes.length);
56+
return pad(Long.toHexString(hasher.getValue()));
57+
}
58+
59+
@Override
60+
public final String pad(String hash) {
61+
return (PADDING + hash).substring(hash.length());
62+
}
63+
64+
private final class Instance implements HashInstance {
65+
private final Checksum checksum;
66+
67+
private Instance(Checksum checksum) {
68+
this.checksum = checksum;
69+
}
70+
71+
@Override
72+
public void update(byte[] data, int offset, int length) {
73+
checksum.update(data, offset, length);
74+
}
75+
76+
@Override
77+
public String finish() {
78+
return ChecksumHashFunction.this.pad(Long.toHexString(checksum.getValue()));
79+
}
80+
}
81+
}

hash-utils/src/main/java/net/minecraftforge/util/hash/HashFunction.java

Lines changed: 58 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -8,150 +8,94 @@
88
import java.io.FileInputStream;
99
import java.io.IOException;
1010
import java.io.InputStream;
11-
import java.math.BigInteger;
12-
import java.nio.charset.StandardCharsets;
13-
import java.security.MessageDigest;
14-
import java.security.NoSuchAlgorithmException;
1511
import java.util.Arrays;
12+
import java.util.Iterator;
1613
import java.util.Locale;
1714

18-
public enum HashFunction {
19-
MD5 ("md5", 32),
20-
SHA1 ("SHA-1", 40),
21-
SHA256("SHA-256", 64),
22-
SHA512("SHA-512", 128);
23-
24-
private final String algo;
25-
private final String pad;
26-
private final String ext;
27-
private Boolean supported;
28-
29-
HashFunction(String algo, int length) {
30-
this.algo = algo;
31-
this.pad = String.format(Locale.ENGLISH, "%0" + length + "d", 0);
32-
this.ext = this.name().toLowerCase(Locale.ENGLISH);
33-
}
15+
public abstract class HashFunction implements Iterable<HashFunction> {
16+
public abstract String extension();
3417

35-
public String extension() {
36-
return this.ext;
37-
}
18+
/*
19+
* Retreives a new instance that can be used for incremental hashing.
20+
*/
21+
public abstract HashInstance instance();
3822

39-
public static HashFunction find(String name) {
40-
String cleaned = name.toUpperCase(Locale.ENGLISH);
41-
for (HashFunction func : values()) {
42-
if (cleaned.equals(func.name()))
43-
return func;
23+
public final String hash(File file) throws IOException {
24+
try (FileInputStream fin = new FileInputStream(file)) {
25+
return hash(fin);
4426
}
27+
}
4528

46-
return null;
29+
public final String hash(File... files) throws IOException {
30+
return hash(Arrays.asList(files));
4731
}
4832

49-
public static HashFunction findByHash(String hash) {
50-
int len = hash.length();
51-
for (HashFunction func : values()) {
52-
if (func.pad.length() == len)
53-
return func;
54-
}
33+
public abstract String hash(Iterable<File> files) throws IOException;
5534

56-
return null;
35+
public final String hash(String data) {
36+
return hash(data == null ? new byte[0] : data.getBytes());
5737
}
5838

59-
public boolean supported() {
60-
if (supported == null) {
61-
try {
62-
MessageDigest.getInstance(algo);
63-
supported = true;
64-
} catch (NoSuchAlgorithmException e) {
65-
supported = false;
66-
}
67-
}
68-
return supported;
69-
}
39+
public abstract String hash(InputStream inputStream) throws IOException;
40+
public abstract String hash(byte[] bytes);
7041

71-
public MessageDigest get() {
72-
try {
73-
return MessageDigest.getInstance(algo);
74-
} catch (NoSuchAlgorithmException e) {
75-
throw new RuntimeException(e);
76-
}
77-
}
42+
public abstract String pad(String hash);
7843

79-
public String hash(File file) throws IOException {
80-
try (FileInputStream fin = new FileInputStream(file)) {
81-
return hash(fin);
82-
}
44+
public static HashFunction alder32() {
45+
return Adler32.INSTANCE;
8346
}
8447

85-
public String sneakyHash(File file) {
86-
try {
87-
return hash(file);
88-
} catch (IOException e) {
89-
return HashUtils.sneak(e);
90-
}
48+
public static HashFunction crc32() {
49+
return CRC32.INSTANCE;
9150
}
9251

93-
public String hash(File... files) throws IOException {
94-
return hash(Arrays.asList(files));
52+
public static HashFunction md5() {
53+
return MD5.INSTANCE;
9554
}
9655

97-
public String sneakyHash(File... files) {
98-
try {
99-
return hash(Arrays.asList(files));
100-
} catch (IOException e) {
101-
return HashUtils.sneak(e);
102-
}
56+
public static HashFunction sha1() {
57+
return SHA1.INSTANCE;
10358
}
10459

105-
public String hash(Iterable<File> files) throws IOException {
106-
MessageDigest hash = get();
107-
byte[] buf = new byte[1024];
108-
109-
for (File file : files) {
110-
if (!file.exists())
111-
continue;
112-
113-
try (FileInputStream fin = new FileInputStream(file)) {
114-
int count = -1;
115-
while ((count = fin.read(buf)) != -1)
116-
hash.update(buf, 0, count);
117-
}
118-
}
119-
return pad(new BigInteger(1, hash.digest()).toString(16));
60+
public static HashFunction sha256() {
61+
return SHA256.INSTANCE;
12062
}
12163

122-
public String hash(String data) {
123-
return hash(data == null ? new byte[0] : data.getBytes(StandardCharsets.UTF_8));
64+
public static HashFunction sha512() {
65+
return SHA512.INSTANCE;
12466
}
12567

126-
public String hash(InputStream stream) throws IOException {
127-
MessageDigest hash = get();
128-
byte[] buf = new byte[1024];
129-
int count = -1;
130-
while ((count = stream.read(buf)) != -1)
131-
hash.update(buf, 0, count);
132-
return pad(new BigInteger(1, hash.digest()).toString(16));
68+
public static HashFunction byName(String extension) {
69+
switch (extension.toLowerCase(Locale.ROOT)) {
70+
case "adler32": return Adler32.INSTANCE;
71+
case "crc32": return CRC32.INSTANCE;
72+
case "md5": return MD5.INSTANCE;
73+
case "sha1": return SHA1.INSTANCE;
74+
case "sha256": return SHA256.INSTANCE;
75+
case "sha512": return SHA512.INSTANCE;
76+
default: throw new UnsupportedOperationException("Unknown hash extension: " + extension);
77+
}
13378
}
13479

135-
public String hash(byte[] data) {
136-
return pad(new BigInteger(1, get().digest(data)).toString(16));
80+
public static HashFunction[] values() {
81+
return new HashFunction[] { Adler32.INSTANCE, CRC32.INSTANCE, MD5.INSTANCE, SHA1.INSTANCE, SHA256.INSTANCE, SHA512.INSTANCE };
13782
}
13883

139-
public String pad(String hash) {
140-
return (pad + hash).substring(hash.length());
141-
}
84+
@Override
85+
public Iterator<HashFunction> iterator() {
86+
return new Iterator<HashFunction>() {
87+
private final HashFunction[] values = values();
88+
private int index = 0;
14289

143-
private static final byte[] HEX_ARRAY = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
144-
public static String bytesToHex(byte[] bytes) {
145-
if (bytes == null || bytes.length == 0)
146-
return "";
147-
byte[] hexChars = new byte[bytes.length * 3 - 1];
148-
for (int j = 0, k = 0; j < bytes.length; j++) {
149-
int v = bytes[j] & 0xFF;
150-
hexChars[k++] = HEX_ARRAY[v >>> 4];
151-
hexChars[k++] = HEX_ARRAY[v & 0x0F];
152-
if (j < bytes.length - 1)
153-
hexChars[k++] = ' ';
154-
}
155-
return new String(hexChars, StandardCharsets.UTF_8);
90+
@Override
91+
public boolean hasNext() {
92+
return values.length > index;
93+
}
94+
95+
@Override
96+
public HashFunction next() {
97+
return values[index++];
98+
}
99+
};
156100
}
157101
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) Forge Development LLC
3+
* SPDX-License-Identifier: LGPL-2.1-only
4+
*/
5+
package net.minecraftforge.util.hash;
6+
7+
interface HashInstance {
8+
/**
9+
* Updates the current hash using the specified array of bytes.
10+
*
11+
* @param input the array of bytes.
12+
*/
13+
default void update(byte[] data) {
14+
update(data, 0, data.length);
15+
}
16+
17+
/**
18+
* Updates the current hash with the specified array of bytes.
19+
* @param data the byte array to update the checksum with
20+
* @param offset the start offset of the data
21+
* @param length the number of bytes to use for the update
22+
*/
23+
void update(byte[] data, int offset, int length);
24+
25+
/*
26+
* Returns the final hash in hex-format.
27+
*/
28+
String finish();
29+
}

0 commit comments

Comments
 (0)