diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index bb0502deba..9aef618b1e 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -22,3 +22,9 @@ message = "Add support for SSO credentials" references = ["smithy-rs#1051", "aws-sdk-rust#4"] meta = { "breaking" = false, "tada" = true, "bug" = false } author = "rcoh" + +[[smithy-rs]] +message = "Upgraded Smithy to 1.16.1" +references = ["smithy-rs#1053"] +meta = { "breaking" = false, "tada" = false, "bug" = false } +author = "jdisanti" diff --git a/aws/sdk-codegen-test/build.gradle.kts b/aws/sdk-codegen-test/build.gradle.kts index 9ab90a2784..eb5ec54239 100644 --- a/aws/sdk-codegen-test/build.gradle.kts +++ b/aws/sdk-codegen-test/build.gradle.kts @@ -14,7 +14,6 @@ plugins { val smithyVersion: String by project - dependencies { implementation(project(":aws:sdk-codegen")) implementation("software.amazon.smithy:smithy-aws-protocol-tests:$smithyVersion") @@ -55,7 +54,6 @@ fun generateSmithyBuild(tests: List): String { """ } - task("generateSmithyBuild") { description = "generate smithy-build.json" doFirst { @@ -63,7 +61,6 @@ task("generateSmithyBuild") { } } - fun generateCargoWorkspace(tests: List): String { return """ [workspace] @@ -82,7 +79,6 @@ task("generateCargoWorkspace") { tasks["smithyBuildJar"].dependsOn("generateSmithyBuild") tasks["assemble"].finalizedBy("generateCargoWorkspace") - tasks.register("cargoCheck") { workingDir("build/smithyprojections/sdk-codegen-test/") // disallow warnings diff --git a/build.gradle.kts b/build.gradle.kts index 9281d42c10..e5eb24bc62 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { } val lintPaths = listOf( - "codegen/src/**/*.kt" + "codegen/src/**/*.kt" ) tasks.register("ktlint") { diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt index aad4fdc121..ea78b93fdf 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt @@ -142,20 +142,22 @@ class ServerProtocolTestGenerator( // This function applies a "fix function" to each broken test before we synthesize it. // Broken tests are those whose definitions in the `awslabs/smithy` repository are wrong, usually because they have // not been written with a server-side perspective in mind. - private fun List.fixBroken(): List = this.map { when (it) { - is TestCase.RequestTest -> { - val howToFixIt = BrokenRequestTests[Pair(codegenContext.serviceShape.id.toString(), it.testCase.id)] - if (howToFixIt == null) { + private fun List.fixBroken(): List = this.map { + when (it) { + is TestCase.RequestTest -> { + val howToFixIt = BrokenRequestTests[Pair(codegenContext.serviceShape.id.toString(), it.testCase.id)] + if (howToFixIt == null) { + it + } else { + val fixed = howToFixIt(it.testCase) + TestCase.RequestTest(fixed, it.targetShape) + } + } + is TestCase.ResponseTest -> { it - } else { - val fixed = howToFixIt(it.testCase) - TestCase.RequestTest(fixed, it.targetShape) } } - is TestCase.ResponseTest -> { - it - } - } } + } private fun renderTestCaseBlock( testCase: HttpMessageTestCase, @@ -431,6 +433,11 @@ class ServerProtocolTestGenerator( private val AwsQuery = "aws.protocoltests.query#AwsQuery" private val Ec2Query = "aws.protocoltests.ec2#AwsEc2" private val ExpectFail = setOf( + FailingTest(RestJson, "RestJsonInputAndOutputWithQuotedStringHeaders", Action.Request), + FailingTest(RestJson, "RestJsonInputAndOutputWithQuotedStringHeaders", Action.Response), + FailingTest(RestJson, "RestJsonOutputUnionWithUnitMember", Action.Response), + FailingTest(RestJson, "RestJsonUnitInputAllowsAccept", Action.Request), + FailingTest(RestJson, "RestJsonUnitInputAndOutputNoOutput", Action.Response), FailingTest(RestJson, "RestJsonAllQueryStringTypes", Action.Request), FailingTest(RestJson, "RestJsonQueryStringEscaping", Action.Request), FailingTest(RestJson, "RestJsonSupportsNaNFloatQueryValues", Action.Request), @@ -572,38 +579,50 @@ class ServerProtocolTestGenerator( // to any "expected" value. // Reference: https://doc.rust-lang.org/std/primitive.f32.html // Request for guidance about this test to Smithy team: https://github.com/awslabs/smithy/pull/1040#discussion_r780418707 - val params = Node.parse("""{ - "queryFloat": "NaN", - "queryDouble": "NaN", - "queryParamsMapOfStringList": { - "Float": ["NaN"], - "Double": ["NaN"] + val params = Node.parse( + """ + { + "queryFloat": "NaN", + "queryDouble": "NaN", + "queryParamsMapOfStringList": { + "Float": ["NaN"], + "Double": ["NaN"] + } } - }""".trimIndent()).asObjectNode().get() + """.trimIndent() + ).asObjectNode().get() return testCase.toBuilder().params(params).build() } private fun fixRestJsonSupportsInfinityFloatQueryValues(testCase: HttpRequestTestCase): HttpRequestTestCase = testCase.toBuilder().params( - Node.parse("""{ - "queryFloat": "Infinity", - "queryDouble": "Infinity", - "queryParamsMapOfStringList": { - "Float": ["Infinity"], - "Double": ["Infinity"] - } - }""".trimMargin()).asObjectNode().get() + Node.parse( + """ + { + "queryFloat": "Infinity", + "queryDouble": "Infinity", + "queryParamsMapOfStringList": { + "Float": ["Infinity"], + "Double": ["Infinity"] + } + } + """.trimMargin() + ).asObjectNode().get() ).build() private fun fixRestJsonSupportsNegativeInfinityFloatQueryValues(testCase: HttpRequestTestCase): HttpRequestTestCase = testCase.toBuilder().params( - Node.parse("""{ - "queryFloat": "-Infinity", - "queryDouble": "-Infinity", - "queryParamsMapOfStringList": { - "Float": ["-Infinity"], - "Double": ["-Infinity"] - } - }""".trimMargin()).asObjectNode().get() + Node.parse( + """ + { + "queryFloat": "-Infinity", + "queryDouble": "-Infinity", + "queryParamsMapOfStringList": { + "Float": ["-Infinity"], + "Double": ["-Infinity"] + } + } + """.trimMargin() + ).asObjectNode().get() ).build() // These are tests whose definitions in the `awslabs/smithy` repository are wrong. diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJson.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJson.kt index 1704e507f5..e83718884a 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJson.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerRestJson.kt @@ -22,6 +22,7 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolContentTypes import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolGeneratorFactory import software.amazon.smithy.rust.codegen.smithy.protocols.parse.JsonParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator +import software.amazon.smithy.rust.codegen.smithy.protocols.restJsonFieldName import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.JsonSerializerGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator @@ -75,11 +76,11 @@ class ServerRestJson(private val codegenContext: CodegenContext) : Protocol { override val defaultTimestampFormat: TimestampFormatTrait.Format = TimestampFormatTrait.Format.EPOCH_SECONDS override fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator { - return JsonParserGenerator(codegenContext, httpBindingResolver) + return JsonParserGenerator(codegenContext, httpBindingResolver, ::restJsonFieldName) } override fun structuredDataSerializer(operationShape: OperationShape): StructuredDataSerializerGenerator { - return JsonSerializerGenerator(codegenContext, httpBindingResolver) + return JsonSerializerGenerator(codegenContext, httpBindingResolver, ::restJsonFieldName) } // NOTE: this method is only needed for the little part of client-codegen we use in tests. diff --git a/codegen-test/model/rest-json-extras.smithy b/codegen-test/model/rest-json-extras.smithy index 9923738334..3b85641643 100644 --- a/codegen-test/model/rest-json-extras.smithy +++ b/codegen-test/model/rest-json-extras.smithy @@ -7,6 +7,46 @@ use aws.api#service use smithy.test#httpRequestTests use smithy.test#httpResponseTests +// TODO(https://github.com/awslabs/smithy/pull/1049): Remove this once the test case in Smithy is fixed +apply InputAndOutputWithHeaders @httpResponseTests([ + { + id: "FIXED_RestJsonInputAndOutputWithQuotedStringHeaders", + documentation: "Tests responses with string list header bindings that require quoting", + protocol: restJson1, + code: 200, + headers: { + "X-StringList": "\"b,c\", \"\\\"def\\\"\", a" + }, + params: { + headerStringList: ["b,c", "\"def\"", "a"] + } + } +]) + +// TODO(https://github.com/awslabs/smithy/pull/1042): Remove this once the test case in Smithy is fixed +apply PostPlayerAction @httpRequestTests([ + { + id: "FIXED_RestJsonInputUnionWithUnitMember", + documentation: "Unit types in unions are serialized like normal structures in requests.", + protocol: restJson1, + method: "POST", + "uri": "/PostPlayerInput", + body: """ + { + "action": { + "quit": {} + } + }""", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + action: { + quit: {} + } + } + } +]) + apply QueryPrecedence @httpRequestTests([ { id: "UrlParamsKeyEncoding", @@ -64,6 +104,8 @@ service RestJsonExtras { NullInNonSparse, CaseInsensitiveErrorOperation, EmptyStructWithContentOnWireOp, + // TODO(https://github.com/awslabs/smithy/pull/1042): Remove this once the test case in Smithy is fixed + PostPlayerAction ], errors: [ExtraError] } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt index cf513725f8..ca5f38614e 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt @@ -77,6 +77,7 @@ class RequestBindingGenerator( ) { private val index = HttpBindingIndex.of(model) private val Encoder = CargoDependency.SmithyTypes(runtimeConfig).asType().member("primitive::Encoder") + private val headerUtil = CargoDependency.SmithyHttp(runtimeConfig).asType().member("header") private val codegenScope = arrayOf( "BuildError" to runtimeConfig.operationBuildError(), @@ -175,6 +176,7 @@ class RequestBindingGenerator( else -> UNREACHABLE("unexpected member for prefix headers: $memberType") } ifSet(memberType, memberSymbol, "&_input.$memberName") { field -> + val listHeader = memberType is CollectionShape rustTemplate( """ for (k, v) in $field { @@ -183,8 +185,8 @@ class RequestBindingGenerator( #{build_error}::InvalidField { field: ${memberName.dq()}, details: format!("`{}` cannot be used as a header name: {}", k, err)} })?; use std::convert::TryFrom; - let header_value = ${headerFmtFun(this, target, memberShape, "v")}; - let header_value = http::header::HeaderValue::try_from(header_value).map_err(|err| { + let header_value = ${headerFmtFun(this, target, memberShape, "v", listHeader)}; + let header_value = http::header::HeaderValue::try_from(&*header_value).map_err(|err| { #{build_error}::InvalidField { field: ${memberName.dq()}, details: format!("`{}` cannot be used as a header value: {}", ${ @@ -210,12 +212,13 @@ class RequestBindingGenerator( val memberSymbol = symbolProvider.toSymbol(memberShape) val memberName = symbolProvider.toMemberName(memberShape) ifSet(memberType, memberSymbol, "&_input.$memberName") { field -> + val isListHeader = memberType is CollectionShape listForEach(memberType, field) { innerField, targetId -> val innerMemberType = model.expectShape(targetId) if (innerMemberType.isPrimitive()) { rust("let mut encoder = #T::from(${autoDeref(innerField)});", Encoder) } - val formatted = headerFmtFun(this, innerMemberType, memberShape, innerField) + val formatted = headerFmtFun(this, innerMemberType, memberShape, innerField, isListHeader) val safeName = safeName("formatted") write("let $safeName = $formatted;") rustBlock("if !$safeName.is_empty()") { @@ -244,21 +247,30 @@ class RequestBindingGenerator( /** * Format [member] in the when used as an HTTP header */ - private fun headerFmtFun(writer: RustWriter, target: Shape, member: MemberShape, targetName: String): String { + private fun headerFmtFun(writer: RustWriter, target: Shape, member: MemberShape, targetName: String, isListHeader: Boolean): String { + fun quoteValue(value: String): String { + // Timestamp shapes are not quoted in header lists + return if (isListHeader && !target.isTimestampShape) { + val quoteFn = writer.format(headerUtil.member("quote_header_value")) + "$quoteFn($value)" + } else { + value + } + } return when { target.isStringShape -> { if (target.hasTrait()) { val func = writer.format(RuntimeType.Base64Encode(runtimeConfig)) "$func(&$targetName)" } else { - "AsRef::::as_ref($targetName)" + quoteValue("AsRef::::as_ref($targetName)") } } target.isTimestampShape -> { val timestampFormat = index.determineTimestampFormat(member, HttpBinding.Location.HEADER, defaultTimestampFormat) val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) - "$targetName.fmt(${writer.format(timestampFormatType)})?" + quoteValue("$targetName.fmt(${writer.format(timestampFormatType)})?") } target.isListShape || target.isMemberShape -> { throw IllegalArgumentException("lists should be handled at a higher level") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGenerator.kt index 5c74c0605d..4a4fbd1174 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/protocol/ProtocolTestGenerator.kt @@ -308,9 +308,9 @@ class ProtocolTestGenerator( rust( """ assert_eq!( - parsed.$memberName.collect().await.unwrap().into_bytes(), - expected_output.$memberName.collect().await.unwrap().into_bytes() - ); + parsed.$memberName.collect().await.unwrap().into_bytes(), + expected_output.$memberName.collect().await.unwrap().into_bytes() + ); """ ) } else { @@ -367,7 +367,7 @@ class ProtocolTestGenerator( } val variableName = "expected_headers" rustWriter.withBlock("let $variableName = [", "];") { - write( + writeWithNoFormatting( headers.entries.joinToString(",") { "(${it.key.dq()}, ${it.value.dq()})" } @@ -450,7 +450,13 @@ class ProtocolTestGenerator( private val RestXml = "aws.protocoltests.restxml#RestXml" private val AwsQuery = "aws.protocoltests.query#AwsQuery" private val Ec2Query = "aws.protocoltests.ec2#AwsEc2" - private val ExpectFail = setOf() + private val ExpectFail = setOf( + // TODO(https://github.com/awslabs/smithy/pull/1049): Remove this once the test case in Smithy is fixed + FailingTest(RestJson, "RestJsonInputAndOutputWithQuotedStringHeaders", Action.Response), + // TODO(https://github.com/awslabs/smithy/pull/1042): Remove this once the test case in Smithy is fixed + FailingTest(RestJson, "RestJsonInputUnionWithUnitMember", Action.Request), + FailingTest("${RestJson}Extras", "RestJsonInputUnionWithUnitMember", Action.Request), + ) private val RunOnly: Set? = null // These tests are not even attempted to be generated, either because they will not compile diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt index 2d637f8ceb..7fb0ae6100 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rust.codegen.smithy.protocols import software.amazon.smithy.model.Model import software.amazon.smithy.model.pattern.UriPattern +import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ToShapeId import software.amazon.smithy.model.traits.HttpTrait @@ -25,7 +26,6 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredData import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.JsonSerializerGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator import software.amazon.smithy.rust.codegen.util.inputShape -import software.amazon.smithy.rust.codegen.util.orNull sealed class AwsJsonVersion { abstract val value: String @@ -72,19 +72,18 @@ class AwsJsonHttpBindingResolver( .uri(UriPattern.parse("/")) .build() - private fun bindings(shape: ToShapeId?) = - shape?.let { model.expectShape(it.toShapeId()) }?.members() - ?.map { HttpBindingDescriptor(it, HttpLocation.DOCUMENT, "document") } - ?.toList() - ?: emptyList() + private fun bindings(shape: ToShapeId) = + shape.let { model.expectShape(it.toShapeId()) }.members() + .map { HttpBindingDescriptor(it, HttpLocation.DOCUMENT, "document") } + .toList() override fun httpTrait(operationShape: OperationShape): HttpTrait = httpTrait override fun requestBindings(operationShape: OperationShape): List = - bindings(operationShape.input.orNull()) + bindings(operationShape.inputShape) override fun responseBindings(operationShape: OperationShape): List = - bindings(operationShape.output.orNull()) + bindings(operationShape.outputShape) override fun errorResponseBindings(errorShape: ToShapeId): List = bindings(errorShape) @@ -103,7 +102,7 @@ class AwsJsonSerializerGenerator( private val codegenContext: CodegenContext, httpBindingResolver: HttpBindingResolver, private val jsonSerializerGenerator: JsonSerializerGenerator = - JsonSerializerGenerator(codegenContext, httpBindingResolver) + JsonSerializerGenerator(codegenContext, httpBindingResolver, ::awsJsonFieldName) ) : StructuredDataSerializerGenerator by jsonSerializerGenerator { private val runtimeConfig = codegenContext.runtimeConfig private val codegenScope = arrayOf( @@ -153,7 +152,7 @@ class AwsJson( listOf("x-amz-target" to "${codegenContext.serviceShape.id.name}.${operationShape.id.name}") override fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator = - JsonParserGenerator(codegenContext, httpBindingResolver) + JsonParserGenerator(codegenContext, httpBindingResolver, ::awsJsonFieldName) override fun structuredDataSerializer(operationShape: OperationShape): StructuredDataSerializerGenerator = AwsJsonSerializerGenerator(codegenContext, httpBindingResolver) @@ -183,3 +182,7 @@ class AwsJson( ) } } + +private fun awsJsonFieldName(member: MemberShape): String { + return member.memberName +} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt index b5e863a40b..5f3a9209ef 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/RestJson.kt @@ -6,7 +6,9 @@ package software.amazon.smithy.rust.codegen.smithy.protocols import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.traits.JsonNameTrait import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.RustModule @@ -19,6 +21,7 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.parse.JsonParserGene import software.amazon.smithy.rust.codegen.smithy.protocols.parse.StructuredDataParserGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.JsonSerializerGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.serialize.StructuredDataSerializerGenerator +import software.amazon.smithy.rust.codegen.util.getTrait class RestJsonFactory : ProtocolGeneratorFactory { override fun protocol(codegenContext: CodegenContext): Protocol = RestJson(codegenContext) @@ -61,13 +64,11 @@ class RestJson(private val codegenContext: CodegenContext) : Protocol { override val defaultTimestampFormat: TimestampFormatTrait.Format = TimestampFormatTrait.Format.EPOCH_SECONDS - override fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator { - return JsonParserGenerator(codegenContext, httpBindingResolver) - } + override fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator = + JsonParserGenerator(codegenContext, httpBindingResolver, ::restJsonFieldName) - override fun structuredDataSerializer(operationShape: OperationShape): StructuredDataSerializerGenerator { - return JsonSerializerGenerator(codegenContext, httpBindingResolver) - } + override fun structuredDataSerializer(operationShape: OperationShape): StructuredDataSerializerGenerator = + JsonSerializerGenerator(codegenContext, httpBindingResolver, ::restJsonFieldName) override fun parseHttpGenericError(operationShape: OperationShape): RuntimeType = RuntimeType.forInlineFun("parse_http_generic_error", jsonDeserModule) { writer -> @@ -94,3 +95,7 @@ class RestJson(private val codegenContext: CodegenContext) : Protocol { ) } } + +fun restJsonFieldName(member: MemberShape): String { + return member.getTrait()?.value ?: member.memberName +} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt index 9a1196f547..040781e566 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt @@ -18,7 +18,6 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.TimestampShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumTrait -import software.amazon.smithy.model.traits.JsonNameTrait import software.amazon.smithy.model.traits.SparseTrait import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.rust.codegen.rustlang.Attribute @@ -48,7 +47,6 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.HttpLocation import software.amazon.smithy.rust.codegen.smithy.protocols.deserializeFunctionName import software.amazon.smithy.rust.codegen.util.PANIC import software.amazon.smithy.rust.codegen.util.dq -import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.outputShape @@ -57,6 +55,8 @@ import software.amazon.smithy.utils.StringUtils class JsonParserGenerator( codegenContext: CodegenContext, private val httpBindingResolver: HttpBindingResolver, + /** Function that maps a MemberShape into a JSON field name */ + private val jsonName: (MemberShape) -> String, ) : StructuredDataParserGenerator { private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider @@ -220,7 +220,7 @@ class JsonParserGenerator( objectKeyLoop(hasMembers = members.isNotEmpty()) { rustBlock("match key.to_unescaped()?.as_ref()") { for (member in members) { - rustBlock("${member.wireName().dq()} =>") { + rustBlock("${jsonName(member).dq()} =>") { withBlock("builder = builder.${member.setterName()}(", ");") { deserializeMember(member) } @@ -430,7 +430,7 @@ class JsonParserGenerator( withBlock("variant = match key.to_unescaped()?.as_ref() {", "};") { for (member in shape.members()) { val variantName = symbolProvider.toMemberName(member) - rustBlock("${member.wireName().dq()} =>") { + rustBlock("${jsonName(member).dq()} =>") { withBlock("Some(#T::$variantName(", "))", symbol) { deserializeMember(member) unwrapOrDefaultOrError(member) @@ -524,6 +524,4 @@ class JsonParserGenerator( } } } - - private fun MemberShape.wireName(): String = getTrait()?.value ?: memberName } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt index 83386c4844..57466d2fc5 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt @@ -20,7 +20,6 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.TimestampShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumTrait -import software.amazon.smithy.model.traits.JsonNameTrait import software.amazon.smithy.model.traits.TimestampFormatTrait.Format.EPOCH_SECONDS import software.amazon.smithy.rust.codegen.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.rustlang.RustModule @@ -44,7 +43,6 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.HttpLocation import software.amazon.smithy.rust.codegen.smithy.protocols.serializeFunctionName import software.amazon.smithy.rust.codegen.smithy.rustType import software.amazon.smithy.rust.codegen.util.dq -import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.outputShape @@ -52,6 +50,8 @@ import software.amazon.smithy.rust.codegen.util.outputShape class JsonSerializerGenerator( codegenContext: CodegenContext, private val httpBindingResolver: HttpBindingResolver, + /** Function that maps a MemberShape into a JSON field name */ + private val jsonName: (MemberShape) -> String, ) : StructuredDataSerializerGenerator { private data class Context( /** Expression that retrieves a JsonValueWriter from either a JsonObjectWriter or JsonArrayWriter */ @@ -91,10 +91,11 @@ class JsonSerializerGenerator( fun structMember( context: StructContext, member: MemberShape, - symProvider: RustSymbolProvider + symProvider: RustSymbolProvider, + jsonName: (MemberShape) -> String, ): MemberContext = MemberContext( - objectValueWriterExpression(context.objectName, member), + objectValueWriterExpression(context.objectName, jsonName(member)), ValueExpression.Value("${context.localName}.${symProvider.toMemberName(member)}"), member ) @@ -102,19 +103,18 @@ class JsonSerializerGenerator( fun unionMember( context: Context, variantReference: String, - member: MemberShape + member: MemberShape, + jsonName: (MemberShape) -> String, ): MemberContext = MemberContext( - objectValueWriterExpression(context.writerExpression, member), + objectValueWriterExpression(context.writerExpression, jsonName(member)), ValueExpression.Reference(variantReference), member ) /** Returns an expression to get a JsonValueWriter from a JsonObjectWriter */ - private fun objectValueWriterExpression(objectWriterName: String, member: MemberShape): String { - val wireName = (member.getTrait()?.value ?: member.memberName).dq() - return "$objectWriterName.key($wireName)" - } + private fun objectValueWriterExpression(objectWriterName: String, jsonName: String): String = + "$objectWriterName.key(${jsonName.dq()})" } } @@ -276,7 +276,7 @@ class JsonSerializerGenerator( rust("let (_, _) = (object, input);") // Suppress unused argument warnings } for (member in members) { - serializeMember(MemberContext.structMember(inner, member, symbolProvider)) + serializeMember(MemberContext.structMember(inner, member, symbolProvider, jsonName)) } } rust("Ok(())") @@ -405,7 +405,7 @@ class JsonSerializerGenerator( for (member in context.shape.members()) { val variantName = symbolProvider.toMemberName(member) withBlock("#T::$variantName(inner) => {", "},", unionSymbol) { - serializeMember(MemberContext.unionMember(context, "inner", member)) + serializeMember(MemberContext.unionMember(context, "inner", member, jsonName)) } } if (mode.renderUnknownVariant()) { diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt index e0ead720fe..ed3416a158 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/UnionGeneratorTest.kt @@ -91,7 +91,7 @@ class UnionGeneratorTest { let union = MyUnion::Unknown; assert!(union.is_unknown()); - """ + """ ) } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt index b665de5a4e..f27fa1b9b8 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGeneratorTest.kt @@ -14,6 +14,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.EnumGenerator import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.HttpTraitHttpBindingResolver import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolContentTypes +import software.amazon.smithy.rust.codegen.smithy.protocols.restJsonFieldName import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.smithy.transformers.RecursiveShapeBoxer import software.amazon.smithy.rust.codegen.testutil.TestWorkspace @@ -115,7 +116,8 @@ class JsonParserGeneratorTest { val symbolProvider = testSymbolProvider(model) val parserGenerator = JsonParserGenerator( testCodegenContext(model), - HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/json")) + HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/json")), + ::restJsonFieldName ) val operationGenerator = parserGenerator.operationParser(model.lookup("test#Op")) val documentGenerator = parserGenerator.documentParser(model.lookup("test#Op")) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt index 026e5910c2..bd84733cdf 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGeneratorTest.kt @@ -14,6 +14,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.EnumGenerator import software.amazon.smithy.rust.codegen.smithy.generators.UnionGenerator import software.amazon.smithy.rust.codegen.smithy.protocols.HttpTraitHttpBindingResolver import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolContentTypes +import software.amazon.smithy.rust.codegen.smithy.protocols.restJsonFieldName import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer import software.amazon.smithy.rust.codegen.smithy.transformers.RecursiveShapeBoxer import software.amazon.smithy.rust.codegen.testutil.TestWorkspace @@ -103,7 +104,8 @@ class JsonSerializerGeneratorTest { val symbolProvider = testSymbolProvider(model) val parserSerializer = JsonSerializerGenerator( testCodegenContext(model), - HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/json")) + HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/json")), + ::restJsonFieldName ) val operationGenerator = parserSerializer.operationSerializer(model.lookup("test#Op")) val documentGenerator = parserSerializer.documentSerializer() diff --git a/gradle.properties b/gradle.properties index 4301dfe54c..dbfac21b46 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ smithy.rs.runtime.crate.version=0.34.1 kotlin.code.style=official # codegen -smithyVersion=1.13.0 +smithyVersion=1.16.1 # kotlin kotlinVersion=1.4.21 diff --git a/rust-runtime/aws-smithy-http/additional-ci b/rust-runtime/aws-smithy-http/additional-ci new file mode 100755 index 0000000000..616d01b5c5 --- /dev/null +++ b/rust-runtime/aws-smithy-http/additional-ci @@ -0,0 +1,12 @@ +#!/bin/bash +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0. +# + +# This script contains additional CI checks to run for this specific package + +set -e + +echo "### Testing feature powerset" +cargo hack test --feature-powerset diff --git a/rust-runtime/aws-smithy-http/fuzz/.gitignore b/rust-runtime/aws-smithy-http/fuzz/.gitignore new file mode 100644 index 0000000000..a0925114d6 --- /dev/null +++ b/rust-runtime/aws-smithy-http/fuzz/.gitignore @@ -0,0 +1,3 @@ +target +corpus +artifacts diff --git a/rust-runtime/aws-smithy-http/fuzz/Cargo.toml b/rust-runtime/aws-smithy-http/fuzz/Cargo.toml new file mode 100644 index 0000000000..8364d5f3cd --- /dev/null +++ b/rust-runtime/aws-smithy-http/fuzz/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "aws-smithy-http-fuzz" +version = "0.0.0" +authors = ["Automatically generated"] +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +http = "0.2.3" + +[dependencies.aws-smithy-http] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "read_many_from_str" +path = "fuzz_targets/read_many_from_str.rs" +test = false +doc = false diff --git a/rust-runtime/aws-smithy-http/fuzz/fuzz_targets/read_many_from_str.rs b/rust-runtime/aws-smithy-http/fuzz/fuzz_targets/read_many_from_str.rs new file mode 100644 index 0000000000..e4a6b286b0 --- /dev/null +++ b/rust-runtime/aws-smithy-http/fuzz/fuzz_targets/read_many_from_str.rs @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#![no_main] +use libfuzzer_sys::fuzz_target; + +use aws_smithy_http::header::read_many_from_str; +use http; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + if let Ok(req) = http::Request::builder().header("test", s).body(()) { + // Shouldn't panic + let _ = read_many_from_str::(req.headers().get_all("test").iter()); + } + } +}); diff --git a/rust-runtime/aws-smithy-http/src/header.rs b/rust-runtime/aws-smithy-http/src/header.rs index bd3d42b2c8..4b8b14eafe 100644 --- a/rust-runtime/aws-smithy-http/src/header.rs +++ b/rust-runtime/aws-smithy-http/src/header.rs @@ -164,30 +164,114 @@ where } } +/// Functions for parsing multiple comma-delimited header values out of a +/// single header. This parsing adheres to +/// [RFC-7230's specification of header values](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6). +mod parse_multi_header { + use super::ParseError; + use std::borrow::Cow; + + fn trim(s: Cow<'_, str>) -> Cow<'_, str> { + match s { + Cow::Owned(s) => Cow::Owned(s.trim().into()), + Cow::Borrowed(s) => Cow::Borrowed(s.trim()), + } + } + + fn replace<'a>(value: Cow<'a, str>, pattern: &str, replacement: &str) -> Cow<'a, str> { + if value.contains(pattern) { + Cow::Owned(value.replace(pattern, replacement)) + } else { + value + } + } + + /// Reads a single value out of the given input, and returns a tuple containing + /// the parsed value and the remainder of the slice that can be used to parse + /// more values. + pub(crate) fn read_value(input: &[u8]) -> Result<(Cow<'_, str>, &[u8]), ParseError> { + for (index, &byte) in input.iter().enumerate() { + let current_slice = &input[index..]; + match byte { + b' ' | b'\t' => { /* skip whitespace */ } + b'"' => return read_quoted_value(¤t_slice[1..]), + _ => { + let (value, rest) = read_unquoted_value(current_slice)?; + return Ok((trim(value), rest)); + } + } + } + + // We only end up here if the entire header value was whitespace or empty + Ok((Cow::Borrowed(""), &[])) + } + + fn read_unquoted_value(input: &[u8]) -> Result<(Cow<'_, str>, &[u8]), ParseError> { + let next_delim = input.iter().position(|&b| b == b',').unwrap_or(input.len()); + let (first, next) = input.split_at(next_delim); + let first = std::str::from_utf8(first) + .map_err(|_| ParseError::new_with_message("header was not valid utf8"))?; + Ok((Cow::Borrowed(first), then_comma(next).unwrap())) + } + + /// Reads a header value that is surrounded by quotation marks and may have escaped + /// quotes inside of it. + fn read_quoted_value(input: &[u8]) -> Result<(Cow<'_, str>, &[u8]), ParseError> { + for index in 0..input.len() { + match input[index] { + b'"' if index == 0 || input[index - 1] != b'\\' => { + let mut inner = + Cow::Borrowed(std::str::from_utf8(&input[0..index]).map_err(|_| { + ParseError::new_with_message("header was not valid utf8") + })?); + inner = replace(inner, "\\\"", "\""); + inner = replace(inner, "\\\\", "\\"); + let rest = then_comma(&input[(index + 1)..])?; + return Ok((inner, rest)); + } + _ => {} + } + } + Err(ParseError::new_with_message( + "header value had quoted value without end quote", + )) + } + + fn then_comma(s: &[u8]) -> Result<&[u8], ParseError> { + if s.is_empty() { + Ok(s) + } else if s.starts_with(b",") { + Ok(&s[1..]) + } else { + Err(ParseError::new_with_message("expected delimiter `,`")) + } + } +} + /// Read one comma delimited value for `FromStr` types fn read_one<'a, T>( s: &'a [u8], f: &impl Fn(&str) -> Result, ) -> Result<(T, &'a [u8]), ParseError> { - let (head, rest) = split_at_delim(s); - let head = std::str::from_utf8(head) - .map_err(|_| ParseError::new_with_message("header was not valid utf8"))?; - Ok((f(head.trim())?, rest)) -} - -fn split_at_delim(s: &[u8]) -> (&[u8], &[u8]) { - let next_delim = s.iter().position(|b| b == &b',').unwrap_or(s.len()); - let (first, next) = s.split_at(next_delim); - (first, then_delim(next).unwrap()) + let (value, rest) = parse_multi_header::read_value(s)?; + Ok((f(&value)?, rest)) } -fn then_delim(s: &[u8]) -> Result<&[u8], ParseError> { - if s.is_empty() { - Ok(s) - } else if s.starts_with(b",") { - Ok(&s[1..]) +/// Conditionally quotes and escapes a header value if the header value contains a comma or quote. +pub fn quote_header_value<'a>(value: impl Into>) -> Cow<'a, str> { + let value = value.into(); + if value.trim().len() != value.len() + || value.contains('"') + || value.contains(',') + || value.contains('(') + || value.contains(')') + { + Cow::Owned(format!( + "\"{}\"", + value.replace('\\', "\\\\").replace('"', "\\\"") + )) } else { - Err(ParseError::new_with_message("expected delimiter `,`")) + value } } @@ -195,12 +279,16 @@ fn then_delim(s: &[u8]) -> Result<&[u8], ParseError> { mod test { use std::collections::HashMap; + use aws_smithy_types::{date_time::Format, DateTime}; use http::header::HeaderName; use crate::header::{ - headers_for_prefix, read_many_primitive, set_header_if_absent, ParseError, + headers_for_prefix, many_dates, read_many_from_str, read_many_primitive, + set_header_if_absent, ParseError, }; + use super::quote_header_value; + #[test] fn put_if_absent() { let builder = http::Request::builder().header("foo", "bar"); @@ -238,6 +326,95 @@ mod test { ) } + #[test] + fn test_many_dates() { + let test_request = http::Request::builder() + .header("Empty", "") + .header("SingleHttpDate", "Wed, 21 Oct 2015 07:28:00 GMT") + .header( + "MultipleHttpDates", + "Wed, 21 Oct 2015 07:28:00 GMT,Thu, 22 Oct 2015 07:28:00 GMT", + ) + .header("SingleEpochSeconds", "1234.5678") + .header("MultipleEpochSeconds", "1234.5678,9012.3456") + .body(()) + .unwrap(); + let read = |name: &str, format: Format| { + many_dates(test_request.headers().get_all(name).iter(), format) + }; + let read_valid = |name: &str, format: Format| read(name, format).expect("valid"); + assert_eq!( + read_valid("Empty", Format::DateTime), + Vec::::new() + ); + assert_eq!( + read_valid("SingleHttpDate", Format::HttpDate), + vec![DateTime::from_secs_and_nanos(1445412480, 0)] + ); + assert_eq!( + read_valid("MultipleHttpDates", Format::HttpDate), + vec![ + DateTime::from_secs_and_nanos(1445412480, 0), + DateTime::from_secs_and_nanos(1445498880, 0) + ] + ); + assert_eq!( + read_valid("SingleEpochSeconds", Format::EpochSeconds), + vec![DateTime::from_secs_and_nanos(1234, 567_800_000)] + ); + assert_eq!( + read_valid("MultipleEpochSeconds", Format::EpochSeconds), + vec![ + DateTime::from_secs_and_nanos(1234, 567_800_000), + DateTime::from_secs_and_nanos(9012, 345_600_000) + ] + ); + } + + #[test] + fn read_many_strings() { + let test_request = http::Request::builder() + .header("Empty", "") + .header("Foo", " foo") + .header("FooTrailing", "foo ") + .header("FooInQuotes", "\" foo \"") + .header("CommaInQuotes", "\"foo,bar\",baz") + .header("CommaInQuotesTrailing", "\"foo,bar\",baz ") + .header("QuoteInQuotes", "\"foo\\\",bar\",\"\\\"asdf\\\"\",baz") + .header( + "QuoteInQuotesWithSpaces", + "\"foo\\\",bar\", \"\\\"asdf\\\"\", baz", + ) + .header("JunkFollowingQuotes", "\"\\\"asdf\\\"\"baz") + .header("EmptyQuotes", "\"\",baz") + .header("EscapedSlashesInQuotes", "foo, \"(foo\\\\bar)\"") + .body(()) + .unwrap(); + let read = + |name: &str| read_many_from_str::(test_request.headers().get_all(name).iter()); + let read_valid = |name: &str| read(name).expect("valid"); + assert_eq!(read_valid("Empty"), Vec::::new()); + assert_eq!(read_valid("Foo"), vec!["foo"]); + assert_eq!(read_valid("FooTrailing"), vec!["foo"]); + assert_eq!(read_valid("FooInQuotes"), vec![" foo "]); + assert_eq!(read_valid("CommaInQuotes"), vec!["foo,bar", "baz"]); + assert_eq!(read_valid("CommaInQuotesTrailing"), vec!["foo,bar", "baz"]); + assert_eq!( + read_valid("QuoteInQuotes"), + vec!["foo\",bar", "\"asdf\"", "baz"] + ); + assert_eq!( + read_valid("QuoteInQuotesWithSpaces"), + vec!["foo\",bar", "\"asdf\"", "baz"] + ); + assert!(read("JunkFollowingQuotes").is_err()); + assert_eq!(read_valid("EmptyQuotes"), vec!["", "baz"]); + assert_eq!( + read_valid("EscapedSlashesInQuotes"), + vec!["foo", "(foo\\bar)"] + ); + } + #[test] fn read_many_bools() { let test_request = http::Request::builder() @@ -246,6 +423,7 @@ mod test { .header("X-Bool", "true") .header("X-Bool-Invalid", "truth,falsy") .header("X-Bool-Single", "true,false,true,true") + .header("X-Bool-Quoted", "true,\"false\",true,true") .body(()) .unwrap(); assert_eq!( @@ -263,6 +441,11 @@ mod test { .unwrap(), vec![true, false, true, true] ); + assert_eq!( + read_many_primitive::(test_request.headers().get_all("X-Bool-Quoted").iter()) + .unwrap(), + vec![true, false, true, true] + ); read_many_primitive::(test_request.headers().get_all("X-Bool-Invalid").iter()) .expect_err("invalid"); } @@ -275,6 +458,7 @@ mod test { .header("X-Num", "777") .header("X-Num-Invalid", "12ef3") .header("X-Num-Single", "1,2,3,-4,5") + .header("X-Num-Quoted", "1, \"2\",3,\"-4\",5") .body(()) .unwrap(); assert_eq!( @@ -292,6 +476,11 @@ mod test { .unwrap(), vec![1, 2, 3, -4, 5] ); + assert_eq!( + read_many_primitive::(test_request.headers().get_all("X-Num-Quoted").iter()) + .unwrap(), + vec![1, 2, 3, -4, 5] + ); read_many_primitive::(test_request.headers().get_all("X-Num-Invalid").iter()) .expect_err("invalid"); } @@ -315,4 +504,18 @@ mod test { let resp = resp.expect("valid"); assert_eq!(resp.get("a"), Some(&vec![123_i16, 456_i16])); } + + #[test] + fn test_quote_header_value() { + assert_eq!("", "e_header_value("")); + assert_eq!("foo", "e_header_value("foo")); + assert_eq!("\" foo\"", "e_header_value(" foo")); + assert_eq!("foo bar", "e_header_value("foo bar")); + assert_eq!("\"foo,bar\"", "e_header_value("foo,bar")); + assert_eq!("\",\"", "e_header_value(",")); + assert_eq!("\"\\\"foo\\\"\"", "e_header_value("\"foo\"")); + assert_eq!("\"\\\"f\\\\oo\\\"\"", "e_header_value("\"f\\oo\"")); + assert_eq!("\"(\"", "e_header_value("(")); + assert_eq!("\")\"", "e_header_value(")")); + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 4fd23db989..491acc0430 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,6 @@ pluginManagement { } } - rootProject.name = "software.amazon.smithy.rust.codegen.smithy-rs" enableFeaturePreview("GRADLE_METADATA")