From 7d6416ecfdd2c12dae3922d6bf0ab48162d85717 Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 18 Mar 2026 22:18:27 -0400 Subject: [PATCH 1/7] refactor so BuiderHelpers and BobUrlParts use StorageImplUtils --- .../com/azure/storage/blob/BlobUrlParts.java | 14 +---- .../src/test/java/BlobUrlPartsTests.java | 43 ++++++++++++++ .../common/implementation/Constants.java | 50 ++++++++++++++++ .../implementation/StorageImplUtils.java | 53 +++++++++++++++++ .../implementation/StorageImplUtilsTests.java | 59 +++++++++++++++++++ .../implementation/util/BuilderHelper.java | 26 ++------ .../implementation/util/BuilderHelper.java | 12 +--- 7 files changed, 214 insertions(+), 43 deletions(-) create mode 100644 sdk/storage/azure-storage-blob/src/test/java/BlobUrlPartsTests.java 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..a22d01d60b39 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,18 +452,9 @@ private static void parseNonIpUrl(URL url, BlobUrlParts parts) { String host = url.getHost(); parts.setHost(host); - //Parse host to get account name + // 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)); - } - } + parts.setAccountName(StorageImplUtils.getAccountNameFromHost(host, Constants.Blob.URI_SUBDOMAIN)); // find the container & blob names (if any) String path = url.getPath(); diff --git a/sdk/storage/azure-storage-blob/src/test/java/BlobUrlPartsTests.java b/sdk/storage/azure-storage-blob/src/test/java/BlobUrlPartsTests.java new file mode 100644 index 000000000000..c3cdbb6b8814 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/java/BlobUrlPartsTests.java @@ -0,0 +1,43 @@ +import com.azure.storage.blob.BlobUrlParts; +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-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..3d70641df64b 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,54 @@ 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 class File { + /** + * The URI subdomain for file storage. + */ + public static final String URI_SUBDOMAIN = "file"; + } + + /** + * Constants for the Queue service subdomain. + */ + public static class Queue { + /** + * The URI subdomain for queue storage. + */ + public static final String URI_SUBDOMAIN = "queue"; + } + + /** + * Constants for the Table service subdomain. + */ + public static class Table { + /** + * The URI subdomain for table storage. + */ + public static final String URI_SUBDOMAIN = "table"; + } + + /** + * Constants for the DFS (Data Lake) service subdomain. + */ + public static class Dfs { + /** + * The URI subdomain for Data Lake Storage. + */ + public static final String URI_SUBDOMAIN = "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..5777d16225a8 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,59 @@ public static String getAccountName(URL url) { return accountName; } + + public static String getAccountName(URL url, String serviceSubDomain) throws MalformedURLException { + 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/StorageImplUtilsTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java index 83418b91668b..3ba493b22eec 100644 --- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java +++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java @@ -7,17 +7,38 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.net.MalformedURLException; +import java.net.URL; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; public class StorageImplUtilsTests { + private static final String[] URL_SUFFIXES = { + "", "-secondary", "-dualstack", "-ipv6", "-secondary-dualstack", "-secondary-ipv6" + }; + + private static final String[] ALL_SUBDOMAINS = { + Constants.Blob.URI_SUBDOMAIN, + Constants.File.URI_SUBDOMAIN, + Constants.Queue.URI_SUBDOMAIN, + Constants.Table.URI_SUBDOMAIN, + Constants.Dfs.URI_SUBDOMAIN + }; + + private static final String[] BLOB_AND_DFS = { + Constants.Blob.URI_SUBDOMAIN, + Constants.Dfs.URI_SUBDOMAIN + }; @ParameterizedTest @MethodSource("exceptionCallables") @@ -29,6 +50,12 @@ void sendRequestThrowsExceptions(Callable operation, Class exceptionCallables() { Callable timeoutCallable = () -> { throw new TimeoutException(); @@ -51,4 +78,36 @@ private static Stream exceptionCallables() { Arguments.of(executionCallable, ExecutionException.class), Arguments.of(interruptedCallable, InterruptedException.class)); } + + private static Stream domainNamesSupplier() throws Exception { + List args = new ArrayList<>(); + + // Standard subdomain variants + for (String subdomain : ALL_SUBDOMAINS) { + args.add(generateURL("accountname", subdomain, "")); + } + + // Secondary, dualstack, and IPv6 suffix variants — blob and dfs only + for (String subdomain : BLOB_AND_DFS) { + for (String suffix : URL_SUFFIXES) { + args.add(generateURL("myaccount", subdomain, suffix)); + } + } + + // Accounts with natural hyphens — should NOT be stripped + for (String subdomain : BLOB_AND_DFS) { + args.add(generateURL("md-d3rqxhqbxbwq", subdomain, "")); + args.add(generateURL("md-ssd-bndub02px100c21", subdomain, "")); + } + + return args.stream(); + } + + private static Arguments generateURL(String account, String subdomain, String suffix) + throws MalformedURLException { + return Arguments.of( + new URL("https://" + account + suffix + "." + subdomain + ".core.windows.net/"), + subdomain, + account); + } } 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-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); From 056e7020963df60ad9f0cce3cca7617ac2ef514b Mon Sep 17 00:00:00 2001 From: browndav Date: Wed, 18 Mar 2026 22:36:21 -0400 Subject: [PATCH 2/7] fix linting issues --- .../implementation/StorageImplUtils.java | 1 - .../implementation/StorageImplUtilsTests.java | 20 ++++++------------- 2 files changed, 6 insertions(+), 15 deletions(-) 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 5777d16225a8..d4f5237db0fa 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,7 +270,6 @@ public static String getAccountName(URL url) { return accountName; } - public static String getAccountName(URL url, String serviceSubDomain) throws MalformedURLException { String host = url.getHost(); return getAccountNameFromHost(host, serviceSubDomain); diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java index 3ba493b22eec..bad12ce47e5e 100644 --- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java +++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java @@ -23,22 +23,17 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class StorageImplUtilsTests { - private static final String[] URL_SUFFIXES = { - "", "-secondary", "-dualstack", "-ipv6", "-secondary-dualstack", "-secondary-ipv6" - }; + private static final String[] URL_SUFFIXES + = { "", "-secondary", "-dualstack", "-ipv6", "-secondary-dualstack", "-secondary-ipv6" }; private static final String[] ALL_SUBDOMAINS = { Constants.Blob.URI_SUBDOMAIN, Constants.File.URI_SUBDOMAIN, Constants.Queue.URI_SUBDOMAIN, Constants.Table.URI_SUBDOMAIN, - Constants.Dfs.URI_SUBDOMAIN - }; + Constants.Dfs.URI_SUBDOMAIN }; - private static final String[] BLOB_AND_DFS = { - Constants.Blob.URI_SUBDOMAIN, - Constants.Dfs.URI_SUBDOMAIN - }; + private static final String[] BLOB_AND_DFS = { Constants.Blob.URI_SUBDOMAIN, Constants.Dfs.URI_SUBDOMAIN }; @ParameterizedTest @MethodSource("exceptionCallables") @@ -103,11 +98,8 @@ private static Stream domainNamesSupplier() throws Exception { return args.stream(); } - private static Arguments generateURL(String account, String subdomain, String suffix) - throws MalformedURLException { - return Arguments.of( - new URL("https://" + account + suffix + "." + subdomain + ".core.windows.net/"), - subdomain, + private static Arguments generateURL(String account, String subdomain, String suffix) throws MalformedURLException { + return Arguments.of(new URL("https://" + account + suffix + "." + subdomain + ".core.windows.net/"), subdomain, account); } } From 3cbaf12d7f67321f39422650a3f5b33b89c807ed Mon Sep 17 00:00:00 2001 From: browndav Date: Thu, 19 Mar 2026 10:40:36 -0400 Subject: [PATCH 3/7] fix linting issues --- .../storage/blob}/BlobUrlPartsTests.java | 6 +++++- .../common/implementation/Constants.java | 20 +++++++++++++++---- .../implementation/StorageImplUtils.java | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) rename sdk/storage/azure-storage-blob/src/test/java/{ => com/azure/storage/blob}/BlobUrlPartsTests.java (93%) diff --git a/sdk/storage/azure-storage-blob/src/test/java/BlobUrlPartsTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobUrlPartsTests.java similarity index 93% rename from sdk/storage/azure-storage-blob/src/test/java/BlobUrlPartsTests.java rename to sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobUrlPartsTests.java index c3cdbb6b8814..0f78db269660 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/BlobUrlPartsTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobUrlPartsTests.java @@ -1,4 +1,8 @@ -import com.azure.storage.blob.BlobUrlParts; +// 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; 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 3d70641df64b..a0e986b6ec78 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 @@ -477,40 +477,52 @@ public static class Blob { /** * Constants for the File service subdomain. */ - public static class File { + 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 class Queue { + 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 class Table { + 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 class Dfs { + 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 d4f5237db0fa..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,7 +270,7 @@ public static String getAccountName(URL url) { return accountName; } - public static String getAccountName(URL url, String serviceSubDomain) throws MalformedURLException { + public static String getAccountName(URL url, String serviceSubDomain) { String host = url.getHost(); return getAccountNameFromHost(host, serviceSubDomain); } From dd794751c67b24681da2ecd5cbfc7a5a4fc1e163 Mon Sep 17 00:00:00 2001 From: browndav Date: Thu, 19 Mar 2026 11:08:48 -0400 Subject: [PATCH 4/7] fix spacing issue in Constant file --- .../azure/storage/common/implementation/Constants.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 a0e986b6ec78..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 @@ -483,7 +483,7 @@ public static final class File { */ public static final String URI_SUBDOMAIN = "file"; - private File(){ + private File() { } } @@ -496,7 +496,7 @@ public static final class Queue { */ public static final String URI_SUBDOMAIN = "queue"; - private Queue(){ + private Queue() { } } @@ -509,7 +509,7 @@ public static final class Table { */ public static final String URI_SUBDOMAIN = "table"; - private Table(){ + private Table() { } } @@ -522,7 +522,7 @@ public static final class Dfs { */ public static final String URI_SUBDOMAIN = "dfs"; - private Dfs(){ + private Dfs() { } } } From 480bb9a93b843798efa1912b0701f4c61ca49131 Mon Sep 17 00:00:00 2001 From: browndav Date: Thu, 19 Mar 2026 16:38:46 -0400 Subject: [PATCH 5/7] add connection string parse test --- .../StorageConnectionStringTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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()); + + } } From 62f526a4fd81de17fbeb0ec96b2ff3c8a63a891f Mon Sep 17 00:00:00 2001 From: browndav Date: Thu, 19 Mar 2026 18:38:47 -0400 Subject: [PATCH 6/7] finish builderhelper tests --- .../storage/blob/BuilderHelperTests.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) 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, From 848dbefda274c373ffa5ddc170a8e6f29b0f00cb Mon Sep 17 00:00:00 2001 From: browndav Date: Thu, 19 Mar 2026 20:34:20 -0400 Subject: [PATCH 7/7] finish refactor of BuilderHelperTests --- .../com/azure/storage/blob/BlobUrlParts.java | 10 +++- .../implementation/StorageImplUtilsTests.java | 51 ------------------ .../file/datalake/BuilderHelperTests.java | 52 +++++++++++++++++++ .../file/share/BuilderHelperTests.java | 46 ++++++++++++++++ .../azure/storage/queue/BuildHelperTests.java | 43 +++++++++++++++ 5 files changed, 149 insertions(+), 53 deletions(-) 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 a22d01d60b39..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 @@ -453,8 +453,14 @@ private static void parseNonIpUrl(URL url, BlobUrlParts parts) { parts.setHost(host); // Parse host to get account name - // host will look like this : .blob.core.windows.net - parts.setAccountName(StorageImplUtils.getAccountNameFromHost(host, Constants.Blob.URI_SUBDOMAIN)); + 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) String path = url.getPath(); diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java index bad12ce47e5e..83418b91668b 100644 --- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java +++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/implementation/StorageImplUtilsTests.java @@ -7,33 +7,17 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.net.MalformedURLException; -import java.net.URL; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; public class StorageImplUtilsTests { - private static final String[] URL_SUFFIXES - = { "", "-secondary", "-dualstack", "-ipv6", "-secondary-dualstack", "-secondary-ipv6" }; - - private static final String[] ALL_SUBDOMAINS = { - Constants.Blob.URI_SUBDOMAIN, - Constants.File.URI_SUBDOMAIN, - Constants.Queue.URI_SUBDOMAIN, - Constants.Table.URI_SUBDOMAIN, - Constants.Dfs.URI_SUBDOMAIN }; - - private static final String[] BLOB_AND_DFS = { Constants.Blob.URI_SUBDOMAIN, Constants.Dfs.URI_SUBDOMAIN }; @ParameterizedTest @MethodSource("exceptionCallables") @@ -45,12 +29,6 @@ void sendRequestThrowsExceptions(Callable operation, Class exceptionCallables() { Callable timeoutCallable = () -> { throw new TimeoutException(); @@ -73,33 +51,4 @@ private static Stream exceptionCallables() { Arguments.of(executionCallable, ExecutionException.class), Arguments.of(interruptedCallable, InterruptedException.class)); } - - private static Stream domainNamesSupplier() throws Exception { - List args = new ArrayList<>(); - - // Standard subdomain variants - for (String subdomain : ALL_SUBDOMAINS) { - args.add(generateURL("accountname", subdomain, "")); - } - - // Secondary, dualstack, and IPv6 suffix variants — blob and dfs only - for (String subdomain : BLOB_AND_DFS) { - for (String suffix : URL_SUFFIXES) { - args.add(generateURL("myaccount", subdomain, suffix)); - } - } - - // Accounts with natural hyphens — should NOT be stripped - for (String subdomain : BLOB_AND_DFS) { - args.add(generateURL("md-d3rqxhqbxbwq", subdomain, "")); - args.add(generateURL("md-ssd-bndub02px100c21", subdomain, "")); - } - - return args.stream(); - } - - private static Arguments generateURL(String account, String subdomain, String suffix) throws MalformedURLException { - return Arguments.of(new URL("https://" + account + suffix + "." + subdomain + ".core.windows.net/"), subdomain, - account); - } } 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/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/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,