diff --git a/zio-http/jvm/src/test/scala/zio/http/FormSpec.scala b/zio-http/jvm/src/test/scala/zio/http/FormSpec.scala index 01274fca2b..65897ad0d5 100644 --- a/zio-http/jvm/src/test/scala/zio/http/FormSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/FormSpec.scala @@ -86,7 +86,7 @@ object FormSpec extends ZIOHttpSpec { form2 == form, ) }, - test("encoding with custom paramaters [charset]") { + test("encoding with custom parameters [charset]") { val form = Form( FormField.textField( @@ -144,6 +144,30 @@ object FormSpec extends ZIOHttpSpec { } }, + test("decoding no lf at the end") { + val body = Chunk.fromArray( + s"""|--(((AaB03x)))${CR} + |Content-Disposition: form-data; name="hocon-data"${CR} + |Content-Type: text/plain${CR} + |${CR} + |foos: []${CR} + |--(((AaB03x)))${CR} + |Content-Disposition: form-data; name="json-data"${CR} + |Content-Type: text/plain${CR} + |${CR} + |{ "bars": [] }${CR} + |--(((AaB03x)))--""".stripMargin.getBytes(), + ) + + val form = Form( + FormField.textField("hocon-data", "foos: []", MediaType.text.`plain`), + FormField.textField("json-data", """{ "bars": [] }""", MediaType.text.`plain`), + ) + + Form + .fromMultipartBytes(body) + .map(form2 => assertTrue(form2 == form)) + }, ) val multiFormStreamingSuite: Spec[Any, Throwable] = diff --git a/zio-http/shared/src/main/scala/zio/http/internal/FormState.scala b/zio-http/shared/src/main/scala/zio/http/internal/FormState.scala index 2ca5c03b21..e7b09752af 100644 --- a/zio-http/shared/src/main/scala/zio/http/internal/FormState.scala +++ b/zio-http/shared/src/main/scala/zio/http/internal/FormState.scala @@ -30,7 +30,10 @@ private[http] object FormState { final class FormStateBuffer(boundary: Boundary) extends FormState { self => private val tree0: ChunkBuilder[FormAST] = ChunkBuilder.make[FormAST]() - private val buffer: ChunkBuilder.Byte = new ChunkBuilder.Byte + private val buffer: ChunkBuilder[Byte] = new ChunkBuilder.Byte + private var bufferSize: Int = 0 + private val closingBoundaryBytesSize = boundary.closingBoundaryBytes.size + private var boundaryMatches: Boolean = true private var lastByte: OptionalByte = OptionalByte.None private var isBufferEmpty = true @@ -54,8 +57,14 @@ private[http] object FormState { val crlf = byte == '\n' && !lastByte.isEmpty && lastByte.get == '\r' + val posInLine = bufferSize + (if (this.lastByte.isEmpty) 0 else 1) + boundaryMatches &&= posInLine < closingBoundaryBytesSize && boundary.closingBoundaryBytes(posInLine) == byte + val boundaryDetected = boundaryMatches && posInLine == closingBoundaryBytesSize - 1 + def flush(ast: FormAST): Unit = { buffer.clear() + bufferSize = 0 + boundaryMatches = true lastByte = OptionalByte.None if (crlf && isBufferEmpty && (phase eq Phase.Part1)) phase0 = Phase.Part2 if (ast.isContent && dropContents) () else addToTree(ast) @@ -72,7 +81,11 @@ private[http] object FormState { case ClosingBoundary(_) => BoundaryClosed(tree) } - } else if (crlf && (phase eq Phase.Part2)) { + } else if ((crlf || boundaryDetected) && (phase eq Phase.Part2)) { + if (boundaryDetected) { + buffer += '-' + buffer += '-' + } val ast = FormAST.makePart2(buffer.result(), boundary) ast match { @@ -87,6 +100,7 @@ private[http] object FormState { if (!lastByte.isEmpty) { if (isBufferEmpty) isBufferEmpty = false buffer += lastByte.get + bufferSize += 1 } lastByte = OptionalByte.Some(byte) self @@ -102,6 +116,8 @@ private[http] object FormState { def reset(): Unit = { tree0.clear() buffer.clear() + bufferSize = 0 + boundaryMatches = true isBufferEmpty = true dropContents = false phase0 = Phase.Part1