diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..58f44dd3 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,11 @@ +# Scala Steward: Reformat with scalafmt 3.9.9 +424ec59eb4865feb383ca53b4278dfb8b9b6c36c + +# Scala Steward: Reformat with scalafmt 3.10.3 +d8af028d2cd271ab7f3adec9842f07cce86a2ac3 + +# Scala Steward: Reformat with scalafmt 3.10.7 +83b819003b44835b1792e3ef8263da91a5b6d4ba + +# Scala Steward: Reformat with scalafmt 3.11.1 +5a8f52d4aeff3380e4e38e5cd8e63626711b2f86 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7ff24e9b..88301aec 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,100 +2,87 @@ name: CI on: pull_request: - paths: - - '**.scala' - - '**.java' - - '**.sbt' - - '.github/workflows/**.yml' - - 'project/build.properties' push: branches: - main - paths: - - '**.scala' - - '**.java' - - '**.sbt' - - '.github/workflows/**.yml' - - 'project/build.properties' - workflow_dispatch: + workflow_dispatch: jobs: - code_format: - name: Code Format + changes: runs-on: ubuntu-latest + outputs: + code: ${{ steps.changes.outputs.code }} + docs: ${{ steps.changes.outputs.docs }} steps: - - uses: actions/checkout@v4 - - name: jcheckstyle - run: ./sbt jcheckStyle - test_jdk21: - name: Test JDK21 + - name: Checkout + uses: actions/checkout@v7 + - uses: dorny/paths-filter@v4 + id: changes + with: + filters: | + code: + - '**.scala' + - '**.java' + - '**.sbt' + - '.github/workflows/**.yml' + - 'project/build.properties' + - 'msgpack-core/**' + - 'msgpack-jackson/**' + docs: + - '**.md' + - '**.txt' + - 'LICENSE' + + code_format: + name: Code Format runs-on: ubuntu-latest + needs: changes + if: ${{ needs.changes.outputs.code == 'true' }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - uses: actions/checkout@v7 + # sbt 2 requires JDK 17+ to run + - uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: '21' - - uses: actions/cache@v4 - with: - path: ~/.cache - key: ${{ runner.os }}-jdk21-${{ hashFiles('**/*.sbt') }} - restore-keys: ${{ runner.os }}-jdk21- - - name: Test - run: ./sbt test - - name: Universal Buffer Test - run: ./sbt test -J-Dmsgpack.universal-buffer=true - test_jdk17: - name: Test JDK17 + - name: jcheckstyle + run: ./sbt jcheckStyle + - name: scalafmtCheckAll + run: ./sbt scalafmtCheckAll + + test: + name: Test JDK${{ matrix.java }} runs-on: ubuntu-latest + needs: changes + if: ${{ needs.changes.outputs.code == 'true' }} + strategy: + matrix: + java: ['8', '11', '17', '21', '24'] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - uses: actions/checkout@v7 + # The JDK under test: msgpack-core forks its tests onto this JDK (via + # TEST_JAVA_HOME) so we still verify runtime behavior on each target JDK. + - uses: actions/setup-java@v5 + id: target-jdk with: distribution: 'zulu' - java-version: '17' - - uses: actions/cache@v4 - with: - path: ~/.cache - key: ${{ runner.os }}-jdk17-${{ hashFiles('**/*.sbt') }} - restore-keys: ${{ runner.os }}-jdk17- - - name: Test - run: ./sbt test - - name: Universal Buffer Test - run: ./sbt test -J-Dmsgpack.universal-buffer=true - test_jdk11: - name: Test JDK11 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + java-version: ${{ matrix.java }} + # sbt 2 itself requires JDK 17+ to run, so install a fixed, newer JDK to + # actually launch sbt. This becomes the default JDK on PATH/JAVA_HOME. + - uses: actions/setup-java@v5 with: distribution: 'zulu' - java-version: '11' - - uses: actions/cache@v4 + java-version: '21' + - uses: actions/cache@v6 with: path: ~/.cache - key: ${{ runner.os }}-jdk11-${{ hashFiles('**/*.sbt') }} - restore-keys: ${{ runner.os }}-jdk11- + key: ${{ runner.os }}-jdk${{ matrix.java }}-${{ hashFiles('**/*.sbt') }} + restore-keys: ${{ runner.os }}-jdk${{ matrix.java }}- - name: Test + env: + TEST_JAVA_HOME: ${{ steps.target-jdk.outputs.path }} run: ./sbt test - name: Universal Buffer Test - run: ./sbt test -J-Dmsgpack.universal-buffer=true - test_jdk8: - name: Test JDK8 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '8' - - uses: actions/cache@v4 - with: - path: ~/.cache - key: ${{ runner.os }}-jdk8-${{ hashFiles('**/*.sbt') }} - restore-keys: ${{ runner.os }}-jdk8- - - name: Test - run: ./sbt test - - name: Universal Buffer Test - run: ./sbt test -J-Dmsgpack.universal-buffer=true + env: + TEST_JAVA_HOME: ${{ steps.target-jdk.outputs.path }} + run: ./sbt test -J-Dmsgpack.universal-buffer=true \ No newline at end of file diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 00000000..fa651d47 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,41 @@ +name: auto-merge +# pull_request_target runs in the base-branch context, which is required so +# that GITHUB_TOKEN gets the write permissions declared below even for PRs +# opened by Dependabot (which would otherwise receive a read-only token on +# regular pull_request events). This workflow never checks out PR head code, +# so the usual pull_request_target injection risk does not apply. +on: pull_request_target + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge-dependabot: + name: Auto-Merge Dependabot PRs + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v3 + - name: Enable auto-merge for non-major updates + if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' }} + run: gh pr merge --squash --auto "${{ github.event.pull_request.html_url }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + auto-merge-scala-steward: + name: Auto-Merge Scala Steward PRs + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.user.login == 'scala-steward' }} + steps: + # Skip auto-merge if the PR carries a semver-major label. Note: this repo + # does not yet apply semver labels to Scala Steward PRs, so this guard is + # a no-op until early-semver-* labels are added to the repo and Scala + # Steward is configured to apply them. + - name: Enable auto-merge for non-major updates + if: ${{ !contains(github.event.pull_request.labels.*.name, 'semver-major') && !contains(github.event.pull_request.labels.*.name, 'early-semver-major') }} + run: gh pr merge --squash --auto "${{ github.event.pull_request.html_url }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b214d8ec..210b7c21 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,17 +11,24 @@ jobs: name: Release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 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 + # We need to compile with JDK8 for Android compatibility + # https://github.com/msgpack/msgpack-java/issues/516 + - uses: actions/setup-java@v5 + id: jdk8 with: - # We need to use JDK8 for Android compatibility https://github.com/msgpack/msgpack-java/issues/516 java-version: 8 distribution: adopt + # sbt 2 itself requires JDK 17+ to run; build.sbt forks the compiler (and tests) + # onto JDK8 via TEST_JAVA_HOME, so this becomes the default JDK on PATH/JAVA_HOME. + - uses: actions/setup-java@v5 + with: + java-version: 21 + distribution: adopt - name: Setup GPG env: PGP_SECRET: ${{ secrets.PGP_SECRET }} @@ -29,10 +36,11 @@ jobs: - name: Build bundle env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + TEST_JAVA_HOME: ${{ steps.jdk8.outputs.path }} run: | ./sbt publishSigned - name: Release to Sonatype env: - SONATYPE_USERNAME: '${{ secrets.SONATYPE_USER }}' - SONATYPE_PASSWORD: '${{ secrets.SONATYPE_PASS }}' - run: ./sbt sonatypeBundleRelease + SONATYPE_USERNAME: '${{ secrets.SONATYPE_USERNAME }}' + SONATYPE_PASSWORD: '${{ secrets.SONATYPE_PASSWORD }}' + run: ./sbt sonaRelease diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index cbce4c14..ec9edbfe 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -16,17 +16,25 @@ jobs: name: Publish snapshots runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 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 + id: jdk11 with: java-version: 11 distribution: adopt + # sbt 2 itself requires JDK 17+ to run; build.sbt forks the compiler onto JDK11 + # via TEST_JAVA_HOME, so this becomes the default JDK on PATH/JAVA_HOME. + - uses: actions/setup-java@v5 + with: + java-version: 21 + distribution: adopt - name: Publish snapshots env: - SONATYPE_USERNAME: '${{ secrets.SONATYPE_USER }}' - SONATYPE_PASSWORD: '${{ secrets.SONATYPE_PASS }}' + SONATYPE_USERNAME: '${{ secrets.SONATYPE_USERNAME }}' + SONATYPE_PASSWORD: '${{ secrets.SONATYPE_PASSWORD }}' + TEST_JAVA_HOME: ${{ steps.jdk11.outputs.path }} run: ./sbt publish diff --git a/.scala-steward.conf b/.scala-steward.conf new file mode 100644 index 00000000..7f143b54 --- /dev/null +++ b/.scala-steward.conf @@ -0,0 +1,8 @@ +# JUnit 6.x requires Java 17 at runtime, but this project still targets and +# tests on JDK 8 (see javacOptions in build.sbt and the test matrix in +# .github/workflows/CI.yml). Pin JUnit to the 5.x line until JDK 8 support is +# dropped. +updates.pin = [ + { groupId = "org.junit.jupiter", artifactId = "junit-jupiter", version = "5." }, + { groupId = "org.junit.vintage", artifactId = "junit-vintage-engine", version = "5." } +] diff --git a/.scalafmt.conf b/.scalafmt.conf index bda502a5..5190ffa6 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,3 +1,17 @@ -maxColumn = 180 +version = 3.11.1 +project.layout = StandardConvention +runner.dialect = scala3 +maxColumn = 100 style = defaultWithAlign +docstrings.blankFirstLine = yes +rewrite.scala3.convertToNewSyntax = true +rewrite.scala3.removeOptionalBraces = yes +rewrite.scala3.insertEndMarkerMinLines = 30 +# Add a new line before case class +newlines.topLevelStatementBlankLines = [ + { + blanks { after = 1 } + } +] +newlines.source = unfold optIn.breaksInsideChains = true diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2e72e982 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +MessagePack-Java is a binary serialization library that provides a fast and compact alternative to JSON. The project consists of two main modules: +- **msgpack-core**: Standalone MessagePack implementation with no external dependencies +- **msgpack-jackson**: Jackson integration for object mapping capabilities + +## Essential Development Commands + +### Build and Compile +```bash +./sbt compile # Compile source code +./sbt "Test / compile" # Compile source and test code +./sbt package # Create JAR files +``` + +### Testing +```bash +./sbt test # Run all tests +./sbt ~test # Run tests continuously on file changes +./sbt testOnly *TestClass # Run specific test class +./sbt "testOnly *TestClass -- -z pattern" # Run tests matching pattern + +# Test with universal buffer mode (for compatibility testing) +./sbt test -J-Dmsgpack.universal-buffer=true +``` + +### Code Quality +```bash +./sbt jcheckStyle # Run checkstyle (Facebook Presto style) +./sbt scalafmtAll # Format all Scala and sbt code +``` + +### Publishing +```bash +./sbt publishLocal # Install to local .ivy2 repository +./sbt publishM2 # Install to local .m2 Maven repository +``` + +## Architecture Overview + +### Core API Structure +The main entry point is the `MessagePack` factory class which creates: +- **MessagePacker**: Serializes objects to MessagePack binary format +- **MessageUnpacker**: Deserializes MessagePack binary data + +Key locations: +- Core interfaces: `msgpack-core/src/main/java/org/msgpack/core/` +- Jackson integration: `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/` + +### Buffer Management System +MessagePack uses an efficient buffer abstraction layer: +- **MessageBuffer**: Platform-optimized buffer implementations + - Uses `sun.misc.Unsafe` for performance when available + - Falls back to ByteBuffer on restricted platforms +- **MessageBufferInput/Output**: Manages buffer sequences for streaming + +### Jackson Integration +The msgpack-jackson module provides: +- **MessagePackFactory**: Jackson JsonFactory implementation +- **MessagePackMapper**: Pre-configured ObjectMapper for MessagePack +- Support for field IDs (integer keys) for compact serialization +- Extension type support including timestamps + +### Testing Structure +- **msgpack-core tests**: Written in Scala (always use the latest Scala 3 version) using AirSpec framework + - Location: `msgpack-core/src/test/scala/` +- **msgpack-jackson tests**: Written in Java using JUnit + - Location: `msgpack-jackson/src/test/java/` + +## Important JVM Options + +For JDK 17+ compatibility, these options are automatically added: +``` +--add-opens=java.base/java.nio=ALL-UNNAMED +--add-opens=java.base/sun.nio.ch=ALL-UNNAMED +``` + +## Code Style Requirements +- Java code follows Facebook Presto style (enforced by checkstyle) +- Scala test code uses Scalafmt with Scala 3 dialect and 100 character line limit +- Checkstyle runs automatically during compilation +- No external dependencies allowed in msgpack-core + +## Key Design Principles +1. **Zero Dependencies**: msgpack-core has no external dependencies +2. **Platform Optimization**: Uses platform-specific optimizations when available +3. **Streaming Support**: Both streaming and object-based APIs +4. **Type Safety**: Immutable Value hierarchy for type-safe data handling +5. **Extension Support**: Extensible type system for custom data types \ No newline at end of file diff --git a/README.md b/README.md index 34a3f277..f23af0b7 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ msgpack-java uses [sbt](http://www.scala-sbt.org/) for building the projects. Fo Coding style * msgpack-java uses [the same coding style](https://github.com/airlift/codestyle) with Facebook Presto * [IntelliJ setting file](https://raw.githubusercontent.com/airlift/codestyle/master/IntelliJIdea14/Airlift.xml) + * Scala test code uses Scalafmt with Scala 3 dialect (always use the latest Scala 3 version) ### Basic sbt commands Enter the sbt console: @@ -76,14 +77,14 @@ $ ./sbt Here is a list of sbt commands for daily development: ``` > ~compile # Compile source codes -> ~test:compile # Compile both source and test codes +> ~"Test / compile" # Compile both source and test codes > ~test # Run tests upon source code change > ~testOnly *MessagePackTest # Run tests in the specified class > ~testOnly *MessagePackTest -- (pattern) # Run tests matching the pattern > project msgpack-core # Focus on a specific project > package # Create a jar file in the target folder of each project > jcheckStyle # Run check style -> scalafmtAll # Reformat code +> scalafmtAll # Format all Scala and sbt code ``` ### Publishing @@ -107,27 +108,38 @@ A new release note will be generated automatically at the [GitHub Releases](http #### Publishing to Sonatype from Local Machine -If you need to publish to Maven central using a local machine, you need to configure [sbt-sonatype](https://github.com/xerial/sbt-sonatype) plugin. First set Sonatype account information (user name and password) in the global sbt settings. To protect your password, never include this file in your project. +If you need to publish to Maven central using a local machine, you need to configure credentials for Sonatype Central. First set Sonatype account information (user name and password) in the global sbt settings. To protect your password, never include this file in your project. -___$HOME/.sbt/(sbt-version)/sonatype.sbt___ +___$HOME/.sbt/1.0/credentials.sbt___ ``` -credentials += Credentials("Sonatype Nexus Repository Manager", - "oss.sonatype.org", - "(Sonatype user name)", - "(Sonatype password)") +credentials += Credentials(Path.userHome / ".sbt" / "sonatype_central_credentials") +``` + +Then create a credentials file at `~/.sbt/sonatype_central_credentials`: + +``` +host=central.sonatype.com +user= +password= +``` + +Alternatively, you can use environment variables: +```bash +export SONATYPE_USERNAME= +export SONATYPE_PASSWORD= ``` You may also need to configure GPG. See the instruction in [sbt-pgp](https://github.com/sbt/sbt-pgp). -Then, run `publishedSigned` followed by `sonatypeBundleRelease`: +Then, run `publishSigned` followed by `sonaRelease`: ``` # [optional] When you need to perform the individual release steps manually, use the following commands: > publishSigned # Publish GPG signed artifacts to the Sonatype repository -> sonatypeBundleRelease # Publish to the Maven Central (It will be synched within less than 4 hours) +> sonaRelease # Publish to the Maven Central (It will be synched within less than 4 hours) ``` -If some sporadic error happens (e.g., Sonatype timeout), rerun `sonatypeBundleRelease` again. +If some sporadic error happens (e.g., Sonatype timeout), rerun `sonaRelease` again. ### Project Structure diff --git a/build.sbt b/build.sbt index 235f6286..301cc3bb 100644 --- a/build.sbt +++ b/build.sbt @@ -1,33 +1,96 @@ +import scala.language.implicitConversions + 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.1" +val AIRFRAME_VERSION = "2026.2.2" // Use dynamic snapshot version strings for non tagged versions ThisBuild / dynverSonatypeSnapshots := true // Use coursier friendly version separator ThisBuild / dynverSeparator := "-" -val buildSettings = Seq[Setting[_]]( - organization := "org.msgpack", - organizationName := "MessagePack", +// 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 / 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", organizationHomepage := Some(url("http://msgpack.org/")), - description := "MessagePack for Java", - scalaVersion := "2.13.12", - 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"), + // -release 8 pins scalac's resolution of JDK API calls to the JDK8 surface (e.g. + // test code calling ByteBuffer.flip() resolves to the inherited Buffer.flip():Buffer + // rather than JDK9's covariant ByteBuffer.flip():ByteBuffer override, which doesn't + // exist on a real JDK8 at runtime -> NoSuchMethodError). Unlike javac's --release, + // this doesn't need an ignore-symbol-file escape hatch since test code never touches + // JDK-internal APIs the way the main sources' Unsafe usage does. + scalacOptions ++= + Seq("-encoding", "UTF-8", "-deprecation", "-unchecked", "-feature", "-release", "8"), Test / javaOptions ++= Seq("-ea"), + // sbt 2 itself requires JDK 17+ to run, but each CI lane still needs to compile and + // test against its own target JDK (e.g. 8) to faithfully reproduce runtime behavior: + // javac resolves API calls against whichever JDK actually runs it (-source/-target + // only constrain language level and bytecode version, not API resolution), so e.g. + // compiling on JDK9+ can bind to covariant overloads like ByteBuffer.flip(): + // ByteBuffer that don't exist on a real JDK8 at runtime. When TEST_JAVA_HOME is set, + // fork both compilation and test execution onto that JDK; otherwise use the JDK + // running sbt, as before. + javaHome := sys.env.get("TEST_JAVA_HOME").map(file), + Test / fork := true, 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") @@ -38,15 +101,22 @@ val buildSettings = Seq[Setting[_]]( } }, // Add sonatype repository settings - publishTo := sonatypePublishToBundle.value, + publishTo := { + val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/" + 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 := Def.uncached((Compile / compile).dependsOn(Compile / jcheckStyle).value), + Test / compile := Def.uncached((Test / compile).dependsOn(Test / jcheckStyle).value) ) -val junitInterface = "com.github.sbt" % "junit-interface" % "0.13.3" % "test" +val junitJupiter = "org.junit.jupiter" % "junit-jupiter" % "5.14.4" % "test" +val junitVintage = "org.junit.vintage" % "junit-vintage-engine" % "5.14.4" % "test" // Project settings lazy val root = Project(id = "msgpack-java", base = file(".")) @@ -54,8 +124,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) @@ -63,56 +133,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" + ), + OsgiKeys.importPackage := Seq("!android.os", "!sun.*"), 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 / fork := true, - libraryDependencies ++= Seq( - // msgpack-core should have no external dependencies - junitInterface, - "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" - ) + 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" + ), + 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( - "com.fasterxml.jackson.core" % "jackson-databind" % "2.18.2", - junitInterface, +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.22.1", + 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 chunks = new ArrayList<>(); + int totalRead = 0; + int remaining = declaredLength; + + while (remaining > 0) { + int bufferRemaining = buffer.size() - position; + if (bufferRemaining == 0) { + // Need more data from input + MessageBuffer next = in.next(); + if (next == null) { + // Input ended before we read the declared size + throw new MessageSizeException( + String.format("Payload declared %,d bytes but input ended after %,d bytes", + declaredLength, totalRead), + declaredLength); + } + totalReadBytes += buffer.size(); + buffer = next; + position = 0; + bufferRemaining = buffer.size(); + } + + int toRead = Math.min(remaining, bufferRemaining); + byte[] chunk = new byte[toRead]; + buffer.getBytes(position, chunk, 0, toRead); + chunks.add(chunk); + totalRead += toRead; + position += toRead; + remaining -= toRead; + } + + // All data verified to exist - combine chunks into result + if (chunks.size() == 1) { + return chunks.get(0); // Common case: single chunk, no copy needed + } + + byte[] result = new byte[declaredLength]; + int offset = 0; + for (byte[] chunk : chunks) { + System.arraycopy(chunk, 0, result, offset, chunk.length); + offset += chunk.length; + } + return result; } /** diff --git a/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala index 6f013838..76f04c97 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/InvalidDataReadTest.scala @@ -5,20 +5,19 @@ import wvlet.airspec.AirSpec /** */ -class InvalidDataReadTest extends AirSpec { +class InvalidDataReadTest extends AirSpec: test("Reading long EXT32") { // Prepare an EXT32 data with 2GB (Int.MaxValue size) payload for testing the behavior of MessageUnpacker.skipValue() // Actually preparing 2GB of data, however, is too much for CI, so we create only the header part. - val msgpack = createMessagePackData(p => p.packExtensionTypeHeader(MessagePack.Code.EXT32, Int.MaxValue)) - val u = MessagePack.newDefaultUnpacker(msgpack) - try { + val msgpack = createMessagePackData(p => + p.packExtensionTypeHeader(MessagePack.Code.EXT32, Int.MaxValue) + ) + val u = MessagePack.newDefaultUnpacker(msgpack) + try // This error will be thrown after reading the header as the input has no EXT32 body intercept[MessageInsufficientBufferException] { u.skipValue() } - } finally { - u.close() - } + finally u.close() } -} diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala index 58b29f43..f4b74986 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageBufferPackerTest.scala @@ -17,10 +17,10 @@ package org.msgpack.core import java.io.ByteArrayOutputStream import java.util.Arrays -import org.msgpack.value.ValueFactory._ +import org.msgpack.value.ValueFactory.* import wvlet.airspec.AirSpec -class MessageBufferPackerTest extends AirSpec { +class MessageBufferPackerTest extends AirSpec: test("MessageBufferPacker") { test("be equivalent to ByteArrayOutputStream") { val packer1 = MessagePack.newDefaultBufferPacker @@ -48,4 +48,3 @@ class MessageBufferPackerTest extends AirSpec { } } -} diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala index 06a58e11..a626978d 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageFormatTest.scala @@ -25,46 +25,38 @@ import scala.util.Random /** * Created on 2014/05/07. */ -class MessageFormatTest extends AirSpec with Benchmark { +class MessageFormatTest extends AirSpec with Benchmark: test("MessageFormat") { test("cover all byte codes") { - def checkV(b: Byte, tpe: ValueType): Unit = { - try MessageFormat.valueOf(b).getValueType shouldBe tpe - catch { + def checkV(b: Byte, tpe: ValueType): Unit = + try + MessageFormat.valueOf(b).getValueType shouldBe tpe + catch case e: AirSpecException => error(f"Failure when looking at byte ${b}%02x") throw e - } - } - def checkF(b: Byte, f: MessageFormat): Unit = { - MessageFormat.valueOf(b) shouldBe f - } + def checkF(b: Byte, f: MessageFormat): Unit = MessageFormat.valueOf(b) shouldBe f - def check(b: Byte, tpe: ValueType, f: MessageFormat): Unit = { + def check(b: Byte, tpe: ValueType, f: MessageFormat): Unit = checkV(b, tpe) checkF(b, f) - } - for (i <- 0 until 0x7f) { + for i <- 0 until 0x7f do check(i.toByte, ValueType.INTEGER, MessageFormat.POSFIXINT) - } - for (i <- 0x80 until 0x8f) { + for i <- 0x80 until 0x8f do check(i.toByte, ValueType.MAP, MessageFormat.FIXMAP) - } - for (i <- 0x90 until 0x9f) { + for i <- 0x90 until 0x9f do check(i.toByte, ValueType.ARRAY, MessageFormat.FIXARRAY) - } check(Code.NIL, ValueType.NIL, MessageFormat.NIL) MessageFormat.valueOf(Code.NEVER_USED) shouldBe MessageFormat.NEVER_USED - for (i <- Seq(Code.TRUE, Code.FALSE)) { + for i <- Seq(Code.TRUE, Code.FALSE) do check(i, ValueType.BOOLEAN, MessageFormat.BOOLEAN) - } check(Code.BIN8, ValueType.BINARY, MessageFormat.BIN8) check(Code.BIN16, ValueType.BINARY, MessageFormat.BIN16) @@ -98,9 +90,8 @@ class MessageFormatTest extends AirSpec with Benchmark { check(Code.ARRAY16, ValueType.ARRAY, MessageFormat.ARRAY16) check(Code.ARRAY32, ValueType.ARRAY, MessageFormat.ARRAY32) - for (i <- 0xe0 to 0xff) { + for i <- 0xe0 to 0xff do check(i.toByte, ValueType.INTEGER, MessageFormat.NEGFIXINT) - } } test("improve the valueOf performance") { @@ -113,20 +104,19 @@ class MessageFormatTest extends AirSpec with Benchmark { time("lookup", repeat = 10) { block("switch") { var i = 0 - while (i < N) { + while i < N do MessageFormat.toMessageFormat(idx(i)) i += 1 - } } block("table") { var i = 0 - while (i < N) { + while i < N do MessageFormat.valueOf(idx(i)) i += 1 - } } } } } -} + +end MessageFormatTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala index dee315cd..21080f65 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackSpec.scala @@ -16,35 +16,34 @@ package org.msgpack.core import wvlet.log.LogLevel -import wvlet.log.io.{TimeReport, Timer} +import wvlet.log.io.TimeReport +import wvlet.log.io.Timer import java.io.ByteArrayOutputStream -object MessagePackSpec { +object MessagePackSpec: def toHex(arr: Array[Byte]) = arr.map(x => f"$x%02x").mkString(" ") - def createMessagePackData(f: MessagePacker => Unit): Array[Byte] = { + def createMessagePackData(f: MessagePacker => Unit): Array[Byte] = val b = new ByteArrayOutputStream() val packer = MessagePack.newDefaultPacker(b) f(packer) packer.close() b.toByteArray - } -} -trait Benchmark extends Timer { +trait Benchmark extends Timer: private val numWarmUpRuns = 10 - override protected def time[A](blockName: String, logLevel: LogLevel = LogLevel.INFO, repeat: Int = 1, blockRepeat: Int = 1)(f: => A): TimeReport = { - super.time(blockName, logLevel = LogLevel.INFO, repeat)(f) - } + override protected def time[A]( + blockName: String, + logLevel: LogLevel = LogLevel.INFO, + repeat: Int = 1, + blockRepeat: Int = 1 + )(f: => A): TimeReport = super.time(blockName, logLevel = LogLevel.INFO, repeat)(f) - override protected def block[A](name: String)(f: => A): TimeReport = { + override protected def block[A](name: String)(f: => A): TimeReport = var i = 0 - while (i < numWarmUpRuns) { + while i < numWarmUpRuns do f i += 1 - } super.block(name)(f) - } -} diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala index c2993f96..16236c65 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackTest.scala @@ -15,43 +15,46 @@ // package org.msgpack.core -import org.msgpack.core.MessagePack.{Code, PackerConfig, UnpackerConfig} +import org.msgpack.core.MessagePack.Code +import org.msgpack.core.MessagePack.PackerConfig +import org.msgpack.core.MessagePack.UnpackerConfig import org.msgpack.core.MessagePackSpec.toHex -import org.msgpack.value.{Value, Variable} +import org.msgpack.value.Value +import org.msgpack.value.Variable import org.scalacheck.Prop.propBoolean -import org.scalacheck.{Arbitrary, Gen} +import org.scalacheck.Arbitrary +import org.scalacheck.Gen import wvlet.airspec.AirSpec import wvlet.airspec.spi.PropertyCheck import java.io.ByteArrayOutputStream import java.math.BigInteger import java.nio.CharBuffer -import java.nio.charset.{CodingErrorAction, UnmappableCharacterException} +import java.nio.charset.CodingErrorAction +import java.nio.charset.UnmappableCharacterException import java.time.Instant import scala.util.Random /** * Created on 2014/05/07. */ -class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { +class MessagePackTest extends AirSpec with PropertyCheck with Benchmark: - private def isValidUTF8(s: String) = { - MessagePack.UTF8.newEncoder().canEncode(s) - } + private def isValidUTF8(s: String) = MessagePack.UTF8.newEncoder().canEncode(s) - private def containsUnmappableCharacter(s: String): Boolean = { - try { - MessagePack.UTF8 + private def containsUnmappableCharacter(s: String): Boolean = + try + MessagePack + .UTF8 .newEncoder() .onUnmappableCharacter(CodingErrorAction.REPORT) .encode(CharBuffer.wrap(s)) false - } catch { + catch case e: UnmappableCharacterException => true - case _: Exception => false - } - } + case _: Exception => + false test("clone packer config") { val config = new PackerConfig() @@ -78,13 +81,11 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { test("detect fixint values") { - for (i <- 0 until 0x7f) { + for i <- 0 until 0x7f do Code.isPosFixInt(i.toByte) shouldBe true - } - for (i <- 0x80 until 0xff) { + for i <- 0x80 until 0xff do Code.isPosFixInt(i.toByte) shouldBe false - } } test("detect fixarray values") { @@ -93,12 +94,11 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { packer.close val bytes = packer.toByteArray MessagePack.newDefaultUnpacker(bytes).unpackArrayHeader() shouldBe 0 - try { + try MessagePack.newDefaultUnpacker(bytes).unpackMapHeader() fail("Shouldn't reach here") - } catch { + catch case e: MessageTypeException => // OK - } } test("detect fixmap values") { @@ -107,12 +107,11 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { packer.close val bytes = packer.toByteArray MessagePack.newDefaultUnpacker(bytes).unpackMapHeader() shouldBe 0 - try { + try MessagePack.newDefaultUnpacker(bytes).unpackArrayHeader() fail("Shouldn't reach here") - } catch { + catch case e: MessageTypeException => // OK - } } test("detect fixint quickly") { @@ -125,34 +124,28 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { block("mask") { var i = 0 var count = 0 - while (i < N) { - if ((idx(i) & Code.POSFIXINT_MASK) == 0) { + while i < N do + if (idx(i) & Code.POSFIXINT_MASK) == 0 then count += 1 - } i += 1 - } } block("mask in func") { var i = 0 var count = 0 - while (i < N) { - if (Code.isPosFixInt(idx(i))) { + while i < N do + if Code.isPosFixInt(idx(i)) then count += 1 - } i += 1 - } } block("shift cmp") { var i = 0 var count = 0 - while (i < N) { - if ((idx(i) >>> 7) == 0) { + while i < N do + if (idx(i) >>> 7) == 0 then count += 1 - } i += 1 - } } @@ -162,13 +155,11 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { test("detect neg fix int values") { - for (i <- 0 until 0xe0) { + for i <- 0 until 0xe0 do Code.isNegFixInt(i.toByte) shouldBe false - } - for (i <- 0xe0 until 0xff) { + for i <- 0xe0 until 0xff do Code.isNegFixInt(i.toByte) shouldBe true - } } @@ -178,9 +169,9 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { unpack: MessageUnpacker => A, packerConfig: PackerConfig = new PackerConfig(), unpackerConfig: UnpackerConfig = new UnpackerConfig() - ): Boolean = { + ): Boolean = var b: Array[Byte] = null - try { + try val bs = new ByteArrayOutputStream() val packer = packerConfig.newPacker(bs) pack(packer) @@ -192,15 +183,12 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { val ret = unpack(unpacker) ret shouldBe v true - } catch { + catch case e: Exception => warn(e.getMessage) - if (b != null) { + if b != null then warn(s"packed data (size:${b.length}): ${toHex(b)}") - } throw e - } - } private def checkException[A]( v: A, @@ -208,7 +196,7 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { unpack: MessageUnpacker => A, packerConfig: PackerConfig = new PackerConfig(), unpaackerConfig: UnpackerConfig = new UnpackerConfig() - ): Unit = { + ): Unit = var b: Array[Byte] = null val bs = new ByteArrayOutputStream() val packer = packerConfig.newPacker(bs) @@ -221,15 +209,16 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { val ret = unpack(unpacker) fail("cannot not reach here") - } - private def checkOverflow[A](v: A, pack: MessagePacker => Unit, unpack: MessageUnpacker => A): Unit = { - try { + private def checkOverflow[A]( + v: A, + pack: MessagePacker => Unit, + unpack: MessageUnpacker => A + ): Unit = + try checkException[A](v, pack, unpack) - } catch { + catch case e: MessageIntegerOverflowException => // OK - } - } test("pack/unpack primitive values") { forAll { (v: Boolean) => @@ -257,7 +246,8 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { null, _.packNil, { unpacker => - unpacker.unpackNil(); null + unpacker.unpackNil(); + null } ) } @@ -279,29 +269,35 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { packer.packString("val") }, { unpacker => - unpacker.tryUnpackNil(); unpacker.unpackString() + unpacker.tryUnpackNil(); + unpacker.unpackString() } ) check( "val", { packer => - packer.packNil(); packer.packString("val") + packer.packNil(); + packer.packString("val") }, { unpacker => - unpacker.tryUnpackNil(); unpacker.unpackString() + unpacker.tryUnpackNil(); + unpacker.unpackString() } ) - try { - checkException(null, { _ => }, _.tryUnpackNil) - } catch { + try + checkException( + null, + { _ => + }, + _.tryUnpackNil + ) + catch case e: MessageInsufficientBufferException => // OK - } } test("pack/unpack integer values") { val sampleData = Seq[Long]( - Int.MinValue.toLong - - 10, + Int.MinValue.toLong - 10, -65535, -8191, -1024, @@ -327,31 +323,26 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { 65536, Int.MaxValue.toLong + 10 ) - for (v <- sampleData) { + for v <- sampleData do check(v, _.packLong(v), _.unpackLong) - if (v.isValidInt) { + if v.isValidInt then val vi = v.toInt check(vi, _.packInt(vi), _.unpackInt) - } else { + else checkOverflow(v, _.packLong(v), _.unpackInt) - } - if (v.isValidShort) { + if v.isValidShort then val vi = v.toShort check(vi, _.packShort(vi), _.unpackShort) - } else { + else checkOverflow(v, _.packLong(v), _.unpackShort) - } - if (v.isValidByte) { + if v.isValidByte then val vi = v.toByte check(vi, _.packByte(vi), _.unpackByte) - } else { + else checkOverflow(v, _.packLong(v), _.unpackByte) - } - - } } @@ -361,23 +352,19 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { check(v, _.packBigInteger(v), _.unpackBigInteger) } - for (bi <- Seq(BigInteger.valueOf(Long.MaxValue).add(BigInteger.valueOf(1)))) { + for bi <- Seq(BigInteger.valueOf(Long.MaxValue).add(BigInteger.valueOf(1))) do check(bi, _.packBigInteger(bi), _.unpackBigInteger()) - } - for (bi <- Seq(BigInteger.valueOf(Long.MaxValue).shiftLeft(10))) { - try { + for bi <- Seq(BigInteger.valueOf(Long.MaxValue).shiftLeft(10)) do + try checkException(bi, _.packBigInteger(bi), _.unpackBigInteger()) fail("cannot reach here") - } catch { + catch case e: IllegalArgumentException => // OK - } - } - } test("pack/unpack strings") { - val utf8Strings = Arbitrary.arbitrary[String].suchThat(isValidUTF8 _) + val utf8Strings = Arbitrary.arbitrary[String].suchThat(isValidUTF8) utf8Strings.map { v => check(v, _.packString(v), _.unpackString) } @@ -386,17 +373,15 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { test("pack/unpack large strings") { // Large string val strLen = Seq(1000, 2000, 10000, 50000, 100000, 500000) - for (l <- strLen) { - val v: String = - Iterator.continually(Random.nextString(l * 10)).find(isValidUTF8).get + for l <- strLen do + val v: String = Iterator.continually(Random.nextString(l * 10)).find(isValidUTF8).get check(v, _.packString(v), _.unpackString) - } } test("report errors when packing/unpacking malformed strings") { pending("We need to produce malformed utf-8 strings in Java 8") // Create 100 malformed UTF8 Strings - val r = new Random(0) + val r = new Random(0) val malformedStrings = Iterator .continually { val b = new Array[Byte](10) @@ -406,16 +391,14 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { .filter(b => !isValidUTF8(new String(b))) .take(100) - for (malformedBytes <- malformedStrings) { + for malformedBytes <- malformedStrings do // Pack tests val malformed = new String(malformedBytes) - try { + try checkException(malformed, _.packString(malformed), _.unpackString()) - } catch { + catch case e: MessageStringCodingException => // OK - } - - try { + try checkException( malformed, { packer => @@ -424,24 +407,22 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { }, _.unpackString() ) - } catch { + catch case e: MessageStringCodingException => // OK - } - } } test("report errors when packing/unpacking strings that contain unmappable characters") { val unmappable = Array[Byte](0xfc.toByte, 0x0a.toByte) - //val unmappableChar = Array[Char](new Character(0xfc0a).toChar) + // val unmappableChar = Array[Char](new Character(0xfc0a).toChar) // Report error on unmappable character val unpackerConfig = new UnpackerConfig() .withActionOnMalformedString(CodingErrorAction.REPORT) .withActionOnUnmappableString(CodingErrorAction.REPORT) - for (bytes <- Seq(unmappable)) { - try { + for bytes <- Seq(unmappable) do + try checkException( bytes, { packer => @@ -452,10 +433,8 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { new PackerConfig(), unpackerConfig ) - } catch { + catch case e: MessageStringCodingException => // OK - } - } } test("pack/unpack binary") { @@ -463,7 +442,8 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { check( v, { packer => - packer.packBinaryHeader(v.length); packer.writePayload(v) + packer.packBinaryHeader(v.length); + packer.writePayload(v) }, { unpacker => val len = unpacker.unpackBinaryHeader() @@ -475,13 +455,14 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { } val len = Seq(1000, 2000, 10000, 50000, 100000, 500000) - for (l <- len) { + for l <- len do val v = new Array[Byte](l) Random.nextBytes(v) check( v, { packer => - packer.packBinaryHeader(v.length); packer.writePayload(v) + packer.packBinaryHeader(v.length); + packer.writePayload(v) }, { unpacker => val len = unpacker.unpackBinaryHeader() @@ -490,10 +471,11 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { out } ) - } } - val testHeaderLength = Seq(1, 2, 4, 8, 16, 17, 32, 64, 255, 256, 1000, 2000, 10000, 50000, 100000, 500000) + val testHeaderLength = Seq( + 1, 2, 4, 8, 16, 17, 32, 64, 255, 256, 1000, 2000, 10000, 50000, 100000, 500000 + ) test("pack/unpack arrays") { forAll { (v: Array[Int]) => @@ -506,24 +488,20 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { { unpacker => val len = unpacker.unpackArrayHeader() val out = new Array[Int](len) - for (i <- 0 until v.length) { + for i <- 0 until v.length do out(i) = unpacker.unpackInt - } out } ) } - for (l <- testHeaderLength) { + for l <- testHeaderLength do check(l, _.packArrayHeader(l), _.unpackArrayHeader()) - } - try { + try checkException(0, _.packArrayHeader(-1), _.unpackArrayHeader) - } catch { + catch case e: IllegalArgumentException => // OK - } - } test("pack/unpack maps") { @@ -534,49 +512,51 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { m, { packer => packer.packMapHeader(v.length) - m.map { - case (k: Int, v: String) => - packer.packInt(k) - packer.packString(v) + m.map { case (k: Int, v: String) => + packer.packInt(k) + packer.packString(v) } }, { unpacker => val len = unpacker.unpackMapHeader() val b = Seq.newBuilder[(Int, String)] - for (i <- 0 until len) { + for i <- 0 until len do b += ((unpacker.unpackInt, unpacker.unpackString)) - } b.result() } ) } - for (l <- testHeaderLength) { + for l <- testHeaderLength do check(l, _.packMapHeader(l), _.unpackMapHeader()) - } - try { + try checkException(0, _.packMapHeader(-1), _.unpackMapHeader) - } catch { + catch case e: IllegalArgumentException => // OK - } - } test("pack/unpack extension types") { forAll { (dataLen: Int, tpe: Byte) => val l = Math.abs(dataLen) l >= 0 ==> { - val ext = - new ExtensionTypeHeader(ExtensionTypeHeader.checkedCastToByte(tpe), l) - check(ext, _.packExtensionTypeHeader(ext.getType, ext.getLength), _.unpackExtensionTypeHeader()) + val ext = new ExtensionTypeHeader(ExtensionTypeHeader.checkedCastToByte(tpe), l) + check( + ext, + _.packExtensionTypeHeader(ext.getType, ext.getLength), + _.unpackExtensionTypeHeader() + ) } } - for (l <- testHeaderLength) { - val ext = new ExtensionTypeHeader(ExtensionTypeHeader.checkedCastToByte(Random.nextInt(128)), l) - check(ext, _.packExtensionTypeHeader(ext.getType, ext.getLength), _.unpackExtensionTypeHeader()) - } + for l <- testHeaderLength do + val ext = + new ExtensionTypeHeader(ExtensionTypeHeader.checkedCastToByte(Random.nextInt(128)), l) + check( + ext, + _.packExtensionTypeHeader(ext.getType, ext.getLength), + _.unpackExtensionTypeHeader() + ) } @@ -587,31 +567,30 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { aMap, { packer => packer.packArrayHeader(aMap.size) - for (m <- aMap) { + for m <- aMap do packer.packMapHeader(m.size) - for ((k, v) <- m) { + for (k, v) <- m do packer.packString(k) packer.packString(v) - } - } }, { unpacker => val v = new Variable() unpacker.unpackValue(v) - import scala.jdk.CollectionConverters._ - v.asArrayValue().asScala + import scala.jdk.CollectionConverters.* + v.asArrayValue() + .asScala .map { m => val mv = m.asMapValue() val kvs = mv.getKeyValueArray kvs .grouped(2) - .map({ kvp: Array[Value] => + .map { (kvp: Array[Value]) => val k = kvp(0) val v = kvp(1) (k.asStringValue().asString, v.asStringValue().asString) - }) + } .toMap } .toList @@ -624,12 +603,24 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { val posInt = Gen.chooseNum(0, 1000000000 - 1) // NANOS_PER_SECOND forAll(posLong, posInt) { (second: Long, nano: Int) => val v = Instant.ofEpochSecond(second, nano) - check(v, { _.packTimestamp(v) }, { _.unpackTimestamp() }) + check( + v, { + _.packTimestamp(v) + }, { + _.unpackTimestamp() + } + ) } // Using different insterfaces forAll(posLong, posInt) { (second: Long, nano: Int) => val v = Instant.ofEpochSecond(second, nano) - check(v, { _.packTimestamp(second, nano) }, { _.unpackTimestamp() }) + check( + v, { + _.packTimestamp(second, nano) + }, { + _.unpackTimestamp() + } + ) } val secLessThan34bits = Gen.chooseNum[Long](0, 1L << 34) forAll(secLessThan34bits, posInt) { (second: Long, nano: Int) => @@ -642,23 +633,30 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { } // Corner-cases around uint32 boundaries - for ( - v <- Seq( - Instant.ofEpochSecond(Instant.now().getEpochSecond, 123456789L), // uint32 nanoseq (out of int32 range) - Instant.ofEpochSecond(-1302749144L, 0), // 1928-09-19T21:14:16Z - Instant.ofEpochSecond(-747359729L, 0), // 1946-04-27T00:04:31Z - Instant.ofEpochSecond(4257387427L, 0) // 2104-11-29T07:37:07Z + for v <- Seq( + Instant.ofEpochSecond( + Instant.now().getEpochSecond, + 123456789L + ), // uint32 nanoseq (out of int32 range) + Instant.ofEpochSecond(-1302749144L, 0), // 1928-09-19T21:14:16Z + Instant.ofEpochSecond(-747359729L, 0), // 1946-04-27T00:04:31Z + Instant.ofEpochSecond(4257387427L, 0) // 2104-11-29T07:37:07Z ) - ) { + do check(v, _.packTimestamp(v), _.unpackTimestamp()) - } } test("pack/unpack timestamp in millis") { val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L) forAll(posLong) { (millis: Long) => val v = Instant.ofEpochMilli(millis) - check(v, { _.packTimestamp(millis) }, { _.unpackTimestamp() }) + check( + v, { + _.packTimestamp(millis) + }, { + _.unpackTimestamp() + } + ) } } @@ -666,15 +664,16 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L) forAll(posLong) { (millis: Long) => val v = Instant.ofEpochMilli(millis) - check(v, { _.packTimestamp(millis) }, + check( + v, { + _.packTimestamp(millis) + }, { u => val extHeader = u.unpackExtensionTypeHeader() - if(extHeader.isTimestampType) { + if extHeader.isTimestampType then u.unpackTimestamp(extHeader) - } - else { + else fail("Cannot reach here") - } } ) } @@ -711,12 +710,11 @@ class MessagePackTest extends AirSpec with PropertyCheck with Benchmark { a.withBufferSize(64 * 1024).equals(b) shouldBe false a.withAllowReadingStringAsBinary(false).equals(b) shouldBe false a.withAllowReadingBinaryAsString(false).equals(b) shouldBe false - a.withActionOnMalformedString(CodingErrorAction.REPORT) - .equals(b) shouldBe false - a.withActionOnUnmappableString(CodingErrorAction.REPORT) - .equals(b) shouldBe false + a.withActionOnMalformedString(CodingErrorAction.REPORT).equals(b) shouldBe false + a.withActionOnUnmappableString(CodingErrorAction.REPORT).equals(b) shouldBe false a.withStringSizeLimit(32).equals(b) shouldBe false a.withStringDecoderBufferSize(32).equals(b) shouldBe false } } -} + +end MessagePackTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala index dea3e4ea..e31853ab 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/MessagePackerTest.scala @@ -16,46 +16,45 @@ package org.msgpack.core import org.msgpack.core.MessagePack.PackerConfig -import org.msgpack.core.buffer.{ChannelBufferOutput, OutputStreamBufferOutput} +import org.msgpack.core.buffer.ChannelBufferOutput +import org.msgpack.core.buffer.OutputStreamBufferOutput import org.msgpack.value.ValueFactory import wvlet.airspec.AirSpec import wvlet.log.io.IOUtil.withResource -import java.io.{ByteArrayOutputStream, File, FileInputStream, FileOutputStream} +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream import scala.util.Random /** */ -class MessagePackerTest extends AirSpec with Benchmark { +class MessagePackerTest extends AirSpec with Benchmark: - private def verifyIntSeq(answer: Array[Int], packed: Array[Byte]): Unit = { + private def verifyIntSeq(answer: Array[Int], packed: Array[Byte]): Unit = val unpacker = MessagePack.newDefaultUnpacker(packed) val b = Array.newBuilder[Int] - while (unpacker.hasNext) { + while unpacker.hasNext do b += unpacker.unpackInt() - } val result = b.result() result.size shouldBe answer.size result shouldBe answer - } - private def createTempFile = { + private def createTempFile = val f = File.createTempFile("msgpackTest", "msgpack") f.deleteOnExit f - } - private def createTempFileWithOutputStream = { + private def createTempFileWithOutputStream = val f = createTempFile val out = new FileOutputStream(f) (f, out) - } - private def createTempFileWithChannel = { + private def createTempFileWithChannel = val (f, out) = createTempFileWithOutputStream val ch = out.getChannel (f, ch) - } test("MessagePacker") { @@ -70,16 +69,14 @@ class MessagePackerTest extends AirSpec with Benchmark { val intSeq2 = intSeq.reverse val b2 = new ByteArrayOutputStream - packer - .reset(new OutputStreamBufferOutput(b2)) + packer.reset(new OutputStreamBufferOutput(b2)) intSeq2 foreach packer.packInt packer.close verifyIntSeq(intSeq2, b2.toByteArray) val intSeq3 = intSeq2.sorted val b3 = new ByteArrayOutputStream - packer - .reset(new OutputStreamBufferOutput(b3)) + packer.reset(new OutputStreamBufferOutput(b3)) intSeq3 foreach packer.packInt packer.close verifyIntSeq(intSeq3, b3.toByteArray) @@ -87,43 +84,41 @@ class MessagePackerTest extends AirSpec with Benchmark { test("improve the performance via reset method") { val N = 1000 - val t = time("packer", repeat = 10) { - block("no-buffer-reset") { - val out = new ByteArrayOutputStream - withResource(MessagePack.newDefaultPacker(out)) { packer => - for (i <- 0 until N) { - val outputStream = new ByteArrayOutputStream() - packer - .reset(new OutputStreamBufferOutput(outputStream)) - packer.packInt(0) - packer.flush() + val t = + time("packer", repeat = 10) { + block("no-buffer-reset") { + val out = new ByteArrayOutputStream + withResource(MessagePack.newDefaultPacker(out)) { packer => + for i <- 0 until N do + val outputStream = new ByteArrayOutputStream() + packer.reset(new OutputStreamBufferOutput(outputStream)) + packer.packInt(0) + packer.flush() } } - } - block("buffer-reset") { - val out = new ByteArrayOutputStream - withResource(MessagePack.newDefaultPacker(out)) { packer => - val bufferOut = - new OutputStreamBufferOutput(new ByteArrayOutputStream()) - for (i <- 0 until N) { - val outputStream = new ByteArrayOutputStream() - bufferOut.reset(outputStream) - packer.reset(bufferOut) - packer.packInt(0) - packer.flush() + block("buffer-reset") { + val out = new ByteArrayOutputStream + withResource(MessagePack.newDefaultPacker(out)) { packer => + val bufferOut = new OutputStreamBufferOutput(new ByteArrayOutputStream()) + for i <- 0 until N do + val outputStream = new ByteArrayOutputStream() + bufferOut.reset(outputStream) + packer.reset(bufferOut) + packer.packInt(0) + packer.flush() } } } - } - t("buffer-reset").averageWithoutMinMax <= t("no-buffer-reset").averageWithoutMinMax shouldBe true + t("buffer-reset").averageWithoutMinMax <= t("no-buffer-reset").averageWithoutMinMax shouldBe + true } test("pack larger string array than byte buf") { // Based on https://github.com/msgpack/msgpack-java/issues/154 - def test(bufferSize: Int, stringSize: Int): Boolean = { + def test(bufferSize: Int, stringSize: Int): Boolean = val str = "a" * stringSize val rawString = ValueFactory.newString(str.getBytes("UTF-8")) val array = ValueFactory.newArray(rawString) @@ -133,16 +128,10 @@ class MessagePackerTest extends AirSpec with Benchmark { packer.close() out.toByteArray true - } - val testCases = Seq( - 32 -> 30, - 33 -> 31, - 32 -> 31, - 34 -> 32 - ) - testCases.foreach { - case (bufferSize, stringSize) => test(bufferSize, stringSize) + val testCases = Seq(32 -> 30, 33 -> 31, 32 -> 31, 34 -> 32) + testCases.foreach { case (bufferSize, stringSize) => + test(bufferSize, stringSize) } } @@ -152,24 +141,20 @@ class MessagePackerTest extends AirSpec with Benchmark { packer.packInt(99) packer.close - val up0 = MessagePack - .newDefaultUnpacker(new FileInputStream(f0)) + val up0 = MessagePack.newDefaultUnpacker(new FileInputStream(f0)) up0.unpackInt shouldBe 99 up0.hasNext shouldBe false up0.close val (f1, out1) = createTempFileWithOutputStream - packer - .reset(new OutputStreamBufferOutput(out1)) + packer.reset(new OutputStreamBufferOutput(out1)) packer.packInt(99) packer.flush - packer - .reset(new OutputStreamBufferOutput(out1)) + packer.reset(new OutputStreamBufferOutput(out1)) packer.packString("hello") packer.close - val up1 = MessagePack - .newDefaultUnpacker(new FileInputStream(f1)) + val up1 = MessagePack.newDefaultUnpacker(new FileInputStream(f1)) up1.unpackInt shouldBe 99 up1.unpackString shouldBe "hello" up1.hasNext shouldBe false @@ -182,24 +167,20 @@ class MessagePackerTest extends AirSpec with Benchmark { packer.packInt(99) packer.close - val up0 = MessagePack - .newDefaultUnpacker(new FileInputStream(f0)) + val up0 = MessagePack.newDefaultUnpacker(new FileInputStream(f0)) up0.unpackInt shouldBe 99 up0.hasNext shouldBe false up0.close val (f1, out1) = createTempFileWithChannel - packer - .reset(new ChannelBufferOutput(out1)) + packer.reset(new ChannelBufferOutput(out1)) packer.packInt(99) packer.flush - packer - .reset(new ChannelBufferOutput(out1)) + packer.reset(new ChannelBufferOutput(out1)) packer.packString("hello") packer.close - val up1 = MessagePack - .newDefaultUnpacker(new FileInputStream(f1)) + val up1 = MessagePack.newDefaultUnpacker(new FileInputStream(f1)) up1.unpackInt shouldBe 99 up1.unpackString shouldBe "hello" up1.hasNext shouldBe false @@ -209,32 +190,32 @@ class MessagePackerTest extends AirSpec with Benchmark { test("pack a lot of String within expected time") { val count = 20000 - def measureDuration(outputStream: java.io.OutputStream) = { + def measureDuration(outputStream: java.io.OutputStream) = val packer = MessagePack.newDefaultPacker(outputStream) var i = 0 - while (i < count) { + while i < count do packer.packString("0123456789ABCDEF") i += 1 - } packer.close - } - val t = time("packString into OutputStream", repeat = 10) { - block("byte-array-output-stream") { - measureDuration(new ByteArrayOutputStream()) - } + val t = + time("packString into OutputStream", repeat = 10) { + block("byte-array-output-stream") { + measureDuration(new ByteArrayOutputStream()) + } - block("file-output-stream") { - val (_, fileOutput) = createTempFileWithOutputStream - measureDuration(fileOutput) + block("file-output-stream") { + val (_, fileOutput) = createTempFileWithOutputStream + measureDuration(fileOutput) + } } - } - t("file-output-stream").averageWithoutMinMax < (t("byte-array-output-stream").averageWithoutMinMax * 5) shouldBe true + t("file-output-stream").averageWithoutMinMax < + (t("byte-array-output-stream").averageWithoutMinMax * 5) shouldBe true } } test("compute totalWrittenBytes") { - val out = new ByteArrayOutputStream + val out = new ByteArrayOutputStream val packerTotalWrittenBytes = withResource(MessagePack.newDefaultPacker(out)) { packer => packer @@ -255,11 +236,7 @@ class MessagePackerTest extends AirSpec with Benchmark { test("support read-only buffer") { val payload = Array[Byte](1) val out = new ByteArrayOutputStream() - val packer = MessagePack - .newDefaultPacker(out) - .packBinaryHeader(1) - .writePayload(payload) - .close() + val packer = MessagePack.newDefaultPacker(out).packBinaryHeader(1).writePayload(payload).close() } test("pack small string with STR8") { @@ -273,8 +250,7 @@ class MessagePackerTest extends AirSpec with Benchmark { } test("be able to disable STR8 for backward compatibility") { - val config = new PackerConfig() - .withStr8FormatSupport(false) + val config = new PackerConfig().withStr8FormatSupport(false) val packer = config.newBufferPacker() packer.packString("Hello. This is a string longer than 32 characters!") @@ -299,16 +275,14 @@ class MessagePackerTest extends AirSpec with Benchmark { test("write raw binary") { val packer = new MessagePack.PackerConfig().newBufferPacker() - val msg = - Array[Byte](-127, -92, 116, 121, 112, 101, -92, 112, 105, 110, 103) + val msg = Array[Byte](-127, -92, 116, 121, 112, 101, -92, 112, 105, 110, 103) packer.writePayload(msg) } test("append raw binary") { val packer = new MessagePack.PackerConfig().newBufferPacker() - val msg = - Array[Byte](-127, -92, 116, 121, 112, 101, -92, 112, 105, 110, 103) + val msg = Array[Byte](-127, -92, 116, 121, 112, 101, -92, 112, 105, 110, 103) packer.addPayload(msg) } -} +end MessagePackerTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala index 3ea5e911..bf43591a 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/MessageUnpackerTest.scala @@ -15,42 +15,39 @@ // package org.msgpack.core -import org.msgpack.core.MessagePackSpec.{createMessagePackData, toHex} -import org.msgpack.core.buffer._ +import org.msgpack.core.MessagePackSpec.createMessagePackData +import org.msgpack.core.MessagePackSpec.toHex +import org.msgpack.core.buffer.* import org.msgpack.value.ValueType import wvlet.airspec.AirSpec import wvlet.log.LogSupport import wvlet.log.io.IOUtil.withResource -import java.io._ +import java.io.* import java.nio.ByteBuffer import java.util.Collections -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* import scala.util.Random -object MessageUnpackerTest { - class SplitMessageBufferInput(array: Array[Array[Byte]]) extends MessageBufferInput { - var cursor = 0 - override def next(): MessageBuffer = { - if (cursor < array.length) { +object MessageUnpackerTest: + class SplitMessageBufferInput(array: Array[Array[Byte]]) extends MessageBufferInput: + var cursor = 0 + override def next(): MessageBuffer = + if cursor < array.length then val a = array(cursor) cursor += 1 MessageBuffer.wrap(a) - } else { + else null - } - } override def close(): Unit = {} - } -} -import org.msgpack.core.MessageUnpackerTest._ +import org.msgpack.core.MessageUnpackerTest.* -class MessageUnpackerTest extends AirSpec with Benchmark { +class MessageUnpackerTest extends AirSpec with Benchmark: - private val universal = MessageBuffer.allocate(0).isInstanceOf[MessageBufferU] - private def testData: Array[Byte] = { + private val universal = MessageBuffer.allocate(0).isInstanceOf[MessageBufferU] + private def testData: Array[Byte] = val out = new ByteArrayOutputStream() val packer = MessagePack.newDefaultPacker(out) @@ -68,17 +65,18 @@ class MessageUnpackerTest extends AirSpec with Benchmark { debug(s"packed: ${toHex(arr)}, size:${arr.length}") arr - } - private val intSeq = (for (i <- 0 until 100) yield Random.nextInt()).toArray[Int] + private val intSeq = + ( + for i <- 0 until 100 + yield Random.nextInt() + ).toArray[Int] - private def testData2: Array[Byte] = { + private def testData2: Array[Byte] = val out = new ByteArrayOutputStream() val packer = MessagePack.newDefaultPacker(out); - packer - .packBoolean(true) - .packBoolean(false) + packer.packBoolean(true).packBoolean(false) intSeq.foreach(packer.packInt) packer.close() @@ -86,15 +84,15 @@ class MessageUnpackerTest extends AirSpec with Benchmark { val arr = out.toByteArray debug(s"packed: ${toHex(arr)}") arr - } - private def write(packer: MessagePacker, r: Random): Unit = { - val tpeIndex = Iterator - .continually(r.nextInt(MessageFormat.values().length)) - .find(_ != MessageFormat.NEVER_USED.ordinal()) - .get + private def write(packer: MessagePacker, r: Random): Unit = + val tpeIndex = + Iterator + .continually(r.nextInt(MessageFormat.values().length)) + .find(_ != MessageFormat.NEVER_USED.ordinal()) + .get val tpe = MessageFormat.values()(tpeIndex) - tpe.getValueType match { + tpe.getValueType match case ValueType.INTEGER => val v = r.nextInt(Int.MaxValue) @@ -124,27 +122,27 @@ class MessageUnpackerTest extends AirSpec with Benchmark { trace(s"array len: $len") packer.packArrayHeader(len) var i = 0 - while (i < len) { + while i < len do write(packer, r) i += 1 - } case ValueType.MAP => val len = r.nextInt(5) + 1 packer.packMapHeader(len) trace(s"map len: ${len}") var i = 0 - while (i < len * 2) { + while i < len * 2 do write(packer, r) i += 1 - } case _ => val v = r.nextInt(Int.MaxValue) trace(s"int: $v") packer.packInt(v) - } - } - private def testData3(N: Int): Array[Byte] = { + end match + + end write + + private def testData3(N: Int): Array[Byte] = val out = new ByteArrayOutputStream() val packer = MessagePack.newDefaultPacker(out) @@ -160,11 +158,10 @@ class MessageUnpackerTest extends AirSpec with Benchmark { trace(s"packed: ${toHex(arr)}") debug(s"size:${arr.length}") arr - } - private def readValue(unpacker: MessageUnpacker): Unit = { + private def readValue(unpacker: MessageUnpacker): Unit = val f = unpacker.getNextFormat() - f.getValueType match { + f.getValueType match case ValueType.ARRAY => val arrLen = unpacker.unpackArrayHeader() debug(s"arr size: $arrLen") @@ -180,24 +177,20 @@ class MessageUnpackerTest extends AirSpec with Benchmark { case other => unpacker.skipValue() debug(s"unknown type: $f") - } - } - private def createTempFile: File = { + private def createTempFile: File = val f = File.createTempFile("msgpackTest", "msgpack") f.deleteOnExit val p = MessagePack.newDefaultPacker(new FileOutputStream(f)) p.packInt(99) p.close f - } - private def checkFile(u: MessageUnpacker): Boolean = { + private def checkFile(u: MessageUnpacker): Boolean = u.unpackInt shouldBe 99 u.hasNext shouldBe false - } - private def unpackers(data: Array[Byte]): Seq[MessageUnpacker] = { + private def unpackers(data: Array[Byte]): Seq[MessageUnpacker] = val bb = ByteBuffer.allocate(data.length) val db = ByteBuffer.allocateDirect(data.length) bb.put(data).flip() @@ -205,20 +198,21 @@ class MessageUnpackerTest extends AirSpec with Benchmark { val builder = Seq.newBuilder[MessageUnpacker] builder += MessagePack.newDefaultUnpacker(data) builder += MessagePack.newDefaultUnpacker(bb) - if (!universal) { + if !universal then builder += MessagePack.newDefaultUnpacker(db) - } builder.result() - } - private def unpackerCollectionWithVariousBuffers(data: Array[Byte], chunkSize: Int): Seq[MessageUnpacker] = { + private def unpackerCollectionWithVariousBuffers( + data: Array[Byte], + chunkSize: Int + ): Seq[MessageUnpacker] = val seqBytes = Seq.newBuilder[MessageBufferInput] val seqByteBuffers = Seq.newBuilder[MessageBufferInput] val seqDirectBuffers = Seq.newBuilder[MessageBufferInput] var left = data.length var position = 0 - while (left > 0) { + while left > 0 do val length = Math.min(chunkSize, left) seqBytes += new ArrayBufferInput(data, position, length) val bb = ByteBuffer.allocate(length) @@ -229,52 +223,56 @@ class MessageUnpackerTest extends AirSpec with Benchmark { seqDirectBuffers += new ByteBufferInput(db) left -= length position += length - } val builder = Seq.newBuilder[MessageUnpacker] - builder += MessagePack.newDefaultUnpacker(new SequenceMessageBufferInput(Collections.enumeration(seqBytes.result().asJava))) - builder += MessagePack.newDefaultUnpacker(new SequenceMessageBufferInput(Collections.enumeration(seqByteBuffers.result().asJava))) - if (!universal) { - builder += MessagePack.newDefaultUnpacker(new SequenceMessageBufferInput(Collections.enumeration(seqDirectBuffers.result().asJava))) - } + builder += + MessagePack.newDefaultUnpacker( + new SequenceMessageBufferInput(Collections.enumeration(seqBytes.result().asJava)) + ) + builder += + MessagePack.newDefaultUnpacker( + new SequenceMessageBufferInput(Collections.enumeration(seqByteBuffers.result().asJava)) + ) + if !universal then + builder += + MessagePack.newDefaultUnpacker( + new SequenceMessageBufferInput(Collections.enumeration(seqDirectBuffers.result().asJava)) + ) builder.result() - } + + end unpackerCollectionWithVariousBuffers test("MessageUnpacker") { test("parse message packed data") { val arr = testData - for (unpacker <- unpackers(arr)) { + for unpacker <- unpackers(arr) do var count = 0 - while (unpacker.hasNext) { + while unpacker.hasNext do count += 1 readValue(unpacker) - } count shouldBe 6 unpacker.getTotalReadBytes shouldBe arr.length unpacker.close() unpacker.getTotalReadBytes shouldBe arr.length - } } test("skip reading values") { - for (unpacker <- unpackers(testData)) { + for unpacker <- unpackers(testData) do var skipCount = 0 - while (unpacker.hasNext) { + while unpacker.hasNext do unpacker.skipValue() skipCount += 1 - } skipCount shouldBe 2 unpacker.getTotalReadBytes shouldBe testData.length unpacker.close() unpacker.getTotalReadBytes shouldBe testData.length - } } test("compare skip performance") { @@ -283,23 +281,20 @@ class MessageUnpackerTest extends AirSpec with Benchmark { time("skip performance", repeat = 100) { block("switch") { - for (unpacker <- unpackers(data)) { + for unpacker <- unpackers(data) do var skipCount = 0 - while (unpacker.hasNext) { + while unpacker.hasNext do unpacker.skipValue() skipCount += 1 - } skipCount shouldBe N - } } } time("bulk skip performance", repeat = 100) { block("switch") { - for (unpacker <- unpackers(data)) { + for unpacker <- unpackers(data) do unpacker.skipValue(N) unpacker.hasNext shouldBe false - } } } @@ -308,12 +303,12 @@ class MessageUnpackerTest extends AirSpec with Benchmark { test("parse int data") { debug(intSeq.mkString(", ")) - for (unpacker <- unpackers(testData2)) { + for unpacker <- unpackers(testData2) do val ib = Seq.newBuilder[Int] - while (unpacker.hasNext) { + while unpacker.hasNext do val f = unpacker.getNextFormat - f.getValueType match { + f.getValueType match case ValueType.INTEGER => val i = unpacker.unpackInt() trace(f"read int: $i%,d") @@ -323,54 +318,48 @@ class MessageUnpackerTest extends AirSpec with Benchmark { trace(s"read boolean: $b") case other => unpacker.skipValue() - } - } ib.result() shouldBe intSeq.toSeq unpacker.getTotalReadBytes shouldBe testData2.length unpacker.close() unpacker.getTotalReadBytes shouldBe testData2.length - } } test("read data at the buffer boundary") { - trait SplitTest extends LogSupport { + trait SplitTest extends LogSupport: val data: Array[Byte] - def run: Unit = { - for (unpacker <- unpackers(data)) { - val numElems = { + def run: Unit = + for unpacker <- unpackers(data) do + val numElems = var c = 0 - while (unpacker.hasNext) { + while unpacker.hasNext do readValue(unpacker) c += 1 - } c - } - for (splitPoint <- 1 until data.length - 1) { + for splitPoint <- 1 until data.length - 1 do debug(s"split at $splitPoint") val (h, t) = data.splitAt(splitPoint) val bin = new SplitMessageBufferInput(Array(h, t)) val unpacker = MessagePack.newDefaultUnpacker(bin) var count = 0 - while (unpacker.hasNext) { + while unpacker.hasNext do count += 1 val f = unpacker.getNextFormat readValue(unpacker) - } count shouldBe numElems unpacker.getTotalReadBytes shouldBe data.length unpacker.close() unpacker.getTotalReadBytes shouldBe data.length - } - } - } - } - new SplitTest { val data = testData }.run - new SplitTest { val data = testData3(30) }.run + new SplitTest: + val data = testData + .run + new SplitTest: + val data = testData3(30) + .run } test("read integer at MessageBuffer boundaries") { @@ -382,18 +371,21 @@ class MessageUnpackerTest extends AirSpec with Benchmark { val data = packer.toByteArray // Boundary test - withResource(MessagePack.newDefaultUnpacker(new InputStreamBufferInput(new ByteArrayInputStream(data), 8192))) { unpacker => + withResource( + MessagePack.newDefaultUnpacker( + new InputStreamBufferInput(new ByteArrayInputStream(data), 8192) + ) + ) { unpacker => (0 until 1170).foreach { i => unpacker.unpackLong() shouldBe 0x0011223344556677L } } // Boundary test for sequences of ByteBuffer, DirectByteBuffer backed MessageInput. - for (unpacker <- unpackerCollectionWithVariousBuffers(data, 32)) { + for unpacker <- unpackerCollectionWithVariousBuffers(data, 32) do (0 until 1170).foreach { i => unpacker.unpackLong() shouldBe 0x0011223344556677L } - } } test("read string at MessageBuffer boundaries") { @@ -405,36 +397,34 @@ class MessageUnpackerTest extends AirSpec with Benchmark { val data = packer.toByteArray // Boundary test - withResource(MessagePack.newDefaultUnpacker(new InputStreamBufferInput(new ByteArrayInputStream(data), 8192))) { unpacker => + withResource( + MessagePack.newDefaultUnpacker( + new InputStreamBufferInput(new ByteArrayInputStream(data), 8192) + ) + ) { unpacker => (0 until 1170).foreach { i => unpacker.unpackString() shouldBe "hello world" } } // Boundary test for sequences of ByteBuffer, DirectByteBuffer backed MessageInput. - for (unpacker <- unpackerCollectionWithVariousBuffers(data, 32)) { + for unpacker <- unpackerCollectionWithVariousBuffers(data, 32) do (0 until 1170).foreach { i => unpacker.unpackString() shouldBe "hello world" } - } } test("be faster than msgpack-v6 skip") { - trait Fixture { + trait Fixture: val unpacker: MessageUnpacker - def run: Unit = { + def run: Unit = var count = 0 - try { - while (unpacker.hasNext) { + try + while unpacker.hasNext do unpacker.skipValue() count += 1 - } - } finally { - unpacker.close() - } - } - } + finally unpacker.close() val data = testData3(10000) val N = 100 @@ -443,64 +433,69 @@ class MessageUnpackerTest extends AirSpec with Benchmark { val db = ByteBuffer.allocateDirect(data.length) db.put(data).flip() - val t = time("skip performance", repeat = N) { - block("v6") { - val v6 = new org.msgpack.MessagePack() - val unpacker = new org.msgpack.unpacker.MessagePackUnpacker(v6, new ByteArrayInputStream(data)) - var count = 0 - try { - while (true) { - unpacker.skip() - count += 1 - } - } catch { - case e: EOFException => - } finally unpacker.close() - } + val t = + time("skip performance", repeat = N) { + block("v6") { + val v6 = new org.msgpack.MessagePack() + val unpacker = + new org.msgpack.unpacker.MessagePackUnpacker(v6, new ByteArrayInputStream(data)) + var count = 0 + try + while true do + unpacker.skip() + count += 1 + catch + case e: EOFException => + finally + unpacker.close() + } - block("v7-array") { - new Fixture { - override val unpacker = MessagePack.newDefaultUnpacker(data) - }.run - } + block("v7-array") { + new Fixture: + override val unpacker = MessagePack.newDefaultUnpacker(data) + .run + } - block("v7-array-buffer") { - new Fixture { - override val unpacker = MessagePack.newDefaultUnpacker(bb) - }.run - } - if (!universal) block("v7-direct-buffer") { - new Fixture { - override val unpacker = MessagePack.newDefaultUnpacker(db) - }.run + block("v7-array-buffer") { + new Fixture: + override val unpacker = MessagePack.newDefaultUnpacker(bb) + .run + } + if !universal then + block("v7-direct-buffer") { + new Fixture: + override val unpacker = MessagePack.newDefaultUnpacker(db) + .run + } } - } t("v7-array").averageWithoutMinMax <= t("v6").averageWithoutMinMax shouldBe true t("v7-array-buffer").averageWithoutMinMax <= t("v6").averageWithoutMinMax shouldBe true - if (!universal) { + if !universal then t("v7-direct-buffer").averageWithoutMinMax <= t("v6").averageWithoutMinMax shouldBe true - } } - import org.msgpack.`type`.{ValueType => ValueTypeV6} + import org.msgpack.`type`.ValueType as ValueTypeV6 test("be faster than msgpack-v6 read value") { - def readValueV6(unpacker: org.msgpack.unpacker.MessagePackUnpacker): Unit = { + def readValueV6(unpacker: org.msgpack.unpacker.MessagePackUnpacker): Unit = val vt = unpacker.getNextType() - vt match { + vt match case ValueTypeV6.ARRAY => val len = unpacker.readArrayBegin() var i = 0 - while (i < len) { readValueV6(unpacker); i += 1 } + while i < len do + readValueV6(unpacker); + i += 1 unpacker.readArrayEnd() case ValueTypeV6.MAP => val len = unpacker.readMapBegin() var i = 0 - while (i < len) { - readValueV6(unpacker); readValueV6(unpacker); i += 1 - } + while i < len do + readValueV6(unpacker); + readValueV6(unpacker); + i += 1 unpacker.readMapEnd() case ValueTypeV6.NIL => unpacker.readNil() @@ -514,23 +509,27 @@ class MessageUnpackerTest extends AirSpec with Benchmark { unpacker.readByteArray() case _ => unpacker.skip() - } - } + end readValueV6 val buf = new Array[Byte](8192) - def readValue(unpacker: MessageUnpacker): Unit = { + def readValue(unpacker: MessageUnpacker): Unit = val f = unpacker.getNextFormat val vt = f.getValueType - vt match { + vt match case ValueType.ARRAY => val len = unpacker.unpackArrayHeader() var i = 0 - while (i < len) { readValue(unpacker); i += 1 } + while i < len do + readValue(unpacker); + i += 1 case ValueType.MAP => val len = unpacker.unpackMapHeader() var i = 0 - while (i < len) { readValue(unpacker); readValue(unpacker); i += 1 } + while i < len do + readValue(unpacker); + readValue(unpacker); + i += 1 case ValueType.NIL => unpacker.unpackNil() case ValueType.INTEGER => @@ -547,20 +546,17 @@ class MessageUnpackerTest extends AirSpec with Benchmark { unpacker.readPayload(buf, 0, len) case _ => unpacker.skipValue() - } - } - trait Fixture { + end match + end readValue + trait Fixture: val unpacker: MessageUnpacker - def run: Unit = { + def run: Unit = var count = 0 - try { - while (unpacker.hasNext) { + try + while unpacker.hasNext do readValue(unpacker) count += 1 - } - } finally unpacker.close() - } - } + finally unpacker.close() val data = testData3(10000) val N = 100 @@ -569,49 +565,55 @@ class MessageUnpackerTest extends AirSpec with Benchmark { val db = ByteBuffer.allocateDirect(data.length) db.put(data).flip() - val t = time("unpack performance", repeat = N) { - block("v6") { - val v6 = new org.msgpack.MessagePack() - val unpacker = new org.msgpack.unpacker.MessagePackUnpacker(v6, new ByteArrayInputStream(data)) - var count = 0 - try { - while (true) { - readValueV6(unpacker) - count += 1 - } - } catch { - case e: EOFException => - } finally unpacker.close() - } + val t = + time("unpack performance", repeat = N) { + block("v6") { + val v6 = new org.msgpack.MessagePack() + val unpacker = + new org.msgpack.unpacker.MessagePackUnpacker(v6, new ByteArrayInputStream(data)) + var count = 0 + try + while true do + readValueV6(unpacker) + count += 1 + catch + case e: EOFException => + finally + unpacker.close() + } - block("v7-array") { - new Fixture { - override val unpacker = MessagePack.newDefaultUnpacker(data) - }.run - } + block("v7-array") { + new Fixture: + override val unpacker = MessagePack.newDefaultUnpacker(data) + .run + } - block("v7-array-buffer") { - new Fixture { - override val unpacker = MessagePack.newDefaultUnpacker(bb) - }.run - } + block("v7-array-buffer") { + new Fixture: + override val unpacker = MessagePack.newDefaultUnpacker(bb) + .run + } - if (!universal) block("v7-direct-buffer") { - new Fixture { - override val unpacker = MessagePack.newDefaultUnpacker(db) - }.run + if !universal then + block("v7-direct-buffer") { + new Fixture: + override val unpacker = MessagePack.newDefaultUnpacker(db) + .run + } } - } - if (t("v7-array").averageWithoutMinMax > t("v6").averageWithoutMinMax) { - warn(s"v7-array ${t("v7-array").averageWithoutMinMax} is slower than v6 ${t("v6").averageWithoutMinMax}") - } - if (t("v7-array-buffer").averageWithoutMinMax > t("v6").averageWithoutMinMax) { - warn(s"v7-array-buffer ${t("v7-array-buffer").averageWithoutMinMax} is slower than v6 ${t("v6").averageWithoutMinMax}") - } - if (!universal) { + if t("v7-array").averageWithoutMinMax > t("v6").averageWithoutMinMax then + warn( + s"v7-array ${t("v7-array").averageWithoutMinMax} is slower than v6 ${t("v6") + .averageWithoutMinMax}" + ) + if t("v7-array-buffer").averageWithoutMinMax > t("v6").averageWithoutMinMax then + warn( + s"v7-array-buffer ${t("v7-array-buffer").averageWithoutMinMax} is slower than v6 ${t("v6") + .averageWithoutMinMax}" + ) + if !universal then t("v7-direct-buffer").averageWithoutMinMax <= t("v6").averageWithoutMinMax shouldBe true - } } test("be faster for reading binary than v6") { @@ -626,31 +628,26 @@ class MessageUnpackerTest extends AirSpec with Benchmark { } packer.close() - trait Fixture { + trait Fixture: val unpacker: MessageUnpacker val loop: Int - def run: Unit = { + def run: Unit = var i = 0 - try { - while (i < loop) { + try + while i < loop do val len = unpacker.unpackBinaryHeader() val out = new Array[Byte](len) unpacker.readPayload(out, 0, len) i += 1 - } - } finally unpacker.close() - } - def runRef: Unit = { + finally unpacker.close() + def runRef: Unit = var i = 0 - try { - while (i < loop) { + try + while i < loop do val len = unpacker.unpackBinaryHeader() val out = unpacker.readPayloadAsReference(len) i += 1 - } - } finally unpacker.close() - } - } + finally unpacker.close() val b = bos.toByteArray val bb = ByteBuffer.allocate(b.length) bb.put(b).flip() @@ -660,65 +657,66 @@ class MessageUnpackerTest extends AirSpec with Benchmark { time("unpackBinary", repeat = 100) { block("v6") { val v6 = new org.msgpack.MessagePack() - val unpacker = new org.msgpack.unpacker.MessagePackUnpacker(v6, new ByteArrayInputStream(b)) - var i = 0 - while (i < R) { + val unpacker = + new org.msgpack.unpacker.MessagePackUnpacker(v6, new ByteArrayInputStream(b)) + var i = 0 + while i < R do val out = unpacker.readByteArray() i += 1 - } unpacker.close() } block("v7-array") { - new Fixture { + new Fixture: override val unpacker = MessagePack.newDefaultUnpacker(b) override val loop = R - }.run + .run } block("v7-array-buffer") { - new Fixture { + new Fixture: override val unpacker = MessagePack.newDefaultUnpacker(bb) override val loop = R - }.run + .run } - if (!universal) block("v7-direct-buffer") { - new Fixture { - override val unpacker = MessagePack.newDefaultUnpacker(db) - override val loop = R - }.run - } + if !universal then + block("v7-direct-buffer") { + new Fixture: + override val unpacker = MessagePack.newDefaultUnpacker(db) + override val loop = R + .run + } block("v7-ref-array") { - new Fixture { + new Fixture: override val unpacker = MessagePack.newDefaultUnpacker(b) override val loop = R - }.runRef + .runRef } block("v7-ref-array-buffer") { - new Fixture { + new Fixture: override val unpacker = MessagePack.newDefaultUnpacker(bb) override val loop = R - }.runRef + .runRef } - if (!universal) block("v7-ref-direct-buffer") { - new Fixture { - override val unpacker = MessagePack.newDefaultUnpacker(db) - override val loop = R - }.runRef - } + if !universal then + block("v7-ref-direct-buffer") { + new Fixture: + override val unpacker = MessagePack.newDefaultUnpacker(db) + override val loop = R + .runRef + } } } test("read payload as a reference") { - val dataSizes = - Seq(0, 1, 5, 8, 16, 32, 128, 256, 1024, 2000, 10000, 100000) + val dataSizes = Seq(0, 1, 5, 8, 16, 32, 128, 256, 1024, 2000, 10000, 100000) - for (s <- dataSizes) { + for s <- dataSizes do test(f"data size is $s%,d") { val data = new Array[Byte](s) Random.nextBytes(data) @@ -728,7 +726,7 @@ class MessageUnpackerTest extends AirSpec with Benchmark { packer.writePayload(data) packer.close() - for (unpacker <- unpackers(b.toByteArray)) { + for unpacker <- unpackers(b.toByteArray) do val len = unpacker.unpackBinaryHeader() len shouldBe s val ref = unpacker.readPayloadAsReference(len) @@ -738,21 +736,18 @@ class MessageUnpackerTest extends AirSpec with Benchmark { ref.getBytes(0, stored, 0, len) stored shouldBe data - } } - } } test("reset the internal states") { val data = intSeq val b = createMessagePackData(packer => data foreach packer.packInt) - for (unpacker <- unpackers(b)) { + for unpacker <- unpackers(b) do val unpacked = Array.newBuilder[Int] - while (unpacker.hasNext) { + while unpacker.hasNext do unpacked += unpacker.unpackInt() - } unpacker.close unpacked.result() shouldBe data @@ -761,9 +756,8 @@ class MessageUnpackerTest extends AirSpec with Benchmark { val bi = new ArrayBufferInput(b2) unpacker.reset(bi) val unpacked2 = Array.newBuilder[Int] - while (unpacker.hasNext) { + while unpacker.hasNext do unpacked2 += unpacker.unpackInt() - } unpacker.close unpacked2.result() shouldBe data2 @@ -771,12 +765,10 @@ class MessageUnpackerTest extends AirSpec with Benchmark { bi.reset(b2) unpacker.reset(bi) val unpacked3 = Array.newBuilder[Int] - while (unpacker.hasNext) { + while unpacker.hasNext do unpacked3 += unpacker.unpackInt() - } unpacker.close unpacked3.result() shouldBe data2 - } } @@ -790,42 +782,40 @@ class MessageUnpackerTest extends AirSpec with Benchmark { val mb = MessageBuffer.wrap(arr) val N = 1000 - val t = time("unpacker", repeat = 10) { - block("no-buffer-reset") { - withResource(MessagePack.newDefaultUnpacker(arr)) { unpacker => - for (i <- 0 until N) { - val buf = new ArrayBufferInput(arr) - unpacker.reset(buf) - unpacker.unpackInt - unpacker.close + val t = + time("unpacker", repeat = 10) { + block("no-buffer-reset") { + withResource(MessagePack.newDefaultUnpacker(arr)) { unpacker => + for i <- 0 until N do + val buf = new ArrayBufferInput(arr) + unpacker.reset(buf) + unpacker.unpackInt + unpacker.close } } - } - block("reuse-array-input") { - withResource(MessagePack.newDefaultUnpacker(arr)) { unpacker => - val buf = new ArrayBufferInput(arr) - for (i <- 0 until N) { - buf.reset(arr) - unpacker.reset(buf) - unpacker.unpackInt - unpacker.close + block("reuse-array-input") { + withResource(MessagePack.newDefaultUnpacker(arr)) { unpacker => + val buf = new ArrayBufferInput(arr) + for i <- 0 until N do + buf.reset(arr) + unpacker.reset(buf) + unpacker.unpackInt + unpacker.close } } - } - block("reuse-message-buffer") { - withResource(MessagePack.newDefaultUnpacker(arr)) { unpacker => - val buf = new ArrayBufferInput(arr) - for (i <- 0 until N) { - buf.reset(mb) - unpacker.reset(buf) - unpacker.unpackInt - unpacker.close + block("reuse-message-buffer") { + withResource(MessagePack.newDefaultUnpacker(arr)) { unpacker => + val buf = new ArrayBufferInput(arr) + for i <- 0 until N do + buf.reset(mb) + unpacker.reset(buf) + unpacker.unpackInt + unpacker.close } } } - } // This performance comparison is too close, so we disabled it // t("reuse-message-buffer").averageWithoutMinMax should be <= t("no-buffer-reset").averageWithoutMinMax @@ -857,24 +847,20 @@ class MessageUnpackerTest extends AirSpec with Benchmark { } test("unpack large string data") { - def createLargeData(stringLength: Int): Array[Byte] = { + def createLargeData(stringLength: Int): Array[Byte] = val out = new ByteArrayOutputStream() val packer = MessagePack.newDefaultPacker(out) - packer - .packArrayHeader(2) - .packString("l" * stringLength) - .packInt(1) + packer.packArrayHeader(2).packString("l" * stringLength).packInt(1) packer.close() out.toByteArray - } Seq(8191, 8192, 8193, 16383, 16384, 16385).foreach { n => val arr = createLargeData(n) - for (unpacker <- unpackers(arr)) { + for unpacker <- unpackers(arr) do unpacker.unpackArrayHeader shouldBe 2 unpacker.unpackString.length shouldBe n @@ -884,12 +870,11 @@ class MessageUnpackerTest extends AirSpec with Benchmark { unpacker.close() unpacker.getTotalReadBytes shouldBe arr.length - } } } test("unpack string crossing end of buffer") { - def check(expected: String, strLen: Int) = { + def check(expected: String, strLen: Int) = val bytes = new Array[Byte](strLen) val out = new ByteArrayOutputStream @@ -899,52 +884,58 @@ class MessageUnpackerTest extends AirSpec with Benchmark { packer.packString(expected) packer.close - val unpacker = MessagePack.newDefaultUnpacker(new InputStreamBufferInput(new ByteArrayInputStream(out.toByteArray))) - val len = unpacker.unpackBinaryHeader + val unpacker = MessagePack.newDefaultUnpacker( + new InputStreamBufferInput(new ByteArrayInputStream(out.toByteArray)) + ) + val len = unpacker.unpackBinaryHeader unpacker.readPayload(len) val got = unpacker.unpackString unpacker.close got shouldBe expected - } - Seq("\u3042", "a\u3042", "\u3042a", "\u3042\u3044\u3046\u3048\u304A\u304B\u304D\u304F\u3051\u3053\u3055\u3057\u3059\u305B\u305D") - .foreach { s => - Seq(8185, 8186, 8187, 8188, 16377, 16378, 16379, 16380).foreach { n => - check(s, n) - } + Seq( + "\u3042", + "a\u3042", + "\u3042a", + "\u3042\u3044\u3046\u3048\u304A\u304B\u304D\u304F\u3051\u3053\u3055\u3057\u3059\u305B\u305D" + ).foreach { s => + Seq(8185, 8186, 8187, 8188, 16377, 16378, 16379, 16380).foreach { n => + check(s, n) } + } } - def readTest(input: MessageBufferInput): Unit = { + def readTest(input: MessageBufferInput): Unit = withResource(MessagePack.newDefaultUnpacker(input)) { unpacker => - while (unpacker.hasNext) { + while unpacker.hasNext do unpacker.unpackValue() - } } - } test("read value length at buffer boundary") { - val input = new SplitMessageBufferInput( - Array( - Array[Byte](MessagePack.Code.STR16), - Array[Byte](0x00), - Array[Byte](0x05), // STR16 length at the boundary - "hello".getBytes(MessagePack.UTF8) + val input = + new SplitMessageBufferInput( + Array( + Array[Byte](MessagePack.Code.STR16), + Array[Byte](0x00), + Array[Byte](0x05), // STR16 length at the boundary + "hello".getBytes(MessagePack.UTF8) + ) ) - ) readTest(input) - val input2 = new SplitMessageBufferInput( - Array( - Array[Byte](MessagePack.Code.STR32), - Array[Byte](0x00), - Array[Byte](0x00, 0x00), - Array[Byte](0x05), // STR32 length at the boundary - "hello".getBytes(MessagePack.UTF8) + val input2 = + new SplitMessageBufferInput( + Array( + Array[Byte](MessagePack.Code.STR32), + Array[Byte](0x00), + Array[Byte](0x00, 0x00), + Array[Byte](0x05), // STR32 length at the boundary + "hello".getBytes(MessagePack.UTF8) + ) ) - ) readTest(input2) } } -} + +end MessageUnpackerTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/PayloadSizeLimitTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/PayloadSizeLimitTest.scala new file mode 100644 index 00000000..7f454c85 --- /dev/null +++ b/msgpack-core/src/test/scala/org/msgpack/core/PayloadSizeLimitTest.scala @@ -0,0 +1,116 @@ +package org.msgpack.core + +import wvlet.airspec.AirSpec + +import java.nio.ByteBuffer + +class PayloadSizeLimitTest extends AirSpec: + + test("detects malicious EXT32 file with huge declared size but tiny actual data") { + // Craft a malicious EXT32 header: + // 0xC9 = EXT32 format + // 4 bytes = declared length (100MB, which exceeds 64MB threshold) + // 1 byte = extension type + // followed by only 1 byte of actual data (not 100MB) + val declaredLength = 100 * 1024 * 1024 // 100 MB + val buffer = ByteBuffer.allocate(7) // header(1) + length(4) + type(1) + data(1) + buffer.put(0xc9.toByte) // EXT32 format code + buffer.putInt(declaredLength) // declared length (big-endian) + buffer.put(0x01.toByte) // extension type + buffer.put(0x41.toByte) // only 1 byte of actual data + val maliciousData = buffer.array() + + val unpacker = MessagePack.newDefaultUnpacker(maliciousData) + + // Should throw MessageSizeException because the declared size exceeds actual data + intercept[MessageSizeException] { + unpacker.unpackValue() + } + } + + test("detects malicious BIN32 file with huge declared size but tiny actual data") { + // Craft a malicious BIN32 header: + // 0xC6 = BIN32 format + // 4 bytes = declared length (100MB, which exceeds 64MB threshold) + // followed by only 1 byte of actual data (not 100MB) + val declaredLength = 100 * 1024 * 1024 // 100 MB + val buffer = ByteBuffer.allocate(6) // header(1) + length(4) + data(1) + buffer.put(0xc6.toByte) // BIN32 format code + buffer.putInt(declaredLength) // declared length (big-endian) + buffer.put(0x41.toByte) // only 1 byte of actual data + val maliciousData = buffer.array() + + val unpacker = MessagePack.newDefaultUnpacker(maliciousData) + + // Should throw MessageSizeException because the declared size exceeds actual data + intercept[MessageSizeException] { + unpacker.unpackValue() + } + } + + test("legitimate extension data works correctly") { + val packer = MessagePack.newDefaultBufferPacker() + val testData = Array.fill[Byte](1000)(0x42) + packer.packExtensionTypeHeader(0x01.toByte, testData.length) + packer.writePayload(testData) + val msgpack = packer.toByteArray + + val unpacker = MessagePack.newDefaultUnpacker(msgpack) + val value = unpacker.unpackValue() + + assert(value.isExtensionValue) + assert(value.asExtensionValue().getData.length == 1000) + } + + test("legitimate binary data works correctly") { + val packer = MessagePack.newDefaultBufferPacker() + val testData = Array.fill[Byte](1000)(0x42) + packer.packBinaryHeader(testData.length) + packer.writePayload(testData) + val msgpack = packer.toByteArray + + val unpacker = MessagePack.newDefaultUnpacker(msgpack) + val value = unpacker.unpackValue() + + assert(value.isBinaryValue) + assert(value.asBinaryValue().asByteArray().length == 1000) + } + + test("readPayload directly with malicious size throws exception") { + // Test readPayload(int) directly with a malicious input + val declaredLength = 100 * 1024 * 1024 // 100 MB + val buffer = ByteBuffer.allocate(6) // header(1) + length(4) + data(1) + buffer.put(0xc6.toByte) // BIN32 format code + buffer.putInt(declaredLength) // declared length (big-endian) + buffer.put(0x41.toByte) // only 1 byte of actual data + val maliciousData = buffer.array() + + val unpacker = MessagePack.newDefaultUnpacker(maliciousData) + + // First, unpack the binary header to get the declared length + val len = unpacker.unpackBinaryHeader() + assert(len == declaredLength) + + // Then try to read the payload - should throw exception + intercept[MessageSizeException] { + unpacker.readPayload(len) + } + } + + test("small payloads under threshold work with upfront allocation") { + // Payloads under 64MB should use the efficient upfront allocation path + val packer = MessagePack.newDefaultBufferPacker() + val testData = Array.fill[Byte](10000)(0x42) // 10KB, well under threshold + packer.packBinaryHeader(testData.length) + packer.writePayload(testData) + val msgpack = packer.toByteArray + + val unpacker = MessagePack.newDefaultUnpacker(msgpack) + val len = unpacker.unpackBinaryHeader() + val payload = unpacker.readPayload(len) + + assert(payload.length == 10000) + assert(payload.forall(_ == 0x42.toByte)) + } + +end PayloadSizeLimitTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/StringLimitTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/StringLimitTest.scala index 96319a7f..c54ce632 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/StringLimitTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/StringLimitTest.scala @@ -4,7 +4,7 @@ import org.msgpack.core.MessagePack.UnpackerConfig import org.msgpack.value.Variable import wvlet.airspec.AirSpec -class StringLimitTest extends AirSpec { +class StringLimitTest extends AirSpec: test("throws an exception when the string size exceeds a limit") { val customLimit = 100 @@ -34,4 +34,5 @@ class StringLimitTest extends AirSpec { } } } -} + +end StringLimitTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala index 42872fc4..06d363fd 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/ByteStringTest.scala @@ -20,28 +20,26 @@ import org.msgpack.core.MessagePack import org.msgpack.core.MessagePackSpec.createMessagePackData import wvlet.airspec.AirSpec -class ByteStringTest extends AirSpec { +class ByteStringTest extends AirSpec: private val unpackedString = "foo" private val byteString = ByteString(createMessagePackData(_.packString(unpackedString))) - private def unpackString(messageBuffer: MessageBuffer) = { - val input = new MessageBufferInput { + private def unpackString(messageBuffer: MessageBuffer) = + val input = + new MessageBufferInput: - private var isRead = false + private var isRead = false - override def next(): MessageBuffer = - if (isRead) { - null - } else { - isRead = true - messageBuffer - } - override def close(): Unit = {} - } + override def next(): MessageBuffer = + if isRead then + null + else + isRead = true + messageBuffer + override def close(): Unit = {} MessagePack.newDefaultUnpacker(input).unpackString() - } test("Unpacking a ByteString's ByteBuffer") { test("fail with a regular MessageBuffer") { @@ -53,4 +51,5 @@ class ByteStringTest extends AirSpec { } } } -} + +end ByteStringTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/DirectBufferAccessTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/DirectBufferAccessTest.scala index 40f4c770..5bb4b49d 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/DirectBufferAccessTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/DirectBufferAccessTest.scala @@ -19,11 +19,10 @@ import wvlet.airspec.AirSpec import java.nio.ByteBuffer -class DirectBufferAccessTest extends AirSpec { +class DirectBufferAccessTest extends AirSpec: test("instantiate DirectBufferAccess") { val bb = ByteBuffer.allocateDirect(1) val addr = DirectBufferAccess.getAddress(bb) } -} diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala index dd1cdb97..5d3c3d9f 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferInputTest.scala @@ -19,71 +19,62 @@ import org.msgpack.core.MessagePack import wvlet.airspec.AirSpec import wvlet.log.io.IOUtil.withResource -import java.io._ +import java.io.* import java.net.InetSocketAddress import java.nio.ByteBuffer -import java.nio.channels.{ServerSocketChannel, SocketChannel} -import java.util.concurrent.{Callable, Executors, TimeUnit} -import java.util.zip.{GZIPInputStream, GZIPOutputStream} +import java.nio.channels.ServerSocketChannel +import java.nio.channels.SocketChannel +import java.util.concurrent.Callable +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream import scala.util.Random -class MessageBufferInputTest extends AirSpec { +class MessageBufferInputTest extends AirSpec: - private val targetInputSize = - Seq(0, 10, 500, 1000, 2000, 4000, 8000, 10000, 30000, 50000, 100000) + private val targetInputSize = Seq(0, 10, 500, 1000, 2000, 4000, 8000, 10000, 30000, 50000, 100000) - private def testData(size: Int): Array[Byte] = { - //debug(s"test data size: ${size}") + private def testData(size: Int): Array[Byte] = + // debug(s"test data size: ${size}") val b = new Array[Byte](size) Random.nextBytes(b) b - } - private def testDataSet: Seq[Array[Byte]] = { - targetInputSize.map(testData) - } + private def testDataSet: Seq[Array[Byte]] = targetInputSize.map(testData) - private def runTest(factory: Array[Byte] => MessageBufferInput): Unit = { - for (b <- testDataSet) { + private def runTest(factory: Array[Byte] => MessageBufferInput): Unit = + for b <- testDataSet do checkInputData(b, factory(b)) - } - } - implicit class InputData(b: Array[Byte]) { - def compress = { + implicit class InputData(b: Array[Byte]): + def compress = val compressed = new ByteArrayOutputStream() val out = new GZIPOutputStream(compressed) out.write(b) out.close() compressed.toByteArray - } - def toByteBuffer = { - ByteBuffer.wrap(b) - } + def toByteBuffer = ByteBuffer.wrap(b) - def saveToTmpFile: File = { - val tmp = File - .createTempFile("testbuf", ".dat", new File("target")) - tmp.getParentFile.mkdirs() + def saveToTmpFile: File = + val dir = new File("target") + dir.mkdirs() + val tmp = File.createTempFile("testbuf", ".dat", dir) tmp.deleteOnExit() withResource(new FileOutputStream(tmp)) { out => out.write(b) } tmp - } - } - private def checkInputData(inputData: Array[Byte], in: MessageBufferInput): Unit = { + private def checkInputData(inputData: Array[Byte], in: MessageBufferInput): Unit = test(s"When input data size = ${inputData.length}") { var cursor = 0 - for (m <- Iterator.continually(in.next).takeWhile(_ != null)) { + for m <- Iterator.continually(in.next).takeWhile(_ != null) do m.toByteArray() shouldBe inputData.slice(cursor, cursor + m.size()) cursor += m.size() - } cursor shouldBe inputData.length } - } test("MessageBufferInput") { test("support byte arrays") { @@ -95,46 +86,40 @@ class MessageBufferInputTest extends AirSpec { } test("support InputStreams") { - runTest(b => new InputStreamBufferInput(new GZIPInputStream(new ByteArrayInputStream(b.compress)))) + runTest(b => + new InputStreamBufferInput(new GZIPInputStream(new ByteArrayInputStream(b.compress))) + ) } test("support file input channel") { runTest { b => val tmp = b.saveToTmpFile - try { - InputStreamBufferInput - .newBufferInput(new FileInputStream(tmp)) - } finally { - tmp.delete() - } + try InputStreamBufferInput.newBufferInput(new FileInputStream(tmp)) + finally tmp.delete() } } } - private def createTempFile = { + private def createTempFile = val f = File.createTempFile("msgpackTest", "msgpack") f.deleteOnExit f - } - private def createTempFileWithInputStream = { + private def createTempFileWithInputStream = val f = createTempFile val out = new FileOutputStream(f) MessagePack.newDefaultPacker(out).packInt(42).close val in = new FileInputStream(f) (f, in) - } - private def createTempFileWithChannel = { + private def createTempFileWithChannel = val (f, in) = createTempFileWithInputStream val ch = in.getChannel (f, ch) - } - private def readInt(buf: MessageBufferInput): Int = { + private def readInt(buf: MessageBufferInput): Int = val unpacker = MessagePack.newDefaultUnpacker(buf) unpacker.unpackInt - } test("InputStreamBufferInput") { test("reset buffer") { @@ -186,42 +171,42 @@ class MessageBufferInputTest extends AirSpec { } test("unpack without blocking") { - val server = - ServerSocketChannel.open.bind(new InetSocketAddress("localhost", 0)) + val server = ServerSocketChannel.open.bind(new InetSocketAddress("localhost", 0)) val executorService = Executors.newCachedThreadPool - try { - executorService.execute(new Runnable { - override def run: Unit = { - val server_ch = server.accept - val packer = MessagePack.newDefaultPacker(server_ch) - packer.packString("0123456789") - packer.flush - // Keep the connection open - while (!executorService.isShutdown) { - TimeUnit.SECONDS.sleep(1) - } - packer.close - } - }) - - val future = executorService.submit(new Callable[String] { - override def call: String = { - val conn_ch = SocketChannel.open(new InetSocketAddress("localhost", server.socket.getLocalPort)) - val unpacker = MessagePack.newDefaultUnpacker(conn_ch) - val s = unpacker.unpackString - unpacker.close - s - } - }) + try + executorService.execute( + new Runnable: + override def run: Unit = + val server_ch = server.accept + val packer = MessagePack.newDefaultPacker(server_ch) + packer.packString("0123456789") + packer.flush + // Keep the connection open + while !executorService.isShutdown do + TimeUnit.SECONDS.sleep(1) + packer.close + ) + + val future = executorService.submit( + new Callable[String]: + override def call: String = + val conn_ch = SocketChannel.open( + new InetSocketAddress("localhost", server.socket.getLocalPort) + ) + val unpacker = MessagePack.newDefaultUnpacker(conn_ch) + val s = unpacker.unpackString + unpacker.close + s + ) future.get(5, TimeUnit.SECONDS) shouldBe "0123456789" - } finally { + finally executorService.shutdown - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + if !executorService.awaitTermination(5, TimeUnit.SECONDS) then executorService.shutdownNow - } - } + end try } } -} + +end MessageBufferInputTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala index ea9cde57..5f6e9b7a 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferOutputTest.scala @@ -17,34 +17,30 @@ package org.msgpack.core.buffer import wvlet.airspec.AirSpec -import java.io._ +import java.io.* -class MessageBufferOutputTest extends AirSpec { +class MessageBufferOutputTest extends AirSpec: - private def createTempFile = { + private def createTempFile = val f = File.createTempFile("msgpackTest", "msgpack") f.deleteOnExit f - } - private def createTempFileWithOutputStream = { + private def createTempFileWithOutputStream = val f = createTempFile val out = new FileOutputStream(f) (f, out) - } - private def createTempFileWithChannel = { + private def createTempFileWithChannel = val (f, out) = createTempFileWithOutputStream val ch = out.getChannel (f, ch) - } - private def writeIntToBuf(buf: MessageBufferOutput) = { + private def writeIntToBuf(buf: MessageBufferOutput) = val mb0 = buf.next(8) mb0.putInt(0, 42) buf.writeBuffer(4) buf.close - } test("OutputStreamBufferOutput") { test("reset buffer") { @@ -73,4 +69,5 @@ class MessageBufferOutputTest extends AirSpec { f1.length.toInt > 0 shouldBe true } } -} + +end MessageBufferOutputTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala index 4de05995..36940473 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/buffer/MessageBufferTest.scala @@ -24,7 +24,7 @@ import scala.util.Random /** * Created on 2014/05/01. */ -class MessageBufferTest extends AirSpec with Benchmark { +class MessageBufferTest extends AirSpec with Benchmark: private val universal = MessageBuffer.allocate(0).isInstanceOf[MessageBufferU] @@ -55,97 +55,87 @@ class MessageBufferTest extends AirSpec with Benchmark { val ub = MessageBuffer.allocate(M) val ud = - if (universal) MessageBuffer.wrap(ByteBuffer.allocate(M)) - else MessageBuffer.wrap(ByteBuffer.allocateDirect(M)) + if universal then + MessageBuffer.wrap(ByteBuffer.allocate(M)) + else + MessageBuffer.wrap(ByteBuffer.allocateDirect(M)) val hb = ByteBuffer.allocate(M) val db = ByteBuffer.allocateDirect(M) - def bench(f: Int => Unit): Unit = { + def bench(f: Int => Unit): Unit = var i = 0 - while (i < N) { + while i < N do f((i * 4) % M) i += 1 - } - } val r = new Random(0) val rs = new Array[Int](N) (0 until N).map(i => rs(i) = r.nextInt(N)) - def randomBench(f: Int => Unit): Unit = { + def randomBench(f: Int => Unit): Unit = var i = 0 - while (i < N) { + while i < N do f((rs(i) * 4) % M) i += 1 - } - } val rep = 3 info(f"Reading buffers (of size:${M}%,d) ${N}%,d x $rep times") time("sequential getInt", repeat = rep) { block("unsafe array") { var i = 0 - while (i < N) { + while i < N do ub.getInt((i * 4) % M) i += 1 - } } block("unsafe direct") { var i = 0 - while (i < N) { + while i < N do ud.getInt((i * 4) % M) i += 1 - } } block("allocate") { var i = 0 - while (i < N) { + while i < N do hb.getInt((i * 4) % M) i += 1 - } } block("allocateDirect") { var i = 0 - while (i < N) { + while i < N do db.getInt((i * 4) % M) i += 1 - } } } time("random getInt", repeat = rep) { block("unsafe array") { var i = 0 - while (i < N) { + while i < N do ub.getInt((rs(i) * 4) % M) i += 1 - } } block("unsafe direct") { var i = 0 - while (i < N) { + while i < N do ud.getInt((rs(i) * 4) % M) i += 1 - } } block("allocate") { var i = 0 - while (i < N) { + while i < N do hb.getInt((rs(i) * 4) % M) i += 1 - } } block("allocateDirect") { var i = 0 - while (i < N) { + while i < N do db.getInt((rs(i) * 4) % M) i += 1 - } } } } @@ -153,20 +143,21 @@ class MessageBufferTest extends AirSpec with Benchmark { private val builder = Seq.newBuilder[MessageBuffer] builder += MessageBuffer.allocate(10) builder += MessageBuffer.wrap(ByteBuffer.allocate(10)) - if (!universal) builder += MessageBuffer.wrap(ByteBuffer.allocateDirect(10)) + if !universal then + builder += MessageBuffer.wrap(ByteBuffer.allocateDirect(10)) + private val buffers = builder.result() test("convert to ByteBuffer") { - for (t <- buffers) { + for t <- buffers do val bb = t.sliceAsByteBuffer bb.position() shouldBe 0 bb.limit() shouldBe 10 bb.capacity shouldBe 10 - } } test("put ByteBuffer on itself") { - for (t <- buffers) { + for t <- buffers do val b = Array[Byte](0x02, 0x03) val srcArray = ByteBuffer.wrap(b) val srcHeap = ByteBuffer.allocate(b.length) @@ -174,7 +165,7 @@ class MessageBufferTest extends AirSpec with Benchmark { val srcOffHeap = ByteBuffer.allocateDirect(b.length) srcOffHeap.put(b).flip - for (src <- Seq(srcArray, srcHeap, srcOffHeap)) { + for src <- Seq(srcArray, srcHeap, srcOffHeap) do // Write header bytes val header = Array[Byte](0x00, 0x01) t.putBytes(0, header, 0, header.length) @@ -185,12 +176,10 @@ class MessageBufferTest extends AirSpec with Benchmark { t.getByte(1) shouldBe 0x01 t.getByte(2) shouldBe 0x02 t.getByte(3) shouldBe 0x03 - } - } } test("put MessageBuffer on itself") { - for (t <- buffers) { + for t <- buffers do val b = Array[Byte](0x02, 0x03) val srcArray = ByteBuffer.wrap(b) val srcHeap = ByteBuffer.allocate(b.length) @@ -199,9 +188,10 @@ class MessageBufferTest extends AirSpec with Benchmark { srcOffHeap.put(b).flip val builder = Seq.newBuilder[ByteBuffer] builder ++= Seq(srcArray, srcHeap) - if (!universal) builder += srcOffHeap + if !universal then + builder += srcOffHeap - for (src <- builder.result().map(d => MessageBuffer.wrap(d))) { + for src <- builder.result().map(d => MessageBuffer.wrap(d)) do // Write header bytes val header = Array[Byte](0x00, 0x01) t.putBytes(0, header, 0, header.length) @@ -212,23 +202,18 @@ class MessageBufferTest extends AirSpec with Benchmark { t.getByte(1) shouldBe 0x01 t.getByte(2) shouldBe 0x02 t.getByte(3) shouldBe 0x03 - } - } } test("copy sliced buffer") { - def prepareBytes: Array[Byte] = { - Array[Byte](0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07) - } + def prepareBytes: Array[Byte] = Array[Byte](0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07) - def prepareDirectBuffer: ByteBuffer = { + def prepareDirectBuffer: ByteBuffer = val directBuffer = ByteBuffer.allocateDirect(prepareBytes.length) directBuffer.put(prepareBytes) directBuffer.flip directBuffer - } - def checkSliceAndCopyTo(srcBuffer: MessageBuffer, dstBuffer: MessageBuffer) = { + def checkSliceAndCopyTo(srcBuffer: MessageBuffer, dstBuffer: MessageBuffer) = val sliced = srcBuffer.slice(2, 5) sliced.size() shouldBe 5 @@ -248,12 +233,17 @@ class MessageBufferTest extends AirSpec with Benchmark { dstBuffer.getByte(5) shouldBe 0x05 dstBuffer.getByte(6) shouldBe 0x06 dstBuffer.getByte(7) shouldBe 0x07 - } checkSliceAndCopyTo(MessageBuffer.wrap(prepareBytes), MessageBuffer.wrap(prepareBytes)) - checkSliceAndCopyTo(MessageBuffer.wrap(ByteBuffer.wrap(prepareBytes)), MessageBuffer.wrap(ByteBuffer.wrap(prepareBytes))) - if (!universal) { - checkSliceAndCopyTo(MessageBuffer.wrap(prepareDirectBuffer), MessageBuffer.wrap(prepareDirectBuffer)) - } + checkSliceAndCopyTo( + MessageBuffer.wrap(ByteBuffer.wrap(prepareBytes)), + MessageBuffer.wrap(ByteBuffer.wrap(prepareBytes)) + ) + if !universal then + checkSliceAndCopyTo( + MessageBuffer.wrap(prepareDirectBuffer), + MessageBuffer.wrap(prepareDirectBuffer) + ) } -} + +end MessageBufferTest diff --git a/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala b/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala index 99876275..1a1a3a3c 100644 --- a/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/core/example/MessagePackExampleTest.scala @@ -19,7 +19,7 @@ import wvlet.airspec.AirSpec /** */ -class MessagePackExampleTest extends AirSpec { +class MessagePackExampleTest extends AirSpec: test("example") { @@ -39,4 +39,3 @@ class MessagePackExampleTest extends AirSpec { MessagePackExample.configuration(); } } -} diff --git a/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala index fb340553..73e1edc5 100644 --- a/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/value/RawStringValueImplTest.scala @@ -17,7 +17,7 @@ package org.msgpack.value import wvlet.airspec.AirSpec -class RawStringValueImplTest extends AirSpec { +class RawStringValueImplTest extends AirSpec: test("return the same hash code if they are equal") { val str = "a" @@ -29,4 +29,3 @@ class RawStringValueImplTest extends AirSpec { a2 shouldBe a1 a2.hashCode shouldBe a1.hashCode } -} diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala index 3fe2a07f..623ca36d 100644 --- a/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueFactoryTest.scala @@ -21,7 +21,7 @@ import wvlet.airspec.spi.PropertyCheck /** */ -class ValueFactoryTest extends AirSpec with PropertyCheck { +class ValueFactoryTest extends AirSpec with PropertyCheck: private def isValid( v: Value, @@ -38,7 +38,7 @@ class ValueFactoryTest extends AirSpec with PropertyCheck { isRaw: Boolean = false, isNumber: Boolean = false, isTimestamp: Boolean = false - ): Boolean = { + ): Boolean = v.isNilValue shouldBe isNil v.isBooleanValue shouldBe isBoolean v.isIntegerValue shouldBe isInteger @@ -52,7 +52,6 @@ class ValueFactoryTest extends AirSpec with PropertyCheck { v.isNumberValue shouldBe isNumber v.isTimestampValue shouldBe isTimestamp true - } test("ValueFactory") { test("nil") { @@ -67,24 +66,44 @@ class ValueFactoryTest extends AirSpec with PropertyCheck { test("int") { forAll { (v: Int) => - isValid(ValueFactory.newInteger(v), expected = ValueType.INTEGER, isInteger = true, isNumber = true) + isValid( + ValueFactory.newInteger(v), + expected = ValueType.INTEGER, + isInteger = true, + isNumber = true + ) } } test("float") { forAll { (v: Float) => - isValid(ValueFactory.newFloat(v), expected = ValueType.FLOAT, isFloat = true, isNumber = true) + isValid( + ValueFactory.newFloat(v), + expected = ValueType.FLOAT, + isFloat = true, + isNumber = true + ) } } test("string") { forAll { (v: String) => - isValid(ValueFactory.newString(v), expected = ValueType.STRING, isString = true, isRaw = true) + isValid( + ValueFactory.newString(v), + expected = ValueType.STRING, + isString = true, + isRaw = true + ) } } test("array") { forAll { (v: Array[Byte]) => - isValid(ValueFactory.newBinary(v), expected = ValueType.BINARY, isBinary = true, isRaw = true) + isValid( + ValueFactory.newBinary(v), + expected = ValueType.BINARY, + isBinary = true, + isRaw = true + ) } } @@ -98,13 +117,23 @@ class ValueFactoryTest extends AirSpec with PropertyCheck { test("ext") { forAll { (v: Array[Byte]) => - isValid(ValueFactory.newExtension(0, v), expected = ValueType.EXTENSION, isExtension = true, isRaw = false) + isValid( + ValueFactory.newExtension(0, v), + expected = ValueType.EXTENSION, + isExtension = true, + isRaw = false + ) } } test("timestamp") { forAll { (millis: Long) => - isValid(ValueFactory.newTimestamp(millis), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true) + isValid( + ValueFactory.newTimestamp(millis), + expected = ValueType.EXTENSION, + isExtension = true, + isTimestamp = true + ) } } @@ -112,8 +141,14 @@ class ValueFactoryTest extends AirSpec with PropertyCheck { val posLong = Gen.chooseNum[Long](-31557014167219200L, 31556889864403199L) val posInt = Gen.chooseNum(0, 1000000000 - 1) // NANOS_PER_SECOND forAll(posLong, posInt) { (sec: Long, nano: Int) => - isValid(ValueFactory.newTimestamp(sec, nano), expected = ValueType.EXTENSION, isExtension = true, isTimestamp = true) + isValid( + ValueFactory.newTimestamp(sec, nano), + expected = ValueType.EXTENSION, + isExtension = true, + isTimestamp = true + ) } } } -} + +end ValueFactoryTest diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala index 83cbde6b..76e2ed27 100644 --- a/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueTest.scala @@ -18,14 +18,17 @@ package org.msgpack.value import org.msgpack.core.MessagePackSpec.createMessagePackData import java.math.BigInteger -import org.msgpack.core._ +import org.msgpack.core.* import org.scalacheck.Prop.propBoolean import wvlet.airframe.json.JSON import wvlet.airspec.AirSpec import wvlet.airspec.spi.PropertyCheck -class ValueTest extends AirSpec with PropertyCheck { - private def checkSuccinctType(pack: MessagePacker => Unit, expectedAtMost: MessageFormat): Boolean = { +class ValueTest extends AirSpec with PropertyCheck: + private def checkSuccinctType( + pack: MessagePacker => Unit, + expectedAtMost: MessageFormat + ): Boolean = val b = createMessagePackData(pack) val v1 = MessagePack.newDefaultUnpacker(b).unpackValue() val mf = v1.asIntegerValue().mostSuccinctMessageFormat() @@ -39,7 +42,6 @@ class ValueTest extends AirSpec with PropertyCheck { mf2.ordinal() <= expectedAtMost.ordinal() shouldBe true true - } test("Value") { test("tell most succinct integer type") { @@ -61,14 +63,17 @@ class ValueTest extends AirSpec with PropertyCheck { forAll { (v: Long) => v > 0 ==> { // Create value between 2^63-1 < v <= 2^64-1 - checkSuccinctType(_.packBigInteger(BigInteger.valueOf(Long.MaxValue).add(BigInteger.valueOf(v))), MessageFormat.UINT64) + checkSuccinctType( + _.packBigInteger(BigInteger.valueOf(Long.MaxValue).add(BigInteger.valueOf(v))), + MessageFormat.UINT64 + ) } } } test("produce json strings") { - import ValueFactory._ + import ValueFactory.* newNil().toJson shouldBe "null" newNil().toString shouldBe "null" @@ -88,7 +93,8 @@ class ValueTest extends AirSpec with PropertyCheck { newArray(newInteger(0), newString("hello")).toJson shouldBe "[0,\"hello\"]" newArray(newInteger(0), newString("hello")).toString shouldBe "[0,\"hello\"]" - newArray(newArray(newString("Apple"), newFloat(0.2)), newNil()).toJson shouldBe """[["Apple",0.2],null]""" + newArray(newArray(newString("Apple"), newFloat(0.2)), newNil()).toJson shouldBe + """[["Apple",0.2],null]""" // Map value val m = newMapBuilder() @@ -112,7 +118,7 @@ class ValueTest extends AirSpec with PropertyCheck { } test("check appropriate range for integers") { - import ValueFactory._ + import ValueFactory.* import java.lang.Byte import java.lang.Short @@ -142,4 +148,5 @@ class ValueTest extends AirSpec with PropertyCheck { } } } -} + +end ValueTest diff --git a/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala index e8b04d5f..53707c99 100644 --- a/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/value/ValueTypeTest.scala @@ -15,71 +15,60 @@ // package org.msgpack.value -import org.msgpack.core.MessagePack.Code._ -import org.msgpack.core.{MessageFormat, MessageFormatException} +import org.msgpack.core.MessagePack.Code.* +import org.msgpack.core.MessageFormat +import org.msgpack.core.MessageFormatException import wvlet.airspec.AirSpec /** * Created on 2014/05/06. */ -class ValueTypeTest extends AirSpec { +class ValueTypeTest extends AirSpec: test("lookup ValueType from a byte value") { - def check(b: Byte, tpe: ValueType): Unit = { - MessageFormat.valueOf(b).getValueType shouldBe tpe - } + def check(b: Byte, tpe: ValueType): Unit = MessageFormat.valueOf(b).getValueType shouldBe tpe - for (i <- 0 until 0x7f) { + for i <- 0 until 0x7f do check(i.toByte, ValueType.INTEGER) - } - for (i <- 0x80 until 0x8f) { + for i <- 0x80 until 0x8f do check(i.toByte, ValueType.MAP) - } - for (i <- 0x90 until 0x9f) { + for i <- 0x90 until 0x9f do check(i.toByte, ValueType.ARRAY) - } check(NIL, ValueType.NIL) - try { + try MessageFormat.valueOf(NEVER_USED).getValueType fail("NEVER_USED type should not have ValueType") - } catch { + catch case e: MessageFormatException => // OK - } check(TRUE, ValueType.BOOLEAN) check(FALSE, ValueType.BOOLEAN) - for (t <- Seq(BIN8, BIN16, BIN32)) { + for t <- Seq(BIN8, BIN16, BIN32) do check(t, ValueType.BINARY) - } - for (t <- Seq(FIXEXT1, FIXEXT2, FIXEXT4, FIXEXT8, FIXEXT16, EXT8, EXT16, EXT32)) { + for t <- Seq(FIXEXT1, FIXEXT2, FIXEXT4, FIXEXT8, FIXEXT16, EXT8, EXT16, EXT32) do check(t, ValueType.EXTENSION) - } - for (t <- Seq(INT8, INT16, INT32, INT64, UINT8, UINT16, UINT32, UINT64)) { + for t <- Seq(INT8, INT16, INT32, INT64, UINT8, UINT16, UINT32, UINT64) do check(t, ValueType.INTEGER) - } - for (t <- Seq(STR8, STR16, STR32)) { + for t <- Seq(STR8, STR16, STR32) do check(t, ValueType.STRING) - } - for (t <- Seq(FLOAT32, FLOAT64)) { + for t <- Seq(FLOAT32, FLOAT64) do check(t, ValueType.FLOAT) - } - for (t <- Seq(ARRAY16, ARRAY32)) { + for t <- Seq(ARRAY16, ARRAY32) do check(t, ValueType.ARRAY) - } - for (i <- 0xe0 until 0xff) { + for i <- 0xe0 until 0xff do check(i.toByte, ValueType.INTEGER) - } } -} + +end ValueTypeTest diff --git a/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala b/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala index 5d97d871..d4754e83 100644 --- a/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala +++ b/msgpack-core/src/test/scala/org/msgpack/value/VariableTest.scala @@ -15,18 +15,20 @@ // package org.msgpack.value -import org.msgpack.core.{MessagePack, MessagePacker, MessageTypeCastException} +import org.msgpack.core.MessagePack +import org.msgpack.core.MessagePacker +import org.msgpack.core.MessageTypeCastException import wvlet.airspec.AirSpec import wvlet.airspec.spi.PropertyCheck import java.time.Instant import java.util -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* /** */ -class VariableTest extends AirSpec with PropertyCheck { - private def check(pack: MessagePacker => Unit, checker: Variable => Unit): Unit = { +class VariableTest extends AirSpec with PropertyCheck: + private def check(pack: MessagePacker => Unit, checker: Variable => Unit): Unit = val packer = MessagePack.newDefaultBufferPacker() pack(packer) val msgpack = packer.toByteArray @@ -36,12 +38,11 @@ class VariableTest extends AirSpec with PropertyCheck { unpacker.unpackValue(v) checker(v) unpacker.close() - } /** * Test Value -> MsgPack -> Value */ - private def roundTrip(v: Value): Unit = { + private def roundTrip(v: Value): Unit = val packer = MessagePack.newDefaultBufferPacker() v.writeTo(packer) val msgpack = packer.toByteArray @@ -50,7 +51,6 @@ class VariableTest extends AirSpec with PropertyCheck { unpacker.close() v shouldBe v1 v.immutableValue() shouldBe v1 - } private def validateValue[V <: Value]( v: V, @@ -64,7 +64,7 @@ class VariableTest extends AirSpec with PropertyCheck { asMap: Boolean = false, asExtension: Boolean = false, asTimestamp: Boolean = false - ): V = { + ): V = v.isNilValue shouldBe asNil v.isBooleanValue shouldBe asBoolean v.isIntegerValue shouldBe asInteger @@ -78,203 +78,200 @@ class VariableTest extends AirSpec with PropertyCheck { v.isExtensionValue shouldBe asExtension | asTimestamp v.isTimestampValue shouldBe asTimestamp - if (asNil) { + if asNil then v.getValueType shouldBe ValueType.NIL roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asNilValue() } - } - if (asBoolean) { + if asBoolean then v.getValueType shouldBe ValueType.BOOLEAN roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asBooleanValue() } - } - if (asInteger) { + if asInteger then v.getValueType shouldBe ValueType.INTEGER roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asIntegerValue() } - } - if (asFloat) { + if asFloat then v.getValueType shouldBe ValueType.FLOAT roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asFloatValue() } - } - if (asBinary | asString) { + if asBinary | asString then v.asRawValue() roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asRawValue() } - } - if (asBinary) { + if asBinary then v.getValueType shouldBe ValueType.BINARY roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asBinaryValue() } - } - if (asString) { + if asString then v.getValueType shouldBe ValueType.STRING roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asStringValue() } - } - if (asArray) { + if asArray then v.getValueType shouldBe ValueType.ARRAY roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asArrayValue() } - } - if (asMap) { + if asMap then v.getValueType shouldBe ValueType.MAP roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asMapValue() } - } - if (asExtension) { + if asExtension then v.getValueType shouldBe ValueType.EXTENSION roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asExtensionValue() } - } - if (asTimestamp) { + if asTimestamp then v.getValueType shouldBe ValueType.EXTENSION roundTrip(v) - } else { + else intercept[MessageTypeCastException] { v.asTimestampValue() } - } v - } + + end validateValue test("Variable") { test("read nil") { check( _.packNil, - checker = { v => - val iv = validateValue(v.asNilValue(), asNil = true) - iv.toJson shouldBe "null" - } + checker = + v => + val iv = validateValue(v.asNilValue(), asNil = true) + iv.toJson shouldBe "null" ) } test("read integers") { - forAll { i: Int => + forAll { (i: Int) => check( _.packInt(i), - checker = { v => - val iv = validateValue(v.asIntegerValue(), asInteger = true) - iv.asInt() shouldBe i - iv.asLong() shouldBe i.toLong - } + checker = + v => + val iv = validateValue(v.asIntegerValue(), asInteger = true) + iv.asInt() shouldBe i + iv.asLong() shouldBe i.toLong ) } } test("read double") { - forAll { x: Double => + forAll { (x: Double) => check( _.packDouble(x), - checker = { v => - val iv = validateValue(v.asFloatValue(), asFloat = true) - //iv.toDouble shouldBe v - //iv.toFloat shouldBe x.toFloat - } + checker = + v => + val iv = validateValue(v.asFloatValue(), asFloat = true) + // iv.toDouble shouldBe v + // iv.toFloat shouldBe x.toFloat ) } } test("read boolean") { - forAll { x: Boolean => + forAll { (x: Boolean) => check( _.packBoolean(x), - checker = { v => - val iv = validateValue(v.asBooleanValue(), asBoolean = true) - iv.getBoolean shouldBe x - } + checker = + v => + val iv = validateValue(v.asBooleanValue(), asBoolean = true) + iv.getBoolean shouldBe x ) } } test("read binary") { - forAll { x: Array[Byte] => + forAll { (x: Array[Byte]) => check( { packer => - packer.packBinaryHeader(x.length); packer.addPayload(x) + packer.packBinaryHeader(x.length); + packer.addPayload(x) }, - checker = { v => - val iv = validateValue(v.asBinaryValue(), asBinary = true) - util.Arrays.equals(iv.asByteArray(), x) - } + checker = + v => + val iv = validateValue(v.asBinaryValue(), asBinary = true) + util.Arrays.equals(iv.asByteArray(), x) ) } } test("read string") { - forAll { x: String => + forAll { (x: String) => check( _.packString(x), - checker = { v => - val iv = validateValue(v.asStringValue(), asString = true) - iv.asString() shouldBe x - } + checker = + v => + val iv = validateValue(v.asStringValue(), asString = true) + iv.asString() shouldBe x ) } } test("read array") { - forAll { x: Seq[Int] => + forAll { (x: Seq[Int]) => check( { packer => packer.packArrayHeader(x.size) - x.foreach { packer.packInt(_) } + x.foreach { + packer.packInt(_) + } }, - checker = { v => - val iv = validateValue(v.asArrayValue(), asArray = true) - val lst = iv.list().asScala.map(_.asIntegerValue().toInt) - lst shouldBe x - } + checker = + v => + val iv = validateValue(v.asArrayValue(), asArray = true) + val lst = iv.list().asScala.map(_.asIntegerValue().toInt) + lst shouldBe x ) } } test("read map") { - forAll { x: Seq[Int] => + forAll { (x: Seq[Int]) => // Generate map with unique keys - val map = x.zipWithIndex.map { case (x, i) => (s"key-${i}", x) } + val map = x + .zipWithIndex + .map { case (x, i) => + (s"key-${i}", x) + } check( { packer => packer.packMapHeader(map.size) @@ -283,27 +280,32 @@ class VariableTest extends AirSpec with PropertyCheck { packer.packInt(x._2) } }, - checker = { v => - val iv = validateValue(v.asMapValue(), asMap = true) - val lst = iv.map().asScala.map(p => (p._1.asStringValue().asString(), p._2.asIntegerValue().asInt())).toSeq - lst.sortBy(_._1) shouldBe map.sortBy(_._1) - } + checker = + v => + val iv = validateValue(v.asMapValue(), asMap = true) + val lst = + iv.map() + .asScala + .map(p => (p._1.asStringValue().asString(), p._2.asIntegerValue().asInt())) + .toSeq + lst.sortBy(_._1) shouldBe map.sortBy(_._1) ) } } test("read timestamps") { - forAll { millis: Long => + forAll { (millis: Long) => val i = Instant.ofEpochMilli(millis) check( _.packTimestamp(i), - checker = { v => - val ts = validateValue(v.asTimestampValue(), asTimestamp = true) - ts.isTimestampValue shouldBe true - ts.toInstant shouldBe i - } + checker = + v => + val ts = validateValue(v.asTimestampValue(), asTimestamp = true) + ts.isTimestampValue shouldBe true + ts.toInstant shouldBe i ) } } } -} + +end VariableTest diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java index bb5064f1..865c0cf4 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.io.ContentReference; import com.fasterxml.jackson.core.io.IOContext; import org.msgpack.core.MessagePack; import org.msgpack.core.annotations.VisibleForTesting; @@ -39,6 +40,7 @@ public class MessagePackFactory private final MessagePack.PackerConfig packerConfig; private boolean reuseResourceInGenerator = true; private boolean reuseResourceInParser = true; + private boolean supportIntegerKeys = false; private ExtensionTypeCustomDeserializers extTypeCustomDesers; public MessagePackFactory() @@ -74,6 +76,12 @@ public MessagePackFactory setReuseResourceInParser(boolean reuseResourceInParser return this; } + public MessagePackFactory setSupportIntegerKeys(boolean supportIntegerKeys) + { + this.supportIntegerKeys = supportIntegerKeys; + return this; + } + public MessagePackFactory setExtTypeCustomDesers(ExtensionTypeCustomDeserializers extTypeCustomDesers) { this.extTypeCustomDesers = extTypeCustomDesers; @@ -84,7 +92,7 @@ public MessagePackFactory setExtTypeCustomDesers(ExtensionTypeCustomDeserializer public JsonGenerator createGenerator(OutputStream out, JsonEncoding enc) throws IOException { - return new MessagePackGenerator(_generatorFeatures, _objectCodec, out, packerConfig, reuseResourceInGenerator); + return new MessagePackGenerator(_generatorFeatures, _objectCodec, out, packerConfig, reuseResourceInGenerator, supportIntegerKeys); } @Override @@ -104,7 +112,7 @@ public JsonGenerator createGenerator(Writer w) public JsonParser createParser(byte[] data) throws IOException { - IOContext ioContext = _createContext(data, false); + IOContext ioContext = _createContext(ContentReference.rawReference(data), false); return _createParser(data, 0, data.length, ioContext); } @@ -112,7 +120,7 @@ public JsonParser createParser(byte[] data) public JsonParser createParser(InputStream in) throws IOException { - IOContext ioContext = _createContext(in, false); + IOContext ioContext = _createContext(ContentReference.rawReference(in), false); return _createParser(in, ioContext); } diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java index 11aba6e5..3dde5604 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -19,7 +19,11 @@ import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.base.GeneratorBase; +import com.fasterxml.jackson.core.io.ContentReference; +import com.fasterxml.jackson.core.io.IOContext; import com.fasterxml.jackson.core.io.SerializedString; +import com.fasterxml.jackson.core.json.JsonWriteContext; +import com.fasterxml.jackson.core.util.BufferRecycler; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; import org.msgpack.core.annotations.Nullable; @@ -50,6 +54,7 @@ public class MessagePackGenerator private static final ThreadLocal messageBufferOutputHolder = new ThreadLocal<>(); private final OutputStream output; private final MessagePack.PackerConfig packerConfig; + private final boolean supportIntegerKeys; private int currentParentElementIndex = -1; private int currentState = IN_ROOT; @@ -184,32 +189,38 @@ else if (value instanceof NodeArray) { } // This is an internal constructor for nested serialization. + @SuppressWarnings("deprecation") private MessagePackGenerator( int features, ObjectCodec codec, OutputStream out, - MessagePack.PackerConfig packerConfig) + MessagePack.PackerConfig packerConfig, + boolean supportIntegerKeys) { - super(features, codec); + super(features, codec, new IOContext(new BufferRecycler(), ContentReference.rawReference(out), false), JsonWriteContext.createRootContext(null)); this.output = out; this.messagePacker = packerConfig.newPacker(out); this.packerConfig = packerConfig; this.nodes = new ArrayList<>(); + this.supportIntegerKeys = supportIntegerKeys; } + @SuppressWarnings("deprecation") public MessagePackGenerator( int features, ObjectCodec codec, OutputStream out, MessagePack.PackerConfig packerConfig, - boolean reuseResourceInGenerator) + boolean reuseResourceInGenerator, + boolean supportIntegerKeys) throws IOException { - super(features, codec); + super(features, codec, new IOContext(new BufferRecycler(), ContentReference.rawReference(out), false), JsonWriteContext.createRootContext(null)); this.output = out; this.messagePacker = packerConfig.newPacker(getMessageBufferOutputForOutputStream(out, reuseResourceInGenerator)); this.packerConfig = packerConfig; this.nodes = new ArrayList<>(); + this.supportIntegerKeys = supportIntegerKeys; } private MessageBufferOutput getMessageBufferOutputForOutputStream( @@ -373,7 +384,7 @@ else if (v instanceof MessagePackExtensionType) { else { messagePacker.flush(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - MessagePackGenerator messagePackGenerator = new MessagePackGenerator(getFeatureMask(), getCodec(), outputStream, packerConfig); + MessagePackGenerator messagePackGenerator = new MessagePackGenerator(getFeatureMask(), getCodec(), outputStream, packerConfig, supportIntegerKeys); getCodec().writeValue(messagePackGenerator, v); output.write(outputStream.toByteArray()); } @@ -513,6 +524,17 @@ private void writeByteArrayTextKey(byte[] text, int offset, int len) throws IOEx addValueNode(new String(text, offset, len, DEFAULT_CHARSET)); } + @Override + public void writeFieldId(long id) throws IOException + { + if (this.supportIntegerKeys) { + addKeyNode(id); + } + else { + super.writeFieldId(id); + } + } + @Override public void writeFieldName(String name) { diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java index 4876c797..72aeed20 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -106,7 +106,7 @@ private MessagePackParser(IOContext ctxt, boolean reuseResourceInParser) throws IOException { - super(features); + super(features, ctxt.streamReadConstraints()); this.codec = objectCodec; ioContext = ctxt; @@ -583,15 +583,29 @@ public JsonStreamContext getParsingContext() } @Override + public JsonLocation currentTokenLocation() + { + return new JsonLocation(ioContext.contentReference(), tokenPosition, -1, -1); + } + + @Override + public JsonLocation currentLocation() + { + return new JsonLocation(ioContext.contentReference(), currentPosition, -1, -1); + } + + @Override + @Deprecated public JsonLocation getTokenLocation() { - return new JsonLocation(ioContext.getSourceReference(), tokenPosition, -1, -1, (int) tokenPosition); + return currentTokenLocation(); } @Override + @Deprecated public JsonLocation getCurrentLocation() { - return new JsonLocation(ioContext.getSourceReference(), currentPosition, -1, -1, (int) currentPosition); + return currentLocation(); } @Override @@ -621,7 +635,13 @@ public String currentName() return streamReadContext.getCurrentName(); } + public boolean isCurrentFieldId() + { + return this.type == Type.INT || this.type == Type.LONG; + } + @Override + @Deprecated public String getCurrentName() throws IOException { diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java index fa2148dc..4100ef4c 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java @@ -16,8 +16,10 @@ package org.msgpack.jackson.dataformat; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig; import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; @@ -43,6 +45,13 @@ public MessagePackSerializerFactory(SerializerFactoryConfig config) } @Override + public JsonSerializer createKeySerializer(SerializerProvider prov, JavaType keyType, JsonSerializer defaultImpl) throws JsonMappingException + { + return new MessagePackKeySerializer(); + } + + @Override + @Deprecated public JsonSerializer createKeySerializer(SerializationConfig config, JavaType keyType, JsonSerializer defaultImpl) { return new MessagePackKeySerializer(); diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java new file mode 100644 index 00000000..bbac6fb9 --- /dev/null +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java @@ -0,0 +1,132 @@ +// +// MessagePack for Java +// +// Licensed 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. +// +package org.msgpack.jackson.dataformat; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.NullValueProvider; +import com.fasterxml.jackson.databind.deser.impl.JDKValueInstantiators; +import com.fasterxml.jackson.databind.deser.std.MapDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.LinkedHashMap; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MessagePackDataformatForFieldIdTest +{ + static class MessagePackMapDeserializer extends MapDeserializer + { + public static KeyDeserializer keyDeserializer = new KeyDeserializer() + { + @Override + public Object deserializeKey(String s, DeserializationContext deserializationContext) + throws IOException + { + JsonParser parser = deserializationContext.getParser(); + if (parser instanceof MessagePackParser) { + MessagePackParser p = (MessagePackParser) parser; + if (p.isCurrentFieldId()) { + return Integer.valueOf(s); + } + } + return s; + } + }; + + public MessagePackMapDeserializer() + { + super( + TypeFactory.defaultInstance().constructMapType(Map.class, Object.class, Object.class), + JDKValueInstantiators.findStdValueInstantiator(null, LinkedHashMap.class), + keyDeserializer, null, null); + } + + public MessagePackMapDeserializer(MapDeserializer src, KeyDeserializer keyDeser, + JsonDeserializer valueDeser, TypeDeserializer valueTypeDeser, NullValueProvider nuller, + Set ignorable, Set includable) + { + super(src, keyDeser, valueDeser, valueTypeDeser, nuller, ignorable, includable); + } + + @Override + protected MapDeserializer withResolved(KeyDeserializer keyDeser, TypeDeserializer valueTypeDeser, + JsonDeserializer valueDeser, NullValueProvider nuller, Set ignorable, + Set includable) + { + return new MessagePackMapDeserializer(this, keyDeser, (JsonDeserializer) valueDeser, valueTypeDeser, + nuller, ignorable, includable); + } + } + + @Test + public void testMixedKeys() + throws IOException + { + ObjectMapper mapper = new ObjectMapper( + new MessagePackFactory() + .setSupportIntegerKeys(true) + ) + .registerModule(new SimpleModule() + .addDeserializer(Map.class, new MessagePackMapDeserializer())); + + Map map = new HashMap<>(); + map.put(1, "one"); + map.put("2", "two"); + + byte[] bytes = mapper.writeValueAsBytes(map); + Map deserializedInit = mapper.readValue(bytes, new TypeReference>() {}); + + Map expected = new HashMap<>(map); + Map actual = new HashMap<>(deserializedInit); + + assertEquals(expected, actual); + } + + @Test + public void testMixedKeysBackwardsCompatiable() + throws IOException + { + ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()) + .registerModule(new SimpleModule() + .addDeserializer(Map.class, new MessagePackMapDeserializer())); + + Map map = new HashMap<>(); + map.put(1, "one"); + map.put("2", "two"); + + byte[] bytes = mapper.writeValueAsBytes(map); + Map deserializedInit = mapper.readValue(bytes, new TypeReference>() {}); + + Map expected = new HashMap<>(); + expected.put("1", "one"); + expected.put("2", "two"); + Map actual = new HashMap<>(deserializedInit); + + assertEquals(expected, actual); + } +} diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java index 52269a7d..e899e4f4 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java @@ -16,7 +16,7 @@ package org.msgpack.jackson.dataformat; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.charset.Charset; @@ -24,9 +24,9 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.hamcrest.MatcherAssert.assertThat; public class MessagePackDataformatForPojoTest extends MessagePackDataformatTestBase diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java index f8d7ac3c..9e376541 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java @@ -23,7 +23,7 @@ import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.msgpack.core.MessagePack; import java.io.IOException; @@ -35,8 +35,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; public class MessagePackFactoryTest extends MessagePackDataformatTestBase diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java index 7ba48a61..a13951b2 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -24,7 +24,7 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.module.SimpleModule; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.msgpack.core.ExtensionTypeHeader; import org.msgpack.core.MessagePack; import org.msgpack.core.MessageUnpacker; @@ -53,13 +53,14 @@ import java.util.concurrent.TimeUnit; import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.hamcrest.MatcherAssert.assertThat; public class MessagePackGeneratorTest extends MessagePackDataformatTestBase @@ -349,7 +350,7 @@ public void testBigDecimal() } } - @Test(expected = IOException.class) + @Test public void testEnableFeatureAutoCloseTarget() throws IOException { @@ -358,7 +359,9 @@ public void testEnableFeatureAutoCloseTarget() ObjectMapper objectMapper = new ObjectMapper(messagePackFactory); List integers = Arrays.asList(1); objectMapper.writeValue(out, integers); - objectMapper.writeValue(out, integers); + assertThrows(IOException.class, () -> { + objectMapper.writeValue(out, integers); + }); } @Test diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java index 68721fce..d14f97f2 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java @@ -16,14 +16,14 @@ package org.msgpack.jackson.dataformat; import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class MessagePackMapperTest { diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java index a416c92b..256c4332 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java @@ -27,7 +27,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; import org.msgpack.value.ExtensionValue; @@ -52,10 +52,11 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; public class MessagePackParserTest extends MessagePackDataformatTestBase @@ -316,43 +317,43 @@ public void testMessagePackParserDirectly() JsonToken jsonToken = parser.nextToken(); assertEquals(JsonToken.START_OBJECT, jsonToken); - assertEquals(-1, parser.getTokenLocation().getLineNr()); - assertEquals(0, parser.getTokenLocation().getColumnNr()); - assertEquals(-1, parser.getCurrentLocation().getLineNr()); - assertEquals(1, parser.getCurrentLocation().getColumnNr()); + assertEquals(-1, parser.currentTokenLocation().getLineNr()); + assertEquals(0, parser.currentTokenLocation().getColumnNr()); + assertEquals(-1, parser.currentLocation().getLineNr()); + assertEquals(1, parser.currentLocation().getColumnNr()); jsonToken = parser.nextToken(); assertEquals(JsonToken.FIELD_NAME, jsonToken); - assertEquals("zero", parser.getCurrentName()); - assertEquals(1, parser.getTokenLocation().getColumnNr()); - assertEquals(6, parser.getCurrentLocation().getColumnNr()); + assertEquals("zero", parser.currentName()); + assertEquals(1, parser.currentTokenLocation().getColumnNr()); + assertEquals(6, parser.currentLocation().getColumnNr()); jsonToken = parser.nextToken(); assertEquals(JsonToken.VALUE_NUMBER_INT, jsonToken); assertEquals(0, parser.getIntValue()); - assertEquals(6, parser.getTokenLocation().getColumnNr()); - assertEquals(7, parser.getCurrentLocation().getColumnNr()); + assertEquals(6, parser.currentTokenLocation().getColumnNr()); + assertEquals(7, parser.currentLocation().getColumnNr()); jsonToken = parser.nextToken(); assertEquals(JsonToken.FIELD_NAME, jsonToken); - assertEquals("one", parser.getCurrentName()); - assertEquals(7, parser.getTokenLocation().getColumnNr()); - assertEquals(11, parser.getCurrentLocation().getColumnNr()); + assertEquals("one", parser.currentName()); + assertEquals(7, parser.currentTokenLocation().getColumnNr()); + assertEquals(11, parser.currentLocation().getColumnNr()); parser.overrideCurrentName("two"); - assertEquals("two", parser.getCurrentName()); + assertEquals("two", parser.currentName()); jsonToken = parser.nextToken(); assertEquals(JsonToken.VALUE_NUMBER_FLOAT, jsonToken); assertEquals(1.0f, parser.getIntValue(), 0.001f); - assertEquals(11, parser.getTokenLocation().getColumnNr()); - assertEquals(16, parser.getCurrentLocation().getColumnNr()); + assertEquals(11, parser.currentTokenLocation().getColumnNr()); + assertEquals(16, parser.currentLocation().getColumnNr()); jsonToken = parser.nextToken(); assertEquals(JsonToken.END_OBJECT, jsonToken); - assertEquals(-1, parser.getTokenLocation().getLineNr()); - assertEquals(16, parser.getTokenLocation().getColumnNr()); - assertEquals(-1, parser.getCurrentLocation().getLineNr()); - assertEquals(16, parser.getCurrentLocation().getColumnNr()); + assertEquals(-1, parser.currentTokenLocation().getLineNr()); + assertEquals(16, parser.currentTokenLocation().getColumnNr()); + assertEquals(-1, parser.currentLocation().getLineNr()); + assertEquals(16, parser.currentLocation().getColumnNr()); parser.close(); parser.close(); // Intentional @@ -450,7 +451,7 @@ public void setup(File f) return tempFile; } - @Test(expected = IOException.class) + @Test public void testEnableFeatureAutoCloseSource() throws Exception { @@ -459,7 +460,9 @@ public void testEnableFeatureAutoCloseSource() FileInputStream in = new FileInputStream(tempFile); ObjectMapper objectMapper = new ObjectMapper(factory); objectMapper.readValue(in, new TypeReference>() {}); - objectMapper.readValue(in, new TypeReference>() {}); + assertThrows(IOException.class, () -> { + objectMapper.readValue(in, new TypeReference>() {}); + }); } @Test diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java index 05851dbc..074d7bf5 100755 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java @@ -16,8 +16,8 @@ package org.msgpack.jackson.dataformat; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; import org.msgpack.core.MessageUnpacker; @@ -26,7 +26,7 @@ import java.io.IOException; import java.time.Instant; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TimestampExtensionModuleTest { @@ -46,7 +46,7 @@ private static class TripleInstants public Instant c; } - @Before + @BeforeEach public void setUp() throws Exception { diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java index fea34fd8..8ae73d0b 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.msgpack.jackson.dataformat.MessagePackFactory; import java.io.File; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java index 2713eaea..8042153d 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java @@ -17,7 +17,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.msgpack.jackson.dataformat.MessagePackFactory; import static org.msgpack.jackson.dataformat.MessagePackDataformatTestBase.NormalPojo; import static org.msgpack.jackson.dataformat.MessagePackDataformatTestBase.Suit; diff --git a/plans/2026-05-05-auto-merge.md b/plans/2026-05-05-auto-merge.md new file mode 100644 index 00000000..0b43e132 --- /dev/null +++ b/plans/2026-05-05-auto-merge.md @@ -0,0 +1,84 @@ +# Add auto-merge GitHub Actions workflow + +## Goal + +Add a workflow that automatically enables auto-merge on PRs from trusted bots +(Dependabot and Scala Steward) for non-major version bumps, mirroring the +pattern used in `wvlet/uni/.github/workflows/auto-merge.yml`. + +## Context + +- Recent PR history shows the repo regularly receives many bot PRs: + - `dependabot[bot]` — for GitHub Actions version bumps + - `scala-steward` — for Scala/sbt/Java library updates +- These currently require manual merge from the maintainer. +- Repo settings already allow auto-merge (`allow_auto_merge: true`) and squash + is the default merge method. +- CI passes branch-protection checks on PRs; once a PR is approved and CI is + green, GitHub will merge it automatically. + +## Differences from wvlet/uni + +- **No GitHub App token.** wvlet/uni uses a GitHub App (`APP_ID` + + `APP_PRIVATE_KEY`) to bypass the read-only `GITHUB_TOKEN` that GitHub + hands to `pull_request` workflows triggered by Dependabot. msgpack/msgpack-java + doesn't have that App configured, so this workflow uses + `on: pull_request_target` instead — that event runs in the base-branch + context where the workflow's declared `permissions:` block actually grants + write access to `GITHUB_TOKEN`. We never check out PR head code, so the + usual `pull_request_target` injection risk does not apply. +- **Scala Steward actor is `scala-steward`**, not wvlet/uni's + `scala-steward-wvlet[bot]` or `xerial-bot` (visible in `gh pr list`). +- **Filter on `github.event.pull_request.user.login`**, not `github.actor`, + because under `pull_request_target` the latter can resolve to the merger + rather than the PR author. + +## Plan + +1. Add `.github/workflows/auto-merge.yml` with two jobs: + - **auto-merge-dependabot**: triggers when + `github.event.pull_request.user.login == 'dependabot[bot]'`, uses + `dependabot/fetch-metadata@v2` to read the update type, and runs + `gh pr merge --squash --auto` only when the update is **not** + `version-update:semver-major`. + - **auto-merge-scala-steward**: triggers when + `github.event.pull_request.user.login == 'scala-steward'` and + auto-merges unless the PR carries a `semver-major` or + `early-semver-major` label. +2. Set workflow-level `permissions` to the minimum required: + `contents: write` and `pull-requests: write`. +3. Use `GITHUB_TOKEN` directly via `env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}`. + +### Scala Steward label caveat + +This repo's current Scala Steward PRs only carry `library-update` (and +sometimes `internal`) — no semver labels are configured upstream, so the +`semver-major` / `early-semver-major` guard is effectively a no-op until: +- The labels are added to the repo (Scala Steward only applies labels that + already exist), and +- Scala Steward is configured (e.g. via `.scala-steward.conf`) to attach + them. + +This matches the same caveat in the wvlet/uni reference implementation, +which uses `semver-spec-major` against `github.event.issue.labels` (not even +the right field on a PR event) — so its guard is also effectively a no-op +in practice. We accept the same trade-off here: most Scala Steward PRs are +patch/minor library updates, CI runs across JDK 8/11/17/21/24 and will fail +the merge-readiness checks if anything regresses, and a major bump that +slips through can be reverted. A follow-up could tighten this by setting up +proper semver labels. + +## Out of scope + +- Setting up a GitHub App for elevated bot identity. +- Auto-approving PRs (a human approval may still be required by branch + protection — auto-merge will simply wait for it). +- Changing branch protection rules. +- Configuring Scala Steward to apply semver labels (see caveat above). + +## Validation + +- Lint the YAML by checking the file parses (visual review + actionlint if + available). +- After merge, watch the next dependabot/scala-steward PR to confirm + auto-merge gets enabled. diff --git a/project/build.properties b/project/build.properties index 8fc29878..e0f81fa6 100755 --- a/project/build.properties +++ b/project/build.properties @@ -1,2 +1,2 @@ -sbt.version=1.10.7 +sbt.version=2.0.6 diff --git a/project/plugins.sbt b/project/plugins.sbt index 01d5e0ad..34eae69c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,11 +1,10 @@ -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2") -addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") // TODO: Fixes jacoco error: // java.lang.NoClassDefFoundError: Could not initialize class org.jacoco.core.internal.flow.ClassProbesAdapter //addSbtPlugin("com.github.sbt" % "sbt-jacoco" % "3.3.0") -addSbtPlugin("org.xerial.sbt" % "sbt-jcheckstyle" % "0.2.1") -addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.10.0") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") -addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.0") +addSbtPlugin("org.xerial.sbt" % "sbt-jcheckstyle" % "0.3.0") +addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.11.0-RC1") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") +addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") scalacOptions ++= Seq("-deprecation", "-feature") diff --git a/sonatype.sbt b/sonatype.sbt deleted file mode 100644 index 3fcb592f..00000000 --- a/sonatype.sbt +++ /dev/null @@ -1,18 +0,0 @@ -import xerial.sbt.Sonatype._ - -ThisBuild / sonatypeProfileName := "org.msgpack" -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 / 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")) -)