Repository: http4s/http4s (scala)

Issue:
Model "Content-Transfer-Encoding" header
According to the [RFC](https://www.ietf.org/rfc/rfc2045.txt).

Also:

- [ ] switch `Part` to the modeled header:
    https://github.com/http4s/http4s/blob/f6e8c94f503720f5155e7ca045851619abbdb81f/core/shared/src/main/scala/org/http4s/multipart/Part.scala#L75

Pull request:
fixes #6793

I had a shot at modeling the header. 

As this is a first for me feedback is very welcome. Especially I was not sure whether to model it using CIString or not (RFC states it is case insensitive).

Patch:
diff --git a/core/shared/src/main/scala/org/http4s/headers/Content-Transfer-Encoding.scala b/core/shared/src/main/scala/org/http4s/headers/Content-Transfer-Encoding.scala
new file mode 100644
index 00000000000..77baf62f37f
--- /dev/null
+++ b/core/shared/src/main/scala/org/http4s/headers/Content-Transfer-Encoding.scala
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2013 http4s.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.http4s
+package headers
+
+import cats.parse.Parser
+import cats.parse.Parser.ignoreCase
+import org.typelevel.ci._
+
+/** This header specifies the way content is mime encoded.
+  *
+  * @see [[https://www.ietf.org/rfc/rfc2045.txt]]
+  */
+sealed abstract class `Content-Transfer-Encoding`(val value: CIString)
+    extends Product
+    with Serializable
+
+object `Content-Transfer-Encoding` {
+  case object `7bit` extends `Content-Transfer-Encoding`(ci"7bit")
+  case object `8bit` extends `Content-Transfer-Encoding`(ci"8bit")
+  case object Binary extends `Content-Transfer-Encoding`(ci"binary")
+  case object QuotedPrintable extends `Content-Transfer-Encoding`(ci"quoted-printable")
+  case object Base64 extends `Content-Transfer-Encoding`(ci"base64")
+  case object IetfToken extends `Content-Transfer-Encoding`(ci"ietf-token")
+  case object XToken extends `Content-Transfer-Encoding`(ci"x-token")
+
+  private[http4s] val parser: Parser[`Content-Transfer-Encoding`] = {
+    val sevenBitParser = ignoreCase("7bit").as(`7bit`)
+    val eightBitParser = ignoreCase("8bit").as(`8bit`)
+    val binaryParser = ignoreCase("binary").as(Binary)
+    val quotedPrintableParser = ignoreCase("quoted-printable").as(QuotedPrintable)
+    val base64Parser = ignoreCase("base64").as(Base64)
+    val ietfTokenParser = ignoreCase("ietf-token").as(IetfToken)
+    val xTokenParser = ignoreCase("x-token").as(XToken)
+    sevenBitParser | eightBitParser | binaryParser | quotedPrintableParser | base64Parser | ietfTokenParser | xTokenParser
+  }
+
+  def parse(s: String): ParseResult[`Content-Transfer-Encoding`] =
+    ParseResult.fromParser(parser, "Invalid Content-Transfer-Encoding header")(s)
+
+  implicit val headerInstance: Header[`Content-Transfer-Encoding`, Header.Single] =
+    Header.create(
+      ci"Content-Transfer-Encoding",
+      _.value.toString,
+      parse,
+    )
+}
diff --git a/core/shared/src/main/scala/org/http4s/multipart/Part.scala b/core/shared/src/main/scala/org/http4s/multipart/Part.scala
index a57bced2ead..c73b1acc440 100644
--- a/core/shared/src/main/scala/org/http4s/multipart/Part.scala
+++ b/core/shared/src/main/scala/org/http4s/multipart/Part.scala
@@ -25,6 +25,7 @@ import fs2.io.file.Path
 import fs2.io.readInputStream
 import fs2.text.utf8
 import org.http4s.headers.`Content-Disposition`
+import org.http4s.headers.`Content-Transfer-Encoding`
 import org.typelevel.ci._
 
 import java.io.File
@@ -68,14 +69,17 @@ object Part {
       filename: String,
       entityBody: EntityBody[F],
       headers: Header.ToRaw*
-  ): Part[F] =
+  ): Part[F] = {
+    val binaryContentTransferEncoding: `Content-Transfer-Encoding` =
+      `Content-Transfer-Encoding`.Binary
     Part(
       Headers(
         `Content-Disposition`("form-data", Map(ci"name" -> name, ci"filename" -> filename)),
-        "Content-Transfer-Encoding" -> "binary",
+        binaryContentTransferEncoding,
       ).put(headers: _*),
       entityBody,
     )
+  }
 
   // The InputStream is passed by name, and we open it in the by-name
   // argument in callers, so we can avoid lifting into an effect.  Exposing
diff --git a/laws/src/main/scala/org/http4s/laws/discipline/arbitrary.scala b/laws/src/main/scala/org/http4s/laws/discipline/arbitrary.scala
index 96d80335ba6..cbcc7b780e7 100644
--- a/laws/src/main/scala/org/http4s/laws/discipline/arbitrary.scala
+++ b/laws/src/main/scala/org/http4s/laws/discipline/arbitrary.scala
@@ -1175,6 +1175,19 @@ private[discipline] trait ArbitraryInstancesBinCompat0 extends ArbitraryInstance
       )
     )
 
+  implicit val arbitraryContentTransferEncoding: Arbitrary[`Content-Transfer-Encoding`] =
+    Arbitrary[`Content-Transfer-Encoding`](
+      Gen.oneOf(
+        `Content-Transfer-Encoding`.`7bit`,
+        `Content-Transfer-Encoding`.`8bit`,
+        `Content-Transfer-Encoding`.Binary,
+        `Content-Transfer-Encoding`.QuotedPrintable,
+        `Content-Transfer-Encoding`.Base64,
+        `Content-Transfer-Encoding`.IetfToken,
+        `Content-Transfer-Encoding`.XToken,
+      )
+    )
+
   val genObsText: Gen[String] = Gen.stringOf(Gen.choose(0x80.toChar, 0xff.toChar))
   val genVcharExceptDquote: Gen[Char] = genVchar.filter(_ != 0x22.toChar)
 


Test patch:
diff --git a/tests/shared/src/test/scala/org/http4s/headers/ContentTransferEncodingSuite.scala b/tests/shared/src/test/scala/org/http4s/headers/ContentTransferEncodingSuite.scala
new file mode 100644
index 00000000000..bf92ffa2f0a
--- /dev/null
+++ b/tests/shared/src/test/scala/org/http4s/headers/ContentTransferEncodingSuite.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 http4s.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.http4s
+package headers
+
+import cats.syntax.either._
+import org.http4s.laws.discipline.arbitrary._
+
+class ContentTransferEncodingSuite extends HeaderLaws {
+  checkAll("Content-Transfer-Encoding", headerLaws[`Content-Transfer-Encoding`])
+
+  test("parsing case insensitive") {
+    assertEquals(
+      `Content-Transfer-Encoding`.parser.parseAll("7BIT"),
+      `Content-Transfer-Encoding`.`7bit`.asRight,
+    )
+  }
+}