Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.

# inline some parsing utils
ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.parsing.package.asciiString")
ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.parsing.package.byteAt")
ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.parsing.package.byteChar")
ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.parsing.package.escape")
ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.parsing.package.logParsingError")
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@
package org.apache.pekko.http.impl.engine

import java.lang.{ StringBuilder => JStringBuilder }

import org.apache.pekko
import pekko.event.LoggingAdapter
import pekko.http.scaladsl.model.ErrorInfo
import pekko.http.scaladsl.settings.ParserSettings
import pekko.util.ByteString

import scala.annotation.tailrec
import pekko.event.LoggingAdapter
import pekko.util.ByteString
import pekko.http.scaladsl.model.{ ErrorInfo, StatusCode, StatusCodes }
import pekko.http.impl.util.SingletonException

/**
* INTERNAL API
*/
package object parsing {

@inline
private[http] def escape(c: Char): String = c match {
case '\t' => "\\t"
case '\r' => "\\r"
Expand All @@ -36,17 +37,29 @@ package object parsing {
case x => x.toString
}

/**
* Like `byteChar` but doesn't throw `NotEnoughDataException` if the index is out of bounds.
* Used in places where we know that the index is valid because we checked the length beforehand.
*/
@inline
private[http] def safeByteChar(input: ByteString, ix: Int): Char =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrudolph thanks for the review. I can look at doing a jmh benchmark but it will take me a week or 2 to get back to it.

One question though: do you think the safeByteChar function is a useful change instead of having a few places in our code where we do the masking in place while usually, we call the byteChar function below? There are places where we know it safe so that we don't need to waste a call on checking the index is in bounds.

(input(ix) & 0xFF).toChar

@inline
private[http] def byteChar(input: ByteString, ix: Int): Char = (byteAt(input, ix) & 0xFF).toChar

@inline
private[http] def byteAt(input: ByteString, ix: Int): Byte =
if (ix < input.length) input(ix) else throw NotEnoughDataException

@inline
private[http] def asciiString(input: ByteString, start: Int, end: Int): String = {
@tailrec def build(ix: Int = start, sb: JStringBuilder = new JStringBuilder(end - start)): String =
if (ix == end) sb.toString else build(ix + 1, sb.append(input(ix).toChar))
if (start == end) "" else build()
}

@inline
private[http] def logParsingError(info: ErrorInfo, log: LoggingAdapter,
settings: ParserSettings.ErrorLoggingVerbosity,
ignoreHeaderNames: Set[String] = Set.empty): Unit =
Expand All @@ -60,29 +73,3 @@ package object parsing {
log.warning(info.formatPretty)
}
}

package parsing {

import pekko.annotation.InternalApi

/**
* INTERNAL API
*/
@InternalApi
private[parsing] class ParsingException(
val status: StatusCode,
val info: ErrorInfo) extends RuntimeException(info.formatPretty) {
def this(status: StatusCode, summary: String) =
this(status, ErrorInfo(if (summary.isEmpty) status.defaultMessage else summary))
def this(summary: String) =
this(StatusCodes.BadRequest, ErrorInfo(summary))
def this(summary: String, detail: String) =
this(StatusCodes.BadRequest, ErrorInfo(summary, detail))
}

/**
* INTERNAL API
*/
@InternalApi
private[parsing] object NotEnoughDataException extends SingletonException
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* license agreements; and to You under the Apache License, version 2.0:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* This file is part of the Apache Pekko project, which was derived from Akka.
*/

/*
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
*/

package org.apache.pekko.http.impl.util

import org.apache.pekko
import pekko.annotation.InternalApi

@InternalApi
private[http] object CharUtils {

/**
* Internal Pekko HTTP Use only.
*
* Efficiently lower-cases the given character.
* Note: only works for 7-bit ASCII letters (which is enough for header names)
*/
@inline def toLowerCase(c: Char): Char =
if (c >= 'A' && c <= 'Z') (c + 0x20 /* - 'A' + 'a' */ ).toChar else c

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* license agreements; and to You under the Apache License, version 2.0:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* This file is part of the Apache Pekko project, which was derived from Akka.
*/

/*
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
*/

package org.apache.pekko.http.impl.engine

import java.lang.{ StringBuilder => JStringBuilder }

import org.apache.pekko
import pekko.event.LoggingAdapter
import pekko.http.scaladsl.model.ErrorInfo
import pekko.http.scaladsl.settings.ParserSettings
import pekko.util.ByteString

import scala.annotation.tailrec

/**
* INTERNAL API
*/
package object parsing {

private[http] inline def escape(c: Char): String = c match {
case '\t' => "\\t"
case '\r' => "\\r"
case '\n' => "\\n"
case x if Character.isISOControl(x) => "\\u%04x".format(c.toInt)
case x => x.toString
}

/**
* Like `byteChar` but doesn't throw `NotEnoughDataException` if the index is out of bounds.
* Used in places where we know that the index is valid because we checked the length beforehand.
*/
private[http] inline def safeByteChar(input: ByteString, ix: Int): Char =
(input(ix) & 0xFF).toChar

private[http] inline def byteChar(input: ByteString, ix: Int): Char = (byteAt(input, ix) & 0xFF).toChar

private[http] inline def byteAt(input: ByteString, ix: Int): Byte =
if (ix < input.length) input(ix) else throw NotEnoughDataException

private[http] inline def asciiString(input: ByteString, start: Int, end: Int): String = {
@tailrec def build(ix: Int = start, sb: JStringBuilder = new JStringBuilder(end - start)): String =
if (ix == end) sb.toString else build(ix + 1, sb.append(input(ix).toChar))
if (start == end) "" else build()
}

private[http] inline def logParsingError(info: ErrorInfo, log: LoggingAdapter,
settings: ParserSettings.ErrorLoggingVerbosity,
ignoreHeaderNames: Set[String] = Set.empty): Unit =
settings match {
case ParserSettings.ErrorLoggingVerbosity.Off => // nothing to do
case ParserSettings.ErrorLoggingVerbosity.Simple =>
if (!ignoreHeaderNames.contains(info.errorHeaderName))
log.warning(info.summary)
case ParserSettings.ErrorLoggingVerbosity.Full =>
if (!ignoreHeaderNames.contains(info.errorHeaderName))
log.warning(info.formatPretty)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ private[http] object CharUtils {
* Efficiently lower-cases the given character.
* Note: only works for 7-bit ASCII letters (which is enough for header names)
*/
final def toLowerCase(c: Char): Char =
inline def toLowerCase(c: Char): Char =
if (c >= 'A' && c <= 'Z') (c + 0x20 /* - 'A' + 'a' */ ).toChar else c

}
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ private[engine] final class HttpHeaderParser private (
private def insert(input: ByteString, value: AnyRef)(cursor: Int = 0, endIx: Int = input.length, nodeIx: Int = 0,
colonIx: Int = 0): Unit = {
val char =
if (cursor < colonIx) toLowerCase((input(cursor) & 0xFF).toChar)
else if (cursor < endIx) (input(cursor) & 0xFF).toChar
if (cursor < colonIx) toLowerCase(safeByteChar(input, cursor))
else if (cursor < endIx) safeByteChar(input, cursor)
else '\u0000'
val node = nodes(nodeIx)
if (char == node) insert(input, value)(cursor + 1, endIx, nodeIx + 1, colonIx) // fast match, descend into only subnode
Expand Down Expand Up @@ -290,7 +290,7 @@ private[engine] final class HttpHeaderParser private (
endIx: Int = input.length, valueIx: Int = newValueIndex, colonIx: Int = 0): Unit = {
val newNodeIx = newNodeIndex
if (cursor < endIx) {
val c = (input(cursor) & 0xFF).toChar
val c = safeByteChar(input, cursor)
val char = if (cursor < colonIx) toLowerCase(c) else c
nodes(newNodeIx) = char
insertRemainingCharsAsNewNodes(input, value)(cursor + 1, endIx, valueIx, colonIx)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* license agreements; and to You under the Apache License, version 2.0:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* This file is part of the Apache Pekko project, which was derived from Akka.
*/

/*
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
*/

package org.apache.pekko.http.impl.engine
package parsing

import org.apache.pekko
import pekko.annotation.InternalApi
import pekko.http.impl.util.SingletonException
import pekko.http.scaladsl.model.{ ErrorInfo, StatusCode, StatusCodes }

/**
* INTERNAL API
*/
@InternalApi
private[parsing] class ParsingException(
val status: StatusCode,
val info: ErrorInfo) extends RuntimeException(info.formatPretty) {
def this(status: StatusCode, summary: String) =
this(status, ErrorInfo(if (summary.isEmpty) status.defaultMessage else summary))
def this(summary: String) =
this(StatusCodes.BadRequest, ErrorInfo(summary))
def this(summary: String, detail: String) =
this(StatusCodes.BadRequest, ErrorInfo(summary, detail))
}

/**
* INTERNAL API
*/
@InternalApi
private[parsing] object NotEnoughDataException extends SingletonException