Sha256.java
package network.ike.plugin.scaffold;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* SHA-256 hash utility for scaffold checksums.
*
* <p>Produces values in the canonical lockfile form
* {@code "sha256:" + lowercase-hex-digest}. All scaffold lockfile and
* manifest hashes must flow through this class so the algorithm label
* stays consistent.
*
* <p>Hashes are computed over the raw byte content of the file or
* string. Consumers that need line-ending normalisation must do so
* before calling this class.
*/
public final class Sha256 {
/** Prefix distinguishing this algorithm in a lockfile value. */
public static final String PREFIX = "sha256:";
private Sha256() {}
/**
* Hash a byte array.
*
* @param data the bytes to hash; {@code null} is treated as empty
* @return {@code "sha256:" + hex-digest}
*/
public static String of(byte[] data) {
MessageDigest md = newDigest();
md.update(data == null ? new byte[0] : data);
return format(md.digest());
}
/**
* Hash a string using UTF-8 encoding.
*
* @param text the string to hash; {@code null} is treated as empty
* @return {@code "sha256:" + hex-digest}
*/
public static String of(String text) {
return of(text == null
? new byte[0]
: text.getBytes(StandardCharsets.UTF_8));
}
/**
* Hash the bytes of a file, streaming so large files don't load
* into memory all at once.
*
* @param path existing readable file
* @return {@code "sha256:" + hex-digest}
* @throws ScaffoldException if the file cannot be read
*/
public static String ofFile(Path path) {
MessageDigest md = newDigest();
try (InputStream in = Files.newInputStream(path)) {
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) > 0) {
md.update(buf, 0, n);
}
return format(md.digest());
} catch (IOException e) {
throw new ScaffoldException(
"Cannot hash file " + path, e);
}
}
/**
* Compare a known hash to the current hash of a file.
*
* @param path existing readable file
* @param expected hash string in the form
* {@code "sha256:" + hex-digest}; {@code null}
* returns {@code false}
* @return {@code true} iff the file's current hash equals
* {@code expected}
*/
public static boolean matches(Path path, String expected) {
if (expected == null) {
return false;
}
return ofFile(path).equals(expected);
}
private static MessageDigest newDigest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(
"SHA-256 not available", e);
}
}
private static String format(byte[] digest) {
StringBuilder sb = new StringBuilder(
PREFIX.length() + digest.length * 2);
sb.append(PREFIX);
for (byte b : digest) {
sb.append(Character.forDigit((b >> 4) & 0xF, 16));
sb.append(Character.forDigit(b & 0xF, 16));
}
return sb.toString();
}
}