diff --git a/sdk/storage/assets.json b/sdk/storage/assets.json index 9c57c8f654..b433440283 100644 --- a/sdk/storage/assets.json +++ b/sdk/storage/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "cpp", "TagPrefix": "cpp/storage", - "Tag": "cpp/storage_d6a62566ee" + "Tag": "cpp/storage_862e6db71b" } diff --git a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_client.hpp b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_client.hpp index 7711b0f9c2..21ce29b1c6 100644 --- a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_client.hpp +++ b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_client.hpp @@ -434,6 +434,10 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::Nullable m_customerProvidedKey; /** @brief Encryption scope. */ Azure::Nullable m_encryptionScope; + /** @brief Upload TransferValidationOptions */ + Azure::Nullable m_uploadValidationOptions; + /** @brief Download TransferValidationOptions */ + Azure::Nullable m_downloadValidationOptions; private: explicit BlobClient( diff --git a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_options.hpp b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_options.hpp index 16383370dc..1bcbb9346e 100644 --- a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_options.hpp +++ b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_options.hpp @@ -85,13 +85,42 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::Nullable TagConditions; }; + /** + * @brief Specifies HTTP options for conditional requests based on AccessTier. + */ + struct AccessTierAccessConditions + { + /** + * @brief Destructor. + * + */ + virtual ~AccessTierAccessConditions() = default; + + /** + * @brief Specify this header value to operate only on a blob if the access-tier has been + * modified since the specified date/time. Note: If this is specified, + * AccessTierIfUnmodifiedSince cannot be specified. + * Only valid for Delete Blob API. + */ + Azure::Nullable AccessTierIfModifiedSince; + + /** + * @brief Specify this header value to operate only on a blob if the access-tier has not been + * modified since the specified date/time. Note: If this is specified, AccessTierIfModifiedSince + * cannot be specified. + * Only valid for Delete Blob API. + */ + Azure::Nullable AccessTierIfUnmodifiedSince; + }; + /** * @brief Specifies access conditions for a blob. */ struct BlobAccessConditions : public Azure::ModifiedConditions, public Azure::MatchConditions, public LeaseAccessConditions, - public TagAccessConditions + public TagAccessConditions, + public AccessTierAccessConditions { }; @@ -168,6 +197,17 @@ namespace Azure { namespace Storage { namespace Blobs { Models::EncryptionAlgorithmType Algorithm; }; + /** + * Configures whether to do content validation for blob uploads and downloads. + */ + struct TransferValidationOptions + { + /** + * @brief The algorithm used for storage checksum. + */ + StorageChecksumAlgorithm Algorithm = StorageChecksumAlgorithm::None; + }; + /** * @brief Client options used to initialize all kinds of blob clients. */ @@ -210,6 +250,16 @@ namespace Azure { namespace Storage { namespace Blobs { * not set. */ Azure::Nullable Audience; + + /** + * @brief Optional. Configures whether to do content validation for blob uploads. + */ + Azure::Nullable UploadValidationOptions; + + /** + * @brief Optional. Configures whether to do content validation for blob downloads. + */ + Azure::Nullable DownloadValidationOptions; }; /** @@ -254,6 +304,11 @@ namespace Azure { namespace Storage { namespace Blobs { * will be truncated to second. */ Azure::DateTime StartsOn = std::chrono::system_clock::now(); + + /** + * The delegated user tenant id in Azure AD. + */ + Nullable DelegatedUserTid; }; /** @@ -684,6 +739,11 @@ namespace Azure { namespace Storage { namespace Blobs { * @brief Optional conditions that must be met to perform this operation. */ BlobAccessConditions AccessConditions; + + /** + * @brief Optional. Configures whether to do content validation for blob downloads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -718,6 +778,11 @@ namespace Azure { namespace Storage { namespace Blobs { */ int32_t Concurrency = 5; } TransferOptions; + + /** + * @brief Optional. Configures whether to do content validation for blob downloads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -906,6 +971,11 @@ namespace Azure { namespace Storage { namespace Blobs { * Indicates whether the blob has a legal hold. */ Azure::Nullable HasLegalHold; + + /** + * @brief Optional. Configures whether to do content validation for blob uploads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -965,6 +1035,11 @@ namespace Azure { namespace Storage { namespace Blobs { * Indicates whether the blob has a legal hold. */ Azure::Nullable HasLegalHold; + + /** + * @brief Optional. Configures whether to do content validation for blob uploads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -1040,6 +1115,12 @@ namespace Azure { namespace Storage { namespace Blobs { * token authentication. Used to indicate the intent of the request. */ Azure::Nullable FileRequestIntent; + + /** + * Optional. Specifies the source customer provided key to use to encrypt the source blob. + * Applicable only for service version 2026-02-06 or later. + */ + Azure::Nullable SourceCustomerProvidedKey; }; /** @@ -1058,6 +1139,11 @@ namespace Azure { namespace Storage { namespace Blobs { * @brief Optional conditions that must be met to perform this operation. */ LeaseAccessConditions AccessConditions; + + /** + * @brief Optional. Configures whether to do content validation for blob uploads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -1102,6 +1188,12 @@ namespace Azure { namespace Storage { namespace Blobs { * token authentication. Used to indicate the intent of the request. */ Azure::Nullable FileRequestIntent; + + /** + * Optional. Specifies the source customer provided key to use to encrypt the source blob. + * Applicable only for service version 2026-02-06 or later. + */ + Azure::Nullable SourceCustomerProvidedKey; }; /** @@ -1364,6 +1456,11 @@ namespace Azure { namespace Storage { namespace Blobs { * @brief Optional conditions that must be met to perform this operation. */ AppendBlobAccessConditions AccessConditions; + + /** + * @brief Optional. Configures whether to do content validation for blob uploads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -1401,6 +1498,12 @@ namespace Azure { namespace Storage { namespace Blobs { * token authentication. Used to indicate the intent of the request. */ Azure::Nullable FileRequestIntent; + + /** + * Optional. Specifies the source customer provided key to use to encrypt the source blob. + * Applicable only for service version 2026-02-06 or later. + */ + Azure::Nullable SourceCustomerProvidedKey; }; /** @@ -1479,6 +1582,11 @@ namespace Azure { namespace Storage { namespace Blobs { * @brief Optional conditions that must be met to perform this operation. */ PageBlobAccessConditions AccessConditions; + + /** + * @brief Optional. Configures whether to do content validation for blob uploads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -1518,6 +1626,12 @@ namespace Azure { namespace Storage { namespace Blobs { * token authentication. Used to indicate the intent of the request. */ Azure::Nullable FileRequestIntent; + + /** + * Optional. Specifies the source customer provided key to use to encrypt the source blob. + * Applicable only for service version 2026-02-06 or later. + */ + Azure::Nullable SourceCustomerProvidedKey; }; /** diff --git a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_sas_builder.hpp b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_sas_builder.hpp index 79ff2210d4..500e9e19ec 100644 --- a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_sas_builder.hpp +++ b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/blob_sas_builder.hpp @@ -262,6 +262,18 @@ namespace Azure { namespace Storage { namespace Sas { */ std::string DelegatedUserObjectId; + /** + * @brief Optional. Custom Request Headers to include in the SAS. Any usage of the SAS must + * include these headers and values in the request. + */ + std::map RequestHeaders; + + /** + * @brief Optional. Custom Request Query Parameters to include in the SAS. Any usage of the SAS + * must include these query parameters and values in the request. + */ + std::map RequestQueryParameters; + /** * @brief Override the value returned for Cache-Control response header.. */ diff --git a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/rest_client.hpp b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/rest_client.hpp index 7544d9424a..720ff1a327 100644 --- a/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/rest_client.hpp +++ b/sdk/storage/azure-storage-blobs/inc/azure/storage/blobs/rest_client.hpp @@ -32,7 +32,7 @@ namespace Azure { namespace Storage { namespace Blobs { /** * The version used for the operations to Azure storage services. */ - constexpr static const char* ApiVersion = "2026-02-06"; + constexpr static const char* ApiVersion = "2026-04-06"; } // namespace _detail namespace Models { /** @@ -502,6 +502,10 @@ namespace Azure { namespace Storage { namespace Blobs { * The date-time the key expires in ISO 8601 UTC time. */ std::string Expiry; + /** + * The delegated user tenant id in Azure AD. + */ + Nullable DelegatedUserTid; }; } // namespace _detail /** @@ -533,6 +537,10 @@ namespace Azure { namespace Storage { namespace Blobs { * The service version that created the key. */ std::string SignedVersion; + /** + * The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + */ + Nullable SignedDelegatedUserTid; /** * The key as a base64 string. */ @@ -1612,6 +1620,16 @@ namespace Azure { namespace Storage { namespace Blobs { * The blob's type. */ Models::BlobType BlobType; + /** + * Indicates the response body contains a structured message and specifies the message schema + * version and properties. + */ + Nullable StructuredBodyType; + /** + * The length of the blob/file content inside the message body when the response body is + * returned as a structured message. Will always be smaller than Content-Length. + */ + Nullable StructuredContentLength; }; /** * @brief Response type for #Azure::Storage::Blobs::BlobClient::GetProperties. @@ -2590,6 +2608,11 @@ namespace Azure { namespace Storage { namespace Blobs { * encryption scope. */ Nullable EncryptionScope; + /** + * Indicates the structured message body was accepted and mirrors back the message schema + * version and properties. + */ + Nullable StructuredBodyType; }; /** * @brief Response type for #Azure::Storage::Blobs::PageBlobClient::ClearPages. @@ -2886,6 +2909,11 @@ namespace Azure { namespace Storage { namespace Blobs { * encryption scope. */ Nullable EncryptionScope; + /** + * Indicates the structured message body was accepted and mirrors back the message schema + * version and properties. + */ + Nullable StructuredBodyType; }; /** * @brief Response type for #Azure::Storage::Blobs::AppendBlobClient::AppendBlockFromUri. @@ -2999,6 +3027,11 @@ namespace Azure { namespace Storage { namespace Blobs { * encryption scope. */ Nullable EncryptionScope; + /** + * Indicates the structured message body was accepted and mirrors back the message schema + * version and properties. + */ + Nullable StructuredBodyType; }; /** * @brief Response type for #Azure::Storage::Blobs::BlockBlobClient::UploadFromUri. @@ -3071,6 +3104,11 @@ namespace Azure { namespace Storage { namespace Blobs { * encryption scope. */ Nullable EncryptionScope; + /** + * Indicates the structured message body was accepted and mirrors back the message schema + * version and properties. + */ + Nullable StructuredBodyType; }; /** * @brief Response type for #Azure::Storage::Blobs::BlockBlobClient::StageBlockFromUri. @@ -3515,6 +3553,7 @@ namespace Azure { namespace Storage { namespace Blobs { Nullable LeaseId; Nullable RangeGetContentMD5; Nullable RangeGetContentCRC64; + Nullable StructuredBodyType; Nullable EncryptionKey; Nullable> EncryptionKeySha256; Nullable EncryptionAlgorithm; @@ -3561,6 +3600,8 @@ namespace Azure { namespace Storage { namespace Blobs { ETag IfMatch; ETag IfNoneMatch; Nullable IfTags; + Nullable AccessTierIfModifiedSince; + Nullable AccessTierIfUnmodifiedSince; }; static Response Delete( Core::Http::_internal::HttpPipeline& pipeline, @@ -3942,6 +3983,8 @@ namespace Azure { namespace Storage { namespace Blobs { ETag IfMatch; ETag IfNoneMatch; Nullable IfTags; + Nullable StructuredBodyType; + Nullable StructuredContentLength; }; static Response UploadPages( Core::Http::_internal::HttpPipeline& pipeline, @@ -3997,6 +4040,9 @@ namespace Azure { namespace Storage { namespace Blobs { ETag SourceIfNoneMatch; Nullable CopySourceAuthorization; Nullable FileRequestIntent; + Nullable SourceEncryptionKey; + Nullable> SourceEncryptionKeySha256; + Nullable SourceEncryptionAlgorithm; }; static Response UploadPagesFromUri( Core::Http::_internal::HttpPipeline& pipeline, @@ -4138,6 +4184,8 @@ namespace Azure { namespace Storage { namespace Blobs { ETag IfMatch; ETag IfNoneMatch; Nullable IfTags; + Nullable StructuredBodyType; + Nullable StructuredContentLength; }; static Response AppendBlock( Core::Http::_internal::HttpPipeline& pipeline, @@ -4170,6 +4218,9 @@ namespace Azure { namespace Storage { namespace Blobs { ETag SourceIfNoneMatch; Nullable CopySourceAuthorization; Nullable FileRequestIntent; + Nullable SourceEncryptionKey; + Nullable> SourceEncryptionKeySha256; + Nullable SourceEncryptionAlgorithm; }; static Response AppendBlockFromUri( Core::Http::_internal::HttpPipeline& pipeline, @@ -4219,6 +4270,8 @@ namespace Azure { namespace Storage { namespace Blobs { Nullable ImmutabilityPolicyMode; Nullable LegalHold; Nullable> TransactionalContentCrc64; + Nullable StructuredBodyType; + Nullable StructuredContentLength; }; static Response Upload( Core::Http::_internal::HttpPipeline& pipeline, @@ -4258,6 +4311,9 @@ namespace Azure { namespace Storage { namespace Blobs { Nullable CopySourceAuthorization; Nullable CopySourceTags; Nullable FileRequestIntent; + Nullable SourceEncryptionKey; + Nullable> SourceEncryptionKeySha256; + Nullable SourceEncryptionAlgorithm; Nullable> SourceContentcrc64; }; static Response UploadFromUri( @@ -4275,6 +4331,8 @@ namespace Azure { namespace Storage { namespace Blobs { Nullable> EncryptionKeySha256; Nullable EncryptionAlgorithm; Nullable EncryptionScope; + Nullable StructuredBodyType; + Nullable StructuredContentLength; }; static Response StageBlock( Core::Http::_internal::HttpPipeline& pipeline, @@ -4300,6 +4358,9 @@ namespace Azure { namespace Storage { namespace Blobs { ETag SourceIfNoneMatch; Nullable CopySourceAuthorization; Nullable FileRequestIntent; + Nullable SourceEncryptionKey; + Nullable> SourceEncryptionKeySha256; + Nullable SourceEncryptionAlgorithm; }; static Response StageBlockFromUri( Core::Http::_internal::HttpPipeline& pipeline, diff --git a/sdk/storage/azure-storage-blobs/src/append_blob_client.cpp b/sdk/storage/azure-storage-blobs/src/append_blob_client.cpp index 33146b2e16..bbe4c77567 100644 --- a/sdk/storage/azure-storage-blobs/src/append_blob_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/append_blob_client.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -141,19 +142,6 @@ namespace Azure { namespace Storage { namespace Blobs { const Azure::Core::Context& context) const { _detail::AppendBlobClient::AppendAppendBlobBlockOptions protocolLayerOptions; - if (options.TransactionalContentHash.HasValue()) - { - if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Md5) - { - protocolLayerOptions.TransactionalContentMD5 - = options.TransactionalContentHash.Value().Value; - } - else if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Crc64) - { - protocolLayerOptions.TransactionalContentCrc64 - = options.TransactionalContentHash.Value().Value; - } - } protocolLayerOptions.LeaseId = options.AccessConditions.LeaseId; protocolLayerOptions.MaxSize = options.AccessConditions.IfMaxSizeLessThanOrEqual; protocolLayerOptions.AppendPosition = options.AccessConditions.IfAppendPositionEqual; @@ -169,6 +157,37 @@ namespace Azure { namespace Storage { namespace Blobs { protocolLayerOptions.EncryptionAlgorithm = m_customerProvidedKey.Value().Algorithm.ToString(); } protocolLayerOptions.EncryptionScope = m_encryptionScope; + if (options.TransactionalContentHash.HasValue()) + { + if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Md5) + { + protocolLayerOptions.TransactionalContentMD5 + = options.TransactionalContentHash.Value().Value; + } + else if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Crc64) + { + protocolLayerOptions.TransactionalContentCrc64 + = options.TransactionalContentHash.Value().Value; + } + } + else + { + Azure::Nullable validationOptions + = options.ValidationOptions.HasValue() ? options.ValidationOptions + : m_uploadValidationOptions; + if (validationOptions.HasValue() + && validationOptions.Value().Algorithm != StorageChecksumAlgorithm::None) + { + protocolLayerOptions.StructuredBodyType = _internal::CrcStructuredMessage; + protocolLayerOptions.StructuredContentLength = content.Length(); + _internal::StructuredMessageEncodingStreamOptions encodingStreamOptions; + encodingStreamOptions.Flags = _internal::StructuredMessageFlags::Crc64; + auto structuredContent + = _internal::StructuredMessageEncodingStream(&content, encodingStreamOptions); + return _detail::AppendBlobClient::AppendBlock( + *m_pipeline, m_blobUrl, structuredContent, protocolLayerOptions, context); + } + } return _detail::AppendBlobClient::AppendBlock( *m_pipeline, m_blobUrl, content, protocolLayerOptions, context); } @@ -221,6 +240,14 @@ namespace Azure { namespace Storage { namespace Blobs { protocolLayerOptions.CopySourceAuthorization = options.SourceAuthorization; } protocolLayerOptions.FileRequestIntent = options.FileRequestIntent; + if (options.SourceCustomerProvidedKey.HasValue()) + { + protocolLayerOptions.SourceEncryptionKey = options.SourceCustomerProvidedKey.Value().Key; + protocolLayerOptions.SourceEncryptionKeySha256 + = options.SourceCustomerProvidedKey.Value().KeyHash; + protocolLayerOptions.SourceEncryptionAlgorithm + = options.SourceCustomerProvidedKey.Value().Algorithm.ToString(); + } return _detail::AppendBlobClient::AppendBlockFromUri( *m_pipeline, m_blobUrl, protocolLayerOptions, context); diff --git a/sdk/storage/azure-storage-blobs/src/blob_client.cpp b/sdk/storage/azure-storage-blobs/src/blob_client.cpp index 0ceaa32f9b..aabea0a3e5 100644 --- a/sdk/storage/azure-storage-blobs/src/blob_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/blob_client.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -110,7 +111,9 @@ namespace Azure { namespace Storage { namespace Blobs { BlobClient::BlobClient(const std::string& blobUrl, const BlobClientOptions& options) : m_blobUrl(blobUrl), m_customerProvidedKey(options.CustomerProvidedKey), - m_encryptionScope(options.EncryptionScope) + m_encryptionScope(options.EncryptionScope), + m_uploadValidationOptions(options.UploadValidationOptions), + m_downloadValidationOptions(options.DownloadValidationOptions) { std::vector> perRetryPolicies; std::vector> perOperationPolicies; @@ -167,6 +170,7 @@ namespace Azure { namespace Storage { namespace Blobs { const DownloadBlobOptions& options, const Azure::Core::Context& context) const { + bool isStructuredMessage = false; _detail::BlobClient::DownloadBlobOptions protocolLayerOptions; if (options.Range.HasValue()) { @@ -189,6 +193,18 @@ namespace Azure { namespace Storage { namespace Blobs { protocolLayerOptions.RangeGetContentCRC64 = true; } } + else + { + Azure::Nullable validationOptions + = options.ValidationOptions.HasValue() ? options.ValidationOptions + : m_downloadValidationOptions; + if (validationOptions.HasValue() + && validationOptions.Value().Algorithm != StorageChecksumAlgorithm::None) + { + isStructuredMessage = true; + protocolLayerOptions.StructuredBodyType = _internal::CrcStructuredMessage; + } + } protocolLayerOptions.LeaseId = options.AccessConditions.LeaseId; protocolLayerOptions.IfModifiedSince = options.AccessConditions.IfModifiedSince; protocolLayerOptions.IfUnmodifiedSince = options.AccessConditions.IfUnmodifiedSince; @@ -243,13 +259,40 @@ namespace Azure { namespace Storage { namespace Blobs { _internal::ReliableStreamOptions reliableStreamOptions; reliableStreamOptions.MaxRetryRequests = _internal::ReliableStreamRetryCount; - downloadResponse.Value.BodyStream = std::make_unique<_internal::ReliableStream>( + auto reliableStream = std::make_unique<_internal::ReliableStream>( std::move(downloadResponse.Value.BodyStream), reliableStreamOptions, retryFunction); + if (isStructuredMessage) + { + _internal::StructuredMessageDecodingStreamOptions decodingOptions; + if (downloadResponse.Value.StructuredContentLength.HasValue()) + { + decodingOptions.ContentLength = downloadResponse.Value.StructuredContentLength.Value(); + } + downloadResponse.Value.BodyStream + = std::make_unique<_internal::StructuredMessageDecodingStream>( + std::move(reliableStream), decodingOptions); + } + else + { + downloadResponse.Value.BodyStream = std::move(reliableStream); + } } if (downloadResponse.RawResponse->GetStatusCode() == Azure::Core::Http::HttpStatusCode::Ok) { - downloadResponse.Value.BlobSize = std::stoll( - downloadResponse.RawResponse->GetHeaders().at(_internal::HttpHeaderContentLength)); + if (isStructuredMessage) + { + if (!downloadResponse.Value.StructuredContentLength.HasValue()) + { + throw StorageException( + "Structured message response without x-ms-structured-content-length header."); + } + downloadResponse.Value.BlobSize = downloadResponse.Value.StructuredContentLength.Value(); + } + else + { + downloadResponse.Value.BlobSize = std::stoll( + downloadResponse.RawResponse->GetHeaders().at(_internal::HttpHeaderContentLength)); + } downloadResponse.Value.ContentRange.Offset = 0; downloadResponse.Value.ContentRange.Length = downloadResponse.Value.BlobSize; } @@ -335,6 +378,7 @@ namespace Azure { namespace Storage { namespace Blobs { { firstChunkOptions.Range.Value().Length = firstChunkLength; } + firstChunkOptions.ValidationOptions = options.ValidationOptions; auto firstChunk = Download(firstChunkOptions, context); const Azure::ETag eTag = firstChunk.Value.Details.ETag; @@ -390,6 +434,7 @@ namespace Azure { namespace Storage { namespace Blobs { chunkOptions.Range.Value().Offset = offset; chunkOptions.Range.Value().Length = length; chunkOptions.AccessConditions.IfMatch = eTag; + chunkOptions.ValidationOptions = options.ValidationOptions; auto chunk = Download(chunkOptions, context); int64_t bytesRead = chunk.Value.BodyStream->ReadToCount( buffer + (offset - firstChunkOffset), @@ -442,6 +487,7 @@ namespace Azure { namespace Storage { namespace Blobs { { firstChunkOptions.Range.Value().Length = firstChunkLength; } + firstChunkOptions.ValidationOptions = options.ValidationOptions; auto firstChunk = Download(firstChunkOptions, context); const Azure::ETag eTag = firstChunk.Value.Details.ETag; @@ -507,6 +553,7 @@ namespace Azure { namespace Storage { namespace Blobs { chunkOptions.Range.Value().Offset = offset; chunkOptions.Range.Value().Length = length; chunkOptions.AccessConditions.IfMatch = eTag; + chunkOptions.ValidationOptions = options.ValidationOptions; auto chunk = Download(chunkOptions, context); bodyStreamToFile( *(chunk.Value.BodyStream), @@ -807,6 +854,10 @@ namespace Azure { namespace Storage { namespace Blobs { protocolLayerOptions.IfMatch = options.AccessConditions.IfMatch; protocolLayerOptions.IfNoneMatch = options.AccessConditions.IfNoneMatch; protocolLayerOptions.IfTags = options.AccessConditions.TagConditions; + protocolLayerOptions.AccessTierIfModifiedSince + = options.AccessConditions.AccessTierIfModifiedSince; + protocolLayerOptions.AccessTierIfUnmodifiedSince + = options.AccessConditions.AccessTierIfUnmodifiedSince; return _detail::BlobClient::Delete(*m_pipeline, m_blobUrl, protocolLayerOptions, context); } diff --git a/sdk/storage/azure-storage-blobs/src/blob_sas_builder.cpp b/sdk/storage/azure-storage-blobs/src/blob_sas_builder.cpp index 8fd3c4871e..b3c2ade702 100644 --- a/sdk/storage/azure-storage-blobs/src/blob_sas_builder.cpp +++ b/sdk/storage/azure-storage-blobs/src/blob_sas_builder.cpp @@ -6,7 +6,9 @@ #include #include -/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, sduoid */ +#include + +/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, skdutid, sduoid, srh, srq */ namespace Azure { namespace Storage { namespace Sas { @@ -36,6 +38,53 @@ namespace Azure { namespace Storage { namespace Sas { throw std::invalid_argument("Unknown BlobSasResource value."); } } + + std::string ParseRequestQueryParameters( + const std::map& queryParameters) + { + if (queryParameters.empty()) + { + return ""; + } + std::string result; + for (const auto& pair : queryParameters) + { + result += "\n" + pair.first + ":" + pair.second; + } + return result; + } + + std::string ParseRequestHeaders(const std::map& headers) + { + if (headers.empty()) + { + return ""; + } + std::string result; + for (const auto& pair : headers) + { + result += pair.first + ":" + pair.second + "\n"; + } + return result; + } + + std::string ParseRequestKeys(const std::map& map) + { + if (map.empty()) + { + return ""; + } + std::string result; + for (auto it = map.begin(); it != map.end(); ++it) + { + result += it->first; + if (std::next(it) != map.end()) + { + result += ","; + } + } + return result; + } } // namespace void BlobSasBuilder::SetPermissions(BlobContainerSasPermissions permissions) @@ -261,10 +310,15 @@ namespace Azure { namespace Storage { namespace Sas { + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion - + "\n\n\n\n\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") - + "\n" + protocol + "\n" + SasVersion + "\n" + resource + "\n" + snapshotVersion + "\n" - + EncryptionScope + "\n" + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding - + "\n" + ContentLanguage + "\n" + ContentType; + + "\n\n\n\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + protocol + "\n" + SasVersion + "\n" + resource + "\n" + snapshotVersion + "\n" + + EncryptionScope + "\n" + ParseRequestHeaders(RequestHeaders) + "\n" + + ParseRequestQueryParameters(RequestQueryParameters) + "\n" + CacheControl + "\n" + + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; std::string signature = Azure::Core::Convert::Base64Encode(_internal::HmacSha256( std::vector(stringToSign.begin(), stringToSign.end()), @@ -294,11 +348,27 @@ namespace Azure { namespace Storage { namespace Sas { "sks", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedService)); builder.AppendQueryParameter( "skv", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedVersion)); + if (userDelegationKey.SignedDelegatedUserTid.HasValue()) + { + builder.AppendQueryParameter( + "skdutid", + _internal::UrlEncodeQueryParameter(userDelegationKey.SignedDelegatedUserTid.Value())); + } if (!DelegatedUserObjectId.empty()) { builder.AppendQueryParameter( "sduoid", _internal::UrlEncodeQueryParameter(DelegatedUserObjectId)); } + if (!RequestHeaders.empty()) + { + builder.AppendQueryParameter( + "srh", _internal::UrlEncodeQueryParameter(ParseRequestKeys(RequestHeaders))); + } + if (!RequestQueryParameters.empty()) + { + builder.AppendQueryParameter( + "srq", _internal::UrlEncodeQueryParameter(ParseRequestKeys(RequestQueryParameters))); + } if (!CacheControl.empty()) { builder.AppendQueryParameter("rscc", _internal::UrlEncodeQueryParameter(CacheControl)); @@ -402,9 +472,14 @@ namespace Azure { namespace Storage { namespace Sas { return Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService - + "\n" + userDelegationKey.SignedVersion + "\n\n\n\n\n" + DelegatedUserObjectId + "\n" - + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" - + resource + "\n" + snapshotVersion + "\n" + EncryptionScope + "\n" + CacheControl + "\n" + + "\n" + userDelegationKey.SignedVersion + "\n\n\n\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + protocol + "\n" + SasVersion + "\n" + resource + "\n" + snapshotVersion + "\n" + + EncryptionScope + "\n" + ParseRequestHeaders(RequestHeaders) + "\n" + + ParseRequestQueryParameters(RequestQueryParameters) + "\n" + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; } diff --git a/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp b/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp index df2fa6c105..fed8532203 100644 --- a/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/blob_service_client.cpp @@ -184,6 +184,7 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); protocolLayerOptions.KeyInfo.Expiry = expiresOn.ToString( Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); + protocolLayerOptions.KeyInfo.DelegatedUserTid = options.DelegatedUserTid; return _detail::ServiceClient::GetUserDelegationKey( *m_pipeline, m_serviceUrl, protocolLayerOptions, _internal::WithReplicaStatus(context)); } diff --git a/sdk/storage/azure-storage-blobs/src/block_blob_client.cpp b/sdk/storage/azure-storage-blobs/src/block_blob_client.cpp index f2ad3e3dad..ea7a362792 100644 --- a/sdk/storage/azure-storage-blobs/src/block_blob_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/block_blob_client.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -98,19 +99,7 @@ namespace Azure { namespace Storage { namespace Blobs { const Azure::Core::Context& context) const { _detail::BlockBlobClient::UploadBlockBlobOptions protocolLayerOptions; - if (options.TransactionalContentHash.HasValue()) - { - if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Md5) - { - protocolLayerOptions.TransactionalContentMD5 - = options.TransactionalContentHash.Value().Value; - } - else if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Crc64) - { - protocolLayerOptions.TransactionalContentCrc64 - = options.TransactionalContentHash.Value().Value; - } - } + protocolLayerOptions.BlobContentType = options.HttpHeaders.ContentType; protocolLayerOptions.BlobContentEncoding = options.HttpHeaders.ContentEncoding; protocolLayerOptions.BlobContentLanguage = options.HttpHeaders.ContentLanguage; @@ -141,6 +130,38 @@ namespace Azure { namespace Storage { namespace Blobs { } protocolLayerOptions.LegalHold = options.HasLegalHold; + if (options.TransactionalContentHash.HasValue()) + { + if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Md5) + { + protocolLayerOptions.TransactionalContentMD5 + = options.TransactionalContentHash.Value().Value; + } + else if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Crc64) + { + protocolLayerOptions.TransactionalContentCrc64 + = options.TransactionalContentHash.Value().Value; + } + } + else + { + Azure::Nullable validationOptions + = options.ValidationOptions.HasValue() ? options.ValidationOptions + : m_uploadValidationOptions; + if (validationOptions.HasValue() + && validationOptions.Value().Algorithm != StorageChecksumAlgorithm::None) + { + protocolLayerOptions.StructuredBodyType = _internal::CrcStructuredMessage; + protocolLayerOptions.StructuredContentLength = content.Length(); + _internal::StructuredMessageEncodingStreamOptions encodingStreamOptions; + encodingStreamOptions.Flags = _internal::StructuredMessageFlags::Crc64; + auto structuredContent + = _internal::StructuredMessageEncodingStream(&content, encodingStreamOptions); + return _detail::BlockBlobClient::Upload( + *m_pipeline, m_blobUrl, structuredContent, protocolLayerOptions, context); + } + } + return _detail::BlockBlobClient::Upload( *m_pipeline, m_blobUrl, content, protocolLayerOptions, context); } @@ -171,6 +192,7 @@ namespace Azure { namespace Storage { namespace Blobs { uploadBlockBlobOptions.AccessTier = options.AccessTier; uploadBlockBlobOptions.ImmutabilityPolicy = options.ImmutabilityPolicy; uploadBlockBlobOptions.HasLegalHold = options.HasLegalHold; + uploadBlockBlobOptions.ValidationOptions = options.ValidationOptions; return Upload(contentStream, uploadBlockBlobOptions, context); } @@ -202,6 +224,7 @@ namespace Azure { namespace Storage { namespace Blobs { auto uploadBlockFunc = [&](int64_t offset, int64_t length, int64_t chunkId, int64_t numChunks) { Azure::Core::IO::MemoryBodyStream contentStream(buffer + offset, static_cast(length)); StageBlockOptions chunkOptions; + chunkOptions.ValidationOptions = options.ValidationOptions; auto blockInfo = StageBlock(getBlockId(chunkId), contentStream, chunkOptions, context); if (chunkId == numChunks - 1) { @@ -277,6 +300,7 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::Core::IO::_internal::RandomAccessFileBodyStream contentStream( fileReader.GetHandle(), offset, length); StageBlockOptions chunkOptions; + chunkOptions.ValidationOptions = options.ValidationOptions; auto blockInfo = StageBlock(getBlockId(chunkId), contentStream, chunkOptions, context); if (chunkId == numChunks - 1) { @@ -384,6 +408,14 @@ namespace Azure { namespace Storage { namespace Blobs { protocolLayerOptions.CopySourceAuthorization = options.SourceAuthorization; } protocolLayerOptions.FileRequestIntent = options.FileRequestIntent; + if (options.SourceCustomerProvidedKey.HasValue()) + { + protocolLayerOptions.SourceEncryptionKey = options.SourceCustomerProvidedKey.Value().Key; + protocolLayerOptions.SourceEncryptionKeySha256 + = options.SourceCustomerProvidedKey.Value().KeyHash; + protocolLayerOptions.SourceEncryptionAlgorithm + = options.SourceCustomerProvidedKey.Value().Algorithm.ToString(); + } return _detail::BlockBlobClient::UploadFromUri( *m_pipeline, m_blobUrl, protocolLayerOptions, context); @@ -397,6 +429,15 @@ namespace Azure { namespace Storage { namespace Blobs { { _detail::BlockBlobClient::StageBlockBlobBlockOptions protocolLayerOptions; protocolLayerOptions.BlockId = blockId; + protocolLayerOptions.LeaseId = options.AccessConditions.LeaseId; + if (m_customerProvidedKey.HasValue()) + { + protocolLayerOptions.EncryptionKey = m_customerProvidedKey.Value().Key; + protocolLayerOptions.EncryptionKeySha256 = m_customerProvidedKey.Value().KeyHash; + protocolLayerOptions.EncryptionAlgorithm = m_customerProvidedKey.Value().Algorithm.ToString(); + } + protocolLayerOptions.EncryptionScope = m_encryptionScope; + if (options.TransactionalContentHash.HasValue()) { if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Md5) @@ -410,14 +451,24 @@ namespace Azure { namespace Storage { namespace Blobs { = options.TransactionalContentHash.Value().Value; } } - protocolLayerOptions.LeaseId = options.AccessConditions.LeaseId; - if (m_customerProvidedKey.HasValue()) + else { - protocolLayerOptions.EncryptionKey = m_customerProvidedKey.Value().Key; - protocolLayerOptions.EncryptionKeySha256 = m_customerProvidedKey.Value().KeyHash; - protocolLayerOptions.EncryptionAlgorithm = m_customerProvidedKey.Value().Algorithm.ToString(); + Azure::Nullable validationOptions + = options.ValidationOptions.HasValue() ? options.ValidationOptions + : m_uploadValidationOptions; + if (validationOptions.HasValue() + && validationOptions.Value().Algorithm != StorageChecksumAlgorithm::None) + { + protocolLayerOptions.StructuredBodyType = _internal::CrcStructuredMessage; + protocolLayerOptions.StructuredContentLength = content.Length(); + _internal::StructuredMessageEncodingStreamOptions encodingStreamOptions; + encodingStreamOptions.Flags = _internal::StructuredMessageFlags::Crc64; + auto structuredContent + = _internal::StructuredMessageEncodingStream(&content, encodingStreamOptions); + return _detail::BlockBlobClient::StageBlock( + *m_pipeline, m_blobUrl, structuredContent, protocolLayerOptions, context); + } } - protocolLayerOptions.EncryptionScope = m_encryptionScope; return _detail::BlockBlobClient::StageBlock( *m_pipeline, m_blobUrl, content, protocolLayerOptions, context); } @@ -469,6 +520,14 @@ namespace Azure { namespace Storage { namespace Blobs { protocolLayerOptions.CopySourceAuthorization = options.SourceAuthorization; } protocolLayerOptions.FileRequestIntent = options.FileRequestIntent; + if (options.SourceCustomerProvidedKey.HasValue()) + { + protocolLayerOptions.SourceEncryptionKey = options.SourceCustomerProvidedKey.Value().Key; + protocolLayerOptions.SourceEncryptionKeySha256 + = options.SourceCustomerProvidedKey.Value().KeyHash; + protocolLayerOptions.SourceEncryptionAlgorithm + = options.SourceCustomerProvidedKey.Value().Algorithm.ToString(); + } return _detail::BlockBlobClient::StageBlockFromUri( *m_pipeline, m_blobUrl, protocolLayerOptions, context); diff --git a/sdk/storage/azure-storage-blobs/src/page_blob_client.cpp b/sdk/storage/azure-storage-blobs/src/page_blob_client.cpp index 1b437e4e4c..a05757a4eb 100644 --- a/sdk/storage/azure-storage-blobs/src/page_blob_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/page_blob_client.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -152,19 +153,6 @@ namespace Azure { namespace Storage { namespace Blobs { _detail::PageBlobClient::UploadPageBlobPagesOptions protocolLayerOptions; protocolLayerOptions.Range = "bytes=" + std::to_string(offset) + "-" + std::to_string(offset + content.Length() - 1); - if (options.TransactionalContentHash.HasValue()) - { - if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Md5) - { - protocolLayerOptions.TransactionalContentMD5 - = options.TransactionalContentHash.Value().Value; - } - else if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Crc64) - { - protocolLayerOptions.TransactionalContentCrc64 - = options.TransactionalContentHash.Value().Value; - } - } protocolLayerOptions.LeaseId = options.AccessConditions.LeaseId; protocolLayerOptions.IfModifiedSince = options.AccessConditions.IfModifiedSince; protocolLayerOptions.IfUnmodifiedSince = options.AccessConditions.IfUnmodifiedSince; @@ -183,6 +171,38 @@ namespace Azure { namespace Storage { namespace Blobs { protocolLayerOptions.EncryptionAlgorithm = m_customerProvidedKey.Value().Algorithm.ToString(); } protocolLayerOptions.EncryptionScope = m_encryptionScope; + + if (options.TransactionalContentHash.HasValue()) + { + if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Md5) + { + protocolLayerOptions.TransactionalContentMD5 + = options.TransactionalContentHash.Value().Value; + } + else if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Crc64) + { + protocolLayerOptions.TransactionalContentCrc64 + = options.TransactionalContentHash.Value().Value; + } + } + else + { + Azure::Nullable validationOptions + = options.ValidationOptions.HasValue() ? options.ValidationOptions + : m_uploadValidationOptions; + if (validationOptions.HasValue() + && validationOptions.Value().Algorithm != StorageChecksumAlgorithm::None) + { + protocolLayerOptions.StructuredBodyType = _internal::CrcStructuredMessage; + protocolLayerOptions.StructuredContentLength = content.Length(); + _internal::StructuredMessageEncodingStreamOptions encodingStreamOptions; + encodingStreamOptions.Flags = _internal::StructuredMessageFlags::Crc64; + auto structuredContent + = _internal::StructuredMessageEncodingStream(&content, encodingStreamOptions); + return _detail::PageBlobClient::UploadPages( + *m_pipeline, m_blobUrl, structuredContent, protocolLayerOptions, context); + } + } return _detail::PageBlobClient::UploadPages( *m_pipeline, m_blobUrl, content, protocolLayerOptions, context); } @@ -238,6 +258,15 @@ namespace Azure { namespace Storage { namespace Blobs { protocolLayerOptions.CopySourceAuthorization = options.SourceAuthorization; } protocolLayerOptions.FileRequestIntent = options.FileRequestIntent; + if (options.SourceCustomerProvidedKey.HasValue()) + { + protocolLayerOptions.SourceEncryptionKey + = options.SourceCustomerProvidedKey.Value().Key; + protocolLayerOptions.SourceEncryptionKeySha256 + = options.SourceCustomerProvidedKey.Value().KeyHash; + protocolLayerOptions.SourceEncryptionAlgorithm + = options.SourceCustomerProvidedKey.Value().Algorithm.ToString(); + } return _detail::PageBlobClient::UploadPagesFromUri( *m_pipeline, m_blobUrl, protocolLayerOptions, context); diff --git a/sdk/storage/azure-storage-blobs/src/rest_client.cpp b/sdk/storage/azure-storage-blobs/src/rest_client.cpp index eea9abe1c5..bf9006ee75 100644 --- a/sdk/storage/azure-storage-blobs/src/rest_client.cpp +++ b/sdk/storage/azure-storage-blobs/src/rest_client.cpp @@ -378,7 +378,7 @@ namespace Azure { namespace Storage { namespace Blobs { request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -398,7 +398,7 @@ namespace Azure { namespace Storage { namespace Blobs { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -694,7 +694,7 @@ namespace Azure { namespace Storage { namespace Blobs { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "stats"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -794,7 +794,7 @@ namespace Azure { namespace Storage { namespace Blobs { _internal::UrlEncodeQueryParameter( ListBlobContainersIncludeFlagsToString(options.Include.Value()))); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -1068,6 +1068,13 @@ namespace Azure { namespace Storage { namespace Blobs { _internal::XmlNode{_internal::XmlNodeType::StartTag, "Start", options.KeyInfo.Start}); writer.Write( _internal::XmlNode{_internal::XmlNodeType::StartTag, "Expiry", options.KeyInfo.Expiry}); + if (options.KeyInfo.DelegatedUserTid.HasValue()) + { + writer.Write(_internal::XmlNode{ + _internal::XmlNodeType::StartTag, + "DelegatedUserTid", + options.KeyInfo.DelegatedUserTid.Value()}); + } writer.Write(_internal::XmlNode{_internal::XmlNodeType::EndTag}); writer.Write(_internal::XmlNode{_internal::XmlNodeType::End}); xmlBody = writer.GetDocument(); @@ -1079,7 +1086,7 @@ namespace Azure { namespace Storage { namespace Blobs { request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "userdelegationkey"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -1101,6 +1108,7 @@ namespace Azure { namespace Storage { namespace Blobs { kSignedExpiry, kSignedService, kSignedVersion, + kSignedDelegatedUserTid, kValue, }; const std::unordered_map XmlTagEnumMap{ @@ -1111,6 +1119,7 @@ namespace Azure { namespace Storage { namespace Blobs { {"SignedExpiry", XmlTagEnum::kSignedExpiry}, {"SignedService", XmlTagEnum::kSignedService}, {"SignedVersion", XmlTagEnum::kSignedVersion}, + {"SignedDelegatedUserTid", XmlTagEnum::kSignedDelegatedUserTid}, {"Value", XmlTagEnum::kValue}, }; std::vector xmlPath; @@ -1166,6 +1175,12 @@ namespace Azure { namespace Storage { namespace Blobs { { response.SignedVersion = node.Value; } + else if ( + xmlPath.size() == 2 && xmlPath[0] == XmlTagEnum::kUserDelegationKey + && xmlPath[1] == XmlTagEnum::kSignedDelegatedUserTid) + { + response.SignedDelegatedUserTid = node.Value; + } else if ( xmlPath.size() == 2 && xmlPath[0] == XmlTagEnum::kUserDelegationKey && xmlPath[1] == XmlTagEnum::kValue) @@ -1194,7 +1209,7 @@ namespace Azure { namespace Storage { namespace Blobs { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "account"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -1224,7 +1239,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("Content-Type", options.MultipartContentType); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -1245,7 +1260,7 @@ namespace Azure { namespace Storage { namespace Blobs { { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("comp", "blobs"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Where.HasValue() && !options.Where.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -1401,7 +1416,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-blob-public-access", options.Access.ToString()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.DefaultEncryptionScope.HasValue() && !options.DefaultEncryptionScope.Value().empty()) { @@ -1438,7 +1453,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -1516,7 +1531,7 @@ namespace Azure { namespace Storage { namespace Blobs { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -1550,7 +1565,7 @@ namespace Azure { namespace Storage { namespace Blobs { "If-Modified-Since", options.IfModifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -1577,7 +1592,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -1746,7 +1761,7 @@ namespace Azure { namespace Storage { namespace Blobs { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -1769,7 +1784,7 @@ namespace Azure { namespace Storage { namespace Blobs { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("restype", "container"); request.GetUrl().AppendQueryParameter("comp", "undelete"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.DeletedContainerName.HasValue() && !options.DeletedContainerName.Value().empty()) { request.SetHeader("x-ms-deleted-container-name", options.DeletedContainerName.Value()); @@ -1799,7 +1814,7 @@ namespace Azure { namespace Storage { namespace Blobs { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("restype", "container"); request.GetUrl().AppendQueryParameter("comp", "rename"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (!options.SourceContainerName.empty()) { request.SetHeader("x-ms-source-container-name", options.SourceContainerName); @@ -1833,7 +1848,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("Content-Type", options.MultipartContentType); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -1855,7 +1870,7 @@ namespace Azure { namespace Storage { namespace Blobs { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "container"); request.GetUrl().AppendQueryParameter("comp", "blobs"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Where.HasValue() && !options.Where.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -2025,7 +2040,7 @@ namespace Azure { namespace Storage { namespace Blobs { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -2066,7 +2081,7 @@ namespace Azure { namespace Storage { namespace Blobs { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -2106,7 +2121,7 @@ namespace Azure { namespace Storage { namespace Blobs { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -2147,7 +2162,7 @@ namespace Azure { namespace Storage { namespace Blobs { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -2192,7 +2207,7 @@ namespace Azure { namespace Storage { namespace Blobs { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -2244,7 +2259,7 @@ namespace Azure { namespace Storage { namespace Blobs { request.GetUrl().AppendQueryParameter( "startFrom", _internal::UrlEncodeQueryParameter(options.StartFrom.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -2928,7 +2943,7 @@ namespace Azure { namespace Storage { namespace Blobs { request.GetUrl().AppendQueryParameter( "startFrom", _internal::UrlEncodeQueryParameter(options.StartFrom.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.ShowOnly.HasValue() && !options.ShowOnly.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -3617,7 +3632,7 @@ namespace Azure { namespace Storage { namespace Blobs { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "account"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -3669,6 +3684,10 @@ namespace Azure { namespace Storage { namespace Blobs { "x-ms-range-get-content-crc64", options.RangeGetContentCRC64.Value() ? "true" : "false"); } + if (options.StructuredBodyType.HasValue() && !options.StructuredBodyType.Value().empty()) + { + request.SetHeader("x-ms-structured-body", options.StructuredBodyType.Value()); + } if (options.EncryptionKey.HasValue() && !options.EncryptionKey.Value().empty()) { request.SetHeader("x-ms-encryption-key", options.EncryptionKey.Value()); @@ -3708,7 +3727,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.UserPrincipalName.HasValue()) { request.SetHeader("x-ms-upn", options.UserPrincipalName.Value() ? "true" : "false"); @@ -3881,6 +3900,15 @@ namespace Azure { namespace Storage { namespace Blobs { response.Details.HasLegalHold = pRawResponse->GetHeaders().at("x-ms-legal-hold") == std::string("true"); } + if (pRawResponse->GetHeaders().count("x-ms-structured-body") != 0) + { + response.StructuredBodyType = pRawResponse->GetHeaders().at("x-ms-structured-body"); + } + if (pRawResponse->GetHeaders().count("x-ms-structured-content-length") != 0) + { + response.StructuredContentLength + = std::stoll(pRawResponse->GetHeaders().at("x-ms-structured-content-length")); + } if (httpStatusCode == Core::Http::HttpStatusCode::Ok) { if (pRawResponse->GetHeaders().count("Content-MD5") != 0) @@ -3988,7 +4016,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.UserPrincipalName.HasValue()) { request.SetHeader("x-ms-upn", options.UserPrincipalName.Value() ? "true" : "false"); @@ -4254,7 +4282,21 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); + if (options.AccessTierIfModifiedSince.HasValue()) + { + request.SetHeader( + "x-ms-access-tier-if-modified-since", + options.AccessTierIfModifiedSince.Value().ToString( + Azure::DateTime::DateFormat::Rfc1123)); + } + if (options.AccessTierIfUnmodifiedSince.HasValue()) + { + request.SetHeader( + "x-ms-access-tier-if-unmodified-since", + options.AccessTierIfUnmodifiedSince.Value().ToString( + Azure::DateTime::DateFormat::Rfc1123)); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -4272,7 +4314,7 @@ namespace Azure { namespace Storage { namespace Blobs { { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("comp", "undelete"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -4291,7 +4333,7 @@ namespace Azure { namespace Storage { namespace Blobs { { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("comp", "expiry"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (!options.ExpiryOptions.ToString().empty()) { request.SetHeader("x-ms-expiry-option", options.ExpiryOptions.ToString()); @@ -4379,7 +4421,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-blob-content-disposition", options.BlobContentDisposition); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -4412,7 +4454,7 @@ namespace Azure { namespace Storage { namespace Blobs { { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("comp", "immutabilityPolicies"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.IfUnmodifiedSince.HasValue()) { request.SetHeader( @@ -4455,7 +4497,7 @@ namespace Azure { namespace Storage { namespace Blobs { { auto request = Core::Http::Request(Core::Http::HttpMethod::Delete, url); request.GetUrl().AppendQueryParameter("comp", "immutabilityPolicies"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -4475,7 +4517,7 @@ namespace Azure { namespace Storage { namespace Blobs { { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("comp", "legalhold"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); request.SetHeader("x-ms-legal-hold", options.LegalHold ? "true" : "false"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -4547,7 +4589,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -4622,7 +4664,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -4680,7 +4722,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -4737,7 +4779,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -4799,7 +4841,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -4857,7 +4899,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -4937,7 +4979,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -5044,7 +5086,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.BlobTagsString.HasValue() && !options.BlobTagsString.Value().empty()) { request.SetHeader("x-ms-tags", options.BlobTagsString.Value()); @@ -5163,7 +5205,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.SourceContentMD5.HasValue() && !Core::Convert::Base64Encode(options.SourceContentMD5.Value()).empty()) { @@ -5278,7 +5320,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::NoContent) @@ -5316,7 +5358,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-rehydrate-priority", options.RehydratePriority.Value().ToString()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -5345,7 +5387,7 @@ namespace Azure { namespace Storage { namespace Blobs { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "account"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -5643,7 +5685,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.EncryptionScope.HasValue() && !options.EncryptionScope.Value().empty()) { request.SetHeader("x-ms-encryption-scope", options.EncryptionScope.Value()); @@ -5692,7 +5734,7 @@ namespace Azure { namespace Storage { namespace Blobs { { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("comp", "tags"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Snapshot.HasValue() && !options.Snapshot.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -5839,7 +5881,7 @@ namespace Azure { namespace Storage { namespace Blobs { request.SetHeader("Content-Type", "application/xml; charset=UTF-8"); request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("comp", "tags"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.VersionId.HasValue() && !options.VersionId.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -5990,7 +6032,7 @@ namespace Azure { namespace Storage { namespace Blobs { request.SetHeader( "x-ms-blob-sequence-number", std::to_string(options.BlobSequenceNumber.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.BlobTagsString.HasValue() && !options.BlobTagsString.Value().empty()) { request.SetHeader("x-ms-tags", options.BlobTagsString.Value()); @@ -6136,7 +6178,17 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); + if (options.StructuredBodyType.HasValue() && !options.StructuredBodyType.Value().empty()) + { + request.SetHeader("x-ms-structured-body", options.StructuredBodyType.Value()); + } + if (options.StructuredContentLength.HasValue()) + { + request.SetHeader( + "x-ms-structured-content-length", + std::to_string(options.StructuredContentLength.Value())); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -6183,6 +6235,10 @@ namespace Azure { namespace Storage { namespace Blobs { { response.EncryptionScope = pRawResponse->GetHeaders().at("x-ms-encryption-scope"); } + if (pRawResponse->GetHeaders().count("x-ms-structured-body") != 0) + { + response.StructuredBodyType = pRawResponse->GetHeaders().at("x-ms-structured-body"); + } return Response(std::move(response), std::move(pRawResponse)); } Response PageBlobClient::ClearPages( @@ -6262,7 +6318,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -6405,7 +6461,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-source-if-none-match", options.SourceIfNoneMatch.ToString()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.CopySourceAuthorization.HasValue() && !options.CopySourceAuthorization.Value().empty()) { @@ -6417,6 +6473,23 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-file-request-intent", options.FileRequestIntent.Value().ToString()); } + if (options.SourceEncryptionKey.HasValue() && !options.SourceEncryptionKey.Value().empty()) + { + request.SetHeader("x-ms-source-encryption-key", options.SourceEncryptionKey.Value()); + } + if (options.SourceEncryptionKeySha256.HasValue() + && !Core::Convert::Base64Encode(options.SourceEncryptionKeySha256.Value()).empty()) + { + request.SetHeader( + "x-ms-source-encryption-key-sha256", + Core::Convert::Base64Encode(options.SourceEncryptionKeySha256.Value())); + } + if (options.SourceEncryptionAlgorithm.HasValue() + && !options.SourceEncryptionAlgorithm.Value().empty()) + { + request.SetHeader( + "x-ms-source-encryption-algorithm", options.SourceEncryptionAlgorithm.Value()); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -6511,7 +6584,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Marker.HasValue() && !options.Marker.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -6689,7 +6762,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Marker.HasValue() && !options.Marker.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -6869,7 +6942,7 @@ namespace Azure { namespace Storage { namespace Blobs { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } request.SetHeader("x-ms-blob-content-length", std::to_string(options.BlobContentLength)); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -6935,7 +7008,7 @@ namespace Azure { namespace Storage { namespace Blobs { request.SetHeader( "x-ms-blob-sequence-number", std::to_string(options.BlobSequenceNumber.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -6993,7 +7066,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-copy-source", options.CopySource); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -7104,7 +7177,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.BlobTagsString.HasValue() && !options.BlobTagsString.Value().empty()) { request.SetHeader("x-ms-tags", options.BlobTagsString.Value()); @@ -7232,7 +7305,17 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); + if (options.StructuredBodyType.HasValue() && !options.StructuredBodyType.Value().empty()) + { + request.SetHeader("x-ms-structured-body", options.StructuredBodyType.Value()); + } + if (options.StructuredContentLength.HasValue()) + { + request.SetHeader( + "x-ms-structured-content-length", + std::to_string(options.StructuredContentLength.Value())); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -7271,6 +7354,10 @@ namespace Azure { namespace Storage { namespace Blobs { { response.EncryptionScope = pRawResponse->GetHeaders().at("x-ms-encryption-scope"); } + if (pRawResponse->GetHeaders().count("x-ms-structured-body") != 0) + { + response.StructuredBodyType = pRawResponse->GetHeaders().at("x-ms-structured-body"); + } return Response(std::move(response), std::move(pRawResponse)); } Response AppendBlobClient::AppendBlockFromUri( @@ -7386,7 +7473,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-source-if-none-match", options.SourceIfNoneMatch.ToString()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.CopySourceAuthorization.HasValue() && !options.CopySourceAuthorization.Value().empty()) { @@ -7398,6 +7485,23 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-file-request-intent", options.FileRequestIntent.Value().ToString()); } + if (options.SourceEncryptionKey.HasValue() && !options.SourceEncryptionKey.Value().empty()) + { + request.SetHeader("x-ms-source-encryption-key", options.SourceEncryptionKey.Value()); + } + if (options.SourceEncryptionKeySha256.HasValue() + && !Core::Convert::Base64Encode(options.SourceEncryptionKeySha256.Value()).empty()) + { + request.SetHeader( + "x-ms-source-encryption-key-sha256", + Core::Convert::Base64Encode(options.SourceEncryptionKeySha256.Value())); + } + if (options.SourceEncryptionAlgorithm.HasValue() + && !options.SourceEncryptionAlgorithm.Value().empty()) + { + request.SetHeader( + "x-ms-source-encryption-algorithm", options.SourceEncryptionAlgorithm.Value()); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -7447,7 +7551,7 @@ namespace Azure { namespace Storage { namespace Blobs { { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("comp", "seal"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -7586,7 +7690,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.BlobTagsString.HasValue() && !options.BlobTagsString.Value().empty()) { request.SetHeader("x-ms-tags", options.BlobTagsString.Value()); @@ -7615,6 +7719,16 @@ namespace Azure { namespace Storage { namespace Blobs { "x-ms-content-crc64", Core::Convert::Base64Encode(options.TransactionalContentCrc64.Value())); } + if (options.StructuredBodyType.HasValue() && !options.StructuredBodyType.Value().empty()) + { + request.SetHeader("x-ms-structured-body", options.StructuredBodyType.Value()); + } + if (options.StructuredContentLength.HasValue()) + { + request.SetHeader( + "x-ms-structured-content-length", + std::to_string(options.StructuredContentLength.Value())); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -7647,6 +7761,10 @@ namespace Azure { namespace Storage { namespace Blobs { { response.EncryptionScope = pRawResponse->GetHeaders().at("x-ms-encryption-scope"); } + if (pRawResponse->GetHeaders().count("x-ms-structured-body") != 0) + { + response.StructuredBodyType = pRawResponse->GetHeaders().at("x-ms-structured-body"); + } if (pRawResponse->GetHeaders().count("x-ms-content-crc64") != 0) { response.TransactionalContentHash = ContentHash(); @@ -7769,7 +7887,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-source-if-tags", options.SourceIfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.SourceContentMD5.HasValue() && !Core::Convert::Base64Encode(options.SourceContentMD5.Value()).empty()) { @@ -7806,6 +7924,23 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-file-request-intent", options.FileRequestIntent.Value().ToString()); } + if (options.SourceEncryptionKey.HasValue() && !options.SourceEncryptionKey.Value().empty()) + { + request.SetHeader("x-ms-source-encryption-key", options.SourceEncryptionKey.Value()); + } + if (options.SourceEncryptionKeySha256.HasValue() + && !Core::Convert::Base64Encode(options.SourceEncryptionKeySha256.Value()).empty()) + { + request.SetHeader( + "x-ms-source-encryption-key-sha256", + Core::Convert::Base64Encode(options.SourceEncryptionKeySha256.Value())); + } + if (options.SourceEncryptionAlgorithm.HasValue() + && !options.SourceEncryptionAlgorithm.Value().empty()) + { + request.SetHeader( + "x-ms-source-encryption-algorithm", options.SourceEncryptionAlgorithm.Value()); + } if (options.SourceContentcrc64.HasValue() && !Core::Convert::Base64Encode(options.SourceContentcrc64.Value()).empty()) { @@ -7906,7 +8041,17 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-encryption-scope", options.EncryptionScope.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); + if (options.StructuredBodyType.HasValue() && !options.StructuredBodyType.Value().empty()) + { + request.SetHeader("x-ms-structured-body", options.StructuredBodyType.Value()); + } + if (options.StructuredContentLength.HasValue()) + { + request.SetHeader( + "x-ms-structured-content-length", + std::to_string(options.StructuredContentLength.Value())); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -7939,6 +8084,10 @@ namespace Azure { namespace Storage { namespace Blobs { { response.EncryptionScope = pRawResponse->GetHeaders().at("x-ms-encryption-scope"); } + if (pRawResponse->GetHeaders().count("x-ms-structured-body") != 0) + { + response.StructuredBodyType = pRawResponse->GetHeaders().at("x-ms-structured-body"); + } return Response(std::move(response), std::move(pRawResponse)); } Response BlockBlobClient::StageBlockFromUri( @@ -8020,7 +8169,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-source-if-none-match", options.SourceIfNoneMatch.ToString()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.CopySourceAuthorization.HasValue() && !options.CopySourceAuthorization.Value().empty()) { @@ -8032,6 +8181,23 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-file-request-intent", options.FileRequestIntent.Value().ToString()); } + if (options.SourceEncryptionKey.HasValue() && !options.SourceEncryptionKey.Value().empty()) + { + request.SetHeader("x-ms-source-encryption-key", options.SourceEncryptionKey.Value()); + } + if (options.SourceEncryptionKeySha256.HasValue() + && !Core::Convert::Base64Encode(options.SourceEncryptionKeySha256.Value()).empty()) + { + request.SetHeader( + "x-ms-source-encryption-key-sha256", + Core::Convert::Base64Encode(options.SourceEncryptionKeySha256.Value())); + } + if (options.SourceEncryptionAlgorithm.HasValue() + && !options.SourceEncryptionAlgorithm.Value().empty()) + { + request.SetHeader( + "x-ms-source-encryption-algorithm", options.SourceEncryptionAlgorithm.Value()); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -8192,7 +8358,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.BlobTagsString.HasValue() && !options.BlobTagsString.Value().empty()) { request.SetHeader("x-ms-tags", options.BlobTagsString.Value()); @@ -8281,7 +8447,7 @@ namespace Azure { namespace Storage { namespace Blobs { { request.SetHeader("x-ms-if-tags", options.IfTags.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) diff --git a/sdk/storage/azure-storage-blobs/swagger/README.md b/sdk/storage/azure-storage-blobs/swagger/README.md index 8cf52cf8b9..dfac310b3b 100644 --- a/sdk/storage/azure-storage-blobs/swagger/README.md +++ b/sdk/storage/azure-storage-blobs/swagger/README.md @@ -9,7 +9,7 @@ package-name: azure-storage-blobs namespace: Azure::Storage::Blobs output-folder: generated clear-output-folder: true -input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-02-06/blob.json +input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/refs/heads/feature/storage/stg101base/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-04-06/blob.json ``` ## ModelFour Options @@ -52,13 +52,12 @@ directive: - from: swagger-document where: $["x-ms-paths"].*.*.parameters transform: > - $ = $.filter(p => !(p["$ref"] && (p["$ref"].endsWith("#/parameters/Timeout") || p["$ref"].endsWith("#/parameters/ClientRequestId") - || p["$ref"].endsWith("#/parameters/StructuredBodyGet") || p["$ref"].endsWith("#/parameters/StructuredBodyPut") || p["$ref"].endsWith("#/parameters/StructuredContentLength")))); + $ = $.filter(p => !(p["$ref"] && (p["$ref"].endsWith("#/parameters/Timeout") || p["$ref"].endsWith("#/parameters/ClientRequestId")))); - from: swagger-document where: $["x-ms-paths"].*.*.responses.*.headers transform: > for (const h in $) { - if (["x-ms-client-request-id", "x-ms-request-id", "x-ms-version", "Date", "x-ms-structured-body", "x-ms-structured-content-length"].includes(h)) { + if (["x-ms-client-request-id", "x-ms-request-id", "x-ms-version", "Date"].includes(h)) { delete $[h]; } } @@ -101,12 +100,12 @@ directive: "name": "ApiVersion", "modelAsString": false }, - "enum": ["2026-02-06"] + "enum": ["2026-04-06"] }; - from: swagger-document where: $.parameters transform: > - $.ApiVersionParameter.enum[0] = "2026-02-06"; + $.ApiVersionParameter.enum[0] = "2026-04-06"; ``` ### Rename Operations @@ -291,6 +290,8 @@ directive: $.SequenceNumberAction["x-ms-enum"]["name"] = "SequenceNumberAction"; delete $.EncryptionAlgorithm["enum"]; delete $.EncryptionAlgorithm["x-ms-enum"]; + delete $.SourceEncryptionAlgorithm["enum"]; + delete $.SourceEncryptionAlgorithm["x-ms-enum"]; $.ImmutabilityPolicyMode.enum = $.ImmutabilityPolicyMode.enum.map(e => e.toLowerCase()); $.CopySourceTags["x-ms-enum"]["name"] = "BlobCopySourceTagsMode"; delete $.FilterBlobsInclude; @@ -334,6 +335,16 @@ directive: }, "x-ms-export": true }; + $.SourceEncryptionAlgorithm = { + "type": "string", + "enum": ["AES256"], + "x-ms-enum": { + "name": "EncryptionAlgorithmType", + "modelAsString": false, + "values": [{"value": "__placeHolder", "name": "__placeHolder"}, {"value": "AES256", "name": "Aes256"}] + }, + "x-ms-export": true + }; $.BlockType = { "type": "string", "enum": ["Committed", "Uncommitted", "Latest"], @@ -384,11 +395,15 @@ directive: if (h === "x-ms-meta") { $[h]["x-ms-format"] = "caseinsensitivemap"; } + if (h === "x-ms-structured-body" || h === "x-ms-structured-content-length") { + $[h]["x-nullable"] = true; + } } - from: swagger-document where: $.parameters transform: > $.EncryptionKeySha256["format"] = "byte"; + $.SourceEncryptionKeySha256["format"] = "byte"; $.BlobContentType["required"] = true; $.BlobContentEncoding["required"] = true; $.BlobContentLanguage["required"] = true; diff --git a/sdk/storage/azure-storage-blobs/test/ut/append_blob_client_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/append_blob_client_test.cpp index bfcb02d12b..48c3300eeb 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/append_blob_client_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/append_blob_client_test.cpp @@ -503,4 +503,48 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_NO_THROW(shareClient.DeleteIfExists()); } + TEST_F(AppendBlobClientTest, StructuredMessageTest) + { + const size_t contentSize = 2 * 1024 + 512; + auto content = RandomBuffer(contentSize); + auto bodyStream = Azure::Core::IO::MemoryBodyStream(content.data(), content.size()); + Blobs::TransferValidationOptions validationOptions; + validationOptions.Algorithm = StorageChecksumAlgorithm::Crc64; + + auto appendBlob = GetAppendBlobClientForTest(LowercaseRandomString()); + appendBlob.Create(); + + // Append + Blobs::AppendBlockOptions appendOptions; + appendOptions.ValidationOptions = validationOptions; + Blobs::Models::AppendBlockResult appendResult; + EXPECT_NO_THROW(appendResult = appendBlob.AppendBlock(bodyStream, appendOptions).Value); + EXPECT_TRUE(appendResult.StructuredBodyType.HasValue()); + EXPECT_EQ(appendResult.StructuredBodyType.Value(), _internal::CrcStructuredMessage); + validationOptions.Algorithm = StorageChecksumAlgorithm::None; + appendOptions.ValidationOptions = validationOptions; + bodyStream.Rewind(); + EXPECT_NO_THROW(appendResult = appendBlob.AppendBlock(bodyStream, appendOptions).Value); + EXPECT_FALSE(appendResult.StructuredBodyType.HasValue()); + validationOptions.Algorithm = StorageChecksumAlgorithm::Auto; + appendOptions.ValidationOptions = validationOptions; + bodyStream.Rewind(); + EXPECT_NO_THROW(appendResult = appendBlob.AppendBlock(bodyStream, appendOptions).Value); + EXPECT_TRUE(appendResult.StructuredBodyType.HasValue()); + EXPECT_EQ(appendResult.StructuredBodyType.Value(), _internal::CrcStructuredMessage); + + // Download + Blobs::DownloadBlobOptions downloadOptions; + downloadOptions.ValidationOptions = validationOptions; + Blobs::Models::DownloadBlobResult downloadResult; + EXPECT_NO_THROW(downloadResult = appendBlob.Download(downloadOptions).Value); + auto downloadedData = downloadResult.BodyStream->ReadToEnd(); + EXPECT_EQ( + content, + std::vector(downloadedData.begin(), downloadedData.begin() + contentSize)); + EXPECT_TRUE(downloadResult.StructuredContentLength.HasValue()); + EXPECT_EQ(downloadResult.StructuredContentLength.Value(), contentSize * 3); + EXPECT_TRUE(downloadResult.StructuredBodyType.HasValue()); + } + }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-blobs/test/ut/blob_sas_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/blob_sas_test.cpp index e497636ed4..2cb771a7ab 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/blob_sas_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/blob_sas_test.cpp @@ -880,7 +880,7 @@ namespace Azure { namespace Storage { namespace Test { return {}; } - TEST_F(BlobSasTest, DISABLED_PrincipalBoundDelegationSas) + TEST_F(BlobSasTest, PrincipalBoundDelegationSas_LIVEONLY_) { auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); @@ -920,7 +920,7 @@ namespace Azure { namespace Storage { namespace Test { AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), GetTestCredential(), InitStorageClientOptions()); - EXPECT_NO_THROW(blobClient1.GetProperties()); + EXPECT_NO_THROW(blobClient1.Download()); blobSasBuilder.DelegatedUserObjectId = "invalidObjectId"; sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); @@ -928,6 +928,146 @@ namespace Azure { namespace Storage { namespace Test { AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), GetTestCredential(), InitStorageClientOptions()); - EXPECT_THROW(blobClient2.GetProperties(), StorageException); + EXPECT_THROW(blobClient2.Download(), StorageException); } + + TEST_F(BlobSasTest, DISABLED_PrincipalBoundDelegationSas_CrossTenant) + { + auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); + auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); + + auto keyCredential + = _internal::ParseConnectionString(StandardStorageConnectionString()).KeyCredential; + auto accountName = keyCredential->AccountName; + Azure::Identity::ClientSecretCredentialOptions credentialOptions; + credentialOptions.AdditionallyAllowedTenants = {"*"}; + auto endUserCredential = std::make_shared( + GetEnv("AZURE_TENANT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_SECRET_CROSS_TENANT")); + auto delegatedUserObjectId = getObjectIdFromTokenCredential(endUserCredential); + + auto blobServiceClient = Blobs::BlobServiceClient( + m_blobServiceClient->GetUrl(), + GetTestCredential(), + InitStorageClientOptions()); + Blobs::Models::UserDelegationKey userDelegationKey; + { + Blobs::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "4ab3a968-f1ae-47a6-b82c-f654612122a9"; + userDelegationKey = blobServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + + auto blobContainerClient = *m_blobContainerClient; + auto blobClient = *m_blockBlobClient; + const std::string blobName = m_blobName; + + Sas::BlobSasBuilder blobSasBuilder; + blobSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + blobSasBuilder.StartsOn = sasStartsOn; + blobSasBuilder.ExpiresOn = sasExpiresOn; + blobSasBuilder.BlobContainerName = m_containerName; + blobSasBuilder.BlobName = blobName; + blobSasBuilder.Resource = Sas::BlobSasResource::Blob; + blobSasBuilder.DelegatedUserObjectId = delegatedUserObjectId; + + blobSasBuilder.SetPermissions(Sas::BlobSasPermissions::All); + auto sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Blobs::BlockBlobClient blobClient1( + AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_NO_THROW(blobClient1.Download()); + + { + Blobs::GetUserDelegationKeyOptions options; + // Invalid Tenant Id + options.DelegatedUserTid = "00000000-0000-0000-0000-000000000000"; + userDelegationKey = blobServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Blobs::BlockBlobClient blobClient2( + AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), + GetTestCredential(), + InitStorageClientOptions()); + EXPECT_THROW(blobClient2.Download(), StorageException); + } + + TEST_F(BlobSasTest, DISABLED_DynamicSas) + { + auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); + auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); + + auto keyCredential + = _internal::ParseConnectionString(StandardStorageConnectionString()).KeyCredential; + auto accountName = keyCredential->AccountName; + + Blobs::Models::UserDelegationKey userDelegationKey; + { + auto blobServiceClient = Blobs::BlobServiceClient( + m_blobServiceClient->GetUrl(), + GetTestCredential(), + InitStorageClientOptions()); + userDelegationKey = blobServiceClient.GetUserDelegationKey(sasExpiresOn).Value; + } + + auto blobContainerClient = *m_blobContainerClient; + auto blobClient = *m_blockBlobClient; + const std::string blobName = m_blobName; + + Sas::BlobSasBuilder blobSasBuilder; + blobSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + blobSasBuilder.StartsOn = sasStartsOn; + blobSasBuilder.ExpiresOn = sasExpiresOn; + blobSasBuilder.BlobContainerName = m_containerName; + blobSasBuilder.BlobName = blobName; + blobSasBuilder.Resource = Sas::BlobSasResource::Blob; + + blobSasBuilder.SetPermissions(Sas::BlobSasPermissions::All); + + // cSpell:disable + std::map requestHeaders; + requestHeaders["x-ms-range"] = "bytes=0-1023"; + requestHeaders["x-ms-range-get-content-md5"] = "true"; + + std::map requestQueryParameters; + requestQueryParameters["spr"] = "https,http"; + requestQueryParameters["sks"] = "b"; + + blobSasBuilder.RequestHeaders = requestHeaders; + blobSasBuilder.RequestQueryParameters = requestQueryParameters; + auto sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Blobs::DownloadBlobOptions downloadOptions; + Core::Http::HttpRange range; + range.Offset = 0; + range.Length = 1024; + downloadOptions.Range = range; + downloadOptions.RangeHashAlgorithm = HashAlgorithm::Md5; + + Blobs::BlockBlobClient blobClient1( + AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), + InitStorageClientOptions()); + EXPECT_NO_THROW(blobClient1.Download(downloadOptions)); + + requestHeaders["foo$"] = "bar!"; + requestHeaders["company"] = "msft"; + requestHeaders["city"] = "redmond,atlanta,reston"; + + requestQueryParameters["hello$"] = "world!"; + requestQueryParameters["abra"] = "cadabra"; + requestQueryParameters["firstName"] = "john,Tim"; + // cSpell:enable + + blobSasBuilder.RequestHeaders = requestHeaders; + blobSasBuilder.RequestQueryParameters = requestQueryParameters; + + sasToken = blobSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Blobs::BlockBlobClient blobClient2( + AppendQueryParameters(Azure::Core::Url(blobClient.GetUrl()), sasToken), + InitStorageClientOptions()); + EXPECT_THROW(blobClient2.Download(downloadOptions), StorageException); + } + }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-blobs/test/ut/blob_service_client_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/blob_service_client_test.cpp index 6bdd9f20c2..54d3f1c154 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/blob_service_client_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/blob_service_client_test.cpp @@ -321,7 +321,7 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_EQ( downloadedProperties.DefaultServiceVersion.HasValue(), properties.DefaultServiceVersion.HasValue()); - if (downloadedProperties.DefaultServiceVersion.HasValue()) + if (downloadedProperties.DefaultServiceVersion.HasValue() && !m_testContext.IsPlaybackMode()) { EXPECT_EQ( downloadedProperties.DefaultServiceVersion.Value(), diff --git a/sdk/storage/azure-storage-blobs/test/ut/block_blob_client_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/block_blob_client_test.cpp index 6bc79a6d5c..bac2ac4744 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/block_blob_client_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/block_blob_client_test.cpp @@ -1469,6 +1469,21 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_NO_THROW( destBlobClient.StageBlockFromUri("YWJjZA==", srcBlobClient.GetUrl() + GetSas(), options)); } + { + auto destBlobClient = GetBlockBlobClientForTest(RandomString() + "dest5"); + Blobs::TransferValidationOptions validationOptions; + validationOptions.Algorithm = StorageChecksumAlgorithm::Crc64; + Blobs::UploadBlockBlobOptions options; + options.ValidationOptions + = validationOptions; // ValidationOptions should not work when ContentHash is set + options.TransactionalContentHash = ContentHash(); + options.TransactionalContentHash.Value().Algorithm = HashAlgorithm::Md5; + options.TransactionalContentHash.Value().Value = contentMd5; + stream.Rewind(); + Blobs::Models::UploadBlockBlobResult uploadResult; + EXPECT_NO_THROW(uploadResult = destBlobClient.Upload(stream, options).Value); + EXPECT_FALSE(uploadResult.StructuredBodyType.HasValue()); + } } TEST_F(BlockBlobClientTest, UploadFromUri) @@ -2349,4 +2364,116 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_THROW(blobClient.GetTags(getOptions), StorageException); } } + + TEST_F(BlockBlobClientTest, StructuredMessageTest) + { + const size_t contentSize = 2 * 1024 + 512; + auto content = RandomBuffer(contentSize); + auto bodyStream = Azure::Core::IO::MemoryBodyStream(content.data(), content.size()); + const std::string tempFileName = RandomString(); + WriteFile(tempFileName, content); + Blobs::TransferValidationOptions validationOptions; + validationOptions.Algorithm = StorageChecksumAlgorithm::Crc64; + + // Upload Download + Blobs::UploadBlockBlobOptions uploadOptions; + uploadOptions.ValidationOptions = validationOptions; + Blobs::Models::UploadBlockBlobResult uploadResult; + EXPECT_NO_THROW(uploadResult = m_blockBlobClient->Upload(bodyStream, uploadOptions).Value); + EXPECT_TRUE(uploadResult.StructuredBodyType.HasValue()); + EXPECT_EQ(uploadResult.StructuredBodyType.Value(), _internal::CrcStructuredMessage); + Blobs::DownloadBlobOptions downloadOptions; + downloadOptions.ValidationOptions = validationOptions; + Blobs::Models::DownloadBlobResult downloadResult; + EXPECT_NO_THROW(downloadResult = m_blockBlobClient->Download(downloadOptions).Value); + auto downloadedData = downloadResult.BodyStream->ReadToEnd(); + EXPECT_EQ(content, downloadedData); + EXPECT_TRUE(downloadResult.StructuredContentLength.HasValue()); + EXPECT_EQ(downloadResult.StructuredContentLength.Value(), contentSize); + EXPECT_TRUE(downloadResult.StructuredBodyType.HasValue()); + EXPECT_EQ(downloadResult.BlobSize, contentSize); + // partial download + downloadOptions.Range = Core::Http::HttpRange(); + downloadOptions.Range.Value().Length = contentSize / 2; + EXPECT_NO_THROW(downloadResult = m_blockBlobClient->Download(downloadOptions).Value); + downloadedData = downloadResult.BodyStream->ReadToEnd(); + EXPECT_EQ( + downloadedData, std::vector(content.begin(), content.begin() + contentSize / 2)); + EXPECT_TRUE(downloadResult.StructuredContentLength.HasValue()); + EXPECT_EQ(downloadResult.StructuredContentLength.Value(), contentSize / 2); + EXPECT_TRUE(downloadResult.StructuredBodyType.HasValue()); + EXPECT_EQ(downloadResult.BlobSize, contentSize); + downloadOptions.Range.Reset(); + + // UploadFrom DownloadTo + Blobs::UploadBlockBlobFromOptions uploadFromOptions; + Blobs::Models::UploadBlockBlobFromResult uploadFromResult; + Blobs::DownloadBlobToOptions downloadToOptions; + Blobs::Models::DownloadBlobToResult downloadToResult; + + // Stream + uploadFromOptions.ValidationOptions = validationOptions; + auto blobClient + = m_blobContainerClient->GetBlockBlobClient("uploadfromstream_" + LowercaseRandomString()); + EXPECT_NO_THROW( + uploadFromResult + = blobClient.UploadFrom(content.data(), contentSize, uploadFromOptions).Value); + downloadToOptions.ValidationOptions = validationOptions; + auto downloadBuffer = std::vector(contentSize, '\x00'); + EXPECT_NO_THROW( + downloadToResult + = blobClient.DownloadTo(downloadBuffer.data(), contentSize, downloadToOptions).Value); + EXPECT_EQ(downloadBuffer, content); + // partial downloadTo + downloadToOptions.Range = Core::Http::HttpRange(); + downloadToOptions.Range.Value().Length = contentSize / 2; + downloadBuffer.resize(static_cast(contentSize / 2), '\x00'); + EXPECT_NO_THROW( + downloadToResult + = blobClient.DownloadTo(downloadBuffer.data(), contentSize / 2, downloadToOptions).Value); + EXPECT_EQ( + downloadBuffer, std::vector(content.begin(), content.begin() + contentSize / 2)); + downloadToOptions.Range.Reset(); + + // File + blobClient + = m_blobContainerClient->GetBlockBlobClient("uploadfromfile_" + LowercaseRandomString()); + EXPECT_NO_THROW(blobClient.UploadFrom(tempFileName, uploadFromOptions)); + std::string downloadToFileName = RandomString(); + EXPECT_NO_THROW( + downloadToResult = blobClient.DownloadTo(downloadToFileName, downloadToOptions).Value); + EXPECT_EQ(ReadFile(downloadToFileName), content); + + // Stage Block + blobClient = m_blobContainerClient->GetBlockBlobClient(LowercaseRandomString()); + const std::vector dataPart1 = RandomBuffer(contentSize); + const std::vector dataPart2 = RandomBuffer(contentSize); + + const std::string blockId1 = Base64EncodeText("0"); + const std::string blockId2 = Base64EncodeText("1"); + + Blobs::StageBlockOptions stageBlockOptions; + stageBlockOptions.ValidationOptions = validationOptions; + auto blockContent = Azure::Core::IO::MemoryBodyStream(dataPart1.data(), dataPart1.size()); + Blobs::Models::StageBlockResult stageResult; + EXPECT_NO_THROW( + stageResult = blobClient.StageBlock(blockId1, blockContent, stageBlockOptions).Value); + EXPECT_TRUE(stageResult.StructuredBodyType.HasValue()); + EXPECT_EQ(stageResult.StructuredBodyType.Value(), _internal::CrcStructuredMessage); + validationOptions.Algorithm = StorageChecksumAlgorithm::None; + stageBlockOptions.ValidationOptions = validationOptions; + blockContent = Azure::Core::IO::MemoryBodyStream(dataPart2.data(), dataPart2.size()); + EXPECT_NO_THROW( + stageResult = blobClient.StageBlock(blockId2, blockContent, stageBlockOptions).Value); + EXPECT_FALSE(stageResult.StructuredBodyType.HasValue()); + EXPECT_NO_THROW(blobClient.CommitBlockList({blockId1, blockId2})); + downloadOptions.ValidationOptions = validationOptions; + EXPECT_NO_THROW(downloadResult = blobClient.Download(downloadOptions).Value); + downloadedData = downloadResult.BodyStream->ReadToEnd(); + EXPECT_EQ( + dataPart1, + std::vector(downloadedData.begin(), downloadedData.begin() + contentSize)); + EXPECT_FALSE(downloadResult.StructuredContentLength.HasValue()); + EXPECT_FALSE(downloadResult.StructuredBodyType.HasValue()); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-blobs/test/ut/page_blob_client_test.cpp b/sdk/storage/azure-storage-blobs/test/ut/page_blob_client_test.cpp index 5c2435d917..2e65ecfc78 100644 --- a/sdk/storage/azure-storage-blobs/test/ut/page_blob_client_test.cpp +++ b/sdk/storage/azure-storage-blobs/test/ut/page_blob_client_test.cpp @@ -818,4 +818,37 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_NO_THROW(blockBlobClient.GetProperties()); } + TEST_F(PageBlobClientTest, StructuredMessageTest) + { + const size_t contentSize = 2 * 1024 + 512; + auto content = RandomBuffer(contentSize); + auto bodyStream = Azure::Core::IO::MemoryBodyStream(content.data(), content.size()); + Blobs::TransferValidationOptions validationOptions; + validationOptions.Algorithm = StorageChecksumAlgorithm::Crc64; + + auto pageBlob = GetPageBlobClientTestForTest(LowercaseRandomString()); + pageBlob.Create(contentSize); + + // Append + Blobs::UploadPagesOptions uploadOptions; + uploadOptions.ValidationOptions = validationOptions; + Blobs::Models::UploadPagesResult uploadResult; + EXPECT_NO_THROW(uploadResult = pageBlob.UploadPages(0, bodyStream, uploadOptions).Value); + EXPECT_TRUE(uploadResult.StructuredBodyType.HasValue()); + EXPECT_EQ(uploadResult.StructuredBodyType.Value(), _internal::CrcStructuredMessage); + + // Download + Blobs::DownloadBlobOptions downloadOptions; + downloadOptions.ValidationOptions = validationOptions; + Blobs::Models::DownloadBlobResult downloadResult; + EXPECT_NO_THROW(downloadResult = pageBlob.Download(downloadOptions).Value); + auto downloadedData = downloadResult.BodyStream->ReadToEnd(); + EXPECT_EQ( + content, + std::vector(downloadedData.begin(), downloadedData.begin() + contentSize)); + EXPECT_TRUE(downloadResult.StructuredContentLength.HasValue()); + EXPECT_EQ(downloadResult.StructuredContentLength.Value(), contentSize); + EXPECT_TRUE(downloadResult.StructuredBodyType.HasValue()); + } + }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-common/CMakeLists.txt b/sdk/storage/azure-storage-common/CMakeLists.txt index 76afb17a3c..a91e1baaf7 100644 --- a/sdk/storage/azure-storage-common/CMakeLists.txt +++ b/sdk/storage/azure-storage-common/CMakeLists.txt @@ -57,6 +57,9 @@ set( inc/azure/storage/common/internal/storage_per_retry_policy.hpp inc/azure/storage/common/internal/storage_service_version_policy.hpp inc/azure/storage/common/internal/storage_switch_to_secondary_policy.hpp + inc/azure/storage/common/internal/structured_message_decoding_stream.hpp + inc/azure/storage/common/internal/structured_message_encoding_stream.hpp + inc/azure/storage/common/internal/structured_message_helper.hpp inc/azure/storage/common/internal/xml_wrapper.hpp inc/azure/storage/common/rtti.hpp inc/azure/storage/common/storage_common.hpp @@ -77,6 +80,9 @@ set( src/storage_exception.cpp src/storage_per_retry_policy.cpp src/storage_switch_to_secondary_policy.cpp + src/structured_message_decoding_stream.cpp + src/structured_message_encoding_stream.cpp + src/structured_message_helper.cpp src/xml_wrapper.cpp ) diff --git a/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/constants.hpp b/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/constants.hpp index 7eb09da54c..798cf2b296 100644 --- a/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/constants.hpp +++ b/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/constants.hpp @@ -26,6 +26,7 @@ namespace Azure { namespace Storage { namespace _internal { = "The provided service version is not enabled on this storage account. Please see " "https://learn.microsoft.com/rest/api/storageservices/" "versioning-for-the-azure-storage-services for additional information."; + constexpr static const char* CrcStructuredMessage = "XSM/1.0; properties=crc64"; constexpr int ReliableStreamRetryCount = 3; }}} // namespace Azure::Storage::_internal diff --git a/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/structured_message_decoding_stream.hpp b/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/structured_message_decoding_stream.hpp new file mode 100644 index 0000000000..9633a7c79a --- /dev/null +++ b/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/structured_message_decoding_stream.hpp @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "azure/storage/common/crypt.hpp" +#include "azure/storage/common/dll_import_export.hpp" +#include "constants.hpp" +#include "structured_message_helper.hpp" + +#include +#include + +#include +#include +#include + +namespace Azure { namespace Storage { namespace _internal { + + // Options used by structured message encode stream + struct StructuredMessageDecodingStreamOptions final + { + /** + * Required. The length of the real data in the structured message. + */ + int64_t ContentLength = 0; + }; + + /** + * @brief TODO + * + */ + class StructuredMessageDecodingStream final : public Azure::Core::IO::BodyStream { + private: + // initial bodyStream. + std::unique_ptr m_inner; + // Configuration for the encode stream + StructuredMessageDecodingStreamOptions const m_options; + + size_t m_streamHeaderLength; + size_t m_segmentHeaderLength; + size_t m_segmentFooterLength; + size_t m_streamFooterLength; + + // Length of the stream + uint64_t m_length; + StructuredMessageFlags m_flags; + uint16_t m_segmentCount; + + int64_t m_offset; + + StructuredMessageCurrentRegion m_currentRegion; + uint16_t m_currentSegmentNumber; + uint64_t m_currentSegmentOffset; + uint64_t m_currentSegmentLength; + + std::vector m_segmentHeaderBuffer; + std::vector m_segmentFooterBuffer; + + std::unique_ptr m_segmentCrc64Hash; + std::unique_ptr m_streamCrc64Hash; + + size_t OnRead(uint8_t* buffer, size_t count, Azure::Core::Context const& context) override; + + public: + explicit StructuredMessageDecodingStream( + std::unique_ptr inner, + StructuredMessageDecodingStreamOptions const options) + : m_inner(std::move(inner)), m_options(options), + m_streamHeaderLength(StructuredMessageHelper::StreamHeaderLength), + m_segmentHeaderLength(StructuredMessageHelper::SegmentHeaderLength), + m_segmentFooterLength(0), m_streamFooterLength(0), m_length(0), + m_flags(StructuredMessageFlags::None), m_segmentCount(0), m_offset(0), + m_currentRegion(StructuredMessageCurrentRegion::StreamHeader), m_currentSegmentNumber(0), + m_currentSegmentOffset(0), m_currentSegmentLength(0), + m_segmentHeaderBuffer(StructuredMessageHelper::SegmentHeaderLength), + m_segmentFooterBuffer(0), m_segmentCrc64Hash(std::make_unique()), + m_streamCrc64Hash(std::make_unique()) + { + } + + int64_t Length() const override { return m_options.ContentLength; } + + void Rewind() override + { + // Rewind directly from a transportAdapter body stream (like libcurl) would throw + this->m_inner->Rewind(); + this->m_segmentFooterLength = 0; + this->m_streamFooterLength = 0; + this->m_length = 0; + this->m_segmentCount = 0; + this->m_flags = StructuredMessageFlags::None; + this->m_offset = 0; + this->m_currentRegion = StructuredMessageCurrentRegion::StreamHeader; + this->m_currentSegmentNumber = 0; + this->m_currentSegmentOffset = 0; + this->m_currentSegmentLength = 0; + this->m_segmentHeaderBuffer.clear(); + this->m_segmentFooterBuffer.clear(); + this->m_segmentCrc64Hash = std::make_unique(); + this->m_streamCrc64Hash = std::make_unique(); + } + }; + +}}} // namespace Azure::Storage::_internal diff --git a/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/structured_message_encoding_stream.hpp b/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/structured_message_encoding_stream.hpp new file mode 100644 index 0000000000..d75785fcac --- /dev/null +++ b/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/structured_message_encoding_stream.hpp @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "azure/storage/common/crypt.hpp" +#include "azure/storage/common/dll_import_export.hpp" +#include "constants.hpp" +#include "structured_message_helper.hpp" + +#include +#include + +#include +#include +#include + +namespace Azure { namespace Storage { namespace _internal { + + // Options used by structured message encode stream + struct StructuredMessageEncodingStreamOptions final + { + // configures the maximun segment length + int64_t MaxSegmentLength = 4 * 1024 * 1024; + + StructuredMessageFlags Flags = StructuredMessageFlags::None; + }; + + /** + * @brief TODO + * + */ + class StructuredMessageEncodingStream final : public Azure::Core::IO::BodyStream { + private: + // initial bodyStream. + Azure::Core::IO::BodyStream* m_inner; + // Configuration for the encode stream + StructuredMessageEncodingStreamOptions const m_options; + + size_t m_streamHeaderLength; + size_t m_segmentHeaderLength; + size_t m_segmentFooterLength; + size_t m_streamFooterLength; + + uint16_t m_segmentCount; + uint16_t m_segmentNumber; + + int64_t m_offset; + int64_t m_innerOffset; + + StructuredMessageCurrentRegion m_currentRegion; + uint64_t m_currentRegionOffset; + + std::vector m_streamHeaderCache; + std::vector m_segmentHeaderCache; + std::vector m_segmentFooterCache; + std::vector m_streamFooterCache; + + std::unique_ptr m_segmentCrc64Hash; + std::unique_ptr m_streamCrc64Hash; + + size_t OnRead(uint8_t* buffer, size_t count, Azure::Core::Context const& context) override; + + public: + explicit StructuredMessageEncodingStream( + Azure::Core::IO::BodyStream* inner, + StructuredMessageEncodingStreamOptions const options) + : m_inner(inner), m_options(options), + m_streamHeaderLength(StructuredMessageHelper::StreamHeaderLength), + m_segmentHeaderLength(StructuredMessageHelper::SegmentHeaderLength), m_segmentCount(0), + m_segmentNumber(0), m_offset(0), m_innerOffset(0), + m_currentRegion(StructuredMessageCurrentRegion::StreamHeader), m_currentRegionOffset(0), + m_streamHeaderCache(0), m_segmentHeaderCache(0), m_segmentFooterCache(0), + m_streamFooterCache(0), m_segmentCrc64Hash(std::make_unique()), + m_streamCrc64Hash(std::make_unique()) + { + m_segmentFooterLength = m_options.Flags == StructuredMessageFlags::Crc64 + ? StructuredMessageHelper::Crc64Length + : 0; + m_streamFooterLength = m_options.Flags == StructuredMessageFlags::Crc64 + ? StructuredMessageHelper::Crc64Length + : 0; + m_segmentCount = static_cast( + (m_inner->Length() + m_options.MaxSegmentLength - 1) / m_options.MaxSegmentLength); + } + + int64_t Length() const override + { + return m_streamHeaderLength + m_streamFooterLength + + (m_segmentHeaderLength + m_segmentFooterLength) * m_segmentCount + + this->m_inner->Length(); + } + + void Rewind() override + { + // Rewind directly from a transportAdapter body stream (like libcurl) would throw + this->m_inner->Rewind(); + this->m_segmentNumber = 0; + this->m_offset = 0; + this->m_innerOffset = 0; + this->m_currentRegion = StructuredMessageCurrentRegion::StreamHeader; + this->m_currentRegionOffset = 0; + this->m_streamHeaderCache.clear(); + this->m_segmentHeaderCache.clear(); + this->m_segmentFooterCache.clear(); + this->m_streamFooterCache.clear(); + this->m_segmentCrc64Hash = std::make_unique(); + this->m_streamCrc64Hash = std::make_unique(); + } + }; + +}}} // namespace Azure::Storage::_internal diff --git a/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/structured_message_helper.hpp b/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/structured_message_helper.hpp new file mode 100644 index 0000000000..9343ad597f --- /dev/null +++ b/sdk/storage/azure-storage-common/inc/azure/storage/common/internal/structured_message_helper.hpp @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "azure/storage/common/dll_import_export.hpp" + +#include +#include +#include + +namespace Azure { namespace Storage { namespace _internal { + + enum StructuredMessageFlags : uint16_t + { + None = 0x0, + Crc64 = 0x1, + }; + + enum StructuredMessageCurrentRegion + { + StreamHeader, + StreamFooter, + SegmentHeader, + SegmentFooter, + SegmentContent, + }; + + class StructuredMessageHelper final { + public: + AZ_STORAGE_COMMON_DLLEXPORT static const size_t Crc64Length; + + AZ_STORAGE_COMMON_DLLEXPORT static const uint8_t StructuredMessageVersion; + + AZ_STORAGE_COMMON_DLLEXPORT static const size_t StreamHeaderLength; + AZ_STORAGE_COMMON_DLLEXPORT static const int64_t StreamHeaderVersionOffset; + AZ_STORAGE_COMMON_DLLEXPORT static const int64_t StreamHeaderMessageLengthOffset; + AZ_STORAGE_COMMON_DLLEXPORT static const int64_t StreamHeaderFlagsOffset; + AZ_STORAGE_COMMON_DLLEXPORT static const int64_t StreamHeaderSegmentCountOffset; + + AZ_STORAGE_COMMON_DLLEXPORT static const size_t SegmentHeaderLength; + AZ_STORAGE_COMMON_DLLEXPORT static const int64_t SegmentHeaderNumOffset; + AZ_STORAGE_COMMON_DLLEXPORT static const int64_t SegmentHeaderContentLengthOffset; + + static void WriteStreamHeader( + uint8_t* buffer, + const uint64_t& messageLength, + const uint16_t& flags, + const uint16_t& segmentCount); + + static void WriteSegmentHeader( + uint8_t* buffer, + const uint16_t& segmentCount, + const uint64_t& segmentLength); + + static void WriteSegmentFooter(uint8_t* buffer, const uint8_t* crc64); + + static void WriteStreamFooter(uint8_t* buffer, const uint8_t* crc64); + + static void ReadStreamHeader( + const uint8_t* buffer, + uint64_t& messageLength, + StructuredMessageFlags& flags, + uint16_t& segmentCount); + + static void ReadSegmentHeader( + const uint8_t* buffer, + uint16_t& segmentCount, + uint64_t& segmentLength); + + static void ReadSegmentFooter(const uint8_t* buffer, uint8_t* crc64); + + static void ReadStreamFooter(const uint8_t* buffer, uint8_t* crc64); + }; + +}}} // namespace Azure::Storage::_internal \ No newline at end of file diff --git a/sdk/storage/azure-storage-common/inc/azure/storage/common/storage_common.hpp b/sdk/storage/azure-storage-common/inc/azure/storage/common/storage_common.hpp index 5ae312ed3b..f1ade0e0fd 100644 --- a/sdk/storage/azure-storage-common/inc/azure/storage/common/storage_common.hpp +++ b/sdk/storage/azure-storage-common/inc/azure/storage/common/storage_common.hpp @@ -54,4 +54,25 @@ namespace Azure { namespace Storage { using Metadata = Azure::Core::CaseInsensitiveMap; + /** + * @brief The algorithm used for storage checksum. + */ + enum class StorageChecksumAlgorithm + { + /** + * @brief No selected algorithm. Do not calculate or request checksums. + */ + None, + + /** + * @brief Recommended. Allow the library to choose an algorithm. Different library versions may + * make different choices. + */ + Auto, + + /** + * @brief Cyclic redundancy check. + */ + Crc64 + }; }} // namespace Azure::Storage diff --git a/sdk/storage/azure-storage-common/src/structured_message_decoding_stream.cpp b/sdk/storage/azure-storage-common/src/structured_message_decoding_stream.cpp new file mode 100644 index 0000000000..07a06f3569 --- /dev/null +++ b/sdk/storage/azure-storage-common/src/structured_message_decoding_stream.cpp @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "azure/storage/common/internal/structured_message_decoding_stream.hpp" + +#include "azure/storage/common/crypt.hpp" +#include "azure/storage/common/storage_exception.hpp" + +#include + +using Azure::Core::Context; +using Azure::Core::IO::BodyStream; + +namespace Azure { namespace Storage { namespace _internal { + + size_t StructuredMessageDecodingStream::OnRead( + uint8_t* buffer, + size_t count, + Context const& context) + { + size_t totalReadContent = 0; + while (totalReadContent < count && m_offset < m_inner->Length()) + { + switch (m_currentRegion) + { + case StructuredMessageCurrentRegion::StreamHeader: { + std::vector streamHeaderBuffer(m_streamHeaderLength); + auto bytesRead = m_inner->Read(streamHeaderBuffer.data(), m_streamHeaderLength, context); + StructuredMessageHelper::ReadStreamHeader( + streamHeaderBuffer.data(), m_length, m_flags, m_segmentCount); + m_streamFooterLength + = m_flags == StructuredMessageFlags::Crc64 ? StructuredMessageHelper::Crc64Length : 0; + m_segmentFooterLength + = m_flags == StructuredMessageFlags::Crc64 ? StructuredMessageHelper::Crc64Length : 0; + m_segmentFooterBuffer.resize(m_segmentFooterLength); + m_offset += bytesRead; + m_currentRegion = StructuredMessageCurrentRegion::SegmentHeader; + break; + } + case StructuredMessageCurrentRegion::SegmentHeader: { + auto bytesRead + = m_inner->Read(m_segmentHeaderBuffer.data(), m_segmentHeaderLength, context); + StructuredMessageHelper::ReadSegmentHeader( + m_segmentHeaderBuffer.data(), m_currentSegmentNumber, m_currentSegmentLength); + m_offset += bytesRead; + m_currentRegion = StructuredMessageCurrentRegion::SegmentContent; + break; + } + case StructuredMessageCurrentRegion::SegmentContent: { + size_t readBytes = std::min( + count - totalReadContent, + static_cast(m_currentSegmentLength - m_currentSegmentOffset)); + auto bytesRead + = m_inner->Read(buffer + totalReadContent, static_cast(readBytes), context); + if (m_flags == StructuredMessageFlags::Crc64) + { + m_segmentCrc64Hash->Append(buffer + totalReadContent, bytesRead); + } + m_offset += bytesRead; + m_currentSegmentOffset += bytesRead; + totalReadContent += bytesRead; + if (m_currentSegmentOffset == m_currentSegmentLength) + { + m_currentRegion = StructuredMessageCurrentRegion::SegmentFooter; + } + break; + } + case StructuredMessageCurrentRegion::SegmentFooter: { + if (m_flags == StructuredMessageFlags::Crc64) + { + auto bytesRead + = m_inner->Read(m_segmentFooterBuffer.data(), m_segmentFooterLength, context); + // in current version, segment footer contains crc64 hash of the segment content. + auto calculatedCrc64 = m_segmentCrc64Hash->Final(); + if (calculatedCrc64 != m_segmentFooterBuffer) + { + throw StorageException( + "Segment Compared checksums did not match. Invalid data may have been written to " + "the " + "destination. calculatedChecksum:" + + std::string(calculatedCrc64.begin(), calculatedCrc64.end()) + + "reportedChecksum: " + + std::string(m_segmentFooterBuffer.begin(), m_segmentFooterBuffer.end())); + } + m_offset += bytesRead; + m_streamCrc64Hash->Concatenate(*m_segmentCrc64Hash); + m_segmentCrc64Hash = std::make_unique(); + } + + m_currentSegmentOffset = 0; + m_currentRegion = m_currentSegmentNumber == m_segmentCount + ? StructuredMessageCurrentRegion::StreamFooter + : StructuredMessageCurrentRegion::SegmentHeader; + break; + } + case StructuredMessageCurrentRegion::StreamFooter: { + if (m_flags == StructuredMessageFlags::Crc64) + { + std::vector streamFooterBuffer(m_streamFooterLength); + auto bytesRead + = m_inner->Read(streamFooterBuffer.data(), m_streamFooterLength, context); + // in current version, segment footer contains crc64 hash of the segment content. + auto calculatedCrc64 = m_streamCrc64Hash->Final(); + if (calculatedCrc64 != streamFooterBuffer) + { + throw StorageException( + "Stream Compared checksums did not match. Invalid data may have been written to " + "the " + "destination. calculatedChecksum:" + + std::string(calculatedCrc64.begin(), calculatedCrc64.end()) + + "reportedChecksum: " + + std::string(streamFooterBuffer.begin(), streamFooterBuffer.end())); + } + m_offset += bytesRead; + m_streamCrc64Hash->Concatenate(*m_segmentCrc64Hash); + m_segmentCrc64Hash = std::make_unique(); + } + break; + } + } + } + return totalReadContent; + } +}}} // namespace Azure::Storage::_internal diff --git a/sdk/storage/azure-storage-common/src/structured_message_encoding_stream.cpp b/sdk/storage/azure-storage-common/src/structured_message_encoding_stream.cpp new file mode 100644 index 0000000000..14dc2862ef --- /dev/null +++ b/sdk/storage/azure-storage-common/src/structured_message_encoding_stream.cpp @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "azure/storage/common/internal/structured_message_encoding_stream.hpp" + +#include "azure/storage/common/crypt.hpp" + +#include + +using Azure::Core::Context; +using Azure::Core::IO::BodyStream; + +namespace Azure { namespace Storage { namespace _internal { + + size_t StructuredMessageEncodingStream::OnRead( + uint8_t* buffer, + size_t count, + Context const& context) + { + size_t totalRead = 0; + while (totalRead < count && m_offset < this->Length()) + { + size_t subreadOffset = totalRead; + switch (m_currentRegion) + { + case StructuredMessageCurrentRegion::StreamHeader: { + size_t readBytes = std::min( + count - totalRead, static_cast(m_streamHeaderLength - m_currentRegionOffset)); + if (m_streamHeaderCache.empty()) + { + m_streamHeaderCache.resize(m_streamHeaderLength); + StructuredMessageHelper::WriteStreamHeader( + m_streamHeaderCache.data(), this->Length(), m_options.Flags, m_segmentCount); + } + std::memcpy( + buffer + subreadOffset, + m_streamHeaderCache.data() + m_currentRegionOffset, + readBytes); + m_offset += readBytes; + m_currentRegionOffset += readBytes; + totalRead += readBytes; + if (m_currentRegionOffset == m_streamHeaderLength) + { + m_currentRegion = StructuredMessageCurrentRegion::SegmentHeader; + m_currentRegionOffset = 0; + } + break; + } + case StructuredMessageCurrentRegion::SegmentHeader: { + size_t readBytes = std::min( + count - totalRead, + static_cast(m_segmentHeaderLength - m_currentRegionOffset)); + if (m_segmentHeaderCache.empty()) + { + m_segmentHeaderCache.resize(m_segmentHeaderLength); + m_segmentNumber += 1; + StructuredMessageHelper::WriteSegmentHeader( + m_segmentHeaderCache.data(), + m_segmentNumber, + std::min(m_options.MaxSegmentLength, m_inner->Length() - m_innerOffset)); + } + std::memcpy( + buffer + subreadOffset, + m_segmentHeaderCache.data() + m_currentRegionOffset, + readBytes); + m_offset += readBytes; + m_currentRegionOffset += readBytes; + totalRead += readBytes; + if (m_currentRegionOffset == m_segmentHeaderLength) + { + m_currentRegion = StructuredMessageCurrentRegion::SegmentContent; + m_currentRegionOffset = 0; + } + break; + } + case StructuredMessageCurrentRegion::SegmentContent: { + size_t readBytes = std::min( + count - totalRead, + static_cast(m_options.MaxSegmentLength - m_currentRegionOffset)); + auto bytesRead + = m_inner->Read(buffer + subreadOffset, static_cast(readBytes), context); + m_offset += bytesRead; + m_innerOffset += bytesRead; + m_currentRegionOffset += bytesRead; + totalRead += bytesRead; + if (m_options.Flags == StructuredMessageFlags::Crc64) + { + m_segmentCrc64Hash->Append(buffer + subreadOffset, bytesRead); + } + if (m_currentRegionOffset == static_cast(m_options.MaxSegmentLength) + || m_innerOffset >= m_inner->Length()) + { + m_currentRegion = StructuredMessageCurrentRegion::SegmentFooter; + m_currentRegionOffset = 0; + } + break; + } + case StructuredMessageCurrentRegion::SegmentFooter: { + if (m_options.Flags == StructuredMessageFlags::Crc64) + { + size_t readBytes = std::min( + count - totalRead, + static_cast(m_segmentFooterLength - m_currentRegionOffset)); + if (m_segmentFooterCache.empty()) + { + m_segmentFooterCache.resize(m_segmentFooterLength); + StructuredMessageHelper::WriteSegmentFooter( + m_segmentFooterCache.data(), m_segmentCrc64Hash->Final().data()); + } + std::memcpy( + buffer + subreadOffset, + m_segmentFooterCache.data() + m_currentRegionOffset, + readBytes); + m_offset += readBytes; + m_currentRegionOffset += readBytes; + totalRead += readBytes; + m_streamCrc64Hash->Concatenate(*m_segmentCrc64Hash); + m_segmentCrc64Hash = std::make_unique(); + } + + if (m_currentRegionOffset == m_segmentFooterLength) + { + m_currentRegion = m_innerOffset == m_inner->Length() + ? StructuredMessageCurrentRegion::StreamFooter + : StructuredMessageCurrentRegion::SegmentHeader; + m_currentRegionOffset = 0; + m_segmentHeaderCache.clear(); + m_segmentFooterCache.clear(); + } + break; + } + case StructuredMessageCurrentRegion::StreamFooter: { + if (m_options.Flags == StructuredMessageFlags::Crc64) + { + size_t readBytes = std::min( + count - totalRead, + static_cast(m_streamFooterLength - m_currentRegionOffset)); + if (readBytes <= 0) + { + return totalRead; + } + if (m_streamFooterCache.empty()) + { + m_streamFooterCache.resize(m_streamFooterLength); + StructuredMessageHelper::WriteStreamFooter( + m_streamFooterCache.data(), m_streamCrc64Hash->Final().data()); + } + std::memcpy( + buffer + subreadOffset, + m_streamFooterCache.data() + m_currentRegionOffset, + readBytes); + m_offset += readBytes; + m_currentRegionOffset += readBytes; + totalRead += readBytes; + } + break; + } + } + } + return totalRead; + } +}}} // namespace Azure::Storage::_internal diff --git a/sdk/storage/azure-storage-common/src/structured_message_helper.cpp b/sdk/storage/azure-storage-common/src/structured_message_helper.cpp new file mode 100644 index 0000000000..71e76eb494 --- /dev/null +++ b/sdk/storage/azure-storage-common/src/structured_message_helper.cpp @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "azure/storage/common/internal/structured_message_helper.hpp" + +namespace Azure { namespace Storage { namespace _internal { + const size_t StructuredMessageHelper::Crc64Length = 8; + const uint8_t StructuredMessageHelper::StructuredMessageVersion = 1; + const size_t StructuredMessageHelper::StreamHeaderLength = 13; + const int64_t StructuredMessageHelper::StreamHeaderVersionOffset = 0; + const int64_t StructuredMessageHelper::StreamHeaderMessageLengthOffset = 1; + const int64_t StructuredMessageHelper::StreamHeaderFlagsOffset = 9; + const int64_t StructuredMessageHelper::StreamHeaderSegmentCountOffset = 11; + const size_t StructuredMessageHelper::SegmentHeaderLength = 10; + const int64_t StructuredMessageHelper::SegmentHeaderNumOffset = 0; + const int64_t StructuredMessageHelper::SegmentHeaderContentLengthOffset = 2; + + void StructuredMessageHelper::WriteStreamHeader( + uint8_t* buffer, + const uint64_t& messageLength, + const uint16_t& flags, + const uint16_t& segmentCount) + { + buffer[StructuredMessageHelper::StreamHeaderVersionOffset] + = StructuredMessageHelper::StructuredMessageVersion; + std::copy( + reinterpret_cast(&messageLength), + reinterpret_cast(&messageLength) + sizeof(uint64_t), + buffer + StructuredMessageHelper::StreamHeaderMessageLengthOffset); + std::copy( + reinterpret_cast(&flags), + reinterpret_cast(&flags) + sizeof(uint16_t), + buffer + StructuredMessageHelper::StreamHeaderFlagsOffset); + std::copy( + reinterpret_cast(&segmentCount), + reinterpret_cast(&segmentCount) + sizeof(uint16_t), + buffer + StructuredMessageHelper::StreamHeaderSegmentCountOffset); + } + + void StructuredMessageHelper::WriteSegmentHeader( + uint8_t* buffer, + const uint16_t& segmentNum, + const uint64_t& segmentLength) + { + std::copy( + reinterpret_cast(&segmentNum), + reinterpret_cast(&segmentNum) + sizeof(uint16_t), + buffer + StructuredMessageHelper::SegmentHeaderNumOffset); + std::copy( + reinterpret_cast(&segmentLength), + reinterpret_cast(&segmentLength) + sizeof(uint64_t), + buffer + StructuredMessageHelper::SegmentHeaderContentLengthOffset); + } + + void StructuredMessageHelper::WriteSegmentFooter(uint8_t* buffer, const uint8_t* crc64) + { + if (crc64 == nullptr) + { + return; + } + std::copy(crc64, crc64 + Crc64Length, buffer); + } + + void StructuredMessageHelper::WriteStreamFooter(uint8_t* buffer, const uint8_t* crc64) + { + if (crc64 == nullptr) + { + return; + } + std::copy(crc64, crc64 + Crc64Length, buffer); + } + + void StructuredMessageHelper::ReadStreamHeader( + const uint8_t* buffer, + uint64_t& messageLength, + StructuredMessageFlags& flags, + uint16_t& segmentCount) + { + messageLength = *reinterpret_cast( + buffer + StructuredMessageHelper::StreamHeaderMessageLengthOffset); + flags = static_cast(*reinterpret_cast( + buffer + StructuredMessageHelper::StreamHeaderFlagsOffset)); + segmentCount = *reinterpret_cast( + buffer + StructuredMessageHelper::StreamHeaderSegmentCountOffset); + } + + void StructuredMessageHelper::ReadSegmentHeader( + const uint8_t* buffer, + uint16_t& segmentNumber, + uint64_t& segmentLength) + { + segmentNumber = *reinterpret_cast( + buffer + StructuredMessageHelper::SegmentHeaderNumOffset); + segmentLength = *reinterpret_cast( + buffer + StructuredMessageHelper::SegmentHeaderContentLengthOffset); + } + + void StructuredMessageHelper::ReadSegmentFooter(const uint8_t* buffer, uint8_t* crc64) + { + if (buffer == nullptr) + { + return; + } + std::copy(buffer, buffer + Crc64Length, crc64); + } + + void StructuredMessageHelper::ReadStreamFooter(const uint8_t* buffer, uint8_t* crc64) + { + if (buffer == nullptr) + { + return; + } + std::copy(buffer, buffer + Crc64Length, crc64); + } +}}} // namespace Azure::Storage::_internal \ No newline at end of file diff --git a/sdk/storage/azure-storage-common/test/ut/CMakeLists.txt b/sdk/storage/azure-storage-common/test/ut/CMakeLists.txt index 22b8ed9cb1..67415f3b51 100644 --- a/sdk/storage/azure-storage-common/test/ut/CMakeLists.txt +++ b/sdk/storage/azure-storage-common/test/ut/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable ( crypt_functions_test.cpp metadata_test.cpp storage_credential_test.cpp + structured_message_test.cpp test_base.cpp test_base.hpp ) diff --git a/sdk/storage/azure-storage-common/test/ut/structured_message_test.cpp b/sdk/storage/azure-storage-common/test/ut/structured_message_test.cpp new file mode 100644 index 0000000000..8fb583b3aa --- /dev/null +++ b/sdk/storage/azure-storage-common/test/ut/structured_message_test.cpp @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "test_base.hpp" + +#include +#include + +namespace Azure { namespace Storage { namespace Test { + + class StructuredMessageTest : public StorageTest { + }; + + std::vector ReadToEnd(Azure::Core::IO::BodyStream& stream, const size_t chunkSize) + { + auto buffer = std::vector(); + + for (auto chunkNumber = 0;; chunkNumber++) + { + buffer.resize((static_cast(chunkNumber) + 1) * chunkSize); + size_t readBytes = stream.ReadToCount(buffer.data() + (chunkNumber * chunkSize), chunkSize); + + if (readBytes < chunkSize) + { + buffer.resize(static_cast((chunkNumber * chunkSize) + readBytes)); + return buffer; + } + } + } + + TEST_F(StructuredMessageTest, BasicFunction) + { + const size_t contentSize = 2 * 1024 + 512; + auto content = RandomBuffer(contentSize); + auto innerStream + = std::make_unique(content.data(), content.size()); + + _internal::StructuredMessageEncodingStreamOptions encodingOptions; + encodingOptions.Flags = _internal::StructuredMessageFlags::Crc64; + encodingOptions.MaxSegmentLength = 1024; + _internal::StructuredMessageEncodingStream encodingStream(innerStream.get(), encodingOptions); + auto encodedData = encodingStream.ReadToEnd(); + + innerStream = std::make_unique( + encodedData.data(), encodedData.size()); + _internal::StructuredMessageDecodingStreamOptions decodingOptions; + decodingOptions.ContentLength = contentSize; + _internal::StructuredMessageDecodingStream decodingStream( + std::move(innerStream), decodingOptions); + auto decodedData = decodingStream.ReadToEnd(); + + EXPECT_EQ(content, decodedData); + } + + TEST_F(StructuredMessageTest, SmallSegment) + { + const size_t contentSize = 2 * 1024 * 1024 + 5122; + auto content = RandomBuffer(contentSize); + auto innerStream + = std::make_unique(content.data(), content.size()); + + _internal::StructuredMessageEncodingStreamOptions encodingOptions; + encodingOptions.Flags = _internal::StructuredMessageFlags::Crc64; + encodingOptions.MaxSegmentLength = 33; + _internal::StructuredMessageEncodingStream encodingStream(innerStream.get(), encodingOptions); + auto encodedData = ReadToEnd(encodingStream, 4096); + + innerStream = std::make_unique( + encodedData.data(), encodedData.size()); + _internal::StructuredMessageDecodingStreamOptions decodingOptions; + decodingOptions.ContentLength = contentSize; + _internal::StructuredMessageDecodingStream decodingStream( + std::move(innerStream), decodingOptions); + auto decodedData = ReadToEnd(decodingStream, 513); + + EXPECT_EQ(content, decodedData); + } + + TEST_F(StructuredMessageTest, ReadSmallRange) + { + const size_t contentSize = 2 * 1024 + 512; + auto content = RandomBuffer(contentSize); + auto innerStream + = std::make_unique(content.data(), content.size()); + + _internal::StructuredMessageEncodingStreamOptions encodingOptions; + encodingOptions.Flags = _internal::StructuredMessageFlags::Crc64; + encodingOptions.MaxSegmentLength = 1024; + _internal::StructuredMessageEncodingStream encodingStream(innerStream.get(), encodingOptions); + std::vector encodedData = ReadToEnd(encodingStream, 7); + + _internal::StructuredMessageDecodingStreamOptions decodingOptions; + decodingOptions.ContentLength = contentSize; + _internal::StructuredMessageDecodingStream decodingStream( + std::make_unique(encodedData.data(), encodedData.size()), + decodingOptions); + auto decodedData = ReadToEnd(decodingStream, 7); + + EXPECT_EQ(content, decodedData); + + // Large encode range + encodingStream.Rewind(); + encodedData = ReadToEnd(encodingStream, 4096); + _internal::StructuredMessageDecodingStream decodingStream1( + std::make_unique(encodedData.data(), encodedData.size()), + decodingOptions); + decodedData = ReadToEnd(decodingStream1, 5); + + EXPECT_EQ(content, decodedData); + + decodingStream1.Rewind(); + decodedData = ReadToEnd(decodingStream1, 6); + + EXPECT_EQ(content, decodedData); + + // Large decode range + encodingStream.Rewind(); + encodedData = ReadToEnd(encodingStream, 8); + _internal::StructuredMessageDecodingStream decodingStream2( + std::make_unique(encodedData.data(), encodedData.size()), + decodingOptions); + decodedData = ReadToEnd(decodingStream2, 4096); + + EXPECT_EQ(content, decodedData); + } + + TEST_F(StructuredMessageTest, ReadBigRange) + { + const size_t contentSize = 4 * 1024 * 1024 + 2 * 1024 + 512 + 3; + auto content = RandomBuffer(contentSize); + auto innerStream + = std::make_unique(content.data(), content.size()); + + _internal::StructuredMessageEncodingStreamOptions encodingOptions; + encodingOptions.Flags = _internal::StructuredMessageFlags::Crc64; + _internal::StructuredMessageEncodingStream encodingStream(innerStream.get(), encodingOptions); + std::vector encodedData = ReadToEnd(encodingStream, 4 * 1024 * 1024); + + innerStream = std::make_unique( + encodedData.data(), encodedData.size()); + _internal::StructuredMessageDecodingStreamOptions decodingOptions; + decodingOptions.ContentLength = contentSize; + _internal::StructuredMessageDecodingStream decodingStream( + std::move(innerStream), decodingOptions); + auto decodedData = ReadToEnd(decodingStream, 4 * 1024 * 1024); + + EXPECT_EQ(content, decodedData); + } + + TEST_F(StructuredMessageTest, NotCrc64) + { + const size_t contentSize = 2 * 1024 + 512; + auto content = RandomBuffer(contentSize); + auto innerStream + = std::make_unique(content.data(), content.size()); + + _internal::StructuredMessageEncodingStreamOptions encodingOptions; + encodingOptions.Flags = _internal::StructuredMessageFlags::None; + encodingOptions.MaxSegmentLength = 1024; + _internal::StructuredMessageEncodingStream encodingStream(innerStream.get(), encodingOptions); + std::vector encodedData = ReadToEnd(encodingStream, 4096); + + _internal::StructuredMessageDecodingStreamOptions decodingOptions; + decodingOptions.ContentLength = contentSize; + innerStream = std::make_unique( + encodedData.data(), encodedData.size()); + _internal::StructuredMessageDecodingStream decodingStream( + std::move(innerStream), decodingOptions); + auto decodedData = ReadToEnd(decodingStream, 4096); + + EXPECT_EQ(content, decodedData); + } + + TEST_F(StructuredMessageTest, NotCrc64SmallRange) + { + const size_t contentSize = 2 * 1024 + 512; + auto content = RandomBuffer(contentSize); + auto innerStream + = std::make_unique(content.data(), content.size()); + + _internal::StructuredMessageEncodingStreamOptions encodingOptions; + encodingOptions.Flags = _internal::StructuredMessageFlags::None; + encodingOptions.MaxSegmentLength = 1024; + _internal::StructuredMessageEncodingStream encodingStream(innerStream.get(), encodingOptions); + std::vector encodedData = ReadToEnd(encodingStream, 7); + + _internal::StructuredMessageDecodingStreamOptions decodingOptions; + decodingOptions.ContentLength = contentSize; + _internal::StructuredMessageDecodingStream decodingStream( + std::make_unique(encodedData.data(), encodedData.size()), + decodingOptions); + auto decodedData = ReadToEnd(decodingStream, 7); + + EXPECT_EQ(content, decodedData); + + // Large encode range + encodingStream.Rewind(); + encodedData = ReadToEnd(encodingStream, 4096); + _internal::StructuredMessageDecodingStream decodingStream1( + std::make_unique(encodedData.data(), encodedData.size()), + decodingOptions); + decodedData = ReadToEnd(decodingStream1, 5); + + EXPECT_EQ(content, decodedData); + + decodingStream1.Rewind(); + decodedData = ReadToEnd(decodingStream1, 6); + + EXPECT_EQ(content, decodedData); + + // Large decode range + encodingStream.Rewind(); + encodedData = ReadToEnd(encodingStream, 8); + _internal::StructuredMessageDecodingStream decodingStream2( + std::make_unique(encodedData.data(), encodedData.size()), + decodingOptions); + decodedData = ReadToEnd(decodingStream2, 4096); + + EXPECT_EQ(content, decodedData); + } + +}}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_file_client.hpp b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_file_client.hpp index 9743563c1f..49c1acbfea 100644 --- a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_file_client.hpp +++ b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_file_client.hpp @@ -295,6 +295,11 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { } + /** @brief Upload TransferValidationOptions */ + Azure::Nullable m_uploadValidationOptions; + /** @brief Download TransferValidationOptions */ + Azure::Nullable m_downloadValidationOptions; + friend class DataLakeFileSystemClient; friend class DataLakeDirectoryClient; }; diff --git a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_options.hpp b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_options.hpp index f7b851c413..d54a2fb8ca 100644 --- a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_options.hpp +++ b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_options.hpp @@ -153,6 +153,17 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { AZ_STORAGE_FILES_DATALAKE_DLLEXPORT const static DataLakeAudience DefaultAudience; }; + /** + * Configures whether to do content validation for file uploads and downloads. + */ + struct TransferValidationOptions + { + /** + * @brief The algorithm used for storage checksum. + */ + StorageChecksumAlgorithm Algorithm = StorageChecksumAlgorithm::None; + }; + /** * @brief Client options used to initialize all DataLake clients. */ @@ -190,6 +201,16 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { * if Audience is not set. */ Azure::Nullable Audience; + + /** + * @brief Optional. Configures whether to do content validation for file uploads. + */ + Azure::Nullable UploadValidationOptions; + + /** + * @brief Optional. Configures whether to do content validation for file downloads. + */ + Azure::Nullable DownloadValidationOptions; }; /** @@ -496,6 +517,11 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { * be changed using renew or change. */ Azure::Nullable LeaseDuration; + + /** + * @brief Optional. Configures whether to do content validation for file appends. + */ + Azure::Nullable ValidationOptions; }; /** @@ -852,6 +878,11 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { * https://learn.microsoft.com/entra/identity/hybrid/connect/plan-connect-userprincipalname#what-is-userprincipalname */ Nullable IncludeUserPrincipalName; + + /** + * @brief Optional. Configures whether to do content validation for file downloads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -978,6 +1009,11 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { */ int32_t Concurrency = 5; } TransferOptions; + + /** + * @brief Optional. Configures whether to do content validation for file uploads. + */ + Azure::Nullable ValidationOptions; }; using AcquireLeaseOptions = Blobs::AcquireLeaseOptions; diff --git a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_responses.hpp b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_responses.hpp index 0d7debf42c..829a04dc47 100644 --- a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_responses.hpp +++ b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_responses.hpp @@ -731,6 +731,18 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { * The permissions of the file. */ Azure::Nullable Permissions; + + /** + * Indicates the response body contains a structured message and specifies the message schema + * version and properties. + */ + Nullable StructuredBodyType; + + /** + * The length of the blob/file content inside the message body when the response body is + * returned as a structured message. Will always be smaller than Content-Length. + */ + Nullable StructuredContentLength; }; /** @@ -758,6 +770,17 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { */ Azure::Nullable TransactionalContentHash; + /** + * Indicates the response body contains a structured message and specifies the message schema + * version and properties. + */ + Nullable StructuredBodyType; + /** + * The length of the blob/file content inside the message body when the response body is + * returned as a structured message. Will always be smaller than Content-Length. + */ + Nullable StructuredContentLength; + /** * The detailed information of the downloaded file. */ diff --git a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_sas_builder.hpp b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_sas_builder.hpp index f1cf2aae21..c981eb53f3 100644 --- a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_sas_builder.hpp +++ b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/datalake_sas_builder.hpp @@ -249,6 +249,18 @@ namespace Azure { namespace Storage { namespace Sas { */ std::string DelegatedUserObjectId; + /** + * @brief Optional. Custom Request Headers to include in the SAS. Any usage of the SAS must + * include these headers and values in the request. + */ + std::map RequestHeaders; + + /** + * @brief Optional. Custom Request Query Parameters to include in the SAS. Any usage of the SAS + * must include these query parameters and values in the request. + */ + std::map RequestQueryParameters; + /** * @brief Override the value returned for Cache-Control response header. */ diff --git a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/rest_client.hpp b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/rest_client.hpp index 0c8f5a395e..dfeede6b7e 100644 --- a/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/rest_client.hpp +++ b/sdk/storage/azure-storage-files-datalake/inc/azure/storage/files/datalake/rest_client.hpp @@ -27,7 +27,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { /** * The version used for the operations to Azure storage services. */ - constexpr static const char* ApiVersion = "2026-02-06"; + constexpr static const char* ApiVersion = "2026-04-06"; } // namespace _detail namespace Models { namespace _detail { @@ -354,6 +354,11 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { * If the lease was auto-renewed with this request. */ Nullable IsLeaseRenewed; + /** + * Indicates the structured message body was accepted and mirrors back the message schema + * version and properties. + */ + Nullable StructuredBodyType; }; } // namespace Models namespace _detail { @@ -537,6 +542,8 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { Nullable> EncryptionKeySha256; Nullable EncryptionAlgorithm; Nullable Flush; + Nullable StructuredBodyType; + Nullable StructuredContentLength; }; static Response Append( Core::Http::_internal::HttpPipeline& pipeline, diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_file_client.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_file_client.cpp index 0995332e82..a41da7dd87 100644 --- a/sdk/storage/azure-storage-files-datalake/src/datalake_file_client.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_file_client.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace Azure { namespace Storage { namespace Files { namespace DataLake { @@ -55,7 +56,9 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { DataLakeFileClient::DataLakeFileClient( const std::string& fileUrl, const DataLakeClientOptions& options) - : DataLakePathClient(fileUrl, options) + : DataLakePathClient(fileUrl, options), + m_uploadValidationOptions(options.UploadValidationOptions), + m_downloadValidationOptions(options.DownloadValidationOptions) { } @@ -67,19 +70,6 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { _detail::FileClient::AppendFileOptions protocolLayerOptions; protocolLayerOptions.Position = offset; - if (options.TransactionalContentHash.HasValue()) - { - if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Crc64) - { - protocolLayerOptions.TransactionalContentCrc64 - = options.TransactionalContentHash.Value().Value; - } - else if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Md5) - { - protocolLayerOptions.TransactionalContentHash - = options.TransactionalContentHash.Value().Value; - } - } protocolLayerOptions.LeaseId = options.AccessConditions.LeaseId; protocolLayerOptions.Flush = options.Flush; if (m_clientConfiguration.CustomerProvidedKey.HasValue()) @@ -96,6 +86,38 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { protocolLayerOptions.LeaseDuration = static_cast(options.LeaseDuration->count()); } + + if (options.TransactionalContentHash.HasValue()) + { + if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Crc64) + { + protocolLayerOptions.TransactionalContentCrc64 + = options.TransactionalContentHash.Value().Value; + } + else if (options.TransactionalContentHash.Value().Algorithm == HashAlgorithm::Md5) + { + protocolLayerOptions.TransactionalContentHash + = options.TransactionalContentHash.Value().Value; + } + } + else + { + Azure::Nullable validationOptions + = options.ValidationOptions.HasValue() ? options.ValidationOptions + : m_uploadValidationOptions; + if (validationOptions.HasValue() + && validationOptions.Value().Algorithm != StorageChecksumAlgorithm::None) + { + protocolLayerOptions.StructuredBodyType = _internal::CrcStructuredMessage; + protocolLayerOptions.StructuredContentLength = content.Length(); + _internal::StructuredMessageEncodingStreamOptions encodingStreamOptions; + encodingStreamOptions.Flags = _internal::StructuredMessageFlags::Crc64; + auto structuredContent + = _internal::StructuredMessageEncodingStream(&content, encodingStreamOptions); + return _detail::FileClient::Append( + *m_pipeline, m_pathUrl, structuredContent, protocolLayerOptions, context); + } + } return _detail::FileClient::Append( *m_pipeline, m_pathUrl, content, protocolLayerOptions, context); } @@ -182,6 +204,12 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { blobOptions.AccessConditions.IfModifiedSince = options.AccessConditions.IfModifiedSince; blobOptions.AccessConditions.IfUnmodifiedSince = options.AccessConditions.IfUnmodifiedSince; blobOptions.AccessConditions.LeaseId = options.AccessConditions.LeaseId; + if (options.ValidationOptions.HasValue()) + { + Blobs::TransferValidationOptions validationOptions; + validationOptions.Algorithm = options.ValidationOptions.Value().Algorithm; + blobOptions.ValidationOptions = std::move(validationOptions); + } auto response = m_blobClient.Download( blobOptions, options.IncludeUserPrincipalName.HasValue() ? context.WithValue( @@ -220,6 +248,8 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { ret.Details.EncryptionKeySha256 = std::move(response.Value.Details.EncryptionKeySha256); ret.Details.EncryptionScope = std::move(response.Value.Details.EncryptionScope); ret.Details.IsServerEncrypted = response.Value.Details.IsServerEncrypted; + ret.StructuredBodyType = response.Value.StructuredBodyType; + ret.StructuredContentLength = response.Value.StructuredContentLength; auto& headers = response.RawResponse->GetHeaders(); auto encryptionContext = headers.find(_detail::EncryptionContextHeaderName); if (encryptionContext != headers.end()) @@ -262,6 +292,12 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { blobOptions.TransferOptions.Concurrency = options.TransferOptions.Concurrency; blobOptions.HttpHeaders = options.HttpHeaders; blobOptions.Metadata = options.Metadata; + if (options.ValidationOptions.HasValue()) + { + Blobs::TransferValidationOptions validationOptions; + validationOptions.Algorithm = options.ValidationOptions.Value().Algorithm; + blobOptions.ValidationOptions = std::move(validationOptions); + } return m_blobClient.AsBlockBlobClient().UploadFrom(fileName, blobOptions, context); } @@ -278,6 +314,12 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { blobOptions.TransferOptions.Concurrency = options.TransferOptions.Concurrency; blobOptions.HttpHeaders = options.HttpHeaders; blobOptions.Metadata = options.Metadata; + if (options.ValidationOptions.HasValue()) + { + Blobs::TransferValidationOptions validationOptions; + validationOptions.Algorithm = options.ValidationOptions.Value().Algorithm; + blobOptions.ValidationOptions = std::move(validationOptions); + } return m_blobClient.AsBlockBlobClient().UploadFrom(buffer, bufferSize, blobOptions, context); } diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_path_client.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_path_client.cpp index 0ad834301e..b1c6b1390b 100644 --- a/sdk/storage/azure-storage-files-datalake/src/datalake_path_client.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_path_client.cpp @@ -381,7 +381,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { = std::move(response.Value.IncrementalCopyDestinationSnapshot); ret.VersionId = std::move(response.Value.VersionId); ret.IsCurrentVersion = std::move(response.Value.IsCurrentVersion); - ret.IsDirectory = _detail::MetadataIncidatesIsDirectory(ret.Metadata); + ret.IsDirectory = _detail::MetadataIndicatesIsDirectory(ret.Metadata); auto& headers = response.RawResponse->GetHeaders(); auto encryptionContext = headers.find(_detail::EncryptionContextHeaderName); if (encryptionContext != headers.end()) diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_sas_builder.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_sas_builder.cpp index d413772136..15c9770796 100644 --- a/sdk/storage/azure-storage-files-datalake/src/datalake_sas_builder.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_sas_builder.cpp @@ -6,7 +6,8 @@ #include #include -/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, saoid, suoid, scid, sduoid */ +/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, saoid, suoid, scid, skdutid, sduoid, + * srh, srq */ namespace Azure { namespace Storage { namespace Sas { namespace { @@ -31,6 +32,53 @@ namespace Azure { namespace Storage { namespace Sas { throw std::invalid_argument("Unknown DataLakeSasResource value."); } } + + std::string ParseRequestQueryParameters( + const std::map& queryParameters) + { + if (queryParameters.empty()) + { + return ""; + } + std::string result; + for (const auto& pair : queryParameters) + { + result += "\n" + pair.first + ":" + pair.second; + } + return result; + } + + std::string ParseRequestHeaders(const std::map& headers) + { + if (headers.empty()) + { + return ""; + } + std::string result; + for (const auto& pair : headers) + { + result += pair.first + ":" + pair.second + "\n"; + } + return result; + } + + std::string ParseRequestKeys(const std::map& map) + { + if (map.empty()) + { + return ""; + } + std::string result; + for (auto it = map.begin(); it != map.end(); ++it) + { + result += it->first; + if (std::next(it) != map.end()) + { + result += ","; + } + } + return result; + } } // namespace void DataLakeSasBuilder::SetPermissions(DataLakeFileSystemSasPermissions permissions) @@ -226,11 +274,15 @@ namespace Azure { namespace Storage { namespace Sas { + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n" - + PreauthorizedAgentObjectId + "\n" + AgentObjectId + "\n" + CorrelationId + "\n" + "\n" - + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + PreauthorizedAgentObjectId + "\n" + AgentObjectId + "\n" + CorrelationId + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" + resource + "\n" + "\n" + EncryptionScope + "\n" - + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage - + "\n" + ContentType; + + ParseRequestHeaders(RequestHeaders) + "\n" + + ParseRequestQueryParameters(RequestQueryParameters) + "\n" + CacheControl + "\n" + + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; std::string signature = Azure::Core::Convert::Base64Encode(_internal::HmacSha256( std::vector(stringToSign.begin(), stringToSign.end()), @@ -273,6 +325,12 @@ namespace Azure { namespace Storage { namespace Sas { { builder.AppendQueryParameter("scid", _internal::UrlEncodeQueryParameter(CorrelationId)); } + if (userDelegationKey.SignedDelegatedUserTid.HasValue()) + { + builder.AppendQueryParameter( + "skdutid", + _internal::UrlEncodeQueryParameter(userDelegationKey.SignedDelegatedUserTid.Value())); + } if (!DelegatedUserObjectId.empty()) { builder.AppendQueryParameter( @@ -283,6 +341,16 @@ namespace Azure { namespace Storage { namespace Sas { builder.AppendQueryParameter( "sdd", _internal::UrlEncodeQueryParameter(std::to_string(DirectoryDepth.Value()))); } + if (!RequestHeaders.empty()) + { + builder.AppendQueryParameter( + "srh", _internal::UrlEncodeQueryParameter(ParseRequestKeys(RequestHeaders))); + } + if (!RequestQueryParameters.empty()) + { + builder.AppendQueryParameter( + "srq", _internal::UrlEncodeQueryParameter(ParseRequestKeys(RequestQueryParameters))); + } if (!CacheControl.empty()) { builder.AppendQueryParameter("rscc", _internal::UrlEncodeQueryParameter(CacheControl)); @@ -365,10 +433,15 @@ namespace Azure { namespace Storage { namespace Sas { + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n" + PreauthorizedAgentObjectId + "\n" - + AgentObjectId + "\n" + CorrelationId + "\n\n" + DelegatedUserObjectId + "\n" - + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" - + resource + "\n" + "\n" + EncryptionScope + "\n" + CacheControl + "\n" + ContentDisposition - + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; + + AgentObjectId + "\n" + CorrelationId + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + protocol + "\n" + SasVersion + "\n" + resource + "\n" + "\n" + EncryptionScope + "\n" + + ParseRequestHeaders(RequestHeaders) + "\n" + + ParseRequestQueryParameters(RequestQueryParameters) + "\n" + CacheControl + "\n" + + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; } }}} // namespace Azure::Storage::Sas diff --git a/sdk/storage/azure-storage-files-datalake/src/datalake_utilities.cpp b/sdk/storage/azure-storage-files-datalake/src/datalake_utilities.cpp index d7cf408502..3f12a2fea7 100644 --- a/sdk/storage/azure-storage-files-datalake/src/datalake_utilities.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/datalake_utilities.cpp @@ -83,7 +83,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { nam return std::string(begin, end); } - bool MetadataIncidatesIsDirectory(const Storage::Metadata& metadata) + bool MetadataIndicatesIsDirectory(const Storage::Metadata& metadata) { auto ite = metadata.find(DataLakeIsDirectoryKey); return ite != metadata.end() && ite->second == "true"; @@ -102,6 +102,18 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { nam { blobOptions.Audience = Blobs::BlobAudience(options.Audience.Value().ToString()); } + if (options.DownloadValidationOptions.HasValue()) + { + Blobs::TransferValidationOptions validationOptions; + validationOptions.Algorithm = options.DownloadValidationOptions.Value().Algorithm; + blobOptions.DownloadValidationOptions = std::move(validationOptions); + } + if (options.UploadValidationOptions.HasValue()) + { + Blobs::TransferValidationOptions validationOptions; + validationOptions.Algorithm = options.UploadValidationOptions.Value().Algorithm; + blobOptions.DownloadValidationOptions = std::move(validationOptions); + } return blobOptions; } diff --git a/sdk/storage/azure-storage-files-datalake/src/private/datalake_utilities.hpp b/sdk/storage/azure-storage-files-datalake/src/private/datalake_utilities.hpp index a8ceb04a43..b487c5b872 100644 --- a/sdk/storage/azure-storage-files-datalake/src/private/datalake_utilities.hpp +++ b/sdk/storage/azure-storage-files-datalake/src/private/datalake_utilities.hpp @@ -25,7 +25,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { nam const std::string& string, std::string::const_iterator& cur); - bool MetadataIncidatesIsDirectory(const Storage::Metadata& metadata); + bool MetadataIndicatesIsDirectory(const Storage::Metadata& metadata); Blobs::BlobClientOptions GetBlobClientOptions(const DataLakeClientOptions& options); diff --git a/sdk/storage/azure-storage-files-datalake/src/rest_client.cpp b/sdk/storage/azure-storage-files-datalake/src/rest_client.cpp index 5445997220..695b7b1583 100644 --- a/sdk/storage/azure-storage-files-datalake/src/rest_client.cpp +++ b/sdk/storage/azure-storage-files-datalake/src/rest_client.cpp @@ -61,7 +61,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.GetUrl().AppendQueryParameter("timeout", std::to_string(options.Timeout.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.ContinuationToken.HasValue() && !options.ContinuationToken.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -162,7 +162,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.GetUrl().AppendQueryParameter("timeout", std::to_string(options.Timeout.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Resource.HasValue() && !options.Resource.Value().ToString().empty()) { request.GetUrl().AppendQueryParameter( @@ -350,7 +350,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.GetUrl().AppendQueryParameter("timeout", std::to_string(options.Timeout.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Recursive.HasValue()) { request.GetUrl().AppendQueryParameter( @@ -448,7 +448,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -495,7 +495,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.SetHeader("x-ms-acl", options.Acl.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -548,7 +548,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.SetHeader("x-ms-undelete-source", options.UndeleteSource.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -599,7 +599,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -698,7 +698,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { "If-Unmodified-Since", options.IfUnmodifiedSince.Value().ToString(Azure::DateTime::DateFormat::Rfc1123)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.EncryptionKey.HasValue() && !options.EncryptionKey.Value().empty()) { request.SetHeader("x-ms-encryption-key", options.EncryptionKey.Value()); @@ -782,7 +782,7 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.SetHeader("x-ms-proposed-lease-id", options.ProposedLeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.EncryptionKey.HasValue() && !options.EncryptionKey.Value().empty()) { request.SetHeader("x-ms-encryption-key", options.EncryptionKey.Value()); @@ -802,6 +802,16 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { { request.GetUrl().AppendQueryParameter("flush", options.Flush.Value() ? "true" : "false"); } + if (options.StructuredBodyType.HasValue() && !options.StructuredBodyType.Value().empty()) + { + request.SetHeader("x-ms-structured-body", options.StructuredBodyType.Value()); + } + if (options.StructuredContentLength.HasValue()) + { + request.SetHeader( + "x-ms-structured-content-length", + std::to_string(options.StructuredContentLength.Value())); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -835,6 +845,10 @@ namespace Azure { namespace Storage { namespace Files { namespace DataLake { response.IsLeaseRenewed = pRawResponse->GetHeaders().at("x-ms-lease-renewed") == std::string("true"); } + if (pRawResponse->GetHeaders().count("x-ms-structured-body") != 0) + { + response.StructuredBodyType = pRawResponse->GetHeaders().at("x-ms-structured-body"); + } return Response(std::move(response), std::move(pRawResponse)); } } // namespace _detail diff --git a/sdk/storage/azure-storage-files-datalake/swagger/README.md b/sdk/storage/azure-storage-files-datalake/swagger/README.md index bc47e52147..6aeaf8fc5c 100644 --- a/sdk/storage/azure-storage-files-datalake/swagger/README.md +++ b/sdk/storage/azure-storage-files-datalake/swagger/README.md @@ -43,13 +43,12 @@ directive: - from: swagger-document where: $["x-ms-paths"].*.*.parameters transform: > - $ = $.filter(p => !(p["$ref"] && (p["$ref"].endsWith("#/parameters/Timeout") || p["$ref"].endsWith("#/parameters/ClientRequestId") - || p["$ref"].endsWith("#/parameters/StructuredBodyGet") || p["$ref"].endsWith("#/parameters/StructuredBodyPut") || p["$ref"].endsWith("#/parameters/StructuredContentLength")))); + $ = $.filter(p => !(p["$ref"] && (p["$ref"].endsWith("#/parameters/Timeout") || p["$ref"].endsWith("#/parameters/ClientRequestId")))); - from: swagger-document where: $["x-ms-paths"].*.*.responses.*.headers transform: > for (const h in $) { - if (["x-ms-client-request-id", "x-ms-request-id", "x-ms-version", "Date", "x-ms-structured-body", "x-ms-structured-content-length"].includes(h)) { + if (["x-ms-client-request-id", "x-ms-request-id", "x-ms-version", "Date"].includes(h)) { delete $[h]; } } @@ -89,12 +88,12 @@ directive: "name": "ApiVersion", "modelAsString": false }, - "enum": ["2026-02-06"] + "enum": ["2026-04-06"] }; - from: swagger-document where: $.parameters transform: > - $.ApiVersionParameter.enum[0] = "2026-02-06"; + $.ApiVersionParameter.enum[0] = "2026-04-06"; ``` ### Rename Operations @@ -211,6 +210,9 @@ directive: if (h === "x-ms-encryption-key-sha256") { $[h]["format"] = "byte"; } + if (h === "x-ms-structured-body" || h === "x-ms-structured-content-length") { + $[h]["x-nullable"] = true; + } } - from: swagger-document where: $.parameters diff --git a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_file_client_test.cpp b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_file_client_test.cpp index b025bdc865..57dcd3910e 100644 --- a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_file_client_test.cpp +++ b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_file_client_test.cpp @@ -1074,4 +1074,89 @@ namespace Azure { namespace Storage { namespace Test { } } + TEST_F(DataLakeFileClientTest, StructuredMessageTest) + { + const size_t contentSize = 2 * 1024 + 512; + auto content = RandomBuffer(contentSize); + auto bodyStream = Azure::Core::IO::MemoryBodyStream(content.data(), content.size()); + const std::string tempFileName = RandomString(); + WriteFile(tempFileName, content); + Files::DataLake::TransferValidationOptions validationOptions; + validationOptions.Algorithm = StorageChecksumAlgorithm::Crc64; + Blobs::TransferValidationOptions blobValidationOptions; + blobValidationOptions.Algorithm = StorageChecksumAlgorithm::Crc64; + + // Append + Files::DataLake::AppendFileOptions appendOptions; + appendOptions.ValidationOptions = validationOptions; + Files::DataLake::Models::AppendFileResult appendResult; + EXPECT_NO_THROW(appendResult = m_fileClient->Append(bodyStream, 0, appendOptions).Value); + EXPECT_TRUE(appendResult.StructuredBodyType.HasValue()); + // Serice Bug: Upper Case returned. + // EXPECT_EQ(appendResult.StructuredBodyType.Value(), _internal::CrcStructuredMessage); + // Flush + m_fileClient->Flush(contentSize); + + // Download + Files::DataLake::DownloadFileOptions downloadOptions; + downloadOptions.ValidationOptions = validationOptions; + Files::DataLake::Models::DownloadFileResult downloadResult; + EXPECT_NO_THROW(downloadResult = m_fileClient->Download(downloadOptions).Value); + auto downloadedData = downloadResult.Body->ReadToEnd(); + EXPECT_EQ(content, downloadedData); + EXPECT_TRUE(downloadResult.StructuredContentLength.HasValue()); + EXPECT_EQ(downloadResult.StructuredContentLength.Value(), contentSize); + EXPECT_TRUE(downloadResult.StructuredBodyType.HasValue()); + EXPECT_EQ(downloadResult.StructuredBodyType.Value(), _internal::CrcStructuredMessage); + EXPECT_EQ(downloadResult.FileSize, contentSize); + // partial download + downloadOptions.Range = Core::Http::HttpRange(); + downloadOptions.Range.Value().Length = contentSize / 2; + EXPECT_NO_THROW(downloadResult = m_fileClient->Download(downloadOptions).Value); + downloadedData = downloadResult.Body->ReadToEnd(); + EXPECT_EQ( + downloadedData, std::vector(content.begin(), content.begin() + contentSize / 2)); + EXPECT_TRUE(downloadResult.StructuredContentLength.HasValue()); + EXPECT_EQ(downloadResult.StructuredContentLength.Value(), contentSize / 2); + EXPECT_TRUE(downloadResult.StructuredBodyType.HasValue()); + EXPECT_EQ(downloadResult.StructuredBodyType.Value(), _internal::CrcStructuredMessage); + EXPECT_EQ(downloadResult.FileSize, contentSize); + downloadOptions.Range.Reset(); + + // UploadFrom DownloadTo + Files::DataLake::UploadFileFromOptions uploadOptions; + uploadOptions.ValidationOptions = validationOptions; + Files::DataLake::Models::UploadFileFromResult uploadFromResult; + Files::DataLake::DownloadFileToOptions downloadToOptions; + downloadToOptions.ValidationOptions = blobValidationOptions; + Files::DataLake::Models::DownloadFileToResult downloadToResult; + + // From stream + auto fileClient = m_fileSystemClient->GetFileClient("uploadfromstream_" + RandomString()); + EXPECT_NO_THROW( + uploadFromResult = fileClient.UploadFrom(content.data(), contentSize, uploadOptions).Value); + auto downloadBuffer = std::vector(contentSize, '\x00'); + EXPECT_NO_THROW( + downloadToResult + = fileClient.DownloadTo(downloadBuffer.data(), contentSize, downloadToOptions).Value); + EXPECT_EQ(downloadBuffer, content); + // partial downloadTo + downloadToOptions.Range = Core::Http::HttpRange(); + downloadToOptions.Range.Value().Length = contentSize / 2; + downloadBuffer.resize(static_cast(contentSize / 2), '\x00'); + EXPECT_NO_THROW( + downloadToResult + = fileClient.DownloadTo(downloadBuffer.data(), contentSize / 2, downloadToOptions).Value); + EXPECT_EQ( + downloadBuffer, std::vector(content.begin(), content.begin() + contentSize / 2)); + downloadToOptions.Range.Reset(); + + // From file + fileClient = m_fileSystemClient->GetFileClient("uploadfromfile_" + RandomString()); + EXPECT_NO_THROW(uploadFromResult = fileClient.UploadFrom(tempFileName, uploadOptions).Value); + auto downloadFileName = RandomString(); + EXPECT_NO_THROW( + downloadToResult = fileClient.DownloadTo(downloadFileName, downloadToOptions).Value); + } + }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_sas_test.cpp b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_sas_test.cpp index 60f5c174bc..48ca88e926 100644 --- a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_sas_test.cpp +++ b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_sas_test.cpp @@ -886,7 +886,7 @@ namespace Azure { namespace Storage { namespace Test { return {}; } - TEST_F(DataLakeSasTest, DISABLED_PrincipalBoundDelegationSas) + TEST_F(DataLakeSasTest, PrincipalBoundDelegationSas_LIVEONLY_) { auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); @@ -932,4 +932,137 @@ namespace Azure { namespace Storage { namespace Test { InitStorageClientOptions()); EXPECT_THROW(fileClient2.GetProperties(), StorageException); } + + TEST_F(DataLakeSasTest, DISABLED_PrincipalBoundDelegationSas_CrossTenant) + { + auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); + auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); + + auto keyCredential = _internal::ParseConnectionString(AdlsGen2ConnectionString()).KeyCredential; + auto accountName = keyCredential->AccountName; + + Azure::Identity::ClientSecretCredentialOptions credentialOptions; + credentialOptions.AdditionallyAllowedTenants = {"*"}; + auto endUserCredential = std::make_shared( + GetEnv("AZURE_TENANT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_SECRET_CROSS_TENANT")); + auto delegatedUserObjectId = getObjectIdFromTokenCredential(endUserCredential); + + Files::DataLake::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "4ab3a968-f1ae-47a6-b82c-f654612122a9"; + Files::DataLake::Models::UserDelegationKey userDelegationKey + = GetDataLakeServiceClientOAuth().GetUserDelegationKey(sasExpiresOn, options).Value; + + std::string fileName = RandomString(); + + auto dataLakeFileSystemClient = *m_fileSystemClient; + auto dataLakeFileClient = dataLakeFileSystemClient.GetFileClient(fileName); + dataLakeFileClient.Create(); + + Sas::DataLakeSasBuilder fileSasBuilder; + fileSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + fileSasBuilder.StartsOn = sasStartsOn; + fileSasBuilder.ExpiresOn = sasExpiresOn; + fileSasBuilder.FileSystemName = m_fileSystemName; + fileSasBuilder.Path = fileName; + fileSasBuilder.Resource = Sas::DataLakeSasResource::File; + fileSasBuilder.DelegatedUserObjectId = delegatedUserObjectId; + + fileSasBuilder.SetPermissions(Sas::DataLakeSasPermissions::All); + auto sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Files::DataLake::DataLakeFileClient fileClient1( + AppendQueryParameters(Azure::Core::Url(dataLakeFileClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_NO_THROW(fileClient1.GetProperties()); + + options.DelegatedUserTid = "00000000-0000-0000-0000-000000000000"; + userDelegationKey + = GetDataLakeServiceClientOAuth().GetUserDelegationKey(sasExpiresOn, options).Value; + + sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Files::DataLake::DataLakeFileClient fileClient2( + AppendQueryParameters(Azure::Core::Url(dataLakeFileClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_THROW(fileClient2.GetProperties(), StorageException); + } + + TEST_F(DataLakeSasTest, DISABLED_DynamicSas) + { + auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); + auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); + + auto keyCredential = _internal::ParseConnectionString(AdlsGen2ConnectionString()).KeyCredential; + auto accountName = keyCredential->AccountName; + + Files::DataLake::Models::UserDelegationKey userDelegationKey + = GetDataLakeServiceClientOAuth().GetUserDelegationKey(sasExpiresOn).Value; + + std::string fileName = RandomString(); + + auto dataLakeFileSystemClient = *m_fileSystemClient; + auto dataLakeFileClient = dataLakeFileSystemClient.GetFileClient(fileName); + dataLakeFileClient.Create(); + auto buffer = RandomBuffer(1024); + auto stream = Azure::Core::IO::MemoryBodyStream(buffer); + Files::DataLake::AppendFileOptions appendOptions; + appendOptions.Flush = true; + dataLakeFileClient.Append(stream, 0, appendOptions); + + Sas::DataLakeSasBuilder fileSasBuilder; + fileSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + fileSasBuilder.StartsOn = sasStartsOn; + fileSasBuilder.ExpiresOn = sasExpiresOn; + fileSasBuilder.FileSystemName = m_fileSystemName; + fileSasBuilder.Path = fileName; + fileSasBuilder.Resource = Sas::DataLakeSasResource::File; + + fileSasBuilder.SetPermissions(Sas::DataLakeSasPermissions::All); + + // cSpell:disable + std::map requestHeaders; + requestHeaders["x-ms-range"] = "bytes=0-1023"; + requestHeaders["x-ms-upn"] = "true"; + + std::map requestQueryParameters; + requestQueryParameters["spr"] = "https,http"; + requestQueryParameters["sks"] = "b"; + + fileSasBuilder.RequestHeaders = requestHeaders; + fileSasBuilder.RequestQueryParameters = requestQueryParameters; + auto sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Files::DataLake::DownloadFileOptions downloadOptions; + Core::Http::HttpRange range; + range.Offset = 0; + range.Length = 1024; + downloadOptions.Range = range; + downloadOptions.IncludeUserPrincipalName = true; + + Files::DataLake::DataLakeFileClient fileClient1( + AppendQueryParameters(Azure::Core::Url(dataLakeFileClient.GetUrl()), sasToken), + InitStorageClientOptions()); + EXPECT_NO_THROW(fileClient1.Download(downloadOptions)); + + requestHeaders["foo$"] = "bar!"; + requestHeaders["company"] = "msft"; + requestHeaders["city"] = "redmond,atlanta,reston"; + + requestQueryParameters["hello$"] = "world!"; + requestQueryParameters["abra"] = "cadabra"; + requestQueryParameters["firstName"] = "john,Tim"; + // cSpell:enable + + fileSasBuilder.RequestHeaders = requestHeaders; + fileSasBuilder.RequestQueryParameters = requestQueryParameters; + + sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Files::DataLake::DataLakeFileClient fileClient2( + AppendQueryParameters(Azure::Core::Url(dataLakeFileClient.GetUrl()), sasToken), + InitStorageClientOptions()); + EXPECT_THROW(fileClient2.Download(downloadOptions), StorageException); + } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_service_client_test.cpp b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_service_client_test.cpp index 97eea750a7..7ae9ec365e 100644 --- a/sdk/storage/azure-storage-files-datalake/test/ut/datalake_service_client_test.cpp +++ b/sdk/storage/azure-storage-files-datalake/test/ut/datalake_service_client_test.cpp @@ -328,7 +328,7 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_EQ( downloadedProperties.DefaultServiceVersion.HasValue(), properties.DefaultServiceVersion.HasValue()); - if (downloadedProperties.DefaultServiceVersion.HasValue()) + if (downloadedProperties.DefaultServiceVersion.HasValue() && !m_testContext.IsPlaybackMode()) { EXPECT_EQ( downloadedProperties.DefaultServiceVersion.Value(), diff --git a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/rest_client.hpp b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/rest_client.hpp index 750d288180..091eb35395 100644 --- a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/rest_client.hpp +++ b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/rest_client.hpp @@ -32,7 +32,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { /** * The version used for the operations to Azure storage services. */ - constexpr static const char* ApiVersion = "2026-02-06"; + constexpr static const char* ApiVersion = "2026-04-06"; } // namespace _detail namespace Models { /** @@ -530,6 +530,10 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * The date-time the key expires in ISO 8601 UTC time. */ std::string Expiry; + /** + * The delegated user tenant id in Azure AD. + */ + Nullable DelegatedUserTid; }; } // namespace _detail /** @@ -561,6 +565,10 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * The service version that created the key. */ std::string SignedVersion; + /** + * The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + */ + Nullable SignedDelegatedUserTid; /** * The key as a base64 string. */ @@ -1844,6 +1852,16 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * Detailed information of the downloaded file. */ DownloadFileDetails Details; + /** + * Indicates the response body contains a structured message and specifies the message + * schema version and properties. + */ + Nullable StructuredBodyType; + /** + * The length of the blob/file content inside the message body when the response body is + * returned as a structured message. Will always be smaller than Content-Length. + */ + Nullable StructuredContentLength; }; /** * @brief Response type for #Azure::Storage::Files::Shares::ShareFileClient::GetProperties. @@ -2141,6 +2159,11 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * encrypted using the specified algorithm, and false otherwise. */ bool IsServerEncrypted = false; + /** + * Indicates the structured message body was accepted and mirrors back the message schema + * version and properties. + */ + Nullable StructuredBodyType; }; /** * @brief Response type for #Azure::Storage::Files::Shares::ShareFileClient::UploadRangeFromUri. @@ -2890,6 +2913,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { Nullable AllowTrailingDot; Nullable Range; Nullable RangeGetContentMD5; + Nullable StructuredBodyType; Nullable LeaseId; Nullable FileRequestIntent; }; @@ -3016,6 +3040,8 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { Nullable FileLastWrittenMode; Nullable AllowTrailingDot; Nullable FileRequestIntent; + Nullable StructuredBodyType; + Nullable StructuredContentLength; }; static Response UploadRange( Core::Http::_internal::HttpPipeline& pipeline, diff --git a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_file_client.hpp b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_file_client.hpp index 94026ef923..42e999e93b 100644 --- a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_file_client.hpp +++ b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_file_client.hpp @@ -422,6 +422,10 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { Nullable m_allowTrailingDot; Nullable m_allowSourceTrailingDot; Nullable m_shareTokenIntent; + /** @brief Upload TransferValidationOptions */ + Azure::Nullable m_uploadValidationOptions; + /** @brief Download TransferValidationOptions */ + Azure::Nullable m_downloadValidationOptions; explicit ShareFileClient( Azure::Core::Url shareFileUrl, diff --git a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_options.hpp b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_options.hpp index fd264e6a10..09f29797c8 100644 --- a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_options.hpp +++ b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_options.hpp @@ -258,6 +258,17 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { AZ_STORAGE_FILES_SHARES_DLLEXPORT const static ShareAudience DefaultAudience; }; + /** + * Configures whether to do content validation for blob uploads and downloads. + */ + struct TransferValidationOptions + { + /** + * @brief The algorithm used for storage checksum. + */ + StorageChecksumAlgorithm Algorithm = StorageChecksumAlgorithm::None; + }; + /** * @brief Client options used to initialize share clients. */ @@ -294,6 +305,16 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * Audience is not set. */ Azure::Nullable Audience; + + /** + * @brief Optional. Configures whether to do content validation for file uploads. + */ + Azure::Nullable UploadValidationOptions; + + /** + * @brief Optional. Configures whether to do content validation for file downloads. + */ + Azure::Nullable DownloadValidationOptions; }; /** @@ -356,6 +377,11 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * will be truncated to second. */ Azure::DateTime StartsOn = std::chrono::system_clock::now(); + + /** + * The delegated user tenant id in Azure AD. + */ + Nullable DelegatedUserTid; }; /** @@ -958,6 +984,11 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * The operation will only succeed if the access condition is met. */ LeaseAccessConditions AccessConditions; + + /** + * @brief Optional. Configures whether to do content validation for blob downloads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -1134,6 +1165,11 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * or the last write time currently associated with the file should be preserved. */ Azure::Nullable FileLastWrittenMode; + + /** + * @brief Optional. Configures whether to do content validation for file uploads. + */ + Azure::Nullable ValidationOptions; }; /** @@ -1269,6 +1305,11 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { */ Azure::Nullable Range; + /** + * @brief Optional. Configures whether to do content validation for file downloads. + */ + Azure::Nullable ValidationOptions; + /** * @brief Options for parallel transfer. */ @@ -1334,6 +1375,11 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { */ Models::FilePosixProperties PosixProperties; + /** + * @brief Optional. Configures whether to do content validation for file uploads. + */ + Azure::Nullable ValidationOptions; + /** * @brief Options for parallel transfer. */ diff --git a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_responses.hpp b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_responses.hpp index 9535f9e169..96355c3a27 100644 --- a/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_responses.hpp +++ b/sdk/storage/azure-storage-files-shares/inc/azure/storage/files/shares/share_responses.hpp @@ -159,6 +159,16 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { * MD5 hash for the downloaded range of data. */ Nullable TransactionalContentHash; + /** + * Indicates the response body contains a structured message and specifies the message + * schema version and properties. + */ + Nullable StructuredBodyType; + /** + * The length of the blob/file content inside the message body when the response body is + * returned as a structured message. Will always be smaller than Content-Length. + */ + Nullable StructuredContentLength; /** * Standard HTTP properties supported files. */ diff --git a/sdk/storage/azure-storage-files-shares/src/rest_client.cpp b/sdk/storage/azure-storage-files-shares/src/rest_client.cpp index 72b11fcfbf..a2af4f6b1b 100644 --- a/sdk/storage/azure-storage-files-shares/src/rest_client.cpp +++ b/sdk/storage/azure-storage-files-shares/src/rest_client.cpp @@ -376,7 +376,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileRequestIntent.HasValue() && !options.FileRequestIntent.Value().ToString().empty()) { @@ -401,7 +401,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileRequestIntent.HasValue() && !options.FileRequestIntent.Value().ToString().empty()) { @@ -696,7 +696,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { _internal::UrlEncodeQueryParameter( ListSharesIncludeFlagsToString(options.Include.Value()))); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileRequestIntent.HasValue() && !options.FileRequestIntent.Value().ToString().empty()) { @@ -1137,6 +1137,13 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { } writer.Write( _internal::XmlNode{_internal::XmlNodeType::StartTag, "Expiry", options.KeyInfo.Expiry}); + if (options.KeyInfo.DelegatedUserTid.HasValue()) + { + writer.Write(_internal::XmlNode{ + _internal::XmlNodeType::StartTag, + "DelegatedUserTid", + options.KeyInfo.DelegatedUserTid.Value()}); + } writer.Write(_internal::XmlNode{_internal::XmlNodeType::EndTag}); writer.Write(_internal::XmlNode{_internal::XmlNodeType::End}); xmlBody = writer.GetDocument(); @@ -1148,7 +1155,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "userdelegationkey"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -1170,6 +1177,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { kSignedExpiry, kSignedService, kSignedVersion, + kSignedDelegatedUserTid, kValue, }; const std::unordered_map XmlTagEnumMap{ @@ -1180,6 +1188,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { {"SignedExpiry", XmlTagEnum::kSignedExpiry}, {"SignedService", XmlTagEnum::kSignedService}, {"SignedVersion", XmlTagEnum::kSignedVersion}, + {"SignedDelegatedUserTid", XmlTagEnum::kSignedDelegatedUserTid}, {"Value", XmlTagEnum::kValue}, }; std::vector xmlPath; @@ -1235,6 +1244,12 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { response.SignedVersion = node.Value; } + else if ( + xmlPath.size() == 2 && xmlPath[0] == XmlTagEnum::kUserDelegationKey + && xmlPath[1] == XmlTagEnum::kSignedDelegatedUserTid) + { + response.SignedDelegatedUserTid = node.Value; + } else if ( xmlPath.size() == 2 && xmlPath[0] == XmlTagEnum::kUserDelegationKey && xmlPath[1] == XmlTagEnum::kValue) @@ -1274,7 +1289,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-access-tier", options.AccessTier.Value().ToString()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.EnabledProtocols.HasValue() && !options.EnabledProtocols.Value().ToString().empty()) { @@ -1373,7 +1388,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.GetUrl().AppendQueryParameter( "sharesnapshot", _internal::UrlEncodeQueryParameter(options.Sharesnapshot.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -1530,7 +1545,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.GetUrl().AppendQueryParameter( "sharesnapshot", _internal::UrlEncodeQueryParameter(options.Sharesnapshot.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.DeleteSnapshots.HasValue() && !options.DeleteSnapshots.Value().ToString().empty()) { request.SetHeader("x-ms-delete-snapshots", options.DeleteSnapshots.Value().ToString()); @@ -1581,7 +1596,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-proposed-lease-id", options.ProposedLeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Sharesnapshot.HasValue() && !options.Sharesnapshot.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -1620,7 +1635,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-lease-id", options.LeaseId); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Sharesnapshot.HasValue() && !options.Sharesnapshot.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -1662,7 +1677,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-proposed-lease-id", options.ProposedLeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Sharesnapshot.HasValue() && !options.Sharesnapshot.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -1701,7 +1716,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-lease-id", options.LeaseId); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Sharesnapshot.HasValue() && !options.Sharesnapshot.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -1744,7 +1759,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Sharesnapshot.HasValue() && !options.Sharesnapshot.Value().empty()) { request.GetUrl().AppendQueryParameter( @@ -1782,7 +1797,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-meta-" + p.first, p.second); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileRequestIntent.HasValue() && !options.FileRequestIntent.Value().ToString().empty()) { @@ -1825,7 +1840,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("restype", "share"); request.GetUrl().AppendQueryParameter("comp", "filepermission"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileRequestIntent.HasValue() && !options.FileRequestIntent.Value().ToString().empty()) { @@ -1861,7 +1876,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader( "x-ms-file-permission-format", options.FilePermissionFormat.Value().ToString()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileRequestIntent.HasValue() && !options.FileRequestIntent.Value().ToString().empty()) { @@ -1896,7 +1911,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("restype", "share"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Quota.HasValue()) { request.SetHeader("x-ms-share-quota", std::to_string(options.Quota.Value())); @@ -2027,7 +2042,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-meta-" + p.first, p.second); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -2058,7 +2073,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "share"); request.GetUrl().AppendQueryParameter("comp", "acl"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -2210,7 +2225,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("restype", "share"); request.GetUrl().AppendQueryParameter("comp", "acl"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -2242,7 +2257,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "share"); request.GetUrl().AppendQueryParameter("comp", "stats"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -2333,7 +2348,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-meta-" + p.first, p.second); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FilePermission.HasValue() && !options.FilePermission.Value().empty()) { request.SetHeader("x-ms-file-permission", options.FilePermission.Value()); @@ -2461,7 +2476,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.GetUrl().AppendQueryParameter( "sharesnapshot", _internal::UrlEncodeQueryParameter(options.Sharesnapshot.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileRequestIntent.HasValue() && !options.FileRequestIntent.Value().ToString().empty()) { @@ -2548,7 +2563,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader( "x-ms-allow-trailing-dot", options.AllowTrailingDot.Value() ? "true" : "false"); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileRequestIntent.HasValue() && !options.FileRequestIntent.Value().ToString().empty()) { @@ -2572,7 +2587,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("restype", "directory"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FilePermission.HasValue() && !options.FilePermission.Value().empty()) { request.SetHeader("x-ms-file-permission", options.FilePermission.Value()); @@ -2695,7 +2710,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-meta-" + p.first, p.second); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.AllowTrailingDot.HasValue()) { request.SetHeader( @@ -2749,7 +2764,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.GetUrl().AppendQueryParameter( "maxresults", std::to_string(options.MaxResults.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Include.HasValue() && !ListFilesIncludeFlagsToString(options.Include.Value()).empty()) { @@ -3142,7 +3157,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-recursive", options.Recursive.Value() ? "true" : "false"); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.AllowTrailingDot.HasValue()) { request.SetHeader( @@ -3355,7 +3370,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-recursive", options.Recursive.Value() ? "true" : "false"); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.AllowTrailingDot.HasValue()) { request.SetHeader( @@ -3393,7 +3408,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("restype", "directory"); request.GetUrl().AppendQueryParameter("comp", "rename"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (!options.RenameSource.empty()) { request.SetHeader("x-ms-file-rename-source", options.RenameSource); @@ -3507,7 +3522,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader( "x-ms-allow-trailing-dot", options.AllowTrailingDot.Value() ? "true" : "false"); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); request.SetHeader("x-ms-content-length", std::to_string(options.FileContentLength)); request.SetHeader("x-ms-type", "file"); if (options.FileContentType.HasValue() && !options.FileContentType.Value().empty()) @@ -3670,7 +3685,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader( "x-ms-allow-trailing-dot", options.AllowTrailingDot.Value() ? "true" : "false"); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Range.HasValue() && !options.Range.Value().empty()) { request.SetHeader("x-ms-range", options.Range.Value()); @@ -3680,6 +3695,10 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader( "x-ms-range-get-content-md5", options.RangeGetContentMD5.Value() ? "true" : "false"); } + if (options.StructuredBodyType.HasValue() && !options.StructuredBodyType.Value().empty()) + { + request.SetHeader("x-ms-structured-body", options.StructuredBodyType.Value()); + } if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -3829,6 +3848,15 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { response.Details.LeaseStatus = Models::LeaseStatus(pRawResponse->GetHeaders().at("x-ms-lease-status")); } + if (pRawResponse->GetHeaders().count("x-ms-structured-body") != 0) + { + response.StructuredBodyType = pRawResponse->GetHeaders().at("x-ms-structured-body"); + } + if (pRawResponse->GetHeaders().count("x-ms-structured-content-length") != 0) + { + response.StructuredContentLength + = std::stoll(pRawResponse->GetHeaders().at("x-ms-structured-content-length")); + } if (pRawResponse->GetHeaders().count("x-ms-mode") != 0) { response.Details.FileMode = pRawResponse->GetHeaders().at("x-ms-mode"); @@ -3875,7 +3903,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.GetUrl().AppendQueryParameter( "sharesnapshot", _internal::UrlEncodeQueryParameter(options.Sharesnapshot.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -4038,7 +4066,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.SetHeader( "x-ms-allow-trailing-dot", options.AllowTrailingDot.Value() ? "true" : "false"); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -4069,7 +4097,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileContentLength.HasValue()) { request.SetHeader("x-ms-content-length", std::to_string(options.FileContentLength.Value())); @@ -4230,7 +4258,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-meta-" + p.first, p.second); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -4274,7 +4302,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-proposed-lease-id", options.ProposedLeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.AllowTrailingDot.HasValue()) { request.SetHeader( @@ -4312,7 +4340,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-lease-id", options.LeaseId); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.AllowTrailingDot.HasValue()) { request.SetHeader( @@ -4353,7 +4381,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-proposed-lease-id", options.ProposedLeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.AllowTrailingDot.HasValue()) { request.SetHeader( @@ -4391,7 +4419,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.AllowTrailingDot.HasValue()) { request.SetHeader( @@ -4438,7 +4466,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("Content-MD5", Core::Convert::Base64Encode(options.ContentMD5.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -4459,6 +4487,16 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-file-request-intent", options.FileRequestIntent.Value().ToString()); } + if (options.StructuredBodyType.HasValue() && !options.StructuredBodyType.Value().empty()) + { + request.SetHeader("x-ms-structured-body", options.StructuredBodyType.Value()); + } + if (options.StructuredContentLength.HasValue()) + { + request.SetHeader( + "x-ms-structured-content-length", + std::to_string(options.StructuredContentLength.Value())); + } auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -4480,6 +4518,10 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { response.IsServerEncrypted = pRawResponse->GetHeaders().at("x-ms-request-server-encrypted") == std::string("true"); } + if (pRawResponse->GetHeaders().count("x-ms-structured-body") != 0) + { + response.StructuredBodyType = pRawResponse->GetHeaders().at("x-ms-structured-body"); + } return Response(std::move(response), std::move(pRawResponse)); } Response FileClient::UploadRangeFromUri( @@ -4525,7 +4567,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { "x-ms-source-if-none-match-crc64", Core::Convert::Base64Encode(options.SourceIfNoneMatchCrc64.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -4601,7 +4643,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { "prevsharesnapshot", _internal::UrlEncodeQueryParameter(options.Prevsharesnapshot.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.Range.HasValue() && !options.Range.Value().empty()) { request.SetHeader("x-ms-range", options.Range.Value()); @@ -4730,7 +4772,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { const Core::Context& context) { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); for (const auto& p : options.Metadata) { request.SetHeader("x-ms-meta-" + p.first, p.second); @@ -4857,7 +4899,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { "copyid", _internal::UrlEncodeQueryParameter(options.CopyId)); } request.SetHeader("x-ms-copy-action", "abort"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { request.SetHeader("x-ms-lease-id", options.LeaseId.Value()); @@ -4904,7 +4946,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.GetUrl().AppendQueryParameter( "sharesnapshot", _internal::UrlEncodeQueryParameter(options.Sharesnapshot.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.AllowTrailingDot.HasValue()) { request.SetHeader( @@ -5113,7 +5155,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { request.SetHeader("x-ms-handle-id", options.HandleId); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.AllowTrailingDot.HasValue()) { request.SetHeader( @@ -5150,7 +5192,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("comp", "rename"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (!options.RenameSource.empty()) { request.SetHeader("x-ms-file-rename-source", options.RenameSource); @@ -5264,7 +5306,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("restype", "symboliclink"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); for (const auto& p : options.Metadata) { request.SetHeader("x-ms-meta-" + p.first, p.second); @@ -5349,7 +5391,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { request.GetUrl().AppendQueryParameter( "sharesnapshot", _internal::UrlEncodeQueryParameter(options.Sharesnapshot.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); if (options.FileRequestIntent.HasValue() && !options.FileRequestIntent.Value().ToString().empty()) { @@ -5377,7 +5419,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { { auto request = Core::Http::Request(Core::Http::HttpMethod::Put, url); request.GetUrl().AppendQueryParameter("restype", "hardlink"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); request.SetHeader("x-ms-type", "file"); if (options.LeaseId.HasValue() && !options.LeaseId.Value().empty()) { diff --git a/sdk/storage/azure-storage-files-shares/src/share_file_client.cpp b/sdk/storage/azure-storage-files-shares/src/share_file_client.cpp index 572ac03327..899570dcae 100644 --- a/sdk/storage/azure-storage-files-shares/src/share_file_client.cpp +++ b/sdk/storage/azure-storage-files-shares/src/share_file_client.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include @@ -51,7 +53,9 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { const ShareClientOptions& options) : m_shareFileUrl(shareFileUrl), m_allowTrailingDot(options.AllowTrailingDot), m_allowSourceTrailingDot(options.AllowSourceTrailingDot), - m_shareTokenIntent(options.ShareTokenIntent) + m_shareTokenIntent(options.ShareTokenIntent), + m_uploadValidationOptions(options.UploadValidationOptions), + m_downloadValidationOptions(options.DownloadValidationOptions) { ShareClientOptions newOptions = options; newOptions.PerRetryPolicies.emplace_back( @@ -76,7 +80,9 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { const ShareClientOptions& options) : m_shareFileUrl(shareFileUrl), m_allowTrailingDot(options.AllowTrailingDot), m_allowSourceTrailingDot(options.AllowSourceTrailingDot), - m_shareTokenIntent(options.ShareTokenIntent) + m_shareTokenIntent(options.ShareTokenIntent), + m_uploadValidationOptions(options.UploadValidationOptions), + m_downloadValidationOptions(options.DownloadValidationOptions) { ShareClientOptions newOptions = options; @@ -108,7 +114,9 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { const ShareClientOptions& options) : m_shareFileUrl(shareFileUrl), m_allowTrailingDot(options.AllowTrailingDot), m_allowSourceTrailingDot(options.AllowSourceTrailingDot), - m_shareTokenIntent(options.ShareTokenIntent) + m_shareTokenIntent(options.ShareTokenIntent), + m_uploadValidationOptions(options.UploadValidationOptions), + m_downloadValidationOptions(options.DownloadValidationOptions) { std::vector> perRetryPolicies; std::vector> perOperationPolicies; @@ -272,6 +280,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { const DownloadFileOptions& options, const Azure::Core::Context& context) const { + bool isStructuredMessage = false; auto protocolLayerOptions = _detail::FileClient::DownloadFileOptions(); if (options.Range.HasValue()) { @@ -298,6 +307,18 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { protocolLayerOptions.RangeGetContentMD5 = true; } } + else + { + Azure::Nullable validationOptions + = options.ValidationOptions.HasValue() ? options.ValidationOptions + : m_downloadValidationOptions; + if (validationOptions.HasValue() + && validationOptions.Value().Algorithm != StorageChecksumAlgorithm::None) + { + isStructuredMessage = true; + protocolLayerOptions.StructuredBodyType = _internal::CrcStructuredMessage; + } + } protocolLayerOptions.LeaseId = options.AccessConditions.LeaseId; protocolLayerOptions.AllowTrailingDot = m_allowTrailingDot; protocolLayerOptions.FileRequestIntent = m_shareTokenIntent; @@ -338,13 +359,40 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { _internal::ReliableStreamOptions reliableStreamOptions; reliableStreamOptions.MaxRetryRequests = _internal::ReliableStreamRetryCount; - downloadResponse.Value.BodyStream = std::make_unique<_internal::ReliableStream>( + auto reliableStream = std::make_unique<_internal::ReliableStream>( std::move(downloadResponse.Value.BodyStream), reliableStreamOptions, retryFunction); + if (isStructuredMessage) + { + _internal::StructuredMessageDecodingStreamOptions decodingOptions; + if (downloadResponse.Value.StructuredContentLength.HasValue()) + { + decodingOptions.ContentLength = downloadResponse.Value.StructuredContentLength.Value(); + } + downloadResponse.Value.BodyStream + = std::make_unique<_internal::StructuredMessageDecodingStream>( + std::move(reliableStream), decodingOptions); + } + else + { + downloadResponse.Value.BodyStream = std::move(reliableStream); + } } if (downloadResponse.RawResponse->GetStatusCode() == Azure::Core::Http::HttpStatusCode::Ok) { - downloadResponse.Value.FileSize = std::stoll( - downloadResponse.RawResponse->GetHeaders().at(_internal::HttpHeaderContentLength)); + if (isStructuredMessage) + { + if (!downloadResponse.Value.StructuredContentLength.HasValue()) + { + throw StorageException( + "Structured message response without x-ms-structured-content-length header."); + } + downloadResponse.Value.FileSize = downloadResponse.Value.StructuredContentLength.Value(); + } + else + { + downloadResponse.Value.FileSize = std::stoll( + downloadResponse.RawResponse->GetHeaders().at(_internal::HttpHeaderContentLength)); + } downloadResponse.Value.ContentRange.Offset = 0; downloadResponse.Value.ContentRange.Length = downloadResponse.Value.FileSize; } @@ -371,6 +419,8 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { result.FileSize = downloadResponse.Value.FileSize; result.HttpHeaders = std::move(downloadResponse.Value.HttpHeaders); result.TransactionalContentHash = std::move(downloadResponse.Value.TransactionalContentHash); + result.StructuredBodyType = std::move(downloadResponse.Value.StructuredBodyType); + result.StructuredContentLength = downloadResponse.Value.StructuredContentLength; result.Details.CopyCompletedOn = std::move(downloadResponse.Value.Details.CopyCompletedOn); result.Details.CopyId = std::move(downloadResponse.Value.Details.CopyId); result.Details.CopyProgress = std::move(downloadResponse.Value.Details.CopyProgress); @@ -726,6 +776,10 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { protocolLayerOptions.FileRangeWrite = "update"; protocolLayerOptions.Range = std::string("bytes=") + std::to_string(offset) + std::string("-") + std::to_string(offset + content.Length() - 1); + protocolLayerOptions.LeaseId = options.AccessConditions.LeaseId; + protocolLayerOptions.FileLastWrittenMode = options.FileLastWrittenMode; + protocolLayerOptions.AllowTrailingDot = m_allowTrailingDot; + protocolLayerOptions.FileRequestIntent = m_shareTokenIntent; if (options.TransactionalContentHash.HasValue()) { AZURE_ASSERT_MSG( @@ -733,10 +787,24 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { "This operation only supports MD5 content hash."); protocolLayerOptions.ContentMD5 = options.TransactionalContentHash.Value().Value; } - protocolLayerOptions.LeaseId = options.AccessConditions.LeaseId; - protocolLayerOptions.FileLastWrittenMode = options.FileLastWrittenMode; - protocolLayerOptions.AllowTrailingDot = m_allowTrailingDot; - protocolLayerOptions.FileRequestIntent = m_shareTokenIntent; + else + { + Azure::Nullable validationOptions + = options.ValidationOptions.HasValue() ? options.ValidationOptions + : m_uploadValidationOptions; + if (validationOptions.HasValue() + && validationOptions.Value().Algorithm != StorageChecksumAlgorithm::None) + { + protocolLayerOptions.StructuredBodyType = _internal::CrcStructuredMessage; + protocolLayerOptions.StructuredContentLength = content.Length(); + _internal::StructuredMessageEncodingStreamOptions encodingStreamOptions; + encodingStreamOptions.Flags = _internal::StructuredMessageFlags::Crc64; + auto structuredContent + = _internal::StructuredMessageEncodingStream(&content, encodingStreamOptions); + return _detail::FileClient::UploadRange( + *m_pipeline, m_shareFileUrl, structuredContent, protocolLayerOptions, context); + } + } return _detail::FileClient::UploadRange( *m_pipeline, m_shareFileUrl, content, protocolLayerOptions, context); } @@ -946,6 +1014,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { DownloadFileOptions firstChunkOptions; firstChunkOptions.Range = options.Range; + firstChunkOptions.ValidationOptions = options.ValidationOptions; if (firstChunkOptions.Range.HasValue()) { firstChunkOptions.Range.Value().Length = firstChunkLength; @@ -1004,6 +1073,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { chunkOptions.Range = Core::Http::HttpRange(); chunkOptions.Range.Value().Offset = offset; chunkOptions.Range.Value().Length = length; + chunkOptions.ValidationOptions = options.ValidationOptions; auto chunk = Download(chunkOptions, context); int64_t bytesRead = chunk.Value.BodyStream->ReadToCount( buffer + (offset - firstChunkOffset), @@ -1056,6 +1126,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { DownloadFileOptions firstChunkOptions; firstChunkOptions.Range = options.Range; + firstChunkOptions.ValidationOptions = options.ValidationOptions; if (firstChunkOptions.Range.HasValue()) { firstChunkOptions.Range.Value().Length = firstChunkLength; @@ -1124,6 +1195,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { chunkOptions.Range = Core::Http::HttpRange(); chunkOptions.Range.Value().Offset = offset; chunkOptions.Range.Value().Length = length; + chunkOptions.ValidationOptions = options.ValidationOptions; auto chunk = Download(chunkOptions, context); if (chunk.Value.Details.ETag != etag) { @@ -1248,6 +1320,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { uploadRangeOptions.FileLastWrittenMode = Azure::Storage::Files::Shares::Models::FileLastWrittenMode::Preserve; } + uploadRangeOptions.ValidationOptions = options.ValidationOptions; UploadRange(offset, contentStream, uploadRangeOptions, context); }; @@ -1360,6 +1433,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { uploadRangeOptions.FileLastWrittenMode = Azure::Storage::Files::Shares::Models::FileLastWrittenMode::Preserve; } + uploadRangeOptions.ValidationOptions = options.ValidationOptions; UploadRange(offset, contentStream, uploadRangeOptions, context); }; diff --git a/sdk/storage/azure-storage-files-shares/src/share_sas_builder.cpp b/sdk/storage/azure-storage-files-shares/src/share_sas_builder.cpp index b0f3d59b35..0f1ba85c45 100644 --- a/sdk/storage/azure-storage-files-shares/src/share_sas_builder.cpp +++ b/sdk/storage/azure-storage-files-shares/src/share_sas_builder.cpp @@ -8,7 +8,7 @@ #include #include -/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, sduoid */ +/* cSpell:ignore rscc, rscd, rsce, rscl, rsct, skoid, sktid, skdutid, sduoid */ namespace Azure { namespace Storage { namespace Sas { @@ -185,8 +185,11 @@ namespace Azure { namespace Storage { namespace Sas { std::string stringToSign = Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr - + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n\n" - + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; @@ -224,6 +227,12 @@ namespace Azure { namespace Storage { namespace Sas { "sks", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedService)); builder.AppendQueryParameter( "skv", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedVersion)); + if (userDelegationKey.SignedDelegatedUserTid.HasValue()) + { + builder.AppendQueryParameter( + "skdutid", + _internal::UrlEncodeQueryParameter(userDelegationKey.SignedDelegatedUserTid.Value())); + } if (!DelegatedUserObjectId.empty()) { builder.AppendQueryParameter( @@ -307,10 +316,13 @@ namespace Azure { namespace Storage { namespace Sas { return Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService - + "\n" + userDelegationKey.SignedVersion + "\n\n" + DelegatedUserObjectId + "\n" - + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion + "\n" - + CacheControl + "\n" + ContentDisposition + "\n" + ContentEncoding + "\n" + ContentLanguage - + "\n" + ContentType; + + "\n" + userDelegationKey.SignedVersion + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + protocol + "\n" + SasVersion + "\n" + CacheControl + "\n" + ContentDisposition + "\n" + + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentType; } }}} // namespace Azure::Storage::Sas diff --git a/sdk/storage/azure-storage-files-shares/src/share_service_client.cpp b/sdk/storage/azure-storage-files-shares/src/share_service_client.cpp index 3b3a7a63cd..c7d7bc0699 100644 --- a/sdk/storage/azure-storage-files-shares/src/share_service_client.cpp +++ b/sdk/storage/azure-storage-files-shares/src/share_service_client.cpp @@ -194,6 +194,7 @@ namespace Azure { namespace Storage { namespace Files { namespace Shares { Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); protocolLayerOptions.KeyInfo.Expiry = expiresOn.ToString( Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); + protocolLayerOptions.KeyInfo.DelegatedUserTid = options.DelegatedUserTid; return _detail::ServiceClient::GetUserDelegationKey( *m_pipeline, m_serviceUrl, protocolLayerOptions, context); } diff --git a/sdk/storage/azure-storage-files-shares/swagger/README.md b/sdk/storage/azure-storage-files-shares/swagger/README.md index 95ca2423e9..0b1a8c3722 100644 --- a/sdk/storage/azure-storage-files-shares/swagger/README.md +++ b/sdk/storage/azure-storage-files-shares/swagger/README.md @@ -9,7 +9,7 @@ package-name: azure-storage-files-shares namespace: Azure::Storage::Files::Shares output-folder: generated clear-output-folder: true -input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/refs/heads/main/specification/storage/data-plane/Microsoft.FileStorage/stable/2026-02-06/file.json +input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/refs/heads/feature/storage/stg101base/specification/storage/data-plane/Microsoft.FileStorage/stable/2026-04-06/file.json ``` ## ModelFour Options @@ -43,13 +43,12 @@ directive: - from: swagger-document where: $["x-ms-paths"].*.*.parameters transform: > - $ = $.filter(p => !(p["$ref"] && (p["$ref"].endsWith("#/parameters/Timeout") || p["$ref"].endsWith("#/parameters/ClientRequestId") - || p["$ref"].endsWith("#/parameters/StructuredBodyGet") || p["$ref"].endsWith("#/parameters/StructuredBodyPut") || p["$ref"].endsWith("#/parameters/StructuredContentLength")))); + $ = $.filter(p => !(p["$ref"] && (p["$ref"].endsWith("#/parameters/Timeout") || p["$ref"].endsWith("#/parameters/ClientRequestId")))); - from: swagger-document where: $["x-ms-paths"].*.*.responses.*.headers transform: > for (const h in $) { - if (["x-ms-client-request-id", "x-ms-request-id", "x-ms-version", "Date", "x-ms-structured-body", "x-ms-structured-content-length"].includes(h)) { + if (["x-ms-client-request-id", "x-ms-request-id", "x-ms-version", "Date"].includes(h)) { delete $[h]; } } @@ -80,12 +79,12 @@ directive: "name": "ApiVersion", "modelAsString": false }, - "enum": ["2026-02-06"] + "enum": ["2026-04-06"] }; - from: swagger-document where: $.parameters transform: > - $.ApiVersionParameter.enum[0] = "2026-02-06"; + $.ApiVersionParameter.enum[0] = "2026-04-06"; ``` ### Rename Operations @@ -361,6 +360,9 @@ directive: if (header === "x-ms-meta") { $[header]["x-ms-format"] = "caseinsensitivemap"; } + if (header === "x-ms-structured-body" || header === "x-ms-structured-content-length") { + $[header]["x-nullable"] = true; + } } ``` diff --git a/sdk/storage/azure-storage-files-shares/test/ut/share_file_client_test.cpp b/sdk/storage/azure-storage-files-shares/test/ut/share_file_client_test.cpp index 78bacb26c0..6eb000847f 100644 --- a/sdk/storage/azure-storage-files-shares/test/ut/share_file_client_test.cpp +++ b/sdk/storage/azure-storage-files-shares/test/ut/share_file_client_test.cpp @@ -2608,4 +2608,87 @@ namespace Azure { namespace Storage { namespace Test { destFileClient = shareClient.GetRootDirectoryClient().GetFileClient(LowercaseRandomString()); EXPECT_THROW(destFileClient.StartCopy(sourceClient.GetUrl(), copyOptions), StorageException); } + + TEST_F(FileShareFileClientTest, StructuredMessageTest) + { + const size_t contentSize = 2 * 1024 + 512; + auto content = RandomBuffer(contentSize); + auto bodyStream = Azure::Core::IO::MemoryBodyStream(content.data(), content.size()); + const std::string tempFileName = RandomString(); + WriteFile(tempFileName, content); + Files::Shares::TransferValidationOptions validationOptions; + validationOptions.Algorithm = StorageChecksumAlgorithm::Crc64; + + // UploadRange + auto fileClient = m_fileShareDirectoryClient->GetFileClient("uploadrange_" + RandomString()); + fileClient.Create(contentSize); + Files::Shares::UploadFileRangeOptions uploadRangeOptions; + uploadRangeOptions.ValidationOptions = validationOptions; + Files::Shares::Models::UploadFileRangeResult uploadRangeResult; + EXPECT_NO_THROW( + uploadRangeResult = fileClient.UploadRange(0, bodyStream, uploadRangeOptions).Value); + EXPECT_TRUE(uploadRangeResult.StructuredBodyType.HasValue()); + + // Download + Files::Shares::DownloadFileOptions downloadOptions; + downloadOptions.ValidationOptions = validationOptions; + Files::Shares::Models::DownloadFileResult downloadResult; + EXPECT_NO_THROW(downloadResult = fileClient.Download(downloadOptions).Value); + auto downloadedData = downloadResult.BodyStream->ReadToEnd(); + EXPECT_EQ(content, downloadedData); + EXPECT_TRUE(downloadResult.StructuredContentLength.HasValue()); + EXPECT_EQ(downloadResult.StructuredContentLength.Value(), contentSize); + EXPECT_TRUE(downloadResult.StructuredBodyType.HasValue()); + EXPECT_EQ(downloadResult.FileSize, contentSize); + // partial download + downloadOptions.Range = Core::Http::HttpRange(); + downloadOptions.Range.Value().Length = contentSize / 2; + EXPECT_NO_THROW(downloadResult = fileClient.Download(downloadOptions).Value); + downloadedData = downloadResult.BodyStream->ReadToEnd(); + EXPECT_EQ( + downloadedData, std::vector(content.begin(), content.begin() + contentSize / 2)); + EXPECT_TRUE(downloadResult.StructuredContentLength.HasValue()); + EXPECT_EQ(downloadResult.StructuredContentLength.Value(), contentSize / 2); + EXPECT_TRUE(downloadResult.StructuredBodyType.HasValue()); + EXPECT_EQ(downloadResult.FileSize, contentSize); + downloadOptions.Range.Reset(); + + // UploadFrom DownloadTo + Files::Shares::UploadFileFromOptions uploadFromOptions; + uploadFromOptions.ValidationOptions = validationOptions; + Files::Shares::Models::UploadFileFromResult uploadFromResult; + Files::Shares::DownloadFileToOptions downloadToOptions; + downloadToOptions.ValidationOptions = validationOptions; + Files::Shares::Models::DownloadFileToResult downloadToResult; + + // From stream + fileClient = m_fileShareDirectoryClient->GetFileClient("uploadfromstream_" + RandomString()); + EXPECT_NO_THROW( + uploadFromResult + = fileClient.UploadFrom(content.data(), contentSize, uploadFromOptions).Value); + auto downloadBuffer = std::vector(contentSize, '\x00'); + EXPECT_NO_THROW( + downloadToResult + = fileClient.DownloadTo(downloadBuffer.data(), contentSize, downloadToOptions).Value); + EXPECT_EQ(downloadBuffer, content); + // partial downloadTo + downloadToOptions.Range = Core::Http::HttpRange(); + downloadToOptions.Range.Value().Length = contentSize / 2; + downloadBuffer.resize(static_cast(contentSize / 2), '\x00'); + EXPECT_NO_THROW( + downloadToResult + = fileClient.DownloadTo(downloadBuffer.data(), contentSize / 2, downloadToOptions).Value); + EXPECT_EQ( + downloadBuffer, std::vector(content.begin(), content.begin() + contentSize / 2)); + downloadToOptions.Range.Reset(); + + // From file + fileClient = m_fileShareDirectoryClient->GetFileClient("uploadfromfile_" + RandomString()); + EXPECT_NO_THROW( + uploadFromResult = fileClient.UploadFrom(tempFileName, uploadFromOptions).Value); + auto downloadFileName = RandomString(); + EXPECT_NO_THROW( + downloadToResult = fileClient.DownloadTo(downloadFileName, downloadToOptions).Value); + } + }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-files-shares/test/ut/share_sas_test.cpp b/sdk/storage/azure-storage-files-shares/test/ut/share_sas_test.cpp index 6b452fedb2..ee0cf5357b 100644 --- a/sdk/storage/azure-storage-files-shares/test/ut/share_sas_test.cpp +++ b/sdk/storage/azure-storage-files-shares/test/ut/share_sas_test.cpp @@ -753,7 +753,7 @@ namespace Azure { namespace Storage { namespace Test { return {}; } - TEST_F(ShareSasTest, DISABLED_PrincipalBoundDelegationSas) + TEST_F(ShareSasTest, PrincipalBoundDelegationSas_LIVEONLY_) { auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); @@ -791,10 +791,12 @@ namespace Azure { namespace Storage { namespace Test { fileSasBuilder.SetPermissions(Sas::ShareFileSasPermissions::All); auto sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + auto fileOptions = InitStorageClientOptions(); + fileOptions.ShareTokenIntent = Files::Shares::Models::ShareTokenIntent::Backup; Files::Shares::ShareFileClient fileClient1( AppendQueryParameters(Azure::Core::Url(fileClient.GetUrl()), sasToken), GetTestCredential(), - InitStorageClientOptions()); + fileOptions); EXPECT_NO_THROW(fileClient1.GetProperties()); fileSasBuilder.DelegatedUserObjectId = "invalidObjectId"; @@ -802,7 +804,73 @@ namespace Azure { namespace Storage { namespace Test { Files::Shares::ShareFileClient fileClient2( AppendQueryParameters(Azure::Core::Url(fileClient.GetUrl()), sasToken), GetTestCredential(), + fileOptions); + EXPECT_THROW(fileClient2.GetProperties(), StorageException); + } + + TEST_F(ShareSasTest, DISABLED_PrincipalBoundDelegationSas_CrossTenant) + { + auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); + auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); + + std::string fileName = RandomString(); + + auto keyCredential + = _internal::ParseConnectionString(StandardStorageConnectionString()).KeyCredential; + auto accountName = keyCredential->AccountName; + Azure::Identity::ClientSecretCredentialOptions credentialOptions; + credentialOptions.AdditionallyAllowedTenants = {"*"}; + auto endUserCredential = std::make_shared( + GetEnv("AZURE_TENANT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_SECRET_CROSS_TENANT")); + auto delegatedUserObjectId = getObjectIdFromTokenCredential(endUserCredential); + + auto shareServiceClient = Files::Shares::ShareServiceClient( + m_shareServiceClient->GetUrl(), + GetTestCredential(), InitStorageClientOptions()); + Files::Shares::Models::UserDelegationKey userDelegationKey; + { + Files::Shares::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "4ab3a968-f1ae-47a6-b82c-f654612122a9"; + userDelegationKey = shareServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + + Sas::ShareSasBuilder fileSasBuilder; + fileSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + fileSasBuilder.StartsOn = sasStartsOn; + fileSasBuilder.ExpiresOn = sasExpiresOn; + fileSasBuilder.ShareName = m_shareName; + fileSasBuilder.FilePath = fileName; + fileSasBuilder.Resource = Sas::ShareSasResource::File; + fileSasBuilder.DelegatedUserObjectId = delegatedUserObjectId; + + auto shareClient = *m_shareClient; + auto fileClient = shareClient.GetRootDirectoryClient().GetFileClient(fileName); + fileClient.Create(1); + + auto fileOptions = InitStorageClientOptions(); + fileOptions.ShareTokenIntent = Files::Shares::Models::ShareTokenIntent::Backup; + fileSasBuilder.SetPermissions(Sas::ShareFileSasPermissions::All); + auto sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Files::Shares::ShareFileClient fileClient1( + AppendQueryParameters(Azure::Core::Url(fileClient.GetUrl()), sasToken), + endUserCredential, + fileOptions); + EXPECT_NO_THROW(fileClient1.GetProperties()); + + { + Files::Shares::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "00000000-0000-0000-0000-000000000000"; + userDelegationKey = shareServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + sasToken = fileSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Files::Shares::ShareFileClient fileClient2( + AppendQueryParameters(Azure::Core::Url(fileClient.GetUrl()), sasToken), + endUserCredential, + fileOptions); EXPECT_THROW(fileClient2.GetProperties(), StorageException); } }}} // namespace Azure::Storage::Test diff --git a/sdk/storage/azure-storage-queues/inc/azure/storage/queues/queue_options.hpp b/sdk/storage/azure-storage-queues/inc/azure/storage/queues/queue_options.hpp index f20f54224a..3a10cf22fc 100644 --- a/sdk/storage/azure-storage-queues/inc/azure/storage/queues/queue_options.hpp +++ b/sdk/storage/azure-storage-queues/inc/azure/storage/queues/queue_options.hpp @@ -208,6 +208,11 @@ namespace Azure { namespace Storage { namespace Queues { * will be truncated to second. */ Azure::DateTime StartsOn = std::chrono::system_clock::now(); + + /** + * The delegated user tenant id in Azure AD. + */ + Nullable DelegatedUserTid; }; /** diff --git a/sdk/storage/azure-storage-queues/inc/azure/storage/queues/rest_client.hpp b/sdk/storage/azure-storage-queues/inc/azure/storage/queues/rest_client.hpp index 2c9a4f581a..8f4fd815d5 100644 --- a/sdk/storage/azure-storage-queues/inc/azure/storage/queues/rest_client.hpp +++ b/sdk/storage/azure-storage-queues/inc/azure/storage/queues/rest_client.hpp @@ -27,7 +27,7 @@ namespace Azure { namespace Storage { namespace Queues { /** * The version used for the operations to Azure storage services. */ - constexpr static const char* ApiVersion = "2026-02-06"; + constexpr static const char* ApiVersion = "2026-04-06"; } // namespace _detail namespace Models { /** @@ -215,6 +215,10 @@ namespace Azure { namespace Storage { namespace Queues { * The date-time the key expires in ISO 8601 UTC time. */ std::string Expiry; + /** + * The delegated user tenant id in Azure AD. + */ + Nullable DelegatedUserTid; }; } // namespace _detail /** @@ -246,6 +250,10 @@ namespace Azure { namespace Storage { namespace Queues { * The service version that created the key. */ std::string SignedVersion; + /** + * The delegated user tenant id in Azure AD. Return if DelegatedUserTid is specified. + */ + Nullable SignedDelegatedUserTid; /** * The key as a base64 string. */ diff --git a/sdk/storage/azure-storage-queues/src/queue_sas_builder.cpp b/sdk/storage/azure-storage-queues/src/queue_sas_builder.cpp index 72f1ddd8d2..b0e444784f 100644 --- a/sdk/storage/azure-storage-queues/src/queue_sas_builder.cpp +++ b/sdk/storage/azure-storage-queues/src/queue_sas_builder.cpp @@ -8,7 +8,7 @@ #include #include -/* cSpell:ignore skoid, sktid, sduoid */ +/* cSpell:ignore skoid, sktid, skdutid, sduoid */ namespace Azure { namespace Storage { namespace Sas { @@ -113,8 +113,11 @@ namespace Azure { namespace Storage { namespace Sas { std::string stringToSign = Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr - + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n\n" - + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + "\n" + userDelegationKey.SignedService + "\n" + userDelegationKey.SignedVersion + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion; std::string signature = Azure::Core::Convert::Base64Encode(_internal::HmacSha256( @@ -150,6 +153,12 @@ namespace Azure { namespace Storage { namespace Sas { "sks", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedService)); builder.AppendQueryParameter( "skv", _internal::UrlEncodeQueryParameter(userDelegationKey.SignedVersion)); + if (userDelegationKey.SignedDelegatedUserTid.HasValue()) + { + builder.AppendQueryParameter( + "skdutid", + _internal::UrlEncodeQueryParameter(userDelegationKey.SignedDelegatedUserTid.Value())); + } if (!DelegatedUserObjectId.empty()) { builder.AppendQueryParameter( @@ -204,7 +213,11 @@ namespace Azure { namespace Storage { namespace Sas { return Permissions + "\n" + startsOnStr + "\n" + expiresOnStr + "\n" + canonicalName + "\n" + userDelegationKey.SignedObjectId + "\n" + userDelegationKey.SignedTenantId + "\n" + signedStartsOnStr + "\n" + signedExpiresOnStr + "\n" + userDelegationKey.SignedService - + "\n" + userDelegationKey.SignedVersion + "\n\n" + DelegatedUserObjectId + "\n" - + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + protocol + "\n" + SasVersion; + + "\n" + userDelegationKey.SignedVersion + "\n" + + (userDelegationKey.SignedDelegatedUserTid.HasValue() + ? userDelegationKey.SignedDelegatedUserTid.Value() + : "") + + "\n" + DelegatedUserObjectId + "\n" + (IPRange.HasValue() ? IPRange.Value() : "") + "\n" + + protocol + "\n" + SasVersion; } }}} // namespace Azure::Storage::Sas diff --git a/sdk/storage/azure-storage-queues/src/queue_service_client.cpp b/sdk/storage/azure-storage-queues/src/queue_service_client.cpp index 480ba9a685..acbcbdb059 100644 --- a/sdk/storage/azure-storage-queues/src/queue_service_client.cpp +++ b/sdk/storage/azure-storage-queues/src/queue_service_client.cpp @@ -203,6 +203,7 @@ namespace Azure { namespace Storage { namespace Queues { Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); protocolLayerOptions.KeyInfo.Expiry = expiresOn.ToString( Azure::DateTime::DateFormat::Rfc3339, Azure::DateTime::TimeFractionFormat::Truncate); + protocolLayerOptions.KeyInfo.DelegatedUserTid = options.DelegatedUserTid; return _detail::ServiceClient::GetUserDelegationKey( *m_pipeline, m_serviceUrl, protocolLayerOptions, context); } diff --git a/sdk/storage/azure-storage-queues/src/rest_client.cpp b/sdk/storage/azure-storage-queues/src/rest_client.cpp index 0454d3928f..ad7231b184 100644 --- a/sdk/storage/azure-storage-queues/src/rest_client.cpp +++ b/sdk/storage/azure-storage-queues/src/rest_client.cpp @@ -189,7 +189,7 @@ namespace Azure { namespace Storage { namespace Queues { request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Accepted) @@ -209,7 +209,7 @@ namespace Azure { namespace Storage { namespace Queues { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "properties"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -446,7 +446,7 @@ namespace Azure { namespace Storage { namespace Queues { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "stats"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -532,6 +532,13 @@ namespace Azure { namespace Storage { namespace Queues { } writer.Write( _internal::XmlNode{_internal::XmlNodeType::StartTag, "Expiry", options.KeyInfo.Expiry}); + if (options.KeyInfo.DelegatedUserTid.HasValue()) + { + writer.Write(_internal::XmlNode{ + _internal::XmlNodeType::StartTag, + "DelegatedUserTid", + options.KeyInfo.DelegatedUserTid.Value()}); + } writer.Write(_internal::XmlNode{_internal::XmlNodeType::EndTag}); writer.Write(_internal::XmlNode{_internal::XmlNodeType::End}); xmlBody = writer.GetDocument(); @@ -543,7 +550,7 @@ namespace Azure { namespace Storage { namespace Queues { request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("restype", "service"); request.GetUrl().AppendQueryParameter("comp", "userdelegationkey"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -565,6 +572,7 @@ namespace Azure { namespace Storage { namespace Queues { kSignedExpiry, kSignedService, kSignedVersion, + kSignedDelegatedUserTid, kValue, }; const std::unordered_map XmlTagEnumMap{ @@ -575,6 +583,7 @@ namespace Azure { namespace Storage { namespace Queues { {"SignedExpiry", XmlTagEnum::kSignedExpiry}, {"SignedService", XmlTagEnum::kSignedService}, {"SignedVersion", XmlTagEnum::kSignedVersion}, + {"SignedDelegatedUserTid", XmlTagEnum::kSignedDelegatedUserTid}, {"Value", XmlTagEnum::kValue}, }; std::vector xmlPath; @@ -630,6 +639,12 @@ namespace Azure { namespace Storage { namespace Queues { { response.SignedVersion = node.Value; } + else if ( + xmlPath.size() == 2 && xmlPath[0] == XmlTagEnum::kUserDelegationKey + && xmlPath[1] == XmlTagEnum::kSignedDelegatedUserTid) + { + response.SignedDelegatedUserTid = node.Value; + } else if ( xmlPath.size() == 2 && xmlPath[0] == XmlTagEnum::kUserDelegationKey && xmlPath[1] == XmlTagEnum::kValue) @@ -680,7 +695,7 @@ namespace Azure { namespace Storage { namespace Queues { _internal::UrlEncodeQueryParameter( ListQueuesIncludeFlagsToString(options.Include.Value()))); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -803,7 +818,7 @@ namespace Azure { namespace Storage { namespace Queues { { request.SetHeader("x-ms-meta-" + p.first, p.second); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (!(httpStatusCode == Core::Http::HttpStatusCode::Created @@ -821,7 +836,7 @@ namespace Azure { namespace Storage { namespace Queues { const Core::Context& context) { auto request = Core::Http::Request(Core::Http::HttpMethod::Delete, url); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -840,7 +855,7 @@ namespace Azure { namespace Storage { namespace Queues { { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("comp", "metadata"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -872,7 +887,7 @@ namespace Azure { namespace Storage { namespace Queues { { request.SetHeader("x-ms-meta-" + p.first, p.second); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::NoContent) @@ -890,7 +905,7 @@ namespace Azure { namespace Storage { namespace Queues { { auto request = Core::Http::Request(Core::Http::HttpMethod::Get, url); request.GetUrl().AppendQueryParameter("comp", "acl"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -1033,7 +1048,7 @@ namespace Azure { namespace Storage { namespace Queues { request.SetHeader("Content-Type", "application/xml; charset=UTF-8"); request.SetHeader("Content-Length", std::to_string(requestBody.Length())); request.GetUrl().AppendQueryParameter("comp", "acl"); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::NoContent) @@ -1061,7 +1076,7 @@ namespace Azure { namespace Storage { namespace Queues { request.GetUrl().AppendQueryParameter( "visibilitytimeout", std::to_string(options.Visibilitytimeout.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -1187,7 +1202,7 @@ namespace Azure { namespace Storage { namespace Queues { const Core::Context& context) { auto request = Core::Http::Request(Core::Http::HttpMethod::Delete, url); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); (void)options; auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); @@ -1229,7 +1244,7 @@ namespace Azure { namespace Storage { namespace Queues { request.GetUrl().AppendQueryParameter( "messagettl", std::to_string(options.MessageTimeToLive.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Created) @@ -1338,7 +1353,7 @@ namespace Azure { namespace Storage { namespace Queues { request.GetUrl().AppendQueryParameter( "numofmessages", std::to_string(options.NumberOfMessages.Value())); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::Ok) @@ -1467,7 +1482,7 @@ namespace Azure { namespace Storage { namespace Queues { } request.GetUrl().AppendQueryParameter( "visibilitytimeout", std::to_string(options.Visibilitytimeout)); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::NoContent) @@ -1493,7 +1508,7 @@ namespace Azure { namespace Storage { namespace Queues { request.GetUrl().AppendQueryParameter( "popreceipt", _internal::UrlEncodeQueryParameter(options.PopReceipt)); } - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::NoContent) @@ -1517,7 +1532,7 @@ namespace Azure { namespace Storage { namespace Queues { } request.GetUrl().AppendQueryParameter( "visibilitytimeout", std::to_string(options.Visibilitytimeout)); - request.SetHeader("x-ms-version", "2026-02-06"); + request.SetHeader("x-ms-version", "2026-04-06"); auto pRawResponse = pipeline.Send(request, context); auto httpStatusCode = pRawResponse->GetStatusCode(); if (httpStatusCode != Core::Http::HttpStatusCode::NoContent) diff --git a/sdk/storage/azure-storage-queues/swagger/README.md b/sdk/storage/azure-storage-queues/swagger/README.md index 3632803ddd..d83aca5842 100644 --- a/sdk/storage/azure-storage-queues/swagger/README.md +++ b/sdk/storage/azure-storage-queues/swagger/README.md @@ -9,7 +9,7 @@ package-name: azure-storage-queues namespace: Azure::Storage::Queues output-folder: generated clear-output-folder: true -input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/refs/heads/main/specification/storage/data-plane/Microsoft.QueueStorage/stable/2026-02-06/queue.json +input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/refs/heads/feature/storage/stg101base/specification/storage/data-plane/Microsoft.QueueStorage/stable/2026-04-06/queue.json ``` ## ModelFour Options @@ -77,13 +77,13 @@ directive: "name": "ApiVersion", "modelAsString": false }, - "enum": ["2026-02-06"], + "enum": ["2026-04-06"], "description": "The version used for the operations to Azure storage services." }; - from: swagger-document where: $.parameters transform: > - $.ApiVersionParameter.enum[0] = "2026-02-06"; + $.ApiVersionParameter.enum[0] = "2026-04-06"; ``` ### Rename Operations diff --git a/sdk/storage/azure-storage-queues/test/ut/queue_sas_test.cpp b/sdk/storage/azure-storage-queues/test/ut/queue_sas_test.cpp index ee2e400d84..85e0ee77ac 100644 --- a/sdk/storage/azure-storage-queues/test/ut/queue_sas_test.cpp +++ b/sdk/storage/azure-storage-queues/test/ut/queue_sas_test.cpp @@ -499,7 +499,7 @@ namespace Azure { namespace Storage { namespace Test { return {}; } - TEST_F(QueueSasTest, DISABLED_PrincipalBoundDelegationSas) + TEST_F(QueueSasTest, PrincipalBoundDelegationSas_LIVEONLY_) { auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); @@ -546,4 +546,61 @@ namespace Azure { namespace Storage { namespace Test { EXPECT_THROW(queueClient2.GetProperties(), StorageException); } + TEST_F(QueueSasTest, DISABLED_PrincipalBoundDelegationSas_CrossTenant) + { + auto sasStartsOn = std::chrono::system_clock::now() - std::chrono::minutes(5); + auto sasExpiresOn = std::chrono::system_clock::now() + std::chrono::minutes(60); + + auto keyCredential + = _internal::ParseConnectionString(StandardStorageConnectionString()).KeyCredential; + Azure::Identity::ClientSecretCredentialOptions credentialOptions; + credentialOptions.AdditionallyAllowedTenants = {"*"}; + auto endUserCredential = std::make_shared( + GetEnv("AZURE_TENANT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_ID_CROSS_TENANT"), + GetEnv("AZURE_CLIENT_SECRET_CROSS_TENANT")); + auto delegatedUserObjectId = getObjectIdFromTokenCredential(endUserCredential); + + auto queueServiceClient = Queues::QueueServiceClient( + m_queueServiceClient->GetUrl(), + GetTestCredential(), + InitStorageClientOptions()); + Queues::Models::UserDelegationKey userDelegationKey; + { + Queues::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "4ab3a968-f1ae-47a6-b82c-f654612122a9"; + userDelegationKey = queueServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + + Sas::QueueSasBuilder queueSasBuilder; + queueSasBuilder.Protocol = Sas::SasProtocol::HttpsAndHttp; + queueSasBuilder.StartsOn = sasStartsOn; + queueSasBuilder.ExpiresOn = sasExpiresOn; + queueSasBuilder.QueueName = m_queueName; + queueSasBuilder.DelegatedUserObjectId = delegatedUserObjectId; + + auto queueClient = *m_queueClient; + auto accountName = keyCredential->AccountName; + + queueSasBuilder.SetPermissions(Sas::QueueSasPermissions::All); + auto sasToken = queueSasBuilder.GenerateSasToken(userDelegationKey, accountName); + + Queues::QueueClient queueClient1( + AppendQueryParameters(Azure::Core::Url(queueClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_NO_THROW(queueClient1.GetProperties()); + + { + Queues::GetUserDelegationKeyOptions options; + options.DelegatedUserTid = "00000000-0000-0000-0000-000000000000"; + userDelegationKey = queueServiceClient.GetUserDelegationKey(sasExpiresOn, options).Value; + } + sasToken = queueSasBuilder.GenerateSasToken(userDelegationKey, accountName); + Queues::QueueClient queueClient2( + AppendQueryParameters(Azure::Core::Url(queueClient.GetUrl()), sasToken), + endUserCredential, + InitStorageClientOptions()); + EXPECT_THROW(queueClient2.GetProperties(), StorageException); + } }}} // namespace Azure::Storage::Test