diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..aead1172 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# Scala Steward: Reformat with scalafmt 3.9.9 +424ec59eb4865feb383ca53b4278dfb8b9b6c36c + +# Scala Steward: Reformat with scalafmt 3.10.3 +d8af028d2cd271ab7f3adec9842f07cce86a2ac3 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4c49c411..a427ec6f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,7 +15,7 @@ jobs: docs: ${{ steps.changes.outputs.docs }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - uses: dorny/paths-filter@v3 id: changes with: @@ -39,7 +39,7 @@ jobs: needs: changes if: ${{ needs.changes.outputs.code == 'true' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: jcheckstyle run: ./sbt jcheckStyle - name: scalafmtCheckAll @@ -54,12 +54,12 @@ jobs: matrix: java: ['8', '11', '17', '21', '24'] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: ${{ matrix.java }} - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ~/.cache key: ${{ runner.os }}-jdk${{ matrix.java }}-${{ hashFiles('**/*.sbt') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3dca7c2..0cec5029 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,13 +11,13 @@ jobs: name: Release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 10000 # Fetch all tags so that sbt-dynver can find the previous release version - run: git fetch --tags -f # Install OpenJDK 8 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: # We need to use JDK8 for Android compatibility https://github.com/msgpack/msgpack-java/issues/516 java-version: 8 diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 4cc85866..dc0fac1d 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -16,12 +16,12 @@ jobs: name: Publish snapshots runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 10000 # Fetch all tags so that sbt-dynver can find the previous release version - run: git fetch --tags - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: 11 distribution: adopt diff --git a/.scalafmt.conf b/.scalafmt.conf index e8563baf..d30b0537 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.9.8 +version = 3.10.3 project.layout = StandardConvention runner.dialect = scala3 maxColumn = 100 diff --git a/build.sbt b/build.sbt index 135145fd..4d72279e 100644 --- a/build.sbt +++ b/build.sbt @@ -1,11 +1,9 @@ Global / onChangedBuildSource := ReloadOnSourceChanges // For performance testing, ensure each test run one-by-one -Global / concurrentRestrictions := Seq( - Tags.limit(Tags.Test, 1) -) +Global / concurrentRestrictions := Seq(Tags.limit(Tags.Test, 1)) -val AIRFRAME_VERSION = "2025.1.14" +val AIRFRAME_VERSION = "2025.1.22" // Use dynamic snapshot version strings for non tagged versions ThisBuild / dynverSonatypeSnapshots := true @@ -15,37 +13,65 @@ ThisBuild / dynverSeparator := "-" // Publishing metadata ThisBuild / homepage := Some(url("https://msgpack.org/")) ThisBuild / licenses := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt")) -ThisBuild / scmInfo := Some( - ScmInfo( - url("https://github.com/msgpack/msgpack-java"), - "scm:git@github.com:msgpack/msgpack-java.git" +ThisBuild / scmInfo := + Some( + ScmInfo( + url("https://github.com/msgpack/msgpack-java"), + "scm:git@github.com:msgpack/msgpack-java.git" + ) ) -) -ThisBuild / developers := List( - Developer(id = "frsyuki", name = "Sadayuki Furuhashi", email = "frsyuki@users.sourceforge.jp", url = url("https://github.com/frsyuki")), - Developer(id = "muga", name = "Muga Nishizawa", email = "muga.nishizawa@gmail.com", url = url("https://github.com/muga")), - Developer(id = "oza", name = "Tsuyoshi Ozawa", email = "ozawa.tsuyoshi@gmail.com", url = url("https://github.com/oza")), - Developer(id = "komamitsu", name = "Mitsunori Komatsu", email = "komamitsu@gmail.com", url = url("https://github.com/komamitsu")), - Developer(id = "xerial", name = "Taro L. Saito", email = "leo@xerial.org", url = url("https://github.com/xerial")) -) +ThisBuild / developers := + List( + Developer( + id = "frsyuki", + name = "Sadayuki Furuhashi", + email = "frsyuki@users.sourceforge.jp", + url = url("https://github.com/frsyuki") + ), + Developer( + id = "muga", + name = "Muga Nishizawa", + email = "muga.nishizawa@gmail.com", + url = url("https://github.com/muga") + ), + Developer( + id = "oza", + name = "Tsuyoshi Ozawa", + email = "ozawa.tsuyoshi@gmail.com", + url = url("https://github.com/oza") + ), + Developer( + id = "komamitsu", + name = "Mitsunori Komatsu", + email = "komamitsu@gmail.com", + url = url("https://github.com/komamitsu") + ), + Developer( + id = "xerial", + name = "Taro L. Saito", + email = "leo@xerial.org", + url = url("https://github.com/xerial") + ) + ) -val buildSettings = Seq[Setting[_]]( - organization := "org.msgpack", - organizationName := "MessagePack", +val buildSettings = Seq[Setting[?]]( + organization := "org.msgpack", + organizationName := "MessagePack", organizationHomepage := Some(url("http://msgpack.org/")), - description := "MessagePack for Java", - scalaVersion := "3.7.1", - Test / logBuffered := false, + description := "MessagePack for Java", + scalaVersion := "3.7.1", + Test / logBuffered := false, // msgpack-java should be a pure-java library, so remove Scala specific configurations - autoScalaLibrary := false, - crossPaths := false, + autoScalaLibrary := false, + crossPaths := false, publishMavenStyle := true, // JVM options for building scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked", "-feature"), Test / javaOptions ++= Seq("-ea"), javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), - Compile / compile / javacOptions ++= Seq("-encoding", "UTF-8", "-Xlint:unchecked", "-Xlint:deprecation"), + Compile / compile / javacOptions ++= + Seq("-encoding", "UTF-8", "-Xlint:unchecked", "-Xlint:deprecation"), // Use lenient validation mode when generating Javadoc (for Java8) doc / javacOptions := { val opts = Seq("-source", "1.8") @@ -58,18 +84,22 @@ val buildSettings = Seq[Setting[_]]( // Add sonatype repository settings publishTo := { val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/" - if (isSnapshot.value) Some("central-snapshots" at centralSnapshots) - else localStaging.value + if (isSnapshot.value) + Some("central-snapshots" at centralSnapshots) + else + localStaging.value }, // Style check config: (sbt-jchekcstyle) jcheckStyleConfig := "facebook", // Run jcheckstyle both for main and test codes - Compile / compile := ((Compile / compile) dependsOn (Compile / jcheckStyle)).value, - Test / compile := ((Test / compile) dependsOn (Test / jcheckStyle)).value + Compile / compile := + ((Compile / compile) dependsOn (Compile / jcheckStyle)).value, + Test / compile := + ((Test / compile) dependsOn (Test / jcheckStyle)).value ) -val junitJupiter = "org.junit.jupiter" % "junit-jupiter" % "5.11.4" % "test" -val junitVintage = "org.junit.vintage" % "junit-vintage-engine" % "5.11.4" % "test" +val junitJupiter = "org.junit.jupiter" % "junit-jupiter" % "5.14.1" % "test" +val junitVintage = "org.junit.vintage" % "junit-vintage-engine" % "5.14.1" % "test" // Project settings lazy val root = Project(id = "msgpack-java", base = file(".")) @@ -77,8 +107,8 @@ lazy val root = Project(id = "msgpack-java", base = file(".")) buildSettings, // Do not publish the root project publishArtifact := false, - publish := {}, - publishLocal := {} + publish := {}, + publishLocal := {} ) .aggregate(msgpackCore, msgpackJackson) @@ -86,58 +116,58 @@ lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core")) .enablePlugins(SbtOsgi) .settings( buildSettings, - description := "Core library of the MessagePack for Java", + description := "Core library of the MessagePack for Java", OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-core", - OsgiKeys.exportPackage := Seq( - // TODO enumerate used packages automatically - "org.msgpack.core", - "org.msgpack.core.annotations", - "org.msgpack.core.buffer", - "org.msgpack.value", - "org.msgpack.value.impl" - ), + OsgiKeys.exportPackage := + Seq( + // TODO enumerate used packages automatically + "org.msgpack.core", + "org.msgpack.core.annotations", + "org.msgpack.core.buffer", + "org.msgpack.value", + "org.msgpack.value.impl" + ), testFrameworks += new TestFramework("wvlet.airspec.Framework"), - Test / javaOptions ++= Seq( - // --add-opens is not available in JDK8 - "-XX:+IgnoreUnrecognizedVMOptions", - "--add-opens=java.base/java.nio=ALL-UNNAMED", - "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" - ), + Test / javaOptions ++= + Seq( + // --add-opens is not available in JDK8 + "-XX:+IgnoreUnrecognizedVMOptions", + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" + ), Test / fork := true, - libraryDependencies ++= Seq( - // msgpack-core should have no external dependencies - junitJupiter, - junitVintage, - "org.wvlet.airframe" %% "airframe-json" % AIRFRAME_VERSION % "test", - "org.wvlet.airframe" %% "airspec" % AIRFRAME_VERSION % "test", - // Add property testing support with forAll methods - "org.scalacheck" %% "scalacheck" % "1.18.1" % "test", - // For performance comparison with msgpack v6 - "org.msgpack" % "msgpack" % "0.6.12" % "test", - // For integration test with Akka - "com.typesafe.akka" %% "akka-actor" % "2.6.20" % "test", - "org.scala-lang.modules" %% "scala-collection-compat" % "2.13.0" % "test" - ) + libraryDependencies ++= + Seq( + // msgpack-core should have no external dependencies + junitJupiter, + junitVintage, + "org.wvlet.airframe" %% "airframe-json" % AIRFRAME_VERSION % "test", + "org.wvlet.airframe" %% "airspec" % AIRFRAME_VERSION % "test", + // Add property testing support with forAll methods + "org.scalacheck" %% "scalacheck" % "1.19.0" % "test", + // For performance comparison with msgpack v6 + "org.msgpack" % "msgpack" % "0.6.12" % "test", + // For integration test with Akka + "com.typesafe.akka" %% "akka-actor" % "2.6.20" % "test", + "org.scala-lang.modules" %% "scala-collection-compat" % "2.14.0" % "test" + ) ) -lazy val msgpackJackson = - Project(id = "msgpack-jackson", base = file("msgpack-jackson")) - .enablePlugins(SbtOsgi) - .settings( - buildSettings, - name := "jackson-dataformat-msgpack", - description := "Jackson extension that adds support for MessagePack", - OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson", - OsgiKeys.exportPackage := Seq( - "org.msgpack.jackson", - "org.msgpack.jackson.dataformat" - ), - libraryDependencies ++= Seq( +lazy val msgpackJackson = Project(id = "msgpack-jackson", base = file("msgpack-jackson")) + .enablePlugins(SbtOsgi) + .settings( + buildSettings, + name := "jackson-dataformat-msgpack", + description := "Jackson extension that adds support for MessagePack", + OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson", + OsgiKeys.exportPackage := Seq("org.msgpack.jackson", "org.msgpack.jackson.dataformat"), + libraryDependencies ++= + Seq( "com.fasterxml.jackson.core" % "jackson-databind" % "2.18.4", junitJupiter, junitVintage, "org.apache.commons" % "commons-math3" % "3.6.1" % "test" ), - testOptions += Tests.Argument(TestFrameworks.JUnit, "-v") - ) - .dependsOn(msgpackCore) + testOptions += Tests.Argument(TestFrameworks.JUnit, "-v") + ) + .dependsOn(msgpackCore) diff --git a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java index 5a9a1a63..5819f6c2 100644 --- a/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java +++ b/msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java @@ -26,6 +26,8 @@ import java.io.Closeable; import java.io.IOException; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; @@ -150,6 +152,14 @@ public class MessageUnpacker { private static final MessageBuffer EMPTY_BUFFER = MessageBuffer.wrap(new byte[0]); + /** + * Threshold for switching from upfront allocation to gradual allocation. + * Payloads up to this size use efficient upfront allocation. + * Payloads exceeding this size use gradual allocation to detect malicious files + * that declare large payload sizes but contain little actual data. + */ + private static final int GRADUAL_ALLOCATION_THRESHOLD = 64 * 1024 * 1024; // 64 MB + private final boolean allowReadingStringAsBinary; private final boolean allowReadingBinaryAsString; private final CodingErrorAction actionOnMalformedString; @@ -1637,18 +1647,85 @@ public void readPayload(byte[] dst) * This method allocates a new byte array and consumes specified amount of bytes into the byte array. * *
- * This method is equivalent to readPayload(new byte[length]).
+ * For sizes up to {@link #GRADUAL_ALLOCATION_THRESHOLD}, this method uses efficient upfront allocation.
+ * For larger sizes, it uses gradual allocation to protect against malicious files that declare
+ * large payload sizes but contain little actual data.
*
* @param length number of bytes to be read
* @return the new byte array
* @throws IOException when underlying input throws IOException
+ * @throws MessageSizeException when the input ends before the declared size is reached (for large payloads)
*/
public byte[] readPayload(int length)
throws IOException
{
- byte[] newArray = new byte[length];
- readPayload(newArray);
- return newArray;
+ if (length <= GRADUAL_ALLOCATION_THRESHOLD) {
+ // Small/moderate size: use efficient upfront allocation
+ byte[] newArray = new byte[length];
+ readPayload(newArray);
+ return newArray;
+ }
+
+ // Large declared size: use gradual allocation to protect against malicious files
+ return readPayloadGradually(length);
+ }
+
+ /**
+ * Read payload gradually, allocating memory only as data becomes available.
+ * This method protects against malicious files that declare large payload sizes
+ * but contain little actual data.
+ *
+ * @param declaredLength the declared payload length
+ * @return the payload bytes
+ * @throws IOException when underlying input throws IOException
+ * @throws MessageSizeException when the input ends before the declared size is reached
+ */
+ private byte[] readPayloadGradually(int declaredLength)
+ throws IOException
+ {
+ List