diff --git a/http-core/src/main/mima-filters/2.0.x.backwards.excludes/inline-utils.excludes b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/inline-utils.excludes new file mode 100644 index 000000000..1b126d3c4 --- /dev/null +++ b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/inline-utils.excludes @@ -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") diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/package.scala b/http-core/src/main/scala-2.13/org/apache/pekko/http/impl/engine/package.scala similarity index 72% rename from http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/package.scala rename to http-core/src/main/scala-2.13/org/apache/pekko/http/impl/engine/package.scala index 53af9461e..1afa8026e 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/package.scala +++ b/http-core/src/main/scala-2.13/org/apache/pekko/http/impl/engine/package.scala @@ -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" @@ -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 = + (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 = @@ -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 -} diff --git a/http-core/src/main/scala-2.13/org/apache/pekko/http/impl/util/CharUtils.scala b/http-core/src/main/scala-2.13/org/apache/pekko/http/impl/util/CharUtils.scala new file mode 100644 index 000000000..b26e9e7da --- /dev/null +++ b/http-core/src/main/scala-2.13/org/apache/pekko/http/impl/util/CharUtils.scala @@ -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. + */ + +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 + +} diff --git a/http-core/src/main/scala-3/org/apache/pekko/http/impl/engine/package.scala b/http-core/src/main/scala-3/org/apache/pekko/http/impl/engine/package.scala new file mode 100644 index 000000000..0913d1296 --- /dev/null +++ b/http-core/src/main/scala-3/org/apache/pekko/http/impl/engine/package.scala @@ -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. + */ + +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) + } +} diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/util/CharUtils.scala b/http-core/src/main/scala-3/org/apache/pekko/http/impl/util/CharUtils.scala similarity index 94% rename from http-core/src/main/scala/org/apache/pekko/http/impl/util/CharUtils.scala rename to http-core/src/main/scala-3/org/apache/pekko/http/impl/util/CharUtils.scala index f15e4f23c..03d0c7f6d 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/util/CharUtils.scala +++ b/http-core/src/main/scala-3/org/apache/pekko/http/impl/util/CharUtils.scala @@ -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 } diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParser.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParser.scala index 7b697a440..5aad5f5bf 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParser.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParser.scala @@ -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 @@ -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) diff --git a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/ParsingException.scala b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/ParsingException.scala new file mode 100644 index 000000000..218e0fec4 --- /dev/null +++ b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/ParsingException.scala @@ -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. + */ + +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