From 78996f1584ce4d55d1c3b3912d49af145f76903b Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 13 May 2026 18:59:27 -0400 Subject: [PATCH 01/20] update swagger file --- sdk/storage/azure-storage-blob/swagger/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/storage/azure-storage-blob/swagger/README.md b/sdk/storage/azure-storage-blob/swagger/README.md index 292d2f7c231d..c70c062f70e5 100644 --- a/sdk/storage/azure-storage-blob/swagger/README.md +++ b/sdk/storage/azure-storage-blob/swagger/README.md @@ -16,7 +16,7 @@ autorest ### Code generation settings ``` yaml use: '@autorest/java@4.1.63' -input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/15d7f54a5389d5906ffb4e56bb2f38fe5525c0d3/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-06-06/blob.json +input-file: https://raw.githubusercontent.com/seanmcc-msft/azure-rest-api-specs/131aabf0b2994a059097aaba48c7c034a6e98054/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-10-06/blob.json java: true output-folder: ../ namespace: com.azure.storage.blob From e2225d6b949c8f35e22626af5c4f2f4314fc17e7 Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 13 May 2026 19:00:53 -0400 Subject: [PATCH 02/20] fix linting --- .../BlockBlobsPutBlobFromUrlHeaders.java | 36 +++++++++++++++++++ .../models/BlockBlobsUploadHeaders.java | 36 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlockBlobsPutBlobFromUrlHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlockBlobsPutBlobFromUrlHeaders.java index 9e8fb72f68db..02c212f1e3fa 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlockBlobsPutBlobFromUrlHeaders.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlockBlobsPutBlobFromUrlHeaders.java @@ -36,6 +36,12 @@ public final class BlockBlobsPutBlobFromUrlHeaders { @Generated private byte[] contentMD5; + /* + * The x-ms-content-crc64 property. + */ + @Generated + private byte[] xMsContentCrc64; + /* * The x-ms-client-request-id property. */ @@ -84,6 +90,8 @@ public final class BlockBlobsPutBlobFromUrlHeaders { @Generated private String xMsEncryptionScope; + private static final HttpHeaderName X_MS_CONTENT_CRC64 = HttpHeaderName.fromString("x-ms-content-crc64"); + private static final HttpHeaderName X_MS_VERSION = HttpHeaderName.fromString("x-ms-version"); private static final HttpHeaderName X_MS_VERSION_ID = HttpHeaderName.fromString("x-ms-version-id"); @@ -116,6 +124,12 @@ public BlockBlobsPutBlobFromUrlHeaders(HttpHeaders rawHeaders) { } else { this.contentMD5 = null; } + String xMsContentCrc64 = rawHeaders.getValue(X_MS_CONTENT_CRC64); + if (xMsContentCrc64 != null) { + this.xMsContentCrc64 = Base64.getDecoder().decode(xMsContentCrc64); + } else { + this.xMsContentCrc64 = null; + } this.xMsClientRequestId = rawHeaders.getValue(HttpHeaderName.X_MS_CLIENT_REQUEST_ID); this.xMsRequestId = rawHeaders.getValue(HttpHeaderName.X_MS_REQUEST_ID); this.xMsVersion = rawHeaders.getValue(X_MS_VERSION); @@ -209,6 +223,28 @@ public BlockBlobsPutBlobFromUrlHeaders setContentMD5(byte[] contentMD5) { return this; } + /** + * Get the xMsContentCrc64 property: The x-ms-content-crc64 property. + * + * @return the xMsContentCrc64 value. + */ + @Generated + public byte[] getXMsContentCrc64() { + return CoreUtils.clone(this.xMsContentCrc64); + } + + /** + * Set the xMsContentCrc64 property: The x-ms-content-crc64 property. + * + * @param xMsContentCrc64 the xMsContentCrc64 value to set. + * @return the BlockBlobsPutBlobFromUrlHeaders object itself. + */ + @Generated + public BlockBlobsPutBlobFromUrlHeaders setXMsContentCrc64(byte[] xMsContentCrc64) { + this.xMsContentCrc64 = CoreUtils.clone(xMsContentCrc64); + return this; + } + /** * Get the xMsClientRequestId property: The x-ms-client-request-id property. * diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlockBlobsUploadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlockBlobsUploadHeaders.java index fb8da8e12407..63fa2cd2212d 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlockBlobsUploadHeaders.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlockBlobsUploadHeaders.java @@ -36,6 +36,12 @@ public final class BlockBlobsUploadHeaders { @Generated private byte[] contentMD5; + /* + * The x-ms-content-crc64 property. + */ + @Generated + private byte[] xMsContentCrc64; + /* * The x-ms-client-request-id property. */ @@ -90,6 +96,8 @@ public final class BlockBlobsUploadHeaders { @Generated private String xMsStructuredBody; + private static final HttpHeaderName X_MS_CONTENT_CRC64 = HttpHeaderName.fromString("x-ms-content-crc64"); + private static final HttpHeaderName X_MS_VERSION = HttpHeaderName.fromString("x-ms-version"); private static final HttpHeaderName X_MS_VERSION_ID = HttpHeaderName.fromString("x-ms-version-id"); @@ -124,6 +132,12 @@ public BlockBlobsUploadHeaders(HttpHeaders rawHeaders) { } else { this.contentMD5 = null; } + String xMsContentCrc64 = rawHeaders.getValue(X_MS_CONTENT_CRC64); + if (xMsContentCrc64 != null) { + this.xMsContentCrc64 = Base64.getDecoder().decode(xMsContentCrc64); + } else { + this.xMsContentCrc64 = null; + } this.xMsClientRequestId = rawHeaders.getValue(HttpHeaderName.X_MS_CLIENT_REQUEST_ID); this.xMsRequestId = rawHeaders.getValue(HttpHeaderName.X_MS_REQUEST_ID); this.xMsVersion = rawHeaders.getValue(X_MS_VERSION); @@ -218,6 +232,28 @@ public BlockBlobsUploadHeaders setContentMD5(byte[] contentMD5) { return this; } + /** + * Get the xMsContentCrc64 property: The x-ms-content-crc64 property. + * + * @return the xMsContentCrc64 value. + */ + @Generated + public byte[] getXMsContentCrc64() { + return CoreUtils.clone(this.xMsContentCrc64); + } + + /** + * Set the xMsContentCrc64 property: The x-ms-content-crc64 property. + * + * @param xMsContentCrc64 the xMsContentCrc64 value to set. + * @return the BlockBlobsUploadHeaders object itself. + */ + @Generated + public BlockBlobsUploadHeaders setXMsContentCrc64(byte[] xMsContentCrc64) { + this.xMsContentCrc64 = CoreUtils.clone(xMsContentCrc64); + return this; + } + /** * Get the xMsClientRequestId property: The x-ms-client-request-id property. * From e33274ca315d9c35eb927159855642f0bf4a0448 Mon Sep 17 00:00:00 2001 From: browndav Date: Thu, 14 May 2026 14:27:47 -0400 Subject: [PATCH 03/20] add crc64 support for AppendBlock --- .../AppendBlobItemConstructorProxy.java | 81 +++++++++++++++++++ .../storage/blob/models/AppendBlobItem.java | 24 ++++++ .../specialized/AppendBlobAsyncClient.java | 15 ++-- .../blob/specialized/AppendBlobApiTests.java | 3 + .../specialized/AppendBlobAsyncApiTests.java | 2 + 5 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/AppendBlobItemConstructorProxy.java diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/AppendBlobItemConstructorProxy.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/AppendBlobItemConstructorProxy.java new file mode 100644 index 000000000000..b62fc2ecf1da --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/AppendBlobItemConstructorProxy.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.accesshelpers; + +import com.azure.storage.blob.models.AppendBlobItem; + +import java.time.OffsetDateTime; + +/** + * Helper class to access private values of {@link AppendBlobItem} across package boundaries. + */ +public final class AppendBlobItemConstructorProxy { + private static AppendBlobItemConstructorAccessor accessor; + + private AppendBlobItemConstructorProxy() { + } + + /** + * Type defining the methods to set the non-public properties of an {@link AppendBlobItem}. + */ + public interface AppendBlobItemConstructorAccessor { + /** + * Creates a new instance of {@link AppendBlobItem}. + * + * @param eTag ETag of the append blob. + * @param lastModified Last modified time of the append blob. + * @param contentMd5 Content MD5 of the append blob. + * @param isServerEncrypted Flag indicating if the append blob is encrypted on the server. + * @param encryptionKeySha256 The encryption key used to encrypt the append blob. + * @param encryptionScope The encryption scope used to encrypt the append blob. + * @param blobAppendOffset The offset at which the block was committed to the append blob. + * @param blobCommittedBlockCount The number of committed blocks in the append blob. + * @param versionId The version identifier of the append blob. + * @param contentCrc64 Content CRC64 of the append blob. + * @return A new instance of {@link AppendBlobItem}. + */ + AppendBlobItem create(String eTag, OffsetDateTime lastModified, byte[] contentMd5, boolean isServerEncrypted, + String encryptionKeySha256, String encryptionScope, String blobAppendOffset, + Integer blobCommittedBlockCount, String versionId, byte[] contentCrc64); + } + + /** + * The method called from {@link AppendBlobItem} to set its accessor. + * + * @param accessor The accessor. + */ + public static void setAccessor(final AppendBlobItemConstructorAccessor accessor) { + AppendBlobItemConstructorProxy.accessor = accessor; + } + + /** + * Creates a new instance of {@link AppendBlobItem}. + * + * @param eTag ETag of the append blob. + * @param lastModified Last modified time of the append blob. + * @param contentMd5 Content MD5 of the append blob. + * @param isServerEncrypted Flag indicating if the append blob is encrypted on the server. + * @param encryptionKeySha256 The encryption key used to encrypt the append blob. + * @param encryptionScope The encryption scope used to encrypt the append blob. + * @param blobAppendOffset The offset at which the block was committed to the append blob. + * @param blobCommittedBlockCount The number of committed blocks in the append blob. + * @param versionId The version identifier of the append blob. + * @param contentCrc64 Content CRC64 of the append blob. + * @return A new instance of {@link AppendBlobItem}. + */ + public static AppendBlobItem create(String eTag, OffsetDateTime lastModified, byte[] contentMd5, + boolean isServerEncrypted, String encryptionKeySha256, String encryptionScope, String blobAppendOffset, + Integer blobCommittedBlockCount, String versionId, byte[] contentCrc64) { + // This looks odd but is necessary, it is possible to engage the access helper before anywhere else in the + // application accesses AppendBlobItem which triggers the accessor to be configured. So, if the accessor is null + // this effectively pokes the class to set up the accessor. + if (accessor == null) { + new AppendBlobItem(null, null, null, false, null, null, null, null, null); + } + + assert accessor != null; + return accessor.create(eTag, lastModified, contentMd5, isServerEncrypted, encryptionKeySha256, encryptionScope, + blobAppendOffset, blobCommittedBlockCount, versionId, contentCrc64); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/AppendBlobItem.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/AppendBlobItem.java index 8acbf7de2813..57f8c54faee4 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/AppendBlobItem.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/AppendBlobItem.java @@ -5,6 +5,7 @@ import com.azure.core.annotation.Immutable; import com.azure.core.util.CoreUtils; +import com.azure.storage.blob.implementation.accesshelpers.AppendBlobItemConstructorProxy; import java.time.OffsetDateTime; @@ -22,6 +23,11 @@ public class AppendBlobItem { private final String blobAppendOffset; private final Integer blobCommittedBlockCount; private final String versionId; + private final byte[] contentCrc64; + + static { + AppendBlobItemConstructorProxy.setAccessor(AppendBlobItem::new); + } /** * Constructs an {@link AppendBlobItem}. @@ -76,6 +82,14 @@ public AppendBlobItem(final String eTag, final OffsetDateTime lastModified, fina public AppendBlobItem(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, final boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, final String blobAppendOffset, final Integer blobCommittedBlockCount, final String versionId) { + this(eTag, lastModified, contentMd5, isServerEncrypted, encryptionKeySha256, encryptionScope, blobAppendOffset, + blobCommittedBlockCount, versionId, null); + } + + private AppendBlobItem(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, + final boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, + final String blobAppendOffset, final Integer blobCommittedBlockCount, final String versionId, + final byte[] contentCrc64) { this.eTag = eTag; this.lastModified = lastModified; this.contentMd5 = CoreUtils.clone(contentMd5); @@ -85,6 +99,7 @@ public AppendBlobItem(final String eTag, final OffsetDateTime lastModified, fina this.blobAppendOffset = blobAppendOffset; this.blobCommittedBlockCount = blobCommittedBlockCount; this.versionId = versionId; + this.contentCrc64 = CoreUtils.clone(contentCrc64); } /** @@ -141,6 +156,15 @@ public byte[] getContentMd5() { return CoreUtils.clone(contentMd5); } + /** + * Gets the calculated CRC64 of the append blob. + * + * @return the calculated CRC64 of the append blob + */ + public byte[] getContentCrc64() { + return CoreUtils.clone(contentCrc64); + } + /** * Gets the offset of the append blob. * diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/AppendBlobAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/AppendBlobAsyncClient.java index 42b7b3f76f1c..6fd42fbfe7d3 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/AppendBlobAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/AppendBlobAsyncClient.java @@ -18,6 +18,7 @@ import com.azure.storage.blob.BlobContainerAsyncClient; import com.azure.storage.blob.BlobServiceAsyncClient; import com.azure.storage.blob.BlobServiceVersion; +import com.azure.storage.blob.implementation.accesshelpers.AppendBlobItemConstructorProxy; import com.azure.storage.blob.implementation.models.AppendBlobsAppendBlockFromUrlHeaders; import com.azure.storage.blob.implementation.models.AppendBlobsAppendBlockHeaders; import com.azure.storage.blob.implementation.models.AppendBlobsCreateHeaders; @@ -479,9 +480,10 @@ Mono> appendBlockWithResponse(Flux data, lo null, null, getCustomerProvidedKey(), encryptionScope, context) .map(rb -> { AppendBlobsAppendBlockHeaders hd = rb.getDeserializedHeaders(); - AppendBlobItem item = new AppendBlobItem(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), - hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), - hd.getXMsBlobAppendOffset(), hd.getXMsBlobCommittedBlockCount()); + AppendBlobItem item + = AppendBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), + hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), + hd.getXMsBlobAppendOffset(), hd.getXMsBlobCommittedBlockCount(), null, hd.getXMsContentCrc64()); return new SimpleResponse<>(rb, item); }); } @@ -627,9 +629,10 @@ Mono> appendBlockFromUrlWithResponse(AppendBlobAppendBl getCustomerProvidedKey(), encryptionScope, context) .map(rb -> { AppendBlobsAppendBlockFromUrlHeaders hd = rb.getDeserializedHeaders(); - AppendBlobItem item = new AppendBlobItem(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), - hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), - hd.getXMsBlobAppendOffset(), hd.getXMsBlobCommittedBlockCount()); + AppendBlobItem item + = AppendBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), + hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), + hd.getXMsBlobAppendOffset(), hd.getXMsBlobCommittedBlockCount(), null, hd.getXMsContentCrc64()); return new SimpleResponse<>(rb, item); }); } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java index 5b65926014b6..ba3fbd13a91c 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java @@ -353,6 +353,9 @@ public void appendBlockDefaults() { validateBasicHeaders(appendResponse.getHeaders()); assertNotNull(appendResponse.getHeaders().getValue(X_MS_CONTENT_CRC64)); + byte[] expectedContentCrc64 + = Base64.getDecoder().decode(appendResponse.getHeaders().getValue(X_MS_CONTENT_CRC64)); + TestUtils.assertArraysEqual(expectedContentCrc64, appendResponse.getValue().getContentCrc64()); assertNotNull(appendResponse.getValue().getBlobAppendOffset()); assertNotNull(appendResponse.getValue().getBlobCommittedBlockCount()); assertEquals(1, bc.getProperties().getCommittedBlockCount()); diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java index cf7a17dade24..9138919a78a5 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java @@ -354,6 +354,8 @@ public void appendBlockDefaults() { .assertNext(r -> { validateBasicHeaders(r.getHeaders()); assertNotNull(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); + byte[] expectedContentCrc64 = Base64.getDecoder().decode(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); + TestUtils.assertArraysEqual(expectedContentCrc64, r.getValue().getContentCrc64()); assertNotNull(r.getValue().getBlobAppendOffset()); assertNotNull(r.getValue().getBlobCommittedBlockCount()); }) From 59e195cb982a5bda6027ad36b2e1b2bfa8425fd7 Mon Sep 17 00:00:00 2001 From: browndav Date: Mon, 18 May 2026 12:05:47 -0400 Subject: [PATCH 04/20] add crc64 support for PageBlob --- sdk/storage/azure-storage-blob/CHANGELOG.md | 2 + .../PageBlobItemConstructorProxy.java | 31 ++++++ .../storage/blob/models/PageBlobItem.java | 23 ++++ .../blob/specialized/PageBlobAsyncClient.java | 14 ++- .../blob/specialized/PageBlobClient.java | 1 + .../blob/specialized/PageBlobApiTests.java | 4 + .../specialized/PageBlobAsyncApiTests.java | 4 + sdk/storage/feature/spec.txt | 103 ++++++++++++++++++ 8 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/PageBlobItemConstructorProxy.java create mode 100644 sdk/storage/feature/spec.txt diff --git a/sdk/storage/azure-storage-blob/CHANGELOG.md b/sdk/storage/azure-storage-blob/CHANGELOG.md index 6c06de3117ad..03c6659081df 100644 --- a/sdk/storage/azure-storage-blob/CHANGELOG.md +++ b/sdk/storage/azure-storage-blob/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added `PageBlobItem.getContentCrc64()` to expose CRC64 values returned by page operations. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/PageBlobItemConstructorProxy.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/PageBlobItemConstructorProxy.java new file mode 100644 index 000000000000..a8af26ecf1c7 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/PageBlobItemConstructorProxy.java @@ -0,0 +1,31 @@ +package com.azure.storage.blob.implementation.accesshelpers; + +import com.azure.storage.blob.models.PageBlobItem; + +import java.time.OffsetDateTime; + +public class PageBlobItemConstructorProxy { + private static PageBlobItemConstructorAccessor accessor; + + public interface PageBlobItemConstructorAccessor { + PageBlobItem create(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, + final Boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, + final Long blobSequenceNumber, final String versionId, final byte[] contentCrc64); + } + + public static void setAccessor(final PageBlobItemConstructorAccessor accessor) { + PageBlobItemConstructorProxy.accessor = accessor; + } + + public static PageBlobItem create(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, + final Boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, + final Long blobSequenceNumber, final String versionId, final byte[] contentCrc64) { + if (accessor == null) { + new PageBlobItem(null, null, null, false, null, null, null, null); + } + + assert accessor != null; + return accessor.create(eTag, lastModified, contentMd5, isServerEncrypted, encryptionKeySha256, encryptionScope, + blobSequenceNumber, versionId, contentCrc64); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/PageBlobItem.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/PageBlobItem.java index ef127458a57a..39783a094794 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/PageBlobItem.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/PageBlobItem.java @@ -5,6 +5,7 @@ import com.azure.core.annotation.Immutable; import com.azure.core.util.CoreUtils; +import com.azure.storage.blob.implementation.accesshelpers.PageBlobItemConstructorProxy; import java.time.OffsetDateTime; @@ -16,12 +17,17 @@ public class PageBlobItem { private final String eTag; private final OffsetDateTime lastModified; private final byte[] contentMd5; + private final byte[] contentCrc64; private final Boolean isServerEncrypted; private final String encryptionKeySha256; private final String encryptionScope; private final Long blobSequenceNumber; private final String versionId; + static { + PageBlobItemConstructorProxy.setAccessor(PageBlobItem::new); + } + /** * Constructs a {@link PageBlobItem}. * @@ -70,9 +76,17 @@ public PageBlobItem(final String eTag, final OffsetDateTime lastModified, final public PageBlobItem(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, final Boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, final Long blobSequenceNumber, final String versionId) { + this(eTag, lastModified, contentMd5, isServerEncrypted, encryptionKeySha256, encryptionScope, + blobSequenceNumber, versionId, null); + } + + private PageBlobItem(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, + final Boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, + final Long blobSequenceNumber, final String versionId, final byte[] contentCrc64) { this.eTag = eTag; this.lastModified = lastModified; this.contentMd5 = CoreUtils.clone(contentMd5); + this.contentCrc64 = CoreUtils.clone(contentCrc64); this.isServerEncrypted = isServerEncrypted; this.encryptionKeySha256 = encryptionKeySha256; this.encryptionScope = encryptionScope; @@ -134,6 +148,15 @@ public byte[] getContentMd5() { return CoreUtils.clone(contentMd5); } + /** + * Gets the calculated CRC64 of the page blob. + * + * @return the calculated CRC64 of the page blob + */ + public byte[] getContentCrc64() { + return CoreUtils.clone(contentCrc64); + } + /** * Gets the current sequence number of the page blob. * diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobAsyncClient.java index 5943047cf49b..3ed1f8713467 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobAsyncClient.java @@ -22,6 +22,7 @@ import com.azure.core.util.logging.ClientLogger; import com.azure.storage.blob.BlobAsyncClient; import com.azure.storage.blob.BlobServiceVersion; +import com.azure.storage.blob.implementation.accesshelpers.PageBlobItemConstructorProxy; import com.azure.storage.blob.implementation.models.EncryptionScope; import com.azure.storage.blob.implementation.models.PageBlobsClearPagesHeaders; import com.azure.storage.blob.implementation.models.PageBlobsCreateHeaders; @@ -529,9 +530,10 @@ Mono> uploadPagesWithResponse(PageRange pageRange, Flux { PageBlobsUploadPagesHeaders hd = rb.getDeserializedHeaders(); - PageBlobItem item = new PageBlobItem(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), - hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), - hd.getXMsBlobSequenceNumber()); + PageBlobItem item + = PageBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), + hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), + hd.getXMsBlobSequenceNumber(), hd.getXMsVersion(), hd.getXMsContentCrc64()); return new SimpleResponse<>(rb, item); }); } @@ -720,8 +722,10 @@ sourceCpkKey, sourceCpkKeySha256, sourceCpkAlgorithm, getCustomerProvidedKey(), context) .map(rb -> { PageBlobsUploadPagesFromURLHeaders hd = rb.getDeserializedHeaders(); - PageBlobItem item = new PageBlobItem(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), - hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), null); + PageBlobItem item + = PageBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), + hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), + hd.getXMsBlobSequenceNumber(), hd.getXMsVersion(), hd.getXMsContentCrc64()); return new SimpleResponse<>(rb, item); }); } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobClient.java index 693c02dbf6eb..dacf5323517b 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobClient.java @@ -22,6 +22,7 @@ import com.azure.core.util.logging.ClientLogger; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobServiceVersion; +import com.azure.storage.blob.implementation.accesshelpers.PageBlobItemConstructorProxy; import com.azure.storage.blob.implementation.models.EncryptionScope; import com.azure.storage.blob.implementation.models.PageBlobsClearPagesHeaders; import com.azure.storage.blob.implementation.models.PageBlobsCopyIncrementalHeaders; diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java index 5c2e32f9a962..8ea59a7a08ca 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java @@ -69,6 +69,7 @@ import java.util.Map; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -372,9 +373,12 @@ public void uploadPage() { = bc.uploadPagesWithResponse(new PageRange().setStart(0).setEnd(PageBlobClient.PAGE_BYTES - 1), new ByteArrayInputStream(getRandomByteArray(PageBlobClient.PAGE_BYTES)), null, null, null, null); + byte[] expectedContentCrc64 = Base64.getDecoder().decode(response.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertResponseStatusCode(response, 201); assertTrue(validateBasicHeaders(response.getHeaders())); assertNotNull(response.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertArrayEquals(expectedContentCrc64, response.getValue().getContentCrc64()); assertEquals(0, response.getValue().getBlobSequenceNumber()); assertTrue(response.getValue().isServerEncrypted()); } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java index 00f44de6b6d2..2a893fc865ba 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java @@ -72,6 +72,7 @@ import java.util.Map; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -378,9 +379,12 @@ public void uploadPage() { .create(bc.uploadPagesWithResponse(new PageRange().setStart(0).setEnd(PageBlobClient.PAGE_BYTES - 1), Flux.just(ByteBuffer.wrap(getRandomByteArray(PageBlobClient.PAGE_BYTES))), null, null)) .assertNext(r -> { + byte[] expectedContentCrc64 = Base64.getDecoder().decode(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertResponseStatusCode(r, 201); assertTrue(validateBasicHeaders(r.getHeaders())); assertNotNull(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertArrayEquals(expectedContentCrc64, r.getValue().getContentCrc64()); assertEquals(0, r.getValue().getBlobSequenceNumber()); assertTrue(r.getValue().isServerEncrypted()); }) diff --git a/sdk/storage/feature/spec.txt b/sdk/storage/feature/spec.txt new file mode 100644 index 000000000000..b45432f98b54 --- /dev/null +++ b/sdk/storage/feature/spec.txt @@ -0,0 +1,103 @@ +MD5/CRC64 Combined Return + +Author: Josh Martin (joshuamartin) + +Date: September 2025 + +Background + +OneDrive/SharePoint (ODSP) is interested in moving away from MD5 to CRC64, primarily to support their construction of an “uber blob”. The uber blob is an ODSP mechanism to combine smaller, rarely accessed, blobs in a larger single blob for archive storage, and will be constructed using PutBlock From URL. Currently, ODSP has MD5 checksums for their data, and they need to exchange this known MD5 for a CRC64. + +When a client makes a PutBlock From URL request today, either the MD5 or the CRC64 value calculated by the service is returned in the response header Content-MD5 or x-ms-content-crc64 respectively. For versions after 2019-02-02, the CRC64 value is returned by default unless a MD5 header was presented with the request for validation with that value, in which case Content-MD5 is returned. + +The ask from ODSP is for XStore to support the transition to CRC64 by returning the service calculated MD5 and CRC64 values for PutBlock from URL APIs when these APIs are provided with a MD5 hash, rather than only the MD5 hash alone. This change drastically simplifies the work on the ODSP side to transition from MD5 to CRC64 values for their uber blob, and also saves significantly on COGS as the team will no longer need to read back the blob to retrieve the CRC. + +Rather than just make the change for this API, we are proposing to support this change for all blob APIs for consistency. + +Technical Details + +Today, assuming a request is made with the 2019-02-02 API version or greater, it will be wrapped with a CrcReaderStream (see here). If a client provides a Content MD5 header, or CRC isn’t supported at all, the stream is wrapped by a MD5ReaderStream (see here). Importantly, these conditions aren’t mutually exclusive, so a request made with a Content-MD5 header using an API version after CRC64 support is introduced will result in the copy source stream being wrapped by both a MD5 Reader and CRC64 reader stream. + +Despite the streams being wrapped together, the service will only return one of the checksum values (MD5 if requested or the version is before 2019-02-02, CRC64 otherwise). This separation is enforced here, among other locations. + +This change would modify the service to return both values if the client sends a Content-MD5 header, so long as the API version for the request is 2026-10-06 or greater. The returned CRC64 value would be in the response header x-ms-content-crc64. + +Impacted APIs + +PutBlock + +PutBlock from URL + +PutPage + +PutPage from URL + +AppendBlock + +AppendBlock from URL + +PutBlob – (Note: support already exists for this behavior today without any change to the service, assuming a blob type of block. Page/append types would be irrelevant as those requests have no content attached to them.) + +PutBlob from URL + +Sample Request and Response + +Request + +PUT https://myaccount.blob.core.windows.net/mycontainer/myblob?comp=block&blockid=id + +x-ms-source-content-md5: {MD5 hash value of source content} + +x-ms-copy-source header: {copy source location} + +*other headers as needed* + +Response + +HTTP/1.1 200 OK + +Content-MD5: {MD5 hash value of source content} + +x-ms-content-crc64: {CRC64 value of source content} + +*other headers as expected* + +Testing + +CVTs covering the positive case for each impacted API are required, both to ensure functional correctness and managed/native parity when necessary. Tests should also ensure requests made prior to CRC64 support being introduced do not return the header value. + +SCTE support should be simple to add, as existing workloads could be modified to check the existence of the CRC64 response header if the API version supports the behavior. + +Perf testing for this change seems unnecessary, as the service is already calculating both values. + +Config Rollout + +This change is gated by two DCs, one managed and one native, that share the same name. + +Managed: XStoreConfigSettings/BlobXfe/Settings/ReturnCrcForAllPutRequests. A background DC that defaults to false in the XSD. + +Native: A native request setting associated with the Oct26 Storage API version. This DC will change from false to true once the LatestEnabledVersion DC has been updated to this API version but can be explicitly set false if needed. This aligns with other versioned DCs. + +Q & A + +Q: Should File APIs also be changed? + +A: The implementation would be very similar, as the streams are already wrapped. At the moment though, only Content-MD5 is documented for PutRange, making this change a larger effort. + +Q: Will this change allow clients to pass both Content-MD5 and x-ms-content-crc64 headers on the same request? + +A: No, this restriction will still be in place. The only behavior change is the service returning the calculated CRC64 value after the MD5 is calculated/verified. + +Q: Will this change the existing error behavior if the provided MD5 does not match the value calculated by the service? + +A: No, the request will still be rejected due to MD5 mismatch, with no CRC returned to the client. + +Q: Are any telemetry changes needed? + +A: None are needed on the happy path. A metric (MissingCrcValue) will be emitted if the CRC cannot be added to the return path when it is expected. + +Q: Will Content-MD5 be returned if not requested? + +A: No. We support both to facilitate migration to crc64 and away from md5. There are no plans to always return an MD5. + +Future questions can go here From dc5ef2c555926c6c2c06acaa14aa3d58d64e3839 Mon Sep 17 00:00:00 2001 From: browndav Date: Mon, 18 May 2026 12:43:16 -0400 Subject: [PATCH 05/20] add class header comment, add javadocs to constructorproxy --- .../PageBlobItemConstructorProxy.java | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/PageBlobItemConstructorProxy.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/PageBlobItemConstructorProxy.java index a8af26ecf1c7..fb241d47a268 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/PageBlobItemConstructorProxy.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/PageBlobItemConstructorProxy.java @@ -1,25 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.azure.storage.blob.implementation.accesshelpers; import com.azure.storage.blob.models.PageBlobItem; import java.time.OffsetDateTime; -public class PageBlobItemConstructorProxy { +/** + * Helper class to access private values of {@link PageBlobItem} across package boundaries. + */ +public final class PageBlobItemConstructorProxy { private static PageBlobItemConstructorAccessor accessor; + private PageBlobItemConstructorProxy() { + } + + /** + * Type defining the methods to set the non-public properties of a {@link PageBlobItem}. + */ public interface PageBlobItemConstructorAccessor { - PageBlobItem create(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, - final Boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, - final Long blobSequenceNumber, final String versionId, final byte[] contentCrc64); + /** + * Creates a new instance of {@link PageBlobItem}. + * + * @param eTag ETag of the page blob. + * @param lastModified Last modified time of the page blob. + * @param contentMd5 Content MD5 of the page blob. + * @param isServerEncrypted Flag indicating if the page blob is encrypted on the server. + * @param encryptionKeySha256 The encryption key used to encrypt the page blob. + * @param encryptionScope The encryption scope used to encrypt the page blob. + * @param blobSequenceNumber The current sequence number for the page blob. + * @param versionId The version identifier of the page blob. + * @param contentCrc64 Content CRC64 of the page blob. + * @return A new instance of {@link PageBlobItem}. + */ + PageBlobItem create(String eTag, OffsetDateTime lastModified, byte[] contentMd5, Boolean isServerEncrypted, + String encryptionKeySha256, String encryptionScope, Long blobSequenceNumber, String versionId, + byte[] contentCrc64); } + /** + * The method called from {@link PageBlobItem} to set its accessor. + * + * @param accessor The accessor. + */ public static void setAccessor(final PageBlobItemConstructorAccessor accessor) { PageBlobItemConstructorProxy.accessor = accessor; } - public static PageBlobItem create(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, - final Boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, - final Long blobSequenceNumber, final String versionId, final byte[] contentCrc64) { + /** + * Creates a new instance of {@link PageBlobItem}. + * + * @param eTag ETag of the page blob. + * @param lastModified Last modified time of the page blob. + * @param contentMd5 Content MD5 of the page blob. + * @param isServerEncrypted Flag indicating if the page blob is encrypted on the server. + * @param encryptionKeySha256 The encryption key used to encrypt the page blob. + * @param encryptionScope The encryption scope used to encrypt the page blob. + * @param blobSequenceNumber The current sequence number for the page blob. + * @param versionId The version identifier of the page blob. + * @param contentCrc64 Content CRC64 of the page blob. + * @return A new instance of {@link PageBlobItem}. + */ + public static PageBlobItem create(String eTag, OffsetDateTime lastModified, byte[] contentMd5, + Boolean isServerEncrypted, String encryptionKeySha256, String encryptionScope, Long blobSequenceNumber, + String versionId, byte[] contentCrc64) { + if (accessor == null) { new PageBlobItem(null, null, null, false, null, null, null, null); } From 831042bafb0590848e2948967e7b2e108e70be79 Mon Sep 17 00:00:00 2001 From: browndav Date: Mon, 18 May 2026 13:02:24 -0400 Subject: [PATCH 06/20] add support for crc64 for url upload, add tests --- .../blob/specialized/PageBlobAsyncClient.java | 14 ++++++-------- .../storage/blob/specialized/PageBlobClient.java | 1 - .../storage/blob/specialized/PageBlobApiTests.java | 12 +++++++++--- .../blob/specialized/PageBlobAsyncApiTests.java | 8 +++++++- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobAsyncClient.java index 3ed1f8713467..ab82584588c9 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobAsyncClient.java @@ -530,10 +530,9 @@ Mono> uploadPagesWithResponse(PageRange pageRange, Flux { PageBlobsUploadPagesHeaders hd = rb.getDeserializedHeaders(); - PageBlobItem item - = PageBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), - hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), - hd.getXMsBlobSequenceNumber(), hd.getXMsVersion(), hd.getXMsContentCrc64()); + PageBlobItem item = PageBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), + hd.getContentMD5(), hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), + hd.getXMsEncryptionScope(), hd.getXMsBlobSequenceNumber(), null, hd.getXMsContentCrc64()); return new SimpleResponse<>(rb, item); }); } @@ -722,10 +721,9 @@ sourceCpkKey, sourceCpkKeySha256, sourceCpkAlgorithm, getCustomerProvidedKey(), context) .map(rb -> { PageBlobsUploadPagesFromURLHeaders hd = rb.getDeserializedHeaders(); - PageBlobItem item - = PageBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), - hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), - hd.getXMsBlobSequenceNumber(), hd.getXMsVersion(), hd.getXMsContentCrc64()); + PageBlobItem item = PageBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), + hd.getContentMD5(), hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), + hd.getXMsEncryptionScope(), hd.getXMsBlobSequenceNumber(), null, hd.getXMsContentCrc64()); return new SimpleResponse<>(rb, item); }); } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobClient.java index dacf5323517b..693c02dbf6eb 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/PageBlobClient.java @@ -22,7 +22,6 @@ import com.azure.core.util.logging.ClientLogger; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobServiceVersion; -import com.azure.storage.blob.implementation.accesshelpers.PageBlobItemConstructorProxy; import com.azure.storage.blob.implementation.models.EncryptionScope; import com.azure.storage.blob.implementation.models.PageBlobsClearPagesHeaders; import com.azure.storage.blob.implementation.models.PageBlobsCopyIncrementalHeaders; diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java index 8ea59a7a08ca..4657eacba8fd 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java @@ -586,7 +586,7 @@ public void uploadPageFromURLIA() { } @Test - public void uploadPageFromURLMD5() { + public void uploadPageFromURLMD5() throws NoSuchAlgorithmException { PageBlobClient destURL = cc.getBlobClient(generateBlobName()).getPageBlobClient(); destURL.create(PageBlobClient.PAGE_BYTES); byte[] data = getRandomByteArray(PageBlobClient.PAGE_BYTES); @@ -595,8 +595,14 @@ public void uploadPageFromURLMD5() { String sas = bc.generateSas(new BlobServiceSasSignatureValues(testResourceNamer.now().plusDays(1), new BlobContainerSasPermission().setReadPermission(true))); - assertDoesNotThrow(() -> destURL.uploadPagesFromUrlWithResponse(pageRange, bc.getBlobUrl() + "?" + sas, null, - MessageDigest.getInstance("MD5").digest(data), null, null, null, null)); + Response response = destURL.uploadPagesFromUrlWithResponse(pageRange, bc.getBlobUrl() + "?" + sas, + null, MessageDigest.getInstance("MD5").digest(data), null, null, null, null); + byte[] expectedCrc64Content = Base64.getDecoder().decode(response.getHeaders().getValue(X_MS_CONTENT_CRC64)); + + assertResponseStatusCode(response, 201); + assertTrue(validateBasicHeaders(response.getHeaders())); + assertNotNull(response.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertArrayEquals(expectedCrc64Content, response.getValue().getContentCrc64()); } @Test diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java index 2a893fc865ba..ec182f423aa9 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java @@ -627,7 +627,13 @@ public void uploadPageFromURLMD5() throws NoSuchAlgorithmException { .then(destURL.uploadPagesFromUrlWithResponse(pageRange, bc.getBlobUrl() + "?" + sas, null, MessageDigest.getInstance("MD5").digest(data), null, null)); - StepVerifier.create(response).expectNextCount(1).verifyComplete(); + StepVerifier.create(response).assertNext(r -> { + assertResponseStatusCode(r, 201); + assertTrue(validateBasicHeaders(r.getHeaders())); + assertNotNull(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertArrayEquals(Base64.getDecoder().decode(r.getHeaders().getValue(X_MS_CONTENT_CRC64)), + r.getValue().getContentCrc64()); + }).verifyComplete(); } @Test From 32ffdb190b17b2fb8254980a1397958f0adfec26 Mon Sep 17 00:00:00 2001 From: browndav Date: Mon, 18 May 2026 13:23:02 -0400 Subject: [PATCH 07/20] add support for crc64 for blockblob --- .../BlockBlobItemConstructorProxy.java | 74 +++++++++++++++++++ .../storage/blob/models/BlockBlobItem.java | 22 ++++++ .../specialized/BlockBlobAsyncClient.java | 19 ++--- .../blob/specialized/BlockBlobApiTests.java | 7 ++ .../specialized/BlockBlobAsyncApiTests.java | 8 ++ 5 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/BlockBlobItemConstructorProxy.java diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/BlockBlobItemConstructorProxy.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/BlockBlobItemConstructorProxy.java new file mode 100644 index 000000000000..0d973e425bea --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/accesshelpers/BlockBlobItemConstructorProxy.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.accesshelpers; + +import com.azure.storage.blob.models.BlockBlobItem; + +import java.time.OffsetDateTime; + +/** + * Helper class to access private values of {@link BlockBlobItem} across package boundaries. + */ +public final class BlockBlobItemConstructorProxy { + private static BlockBlobItemConstructorAccessor accessor; + + private BlockBlobItemConstructorProxy() { + } + + /** + * Type defining the methods to set the non-public properties of a {@link BlockBlobItem}. + */ + public interface BlockBlobItemConstructorAccessor { + /** + * Creates a new instance of {@link BlockBlobItem}. + * + * @param eTag ETag of the block blob. + * @param lastModified Last modified time of the block blob. + * @param contentMd5 Content MD5 of the block blob. + * @param isServerEncrypted Flag indicating if the block blob is encrypted on the server. + * @param encryptionKeySha256 The encryption key used to encrypt the block blob. + * @param encryptionScope The encryption scope used to encrypt the block blob. + * @param versionId The version identifier of the block blob. + * @param contentCrc64 Content CRC64 of the block blob. + * @return A new instance of {@link BlockBlobItem}. + */ + BlockBlobItem create(String eTag, OffsetDateTime lastModified, byte[] contentMd5, Boolean isServerEncrypted, + String encryptionKeySha256, String encryptionScope, String versionId, byte[] contentCrc64); + } + + /** + * The method called from {@link BlockBlobItem} to set its accessor. + * + * @param accessor The accessor. + */ + public static void setAccessor(final BlockBlobItemConstructorAccessor accessor) { + BlockBlobItemConstructorProxy.accessor = accessor; + } + + /** + * Creates a new instance of {@link BlockBlobItem}. + * + * @param eTag ETag of the block blob. + * @param lastModified Last modified time of the block blob. + * @param contentMd5 Content MD5 of the block blob. + * @param isServerEncrypted Flag indicating if the block blob is encrypted on the server. + * @param encryptionKeySha256 The encryption key used to encrypt the block blob. + * @param encryptionScope The encryption scope used to encrypt the block blob. + * @param versionId The version identifier of the block blob. + * @param contentCrc64 Content CRC64 of the block blob. + * @return A new instance of {@link BlockBlobItem}. + */ + public static BlockBlobItem create(String eTag, OffsetDateTime lastModified, byte[] contentMd5, + Boolean isServerEncrypted, String encryptionKeySha256, String encryptionScope, String versionId, + byte[] contentCrc64) { + + if (accessor == null) { + new BlockBlobItem(null, null, null, (Boolean) null, null, null, null); + } + + assert accessor != null; + return accessor.create(eTag, lastModified, contentMd5, isServerEncrypted, encryptionKeySha256, encryptionScope, + versionId, contentCrc64); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlockBlobItem.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlockBlobItem.java index bdad9c963208..519c5bbabf06 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlockBlobItem.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlockBlobItem.java @@ -5,6 +5,7 @@ import com.azure.core.annotation.Immutable; import com.azure.core.util.CoreUtils; +import com.azure.storage.blob.implementation.accesshelpers.BlockBlobItemConstructorProxy; import java.time.OffsetDateTime; @@ -20,6 +21,11 @@ public class BlockBlobItem { private final String encryptionKeySha256; private final String encryptionScope; private final String versionId; + private final byte[] contentCrc64; + + static { + BlockBlobItemConstructorProxy.setAccessor(BlockBlobItem::new); + } /** * Constructs a {@link BlockBlobItem}. @@ -88,6 +94,12 @@ public BlockBlobItem(final String eTag, final OffsetDateTime lastModified, final public BlockBlobItem(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, final Boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, final String versionId) { + this(eTag, lastModified, contentMd5, isServerEncrypted, encryptionKeySha256, encryptionScope, versionId, null); + } + + private BlockBlobItem(final String eTag, final OffsetDateTime lastModified, final byte[] contentMd5, + final Boolean isServerEncrypted, final String encryptionKeySha256, final String encryptionScope, + final String versionId, final byte[] contentCrc64) { this.eTag = eTag; this.lastModified = lastModified; this.contentMd5 = CoreUtils.clone(contentMd5); @@ -95,6 +107,7 @@ public BlockBlobItem(final String eTag, final OffsetDateTime lastModified, final this.encryptionKeySha256 = encryptionKeySha256; this.encryptionScope = encryptionScope; this.versionId = versionId; + this.contentCrc64 = CoreUtils.clone(contentCrc64); } /** @@ -151,6 +164,15 @@ public byte[] getContentMd5() { return CoreUtils.clone(contentMd5); } + /** + * Gets the calculated CRC64 of the block blob. + * + * @return the calculated CRC64 of the block blob + */ + public byte[] getContentCrc64() { + return CoreUtils.clone(contentCrc64); + } + /** * Gets the version identifier of the block blob. * diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/BlockBlobAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/BlockBlobAsyncClient.java index f938e21b4793..d1d4464f74b7 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/BlockBlobAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/BlockBlobAsyncClient.java @@ -15,6 +15,7 @@ import com.azure.core.util.logging.ClientLogger; import com.azure.storage.blob.BlobAsyncClient; import com.azure.storage.blob.BlobServiceVersion; +import com.azure.storage.blob.implementation.accesshelpers.BlockBlobItemConstructorProxy; import com.azure.storage.blob.implementation.models.BlockBlobsCommitBlockListHeaders; import com.azure.storage.blob.implementation.models.BlockBlobsPutBlobFromUrlHeaders; import com.azure.storage.blob.implementation.models.BlockBlobsUploadHeaders; @@ -442,9 +443,9 @@ Mono> uploadWithResponse(BlockBlobSimpleUploadOptions op null, null, options.getHeaders(), getCustomerProvidedKey(), encryptionScope, finalContext) .map(rb -> { BlockBlobsUploadHeaders hd = rb.getDeserializedHeaders(); - BlockBlobItem item = new BlockBlobItem(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), - hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), - hd.getXMsVersionId()); + BlockBlobItem item = BlockBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), + hd.getContentMD5(), hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), + hd.getXMsEncryptionScope(), hd.getXMsVersionId(), hd.getXMsContentCrc64()); return new SimpleResponse<>(rb, item); })); } @@ -601,9 +602,9 @@ Mono> uploadFromUrlWithResponse(BlobUploadFromUrlOptions options.getHeaders(), getCustomerProvidedKey(), encryptionScope, context) .map(rb -> { BlockBlobsPutBlobFromUrlHeaders hd = rb.getDeserializedHeaders(); - BlockBlobItem item = new BlockBlobItem(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), - hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), - hd.getXMsVersionId()); + BlockBlobItem item = BlockBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), + hd.getContentMD5(), hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), + hd.getXMsEncryptionScope(), hd.getXMsVersionId(), hd.getXMsContentCrc64()); return new SimpleResponse<>(rb, item); }); } @@ -1178,9 +1179,9 @@ Mono> commitBlockListWithResponse(BlockBlobCommitBlockLi getCustomerProvidedKey(), encryptionScope, context) .map(rb -> { BlockBlobsCommitBlockListHeaders hd = rb.getDeserializedHeaders(); - BlockBlobItem item = new BlockBlobItem(hd.getETag(), hd.getLastModified(), hd.getContentMD5(), - hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), hd.getXMsEncryptionScope(), - hd.getXMsVersionId()); + BlockBlobItem item = BlockBlobItemConstructorProxy.create(hd.getETag(), hd.getLastModified(), + hd.getContentMD5(), hd.isXMsRequestServerEncrypted(), hd.getXMsEncryptionKeySha256(), + hd.getXMsEncryptionScope(), hd.getXMsVersionId(), hd.getXMsContentCrc64()); return new SimpleResponse<>(rb, item); }); } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java index c2a903145440..8d0c561c6cb6 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java @@ -543,9 +543,12 @@ public void commitBlockList() { = blockBlobClient.commitBlockListWithResponse(ids, null, null, null, null, null, null); HttpHeaders headers = response.getHeaders(); + byte[] expectedCrc64Content = Base64.getDecoder().decode(headers.getValue(X_MS_CONTENT_CRC64)); + assertResponseStatusCode(response, 201); validateBasicHeaders(headers); assertNotNull(headers.getValue(X_MS_CONTENT_CRC64)); + TestUtils.assertArraysEqual(expectedCrc64Content, response.getValue().getContentCrc64()); assertTrue(Boolean.parseBoolean(headers.getValue(X_MS_REQUEST_SERVER_ENCRYPTED))); } @@ -839,8 +842,12 @@ public void upload() { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); blockBlobClient.downloadStream(outStream); TestUtils.assertArraysEqual(outStream.toByteArray(), DATA.getDefaultText().getBytes(StandardCharsets.UTF_8)); + byte[] expectedCrc64Content = Base64.getDecoder().decode(response.getHeaders().getValue(X_MS_CONTENT_CRC64)); + validateBasicHeaders(response.getHeaders()); assertNotNull(response.getHeaders().getValue(HttpHeaderName.CONTENT_MD5)); + assertNotNull(response.getHeaders().getValue(X_MS_CONTENT_CRC64)); + TestUtils.assertArraysEqual(expectedCrc64Content, response.getValue().getContentCrc64()); assertTrue(Boolean.parseBoolean(response.getHeaders().getValue(X_MS_REQUEST_SERVER_ENCRYPTED))); } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java index 664fe555846d..9d12861a5d59 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java @@ -625,8 +625,11 @@ public void commitBlockList() { .then(blockBlobAsyncClient.commitBlockListWithResponse(ids, null, null, null, null))).assertNext(r -> { assertResponseStatusCode(r, 201); HttpHeaders headers = r.getHeaders(); + byte[] expectedCrc64Content = Base64.getDecoder().decode(headers.getValue(X_MS_CONTENT_CRC64)); + validateBasicHeaders(headers); assertNotNull(headers.getValue(X_MS_CONTENT_CRC64)); + TestUtils.assertArraysEqual(expectedCrc64Content, r.getValue().getContentCrc64()); assertTrue(Boolean.parseBoolean(headers.getValue(X_MS_REQUEST_SERVER_ENCRYPTED))); }).verifyComplete(); } @@ -956,7 +959,12 @@ public void upload() { .assertNext(r -> { assertResponseStatusCode(r, 201); validateBasicHeaders(r.getHeaders()); + byte[] expectedCrc64Content = Base64.getDecoder().decode(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertNotNull(r.getHeaders().getValue(HttpHeaderName.CONTENT_MD5)); + assertNotNull(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); + TestUtils.assertArraysEqual(Base64.getDecoder().decode(r.getHeaders().getValue(X_MS_CONTENT_CRC64)), + r.getValue().getContentCrc64()); assertTrue(Boolean.parseBoolean(r.getHeaders().getValue(X_MS_REQUEST_SERVER_ENCRYPTED))); }) .verifyComplete(); From 6ba8ad47a220df4e65a566d098dead6de4ce0eb0 Mon Sep 17 00:00:00 2001 From: browndav Date: Mon, 18 May 2026 19:22:47 -0400 Subject: [PATCH 08/20] add service versions for tests, add recordings --- sdk/storage/azure-storage-blob/assets.json | 2 +- .../com/azure/storage/blob/specialized/AppendBlobApiTests.java | 1 + .../azure/storage/blob/specialized/AppendBlobAsyncApiTests.java | 1 + .../com/azure/storage/blob/specialized/BlockBlobApiTests.java | 2 ++ .../azure/storage/blob/specialized/BlockBlobAsyncApiTests.java | 2 ++ .../com/azure/storage/blob/specialized/PageBlobApiTests.java | 2 ++ .../azure/storage/blob/specialized/PageBlobAsyncApiTests.java | 2 ++ 7 files changed, 11 insertions(+), 1 deletion(-) diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index 8cad139f33ff..dc9d5392be3f 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-blob", - "Tag": "java/storage/azure-storage-blob_47f4243e59" + "Tag": "java/storage/azure-storage-blob_decbaa1f11" } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java index ba3fbd13a91c..fcdcbc96f2b8 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java @@ -342,6 +342,7 @@ private Stream createIfNotExistsTagsSupplier() { Arguments.of(" +-./:=_ +-./:=_", " +-./:=_", null, null)); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void appendBlockDefaults() { Response appendResponse = bc.appendBlockWithResponse(DATA.getDefaultInputStream(), diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java index 9138919a78a5..99c7a153d3d7 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java @@ -348,6 +348,7 @@ private Stream createIfNotExistsTagsSupplier() { Arguments.of(" +-./:=_ +-./:=_", " +-./:=_", null, null)); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void appendBlockDefaults() { StepVerifier.create(bc.appendBlockWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, null)) diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java index 8d0c561c6cb6..67d6b6805a6c 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java @@ -533,6 +533,7 @@ private static Stream stageBlockFromURLSourceACFailSupplier() { Arguments.of(null, null, GARBAGE_ETAG, null), Arguments.of(null, null, null, RECEIVED_ETAG)); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void commitBlockList() { String blockID = getBlockID(); @@ -833,6 +834,7 @@ public void getBlockListError() { () -> blockBlobClient.listBlocks(BlockListType.ALL).getCommittedBlocks().iterator().hasNext()); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void upload() { Response response = blockBlobClient.uploadWithResponse(DATA.getDefaultInputStream(), diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java index 9d12861a5d59..aad50139e449 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java @@ -616,6 +616,7 @@ private static Stream stageBlockFromURLSourceACFailSupplier() { Arguments.of(null, null, GARBAGE_ETAG, null), Arguments.of(null, null, null, RECEIVED_ETAG)); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void commitBlockList() { String blockID = getBlockID(); @@ -951,6 +952,7 @@ public void getBlockListError() { StepVerifier.create(blockBlobAsyncClient.listBlocks(BlockListType.ALL)).verifyError(BlobStorageException.class); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void upload() { StepVerifier diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java index 4657eacba8fd..0e87b9cafa67 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java @@ -367,6 +367,7 @@ private static Stream createIfNotExistsTagsSupplier() { Arguments.of(" +-./:=_ +-./:=_", " +-./:=_", null, null)); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void uploadPage() { Response response @@ -585,6 +586,7 @@ public void uploadPageFromURLIA() { () -> bc.uploadPagesFromUrl(null, bc.getBlobUrl(), (long) PageBlobClient.PAGE_BYTES)); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void uploadPageFromURLMD5() throws NoSuchAlgorithmException { PageBlobClient destURL = cc.getBlobClient(generateBlobName()).getPageBlobClient(); diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java index ec182f423aa9..4005fa920ba0 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java @@ -373,6 +373,7 @@ private static Stream createIfNotExistsTagsSupplier() { Arguments.of(" +-./:=_ +-./:=_", " +-./:=_", null, null)); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void uploadPage() { StepVerifier @@ -614,6 +615,7 @@ public void uploadPageFromURLIA() { .verifyError(IllegalArgumentException.class); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void uploadPageFromURLMD5() throws NoSuchAlgorithmException { PageBlobAsyncClient destURL = ccAsync.getBlobAsyncClient(generateBlobName()).getPageBlobAsyncClient(); From 7451e0d0d64d6bf37b3619532adef4b6b479d937 Mon Sep 17 00:00:00 2001 From: browndav Date: Mon, 18 May 2026 19:30:36 -0400 Subject: [PATCH 09/20] remove added folder --- sdk/storage/feature/spec.txt | 103 ----------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 sdk/storage/feature/spec.txt diff --git a/sdk/storage/feature/spec.txt b/sdk/storage/feature/spec.txt deleted file mode 100644 index b45432f98b54..000000000000 --- a/sdk/storage/feature/spec.txt +++ /dev/null @@ -1,103 +0,0 @@ -MD5/CRC64 Combined Return - -Author: Josh Martin (joshuamartin) - -Date: September 2025 - -Background - -OneDrive/SharePoint (ODSP) is interested in moving away from MD5 to CRC64, primarily to support their construction of an “uber blob”. The uber blob is an ODSP mechanism to combine smaller, rarely accessed, blobs in a larger single blob for archive storage, and will be constructed using PutBlock From URL. Currently, ODSP has MD5 checksums for their data, and they need to exchange this known MD5 for a CRC64. - -When a client makes a PutBlock From URL request today, either the MD5 or the CRC64 value calculated by the service is returned in the response header Content-MD5 or x-ms-content-crc64 respectively. For versions after 2019-02-02, the CRC64 value is returned by default unless a MD5 header was presented with the request for validation with that value, in which case Content-MD5 is returned. - -The ask from ODSP is for XStore to support the transition to CRC64 by returning the service calculated MD5 and CRC64 values for PutBlock from URL APIs when these APIs are provided with a MD5 hash, rather than only the MD5 hash alone. This change drastically simplifies the work on the ODSP side to transition from MD5 to CRC64 values for their uber blob, and also saves significantly on COGS as the team will no longer need to read back the blob to retrieve the CRC. - -Rather than just make the change for this API, we are proposing to support this change for all blob APIs for consistency. - -Technical Details - -Today, assuming a request is made with the 2019-02-02 API version or greater, it will be wrapped with a CrcReaderStream (see here). If a client provides a Content MD5 header, or CRC isn’t supported at all, the stream is wrapped by a MD5ReaderStream (see here). Importantly, these conditions aren’t mutually exclusive, so a request made with a Content-MD5 header using an API version after CRC64 support is introduced will result in the copy source stream being wrapped by both a MD5 Reader and CRC64 reader stream. - -Despite the streams being wrapped together, the service will only return one of the checksum values (MD5 if requested or the version is before 2019-02-02, CRC64 otherwise). This separation is enforced here, among other locations. - -This change would modify the service to return both values if the client sends a Content-MD5 header, so long as the API version for the request is 2026-10-06 or greater. The returned CRC64 value would be in the response header x-ms-content-crc64. - -Impacted APIs - -PutBlock - -PutBlock from URL - -PutPage - -PutPage from URL - -AppendBlock - -AppendBlock from URL - -PutBlob – (Note: support already exists for this behavior today without any change to the service, assuming a blob type of block. Page/append types would be irrelevant as those requests have no content attached to them.) - -PutBlob from URL - -Sample Request and Response - -Request - -PUT https://myaccount.blob.core.windows.net/mycontainer/myblob?comp=block&blockid=id - -x-ms-source-content-md5: {MD5 hash value of source content} - -x-ms-copy-source header: {copy source location} - -*other headers as needed* - -Response - -HTTP/1.1 200 OK - -Content-MD5: {MD5 hash value of source content} - -x-ms-content-crc64: {CRC64 value of source content} - -*other headers as expected* - -Testing - -CVTs covering the positive case for each impacted API are required, both to ensure functional correctness and managed/native parity when necessary. Tests should also ensure requests made prior to CRC64 support being introduced do not return the header value. - -SCTE support should be simple to add, as existing workloads could be modified to check the existence of the CRC64 response header if the API version supports the behavior. - -Perf testing for this change seems unnecessary, as the service is already calculating both values. - -Config Rollout - -This change is gated by two DCs, one managed and one native, that share the same name. - -Managed: XStoreConfigSettings/BlobXfe/Settings/ReturnCrcForAllPutRequests. A background DC that defaults to false in the XSD. - -Native: A native request setting associated with the Oct26 Storage API version. This DC will change from false to true once the LatestEnabledVersion DC has been updated to this API version but can be explicitly set false if needed. This aligns with other versioned DCs. - -Q & A - -Q: Should File APIs also be changed? - -A: The implementation would be very similar, as the streams are already wrapped. At the moment though, only Content-MD5 is documented for PutRange, making this change a larger effort. - -Q: Will this change allow clients to pass both Content-MD5 and x-ms-content-crc64 headers on the same request? - -A: No, this restriction will still be in place. The only behavior change is the service returning the calculated CRC64 value after the MD5 is calculated/verified. - -Q: Will this change the existing error behavior if the provided MD5 does not match the value calculated by the service? - -A: No, the request will still be rejected due to MD5 mismatch, with no CRC returned to the client. - -Q: Are any telemetry changes needed? - -A: None are needed on the happy path. A metric (MissingCrcValue) will be emitted if the CRC cannot be added to the return path when it is expected. - -Q: Will Content-MD5 be returned if not requested? - -A: No. We support both to facilitate migration to crc64 and away from md5. There are no plans to always return an MD5. - -Future questions can go here From dca4a80328ac0331026062ffbe668e29a6343e73 Mon Sep 17 00:00:00 2001 From: browndav Date: Mon, 18 May 2026 19:31:53 -0400 Subject: [PATCH 10/20] remove changelog entry --- sdk/storage/azure-storage-blob/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/storage/azure-storage-blob/CHANGELOG.md b/sdk/storage/azure-storage-blob/CHANGELOG.md index 03c6659081df..6c06de3117ad 100644 --- a/sdk/storage/azure-storage-blob/CHANGELOG.md +++ b/sdk/storage/azure-storage-blob/CHANGELOG.md @@ -4,8 +4,6 @@ ### Features Added -- Added `PageBlobItem.getContentCrc64()` to expose CRC64 values returned by page operations. - ### Breaking Changes ### Bugs Fixed From b5f28d59a1c67e8f8bb74adb9cd1cd6104a4f530 Mon Sep 17 00:00:00 2001 From: browndav Date: Tue, 19 May 2026 13:14:58 -0400 Subject: [PATCH 11/20] add tests for uploadUrl to BlockBlob and AppendBlob --- .../blob/specialized/AppendBlobApiTests.java | 15 ++++++++++++--- .../specialized/AppendBlobAsyncApiTests.java | 16 +++++++++++++--- .../blob/specialized/BlockBlobApiTests.java | 2 ++ .../blob/specialized/BlockBlobAsyncApiTests.java | 6 +++--- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java index fcdcbc96f2b8..e61da712befe 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java @@ -54,6 +54,7 @@ import java.util.stream.Stream; import static com.azure.storage.blob.specialized.AppendBlobClient.MAX_APPEND_BLOCKS; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -564,9 +565,11 @@ public void appendBlockFromURLRange() { TestUtils.assertArraysEqual(data, 2 * 1024, downloadStream.toByteArray(), 0, 1024); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test - public void appendBlockFromURLMD5() { + public void appendBlockFromURLMD5() throws NoSuchAlgorithmException { byte[] data = getRandomByteArray(1024); + byte[] expectedContentMd5 = MessageDigest.getInstance("MD5").digest(data); bc.appendBlock(new ByteArrayInputStream(data), data.length); AppendBlobClient destURL = cc.getBlobClient(generateBlobName()).getAppendBlobClient(); @@ -574,9 +577,15 @@ public void appendBlockFromURLMD5() { String sas = bc.generateSas(new BlobServiceSasSignatureValues(testResourceNamer.now().plusDays(1), new BlobContainerSasPermission().setReadPermission(true))); - assertDoesNotThrow(() -> destURL.appendBlockFromUrlWithResponse(bc.getBlobUrl() + "?" + sas, null, - MessageDigest.getInstance("MD5").digest(data), null, null, null, Context.NONE)); + Response response = destURL.appendBlockFromUrlWithResponse(bc.getBlobUrl() + "?" + sas, null, + expectedContentMd5, null, null, null, Context.NONE); + assertResponseStatusCode(response, 201); + validateBasicHeaders(response.getHeaders()); + assertArrayEquals(expectedContentMd5, response.getValue().getContentMd5()); + String contentCrc64 = response.getHeaders().getValue(X_MS_CONTENT_CRC64); + assertNotNull(contentCrc64); + assertArrayEquals(Base64.getDecoder().decode(contentCrc64), response.getValue().getContentCrc64()); } @Test diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java index 99c7a153d3d7..3b8d6a5b7e66 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java @@ -54,6 +54,7 @@ import java.util.stream.Stream; import static com.azure.storage.blob.specialized.AppendBlobClient.MAX_APPEND_BLOCKS; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -577,9 +578,11 @@ public void appendBlockFromURLRange() { .verifyComplete(); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void appendBlockFromURLMD5() throws NoSuchAlgorithmException { byte[] data = getRandomByteArray(1024); + byte[] expectedContentMd5 = MessageDigest.getInstance("MD5").digest(data); AppendBlobAsyncClient destURL = ccAsync.getBlobAsyncClient(generateBlobName()).getAppendBlobAsyncClient(); @@ -588,10 +591,17 @@ public void appendBlockFromURLMD5() throws NoSuchAlgorithmException { Mono> response = bc.appendBlock(Flux.just(ByteBuffer.wrap(data)), data.length) .then(destURL.create()) - .then(destURL.appendBlockFromUrlWithResponse(bc.getBlobUrl() + "?" + sas, null, - MessageDigest.getInstance("MD5").digest(data), null, null)); + .then(destURL.appendBlockFromUrlWithResponse(bc.getBlobUrl() + "?" + sas, null, expectedContentMd5, null, + null)); - StepVerifier.create(response).expectNextCount(1).verifyComplete(); + StepVerifier.create(response).assertNext(r -> { + assertResponseStatusCode(r, 201); + validateBasicHeaders(r.getHeaders()); + assertArrayEquals(expectedContentMd5, r.getValue().getContentMd5()); + String contentCrc64 = r.getHeaders().getValue(X_MS_CONTENT_CRC64); + assertNotNull(contentCrc64); + assertArrayEquals(Base64.getDecoder().decode(contentCrc64), r.getValue().getContentCrc64()); + }).verifyComplete(); } @Test diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java index 67d6b6805a6c..75bebce582a1 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java @@ -1514,6 +1514,8 @@ public void uploadFromUrlMax() throws NoSuchAlgorithmException { assertNotNull(blockBlobItem); assertNotNull(blockBlobItem.getETag()); assertNotNull(blockBlobItem.getLastModified()); + assertNotNull(blockBlobItem.getContentMd5()); + assertNotNull(blockBlobItem.getContentCrc64()); TestUtils.assertArraysEqual(DATA.getDefaultBytes(), os.toByteArray()); assertEquals("en-GB", destinationProperties.getContentLanguage()); assertEquals("text", destinationProperties.getContentType()); diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java index aad50139e449..b9c39c44ceae 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java @@ -965,8 +965,7 @@ public void upload() { assertNotNull(r.getHeaders().getValue(HttpHeaderName.CONTENT_MD5)); assertNotNull(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); - TestUtils.assertArraysEqual(Base64.getDecoder().decode(r.getHeaders().getValue(X_MS_CONTENT_CRC64)), - r.getValue().getContentCrc64()); + TestUtils.assertArraysEqual(expectedCrc64Content, r.getValue().getContentCrc64()); assertTrue(Boolean.parseBoolean(r.getHeaders().getValue(X_MS_REQUEST_SERVER_ENCRYPTED))); }) .verifyComplete(); @@ -2469,7 +2468,7 @@ public void uploadFromUrlOverwriteFailsOnExistingBlob() { }); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-04-08") + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void uploadFromUrlMax() throws NoSuchAlgorithmException { BlobAsyncClient sourceBlob = primaryBlobServiceAsyncClient.getBlobContainerAsyncClient(containerName) @@ -2501,6 +2500,7 @@ public void uploadFromUrlMax() throws NoSuchAlgorithmException { assertNotNull(blockBlobItem); assertNotNull(blockBlobItem.getETag()); assertNotNull(blockBlobItem.getLastModified()); + assertNotNull(blockBlobItem.getContentCrc64()); }).verifyComplete(); StepVerifier.create(blobAsyncClient.getProperties()).assertNext(r -> { From d1a0f782a412511d15efec9d246b13d2fe86fb9c Mon Sep 17 00:00:00 2001 From: browndav Date: Tue, 19 May 2026 13:15:50 -0400 Subject: [PATCH 12/20] add recordings for uploadUrl for appendblob and blockblob --- sdk/storage/azure-storage-blob/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index dc9d5392be3f..846f6b9cf17b 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-blob", - "Tag": "java/storage/azure-storage-blob_decbaa1f11" + "Tag": "java/storage/azure-storage-blob_08d9fe3e3e" } From 97396f4caababfa74ffa1c07a33ff9064fed8c66 Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 20 May 2026 12:48:47 -0400 Subject: [PATCH 13/20] change appendblob tests --- .../blob/specialized/AppendBlobApiTests.java | 35 +++++++++++++++++- .../specialized/AppendBlobAsyncApiTests.java | 37 ++++++++++++++++++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java index e61da712befe..7b9925cb808c 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobApiTests.java @@ -343,7 +343,6 @@ private Stream createIfNotExistsTagsSupplier() { Arguments.of(" +-./:=_ +-./:=_", " +-./:=_", null, null)); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void appendBlockDefaults() { Response appendResponse = bc.appendBlockWithResponse(DATA.getDefaultInputStream(), @@ -353,6 +352,23 @@ public void appendBlockDefaults() { bc.downloadStream(downloadStream); TestUtils.assertArraysEqual(DATA.getDefaultBytes(), downloadStream.toByteArray()); + validateBasicHeaders(appendResponse.getHeaders()); + assertNotNull(appendResponse.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertNotNull(appendResponse.getValue().getBlobAppendOffset()); + assertNotNull(appendResponse.getValue().getBlobCommittedBlockCount()); + assertEquals(1, bc.getProperties().getCommittedBlockCount()); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void appendBlockDefaultsWithCrc64() { + Response appendResponse = bc.appendBlockWithResponse(DATA.getDefaultInputStream(), + DATA.getDefaultDataSize(), null, null, null, null); + + ByteArrayOutputStream downloadStream = new ByteArrayOutputStream(); + bc.downloadStream(downloadStream); + TestUtils.assertArraysEqual(DATA.getDefaultBytes(), downloadStream.toByteArray()); + validateBasicHeaders(appendResponse.getHeaders()); assertNotNull(appendResponse.getHeaders().getValue(X_MS_CONTENT_CRC64)); byte[] expectedContentCrc64 @@ -565,9 +581,24 @@ public void appendBlockFromURLRange() { TestUtils.assertArraysEqual(data, 2 * 1024, downloadStream.toByteArray(), 0, 1024); } + @Test + public void appendBlockFromURLMD5() { + byte[] data = getRandomByteArray(1024); + bc.appendBlock(new ByteArrayInputStream(data), data.length); + + AppendBlobClient destURL = cc.getBlobClient(generateBlobName()).getAppendBlobClient(); + destURL.create(); + + String sas = bc.generateSas(new BlobServiceSasSignatureValues(testResourceNamer.now().plusDays(1), + new BlobContainerSasPermission().setReadPermission(true))); + assertDoesNotThrow(() -> destURL.appendBlockFromUrlWithResponse(bc.getBlobUrl() + "?" + sas, null, + MessageDigest.getInstance("MD5").digest(data), null, null, null, Context.NONE)); + + } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test - public void appendBlockFromURLMD5() throws NoSuchAlgorithmException { + public void appendBlockFromUrlMd5Crc64() throws NoSuchAlgorithmException { byte[] data = getRandomByteArray(1024); byte[] expectedContentMd5 = MessageDigest.getInstance("MD5").digest(data); bc.appendBlock(new ByteArrayInputStream(data), data.length); diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java index 3b8d6a5b7e66..17d4a1e074da 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/AppendBlobAsyncApiTests.java @@ -349,9 +349,25 @@ private Stream createIfNotExistsTagsSupplier() { Arguments.of(" +-./:=_ +-./:=_", " +-./:=_", null, null)); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void appendBlockDefaults() { + StepVerifier.create(bc.appendBlockWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, null)) + .assertNext(r -> { + validateBasicHeaders(r.getHeaders()); + assertNotNull(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertNotNull(r.getValue().getBlobAppendOffset()); + assertNotNull(r.getValue().getBlobCommittedBlockCount()); + }) + .verifyComplete(); + + StepVerifier.create(FluxUtil.collectBytesInByteBufferStream(bc.downloadStream())) + .assertNext(r -> TestUtils.assertArraysEqual(DATA.getDefaultBytes(), r)) + .verifyComplete(); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void appendBlockDefaultsWithCrc64() { StepVerifier.create(bc.appendBlockWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, null)) .assertNext(r -> { validateBasicHeaders(r.getHeaders()); @@ -578,10 +594,27 @@ public void appendBlockFromURLRange() { .verifyComplete(); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void appendBlockFromURLMD5() throws NoSuchAlgorithmException { byte[] data = getRandomByteArray(1024); + + AppendBlobAsyncClient destURL = ccAsync.getBlobAsyncClient(generateBlobName()).getAppendBlobAsyncClient(); + + String sas = bc.generateSas(new BlobServiceSasSignatureValues(testResourceNamer.now().plusDays(1), + new BlobSasPermission().setTagsPermission(true).setReadPermission(true))); + + Mono> response = bc.appendBlock(Flux.just(ByteBuffer.wrap(data)), data.length) + .then(destURL.create()) + .then(destURL.appendBlockFromUrlWithResponse(bc.getBlobUrl() + "?" + sas, null, + MessageDigest.getInstance("MD5").digest(data), null, null)); + + StepVerifier.create(response).expectNextCount(1).verifyComplete(); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void appendBlockFromUrlMd5Crc64() throws NoSuchAlgorithmException { + byte[] data = getRandomByteArray(1024); byte[] expectedContentMd5 = MessageDigest.getInstance("MD5").digest(data); AppendBlobAsyncClient destURL = ccAsync.getBlobAsyncClient(generateBlobName()).getAppendBlobAsyncClient(); From 342c7458094853c74e66967174e7d65f5e8dc395 Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 20 May 2026 13:19:18 -0400 Subject: [PATCH 14/20] change blockblobapi tests --- .../blob/specialized/BlockBlobApiTests.java | 35 ++++++++++++++++- .../specialized/BlockBlobAsyncApiTests.java | 38 ++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java index 75bebce582a1..4f917bf34046 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java @@ -533,7 +533,6 @@ private static Stream stageBlockFromURLSourceACFailSupplier() { Arguments.of(null, null, GARBAGE_ETAG, null), Arguments.of(null, null, null, RECEIVED_ETAG)); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void commitBlockList() { String blockID = getBlockID(); @@ -544,6 +543,23 @@ public void commitBlockList() { = blockBlobClient.commitBlockListWithResponse(ids, null, null, null, null, null, null); HttpHeaders headers = response.getHeaders(); + assertResponseStatusCode(response, 201); + validateBasicHeaders(headers); + assertNotNull(headers.getValue(X_MS_CONTENT_CRC64)); + assertTrue(Boolean.parseBoolean(headers.getValue(X_MS_REQUEST_SERVER_ENCRYPTED))); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void commitBlockListCrc64() { + String blockID = getBlockID(); + blockBlobClient.stageBlock(blockID, DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + List ids = Collections.singletonList(blockID); + + Response response + = blockBlobClient.commitBlockListWithResponse(ids, null, null, null, null, null, null); + HttpHeaders headers = response.getHeaders(); + byte[] expectedCrc64Content = Base64.getDecoder().decode(headers.getValue(X_MS_CONTENT_CRC64)); assertResponseStatusCode(response, 201); @@ -834,12 +850,27 @@ public void getBlockListError() { () -> blockBlobClient.listBlocks(BlockListType.ALL).getCommittedBlocks().iterator().hasNext()); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void upload() { Response response = blockBlobClient.uploadWithResponse(DATA.getDefaultInputStream(), DATA.getDefaultDataSize(), null, null, null, null, null, null, null); + assertResponseStatusCode(response, 201); + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + blockBlobClient.downloadStream(outStream); + TestUtils.assertArraysEqual(outStream.toByteArray(), DATA.getDefaultText().getBytes(StandardCharsets.UTF_8)); + + validateBasicHeaders(response.getHeaders()); + assertNotNull(response.getHeaders().getValue(HttpHeaderName.CONTENT_MD5)); + assertTrue(Boolean.parseBoolean(response.getHeaders().getValue(X_MS_REQUEST_SERVER_ENCRYPTED))); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void uploadReturnsCrc64Content() { + Response response = blockBlobClient.uploadWithResponse(DATA.getDefaultInputStream(), + DATA.getDefaultDataSize(), null, null, null, null, null, null, null); + assertResponseStatusCode(response, 201); ByteArrayOutputStream outStream = new ByteArrayOutputStream(); blockBlobClient.downloadStream(outStream); diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java index b9c39c44ceae..13795115d2b4 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java @@ -616,12 +616,28 @@ private static Stream stageBlockFromURLSourceACFailSupplier() { Arguments.of(null, null, GARBAGE_ETAG, null), Arguments.of(null, null, null, RECEIVED_ETAG)); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void commitBlockList() { String blockID = getBlockID(); List ids = Collections.singletonList(blockID); + StepVerifier.create(blockBlobAsyncClient.stageBlock(blockID, DATA.getDefaultFlux(), DATA.getDefaultDataSize()) + .then(blockBlobAsyncClient.commitBlockListWithResponse(ids, null, null, null, null))).assertNext(r -> { + assertResponseStatusCode(r, 201); + HttpHeaders headers = r.getHeaders(); + + validateBasicHeaders(headers); + assertNotNull(headers.getValue(X_MS_CONTENT_CRC64)); + assertTrue(Boolean.parseBoolean(headers.getValue(X_MS_REQUEST_SERVER_ENCRYPTED))); + }).verifyComplete(); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void commitBlockListCrc64() { + String blockID = getBlockID(); + List ids = Collections.singletonList(blockID); + StepVerifier.create(blockBlobAsyncClient.stageBlock(blockID, DATA.getDefaultFlux(), DATA.getDefaultDataSize()) .then(blockBlobAsyncClient.commitBlockListWithResponse(ids, null, null, null, null))).assertNext(r -> { assertResponseStatusCode(r, 201); @@ -955,6 +971,26 @@ public void getBlockListError() { @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void upload() { + StepVerifier + .create(blockBlobAsyncClient.uploadWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, + null, null, null, null)) + .assertNext(r -> { + assertResponseStatusCode(r, 201); + validateBasicHeaders(r.getHeaders()); + + assertNotNull(r.getHeaders().getValue(HttpHeaderName.CONTENT_MD5)); + assertTrue(Boolean.parseBoolean(r.getHeaders().getValue(X_MS_REQUEST_SERVER_ENCRYPTED))); + }) + .verifyComplete(); + + StepVerifier.create(FluxUtil.collectBytesInByteBufferStream(blockBlobAsyncClient.downloadStream())) + .assertNext(r -> TestUtils.assertArraysEqual(r, DATA.getDefaultBytes())) + .verifyComplete(); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void uploadReturnsCrc64Content() { StepVerifier .create(blockBlobAsyncClient.uploadWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, null, null, null, null)) From 1d7e404c18302afb2ab37faa1bd0455d58143fde Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 20 May 2026 13:36:01 -0400 Subject: [PATCH 15/20] change pageblob api tests --- .../blob/specialized/PageBlobApiTests.java | 35 ++++++++++++++++-- .../specialized/PageBlobAsyncApiTests.java | 36 +++++++++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java index 0e87b9cafa67..ffae25810b3f 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobApiTests.java @@ -367,13 +367,26 @@ private static Stream createIfNotExistsTagsSupplier() { Arguments.of(" +-./:=_ +-./:=_", " +-./:=_", null, null)); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void uploadPage() { Response response = bc.uploadPagesWithResponse(new PageRange().setStart(0).setEnd(PageBlobClient.PAGE_BYTES - 1), new ByteArrayInputStream(getRandomByteArray(PageBlobClient.PAGE_BYTES)), null, null, null, null); + assertResponseStatusCode(response, 201); + assertTrue(validateBasicHeaders(response.getHeaders())); + assertNotNull(response.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertEquals(0, response.getValue().getBlobSequenceNumber()); + assertTrue(response.getValue().isServerEncrypted()); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void uploadPageCrc64() { + Response response + = bc.uploadPagesWithResponse(new PageRange().setStart(0).setEnd(PageBlobClient.PAGE_BYTES - 1), + new ByteArrayInputStream(getRandomByteArray(PageBlobClient.PAGE_BYTES)), null, null, null, null); + byte[] expectedContentCrc64 = Base64.getDecoder().decode(response.getHeaders().getValue(X_MS_CONTENT_CRC64)); assertResponseStatusCode(response, 201); @@ -586,7 +599,6 @@ public void uploadPageFromURLIA() { () -> bc.uploadPagesFromUrl(null, bc.getBlobUrl(), (long) PageBlobClient.PAGE_BYTES)); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void uploadPageFromURLMD5() throws NoSuchAlgorithmException { PageBlobClient destURL = cc.getBlobClient(generateBlobName()).getPageBlobClient(); @@ -595,6 +607,25 @@ public void uploadPageFromURLMD5() throws NoSuchAlgorithmException { PageRange pageRange = new PageRange().setStart(0).setEnd(PageBlobClient.PAGE_BYTES - 1); bc.uploadPages(pageRange, new ByteArrayInputStream(data)); + String sas = bc.generateSas(new BlobServiceSasSignatureValues(testResourceNamer.now().plusDays(1), + new BlobContainerSasPermission().setReadPermission(true))); + Response response = destURL.uploadPagesFromUrlWithResponse(pageRange, bc.getBlobUrl() + "?" + sas, + null, MessageDigest.getInstance("MD5").digest(data), null, null, null, null); + + assertResponseStatusCode(response, 201); + assertTrue(validateBasicHeaders(response.getHeaders())); + assertNotNull(response.getHeaders().getValue(X_MS_CONTENT_CRC64)); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void uploadPageFromURLMD5AndCrc64() throws NoSuchAlgorithmException { + PageBlobClient destURL = cc.getBlobClient(generateBlobName()).getPageBlobClient(); + destURL.create(PageBlobClient.PAGE_BYTES); + byte[] data = getRandomByteArray(PageBlobClient.PAGE_BYTES); + PageRange pageRange = new PageRange().setStart(0).setEnd(PageBlobClient.PAGE_BYTES - 1); + bc.uploadPages(pageRange, new ByteArrayInputStream(data)); + String sas = bc.generateSas(new BlobServiceSasSignatureValues(testResourceNamer.now().plusDays(1), new BlobContainerSasPermission().setReadPermission(true))); Response response = destURL.uploadPagesFromUrlWithResponse(pageRange, bc.getBlobUrl() + "?" + sas, diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java index 4005fa920ba0..27839e45ee25 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/PageBlobAsyncApiTests.java @@ -373,9 +373,24 @@ private static Stream createIfNotExistsTagsSupplier() { Arguments.of(" +-./:=_ +-./:=_", " +-./:=_", null, null)); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void uploadPage() { + StepVerifier + .create(bc.uploadPagesWithResponse(new PageRange().setStart(0).setEnd(PageBlobClient.PAGE_BYTES - 1), + Flux.just(ByteBuffer.wrap(getRandomByteArray(PageBlobClient.PAGE_BYTES))), null, null)) + .assertNext(r -> { + assertResponseStatusCode(r, 201); + assertTrue(validateBasicHeaders(r.getHeaders())); + assertNotNull(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); + assertEquals(0, r.getValue().getBlobSequenceNumber()); + assertTrue(r.getValue().isServerEncrypted()); + }) + .verifyComplete(); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void uploadPageCrc64() { StepVerifier .create(bc.uploadPagesWithResponse(new PageRange().setStart(0).setEnd(PageBlobClient.PAGE_BYTES - 1), Flux.just(ByteBuffer.wrap(getRandomByteArray(PageBlobClient.PAGE_BYTES))), null, null)) @@ -615,7 +630,6 @@ public void uploadPageFromURLIA() { .verifyError(IllegalArgumentException.class); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void uploadPageFromURLMD5() throws NoSuchAlgorithmException { PageBlobAsyncClient destURL = ccAsync.getBlobAsyncClient(generateBlobName()).getPageBlobAsyncClient(); @@ -629,10 +643,26 @@ public void uploadPageFromURLMD5() throws NoSuchAlgorithmException { .then(destURL.uploadPagesFromUrlWithResponse(pageRange, bc.getBlobUrl() + "?" + sas, null, MessageDigest.getInstance("MD5").digest(data), null, null)); + StepVerifier.create(response).expectNextCount(1).verifyComplete(); + } + + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void uploadPageFromURLMD5AndCrc64() throws NoSuchAlgorithmException { + PageBlobAsyncClient destURL = ccAsync.getBlobAsyncClient(generateBlobName()).getPageBlobAsyncClient(); + byte[] data = getRandomByteArray(PageBlobClient.PAGE_BYTES); + PageRange pageRange = new PageRange().setStart(0).setEnd(PageBlobClient.PAGE_BYTES - 1); + String sas = bc.generateSas(new BlobServiceSasSignatureValues(testResourceNamer.now().plusDays(1), + new BlobSasPermission().setTagsPermission(true).setReadPermission(true))); + + Mono> response = destURL.create(PageBlobClient.PAGE_BYTES) + .then(bc.uploadPages(pageRange, Flux.just(ByteBuffer.wrap(data)))) + .then(destURL.uploadPagesFromUrlWithResponse(pageRange, bc.getBlobUrl() + "?" + sas, null, + MessageDigest.getInstance("MD5").digest(data), null, null)); + StepVerifier.create(response).assertNext(r -> { assertResponseStatusCode(r, 201); assertTrue(validateBasicHeaders(r.getHeaders())); - assertNotNull(r.getHeaders().getValue(X_MS_CONTENT_CRC64)); assertArrayEquals(Base64.getDecoder().decode(r.getHeaders().getValue(X_MS_CONTENT_CRC64)), r.getValue().getContentCrc64()); }).verifyComplete(); From 9334a0aa71995ef064868f1c6e993978b72269f7 Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 20 May 2026 13:37:08 -0400 Subject: [PATCH 16/20] add recordings for new tests --- sdk/storage/azure-storage-blob/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index 846f6b9cf17b..5bd912cb95d8 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-blob", - "Tag": "java/storage/azure-storage-blob_08d9fe3e3e" + "Tag": "java/storage/azure-storage-blob_9a674d6008" } From bd80fd7af5426a58f90b36fa427b89432f7ff461 Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 20 May 2026 19:20:46 -0400 Subject: [PATCH 17/20] add separate upload from url tests for block blob api --- .../blob/specialized/BlockBlobApiTests.java | 21 +++++++++++++++ .../specialized/BlockBlobAsyncApiTests.java | 27 +++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java index 4f917bf34046..718c22897a6d 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobApiTests.java @@ -1553,6 +1553,27 @@ public void uploadFromUrlMax() throws NoSuchAlgorithmException { assertEquals(AccessTier.COOL, destinationProperties.getAccessTier()); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void uploadFromUrlMaxReturnsCrc64() throws NoSuchAlgorithmException { + BlobClient sourceBlob + = primaryBlobServiceClient.getBlobContainerClient(containerName).getBlobClient(generateBlobName()); + sourceBlob.upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + byte[] sourceBlobMD5 = MessageDigest.getInstance("MD5").digest(DATA.getDefaultBytes()); + String sas = sourceBlob.generateSas(new BlobServiceSasSignatureValues(testResourceNamer.now().plusDays(1), + new BlobContainerSasPermission().setReadPermission(true))); + + BlobUploadFromUrlOptions options + = new BlobUploadFromUrlOptions(sourceBlob.getBlobUrl() + "?" + sas).setContentMd5(sourceBlobMD5); + Response response = blockBlobClient.uploadFromUrlWithResponse(options, null, null); + String contentCrc64 = response.getHeaders().getValue(X_MS_CONTENT_CRC64); + BlockBlobItem blockBlobItem = response.getValue(); + + assertNotNull(contentCrc64); + assertNotNull(blockBlobItem); + TestUtils.assertArraysEqual(Base64.getDecoder().decode(contentCrc64), blockBlobItem.getContentCrc64()); + } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-04-08") @Test public void uploadFromWithInvalidSourceMD5() throws NoSuchAlgorithmException { diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java index 13795115d2b4..86d1bdd42f87 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java @@ -2504,7 +2504,7 @@ public void uploadFromUrlOverwriteFailsOnExistingBlob() { }); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-04-08") @Test public void uploadFromUrlMax() throws NoSuchAlgorithmException { BlobAsyncClient sourceBlob = primaryBlobServiceAsyncClient.getBlobContainerAsyncClient(containerName) @@ -2536,7 +2536,6 @@ public void uploadFromUrlMax() throws NoSuchAlgorithmException { assertNotNull(blockBlobItem); assertNotNull(blockBlobItem.getETag()); assertNotNull(blockBlobItem.getLastModified()); - assertNotNull(blockBlobItem.getContentCrc64()); }).verifyComplete(); StepVerifier.create(blobAsyncClient.getProperties()).assertNext(r -> { @@ -2550,6 +2549,30 @@ public void uploadFromUrlMax() throws NoSuchAlgorithmException { .verifyComplete(); } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") + @Test + public void uploadFromUrlMaxReturnsCrc64() throws NoSuchAlgorithmException { + BlobAsyncClient sourceBlob = primaryBlobServiceAsyncClient.getBlobContainerAsyncClient(containerName) + .getBlobAsyncClient(generateBlobName()); + byte[] sourceBlobMD5 = MessageDigest.getInstance("MD5").digest(DATA.getDefaultBytes()); + String sas = sourceBlob.generateSas(new BlobServiceSasSignatureValues(testResourceNamer.now().plusDays(1), + new BlobContainerSasPermission().setReadPermission(true))); + + BlobUploadFromUrlOptions options + = new BlobUploadFromUrlOptions(sourceBlob.getBlobUrl() + "?" + sas).setContentMd5(sourceBlobMD5); + Mono> response = sourceBlob.upload(DATA.getDefaultFlux(), null) + .then(blockBlobAsyncClient.uploadFromUrlWithResponse(options)); + + StepVerifier.create(response).assertNext(r -> { + String contentCrc64 = r.getHeaders().getValue(X_MS_CONTENT_CRC64); + BlockBlobItem blockBlobItem = r.getValue(); + + assertNotNull(contentCrc64); + assertNotNull(blockBlobItem); + TestUtils.assertArraysEqual(Base64.getDecoder().decode(contentCrc64), blockBlobItem.getContentCrc64()); + }).verifyComplete(); + } + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-04-08") @Test public void uploadFromWithInvalidSourceMD5() throws NoSuchAlgorithmException { From a97b60b9e4f07d9a3a49ec53bfb894fc947492c0 Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 20 May 2026 19:44:30 -0400 Subject: [PATCH 18/20] add recordings for uploadFromUrlMaxReturnsCrc64 --- sdk/storage/azure-storage-blob/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index 5bd912cb95d8..39fe11f75d08 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-blob", - "Tag": "java/storage/azure-storage-blob_9a674d6008" + "Tag": "java/storage/azure-storage-blob_b9e10070f4" } From de43f6763e07a19e4174cc734efab86ee08e7149 Mon Sep 17 00:00:00 2001 From: browndav Date: Thu, 21 May 2026 14:26:36 -0400 Subject: [PATCH 19/20] add missing recordings --- sdk/storage/azure-storage-blob/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index 39fe11f75d08..1279a1ae962b 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-blob", - "Tag": "java/storage/azure-storage-blob_b9e10070f4" + "Tag": "java/storage/azure-storage-blob_48aa31792b" } From 7d8e539467714e30af97afdd33449a7303a2b5b2 Mon Sep 17 00:00:00 2001 From: browndav Date: Thu, 21 May 2026 22:26:56 -0400 Subject: [PATCH 20/20] remove unneeded service version restrictions in test blockblob api test --- .../azure/storage/blob/specialized/BlockBlobAsyncApiTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java index 86d1bdd42f87..63cf0782dfed 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/BlockBlobAsyncApiTests.java @@ -968,7 +968,6 @@ public void getBlockListError() { StepVerifier.create(blockBlobAsyncClient.listBlocks(BlockListType.ALL)).verifyError(BlobStorageException.class); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") @Test public void upload() { StepVerifier @@ -2504,7 +2503,7 @@ public void uploadFromUrlOverwriteFailsOnExistingBlob() { }); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-04-08") + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-04-08") @Test public void uploadFromUrlMax() throws NoSuchAlgorithmException { BlobAsyncClient sourceBlob = primaryBlobServiceAsyncClient.getBlobContainerAsyncClient(containerName)