Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
massHashVersion = 1.3.2.1
massHashVersion = 2.0.0.0
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[versions]
guava_version = "33.5.0-jre"
junit_version = "6.0.1"
log4j_version = "2.25.2"
junit_version = "6.0.2"
log4j_version = "2.25.3"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava_version" }
Expand Down
244 changes: 106 additions & 138 deletions src/main/java/com/wildermods/masshash/Blob.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,167 +2,105 @@

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.UncheckedIOException;
import java.security.MessageDigest;
import java.util.Objects;
import java.util.function.Supplier;

import com.wildermods.masshash.exception.IntegrityException;
import com.wildermods.masshash.utils.ByteUtil;

/**
* Represents a data blob with associated hash. The Blob can store the data as
* a byte array and its hash, and includes methods for verifying the integrity
* of the data by checking its hash.
* A lightweight implementation of {@link IBlob} that represents data which can be read
* as a stream. Does not necessarily store the full byte array in memory.
* <p>
* This is particularly useful for large files or streams (e.g., files on disk, network streams),
* where reading the entire content into memory is undesirable. The hash of the data is always stored
* and can be verified without retaining the raw bytes.
* </p>
*/
public record Blob(byte[] data, String hash) implements IBlob {
public final class Blob implements IBlob {

/**
* Constructs a Blob from the given data and computes its hash.
*
* @param data The byte array representing the data.
*/
public Blob(byte[] data) {
this(data, ByteUtil.hash(data));
}

/**
* Constructs a Blob from the given data and hash.
*
* @param data The byte array representing the data.
* @param hash The hash of the data.
*
* @throws IntegrityException if the provided hash does not match the data's hash.
*/
public Blob(byte[] data, Hash hash) throws IntegrityException {
this(data, hash.hash());
verify();
}

/**
* Constructs a Blob from the contents of a file.
*
* @param path The path to the file.
* @throws IOException if an I/O error occurs while reading the file.
*/
public Blob(Path path) throws IOException {
this(Files.readAllBytes(path));
}

/**
* Constructs a Blob from the contents of a file and verifies its hash.
*
* @param path The path to the file.
* @param hash The expected hash of the file contents.
*
* @throws IOException if an I/O error occurs while reading the file.
* @throws IntegrityException if the file's hash does not match the provided hash.
*/
public Blob(Path path, String hash) throws IOException, IntegrityException {
this(Files.readAllBytes(path), hash);
verify();
}

/**
* Constructs a Blob from the contents of a file and verifies its hash.
*
* @param path The path to the file.
* @param hash The expected hash of the file contents.
*
* @throws IOException if an I/O error occurs while reading the file.
* @throws IntegrityException if the file's hash does not match the provided hash.
*/
public Blob(Path path, Hash hash) throws IOException, IntegrityException {
this(path, hash.hash());
}

/**
* Constructs a Blob from the data read from an InputStream.
*
* @param stream The InputStream from which data is read.
* @throws IOException if an I/O error occurs while reading from the stream.
*/
public Blob(InputStream stream) throws IOException {
this(stream.readAllBytes());
}

/**
* Constructs a Blob from the data read from an InputStream and verifies its hash.
*
* @param stream The InputStream from which data is read.
* @param hash The expected hash of the data.
*
* @throws IOException if an I/O error occurs while reading from the stream.
* @throws IntegrityException if the data's hash does not match the provided hash.
*/
public Blob(InputStream stream, String hash) throws IOException, IntegrityException {
this(stream.readAllBytes(), hash);
verify();
}

/**
* Constructs a Blob from the data read from an InputStream and verifies its hash.
*
* @param stream The InputStream from which data is read.
* @param hash The expected hash of the data.
*
* @throws IOException if an I/O error occurs while reading from the stream.
* @throws IntegrityException if the data's hash does not match the provided hash.
*/
public Blob(InputStream stream, Hash hash) throws IOException, IntegrityException {
this(stream, hash.hash());
}
private final Supplier<MessageDigest> digest;
private final Supplier<InputStream> streamSupplier;
private final String hash;

/**
* Drops the data from the current object and returns a new Hash that represents the hash of this Blob.
* The original blob still holds the data for as long as you keep it referenced.
*
* @return A new {@link Hash} object that represents this blob, but with no associated data.
*/
public Hash dropData() {
if(isTransient()) {
throw new UnsupportedOperationException("Data already dropped!");
}
return new Blob((byte[])null, hash);
Blob (Supplier<MessageDigest> digest, Supplier<InputStream> streamSupplier, String hash) {
Objects.requireNonNull(digest);
Objects.requireNonNull(streamSupplier);
Objects.requireNonNull(streamSupplier.get());
Objects.requireNonNull(hash);
this.digest = digest;
this.streamSupplier = streamSupplier;
this.hash = hash;
}

Blob(Supplier<InputStream> streamSupplier, String hash) {
this(ByteUtil.DEFAULT_DIGEST, streamSupplier, hash);
}

public Supplier<MessageDigest> digest() {
return digest;
}

public String hash() {
return hash;
}

/**
* Returns the data associated with this Blob
*
* @return a byte array that contains the data stored in this blob
* {@inheritDoc}
*/
@Override
@Deprecated
public byte[] data() {
if(data == null) {
throw new UnsupportedOperationException("Null data! Was the data dropped?");
try (InputStream stream = dataStream()){
return stream.readAllBytes();
}
catch(IOException e) {
throw new UncheckedIOException(e);
}
return data;
}

/**
* Checks if this blob is transient, meaning the underlying stream cannot be opened.
* <p>
* This can occur if the file is deleted, the network stream fails, or any other I/O error
* prevents the stream from being accessed.
* </p>
*
* @return {@code true} if the data stream cannot be opened, {@code false} otherwise
*/
@Override
public int hashCode() {
return hash.hashCode();
public boolean isTransient() {
try (InputStream stream = streamSupplier.get()){
return false;
}
catch(Exception e) {
return true;
}
}

/**
* Compares this object with another Hash object for equality. All {@link Blob} objects are also instances of {@link Hash}.
* Returns a fresh {@link InputStream} for reading the blob's data.
* <p>
* Two {@link Hash} objects are considered equal if their hashes are the same. This method specifically compares
* the hash of the other object with the hash of this object. If the other object is not an instance of {@link Hash},
* the method returns {@code false}.
* Each call returns a new stream. The caller is responsible for closing it.
* </p>
*
* @param o the object to compare with this Hash object.
* @return {@code true} if the other object is a {@link Hash} and has the same hash; {@code false} otherwise.
*
* @return a fresh {@link InputStream} for reading the blob's contents
* @throws IOException if the stream cannot be opened
*/
@Override
public boolean equals(Object o) {
if(o instanceof Hash) {
return hash().equals(((Hash) o).hash());
}
return false;
public InputStream dataStream() throws IOException {
try {
return streamSupplier.get();
}
catch(Exception e) {
throw new IOException(e);
}
}

/**
* Verifies that the data stored in this object matches the provided hash.
* Verifies that the data matches the provided hash.
* <p>
* This method computes the hash of the current data and compares it to the expected hash. If the hashes do not match,
* an {@link IntegrityException} is thrown. This method ensures the integrity of the data.
Expand All @@ -172,10 +110,39 @@ public boolean equals(Object o) {
*/
@Override
public void verify() throws IntegrityException {
String dataHash = ByteUtil.hash(data());
if(!dataHash.equals(hash)) {
throw new IntegrityException("Expected hash " + hash + " but got " + dataHash);
try (InputStream stream = dataStream()){
String actualHash = ByteUtil.hash(stream);
if(!actualHash.equals(hash)) {
throw new IntegrityException("Expected hash " + hash + " but got " + actualHash);
}
}
catch(IOException e) {
throw new UncheckedIOException(e);
}
}

@Override
public int hashCode() {
return hash.hashCode();
}

/**
* Compares this object with another Hash object for equality. All {@link IBlob} objects are also instances of {@link Hash}.
* <p>
* Two {@link Hash} objects are considered equal if their hashes are the same. This method specifically compares
* the hash of the other object with the hash of this object. If the other object is not an instance of {@link Hash},
* the method returns {@code false}.
* </p>
*
* @param o the object to compare with this Hash object.
* @return {@code true} if the other object is a {@link Hash} and has the same hash; {@code false} otherwise.
*/
@Override
public boolean equals(Object o) {
if(o instanceof Hash) {
return hash().equals(((Hash) o).hash());
}
return false;
}

/**
Expand All @@ -189,6 +156,7 @@ public void verify() throws IntegrityException {
*/
@Override
public String toString() {
return hash();
return hash();
}

}
Loading
Loading