diff --git a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsChunkedContentEncodingDecorator.kt b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsChunkedContentEncodingDecorator.kt new file mode 100644 index 0000000000..584242eb5e --- /dev/null +++ b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsChunkedContentEncodingDecorator.kt @@ -0,0 +1,93 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk + +import software.amazon.smithy.aws.traits.HttpChecksumTrait +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.traits.HttpHeaderTrait +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationSection +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.Visibility +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.util.getTrait +import software.amazon.smithy.rust.codegen.core.util.hasStreamingMember + +class AwsChunkedContentEncodingDecorator : ClientCodegenDecorator { + override val name: String = "AwsChunkedContentEncoding" + + // This decorator must decorate after any of the following: + // - HttpRequestChecksumDecorator + // - HttpRequestCompressionDecorator + override val order: Byte = (minOf(HttpRequestChecksumDecorator.ORDER, HttpRequestCompressionDecorator.ORDER) - 1).toByte() + + override fun operationCustomizations( + codegenContext: ClientCodegenContext, + operation: OperationShape, + baseCustomizations: List, + ) = baseCustomizations + AwsChunkedOparationCustomization(codegenContext, operation) +} + +private class AwsChunkedOparationCustomization( + private val codegenContext: ClientCodegenContext, + private val operation: OperationShape, +) : OperationCustomization() { + private val model = codegenContext.model + private val runtimeConfig = codegenContext.runtimeConfig + + override fun section(section: OperationSection) = + writable { + when (section) { + is OperationSection.AdditionalInterceptors -> { + // TODO(https://github.com/smithy-lang/smithy-rs/issues/4382): Remove all of these early returns + // once we have the dedicated trait available in Smithy. + val checksumTrait = operation.getTrait() ?: return@writable + val requestAlgorithmMember = + checksumTrait.requestAlgorithmMemberShape(codegenContext, operation) ?: return@writable + requestAlgorithmMember.getTrait()?.value ?: return@writable + val input = model.expectShape(operation.inputShape, StructureShape::class.java) + if (!input.hasStreamingMember(model)) { + return@writable + } + + section.registerInterceptor(runtimeConfig, this) { + rustTemplate( + """ + #{AwsChunkedContentEncodingInterceptor} + """, + "AwsChunkedContentEncodingInterceptor" to + runtimeConfig.awsChunked() + .resolve("AwsChunkedContentEncodingInterceptor"), + ) + } + } + + else -> emptySection + } + } +} + +private fun RuntimeConfig.awsChunked() = + RuntimeType.forInlineDependency( + InlineAwsDependency.forRustFile( + "aws_chunked", visibility = Visibility.PUBCRATE, + CargoDependency.Bytes, + CargoDependency.Http, + CargoDependency.HttpBody, + CargoDependency.Tracing, + AwsCargoDependency.awsRuntime(this).withFeature("http-02x"), + CargoDependency.smithyRuntimeApiClient(this), + CargoDependency.smithyTypes(this), + AwsCargoDependency.awsSigv4(this), + CargoDependency.TempFile.toDevDependency(), + ), + ) diff --git a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt index 700a9c2651..267fce553b 100644 --- a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt +++ b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt @@ -56,6 +56,7 @@ val DECORATORS: List = SdkConfigDecorator(), ServiceConfigDecorator(), AwsPresigningDecorator(), + AwsChunkedContentEncodingDecorator(), AwsCrateDocsDecorator(), AwsEndpointsStdLib(), *PromotedBuiltInsDecorators, diff --git a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt index b56e1a23ff..d00194a604 100644 --- a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt +++ b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt @@ -55,8 +55,12 @@ internal fun RuntimeConfig.awsInlineableHttpRequestChecksum() = ) class HttpRequestChecksumDecorator : ClientCodegenDecorator { + companion object { + const val ORDER: Byte = 0 + } + override val name: String = "HttpRequestChecksum" - override val order: Byte = 0 + override val order: Byte = ORDER override fun operationCustomizations( codegenContext: ClientCodegenContext, diff --git a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestCompressionDecorator.kt b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestCompressionDecorator.kt index 68ce154968..a2f66a5b81 100644 --- a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestCompressionDecorator.kt +++ b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestCompressionDecorator.kt @@ -21,8 +21,12 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.adhocCustomizat import software.amazon.smithy.rust.codegen.core.util.thenSingletonListOf class HttpRequestCompressionDecorator : ClientCodegenDecorator { + companion object { + const val ORDER: Byte = 0 + } + override val name: String = "HttpRequestCompression" - override val order: Byte = 0 + override val order: Byte = ORDER private fun usesRequestCompression(codegenContext: ClientCodegenContext): Boolean { val index = TopDownIndex.of(codegenContext.model) diff --git a/aws/rust-runtime/Cargo.lock b/aws/rust-runtime/Cargo.lock index d04b1e4437..a71c6cb972 100644 --- a/aws/rust-runtime/Cargo.lock +++ b/aws/rust-runtime/Cargo.lock @@ -113,7 +113,7 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.14" +version = "1.5.15" dependencies = [ "arbitrary", "aws-credential-types", diff --git a/aws/rust-runtime/aws-inlineable/src/aws_chunked.rs b/aws/rust-runtime/aws-inlineable/src/aws_chunked.rs new file mode 100644 index 0000000000..bb647f248f --- /dev/null +++ b/aws/rust-runtime/aws-inlineable/src/aws_chunked.rs @@ -0,0 +1,331 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#![allow(dead_code)] + +use std::fmt; + +use aws_runtime::{ + auth::PayloadSigningOverride, + content_encoding::{header_value::AWS_CHUNKED, AwsChunkedBody, AwsChunkedBodyOptions}, +}; +use aws_smithy_runtime_api::{ + box_error::BoxError, + client::{ + interceptors::{context::BeforeTransmitInterceptorContextMut, Intercept}, + runtime_components::RuntimeComponents, + }, + http::Request, +}; +use aws_smithy_types::{body::SdkBody, config_bag::ConfigBag, error::operation::BuildError}; +use http::{header, HeaderValue}; +use http_body::Body; + +const X_AMZ_DECODED_CONTENT_LENGTH: &str = "x-amz-decoded-content-length"; + +/// Errors related to constructing aws-chunked encoded HTTP requests. +#[derive(Debug)] +enum Error { + UnsizedRequestBody, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsizedRequestBody => write!( + f, + "Only request bodies with a known size can be aws-chunked encoded." + ), + } + } +} + +impl std::error::Error for Error {} + +#[derive(Debug)] +pub(crate) struct AwsChunkedContentEncodingInterceptor; + +impl Intercept for AwsChunkedContentEncodingInterceptor { + fn name(&self) -> &'static str { + "AwsChunkedContentEncodingInterceptor" + } + + fn modify_before_signing( + &self, + context: &mut BeforeTransmitInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + if must_not_use_chunked_encoding(context.request(), cfg) { + tracing::debug!( + "short-circuiting modify_before_signing because chunked encoding must not be used" + ); + return Ok(()); + } + + let original_body_size = if let Some(size) = context + .request() + .headers() + .get(header::CONTENT_LENGTH) + .and_then(|s| s.parse::().ok()) + .or_else(|| context.request().body().size_hint().exact()) + { + size + } else { + return Err(BuildError::other(Error::UnsizedRequestBody))?; + }; + + let chunked_body_options = if let Some(chunked_body_options) = + cfg.get_mut_from_interceptor_state::() + { + let chunked_body_options = std::mem::take(chunked_body_options); + chunked_body_options.with_stream_length(original_body_size) + } else { + AwsChunkedBodyOptions::default().with_stream_length(original_body_size) + }; + + let request = context.request_mut(); + // For for aws-chunked encoding, `x-amz-decoded-content-length` must be set to the original body size. + request.headers_mut().insert( + header::HeaderName::from_static(X_AMZ_DECODED_CONTENT_LENGTH), + HeaderValue::from(original_body_size), + ); + // Other than `x-amz-decoded-content-length`, either `content-length` or `transfer-encoding` + // must be set, but not both. For uses cases we support, we know the original body size and + // can calculate the encoded size, so we set `content-length`. + request.headers_mut().insert( + header::CONTENT_LENGTH, + HeaderValue::from(chunked_body_options.encoded_length()), + ); + // Setting `content-length` above means we must unset `transfer-encoding`. + request.headers_mut().remove(header::TRANSFER_ENCODING); + request.headers_mut().append( + header::CONTENT_ENCODING, + HeaderValue::from_str(AWS_CHUNKED) + .map_err(BuildError::other) + .expect("\"aws-chunked\" will always be a valid HeaderValue"), + ); + + cfg.interceptor_state().store_put(chunked_body_options); + cfg.interceptor_state() + .store_put(PayloadSigningOverride::StreamingUnsignedPayloadTrailer); + + Ok(()) + } + + fn modify_before_transmit( + &self, + ctx: &mut BeforeTransmitInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + if must_not_use_chunked_encoding(ctx.request(), cfg) { + tracing::debug!( + "short-circuiting modify_before_transmit because chunked encoding must not be used" + ); + return Ok(()); + } + + let request = ctx.request_mut(); + + let mut body = { + let body = std::mem::replace(request.body_mut(), SdkBody::taken()); + let opt = cfg + .get_mut_from_interceptor_state::() + .ok_or_else(|| { + BuildError::other("AwsChunkedBodyOptions missing from config bag") + })?; + let aws_chunked_body_options = std::mem::take(opt); + body.map(move |body| { + let body = AwsChunkedBody::new(body, aws_chunked_body_options.clone()); + SdkBody::from_body_0_4(body) + }) + }; + + std::mem::swap(request.body_mut(), &mut body); + + Ok(()) + } +} + +// Determine if chunked encoding must not be used; returns true when any of the following is true: +// - If the body is in-memory +// - If chunked encoding is disabled via `AwsChunkedBodyOptions` +fn must_not_use_chunked_encoding(request: &Request, cfg: &ConfigBag) -> bool { + match (request.body().bytes(), cfg.load::()) { + (Some(_), _) => true, + (_, Some(options)) if options.disabled() => true, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_smithy_runtime_api::client::interceptors::context::{ + BeforeTransmitInterceptorContextMut, Input, InterceptorContext, + }; + use aws_smithy_runtime_api::client::orchestrator::HttpRequest; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; + use aws_smithy_types::byte_stream::ByteStream; + use bytes::BytesMut; + use http_body::Body; + use tempfile::NamedTempFile; + + #[tokio::test] + async fn test_aws_chunked_body_is_retryable() { + use std::io::Write; + let mut file = NamedTempFile::new().unwrap(); + + for i in 0..10000 { + let line = format!("This is a large file created for testing purposes {}", i); + file.as_file_mut().write_all(line.as_bytes()).unwrap(); + } + + let request = HttpRequest::new( + ByteStream::read_from() + .path(&file) + .buffer_size(1024) + .build() + .await + .unwrap() + .into_inner(), + ); + + // ensure original SdkBody is retryable + assert!(request.body().try_clone().is_some()); + + let interceptor = AwsChunkedContentEncodingInterceptor; + let mut cfg = ConfigBag::base(); + cfg.interceptor_state() + .store_put(AwsChunkedBodyOptions::default()); + let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut ctx = InterceptorContext::new(Input::doesnt_matter()); + ctx.enter_serialization_phase(); + let _ = ctx.take_input(); + ctx.set_request(request); + ctx.enter_before_transmit_phase(); + let mut ctx: BeforeTransmitInterceptorContextMut<'_> = (&mut ctx).into(); + interceptor + .modify_before_transmit(&mut ctx, &runtime_components, &mut cfg) + .unwrap(); + + // ensure wrapped SdkBody is retryable + let mut body = ctx.request().body().try_clone().expect("body is retryable"); + + let mut body_data = BytesMut::new(); + while let Some(data) = body.data().await { + body_data.extend_from_slice(&data.unwrap()) + } + let body_str = std::str::from_utf8(&body_data).unwrap(); + assert!(body_str.ends_with("0\r\n\r\n")); + } + + #[tokio::test] + async fn test_short_circuit_modify_before_signing() { + let mut ctx = InterceptorContext::new(Input::doesnt_matter()); + ctx.enter_serialization_phase(); + let _ = ctx.take_input(); + let request = HttpRequest::new(SdkBody::from( + "in-memory body, must not use chunked encoding", + )); + ctx.set_request(request); + ctx.enter_before_transmit_phase(); + let mut ctx: BeforeTransmitInterceptorContextMut<'_> = (&mut ctx).into(); + + let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap(); + + let mut cfg = ConfigBag::base(); + cfg.interceptor_state() + .store_put(AwsChunkedBodyOptions::default()); + + let interceptor = AwsChunkedContentEncodingInterceptor; + interceptor + .modify_before_signing(&mut ctx, &runtime_components, &mut cfg) + .unwrap(); + + let request = ctx.request(); + assert!(request.headers().get(header::CONTENT_ENCODING).is_none()); + assert!(request + .headers() + .get(header::HeaderName::from_static( + X_AMZ_DECODED_CONTENT_LENGTH + )) + .is_none()); + } + + #[tokio::test] + async fn test_short_circuit_modify_before_transmit() { + let mut ctx = InterceptorContext::new(Input::doesnt_matter()); + ctx.enter_serialization_phase(); + let _ = ctx.take_input(); + let request = HttpRequest::new(SdkBody::from( + "in-memory body, must not use chunked encoding", + )); + ctx.set_request(request); + ctx.enter_before_transmit_phase(); + let mut ctx: BeforeTransmitInterceptorContextMut<'_> = (&mut ctx).into(); + + let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap(); + + let mut cfg = ConfigBag::base(); + // Don't need to set the stream length properly because we expect the body won't be wrapped by `AwsChunkedBody`. + cfg.interceptor_state() + .store_put(AwsChunkedBodyOptions::default()); + + let interceptor = AwsChunkedContentEncodingInterceptor; + interceptor + .modify_before_transmit(&mut ctx, &runtime_components, &mut cfg) + .unwrap(); + + let mut body = ctx.request().body().try_clone().expect("body is retryable"); + + let mut body_data = BytesMut::new(); + while let Some(data) = body.data().await { + body_data.extend_from_slice(&data.unwrap()) + } + let body_str = std::str::from_utf8(&body_data).unwrap(); + // Also implies that `assert!(!body_str.ends_with("0\r\n\r\n"));`, i.e., shouldn't see chunked encoding epilogue. + assert_eq!("in-memory body, must not use chunked encoding", body_str); + } + + #[test] + fn test_must_not_use_chunked_encoding_with_in_memory_body() { + let request = HttpRequest::new(SdkBody::from("test body")); + let cfg = ConfigBag::base(); + + assert!(must_not_use_chunked_encoding(&request, &cfg)); + } + + async fn streaming_body(path: impl AsRef) -> SdkBody { + let file = path.as_ref(); + ByteStream::read_from() + .path(&file) + .build() + .await + .unwrap() + .into_inner() + } + + #[tokio::test] + async fn test_must_not_use_chunked_encoding_with_disabled_option() { + let file = NamedTempFile::new().unwrap(); + let request = HttpRequest::new(streaming_body(&file).await); + let mut cfg = ConfigBag::base(); + cfg.interceptor_state() + .store_put(AwsChunkedBodyOptions::disable_chunked_encoding()); + + assert!(must_not_use_chunked_encoding(&request, &cfg)); + } + + #[tokio::test] + async fn test_chunked_encoding_is_used() { + let file = NamedTempFile::new().unwrap(); + let request = HttpRequest::new(streaming_body(&file).await); + let cfg = ConfigBag::base(); + + assert!(!must_not_use_chunked_encoding(&request, &cfg)); + } +} diff --git a/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs b/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs index cd239a0647..c369584000 100644 --- a/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs +++ b/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs @@ -8,27 +8,23 @@ //! Interceptor for handling Smithy `@httpChecksum` request checksumming with AWS SigV4 use crate::presigning::PresigningMarker; -use aws_runtime::auth::PayloadSigningOverride; -use aws_runtime::content_encoding::header_value::AWS_CHUNKED; -use aws_runtime::content_encoding::{AwsChunkedBody, AwsChunkedBodyOptions}; +use aws_runtime::content_encoding::AwsChunkedBodyOptions; +use aws_smithy_checksums::body::calculate; use aws_smithy_checksums::body::ChecksumCache; +use aws_smithy_checksums::http::HttpChecksum; use aws_smithy_checksums::ChecksumAlgorithm; -use aws_smithy_checksums::{body::calculate, http::HttpChecksum}; use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature; use aws_smithy_runtime_api::box_error::BoxError; use aws_smithy_runtime_api::client::interceptors::context::{ BeforeSerializationInterceptorContextMut, BeforeTransmitInterceptorContextMut, Input, }; use aws_smithy_runtime_api::client::interceptors::Intercept; -use aws_smithy_runtime_api::client::orchestrator::HttpRequest; use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; use aws_smithy_runtime_api::http::Request; use aws_smithy_types::body::SdkBody; use aws_smithy_types::checksum_config::RequestChecksumCalculation; -use aws_smithy_types::config_bag::{ConfigBag, Layer, Storable, StoreReplace}; -use aws_smithy_types::error::operation::BuildError; -use http::HeaderValue; -use http_body::Body; +use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace}; +use http::HeaderMap; use std::str::FromStr; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -38,18 +34,12 @@ use std::{fmt, mem}; /// Errors related to constructing checksum-validated HTTP requests #[derive(Debug)] pub(crate) enum Error { - /// Only request bodies with a known size can be checksum validated - UnsizedRequestBody, ChecksumHeadersAreUnsupportedForStreamingBody, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::UnsizedRequestBody => write!( - f, - "Only request bodies with a known size can be checksum validated." - ), Self::ChecksumHeadersAreUnsupportedForStreamingBody => write!( f, "Checksum header insertion is only supported for non-streaming HTTP bodies. \ @@ -61,7 +51,7 @@ impl fmt::Display for Error { impl std::error::Error for Error {} -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] struct RequestChecksumInterceptorState { /// The checksum algorithm to calculate checksum_algorithm: Option, @@ -70,6 +60,19 @@ struct RequestChecksumInterceptorState { calculate_checksum: Arc, checksum_cache: ChecksumCache, } + +impl RequestChecksumInterceptorState { + fn checksum_algorithm(&self) -> Option { + self.checksum_algorithm + .as_ref() + .and_then(|s| ChecksumAlgorithm::from_str(s.as_str()).ok()) + } + + fn calculate_checksum(&self) -> bool { + self.calculate_checksum.load(Ordering::SeqCst) + } +} + impl Storable for RequestChecksumInterceptorState { type Storer = StoreReplace; } @@ -151,14 +154,13 @@ where let (checksum_algorithm, request_checksum_required) = (self.algorithm_provider)(context.input()); - let mut layer = Layer::new("RequestChecksumInterceptor"); - layer.store_put(RequestChecksumInterceptorState { - checksum_algorithm, - request_checksum_required, - checksum_cache: ChecksumCache::new(), - calculate_checksum: Arc::new(AtomicBool::new(false)), - }); - cfg.push_layer(layer); + cfg.interceptor_state() + .store_put(RequestChecksumInterceptorState { + checksum_algorithm, + request_checksum_required, + checksum_cache: ChecksumCache::new(), + calculate_checksum: Arc::new(AtomicBool::new(false)), + }); Ok(()) } @@ -170,20 +172,21 @@ where _runtime_components: &RuntimeComponents, cfg: &mut ConfigBag, ) -> Result<(), BoxError> { - let state = cfg - .load::() - .expect("set in `read_before_serialization`"); - let user_set_checksum_value = (self.checksum_mutator)(context.request_mut(), cfg) .expect("Checksum header mutation should not fail"); + let is_presigned = cfg.load::().is_some(); - // If the user manually set a checksum header we short circuit - if user_set_checksum_value { + // If the user manually set a checksum header or if this is a presigned request, we short circuit + if user_set_checksum_value || is_presigned { + // Disable aws-chunked encoding since either the user has set a custom checksum + cfg.interceptor_state() + .store_put(AwsChunkedBodyOptions::disable_chunked_encoding()); return Ok(()); } - // This value is from the trait, but is needed for runtime logic - let request_checksum_required = state.request_checksum_required; + let state = cfg + .get_mut_from_interceptor_state::() + .expect("set in `read_before_serialization`"); // If the algorithm fails to parse it is not one we support and we error let checksum_algorithm = state @@ -192,74 +195,34 @@ where .map(|s| ChecksumAlgorithm::from_str(s.as_str())) .transpose()?; - // This value is set by the user on the SdkConfig to indicate their preference - // We provide a default here for users that use a client config instead of the SdkConfig - let request_checksum_calculation = cfg - .load::() - .unwrap_or(&RequestChecksumCalculation::WhenSupported); - - // Need to know if this is a presigned req because we do not calculate checksums for those. - let is_presigned_req = cfg.load::().is_some(); - - // Determine if we actually calculate the checksum. If this is a presigned request we do not - // If the user setting is WhenSupported (the default) we always calculate it (because this interceptor - // isn't added if it isn't supported). If it is WhenRequired we only calculate it if the checksum - // is marked required on the trait. - let calculate_checksum = match (request_checksum_calculation, is_presigned_req) { - (_, true) => false, - (RequestChecksumCalculation::WhenRequired, false) => request_checksum_required, - (RequestChecksumCalculation::WhenSupported, false) => true, - _ => true, - }; - - // If a checksum override is set in the ConfigBag we use that instead (currently only used by S3Express) - // If we have made it this far without a checksum being set we set the default (currently Crc32) - let checksum_algorithm = - incorporate_custom_default(checksum_algorithm, cfg).unwrap_or_default(); + let mut state = std::mem::take(state); - if calculate_checksum { + if calculate_checksum(cfg, &state) { state.calculate_checksum.store(true, Ordering::Release); - // Set the user-agent metric for the selected checksum algorithm + // If a checksum override is set in the ConfigBag we use that instead (currently only used by S3Express) + // If we have made it this far without a checksum being set we set the default (currently Crc32) + let checksum_algorithm = + incorporate_custom_default(checksum_algorithm, cfg).unwrap_or_default(); + state.checksum_algorithm = Some(checksum_algorithm.as_str().to_owned()); + // NOTE: We have to do this in modify_before_retry_loop since UA interceptor also runs // in modify_before_signing but is registered before this interceptor (client level vs operation level). - match checksum_algorithm { - ChecksumAlgorithm::Crc32 => { - cfg.interceptor_state() - .store_append(SmithySdkFeature::FlexibleChecksumsReqCrc32); - } - ChecksumAlgorithm::Crc32c => { - cfg.interceptor_state() - .store_append(SmithySdkFeature::FlexibleChecksumsReqCrc32c); - } - ChecksumAlgorithm::Crc64Nvme => { - cfg.interceptor_state() - .store_append(SmithySdkFeature::FlexibleChecksumsReqCrc64); - } - #[allow(deprecated)] - ChecksumAlgorithm::Md5 => { - tracing::warn!(more_info = "Unsupported ChecksumAlgorithm MD5 set"); - } - ChecksumAlgorithm::Sha1 => { - cfg.interceptor_state() - .store_append(SmithySdkFeature::FlexibleChecksumsReqSha1); - } - ChecksumAlgorithm::Sha256 => { - cfg.interceptor_state() - .store_append(SmithySdkFeature::FlexibleChecksumsReqSha256); - } - unsupported => tracing::warn!( - more_info = "Unsupported value of ChecksumAlgorithm detected when setting user-agent metrics", - unsupported = ?unsupported), - } + track_metric_for_selected_checksum_algorithm(cfg, &checksum_algorithm); + } else { + // No checksum calculation needed so disable aws-chunked encoding + cfg.interceptor_state() + .store_put(AwsChunkedBodyOptions::disable_chunked_encoding()); } + cfg.interceptor_state().store_put(state); + Ok(()) } - /// Calculate a checksum and modify the request to include the checksum as a header - /// (for in-memory request bodies) or a trailer (for streaming request bodies). - /// Streaming bodies must be sized or this will return an error. + /// Calculate a checksum and modify the request to do either of the following: + /// - include the checksum as a header for signing with in-memory request bodies. + /// - include the checksum as a trailer for streaming request bodies. fn modify_before_signing( &self, context: &mut BeforeTransmitInterceptorContextMut<'_>, @@ -270,56 +233,88 @@ where .load::() .expect("set in `read_before_serialization`"); - let checksum_cache = state.checksum_cache.clone(); + if !state.calculate_checksum() { + return Ok(()); + } let checksum_algorithm = state - .checksum_algorithm - .clone() - .map(|s| ChecksumAlgorithm::from_str(s.as_str())) - .transpose()?; + .checksum_algorithm() + .expect("set in `modify_before_retry_loop`"); + let mut checksum = checksum_algorithm.into_impl(); - let calculate_checksum = state.calculate_checksum.load(Ordering::SeqCst); - - // Calculate the checksum if necessary - if calculate_checksum { - // If a checksum override is set in the ConfigBag we use that instead (currently only used by S3Express) - // If we have made it this far without a checksum being set we set the default (currently Crc32) - let checksum_algorithm = - incorporate_custom_default(checksum_algorithm, cfg).unwrap_or_default(); + match context.request().body().bytes() { + Some(data) => { + tracing::debug!("applying {checksum_algorithm:?} of the request body as a header"); + checksum.update(data); - let request = context.request_mut(); - add_checksum_for_request_body(request, checksum_algorithm, checksum_cache, cfg)?; + for (hdr_name, hdr_value) in + get_or_cache_headers(checksum.headers(), &state.checksum_cache).iter() + { + context + .request_mut() + .headers_mut() + .insert(hdr_name.clone(), hdr_value.clone()); + } + } + None => { + tracing::debug!("applying {checksum_algorithm:?} of the request body as a trailer"); + context.request_mut().headers_mut().insert( + http::header::HeaderName::from_static("x-amz-trailer"), + checksum.header_name(), + ); + + // Take checksum header into account for `AwsChunkedBodyOptions`'s trailer length + let trailer_len = HttpChecksum::size(checksum.as_ref()); + let chunked_body_options = + AwsChunkedBodyOptions::default().with_trailer_len(trailer_len); + cfg.interceptor_state().store_put(chunked_body_options); + } } Ok(()) } - /// Set the user-agent metrics for `RequestChecksumCalculation` here to avoid ownership issues - /// with the mutable borrow of cfg in `modify_before_signing` - fn read_after_serialization( + fn modify_before_transmit( &self, - _context: &aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextRef<'_>, + ctx: &mut BeforeTransmitInterceptorContextMut<'_>, _runtime_components: &RuntimeComponents, cfg: &mut ConfigBag, ) -> Result<(), BoxError> { - let request_checksum_calculation = cfg - .load::() - .unwrap_or(&RequestChecksumCalculation::WhenSupported); - - match request_checksum_calculation { - RequestChecksumCalculation::WhenSupported => { - cfg.interceptor_state() - .store_append(SmithySdkFeature::FlexibleChecksumsReqWhenSupported); - } - RequestChecksumCalculation::WhenRequired => { - cfg.interceptor_state() - .store_append(SmithySdkFeature::FlexibleChecksumsReqWhenRequired); - } - unsupported => tracing::warn!( - more_info = "Unsupported value of RequestChecksumCalculation when setting user-agent metrics", - unsupported = ?unsupported), + if ctx.request().body().bytes().is_some() { + // Nothing to do for non-streaming bodies since the checksum was added to the the header + // in `modify_before_signing` and signing has already been done by the time this hook is called. + return Ok(()); + } + + let state = cfg + .load::() + .expect("set in `read_before_serialization`"); + + if !state.calculate_checksum() { + return Ok(()); + } + + let request = ctx.request_mut(); + + let mut body = { + let body = mem::replace(request.body_mut(), SdkBody::taken()); + + let checksum_algorithm = state + .checksum_algorithm() + .expect("set in `modify_before_retry_loop`"); + let checksum_cache = state.checksum_cache.clone(); + + body.map(move |body| { + let checksum = checksum_algorithm.into_impl(); + let body = + calculate::ChecksumBody::new(body, checksum).with_cache(checksum_cache.clone()); + + SdkBody::from_body_0_4(body) + }) }; + mem::swap(request.body_mut(), &mut body); + Ok(()) } } @@ -334,181 +329,123 @@ fn incorporate_custom_default( } } -fn add_checksum_for_request_body( - request: &mut HttpRequest, - checksum_algorithm: ChecksumAlgorithm, - checksum_cache: ChecksumCache, - cfg: &mut ConfigBag, -) -> Result<(), BoxError> { - match request.body().bytes() { - // Body is in-memory: read it and insert the checksum as a header. - Some(data) => { - let mut checksum = checksum_algorithm.into_impl(); - - // If the header has not already been set we set it. If it was already set by the user - // we do nothing and maintain their set value. - if request.headers().get(checksum.header_name()).is_none() { - tracing::debug!("applying {checksum_algorithm:?} of the request body as a header"); - checksum.update(data); +fn get_or_cache_headers( + calculated_headers: HeaderMap, + checksum_cache: &ChecksumCache, +) -> HeaderMap { + if let Some(cached_headers) = checksum_cache.get() { + if cached_headers != calculated_headers { + tracing::warn!(cached = ?cached_headers, calculated = ?calculated_headers, "calculated checksum differs from cached checksum!"); + } + cached_headers + } else { + checksum_cache.set(calculated_headers.clone()); + calculated_headers + } +} - let calculated_headers = checksum.headers(); - let checksum_headers = if let Some(cached_headers) = checksum_cache.get() { - if cached_headers != calculated_headers { - tracing::warn!(cached = ?cached_headers, calculated = ?calculated_headers, "calculated checksum differs from cached checksum!"); - } - cached_headers - } else { - checksum_cache.set(calculated_headers.clone()); - calculated_headers - }; - - for (hdr_name, hdr_value) in checksum_headers.iter() { - request - .headers_mut() - .insert(hdr_name.clone(), hdr_value.clone()); - } - } +// Determine if we actually calculate the checksum +fn calculate_checksum(cfg: &mut ConfigBag, state: &RequestChecksumInterceptorState) -> bool { + // This value is set by the user on the SdkConfig to indicate their preference + // We provide a default here for users that use a client config instead of the SdkConfig + let request_checksum_calculation = cfg + .load::() + .unwrap_or(&RequestChecksumCalculation::WhenSupported); + + // If the user setting is WhenSupported (the default) we always calculate it (because this interceptor + // isn't added if it isn't supported). If it is WhenRequired we only calculate it if the checksum + // is marked required on the trait. + match request_checksum_calculation { + RequestChecksumCalculation::WhenRequired => { + cfg.interceptor_state() + .store_append(SmithySdkFeature::FlexibleChecksumsReqWhenRequired); + state.request_checksum_required } - // Body is streaming: wrap the body so it will emit a checksum as a trailer. - None => { - tracing::debug!("applying {checksum_algorithm:?} of the request body as a trailer"); + RequestChecksumCalculation::WhenSupported => { cfg.interceptor_state() - .store_put(PayloadSigningOverride::StreamingUnsignedPayloadTrailer); - wrap_streaming_request_body_in_checksum_calculating_body( - request, - checksum_algorithm, - checksum_cache.clone(), - )?; + .store_append(SmithySdkFeature::FlexibleChecksumsReqWhenSupported); + true + } + unsupported => { + tracing::warn!( + more_info = "Unsupported value of RequestChecksumCalculation when setting user-agent metrics", + unsupported = ?unsupported + ); + true } } - Ok(()) } -fn wrap_streaming_request_body_in_checksum_calculating_body( - request: &mut HttpRequest, - checksum_algorithm: ChecksumAlgorithm, - checksum_cache: ChecksumCache, -) -> Result<(), BuildError> { - let checksum = checksum_algorithm.into_impl(); - - // If the user already set the header value then do nothing and return early - if request.headers().get(checksum.header_name()).is_some() { - return Ok(()); +// Set the user-agent metric for the selected checksum algorithm +fn track_metric_for_selected_checksum_algorithm( + cfg: &mut ConfigBag, + checksum_algorithm: &ChecksumAlgorithm, +) { + match checksum_algorithm { + ChecksumAlgorithm::Crc32 => { + cfg.interceptor_state() + .store_append(SmithySdkFeature::FlexibleChecksumsReqCrc32); + } + ChecksumAlgorithm::Crc32c => { + cfg.interceptor_state() + .store_append(SmithySdkFeature::FlexibleChecksumsReqCrc32c); + } + ChecksumAlgorithm::Crc64Nvme => { + cfg.interceptor_state() + .store_append(SmithySdkFeature::FlexibleChecksumsReqCrc64); + } + #[allow(deprecated)] + ChecksumAlgorithm::Md5 => { + tracing::warn!(more_info = "Unsupported ChecksumAlgorithm MD5 set"); + } + ChecksumAlgorithm::Sha1 => { + cfg.interceptor_state() + .store_append(SmithySdkFeature::FlexibleChecksumsReqSha1); + } + ChecksumAlgorithm::Sha256 => { + cfg.interceptor_state() + .store_append(SmithySdkFeature::FlexibleChecksumsReqSha256); + } + unsupported => tracing::warn!( + more_info = "Unsupported value of ChecksumAlgorithm detected when setting user-agent metrics", + unsupported = ?unsupported), } - - let original_body_size = request - .body() - .size_hint() - .exact() - .ok_or_else(|| BuildError::other(Error::UnsizedRequestBody))?; - - let mut body = { - let body = mem::replace(request.body_mut(), SdkBody::taken()); - - body.map(move |body| { - let checksum = checksum_algorithm.into_impl(); - let trailer_len = HttpChecksum::size(checksum.as_ref()); - let body = - calculate::ChecksumBody::new(body, checksum).with_cache(checksum_cache.clone()); - let aws_chunked_body_options = - AwsChunkedBodyOptions::new(original_body_size, vec![trailer_len]); - - let body = AwsChunkedBody::new(body, aws_chunked_body_options); - - SdkBody::from_body_0_4(body) - }) - }; - - let encoded_content_length = body - .size_hint() - .exact() - .ok_or_else(|| BuildError::other(Error::UnsizedRequestBody))?; - - let headers = request.headers_mut(); - - headers.insert( - http::header::HeaderName::from_static("x-amz-trailer"), - checksum.header_name(), - ); - - headers.insert( - http::header::CONTENT_LENGTH, - HeaderValue::from(encoded_content_length), - ); - headers.insert( - http::header::HeaderName::from_static("x-amz-decoded-content-length"), - HeaderValue::from(original_body_size), - ); - // The target service does not depend on where `aws-chunked` appears in the `Content-Encoding` header, - // as it will ultimately be stripped. - headers.append( - http::header::CONTENT_ENCODING, - HeaderValue::from_str(AWS_CHUNKED) - .map_err(BuildError::other) - .expect("\"aws-chunked\" will always be a valid HeaderValue"), - ); - - mem::swap(request.body_mut(), &mut body); - - Ok(()) } #[cfg(test)] mod tests { - use crate::http_request_checksum::wrap_streaming_request_body_in_checksum_calculating_body; - use aws_smithy_checksums::body::ChecksumCache; + use super::*; use aws_smithy_checksums::ChecksumAlgorithm; + use aws_smithy_runtime_api::client::interceptors::context::{ + BeforeTransmitInterceptorContextMut, InterceptorContext, + }; use aws_smithy_runtime_api::client::orchestrator::HttpRequest; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; use aws_smithy_types::base64; - use aws_smithy_types::body::SdkBody; use aws_smithy_types::byte_stream::ByteStream; use bytes::BytesMut; use http_body::Body; use tempfile::NamedTempFile; - #[tokio::test] - async fn test_checksum_body_is_retryable() { - let input_text = "Hello world"; - let chunk_len_hex = format!("{:X}", input_text.len()); - let mut request: HttpRequest = http::Request::builder() - .body(SdkBody::retryable(move || SdkBody::from(input_text))) - .unwrap() - .try_into() - .unwrap(); - - // ensure original SdkBody is retryable - assert!(request.body().try_clone().is_some()); - - let checksum_algorithm: ChecksumAlgorithm = "crc32".parse().unwrap(); - let checksum_cache = ChecksumCache::new(); - wrap_streaming_request_body_in_checksum_calculating_body( - &mut request, - checksum_algorithm, - checksum_cache, - ) - .unwrap(); - - // ensure wrapped SdkBody is retryable - let mut body = request.body().try_clone().expect("body is retryable"); - - let mut body_data = BytesMut::new(); - while let Some(data) = body.data().await { - body_data.extend_from_slice(&data.unwrap()) + fn create_test_interceptor() -> RequestChecksumInterceptor< + impl Fn(&Input) -> (Option, bool) + Send + Sync, + impl Fn(&mut Request, &ConfigBag) -> Result + Send + Sync, + > { + fn algo(_: &Input) -> (Option, bool) { + (Some("crc32".to_string()), false) } - let body = std::str::from_utf8(&body_data).unwrap(); - assert_eq!( - format!( - "{chunk_len_hex}\r\n{input_text}\r\n0\r\nx-amz-checksum-crc32:i9aeUg==\r\n\r\n" - ), - body - ); + fn mutator(_: &mut Request, _: &ConfigBag) -> Result { + Ok(false) + } + RequestChecksumInterceptor::new(algo, mutator) } #[tokio::test] - async fn test_checksum_body_from_file_is_retryable() { + async fn test_checksum_body_is_retryable() { use std::io::Write; let mut file = NamedTempFile::new().unwrap(); - let checksum_algorithm: ChecksumAlgorithm = "crc32c".parse().unwrap(); + let algorithm_str = "crc32c"; + let checksum_algorithm: ChecksumAlgorithm = algorithm_str.parse().unwrap(); let mut crc32c_checksum = checksum_algorithm.into_impl(); for i in 0..10000 { @@ -518,7 +455,7 @@ mod tests { } let crc32c_checksum = crc32c_checksum.finalize(); - let mut request = HttpRequest::new( + let request = HttpRequest::new( ByteStream::read_from() .path(&file) .buffer_size(1024) @@ -531,27 +468,47 @@ mod tests { // ensure original SdkBody is retryable assert!(request.body().try_clone().is_some()); - let checksum_cache = ChecksumCache::new(); - wrap_streaming_request_body_in_checksum_calculating_body( - &mut request, - checksum_algorithm, - checksum_cache, - ) - .unwrap(); + let interceptor = create_test_interceptor(); + let mut cfg = ConfigBag::base(); + cfg.interceptor_state() + .store_put(RequestChecksumInterceptorState { + checksum_algorithm: Some(algorithm_str.to_string()), + calculate_checksum: Arc::new(AtomicBool::new(true)), + ..Default::default() + }); + let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut ctx = InterceptorContext::new(Input::doesnt_matter()); + ctx.enter_serialization_phase(); + let _ = ctx.take_input(); + ctx.set_request(request); + ctx.enter_before_transmit_phase(); + let mut ctx: BeforeTransmitInterceptorContextMut<'_> = (&mut ctx).into(); + interceptor + .modify_before_transmit(&mut ctx, &runtime_components, &mut cfg) + .unwrap(); // ensure wrapped SdkBody is retryable - let mut body = request.body().try_clone().expect("body is retryable"); + let mut body = ctx.request().body().try_clone().expect("body is retryable"); let mut body_data = BytesMut::new(); while let Some(data) = body.data().await { body_data.extend_from_slice(&data.unwrap()) } - let body = std::str::from_utf8(&body_data).unwrap(); - let expected_checksum = base64::encode(&crc32c_checksum); - let expected = format!("This is a large file created for testing purposes 9999\r\n0\r\nx-amz-checksum-crc32c:{expected_checksum}\r\n\r\n"); + let body_str = std::str::from_utf8(&body_data).unwrap(); + let expected = format!("This is a large file created for testing purposes 9999"); assert!( - body.ends_with(&expected), - "expected {body} to end with '{expected}'" + body_str.ends_with(&expected), + "expected '{body_str}' to end with '{expected}'" ); + let expected_checksum = base64::encode(&crc32c_checksum); + while let Ok(Some(trailer)) = body.trailers().await { + if let Some(header_value) = trailer.get("x-amz-checksum-crc32c") { + let header_value = header_value.to_str().unwrap(); + assert_eq!( + header_value, expected_checksum, + "expected checksum '{header_value}' to match '{expected_checksum}'" + ); + } + } } } diff --git a/aws/rust-runtime/aws-inlineable/src/lib.rs b/aws/rust-runtime/aws-inlineable/src/lib.rs index 039dccdfa5..fd5c34775f 100644 --- a/aws/rust-runtime/aws-inlineable/src/lib.rs +++ b/aws/rust-runtime/aws-inlineable/src/lib.rs @@ -26,6 +26,9 @@ #[allow(dead_code)] pub mod account_id_endpoint; +/// Supporting code for the aws-chunked content encoding. +pub mod aws_chunked; + /// Supporting code to determine auth scheme options based on the `authSchemes` endpoint list property. #[allow(dead_code)] pub mod endpoint_auth; diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index bd312869b6..5a8f4bbcc2 100644 --- a/aws/rust-runtime/aws-runtime/Cargo.toml +++ b/aws/rust-runtime/aws-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-runtime" -version = "1.5.14" +version = "1.5.15" authors = ["AWS Rust SDK Team "] description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly." edition = "2021" diff --git a/aws/rust-runtime/aws-runtime/src/content_encoding.rs b/aws/rust-runtime/aws-runtime/src/content_encoding.rs index e16bd2cf0d..33f1521fb9 100644 --- a/aws/rust-runtime/aws-runtime/src/content_encoding.rs +++ b/aws/rust-runtime/aws-runtime/src/content_encoding.rs @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +use aws_smithy_types::config_bag::{Storable, StoreReplace}; use bytes::{Bytes, BytesMut}; use http_02x::{HeaderMap, HeaderValue}; use http_body_04x::{Body, SizeHint}; @@ -22,7 +23,7 @@ pub mod header_value { } /// Options used when constructing an [`AwsChunkedBody`]. -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] #[non_exhaustive] pub struct AwsChunkedBodyOptions { /// The total size of the stream. Because we only support unsigned encoding @@ -32,6 +33,13 @@ pub struct AwsChunkedBodyOptions { /// The length of each trailer sent within an `AwsChunkedBody`. Necessary in /// order to correctly calculate the total size of the body accurately. trailer_lengths: Vec, + /// Whether the aws-chunked encoding is disabled. This could occur, for instance, + /// if a user specifies a custom checksum, rendering aws-chunked encoding unnecessary. + disabled: bool, +} + +impl Storable for AwsChunkedBodyOptions { + type Storer = StoreReplace; } impl AwsChunkedBodyOptions { @@ -40,6 +48,7 @@ impl AwsChunkedBodyOptions { Self { stream_length, trailer_lengths, + disabled: false, } } @@ -49,11 +58,53 @@ impl AwsChunkedBodyOptions { + (self.trailer_lengths.len() * CRLF.len()) as u64 } - /// Set a trailer len + /// Set the stream length in the options + pub fn with_stream_length(mut self, stream_length: u64) -> Self { + self.stream_length = stream_length; + self + } + + /// Append a trailer length to the options pub fn with_trailer_len(mut self, trailer_len: u64) -> Self { self.trailer_lengths.push(trailer_len); self } + + /// Create a new [`AwsChunkedBodyOptions`] with aws-chunked encoding disabled. + /// + /// When the option is disabled, the body must not be wrapped in an `AwsChunkedBody`. + pub fn disable_chunked_encoding() -> Self { + Self { + disabled: true, + ..Default::default() + } + } + + /// Return whether aws-chunked encoding is disabled. + pub fn disabled(&self) -> bool { + self.disabled + } + + /// Return the length of the body after `aws-chunked` encoding is applied + pub fn encoded_length(&self) -> u64 { + let mut length = 0; + if self.stream_length != 0 { + length += get_unsigned_chunk_bytes_length(self.stream_length); + } + + // End chunk + length += CHUNK_TERMINATOR.len() as u64; + + // Trailers + for len in self.trailer_lengths.iter() { + length += len + CRLF.len() as u64; + } + + // Encoding terminator + length += CRLF.len() as u64; + + length + } } #[derive(Debug, PartialEq, Eq)] @@ -114,26 +165,6 @@ impl AwsChunkedBody { inner_body_bytes_read_so_far: 0, } } - - fn encoded_length(&self) -> u64 { - let mut length = 0; - if self.options.stream_length != 0 { - length += get_unsigned_chunk_bytes_length(self.options.stream_length); - } - - // End chunk - length += CHUNK_TERMINATOR.len() as u64; - - // Trailers - for len in self.options.trailer_lengths.iter() { - length += len + CRLF.len() as u64; - } - - // Encoding terminator - length += CRLF.len() as u64; - - length - } } fn get_unsigned_chunk_bytes_length(payload_length: u64) -> u64 { @@ -297,7 +328,7 @@ where } fn size_hint(&self) -> SizeHint { - SizeHint::with_exact(self.encoded_length()) + SizeHint::with_exact(self.options.encoded_length()) } } diff --git a/gradle.properties b/gradle.properties index 82bc584816..d0333b3cf2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,4 +17,4 @@ allowLocalDeps=false # Avoid registering dependencies/plugins/tasks that are only used for testing purposes isTestingEnabled=true # codegen publication version -codegenVersion=0.1.5 +codegenVersion=0.1.6