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();