Skip to content

Commit

Permalink
core: fix EOL detection for body part parsing
Browse files Browse the repository at this point in the history
Refs #2581

 * Improved the heuristic for detecting end of line character(s)
 * Added tests for UndefinedEndOfLineConfiguration

Co-authored-by: Johannes Rudolph <johannes.rudolph@gmail.com>
  • Loading branch information
cyburgee and jrudolph committed May 11, 2020
1 parent ce525d9 commit 428877e
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 9 deletions.
Expand Up @@ -120,13 +120,13 @@ private[http] final class BodyPartParser(
val ix = eolConfiguration.boundaryLength
if (eolConfiguration.isEndOfLine(input, ix)) parseHeaderLines(input, ix + eolConfiguration.eolLength)
else if (doubleDash(input, ix)) setShouldTerminate()
else parsePreamble(input, 0)
} else parsePreamble(input, 0)
else parsePreamble(input)
} else parsePreamble(input)
} catch {
case NotEnoughDataException => continue(input, 0)((newInput, _) => tryParseInitialBoundary(newInput))
}

def parsePreamble(input: ByteString, offset: Int): StateResult =
def parsePreamble(input: ByteString): StateResult =
try {
@tailrec def rec(index: Int): StateResult = {
val needleEnd = eolConfiguration.boyerMoore.nextIndex(input, index) + eolConfiguration.needle.length
Expand All @@ -135,9 +135,9 @@ private[http] final class BodyPartParser(
else rec(needleEnd)
}
eolConfiguration = eolConfiguration.defineOnce(input)
rec(offset)
rec(0)
} catch {
case NotEnoughDataException => continue(input.takeRight(eolConfiguration.needle.length + eolConfiguration.eolLength), 0)(parsePreamble)
case NotEnoughDataException => continue(input, 0)((newInput, _) => parsePreamble(newInput))
}

@tailrec def parseHeaderLines(input: ByteString, lineStart: Int, headers: ListBuffer[HttpHeader] = ListBuffer[HttpHeader](),
Expand Down Expand Up @@ -340,10 +340,10 @@ private[http] object BodyPartParser {

override def defineOnce(byteString: ByteString): EndOfLineConfiguration = {
// Hypothesis: There is either CRLF or LF as EOL, no mix possible
val cr = '\r'.toByte
val lf = '\n'.toByte
if (byteString.contains(cr)) DefinedEndOfLineConfiguration("\r\n", boundary)
else if (byteString.contains(lf)) DefinedEndOfLineConfiguration("\n", boundary)
val crLfNeedle = ByteString(s"$boundary\r\n")
val lfNeedle = ByteString(s"$boundary\n")
if (byteString.containsSlice(crLfNeedle)) DefinedEndOfLineConfiguration("\r\n", boundary)
else if (byteString.containsSlice(lfNeedle)) DefinedEndOfLineConfiguration("\n", boundary)
else this
}
}
Expand Down
Expand Up @@ -143,6 +143,24 @@ trait MultipartUnmarshallersSpec extends AnyFreeSpec with Matchers with BeforeAn
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("second part, explicitly typed"))))
}
"a full example (Strict) containing a CR in the body part" in {
Unmarshal(HttpEntity(
`multipart/mixed` withBoundary "12345",
ByteString("""preamble and
|more preamble
|--12345
|
|first part, impliINSERT_CR_HEREcitly typed
|--12345
|Content-Type: application/octet-stream
|
|second part, explicitly typed
|--12345--
|epilogue and
|more epilogue""".stripMarginWithNewline(lineFeed).replaceAll("INSERT_CR_HERE", "\r")))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, impli\rcitly typed")),
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("second part, explicitly typed"))))
}
"a full example (Default)" in {
val content = """preamble and
|more preamble
Expand All @@ -162,6 +180,25 @@ trait MultipartUnmarshallersSpec extends AnyFreeSpec with Matchers with BeforeAn
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("second part, explicitly typed"))))
}
"a full example (Default) containing a CR in the body part" in {
val content = """preamble and
|more preamble
|--12345
|
|first paINSERT_CR_HERErt, implicitly typed
|--12345
|Content-Type: application/octet-stream
|
|second part, explicitly typed
|--12345--
|epilogue and
|more epilogue""".stripMarginWithNewline(lineFeed).replaceAll("INSERT_CR_HERE", "\r")
val byteStrings = content.map(c => ByteString(c.toString)) // one-char ByteStrings
Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "12345", content.length, Source(byteStrings)))
.to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first pa\rrt, implicitly typed")),
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("second part, explicitly typed"))))
}
"a boundary with spaces" in {
Unmarshal(HttpEntity(
`multipart/mixed` withBoundary "simple boundary",
Expand Down

0 comments on commit 428877e

Please sign in to comment.