diff --git a/sdk/storage/Azure.Storage.Blobs/assets.json b/sdk/storage/Azure.Storage.Blobs/assets.json index 46a74dc97b01..e0cc7497a2f2 100644 --- a/sdk/storage/Azure.Storage.Blobs/assets.json +++ b/sdk/storage/Azure.Storage.Blobs/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/storage/Azure.Storage.Blobs", - "Tag": "net/storage/Azure.Storage.Blobs_be75b1430c" + "Tag": "net/storage/Azure.Storage.Blobs_14eb1d6279" } diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs index 14116ab039dd..25db387f24a9 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs @@ -5110,6 +5110,83 @@ await TestHelper.AssertExpectedExceptionAsync( e => Assert.AreEqual("BlobNotFound", e.ErrorCode)); } + [RecordedTest] + public async Task SetMetadataAsync_Sort() + { + await using DisposingContainer test = await GetTestContainerAsync(); + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container); + IDictionary metadata = new Dictionary() { + { "a0", "a" }, + { "a1", "a" }, + { "a2", "a" }, + { "a3", "a" }, + { "a4", "a" }, + { "a5", "a" }, + { "a6", "a" }, + { "a7", "a" }, + { "a8", "a" }, + { "a9", "a" }, + { "_", "a" }, + { "_a", "a" }, + { "a_", "a" }, + { "__", "a" }, + { "_a_", "a" }, + { "b", "a" }, + { "c", "a" }, + { "y", "a" }, + { "z", "z_" }, + { "_z", "a" }, + { "_F", "a" }, + { "F", "a" }, + { "F_", "a" }, + { "_F_", "a" }, + { "__F", "a" }, + { "__a", "a" }, + { "a__", "a" } + }; + + // Act + Response response = await blob.SetMetadataAsync(metadata); + + // Assert + + // Ensure that we grab the whole ETag value from the service without removing the quotes + Assert.AreEqual(response.Value.ETag.ToString(), $"\"{response.GetRawResponse().Headers.ETag}\""); + + // Ensure the value has been correctly set by doing a GetProperties call + Response getPropertiesResponse = await blob.GetPropertiesAsync(); + AssertDictionaryEquality(metadata, getPropertiesResponse.Value.Metadata); + } + + [RecordedTest] + public async Task SetMetadataAsync_Sort_InvalidMetadata() + { + await using DisposingContainer test = await GetTestContainerAsync(); + // Arrange + BlobBaseClient blob = await GetNewBlobClient(test.Container); + IDictionary metadata = new Dictionary() { + { "test", "val" }, + { "test-", "val" }, + { "test--", "val" }, + { "test-_", "val" }, + { "test_-", "val" }, + { "test__", "val" }, + { "test-a", "val" }, + { "test-_A", "val" }, + { "test_a", "val" }, + { "test_Z", "val" }, + { "test_a_", "val" }, + { "test_a-", "val" }, + { "test_a-_", "val" }, + }; + + // Act + await TestHelper.AssertExpectedExceptionAsync( + blob.SetMetadataAsync(metadata), + e => Assert.AreEqual(BlobErrorCode.InvalidMetadata.ToString(), e.ErrorCode)); + } + [RecordedTest] public async Task CreateSnapshotAsync() { diff --git a/sdk/storage/Azure.Storage.Common/CHANGELOG.md b/sdk/storage/Azure.Storage.Common/CHANGELOG.md index 47be03adb2b6..362318ed38bb 100644 --- a/sdk/storage/Azure.Storage.Common/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.Common/CHANGELOG.md @@ -7,6 +7,7 @@ ### Breaking Changes ### Bugs Fixed +- Fixed \[BUG\] Azure Blob Storage Client SDK No Longer Supports Globalization Invariant Mode for Account Key Authentication #45052 ### Other Changes diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs b/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs index 48b4ca18cd54..6aebef8430e4 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs @@ -127,7 +127,6 @@ internal static class Constants public const string DisableExpectContinueHeaderEnvVar = "AZURE_STORAGE_DISABLE_EXPECT_CONTINUE_HEADER"; public const string DefaultScope = "/.default"; - public const string EnUsCulture = "en-US"; /// /// Storage Connection String constant values. diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StorageSharedKeyPipelinePolicy.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StorageSharedKeyPipelinePolicy.cs index fe787729a6ba..fddbb572c38f 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/StorageSharedKeyPipelinePolicy.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StorageSharedKeyPipelinePolicy.cs @@ -23,21 +23,23 @@ internal sealed class StorageSharedKeyPipelinePolicy : HttpPipelineSynchronousPo private const bool IncludeXMsDate = true; /// - /// CultureInfo used to sort headers in the string to sign. + /// Shared key credentials used to sign requests /// - private static readonly CultureInfo s_cultureInfo = new CultureInfo(Constants.EnUsCulture, useUserOverride: false); + private readonly StorageSharedKeyCredential _credentials; /// - /// Shared key credentials used to sign requests + /// Used to sort headers to build the string to sign. /// - private readonly StorageSharedKeyCredential _credentials; + private static readonly HeaderComparer s_headerComparer = new HeaderComparer(); /// /// Create a new SharedKeyPipelinePolicy /// /// SharedKeyCredentials to authenticate requests. public StorageSharedKeyPipelinePolicy(StorageSharedKeyCredential credentials) - => _credentials = credentials; + { + _credentials = credentials; + } /// /// Sign the request using the shared key credentials. @@ -117,10 +119,7 @@ private static void BuildCanonicalizedHeaders(StringBuilder stringBuilder, HttpM } } - headers.Sort(static (x, y) => - {; - return string.Compare(x.Name, y.Name, s_cultureInfo, CompareOptions.IgnoreSymbols); - }); + headers.Sort(s_headerComparer); foreach (var header in headers) { @@ -162,5 +161,102 @@ private void BuildCanonicalizedResource(StringBuilder stringBuilder, Uri resourc } } } + + internal class HeaderComparer : IComparer + { + private static readonly HeaderStringComparer s_headerComparer = new HeaderStringComparer(); + + public int Compare(HttpHeader x, HttpHeader y) + { + return s_headerComparer.Compare(x.Name, y.Name); + } + } + + internal class HeaderStringComparer : IComparer + { + private static readonly int[] s_table_lv0 = + { + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x71c, 0x0, 0x71f, 0x721, 0x723, 0x725, + 0x0, 0x0, 0x0, 0x72d, 0x803, 0x0, 0x0, 0x733, 0x0, 0xd03, 0xd1a, 0xd1c, 0xd1e, + 0xd20, 0xd22, 0xd24, 0xd26, 0xd28, 0xd2a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xe02, 0xe09, 0xe0a, 0xe1a, 0xe21, 0xe23, 0xe25, 0xe2c, 0xe32, 0xe35, 0xe36, 0xe48, 0xe51, + 0xe70, 0xe7c, 0xe7e, 0xe89, 0xe8a, 0xe91, 0xe99, 0xe9f, 0xea2, 0xea4, 0xea6, 0xea7, 0xea9, + 0x0, 0x0, 0x0, 0x743, 0x744, 0x748, 0xe02, 0xe09, 0xe0a, 0xe1a, 0xe21, 0xe23, 0xe25, + 0xe2c, 0xe32, 0xe35, 0xe36, 0xe48, 0xe51, 0xe70, 0xe7c, 0xe7e, 0xe89, 0xe8a, 0xe91, 0xe99, + 0xe9f, 0xea2, 0xea4, 0xea6, 0xea7, 0xea9, 0x0, 0x74c, 0x0, 0x750, 0x0, + }; + + private static readonly int[] s_table_lv2 = + { + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + private static readonly int[] s_table_lv4 = + { + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8012, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8212, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + private static readonly int[][] s_tables = { s_table_lv0, s_table_lv2, s_table_lv4 }; + + public int Compare(string x, string y) + { + int currentLevel = 0; + int i = 0; + int j = 0; + + while (currentLevel < s_tables.Length) + { + if (currentLevel == s_tables.Length - 1 && i != j) + { + return j - i; + } + + int weight1 = i < x.Length ? s_tables[currentLevel][x[i]] : 0x1; + int weight2 = j < y.Length ? s_tables[currentLevel][y[j]] : 0x1; + + if (weight1 == 0x1 && weight2 == 0x1) + { + i = 0; + j = 0; + currentLevel++; + } + else if (weight1 == weight2) + { + i++; + j++; + } + else if (weight1 == 0) + { + i++; + } + else if (weight2 == 0) + { + j++; + } + else + { + return weight1 - weight2; + } + } + + return 0; + } + } } }