diff --git a/README.md b/README.md index b59947fc..45c8b7d4 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,11 @@ If you have a question, or hit a problem, feel free to ask on our [gitter channe Or, if you encounter a bug, something is unclear in the code or documentation, don’t hesitate and open an issue on GitHub. +### Testing Scala.js + +Scala.js tests require [chromedriver](https://chromedriver.chromium.org/). It is downloaded automatically +by the `test` task. Make sure to run it before running tests in any other way, e.g. using `testOnly`. + ### Building & testing the scala-native version By default, sttp-native will **not** be included in the aggregate build of the root project. To include it, define the `STTP_NATIVE` environmental variable before running sbt, e.g.: diff --git a/core/src/main/scala/sttp/model/Part.scala b/core/src/main/scala/sttp/model/Part.scala index ebb5341d..46159af8 100644 --- a/core/src/main/scala/sttp/model/Part.scala +++ b/core/src/main/scala/sttp/model/Part.scala @@ -41,9 +41,30 @@ case class Part[+T]( // enumerate all variants so that overload resolution works correctly override def header(h: String): Option[String] = super.header(h) + /** + * Returns the value of the `Content-Disposition` header, which should be used when sending this part in a + * multipart request. + * + * The syntax is specified by [[https://datatracker.ietf.org/doc/html/rfc6266#section-4.1 RFC6266 section 4.1]]. + * For safety and simplicity, disposition parameter values are represented as `quoted-string`, defined in + * [[https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.4 RFC9110 section 5.6.4]]. + * + * `quoted-string` allows usage of visible ASCII characters (`%x21-7E`), except for `"` and `\`, which must be escaped + * with a backslash. Additionally, space and horizontal tab is allowed, as well as octets `0x80-FF` (`obs-data`). + * In practice this means that - while not explicitly allowed - non-ASCII UTF-8 characters are valid + * according to this grammar. Additionally, [[https://datatracker.ietf.org/doc/html/rfc6532#section-3.2 RFC6532]] + * makes it more explicit that non-ASCII UTF-8 is allowed. Control characters are not allowed. + * + * This method makes sure that `"` and `\` are escaped, while leaving possible rejection of forbidden characters to + * lower layers (`sttp` backends). + */ def contentDispositionHeaderValue: String = { - def encode(s: String): String = new String(s.getBytes("utf-8"), "iso-8859-1") - "form-data; " + dispositionParamsSeq.map { case (k, v) => s"""$k="${encode(v)}"""" }.mkString("; ") + def escape(str: String): String = str.flatMap { + case '"' => "\\\"" + case '\\' => "\\\\" + case c => c.toString + } + "form-data; " + dispositionParamsSeq.map { case (k, v) => s"""$k="${escape(v)}"""" }.mkString("; ") } def dispositionParams: Map[String, String] = dispositionParamsSeq.toMap diff --git a/core/src/test/scala/sttp/model/PartTest.scala b/core/src/test/scala/sttp/model/PartTest.scala new file mode 100644 index 00000000..e4faad99 --- /dev/null +++ b/core/src/test/scala/sttp/model/PartTest.scala @@ -0,0 +1,21 @@ +package sttp.model + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class PartTest extends AnyFlatSpec with Matchers { + it should "encode content-disposition header correctly" in { + val part = Part("name", 42).fileName("f1.txt") + part.contentDispositionHeaderValue shouldBe """form-data; name="name"; filename="f1.txt"""" + } + + it should "retain non-ascii characters in filename" in { + val part = Part("name", 42).fileName("fó1.txt") + part.contentDispositionHeaderValue shouldBe """form-data; name="name"; filename="fó1.txt"""" + } + + it should "escape double quote and backslash in filename" in { + val part = Part("name", 42).fileName("f\"\\1.txt") + part.contentDispositionHeaderValue shouldBe """form-data; name="name"; filename="f\"\\1.txt"""" + } +}