Skip to content

Commit 1f8fbc1

Browse files
authored
use byteAt instead of byteChar where it is easy to avoid the char conversion (#801)
* more performant boundary check * try to fix tests * Update BodyPartParser.scala * Update BodyPartParser.scala * Update BodyPartParser.scala * Update BodyPartParser.scala * add HttpConstants * use ByteAt to avoid some char conversions more changes more changes * add dash constant
1 parent 957f913 commit 1f8fbc1

File tree

10 files changed

+77
-55
lines changed

10 files changed

+77
-55
lines changed

http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/BodyPartParser.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import pekko.NotUsed
1818
import pekko.annotation.InternalApi
1919
import pekko.event.LoggingAdapter
2020
import pekko.http.impl.util._
21+
import pekko.http.impl.util.HttpConstants._
2122
import pekko.http.scaladsl.model._
2223
import pekko.http.scaladsl.model.headers._
2324
import pekko.stream.{ Attributes, FlowShape, Inlet, Outlet }
@@ -281,7 +282,7 @@ private[http] final class BodyPartParser(
281282
def done(): StateResult = null // StateResult is a phantom type
282283

283284
def doubleDash(input: ByteString, offset: Int): Boolean =
284-
byteChar(input, offset) == '-' && byteChar(input, offset + 1) == '-'
285+
byteAt(input, offset) == DASH_BYTE && byteAt(input, offset + 1) == DASH_BYTE
285286
}
286287
}
287288

@@ -354,7 +355,6 @@ private[http] object BodyPartParser {
354355
}
355356

