Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ da85b978db204adf6f236bfd4e453628a6d4cdda

# Scala Steward: Reformat with scalafmt 3.11.2
400127d53ebf236548879443afc5b8d08afdd625

# Scala Steward: Reformat with scalafmt 3.11.3
0bc1a9c10d5f2a78304535fdcb74b60c43c70839
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.11.2
version = 3.11.3
maxColumn = 140
runner.dialect = scala213source3
fileOverride {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ object OutComponent {
val wrappers = declsByWrapperClassName
.map { case (name, seq) =>
val defns =
seq.map { case (_, t, _, ct) => s"""override def ${NameHelpers.safeVariableName(ct)}: () => $t = () => value""" }.sorted
seq
.map { case (_, t, _, ct) => s"""override def ${NameHelpers.safeVariableName(ct)}: () => $t = () => value""" }
.sorted
.mkString("\n")
s"""case class ${name}(value: ${seq.head._2}) extends $traitName{
|${indent(2)(defns)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ object CirceSerdeImpl {
val encoders = subtypeNames
.map { t =>
val jsonTypeName = JavaEscape.escapeString(schemaToJsonMapping(t))
s"""case x: $t => io.circe.Encoder[$t].apply(x).mapObject(_.add("${JavaEscape.escapeString(discriminator.propertyName)}", io.circe.Json.fromString("$jsonTypeName")))"""
s"""case x: $t => io.circe.Encoder[$t].apply(x).mapObject(_.add("${JavaEscape.escapeString(
discriminator.propertyName
)}", io.circe.Json.fromString("$jsonTypeName")))"""
}
.mkString("\n")
val decoders = subtypeNames
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,21 +270,27 @@ object JsoniterSerdeImpl {
val codecName = getJsoniterName(name)
val serde =
if (useCustomJsoniterSerdes)
s"""implicit lazy val $codecName: $jsoniterPkgCore.JsonValueCodec[$name] = $jsoniterPkgMacros.JsonCodecMaker.makeOpenapiLike("${JavaEscape.escapeString(discriminator.propertyName)}", {$discriminatorMap})"""
s"""implicit lazy val $codecName: $jsoniterPkgCore.JsonValueCodec[$name] = $jsoniterPkgMacros.JsonCodecMaker.makeOpenapiLike("${JavaEscape
.escapeString(discriminator.propertyName)}", {$discriminatorMap})"""
else {
val config =
s"""$jsoniterBaseConfig.withRequireDiscriminatorFirst(false).withDiscriminatorFieldName(Some("${JavaEscape.escapeString(discriminator.propertyName)}")).withAdtLeafClassNameMapper(x => com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker.simpleClassName(x) match {$discriminatorMap})"""
s"""$jsoniterBaseConfig.withRequireDiscriminatorFirst(false).withDiscriminatorFieldName(Some("${JavaEscape.escapeString(
discriminator.propertyName
)}")).withAdtLeafClassNameMapper(x => com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker.simpleClassName(x) match {$discriminatorMap})"""
s"implicit lazy val $codecName: $jsoniterPkgCore.JsonValueCodec[$name] = $jsoniterPkgMacros.JsonCodecMaker.make($config)"
}

s"""$serde
|""".stripMargin
} else {
if (useCustomJsoniterSerdes)
s"""implicit lazy val $codecName: $jsoniterPkgCore.JsonValueCodec[$name] = $jsoniterPkgMacros.JsonCodecMaker.makeOpenapiLike("${JavaEscape.escapeString(discriminator.propertyName)}")"""
s"""implicit lazy val $codecName: $jsoniterPkgCore.JsonValueCodec[$name] = $jsoniterPkgMacros.JsonCodecMaker.makeOpenapiLike("${JavaEscape
.escapeString(discriminator.propertyName)}")"""
else {
val config =
s"""$jsoniterBaseConfig.withRequireDiscriminatorFirst(false).withDiscriminatorFieldName(Some("${JavaEscape.escapeString(discriminator.propertyName)}"))"""
s"""$jsoniterBaseConfig.withRequireDiscriminatorFirst(false).withDiscriminatorFieldName(Some("${JavaEscape.escapeString(
discriminator.propertyName
)}"))"""
s"implicit lazy val $codecName: $jsoniterPkgCore.JsonValueCodec[$name] = $jsoniterPkgMacros.JsonCodecMaker.make($config)"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,16 @@ object ZioSerdeImpl {
val encoders = subtypeNames
.map { t =>
val jsonTypeName = JavaEscape.escapeString(schemaToJsonMapping(t))
s"""case x: $t => zio.json.ast.Json.decoder.decodeJson(zio.json.JsonEncoder[$t].encodeJson(x)).getOrElse(throw new RuntimeException("Unable to encode tagged ADT type ${name} to json")).mapObject(_.add("${JavaEscape.escapeString(discriminator.propertyName)}", zio.json.ast.Json.Str("$jsonTypeName")))"""
s"""case x: $t => zio.json.ast.Json.decoder.decodeJson(zio.json.JsonEncoder[$t].encodeJson(x)).getOrElse(throw new RuntimeException("Unable to encode tagged ADT type ${name} to json")).mapObject(_.add("${JavaEscape
.escapeString(discriminator.propertyName)}", zio.json.ast.Json.Str("$jsonTypeName")))"""
}
.mkString("\n")
val decoders = subtypeNames
.map { t => s"""case zio.json.ast.Json.Str("${JavaEscape.escapeString(schemaToJsonMapping(t))}") => zio.json.JsonDecoder[$t].fromJsonAST(json)""" }
.map { t =>
s"""case zio.json.ast.Json.Str("${JavaEscape.escapeString(
schemaToJsonMapping(t)
)}") => zio.json.JsonDecoder[$t].fromJsonAST(json)"""
}
.mkString("\n")
s"""implicit lazy val ${uncapitalisedName}JsonEncoder: zio.json.JsonEncoder[$name] = zio.json.JsonEncoder[zio.json.ast.Json].contramap {
|${indent(2)(encoders)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ object DefaultValueRenderer {
thisType match {
case ref: OpenapiSchemaRef => renderMapWithName(kvs)(allModels, lookup(allModels, ref), ref.stripped)
case OpenapiSchemaMap(types, _, _) =>
s"Map(${kvs.map { case (k, v) =>
s""""${JavaEscape.escapeString(k)}" -> ${render(allModels, types, isOptional = false, RenderConfig())(v)}"""
}.mkString(", ")})"
s"Map(${kvs
.map { case (k, v) =>
s""""${JavaEscape.escapeString(k)}" -> ${render(allModels, types, isOptional = false, RenderConfig())(v)}"""
}
.mkString(", ")})"
case OpenapiSchemaObject(properties, required, _, _) =>
val kvsWithProps = kvs.map { case (k, v) => (k, (v, properties.get(k).getOrElse(errorForKey(k)))) }
s"$name(${kvsWithProps
Expand Down Expand Up @@ -127,9 +129,11 @@ object DefaultValueRenderer {
renderMapWithName(jsonObject.toMap)(allModels, lookup(allModels, ref), ref.stripped)
case OpenapiSchemaAllOf(Seq(singleElement)) => render(allModels, singleElement, isOptional = false, config)(json)
case OpenapiSchemaMap(types, _, _) =>
s"Map(${jsonObject.toMap.map { case (k, v) =>
s""""${JavaEscape.escapeString(k)}" -> ${render(allModels, types, isOptional = false, config)(v)}"""
}.mkString(", ")})"
s"Map(${jsonObject.toMap
.map { case (k, v) =>
s""""${JavaEscape.escapeString(k)}" -> ${render(allModels, types, isOptional = false, config)(v)}"""
}
.mkString(", ")})"
case other => fail("map", other)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ object SecurityGenerator {
val refreshUrl = f.refreshUrl.map(u => s"""Some("${JavaEscape.escapeString(u)}")""").getOrElse("None")
(s"""auth.oauth2.clientCredentialsFlow("$tokenUrl", $refreshUrl)""", "Bearer", schemeName)
case (OAuth2FlowType.authorizationCode, f) =>
val authUrl = JavaEscape.escapeString(f.authorizationUrl.getOrElse(bail("authorizationUrl required for authorizationCode flow")))
val authUrl =
JavaEscape.escapeString(f.authorizationUrl.getOrElse(bail("authorizationUrl required for authorizationCode flow")))
val tokenUrl = JavaEscape.escapeString(f.tokenUrl.getOrElse(bail("tokenUrl required for authorizationCode flow")))
val refreshUrl = f.refreshUrl.map(u => s"""Some("${JavaEscape.escapeString(u)}")""").getOrElse("None")
(s"""auth.oauth2.authorizationCodeFlow("$authUrl", "$tokenUrl", $refreshUrl)""", "Bearer", schemeName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ import sttp.tapir.codegen.openapi.models.OpenapiModels.{OpenapiDocument, Openapi
import sttp.tapir.codegen.openapi.models.OpenapiSchemaType
import sttp.tapir.codegen.openapi.models.OpenapiSchemaType._

/** Ingestion-time validation of names taken from an (untrusted) OpenAPI document that the code generator emits into
* *identifier* positions β€” component schema names, `$ref` targets, object property names and (when used for object
* names) tags. These become class/trait/type/field/val names in the generated source, frequently by raw string
* concatenation (e.g. `${name.capitalize}Decoder`) rather than backtick-quoting, so a name containing characters
* outside the OpenAPI-permitted identifier set could inject arbitrary Scala code.
/** Ingestion-time validation of names taken from an (untrusted) OpenAPI document that the code generator emits into *identifier* positions
* β€” component schema names, `$ref` targets, object property names and (when used for object names) tags. These become
* class/trait/type/field/val names in the generated source, frequently by raw string concatenation (e.g. `${name.capitalize}Decoder`)
* rather than backtick-quoting, so a name containing characters outside the OpenAPI-permitted identifier set could inject arbitrary Scala
* code.
*
* We restrict them to a safe character set: the set OpenAPI permits for component names
* (https://spec.openapis.org/oas/v3.1.0#components-object) plus `$` and `+`. None of these can form executable Scala
* (`$` is a valid Scala identifier character; `+`/`.`/`-` at worst yield a non-compiling identifier, never a break-out),
* while `$`/`+` occur in real-world property names (e.g. GitHub's `+1`, .NET's `$type`). Values that are emitted as
* string literals instead (parameter names, URLs, descriptions, default values, discriminator values, enum values,
* XML names) can legitimately contain any character and are escaped at their emission site rather than restricted
* here. See GHSA-gpcc-36pq-8qxr.
* (https://spec.openapis.org/oas/v3.1.0#components-object) plus `$` and `+`. None of these can form executable Scala (`$` is a valid Scala
* identifier character; `+`/`.`/`-` at worst yield a non-compiling identifier, never a break-out), while `$`/`+` occur in real-world
* property names (e.g. GitHub's `+1`, .NET's `$type`). Values that are emitted as string literals instead (parameter names, URLs,
* descriptions, default values, discriminator values, enum values, XML names) can legitimately contain any character and are escaped at
* their emission site rather than restricted here. See GHSA-gpcc-36pq-8qxr.
*/
object NameValidation {

Expand All @@ -28,9 +27,9 @@ object NameValidation {
s"Unsafe $kind '$name' in OpenAPI document: only characters [A-Za-z0-9._$$+-] are permitted (see GHSA-gpcc-36pq-8qxr)"
)

/** Validate a single name that is about to be emitted into an identifier position (e.g. a `$ref` target spliced as
* a type). Use this as a sink-side guard where a name may not have passed through document-level validation (path
* schemas resolved from components, response headers, etc.).
/** Validate a single name that is about to be emitted into an identifier position (e.g. a `$ref` target spliced as a type). Use this as a
* sink-side guard where a name may not have passed through document-level validation (path schemas resolved from components, response
* headers, etc.).
*/
def validateName(kind: String, name: String): Unit = check(kind, name)

Expand All @@ -57,8 +56,8 @@ object NameValidation {
case _ => Nil
}

/** Validate every name in the document that reaches an identifier position. Throws IllegalArgumentException on the
* first unsafe name. Idempotent and cheap, so it is safe to call from each public generator entry point.
/** Validate every name in the document that reaches an identifier position. Throws IllegalArgumentException on the first unsafe name.
* Idempotent and cheap, so it is safe to call from each public generator entry point.
*/
def validateDocumentNames(doc: OpenapiDocument, useHeadTagForObjectNames: Boolean): Unit = {
val schemas = doc.components.toSeq.flatMap(_.schemas)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ object JavaEscape {
}
}

/** An untrusted string as a complete, escaped Scala string literal (surrounding quotes included). Prefer this over
* hand-writing `"\"" + escapeString(x) + "\""` so the escape and the quotes can't get out of sync.
/** An untrusted string as a complete, escaped Scala string literal (surrounding quotes included). Prefer this over hand-writing
* `"\"" + escapeString(x) + "\""` so the escape and the quotes can't get out of sync.
*/
def quote(str: String): String = "\"" + escapeString(str) + "\""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ object XmlSerdeGenerator {
val in = c.flatMap(_.itemName).getOrElse(t)
Some(seqSubtype -> s"""type $seqSubtype <: Seq[$t]
|implicit val ${seqSubtype}SeqDecoder: cats.xml.codec.Decoder[$seqSubtype] = seqDecoder[$t]("${JavaEscape
.escapeString(name)}", isWrapped = $w).map(_.asInstanceOf[$seqSubtype])
.escapeString(name)}", isWrapped = $w).map(_.asInstanceOf[$seqSubtype])
|implicit val ${seqSubtype}SeqEncoder: cats.xml.codec.Encoder[$seqSubtype] =
| seqEncoder[$t]("${JavaEscape.escapeString(name)}", isWrapped = $w, itemName = "${JavaEscape.escapeString(in)}").contramap(_.asInstanceOf[Seq[$t]])
| seqEncoder[$t]("${JavaEscape.escapeString(name)}", isWrapped = $w, itemName = "${JavaEscape.escapeString(
in
)}").contramap(_.asInstanceOf[Seq[$t]])
|implicit val ${seqSubtype}SeqSchema: sttp.tapir.Schema[${seqSubtype}] =
| implicitly[Schema[Seq[$t]]].map(x => Some(x.asInstanceOf[${seqSubtype}]))(_.asInstanceOf[Seq[$t]])""".stripMargin)
case _ => None
Expand Down Expand Up @@ -135,7 +137,9 @@ object XmlSerdeGenerator {
val in = c.flatMap(_.itemName).getOrElse(n)
val w = c.exists(_.isWrapped)
s"""implicit val $ref${n.capitalize}SeqEncoder: Encoder[Seq[$t]] =
| seqEncoder[$t]("${JavaEscape.escapeString(c.flatMap(_.name).getOrElse(n))}", isWrapped = $w, itemName = "${JavaEscape
| seqEncoder[$t]("${JavaEscape.escapeString(
c.flatMap(_.name).getOrElse(n)
)}", isWrapped = $w, itemName = "${JavaEscape
.escapeString(in)}")""".stripMargin
case ScopedAuxCodecParams(n, t, _, OtherType, _) =>
s"""implicit val $ref${n.capitalize}Encoder: Encoder[$t] = deriveConfiguredEncoder[$t]""".stripMargin
Expand Down
Loading
Loading