package io.fathom.cloud.storage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import com.google.common.io.ByteSource;
import com.google.common.io.Files;
import com.google.common.io.InputSupplier;
import com.google.protobuf.ByteString;
/**
* Allows us to grab the MD5 state
*/
public class ResumableMD5Digest {
private static final MessageDigest MASTER;
private static final Field FIELD_DIGESTSPI;
private static final Field FIELD_STATE;
private static final Field FIELD_BYTESPROCESSED;
private static final Field FIELD_BUFFER;
private static final Field FIELD_BUFFEROFFSET;
final MessageDigest md;
final int[] state;
private Object sunmd5;
public ResumableMD5Digest() {
try {
this.md = (MessageDigest) MASTER.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException("Unable to clone MD5", e);
}
try {
this.sunmd5 = FIELD_DIGESTSPI.get(this.md);
int[] state = (int[]) FIELD_STATE.get(sunmd5);
this.state = state;
} catch (Exception e) {
throw new IllegalStateException("Unable to access state in MD5 object", e);
}
}
static {
try {
// Provider[] providers = Security.getProviders();
MASTER = MessageDigest.getInstance("MD5", "SUN");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Unable to get MD5", e);
} catch (NoSuchProviderException e) {
throw new IllegalStateException("Unable to get MD5", e);
}
try {
Class<? extends MessageDigest> clazz = MASTER.getClass();
FIELD_DIGESTSPI = getField(clazz, "digestSpi");
Class<?> md5Class = sun.security.provider.MD5.class;
FIELD_STATE = getField(md5Class, "state");
Class<?> digestBaseClass = md5Class.getSuperclass();
FIELD_BYTESPROCESSED = getField(digestBaseClass, "bytesProcessed");
FIELD_BUFFER = getField(digestBaseClass, "buffer");
FIELD_BUFFEROFFSET = getField(digestBaseClass, "bufOfs");
} catch (Exception e) {
throw new IllegalStateException("Error finding internal fields of MD5 implementation", e);
}
}
private static Field getField(Class<?> clazz, String name) throws NoSuchFieldException, SecurityException {
// for (Field field : clazz.getDeclaredField(name)()) {
// if (field.getName().equals(name)) {
// field.setAccessible(true);
// return field;
// }
// }
// throw new IllegalStateException("Unable to find field: " + name);
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field;
}
public static ResumableMD5Digest get() {
// TODO: Pool objects?
ResumableMD5Digest md5 = new ResumableMD5Digest();
md5.reset();
return md5;
}
private void reset() {
this.md.reset();
}
public void update(byte[] data) {
this.md.update(data);
}
public void update(byte[] data, int offset, int length) {
this.md.update(data, offset, length);
}
public byte[] digest() {
// TODO: Return to pool now?
return this.md.digest();
}
public ByteString getState() {
try {
int bufferOffset = FIELD_BUFFEROFFSET.getInt(sunmd5);
ByteBuffer buf = ByteBuffer.allocate(16 + bufferOffset);
for (int i = 0; i < state.length; i++) {
buf.putInt(state[i]);
}
if (bufferOffset != 0) {
byte[] buffer = (byte[]) FIELD_BUFFER.get(sunmd5);
buf.put(buffer, 0, bufferOffset);
}
buf.flip();
return ByteString.copyFrom(buf);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Error getting MD5 state", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Error getting MD5 state", e);
}
}
public void setState(ByteString state, long length) {
this.md.reset();
ByteBuffer buf = state.asReadOnlyByteBuffer();
for (int i = 0; i < this.state.length; i++) {
this.state[i] = buf.getInt();
}
int bufferOffset = buf.remaining();
try {
if (bufferOffset != 0) {
byte[] buffer = (byte[]) FIELD_BUFFER.get(sunmd5);
buf.get(buffer, 0, bufferOffset);
}
FIELD_BUFFEROFFSET.setInt(sunmd5, bufferOffset);
FIELD_BYTESPROCESSED.setLong(sunmd5, length);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Error setting bytesProcessed", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Error setting bytesProcessed", e);
}
}
public void update(InputStream is) throws IOException {
byte[] buf = new byte[4096];
while (true) {
int n = is.read(buf);
if (n == -1) {
break;
}
update(buf, 0, n);
}
}
public void update(InputSupplier<? extends InputStream> iss) throws IOException {
try (InputStream is = iss.getInput()) {
update(is);
}
}
public void update(File file) throws IOException {
update(Files.newInputStreamSupplier(file));
}
public void update(ByteSource src) throws IOException {
try (InputStream is = src.openStream()) {
update(is);
}
}
}