356357
case class UndefinedEndOfLineConfiguration(boundary: String) extends EndOfLineConfiguration {
357-
import HttpConstants._
358358

359359
override def eol: String = "\r\n"
360360

http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParser.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,22 @@ import java.lang.{ StringBuilder => JStringBuilder }
2020
import org.apache.pekko
2121
import pekko.annotation.InternalApi
2222
import pekko.event.LoggingAdapter
23+
import pekko.http.scaladsl.settings.ParserSettings
2324
import pekko.http.scaladsl.settings.ParserSettings.{
25+
ErrorLoggingVerbosity,
2426
IllegalResponseHeaderNameProcessingMode,
2527
IllegalResponseHeaderValueProcessingMode
2628
}
27-
import pekko.http.scaladsl.settings.ParserSettings.ErrorLoggingVerbosity
28-
import pekko.http.scaladsl.settings.ParserSettings
29-
30-
import scala.annotation.tailrec
31-
import pekko.util.ByteString
3229
import pekko.http.ccompat._
3330
import pekko.http.impl.util._
31+
import pekko.http.impl.util.HttpConstants._
3432
import pekko.http.scaladsl.model.{ ErrorInfo, HttpHeader, MediaTypes, StatusCode, StatusCodes }
3533
import pekko.http.scaladsl.model.headers.{ EmptyHeader, RawHeader }
3634
import pekko.http.impl.model.parser.HeaderParser
3735
import pekko.http.impl.model.parser.CharacterClasses._
36+
import pekko.util.ByteString
37+
38+
import scala.annotation.tailrec
3839

3940
/**
4041
* INTERNAL API
@@ -600,7 +601,7 @@ private[http] object HttpHeaderParser {
600601
if (ix < limit)
601602
byteChar(input, ix) match {
602603
case '\t' => scanHeaderValue(hhp, input, start, limit, log, mode)(appended(' '), ix + 1)
603-
case '\r' if byteChar(input, ix + 1) == '\n' =>
604+
case '\r' if byteAt(input, ix + 1) == LF_BYTE =>
604605
if (WSP(byteChar(input, ix + 2))) scanHeaderValue(hhp, input, start, limit, log, mode)(appended(' '), ix + 3)
605606
else (if (sb != null) sb.toString else asciiString(input, start, ix), ix + 2)
606607
case '\n' =>

http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpMessageParser.scala

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,23 @@ package org.apache.pekko.http.impl.engine.parsing
1515

1616
import javax.net.ssl.SSLSession
1717

18-
import org.apache.pekko
19-
import pekko.stream.TLSProtocol._
20-
2118
import scala.annotation.tailrec
2219
import scala.collection.mutable.ListBuffer
20+
2321
import org.parboiled2.CharUtils
24-
import pekko.util.ByteString
22+
23+
import org.apache.pekko
24+
import pekko.annotation.InternalApi
2525
import pekko.http.impl.model.parser.CharacterClasses
26-
import pekko.http.scaladsl.settings.ParserSettings
26+
import pekko.http.impl.util.HttpConstants._
2727
import pekko.http.scaladsl.model.{ ParsingException => _, _ }
2828
import pekko.http.scaladsl.model.headers._
2929
import HttpProtocols._
3030
import ParserOutput._
31-
import pekko.annotation.InternalApi
31+
import pekko.http.scaladsl.settings.ParserSettings
3232
import pekko.http.scaladsl.settings.ParserSettings.ConflictingContentTypeHeaderProcessingMode
33+
import pekko.stream.TLSProtocol._
34+
import pekko.util.ByteString
3335

3436
/**
3537
* INTERNAL API
@@ -288,20 +290,20 @@ private[http] trait HttpMessageParser[Output >: MessageOutput <: ParserOutput] {
288290
emit(EntityChunk(HttpEntity.Chunk(input.slice(cursor, chunkBodyEnd).compact, extension)))
289291
Trampoline(_ => parseChunk(input, chunkBodyEnd + terminatorLen, isLastMessage, totalBytesRead + chunkSize))
290292
}
291-
byteChar(input, chunkBodyEnd) match {
292-
case '\r' if byteChar(input, chunkBodyEnd + 1) == '\n' => result(2)
293-
case '\n' => result(1)
294-
case x => failEntityStream("Illegal chunk termination")
293+
byteAt(input, chunkBodyEnd) match {
294+
case CR_BYTE if byteAt(input, chunkBodyEnd + 1) == LF_BYTE => result(2)
295+
case LF_BYTE => result(1)
296+
case x => failEntityStream("Illegal chunk termination")
295297
}
296298
} else parseTrailer(extension, cursor)
297299

298300
@tailrec def parseChunkExtensions(chunkSize: Int, cursor: Int)(startIx: Int = cursor): StateResult =
299301
if (cursor - startIx <= settings.maxChunkExtLength) {
300302
def extension = asciiString(input, startIx, cursor)
301-
byteChar(input, cursor) match {
302-
case '\r' if byteChar(input, cursor + 1) == '\n' => parseChunkBody(chunkSize, extension, cursor + 2)
303-
case '\n' => parseChunkBody(chunkSize, extension, cursor + 1)
304-
case _ => parseChunkExtensions(chunkSize, cursor + 1)(startIx)
303+
byteAt(input, cursor) match {
304+
case CR_BYTE if byteAt(input, cursor + 1) == LF_BYTE => parseChunkBody(chunkSize, extension, cursor + 2)
305+
case LF_BYTE => parseChunkBody(chunkSize, extension, cursor + 1)
306+
case _ => parseChunkExtensions(chunkSize, cursor + 1)(startIx)
305307
}
306308
} else failEntityStream(
307309
s"HTTP chunk extension length exceeds configured limit of ${settings.maxChunkExtLength} characters")
@@ -314,7 +316,7 @@ private[http] trait HttpMessageParser[Output >: MessageOutput <: ParserOutput] {
314316
failEntityStream(
315317
s"HTTP chunk of $size bytes exceeds the configured limit of ${settings.maxChunkSize} bytes")
316318
case ';' if cursor > offset => parseChunkExtensions(size.toInt, cursor + 1)()
317-
case '\r' if cursor > offset && byteChar(input, cursor + 1) == '\n' =>
319+
case '\r' if cursor > offset && byteAt(input, cursor + 1) == LF_BYTE =>
318320
parseChunkBody(size.toInt, "", cursor + 2)
319321
case '\n' if cursor > offset => parseChunkBody(size.toInt, "", cursor + 1)
320322
case c if CharacterClasses.WSP(c) => parseSize(cursor + 1, size) // illegal according to the spec but can happen, see issue #1812

http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpRequestParser.scala

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,22 @@ import javax.net.ssl.SSLSession
1919
import scala.annotation.{ switch, tailrec }
2020

2121
import org.apache.pekko
22-
import pekko.http.scaladsl.settings.{ ParserSettings, WebSocketSettings }
23-
import pekko.util.ByteString
24-
import pekko.util.OptionVal
22+
import pekko.annotation.InternalApi
23+
import pekko.http.impl.engine.server.HttpAttributes
24+
import pekko.http.impl.util.ByteStringParserInput
25+
import pekko.http.impl.util.HttpConstants._
2526
import pekko.http.impl.engine.ws.Handshake
2627
import pekko.http.impl.model.parser.{ CharacterClasses, UriParser }
2728
import pekko.http.scaladsl.model.{ ParsingException => _, _ }
2829
import pekko.http.scaladsl.model.headers._
2930
import pekko.http.scaladsl.model.StatusCodes._
31+
import pekko.http.scaladsl.settings.{ ParserSettings, WebSocketSettings }
3032
import ParserOutput._
31-
import pekko.annotation.InternalApi
32-
import pekko.http.impl.engine.server.HttpAttributes
33-
import pekko.http.impl.util.ByteStringParserInput
3433
import pekko.stream.{ Attributes, FlowShape, Inlet, Outlet }
3534
import pekko.stream.TLSProtocol.SessionBytes
3635
import pekko.stream.stage.{ GraphStage, GraphStageLogic, InHandler, OutHandler }
36+
import pekko.util.ByteString
37+
import pekko.util.OptionVal
3738
import org.parboiled2.ParserInput
3839

3940
/**
@@ -91,9 +92,9 @@ private[http] final class HttpRequestParser(
9192
var cursor = parseMethod(input, offset)
9293
cursor = parseRequestTarget(input, cursor)
9394
cursor = parseProtocol(input, cursor)
94-
if (byteChar(input, cursor) == '\r' && byteChar(input, cursor + 1) == '\n')
95+
if (byteAt(input, cursor) == CR_BYTE && byteAt(input, cursor + 1) == LF_BYTE)
9596
parseHeaderLines(input, cursor + 2)
96-
else if (byteChar(input, cursor) == '\n')
97+
else if (byteAt(input, cursor) == LF_BYTE)
9798
parseHeaderLines(input, cursor + 1)
9899
else onBadProtocol(input.drop(cursor))
99100
} else
@@ -125,7 +126,7 @@ private[http] final class HttpRequestParser(
125126

126127
@tailrec def parseMethod(meth: HttpMethod, ix: Int = 1): Int =
127128
if (ix == meth.value.length)
128-
if (byteChar(input, cursor + ix) == ' ') {
129+
if (byteAt(input, cursor + ix) == SPACE_BYTE) {
129130
method = meth
130131
cursor + ix + 1
131132
} else parseCustomMethod()

http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpResponseParser.scala

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ import javax.net.ssl.SSLSession
1717
import scala.annotation.tailrec
1818
import scala.concurrent.Promise
1919
import scala.util.control.{ NoStackTrace, NonFatal }
20+
2021
import org.apache.pekko
22+
import pekko.annotation.InternalApi
2123
import pekko.http.scaladsl.settings.ParserSettings
2224
import pekko.http.impl.model.parser.CharacterClasses
25+
import pekko.http.impl.util.HttpConstants._
2326
import pekko.util.ByteString
2427
import pekko.http.scaladsl.model.{ ParsingException => _, _ }
2528
import pekko.http.scaladsl.model.headers._
2629
import ParserOutput._
27-
import pekko.annotation.InternalApi
2830
import pekko.http.impl.util.LogByteStringTools
2931
import pekko.stream.scaladsl.Source
3032

@@ -61,7 +63,7 @@ private[http] class HttpResponseParser(protected val settings: ParserSettings,
6163
override protected def parseMessage(input: ByteString, offset: Int): StateResult =
6264
if (contextForCurrentResponse.isDefined) {
6365
var cursor = parseProtocol(input, offset)
64-
if (byteChar(input, cursor) == ' ') {
66+
if (byteAt(input, cursor) == SPACE_BYTE) {
6567
cursor = parseStatus(input, cursor + 1)
6668
parseHeaderLines(input, cursor)
6769
} else onBadProtocol(input.drop(cursor))
@@ -104,8 +106,8 @@ private[http] class HttpResponseParser(protected val settings: ParserSettings,
104106
}
105107
}
106108

107-
def isLF(idx: Int) = byteChar(input, idx) == '\n'
108-
def isCRLF(idx: Int) = byteChar(input, idx) == '\r' && isLF(idx + 1)
109+
def isLF(idx: Int) = byteAt(input, idx) == LF_BYTE
110+
def isCRLF(idx: Int) = byteAt(input, idx) == CR_BYTE && isLF(idx + 1)
109111
def isNewLine(idx: Int) = isLF(idx) || isCRLF(idx)
110112

111113
def skipNewLine(idx: Int) = {
@@ -114,7 +116,7 @@ private[http] class HttpResponseParser(protected val settings: ParserSettings,
114116
else idx
115117
}
116118

117-
if (byteChar(input, cursor + 3) == ' ') {
119+
if (byteAt(input, cursor + 3) == SPACE_BYTE) {
118120
val startIdx = cursor + 4
119121
@tailrec def scanNewLineIdx(idx: Int): Int =
120122
if (idx - startIdx <= maxResponseReasonLength)

http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/SpecializedHeaderValueParsers.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import pekko.annotation.InternalApi
1919
import scala.annotation.tailrec
2020
import pekko.util.ByteString
2121
import pekko.http.impl.model.parser.CharacterClasses._
22+
import pekko.http.impl.util.HttpConstants._
2223
import pekko.http.scaladsl.model.{ ErrorInfo, HttpHeader }
2324
import pekko.http.scaladsl.model.headers.`Content-Length`
2425

@@ -39,7 +40,7 @@ private[parsing] object SpecializedHeaderValueParsers {
3940
if (result < 0) fail("`Content-Length` header value must not exceed 63-bit integer range")
4041
else if (DIGIT(c)) recurse(ix + 1, result * 10 + c - '0')
4142
else if (WSP(c)) recurse(ix + 1, result)
42-
else if (c == '\r' && byteChar(input, ix + 1) == '\n') (`Content-Length`(result), ix + 2)
43+
else if (c == '\r' && byteAt(input, ix + 1) == LF_BYTE) (`Content-Length`(result), ix + 2)
4344
else if (c == '\n') (`Content-Length`(result), ix + 1)
4445
else fail("Illegal `Content-Length` header value")
4546
}

http-core/src/main/scala/org/apache/pekko/http/impl/model/parser/CharacterClasses.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ private[http] object CharacterClasses {
2323
def ALPHA = CharPredicate.Alpha
2424
def LOWER_ALPHA = CharPredicate.LowerAlpha
2525
def UPPER_ALPHA = CharPredicate.UpperAlpha
26-
def CR = '\r'
26+
final val CR = '\r'
2727
val CTL = CharPredicate('\u0000' to '\u001F', '\u007F')
2828
def DIGIT = CharPredicate.Digit
2929
def ALPHANUM = CharPredicate.AlphaNum
30-
def DQUOTE = '"'
30+
final val DQUOTE = '"'
3131
def HEXDIG = CharPredicate.HexDigit
32-
def HTAB = '\t'
33-
def LF = '\n'
34-
def SP = ' '
32+
final val HTAB = '\t'
33+
final val LF = '\n'
34+
final val SP = ' '
3535
def VCHAR = CharPredicate.Visible
3636
val WSP = CharPredicate(SP, HTAB)
3737
val WSPCRLF = WSP ++ CR ++ LF

http-core/src/main/scala/org/apache/pekko/http/impl/util/HttpConstants.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ import org.apache.pekko.annotation.InternalApi
2929
private[http] object HttpConstants {
3030
final val CR_BYTE: Byte = 13
3131
final val LF_BYTE: Byte = 10
32+
final val SPACE_BYTE: Byte = 32
33+
final val DASH_BYTE: Byte = 45 // '-' (minus, dash, hyphen)
3234
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
# remove LineParser constants (moved to HttpConstants)
19+
ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.scaladsl.unmarshalling.sse.LineParser.CR")
20+
ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.scaladsl.unmarshalling.sse.LineParser.LF")

http/src/main/scala/org/apache/pekko/http/scaladsl/unmarshalling/sse/LineParser.scala

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,12 @@ import scala.annotation.tailrec
2121
import org.apache.pekko
2222
import pekko.annotation.InternalApi
2323
import pekko.event.Logging
24+
import pekko.http.impl.util.HttpConstants._
2425
import pekko.http.scaladsl.settings.OversizedSseStrategy
2526
import pekko.stream.{ Attributes, FlowShape, Inlet, Outlet }
2627
import pekko.stream.stage.{ GraphStage, GraphStageLogic, InHandler, OutHandler }
2728
import pekko.util.ByteString
2829

29-
/** INTERNAL API */
30-
@InternalApi
31-
private object LineParser {
32-
val CR = '\r'.toByte
33-
val LF = '\n'.toByte
34-
}
35-
3630
/**
3731
* A wrapper for an SSE line which exceeds the configured limit. Used for pattern matching.
3832
* @param line The oversized contents of the SSE line being parsed.
@@ -51,7 +45,6 @@ private final class LineParser(maxLineSize: Int,
5145

5246
override def createLogic(attributes: Attributes) =
5347
new GraphStageLogic(shape) with InHandler with OutHandler {
54-
import LineParser._
5548
import shape._
5649

5750
private var buffer = ByteString.empty
@@ -91,7 +84,7 @@ private final class LineParser(maxLineSize: Int,
9184
(bs.drop(from), parsedLines, lastCharWasCr)
9285
else
9386
bs(at) match {
94-
case CR if at < bs.length - 1 && bs(at + 1) == LF =>
87+
case CR_BYTE if at < bs.length - 1 && bs(at + 1) == LF_BYTE =>
9588
// Lookahead for LF after CR
9689
val lineByteSize = at - from
9790
val line = bs.slice(from, at).utf8String
@@ -102,7 +95,7 @@ private final class LineParser(maxLineSize: Int,
10295
}
10396
val newParsedLines = processedLine.fold(parsedLines)(parsedLines :+ _)
10497
parseLines(bs, at + 2, at + 2, newParsedLines, lastCharWasCr = false)
105-
case CR =>
98+
case CR_BYTE =>
10699
// if is a CR but we don't know the next character, slice it but flag that the last character was a CR so if the next happens to be a LF we just ignore
107100
val lineByteSize = at - from
108101
val line = bs.slice(from, at).utf8String
@@ -113,10 +106,10 @@ private final class LineParser(maxLineSize: Int,
113106
}
114107
val newParsedLines = processedLine.fold(parsedLines)(parsedLines :+ _)
115108
parseLines(bs, at + 1, at + 1, newParsedLines, lastCharWasCr = true)
116-
case LF if lastCharWasCr =>
109+
case LF_BYTE if lastCharWasCr =>
117110
// if is a LF and we just sliced a CR then we simply advance
118111
parseLines(bs, at + 1, at + 1, parsedLines, lastCharWasCr = false)
119-
case LF =>
112+
case LF_BYTE =>
120113
// a LF that wasn't preceded by a CR means we found a new slice
121114
val lineByteSize = at - from
122115
val line = bs.slice(from, at).utf8String

0 commit comments

Comments
 (0)