From 9be0f5f81b88082700cce7f531a7be8a39002f64 Mon Sep 17 00:00:00 2001 From: Brian Long Date: Mon, 22 Feb 2021 09:57:09 -0500 Subject: [PATCH] added digest channel impl/tests --- .../nio/AbstractDigestByteChannel.java | 77 ++++++++++++ .../nio/HashingReadableByteChannel.java | 100 ++++++++++++++++ .../nio/HashingWritableByteChannel.java | 113 ++++++++++++++++++ .../AbstractHashingByteChannelUnitTest.java | 71 +++++++++++ .../HashingReadableByteChannelUnitTest.java | 36 ++++++ 5 files changed, 397 insertions(+) create mode 100644 src/main/java/com/inteligr8/nio/AbstractDigestByteChannel.java create mode 100644 src/main/java/com/inteligr8/nio/HashingReadableByteChannel.java create mode 100644 src/main/java/com/inteligr8/nio/HashingWritableByteChannel.java create mode 100644 src/test/java/com/inteligr8/nio/AbstractHashingByteChannelUnitTest.java create mode 100644 src/test/java/com/inteligr8/nio/HashingReadableByteChannelUnitTest.java diff --git a/src/main/java/com/inteligr8/nio/AbstractDigestByteChannel.java b/src/main/java/com/inteligr8/nio/AbstractDigestByteChannel.java new file mode 100644 index 0000000..8d5409b --- /dev/null +++ b/src/main/java/com/inteligr8/nio/AbstractDigestByteChannel.java @@ -0,0 +1,77 @@ +package com.inteligr8.nio; + +import java.io.IOException; +import java.nio.channels.Channel; +import java.security.NoSuchAlgorithmException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The base class for a hashing Java NIO ByteChannel. + * + * @author brian@inteligr8.com + */ +public class AbstractDigestByteChannel implements Channel { + + private final Logger logger = LoggerFactory.getLogger(AbstractDigestByteChannel.class); + + private final DigestBuffer digest; + private final Channel channel; + + private boolean closed = false; + + public AbstractDigestByteChannel(Channel channel, DigestParameters dparams) throws NoSuchAlgorithmException { + this.digest = new DigestBuffer(dparams); + this.channel = channel; + } + + protected DigestBuffer getDigest() { + return this.digest; + } + + public int getHashSize() { + return this.digest.getMessageDigest().getDigestLength(); + } + + /** + * The number of bytes read as input for this channel. + * + * @return A number of bytes; 0 if not yet read; never negative + */ + public long getTotalBytesRead() { + return this.digest.getTotalBytesRead(); + } + + /** + * The number of bytes written as output for this channel. + * + * @return A number of bytes; 0 if not yet read; never negative + */ + public long getTotalBytesWritten() { + return this.digest.getTotalBytesWritten(); + } + + /** + * This channel is open. If the underlying channel is closed, this channel + * is considered not open. + * + * @return true if open; false if closed; default state is open + */ + @Override + public boolean isOpen() { + return this.channel.isOpen() && !this.closed; + } + + /** + * This method marks this channel as closed. It does not close the + * underlying channel. This allows for mid-channel hashing use cases. + */ + @Override + public void close() throws IOException { + if (!this.closed && Status.Started.equals(this.getDigest().getStatus())) + this.logger.warn("A channel was closed before it was properly finished"); + this.closed = true; + } + +} diff --git a/src/main/java/com/inteligr8/nio/HashingReadableByteChannel.java b/src/main/java/com/inteligr8/nio/HashingReadableByteChannel.java new file mode 100644 index 0000000..810f8da --- /dev/null +++ b/src/main/java/com/inteligr8/nio/HashingReadableByteChannel.java @@ -0,0 +1,100 @@ +package com.inteligr8.nio; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.security.DigestException; +import java.security.NoSuchAlgorithmException; + +public class HashingReadableByteChannel extends AbstractDigestByteChannel implements ReadableByteChannel { + + protected final ReadableByteChannel rbchannel; + + private int chunkSize; + protected ByteBuffer readBuffer; + + public HashingReadableByteChannel(ReadableByteChannel rbchannel, DigestParameters dparams) throws NoSuchAlgorithmException { + super(rbchannel, dparams); + this.rbchannel = rbchannel; + this.setChunkSize(16384); + } + + /** + * This method overrides the default chunk size for reading this channel + * into buffers. The lower the value, the less memory usage. The higher + * the value, the higher performance. + * + * @param chunkSize A number of bytes; defaults to 16KB + */ + public void setChunkSize(int chunkSize) { + if (this.readBuffer != null && this.readBuffer.hasRemaining()) + throw new IllegalStateException("You cannot change the buffer size while the buffer is actively used"); + this.chunkSize = chunkSize; + this.readBuffer = ByteBuffer.allocate(this.chunkSize); + this.readBuffer.flip(); // ready for reading by default + } + + public long getRawBytesRead() { + return this.getTotalBytesRead(); + } + + public long getHashBytesWritten() { + return this.getTotalBytesWritten(); + } + + /** + * This method reads and hashes the underlying channel until it is empty. + * It writes the hash to the specified buffer. + * + * @param buffer A NIO buffer ready for writing + * @return The number of bytes read from the underlying channel; -1 for EOF; 0 is possible and should be re-read + * @throws IOException An I/O or crypto exception occurred + */ + @Override + public int read(ByteBuffer buffer) throws IOException { + try { + return this._read(buffer); + } catch (DigestException de) { + throw new IOException("This should never happen", de); + } + } + + /** + * This method reads and hashes the underlying channel until it is empty. + * It writes the hash to the specified buffer. + * + * @param buffer A NIO buffer ready for writing + * @return The number of bytes read from the underlying channel; -1 for EOF; 0 is possible and should be re-read + * @throws IOException An I/O or hashing exception occurred + */ + protected int _read(ByteBuffer buffer) throws IOException, DigestException { + int totalBytesRead = 0; + + this.readBuffer.compact(); // ready for writing + int bytesRead = this.rbchannel.read(this.readBuffer); + this.readBuffer.flip(); // ready for reading + + if (!Status.Finished.equals(this.getDigest().getStatus())) { + do { + if (bytesRead > 0) + totalBytesRead += bytesRead; + + this.getDigest().stream(this.readBuffer); + + this.readBuffer.compact(); // ready for writing + bytesRead = this.rbchannel.read(this.readBuffer); + this.readBuffer.flip(); // ready for reading + } while (bytesRead > 0); + } + + if (bytesRead > 0) + totalBytesRead += bytesRead; + + if (totalBytesRead > 0) + // something read from underlying channel + return totalBytesRead; + + return this.getDigest().flush(buffer) ? 0 : -1; + } + +} diff --git a/src/main/java/com/inteligr8/nio/HashingWritableByteChannel.java b/src/main/java/com/inteligr8/nio/HashingWritableByteChannel.java new file mode 100644 index 0000000..ef5d402 --- /dev/null +++ b/src/main/java/com/inteligr8/nio/HashingWritableByteChannel.java @@ -0,0 +1,113 @@ +package com.inteligr8.nio; + +import java.io.Flushable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import java.security.DigestException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; + +public class HashingWritableByteChannel extends AbstractDigestByteChannel implements WritableByteChannel, Flushable { + + protected final WritableByteChannel wbchannel; + + public HashingWritableByteChannel(WritableByteChannel wbchannel, DigestParameters dparams) throws NoSuchAlgorithmException { + super(wbchannel, dparams); + this.wbchannel = wbchannel; + } + + public long getRawBytesRead() { + return this.getTotalBytesRead(); + } + + public long getHashBytesWritten() { + return this.getTotalBytesWritten(); + } + + /** + * This method hashes to the underlying channel until the specified buffer + * is empty. + * + * @param buffer A NIO buffer ready for reading + * @return The number of bytes written to the underlying channel; never negative + * @throws IOException An I/O or hashing exception occurred + */ + @Override + public int write(ByteBuffer buffer) throws IOException { + try { + return this._write(buffer); + } catch (DigestException de) { + throw new IOException("This should never happen", de); + } + } + + @Override + public void flush() throws IOException { + try { + this._flush(); + } catch (DigestException de) { + throw new IOException("This should never happen", de); + } + } + + /** + * This method flushes and closes this channel. + * + * @throws IOException An I/O or crypto exception occurred + */ + @Override + public void close() throws IOException { + this.flush(); + super.close(); + } + + /** + * This method hashes and writes to the underlying channel until the + * specified buffer is empty. + * + * @param buffer A NIO buffer ready for reading + * @return The number of bytes written to the underlying channel; never negative + * @throws IOException An I/O or hashing exception occurred + * @throws DigestException + */ + protected int _write(ByteBuffer buffer) throws IOException, DigestException { + if (!this.isOpen()) + return 0; + + int bytesToWrite = buffer.remaining(); + this.getDigest().stream(buffer); + return bytesToWrite - buffer.remaining(); + } + + /** + * This method encrypts or decrypts and writes to the underlying channel + * until the internal caches and buffers are empty. + * + * @return The number of bytes written to the underlying channel; never negative + * @throws IOException An I/O or crypto exception occurred + * @throws IllegalBlockSizeException + * @throws BadPaddingException + */ + protected int _flush() throws IOException, DigestException { + if (!this.isOpen()) + return 0; + + int totalBytesWritten = 0; + + // ready for writing + ByteBuffer writeBuffer = ByteBuffer.allocate(this.getDigest().getMessageDigest().getDigestLength()); + + while (this.getDigest().flush(writeBuffer)) + ; + + writeBuffer.flip(); // ready for reading + totalBytesWritten += this.wbchannel.write(writeBuffer); + writeBuffer.compact(); // ready for writing + + return totalBytesWritten; + } + +} diff --git a/src/test/java/com/inteligr8/nio/AbstractHashingByteChannelUnitTest.java b/src/test/java/com/inteligr8/nio/AbstractHashingByteChannelUnitTest.java new file mode 100644 index 0000000..5698008 --- /dev/null +++ b/src/test/java/com/inteligr8/nio/AbstractHashingByteChannelUnitTest.java @@ -0,0 +1,71 @@ +package com.inteligr8.nio; + +import java.io.File; +import java.nio.channels.ByteChannel; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +import org.junit.Test; + +public abstract class AbstractHashingByteChannelUnitTest { + + private final Charset charset = Charset.forName("utf-8"); + + public Charset getCharset() { + return this.charset; + } + + @Test + public void empty() throws Exception { + this.test("SHA-256", "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + } + + @Test + public void lessThan8b() throws Exception { + this.test("SHA-1", "Hello", "f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"); + } + + @Test + public void exactly8b() throws Exception { + this.test("MD5", "Hi there", "d9385462d3deff78c352ebb3f941ce12"); + } + + @Test + public void between8b16b() throws Exception { + this.test("SHA-512", "Hello World!", "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"); + } + + @Test + public void exactly16b() throws Exception { + this.test("sha-384", "Hello over there", "3f8b70c00fed6d8c44434508acb99671f15118854880d50d1f997951a4e40d99fb4451e8c23975f02c274aacf6253570"); + } + + @Test + public void between16b32b() throws Exception { + this.test("sha-256", "Howdy y'all compadres!", "f0e28418a2ccab2254997b1a78537e900e0368e6472470e99a1e858e04cf42d3"); + } + + @Test + public void moreThan32b() throws Exception { + this.test("sha-256", "Hello to all and to all a good night", "277f6a3626c62240cebced1ae279e3cc49bf1cf3a6ffdc0ae5709033e4e3044e"); + } + + @Test + public void javaDigestBuffer() throws Exception { + this.test("sha-256", new File("src/main/java/com/inteligr8/nio/DigestBuffer.java"), "d01c874ffb0c6633a5d6ff4537da36c44d50af856970abddc1215866b0d08f44"); + } + + public void test(String algorithm, String text, String hex) throws Exception { + ByteBufferChannel bbchannel = new ByteBufferChannel(1024); + bbchannel.write(this.getCharset().encode(text)); + this.test(algorithm, bbchannel, hex); + } + + public void test(String algorithm, File file, String hex) throws Exception { + FileChannel fchannel = FileChannel.open(file.toPath()); + this.test(algorithm, fchannel, hex); + } + + public abstract void test(String algorithm, ByteChannel bchannel, String hex) throws Exception; + +} diff --git a/src/test/java/com/inteligr8/nio/HashingReadableByteChannelUnitTest.java b/src/test/java/com/inteligr8/nio/HashingReadableByteChannelUnitTest.java new file mode 100644 index 0000000..5a66601 --- /dev/null +++ b/src/test/java/com/inteligr8/nio/HashingReadableByteChannelUnitTest.java @@ -0,0 +1,36 @@ +package com.inteligr8.nio; + +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.ReadableByteChannel; + +import org.apache.commons.codec.binary.Hex; +import org.junit.Assert; + +public class HashingReadableByteChannelUnitTest extends AbstractHashingByteChannelUnitTest { + + @Override + public void test(String algorithm, ByteChannel bchannel, String hex) throws Exception { + this.test(algorithm, (ReadableByteChannel)bchannel, hex); + } + + public void test(String algorithm, ReadableByteChannel rbchannel, String hex) throws Exception { + HashingReadableByteChannel hrbchannel = new HashingReadableByteChannel(rbchannel, new DigestParameters(algorithm)); + try { + ByteBuffer bbuffer = ByteBuffer.allocate(hrbchannel.getHashSize()); + + while (hrbchannel.read(bbuffer) >= 0) + ; + + Assert.assertEquals(hrbchannel.getHashSize(), hrbchannel.getHashBytesWritten()); + + bbuffer.flip(); + byte[] bytes = new byte[bbuffer.remaining()]; + bbuffer.get(bytes); + Assert.assertEquals(hex, new String(Hex.encodeHex(bytes))); + } finally { + hrbchannel.close(); + } + } + +}