diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
index f21e02153..463f34f90 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
@@ -34,8 +34,8 @@
import com.google.common.io.BaseEncoding;
import java.io.Serializable;
import java.math.BigInteger;
+import java.nio.ByteBuffer;
import java.util.AbstractMap;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -232,6 +232,7 @@ public abstract static class Builder {
*
* @see Hashes and ETags:
* Best Practices
+ * @throws IllegalArgumentException when given an invalid hexadecimal value.
*/
public abstract Builder setMd5FromHexString(String md5HexString);
@@ -252,6 +253,7 @@ public abstract static class Builder {
*
* @see Hashes and ETags:
* Best Practices
+ * @throws IllegalArgumentException when given an invalid hexadecimal value.
*/
public abstract Builder setCrc32cFromHexString(String crc32cHexString);
@@ -293,7 +295,7 @@ public abstract static class Builder {
}
static final class BuilderImpl extends Builder {
-
+ private final String hexDecimalValues = "0123456789abcdef";
private BlobId blobId;
private String generatedId;
private String contentType;
@@ -442,16 +444,27 @@ public Builder setMd5(String md5) {
return this;
}
+ @Override
public Builder setMd5FromHexString(String md5HexString) {
if (md5HexString == null) {
return this;
}
- byte[] bytes = new BigInteger(md5HexString, 16).toByteArray();
- int leadingEmptyBytes = bytes.length - md5HexString.length() / 2;
- if (leadingEmptyBytes > 0) {
- bytes = Arrays.copyOfRange(bytes, leadingEmptyBytes, bytes.length);
+ if (md5HexString.length() % 2 != 0) {
+ throw new IllegalArgumentException(
+ "each byte must be represented by 2 valid hexadecimal characters");
+ }
+ String md5HexStringLower = md5HexString.toLowerCase();
+ ByteBuffer md5ByteBuffer = ByteBuffer.allocate(md5HexStringLower.length() / 2);
+ for (int charIndex = 0; charIndex < md5HexStringLower.length(); charIndex += 2) {
+ int higherOrderBits = this.hexDecimalValues.indexOf(md5HexStringLower.charAt(charIndex));
+ int lowerOrderBits = this.hexDecimalValues.indexOf(md5HexStringLower.charAt(charIndex + 1));
+ if (higherOrderBits == -1 || lowerOrderBits == -1) {
+ throw new IllegalArgumentException(
+ "each byte must be represented by 2 valid hexadecimal characters");
+ }
+ md5ByteBuffer.put((byte) (higherOrderBits << 4 | lowerOrderBits));
}
- this.md5 = BaseEncoding.base64().encode(bytes);
+ this.md5 = BaseEncoding.base64().encode(md5ByteBuffer.array());
return this;
}
@@ -466,12 +479,23 @@ public Builder setCrc32cFromHexString(String crc32cHexString) {
if (crc32cHexString == null) {
return this;
}
- byte[] bytes = new BigInteger(crc32cHexString, 16).toByteArray();
- int leadingEmptyBytes = bytes.length - crc32cHexString.length() / 2;
- if (leadingEmptyBytes > 0) {
- bytes = Arrays.copyOfRange(bytes, leadingEmptyBytes, bytes.length);
+ if (crc32cHexString.length() % 2 != 0) {
+ throw new IllegalArgumentException(
+ "each byte must be represented by 2 valid hexadecimal characters");
+ }
+ String crc32cHexStringLower = crc32cHexString.toLowerCase();
+ ByteBuffer crc32cByteBuffer = ByteBuffer.allocate(crc32cHexStringLower.length() / 2);
+ for (int charIndex = 0; charIndex < crc32cHexStringLower.length(); charIndex += 2) {
+ int higherOrderBits = this.hexDecimalValues.indexOf(crc32cHexStringLower.charAt(charIndex));
+ int lowerOrderBits =
+ this.hexDecimalValues.indexOf(crc32cHexStringLower.charAt(charIndex + 1));
+ if (higherOrderBits == -1 || lowerOrderBits == -1) {
+ throw new IllegalArgumentException(
+ "each byte must be represented by 2 valid hexadecimal characters");
+ }
+ crc32cByteBuffer.put((byte) (higherOrderBits << 4 | lowerOrderBits));
}
- this.crc32c = BaseEncoding.base64().encode(bytes);
+ this.crc32c = BaseEncoding.base64().encode(crc32cByteBuffer.array());
return this;
}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java
index a57be6df6..ea4f6dbda 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java
@@ -49,12 +49,16 @@ public class BlobInfoTest {
private static final String CONTENT_LANGUAGE = "En";
private static final String CRC32 = "FF00";
private static final String CRC32_HEX_STRING = "145d34";
+ private static final String CRC32_HEX_STRING_LEADING_ZEROS = "005d34";
+ private static final String CRC32_BASE64_LEADING_ZEROS = "AF00";
private static final Long DELETE_TIME = System.currentTimeMillis();
private static final String ETAG = "0xFF00";
private static final Long GENERATION = 1L;
private static final String GENERATED_ID = "B/N:1";
private static final String MD5 = "FF00";
private static final String MD5_HEX_STRING = "145d34";
+ private static final String MD5_HEX_STRING_LEADING_ZEROS = "0006a7de52b4e0b82602ce09809523ca";
+ private static final String MD5_BASE64_LEADING_ZEROS = "AAan3lK04LgmAs4JgJUjyg==";
private static final String MEDIA_LINK = "http://media/b/n";
private static final Map METADATA = ImmutableMap.of("n1", "v1", "n2", "v2");
private static final Long META_GENERATION = 10L;
@@ -132,6 +136,15 @@ public void testToBuilderSetMd5FromHexString() {
assertEquals(MD5, blobInfo.getMd5());
}
+ @Test
+ public void testToBuilderSetMd5FromHexStringLeadingZeros() {
+ BlobInfo blobInfo =
+ BlobInfo.newBuilder(BlobId.of("b2", "n2"))
+ .setMd5FromHexString(MD5_HEX_STRING_LEADING_ZEROS)
+ .build();
+ assertEquals(MD5_BASE64_LEADING_ZEROS, blobInfo.getMd5());
+ }
+
@Test
public void testToBuilderSetCrc32cFromHexString() {
BlobInfo blobInfo =
@@ -139,6 +152,15 @@ public void testToBuilderSetCrc32cFromHexString() {
assertEquals(CRC32, blobInfo.getCrc32c());
}
+ @Test
+ public void testToBuilderSetCrc32cFromHexStringLeadingZeros() {
+ BlobInfo blobInfo =
+ BlobInfo.newBuilder(BlobId.of("b2", "n2"))
+ .setCrc32cFromHexString(CRC32_HEX_STRING_LEADING_ZEROS)
+ .build();
+ assertEquals(CRC32_BASE64_LEADING_ZEROS, blobInfo.getCrc32c());
+ }
+
@Test
public void testToBuilderIncomplete() {
BlobInfo incompleteBlobInfo = BlobInfo.newBuilder(BlobId.of("b2", "n2")).build();