diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java index 485b9c553788..84677fcae0cd 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java @@ -676,7 +676,7 @@ private MethodSpec setMetricValuesMethod() { b.endControlFlow(); if (endpointRulesSpecUtils.isS3()) { - b.addStatement("$T.addS3ExpressBusinessMetricIfApplicable(executionAttributes)", + b.addStatement("$T.addS3ExpressBusinessMetricIfApplicable(endpoint, executionAttributes)", ClassName.get("software.amazon.awssdk.services.s3.internal.s3express", "S3ExpressUtils")); } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java index 4f41247a84c1..1fd62459632f 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java @@ -38,8 +38,8 @@ import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider; import software.amazon.awssdk.awscore.endpoint.DualstackEnabledProvider; import software.amazon.awssdk.awscore.endpoint.FipsEnabledProvider; -import software.amazon.awssdk.awscore.internal.defaultsmode.DefaultsModeConfiguration; import software.amazon.awssdk.awscore.endpoints.AwsEndpointProviderUtils; +import software.amazon.awssdk.awscore.internal.defaultsmode.DefaultsModeConfiguration; import software.amazon.awssdk.core.ClientEndpointProvider; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java index 2436a27487fa..5c5317e4d40a 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java @@ -430,7 +430,7 @@ public void afterMarshalling(Context.AfterMarshalling context, .put(SIGNING_REGION, executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION)) .put(S3InternalSdkHttpExecutionAttribute.OBJECT_FILE_PATH, executionAttributes.getAttribute(OBJECT_FILE_PATH)) - .put(USE_S3_EXPRESS_AUTH, S3ExpressUtils.useS3ExpressAuthScheme(executionAttributes)) + .put(USE_S3_EXPRESS_AUTH, S3ExpressUtils.isS3ExpressAuthRequest(context.request(), executionAttributes)) .put(SIGNING_NAME, executionAttributes.getAttribute(SERVICE_SIGNING_NAME)) .put(REQUEST_CHECKSUM_CALCULATION, executionAttributes.getAttribute(SdkInternalExecutionAttribute.REQUEST_CHECKSUM_CALCULATION)) diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/EnableTrailingChecksumInterceptor.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/EnableTrailingChecksumInterceptor.java index 098ecff45f5f..e38cf40b71ff 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/EnableTrailingChecksumInterceptor.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/EnableTrailingChecksumInterceptor.java @@ -48,7 +48,7 @@ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttribut SdkRequest request = context.request(); if (getObjectChecksumEnabledPerRequest(request, executionAttributes) - && S3ExpressUtils.useS3Express(executionAttributes)) { + && S3ExpressUtils.isS3ExpressBucket(request)) { return ((GetObjectRequest) request).toBuilder().checksumMode(ChecksumMode.ENABLED).build(); } return request; @@ -63,7 +63,7 @@ public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { if (getObjectChecksumEnabledPerRequest(context.request(), executionAttributes) - && !S3ExpressUtils.useS3Express(executionAttributes)) { + && !S3ExpressUtils.isS3ExpressBucket(context.request())) { return context.httpRequest() .toBuilder() .putHeader(ENABLE_CHECKSUM_REQUEST_HEADER, ENABLE_MD5_CHECKSUM_HEADER_VALUE) diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java index 441e575687e4..42c7928db434 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java @@ -17,10 +17,13 @@ import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME; +import java.util.List; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.SelectedAuthScheme; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.core.spi.identity.AuthSchemeOptionsResolver; import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.endpoints.Endpoint; import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption; @@ -31,26 +34,37 @@ public final class S3ExpressUtils { public static final String S3_EXPRESS = "S3Express"; + private static final String S3_EXPRESS_BUCKET_SUFFIX = "--x-s3"; private S3ExpressUtils() { } /** - * Returns true if the resolved endpoint contains S3Express, else false. + * Determines if this request targets an S3Express bucket by checking the bucket name suffix. */ - public static boolean useS3Express(ExecutionAttributes executionAttributes) { - Endpoint endpoint = executionAttributes.getAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT); - if (endpoint != null) { - String useS3Express = endpoint.attribute(KnownS3ExpressEndpointProperty.BACKEND); - return S3_EXPRESS.equals(useS3Express); + public static boolean isS3ExpressBucket(SdkRequest request) { + return request.getValueForField("Bucket", String.class) + .map(b -> b.endsWith(S3_EXPRESS_BUCKET_SUFFIX)) + .orElse(false); + } + + /** + * Determines if this request uses S3Express auth by checking the auth scheme options. + */ + public static boolean isS3ExpressAuthRequest(SdkRequest request, ExecutionAttributes executionAttributes) { + AuthSchemeOptionsResolver resolver = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.AUTH_SCHEME_OPTIONS_RESOLVER); + if (resolver != null) { + List options = resolver.resolve(request); + return options.stream().anyMatch(o -> S3ExpressAuthScheme.SCHEME_ID.equals(o.schemeId())); } return false; } /** - * Whether aws.auth#sigv4-s3express is used or not + * Whether aws.auth#sigv4-s3express is the selected auth scheme. */ - public static boolean useS3ExpressAuthScheme(ExecutionAttributes executionAttributes) { + private static boolean useS3ExpressAuthScheme(ExecutionAttributes executionAttributes) { SelectedAuthScheme selectedAuthScheme = executionAttributes.getAttribute(SELECTED_AUTH_SCHEME); if (selectedAuthScheme != null) { AuthSchemeOption authSchemeOption = selectedAuthScheme.authSchemeOption(); @@ -62,8 +76,10 @@ public static boolean useS3ExpressAuthScheme(ExecutionAttributes executionAttrib /** * Adds S3 Express business metric if applicable for the current operation. */ - public static void addS3ExpressBusinessMetricIfApplicable(ExecutionAttributes executionAttributes) { - if (executionAttributes != null && useS3Express(executionAttributes) && useS3ExpressAuthScheme(executionAttributes)) { + public static void addS3ExpressBusinessMetricIfApplicable(Endpoint endpoint, ExecutionAttributes executionAttributes) { + if (endpoint != null && executionAttributes != null + && S3_EXPRESS.equals(endpoint.attribute(KnownS3ExpressEndpointProperty.BACKEND)) + && useS3ExpressAuthScheme(executionAttributes)) { executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS) .ifPresent(businessMetrics -> businessMetrics.addMetric(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value())); diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java index 143c91be4826..d0374419ef98 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java @@ -44,10 +44,10 @@ import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider; import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; +import software.amazon.awssdk.awscore.endpoints.AwsEndpointProviderUtils; import software.amazon.awssdk.awscore.endpoints.authscheme.EndpointAuthScheme; import software.amazon.awssdk.awscore.internal.AwsExecutionContextBuilder; import software.amazon.awssdk.awscore.internal.defaultsmode.DefaultsModeConfiguration; -import software.amazon.awssdk.awscore.endpoints.AwsEndpointProviderUtils; import software.amazon.awssdk.awscore.presigner.PresignRequest; import software.amazon.awssdk.awscore.presigner.PresignedRequest; import software.amazon.awssdk.core.ClientEndpointProvider; diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/S3ExpressCreateSessionTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/S3ExpressCreateSessionTest.java index 815736f9d1e6..45c1f05f65ea 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/S3ExpressCreateSessionTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/S3ExpressCreateSessionTest.java @@ -316,7 +316,11 @@ private static final class PathStyleEnforcingInterceptor implements ExecutionInt public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { SdkHttpRequest sdkHttpRequest = context.httpRequest(); String host = sdkHttpRequest.host(); - String bucket = host.substring(0, host.indexOf(".localhost")); + int idx = host.indexOf(".localhost"); + if (idx < 0) { + return sdkHttpRequest; + } + String bucket = host.substring(0, idx); return sdkHttpRequest.toBuilder().host("localhost") .encodedPath(SdkHttpUtils.appendUri(bucket, sdkHttpRequest.encodedPath())) diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/S3ExpressTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/S3ExpressTest.java index 10414df6c14f..8c4a782f4e01 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/S3ExpressTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/functionaltests/S3ExpressTest.java @@ -411,7 +411,11 @@ private static final class PathStyleEnforcingInterceptor implements ExecutionInt public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { SdkHttpRequest sdkHttpRequest = context.httpRequest(); String host = sdkHttpRequest.host(); - String bucket = host.substring(0, host.indexOf(".localhost")); + int idx = host.indexOf(".localhost"); + if (idx < 0) { + return sdkHttpRequest; + } + String bucket = host.substring(0, idx); return sdkHttpRequest.toBuilder().host("localhost") .encodedPath(SdkHttpUtils.appendUri(bucket, sdkHttpRequest.encodedPath())) diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClientTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClientTest.java index 0276d116b16b..ca34f9c045ca 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClientTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClientTest.java @@ -22,7 +22,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.auth.signer.AwsS3V4Signer; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; @@ -31,8 +33,10 @@ import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.http.SdkHttpExecutionAttributes; import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.DelegatingS3AsyncClient; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.endpoints.S3ClientContextParams; @@ -161,4 +165,75 @@ void build_withAdvancedOptions() { assertThat(client).isInstanceOf(DefaultS3CrtAsyncClient.class); } } + + @Test + void s3ExpressBucket_defaultConfig_useS3ExpressAuthIsTrue() { + AtomicReference capturedUseS3ExpressAuth = new AtomicReference<>(); + + ExecutionInterceptor captor = new ExecutionInterceptor() { + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + SdkHttpExecutionAttributes httpAttrs = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.SDK_HTTP_EXECUTION_ATTRIBUTES); + if (httpAttrs != null) { + capturedUseS3ExpressAuth.set(httpAttrs.getAttribute(S3InternalSdkHttpExecutionAttribute.USE_S3_EXPRESS_AUTH)); + } + throw new RuntimeException("STOP"); + } + }; + + DefaultS3CrtAsyncClient.DefaultS3CrtClientBuilder builder = + (DefaultS3CrtAsyncClient.DefaultS3CrtClientBuilder) S3CrtAsyncClient.builder(); + builder.addExecutionInterceptor(captor); + + try (S3AsyncClient client = builder + .region(Region.US_EAST_1) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("key", "secret"))) + .build()) { + + assertThatThrownBy(() -> client.getObject( + r -> r.bucket("my-bucket--usw2-az1--x-s3").key("key"), + AsyncResponseTransformer.toBytes()).join()) + .hasMessageContaining("STOP"); + } + + assertThat(capturedUseS3ExpressAuth.get()).isTrue(); + } + + @Test + void s3ExpressBucket_disableS3ExpressSessionAuth_useS3ExpressAuthIsFalse() { + AtomicReference capturedUseS3ExpressAuth = new AtomicReference<>(); + + ExecutionInterceptor captor = new ExecutionInterceptor() { + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + SdkHttpExecutionAttributes httpAttrs = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.SDK_HTTP_EXECUTION_ATTRIBUTES); + if (httpAttrs != null) { + capturedUseS3ExpressAuth.set(httpAttrs.getAttribute(S3InternalSdkHttpExecutionAttribute.USE_S3_EXPRESS_AUTH)); + } + throw new RuntimeException("STOP"); + } + }; + + DefaultS3CrtAsyncClient.DefaultS3CrtClientBuilder builder = + (DefaultS3CrtAsyncClient.DefaultS3CrtClientBuilder) S3CrtAsyncClient.builder(); + builder.addExecutionInterceptor(captor); + + try (S3AsyncClient client = builder + .region(Region.US_EAST_1) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("key", "secret"))) + .disableS3ExpressSessionAuth(true) + .build()) { + + assertThatThrownBy(() -> client.getObject( + r -> r.bucket("my-bucket--usw2-az1--x-s3").key("key"), + AsyncResponseTransformer.toBytes()).join()) + .hasMessageContaining("STOP"); + } + + assertThat(capturedUseS3ExpressAuth.get()).isFalse(); + } } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressCacheFunctionalTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressCacheFunctionalTest.java index 22b3e3983b71..a91887cb8deb 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressCacheFunctionalTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressCacheFunctionalTest.java @@ -44,6 +44,7 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; import software.amazon.awssdk.core.SelectedAuthScheme; import software.amazon.awssdk.core.interceptor.Context; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; @@ -278,21 +279,29 @@ public List> apiCredentialsProviders() @Override public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) { - IdentityProviders providers = executionAttributes.getAttribute(SdkInternalExecutionAttribute.IDENTITY_PROVIDERS); - IdentityProvider awsCredentialsIdentityIdentityProvider = - providers.identityProvider(AwsCredentialsIdentity.class); + + IdentityProvider credentialsProvider = context.request() + .overrideConfiguration() + .filter(c -> c instanceof AwsRequestOverrideConfiguration) + .map(c -> (AwsRequestOverrideConfiguration) c) + .flatMap(AwsRequestOverrideConfiguration::credentialsIdentityProvider) + .map(p -> (IdentityProvider) p) + .orElseGet(() -> { + IdentityProviders providers = executionAttributes.getAttribute(SdkInternalExecutionAttribute.IDENTITY_PROVIDERS); + return providers.identityProvider(AwsCredentialsIdentity.class); + }); String operationName = executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); if (operationName.equalsIgnoreCase("createsession")) { sessionRequests++; - sessionCredentialsProvider.add(awsCredentialsIdentityIdentityProvider); + sessionCredentialsProvider.add(credentialsProvider); } else { - apiCredentialsProvider.add(awsCredentialsIdentityIdentityProvider); + apiCredentialsProvider.add(credentialsProvider); } } @Override - public void beforeMarshalling(Context.BeforeMarshalling context, ExecutionAttributes executionAttributes) { + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { SelectedAuthScheme attribute = executionAttributes.getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME); CompletableFuture identity = attribute.identity(); @@ -311,7 +320,11 @@ private static final class PathStyleEnforcingInterceptor implements ExecutionInt public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { SdkHttpRequest sdkHttpRequest = context.httpRequest(); String host = sdkHttpRequest.host(); - String bucket = host.substring(0, host.indexOf(".localhost")); + int idx = host.indexOf(".localhost"); + if (idx < 0) { + return sdkHttpRequest; + } + String bucket = host.substring(0, idx); return sdkHttpRequest.toBuilder().host("localhost") .encodedPath(SdkHttpUtils.appendUri(bucket, sdkHttpRequest.encodedPath())) diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtilsTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtilsTest.java new file mode 100644 index 000000000000..53d84f42ad66 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtilsTest.java @@ -0,0 +1,111 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.services.s3.internal.s3express; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; + +class S3ExpressUtilsTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void isS3ExpressBucket_bucketWithS3ExpressSuffix_returnsTrue() { + GetObjectRequest request = GetObjectRequest.builder() + .bucket("my-bucket--use1-az1--x-s3") + .key("key") + .build(); + assertThat(S3ExpressUtils.isS3ExpressBucket(request)).isTrue(); + } + + @Test + void isS3ExpressBucket_regularBucket_returnsFalse() { + GetObjectRequest request = GetObjectRequest.builder() + .bucket("my-regular-bucket") + .key("key") + .build(); + assertThat(S3ExpressUtils.isS3ExpressBucket(request)).isFalse(); + } + + @Test + void isS3ExpressBucket_noBucketField_returnsFalse() { + GetObjectRequest request = GetObjectRequest.builder() + .key("key") + .build(); + assertThat(S3ExpressUtils.isS3ExpressBucket(request)).isFalse(); + } + + /** + * Validates that the S3Express bucket suffix used in {@link S3ExpressUtils#isS3ExpressBucket} matches the suffix + * defined in the endpoint ruleset. + */ + @Test + void isS3ExpressBucket_suffixMatchesEndpointRuleset() throws IOException { + String rulesetSuffix = extractBucketSuffixFromRuleset(); + assertThat(rulesetSuffix).isEqualTo("--x-s3"); + GetObjectRequest request = GetObjectRequest.builder() + .bucket("test-bucket" + rulesetSuffix) + .key("key") + .build(); + assertThat(S3ExpressUtils.isS3ExpressBucket(request)) + .as("isS3ExpressBucket should recognize the suffix '%s' from the endpoint ruleset", rulesetSuffix) + .isTrue(); + } + + /** + * Parses the endpoint-rule-set.json and extracts the S3Express bucket suffix. + */ + private String extractBucketSuffixFromRuleset() throws IOException { + Path rulesetPath = Paths.get("src/main/resources/codegen-resources/endpoint-rule-set.json"); + assertThat(rulesetPath.toFile()).as("endpoint-rule-set.json should exist").exists(); + JsonNode root = MAPPER.readTree(rulesetPath.toFile()); + List suffixes = new ArrayList<>(); + findBucketSuffixValues(root, suffixes); + assertThat(suffixes) + .as("Expected exactly one bucketSuffix stringEquals check in the endpoint ruleset") + .hasSize(1); + return suffixes.get(0); + } + + private void findBucketSuffixValues(JsonNode node, List results) { + if (node.isObject() && "stringEquals".equals(node.path("fn").asText(null))) { + JsonNode argv = node.path("argv"); + if (argv.isArray() && argv.size() == 2) { + for (int i = 0; i < 2; i++) { + JsonNode arg = argv.get(i); + JsonNode other = argv.get(1 - i); + if (arg.isObject() && "bucketSuffix".equals(arg.path("ref").asText(null)) + && other.isTextual()) { + results.add(other.asText()); + return; + } + } + } + } + for (JsonNode child : node) { + findBucketSuffixValues(child, results); + } + } +}