diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobUrlParts.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobUrlParts.java index 4378e774affa..fe75504cff82 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobUrlParts.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobUrlParts.java @@ -12,6 +12,7 @@ import com.azure.storage.common.implementation.SasImplUtils; import com.azure.storage.common.sas.CommonSasQueryParameters; import com.azure.storage.common.implementation.Constants; +import com.azure.storage.common.implementation.StorageImplUtils; import java.net.MalformedURLException; import java.net.URL; @@ -451,17 +452,14 @@ private static void parseNonIpUrl(URL url, BlobUrlParts parts) { String host = url.getHost(); parts.setHost(host); - //Parse host to get account name - // host will look like this : .blob.core.windows.net - if (!CoreUtils.isNullOrEmpty(host)) { - int accountNameIndex = host.indexOf('.'); - if (accountNameIndex == -1) { - // host only contains account name - parts.setAccountName(host); - } else { - // if host is separated by . - parts.setAccountName(host.substring(0, accountNameIndex)); - } + // Parse host to get account name + if (url.toString().contains(Constants.Blob.URI_SUBDOMAIN)) { + parts.setAccountName(StorageImplUtils.getAccountNameFromHost(host, Constants.Blob.URI_SUBDOMAIN)); + } else if (url.toString().contains(Constants.Dfs.URI_SUBDOMAIN)) { + parts.setAccountName(StorageImplUtils.getAccountNameFromHost(host, Constants.Dfs.URI_SUBDOMAIN)); + } else { + throw LOGGER.logExceptionAsError( + new IllegalArgumentException("Host does not contain the expected subdomain. Host: " + host)); } // find the container & blob names (if any) diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobUrlPartsTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobUrlPartsTests.java new file mode 100644 index 000000000000..0f78db269660 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobUrlPartsTests.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class BlobUrlPartsTests { + + @ParameterizedTest + @MethodSource("urlSupplier") + public void testParseUrl(String uriString) throws MalformedURLException { + // Arrange + URL originalUri = new URL(uriString); + + // Act + BlobUrlParts blobUriBuilder = BlobUrlParts.parse(originalUri); + String newUri = blobUriBuilder.toUrl().toString(); + + // Assert + assertEquals("https", blobUriBuilder.getScheme()); + assertEquals("myaccount", blobUriBuilder.getAccountName()); + assertEquals("", blobUriBuilder.getBlobContainerName()); + assertNull(blobUriBuilder.getBlobName()); + assertNull(blobUriBuilder.getSnapshot()); + assertEquals("", blobUriBuilder.getCommonSasQueryParameters().encode()); + assertEquals(originalUri.toString(), newUri); + } + + public static Stream urlSupplier() { + return Stream.of(Arguments.of("https://myaccount.blob.core.windows.net/"), + Arguments.of("https://myaccount-secondary.blob.core.windows.net/"), + Arguments.of("https://myaccount-dualstack.blob.core.windows.net/"), + Arguments.of("https://myaccount-ipv6.blob.core.windows.net/"), + Arguments.of("https://myaccount-secondary-dualstack.blob.core.windows.net/"), + Arguments.of("https://myaccount-secondary-ipv6.blob.core.windows.net/")); + } +} diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BuilderHelperTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BuilderHelperTests.java index 6ae92f47bac5..0af01b5fe437 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BuilderHelperTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BuilderHelperTests.java @@ -38,6 +38,8 @@ import reactor.test.StepVerifier; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -47,6 +49,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -523,6 +526,54 @@ public void throwsOnAmbiguousCredentialsWithAzureSasCredential() { .buildClient()); } + @ParameterizedTest + @MethodSource("blobAccountNameSupplier") + void secondaryIpv6Dualstack(String urlString, String expectedAccountName) throws MalformedURLException { + BlobUrlParts blobUrlParts = BlobUrlParts.parse(new URL(urlString)); + + assertEquals("https", blobUrlParts.getScheme()); + assertEquals(expectedAccountName, blobUrlParts.getAccountName()); + assertEquals("", blobUrlParts.getBlobContainerName()); + assertNull(blobUrlParts.getSnapshot()); + assertEquals("", blobUrlParts.getCommonSasQueryParameters().encode()); + assertNull(blobUrlParts.getVersionId()); + + // Verify the endpoint can be used to reconstruct the original URL + String newUri = blobUrlParts.toUrl().toString(); + assertEquals(urlString, newUri); + } + + private static Stream blobAccountNameSupplier() { + return Stream.of(Arguments.of("https://myaccount.blob.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary.blob.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-dualstack.blob.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-ipv6.blob.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary-dualstack.blob.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary-ipv6.blob.core.windows.net/", "myaccount")); + } + + @ParameterizedTest + @MethodSource("blobManagedDiskAccountNameSupplier") + void ipv6InternalAccounts(String urlString, String expectedAccountName) throws MalformedURLException { + BlobUrlParts blobUrlParts = BlobUrlParts.parse(new URL(urlString)); + + assertEquals("https", blobUrlParts.getScheme()); + assertEquals(expectedAccountName, blobUrlParts.getAccountName()); + assertEquals("", blobUrlParts.getBlobContainerName()); + assertNull(blobUrlParts.getSnapshot()); + assertEquals("", blobUrlParts.getCommonSasQueryParameters().encode()); + assertNull(blobUrlParts.getVersionId()); + + // Verify the endpoint can be used to reconstruct the original URL + String newUri = blobUrlParts.toUrl().toString(); + assertEquals(urlString, newUri); + } + + private static Stream blobManagedDiskAccountNameSupplier() { + return Stream.of(Arguments.of("https://md-d3rqxhqbxbwq.blob.core.windows.net/", "md-d3rqxhqbxbwq"), + Arguments.of("https://md-ssd-bndub02px100c21.blob.core.windows.net/", "md-ssd-bndub02px100c21")); + } + @Test public void onlyOneRetryOptionsCanBeApplied() { assertThrows(IllegalStateException.class, diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java index d40b9b82d585..ce1f5b30e25a 100644 --- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java @@ -463,4 +463,66 @@ private UrlConstants() { // Private to prevent construction. } } + + /** + * Constants for the Blob service subdomain. + */ + public static class Blob { + /** + * The URI subdomain for blob storage. + */ + public static final String URI_SUBDOMAIN = "blob"; + } + + /** + * Constants for the File service subdomain. + */ + public static final class File { + /** + * The URI subdomain for file storage. + */ + public static final String URI_SUBDOMAIN = "file"; + + private File() { + } + } + + /** + * Constants for the Queue service subdomain. + */ + public static final class Queue { + /** + * The URI subdomain for queue storage. + */ + public static final String URI_SUBDOMAIN = "queue"; + + private Queue() { + } + } + + /** + * Constants for the Table service subdomain. + */ + public static final class Table { + /** + * The URI subdomain for table storage. + */ + public static final String URI_SUBDOMAIN = "table"; + + private Table() { + } + } + + /** + * Constants for the DFS (Data Lake) service subdomain. + */ + public static final class Dfs { + /** + * The URI subdomain for Data Lake Storage. + */ + public static final String URI_SUBDOMAIN = "dfs"; + + private Dfs() { + } + } } diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java index 4b564a668fac..90aad19fffbb 100644 --- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java @@ -270,6 +270,58 @@ public static String getAccountName(URL url) { return accountName; } + public static String getAccountName(URL url, String serviceSubDomain) { + String host = url.getHost(); + return getAccountNameFromHost(host, serviceSubDomain); + } + + /** + * Gets the account name from a host string, stripping IPv6, dualstack, and secondary suffixes. + *

+ * For IPv6/dualstack endpoints, hosts look like {@code accountname-ipv6.blob.core.windows.net} or + * {@code accountname-secondary-dualstack.blob.core.windows.net}. This method extracts the base account name + * by verifying the service subdomain is present, then stripping known suffixes. + *

+ * Suffixes are stripped in order: first {@code -ipv6} or {@code -dualstack}, then {@code -secondary}, + * to handle compound cases like {@code accountname-secondary-ipv6}. + * + * @param host The host string from a URL. + * @param serviceSubDomain The service subdomain (e.g., "blob", "file", "queue", "dfs"). + * @return The account name, or {@code null} if it cannot be parsed. + */ + public static String getAccountNameFromHost(String host, String serviceSubDomain) { + if (CoreUtils.isNullOrEmpty(host)) { + return null; + } + + int accountEndIndex = host.indexOf('.'); + if (accountEndIndex >= 0) { + int serviceStartIndex = host.indexOf(serviceSubDomain, accountEndIndex); + if (serviceStartIndex > -1) { + String accountName = host.substring(0, accountEndIndex); + + // Note: The suffixes are specifically checked/trimmed in this order to + // take into account of cases with both "-secondary" and "-ipv6"/"-dualstack" + // ie. "accountname-secondary-ipv6" + + // Remove "-ipv6" or "-dualstack" from end if present + if (accountName.endsWith("-ipv6")) { + accountName = accountName.substring(0, accountName.length() - "-ipv6".length()); + } else if (accountName.endsWith("-dualstack")) { + accountName = accountName.substring(0, accountName.length() - "-dualstack".length()); + } + + // Remove "-secondary" from end if present + if (accountName.endsWith("-secondary")) { + accountName = accountName.substring(0, accountName.length() - "-secondary".length()); + } + + return accountName; + } + } + return null; + } + /** Returns an empty string if value is {@code null}, otherwise returns value * @param value The value to check and return. * @return The value or empty string. diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/connectionstring/StorageConnectionStringTest.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/connectionstring/StorageConnectionStringTest.java index 11cdfeba90e2..92f914c4782c 100644 --- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/connectionstring/StorageConnectionStringTest.java +++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/connectionstring/StorageConnectionStringTest.java @@ -7,6 +7,8 @@ import com.azure.storage.common.implementation.SasImplUtils; import org.junit.jupiter.api.Test; +import java.net.MalformedURLException; +import java.net.URL; import java.util.Arrays; import java.util.HashSet; import java.util.Map; @@ -359,4 +361,19 @@ public void overrideDefaultProtocolToHttp() { .equalsIgnoreCase(String.format("http://%s.blob.%s", // http protocol ACCOUNT_NAME_VALUE, CHINA_CLOUD_ENDPOINT_SUFFIX))); } + + @Test + public void parseIPv6ConnectionString() throws MalformedURLException { + String accountName = "storagesample"; + String blobEndpoint = "https://" + accountName + ".blob.core.windows.net"; + String connectionString = String.format( + "DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=123=;BlobEndpoint=%s;EndpointSuffix=core.windows.net", + accountName, blobEndpoint); + StorageConnectionString storageConnectionString = StorageConnectionString.create(connectionString, LOGGER); + + assertNotNull(storageConnectionString); + assertEquals(accountName, storageConnectionString.getAccountName()); + assertEquals((new URL(blobEndpoint)).toString(), storageConnectionString.getBlobEndpoint().getPrimaryUri()); + + } } diff --git a/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/BuilderHelperTests.java b/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/BuilderHelperTests.java index 55e0a528a480..94621f2877ff 100644 --- a/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/BuilderHelperTests.java +++ b/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/BuilderHelperTests.java @@ -17,10 +17,12 @@ import com.azure.core.util.ClientOptions; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; +import com.azure.storage.blob.BlobUrlParts; import com.azure.storage.common.StorageSharedKeyCredential; import com.azure.storage.common.policy.RequestRetryOptions; import com.azure.storage.common.policy.RetryPolicyType; import com.azure.storage.file.datalake.implementation.util.BuilderHelper; +import com.azure.storage.file.datalake.implementation.util.DataLakeImplUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -30,6 +32,8 @@ import reactor.test.StepVerifier; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.time.Duration; import java.util.ArrayList; import java.util.Map; @@ -40,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -232,6 +237,53 @@ public void pathClientCustomApplicationIdInUAString(String logOptionsUA, String .verifyComplete(); } + @ParameterizedTest + @MethodSource("dfsAccountNameSupplier") + void secondaryIpv6Dualstack(String urlString, String expectedAccountName) throws MalformedURLException { + BlobUrlParts blobUrlParts = BlobUrlParts.parse(new URL(urlString)); + + assertEquals("https", blobUrlParts.getScheme()); + assertEquals(expectedAccountName, blobUrlParts.getAccountName()); + assertEquals("", blobUrlParts.getBlobContainerName()); + assertNull(blobUrlParts.getSnapshot()); + assertEquals("", blobUrlParts.getCommonSasQueryParameters().encode()); + assertNull(blobUrlParts.getVersionId()); + + String newUri = DataLakeImplUtils.endpointToDesiredEndpoint(blobUrlParts.toUrl().toString(), "dfs", "blob"); + assertEquals(urlString, newUri); + } + + private static Stream dfsAccountNameSupplier() { + return Stream.of(Arguments.of("https://myaccount.dfs.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary.dfs.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-dualstack.dfs.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-ipv6.dfs.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary-dualstack.dfs.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary-ipv6.dfs.core.windows.net/", "myaccount")); + } + + @ParameterizedTest + @MethodSource("dfsManagedDiskAccountNameSupplier") + void ipv6InternalAccounts(String urlString, String expectedAccountName) throws MalformedURLException { + String blobUrlString = DataLakeImplUtils.endpointToDesiredEndpoint(urlString, "blob", "dfs"); + BlobUrlParts blobUrlParts = BlobUrlParts.parse(new URL(blobUrlString)); + + assertEquals("https", blobUrlParts.getScheme()); + assertEquals(expectedAccountName, blobUrlParts.getAccountName()); + assertEquals("", blobUrlParts.getBlobContainerName()); + assertNull(blobUrlParts.getSnapshot()); + assertEquals("", blobUrlParts.getCommonSasQueryParameters().encode()); + assertNull(blobUrlParts.getVersionId()); + + String newUri = DataLakeImplUtils.endpointToDesiredEndpoint(blobUrlParts.toUrl().toString(), "dfs", "blob"); + assertEquals(urlString, newUri); + } + + private static Stream dfsManagedDiskAccountNameSupplier() { + return Stream.of(Arguments.of("https://md-d3rqxhqbxbwq.dfs.core.windows.net/", "md-d3rqxhqbxbwq"), + Arguments.of("https://md-ssd-bndub02px100c21.dfs.core.windows.net/", "md-ssd-bndub02px100c21")); + } + @Test public void doesNotThrowOnAmbiguousCredentialsWithoutAzureSasCredential() { assertDoesNotThrow(() -> new DataLakeFileSystemClientBuilder().endpoint(ENDPOINT) diff --git a/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/implementation/util/BuilderHelper.java b/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/implementation/util/BuilderHelper.java index c58fbdddd2a8..88b7e4861de2 100644 --- a/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/implementation/util/BuilderHelper.java +++ b/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/implementation/util/BuilderHelper.java @@ -31,6 +31,7 @@ import com.azure.storage.common.implementation.BuilderUtils; import com.azure.storage.common.implementation.Constants; import com.azure.storage.common.implementation.SasImplUtils; +import com.azure.storage.common.implementation.StorageImplUtils; import com.azure.storage.common.implementation.credentials.CredentialValidator; import com.azure.storage.common.policy.MetadataValidationPolicy; import com.azure.storage.common.policy.RequestRetryOptions; @@ -205,19 +206,9 @@ public static String getAccountName(URL url) { String[] pathPieces = path.split("/", 1); return (pathPieces.length == 1) ? pathPieces[0] : null; } else { - // URL is using a pattern of http://accountName.blob.core.windows.net + // URL is using a pattern of http://accountName.file.core.windows.net String host = url.getHost(); - - if (CoreUtils.isNullOrEmpty(host)) { - return null; - } - - int accountNameIndex = host.indexOf('.'); - if (accountNameIndex == -1) { - return host; - } else { - return host.substring(0, accountNameIndex); - } + return StorageImplUtils.getAccountNameFromHost(host, Constants.File.URI_SUBDOMAIN); } } @@ -269,16 +260,7 @@ public static ShareUrlParts parseEndpoint(String endpoint, ClientLogger logger) } else { // URL is using a pattern of http://accountName.file.core.windows.net/shareName String host = url.getHost(); - - String accountName = null; - if (!CoreUtils.isNullOrEmpty(host)) { - int accountNameIndex = host.indexOf('.'); - if (accountNameIndex == -1) { - accountName = host; - } else { - accountName = host.substring(0, accountNameIndex); - } - } + String accountName = StorageImplUtils.getAccountNameFromHost(host, Constants.File.URI_SUBDOMAIN); parts.setAccountName(accountName); diff --git a/sdk/storage/azure-storage-file-share/src/test/java/com/azure/storage/file/share/BuilderHelperTests.java b/sdk/storage/azure-storage-file-share/src/test/java/com/azure/storage/file/share/BuilderHelperTests.java index 149b21eb30f9..4b2a7218c54a 100644 --- a/sdk/storage/azure-storage-file-share/src/test/java/com/azure/storage/file/share/BuilderHelperTests.java +++ b/sdk/storage/azure-storage-file-share/src/test/java/com/azure/storage/file/share/BuilderHelperTests.java @@ -34,6 +34,8 @@ import reactor.test.StepVerifier; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -42,9 +44,11 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; public class BuilderHelperTests { + private static final ClientLogger LOGGER = new ClientLogger(BuilderHelperTests.class); private static final StorageSharedKeyCredential CREDENTIALS = new StorageSharedKeyCredential("accountName", "accountKey"); @@ -403,6 +407,48 @@ void throwsOnAmbiguousCREDENTIALSWithAzureSasCredential() { .buildClient()); } + @ParameterizedTest + @MethodSource("fileAccountNameSupplier") + void secondaryIpv6Dualstack(String urlString, String expectedAccountName) { + BuilderHelper.ShareUrlParts shareUrlParts = BuilderHelper.parseEndpoint(urlString, LOGGER); + + assertEquals("https", shareUrlParts.getScheme()); + assertEquals(expectedAccountName, shareUrlParts.getAccountName()); + assertNull(shareUrlParts.getShareName()); + assertNull(shareUrlParts.getSasToken()); + + String newUri = shareUrlParts.getEndpoint() + "/"; + assertEquals(urlString, newUri); + } + + private static Stream fileAccountNameSupplier() { + return Stream.of(Arguments.of("https://myaccount.file.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary.file.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-dualstack.file.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-ipv6.file.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary-dualstack.file.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary-ipv6.file.core.windows.net/", "myaccount")); + } + + @ParameterizedTest + @MethodSource("fileManagedDiskAccountNameSupplier") + void ipv6InternalAccounts(String urlString, String expectedAccountName) { + BuilderHelper.ShareUrlParts shareUrlParts = BuilderHelper.parseEndpoint(urlString, LOGGER); + + assertEquals("https", shareUrlParts.getScheme()); + assertEquals(expectedAccountName, shareUrlParts.getAccountName()); + assertNull(shareUrlParts.getShareName()); + assertNull(shareUrlParts.getSasToken()); + + String newUri = shareUrlParts.getEndpoint() + "/"; + assertEquals(urlString, newUri); + } + + private static Stream fileManagedDiskAccountNameSupplier() { + return Stream.of(Arguments.of("https://md-d3rqxhqbxbwq.file.core.windows.net/", "md-d3rqxhqbxbwq"), + Arguments.of("https://md-ssd-bndub02px100c21.file.core.windows.net/", "md-ssd-bndub02px100c21")); + } + @Test void onlyOneRetryOptionsCanBeApplied() { assertThrows(IllegalStateException.class, diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/util/BuilderHelper.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/util/BuilderHelper.java index c19628576232..711d3f6e58e0 100644 --- a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/util/BuilderHelper.java +++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/util/BuilderHelper.java @@ -31,6 +31,7 @@ import com.azure.storage.common.implementation.BuilderUtils; import com.azure.storage.common.implementation.Constants; import com.azure.storage.common.implementation.SasImplUtils; +import com.azure.storage.common.implementation.StorageImplUtils; import com.azure.storage.common.implementation.credentials.CredentialValidator; import com.azure.storage.common.policy.MetadataValidationPolicy; import com.azure.storage.common.policy.RequestRetryOptions; @@ -107,16 +108,7 @@ public static QueueUrlParts parseEndpoint(String endpoint, ClientLogger logger) } else { // URL is using a pattern of http://accountName.queue.core.windows.net/queueName String host = url.getHost(); - - String accountName = null; - if (!CoreUtils.isNullOrEmpty(host)) { - int accountNameIndex = host.indexOf('.'); - if (accountNameIndex == -1) { - accountName = host; - } else { - accountName = host.substring(0, accountNameIndex); - } - } + String accountName = StorageImplUtils.getAccountNameFromHost(host, Constants.Queue.URI_SUBDOMAIN); parts.setAccountName(accountName); diff --git a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/BuildHelperTests.java b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/BuildHelperTests.java index c9fc01347057..a87708dd7434 100644 --- a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/BuildHelperTests.java +++ b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/BuildHelperTests.java @@ -47,6 +47,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -284,6 +285,48 @@ public void throwsOnAmbiguousCredentialsWithAzureSasCredential() { .buildClient()); } + @ParameterizedTest + @MethodSource("queueAccountNameSupplier") + void secondaryIpv6Dualstack(String urlString, String expectedAccountName) { + BuilderHelper.QueueUrlParts queueUrlParts = BuilderHelper.parseEndpoint(urlString, LOGGER); + + assertEquals("https", queueUrlParts.getScheme()); + assertEquals(expectedAccountName, queueUrlParts.getAccountName()); + assertNull(queueUrlParts.getQueueName()); + assertNull(queueUrlParts.getSasToken()); + + String newUri = queueUrlParts.getEndpoint() + "/"; + assertEquals(urlString, newUri); + } + + private static Stream queueAccountNameSupplier() { + return Stream.of(Arguments.of("https://myaccount.queue.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary.queue.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-dualstack.queue.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-ipv6.queue.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary-dualstack.queue.core.windows.net/", "myaccount"), + Arguments.of("https://myaccount-secondary-ipv6.queue.core.windows.net/", "myaccount")); + } + + @ParameterizedTest + @MethodSource("queueManagedDiskAccountNameSupplier") + void ipv6InternalAccounts(String urlString, String expectedAccountName) { + BuilderHelper.QueueUrlParts queueUrlParts = BuilderHelper.parseEndpoint(urlString, LOGGER); + + assertEquals("https", queueUrlParts.getScheme()); + assertEquals(expectedAccountName, queueUrlParts.getAccountName()); + assertNull(queueUrlParts.getQueueName()); + assertNull(queueUrlParts.getSasToken()); + + String newUri = queueUrlParts.getEndpoint() + "/"; + assertEquals(urlString, newUri); + } + + private static Stream queueManagedDiskAccountNameSupplier() { + return Stream.of(Arguments.of("https://md-d3rqxhqbxbwq.queue.core.windows.net/", "md-d3rqxhqbxbwq"), + Arguments.of("https://md-ssd-bndub02px100c21.queue.core.windows.net/", "md-ssd-bndub02px100c21")); + } + @Test public void onlyOneRetryOptionsCanBeApplied() { assertThrows(IllegalStateException.class,