From b3f33f96d22ff80314a202f892320263cb1460b9 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 6 Oct 2025 23:08:35 +0100 Subject: [PATCH 1/3] inline some functions used in parsing scalafmt --- .../pekko/http/impl/engine}/package.scala | 38 +++--------- .../pekko/http/impl/util/CharUtils.scala | 31 ++++++++++ .../pekko/http/impl/engine/package.scala | 62 +++++++++++++++++++ .../pekko/http/impl/util/CharUtils.scala | 2 +- .../engine/parsing/HttpHeaderParser.scala | 6 +- .../engine/parsing/ParsingException.scala | 41 ++++++++++++ 6 files changed, 146 insertions(+), 34 deletions(-) rename http-core/src/main/{scala/org/apache/pekko/http/impl/engine/parsing => scala-2.13/org/apache/pekko/http/impl/engine}/package.scala (72%) create mode 100644 http-core/src/main/scala-2.13/org/apache/pekko/http/impl/util/CharUtils.scala create mode 100644 http-core/src/main/scala-3/org/apache/pekko/http/impl/engine/package.scala rename http-core/src/main/{scala => scala-3}/org/apache/pekko/http/impl/util/CharUtils.scala (94%) create mode 100644 http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/ParsingException.scala 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..c3b4a9ed6 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,14 +14,14 @@ 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 @@ -36,17 +36,21 @@ package object parsing { case x => x.toString } + @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 +64,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..117d14f70 --- /dev/null +++ b/http-core/src/main/scala-3/org/apache/pekko/http/impl/engine/package.scala @@ -0,0 +1,62 @@ +/* + * 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 + } + + 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..f9c38a7b6 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(byteChar(input, cursor)) + else if (cursor < endIx) byteChar(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 = byteChar(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 From 3cc5f767b0131368d465526fa37a89ee7b5563c1 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 6 Oct 2025 23:22:53 +0100 Subject: [PATCH 2/3] Create inline-utils.excludes --- .../inline-utils.excludes | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 http-core/src/main/mima-filters/2.0.x.backwards.excludes/inline-utils.excludes 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") From de0fa119944572c59cbf2ccb5a9d0c3a340eeca0 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Tue, 7 Oct 2025 00:21:23 +0100 Subject: [PATCH 3/3] Update package.scala add safeByteChar scalafmt --- .../org/apache/pekko/http/impl/engine/package.scala | 9 +++++++++ .../org/apache/pekko/http/impl/engine/package.scala | 7 +++++++ .../http/impl/engine/parsing/HttpHeaderParser.scala | 6 +++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/http-core/src/main/scala-2.13/org/apache/pekko/http/impl/engine/package.scala b/http-core/src/main/scala-2.13/org/apache/pekko/http/impl/engine/package.scala index c3b4a9ed6..1afa8026e 100644 --- a/http-core/src/main/scala-2.13/org/apache/pekko/http/impl/engine/package.scala +++ b/http-core/src/main/scala-2.13/org/apache/pekko/http/impl/engine/package.scala @@ -28,6 +28,7 @@ import scala.annotation.tailrec */ package object parsing { + @inline private[http] def escape(c: Char): String = c match { case '\t' => "\\t" case '\r' => "\\r" @@ -36,6 +37,14 @@ 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 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 index 117d14f70..0913d1296 100644 --- 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 @@ -36,6 +36,13 @@ 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. + */ + 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 = 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 f9c38a7b6..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(byteChar(input, cursor)) - else if (cursor < endIx) byteChar(input, cursor) + 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 = byteChar(input, cursor) + 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)