diff --git a/.config/mise.lock b/.config/mise.lock
new file mode 100644
index 0000000..d3617ba
--- /dev/null
+++ b/.config/mise.lock
@@ -0,0 +1,130 @@
+# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
+
+lockfile_version = 3
+
+[[tools.biome]]
+version = "2.5.15"
+backend = "aqua:biomejs/biome"
+specifiers = ["2"]
+
+[tools.biome."platforms.linux-arm64"]
+checksum = "sha256:a56bcd73ddbfdb0f57a82bb67b7ea698eed4027aff602b95aa18fafe919d59e0"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.15/biome-linux-arm64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/601227632"
+provenance = "github-attestations"
+
+[tools.biome."platforms.linux-arm64-musl"]
+checksum = "sha256:a56bcd73ddbfdb0f57a82bb67b7ea698eed4027aff602b95aa18fafe919d59e0"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.15/biome-linux-arm64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/601227632"
+provenance = "github-attestations"
+
+[tools.biome."platforms.linux-x64"]
+checksum = "sha256:5d867a0b2ccea1755b7e508b01d836a8c64e31a1e4d11ec24b43b6b4eec8b3b6"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.15/biome-linux-x64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/601227626"
+provenance = "github-attestations"
+
+[tools.biome."platforms.linux-x64-musl"]
+checksum = "sha256:5d867a0b2ccea1755b7e508b01d836a8c64e31a1e4d11ec24b43b6b4eec8b3b6"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.15/biome-linux-x64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/601227626"
+provenance = "github-attestations"
+
+[tools.biome."platforms.macos-arm64"]
+checksum = "sha256:3636796d8c78bfcfe0d45946dc93e21e12d27a49eba7401e18af1ea854d25503"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.15/biome-darwin-arm64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/601227628"
+provenance = "github-attestations"
+
+[tools.biome."platforms.macos-x64"]
+checksum = "sha256:86fc618563d19f373ac24eb5b8f1e41bd753a506d5df1f198c152ec365141490"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.15/biome-darwin-x64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/601227623"
+provenance = "github-attestations"
+
+[tools.biome."platforms.windows-x64"]
+checksum = "sha256:f2beadee753b78fe76081158123538be886a31b5e67a34863e42535f3f491926"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.15/biome-win32-x64.exe"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/601227624"
+provenance = "github-attestations"
+
+[[tools.node]]
+version = "26.12.0"
+backend = "core:node"
+specifiers = ["26"]
+
+[tools.node."platforms.linux-arm64"]
+checksum = "sha256:ec53c915c3fdd8786c5685abd25ae3fe9fba0692307f9811d7caaadea1bcaf03"
+url = "https://nodejs.org/dist/v26.12.0/node-v26.12.0-linux-arm64.tar.gz"
+
+[tools.node."platforms.linux-arm64-musl"]
+checksum = "sha256:2e70b6d5adbae94546c4d7e01de7b64c50bf8d3dbfaad2bb0774942e9da902b8"
+url = "https://unofficial-builds.nodejs.org/download/release/v26.12.0/node-v26.12.0-linux-arm64-musl.tar.gz"
+
+[tools.node."platforms.linux-x64"]
+checksum = "sha256:d9f88af72ad34f7ddf8e67289829f1698f54b6254f7d4f54b7453117ff1a3d2c"
+url = "https://nodejs.org/dist/v26.12.0/node-v26.12.0-linux-x64.tar.gz"
+
+[tools.node."platforms.linux-x64-musl"]
+checksum = "sha256:48ba1cdf21e684f13f38b6295f43bfea951f1ffd008b4cf93361c966b33f6938"
+url = "https://unofficial-builds.nodejs.org/download/release/v26.12.0/node-v26.12.0-linux-x64-musl.tar.gz"
+
+[tools.node."platforms.macos-arm64"]
+checksum = "sha256:2a154d4a5458930e0a2d6a1ecf64eddf5a2b3e1c13636b609c2ecb042e8c4553"
+url = "https://nodejs.org/dist/v26.12.0/node-v26.12.0-darwin-arm64.tar.gz"
+
+[tools.node."platforms.macos-x64"]
+checksum = "sha256:878ecede191c7abc1bff01d3fb81f7ed81aa1161040dba18776b8ff8593614b1"
+url = "https://nodejs.org/dist/v26.12.0/node-v26.12.0-darwin-x64.tar.gz"
+
+[tools.node."platforms.windows-x64"]
+checksum = "sha256:50efaf01a08c520b4bbe499c09a8c5a5c370002c419c0d9da2c6815d3066a453"
+url = "https://nodejs.org/dist/v26.12.0/node-v26.12.0-win-x64.zip"
+
+[[tools.pnpm]]
+version = "12.13.0"
+backend = "aqua:pnpm/pnpm"
+specifiers = ["12"]
+
+[tools.pnpm."platforms.linux-arm64"]
+checksum = "sha256:1e1b4d7ddef2577d7b50444a30b76bf4f62dcb45039ef0c4454a377384a36618"
+url = "https://github.com/pnpm/pnpm/releases/download/v12.13.0/pnpm-linux-arm64.tar.gz"
+url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/636319805"
+provenance = "github-attestations"
+
+[tools.pnpm."platforms.linux-arm64-musl"]
+checksum = "sha256:948d410fe403f1cd87d70d618480dc8b97429cd2287c6fb9777ec77f17a851c3"
+url = "https://github.com/pnpm/pnpm/releases/download/v12.13.0/pnpm-linux-arm64-musl.tar.gz"
+url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/636319802"
+provenance = "github-attestations"
+
+[tools.pnpm."platforms.linux-x64"]
+checksum = "sha256:7a796532f2b8801e7fab92389cb02c7ada0cdfbaaded5d291052d83e52c06047"
+url = "https://github.com/pnpm/pnpm/releases/download/v12.13.0/pnpm-linux-x64.tar.gz"
+url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/636319815"
+provenance = "github-attestations"
+
+[tools.pnpm."platforms.linux-x64-musl"]
+checksum = "sha256:402f3df5c7ae51271a13f34bfbdb97ef5aaab0ae359d8ef825574307dc59db13"
+url = "https://github.com/pnpm/pnpm/releases/download/v12.13.0/pnpm-linux-x64-musl.tar.gz"
+url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/636319800"
+provenance = "github-attestations"
+
+[tools.pnpm."platforms.macos-arm64"]
+checksum = "sha256:697961ea2f337fff60bd59ed25991d6165826751bc75ee4b7fad6923b7584c6e"
+url = "https://github.com/pnpm/pnpm/releases/download/v12.13.0/pnpm-darwin-arm64.tar.gz"
+url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/636319811"
+provenance = "github-attestations"
+
+[tools.pnpm."platforms.macos-x64"]
+checksum = "sha256:9803f36f1b7fe08a0e968776699fb10715b29e15a4b92710c25c0c9b5519ffcf"
+url = "https://github.com/pnpm/pnpm/releases/download/v12.13.0/pnpm-darwin-x64.tar.gz"
+url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/636319810"
+provenance = "github-attestations"
+
+[tools.pnpm."platforms.windows-x64"]
+checksum = "sha256:7e235180c70c0fc6f7805af1a19a3c277d7c30ce2db17045f5dd430e37ee582e"
+url = "https://github.com/pnpm/pnpm/releases/download/v12.13.0/pnpm-win32-x64.zip"
+url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/636319814"
+provenance = "github-attestations"
diff --git a/.config/mise.toml b/.config/mise.toml
new file mode 100644
index 0000000..108516e
--- /dev/null
+++ b/.config/mise.toml
@@ -0,0 +1,5 @@
+# Development tools (the published package itself supports Node.js >= 6).
+[tools]
+node = "26"
+pnpm = "12"
+biome = "2"
diff --git a/.gitattributes b/.gitattributes
index 9db71a9..474cb10 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,3 @@
-/test/data/*.properties binary
+* text=auto eol=lf
+# Test fixtures: byte-exact (some use CRLF / mixed line endings on purpose)
+/test/fixtures/*.properties binary
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..afcfa47
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,19 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: /
+ schedule:
+ interval: monthly
+ cooldown:
+ default-days: 7 # like minimumReleaseAge in pnpm-workspace.yaml
+ groups:
+ dev-dependencies:
+ patterns: ['*']
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ groups:
+ github-actions:
+ patterns: ['*']
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..1d25ec2
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,103 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ test:
+ name: Lint, test, build (${{ matrix.os }})
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v7
+ - uses: jdx/mise-action@v5
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm run lint
+ - run: pnpm test
+ - run: pnpm run build
+ - run: node test/compat/check-package.mjs
+ - if: matrix.os == 'ubuntu-latest'
+ uses: actions/upload-artifact@v7
+ with:
+ name: dist
+ path: dist/
+ retention-days: 7
+
+ # The built package must keep working on every supported Node.js version
+ compat-node:
+ name: Node.js ${{ matrix.node }}
+ needs: test
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/download-artifact@v8
+ with:
+ name: dist
+ path: dist/
+ - uses: actions/setup-node@v7
+ with:
+ node-version: ${{ matrix.node }}
+ - run: node test/compat/run-smoke.js
+ - if: matrix.node >= 12
+ run: node test/compat/check-esm.mjs
+
+ # The type declarations must keep working with every supported TypeScript version
+ compat-types:
+ name: TypeScript ${{ matrix.typescript }}
+ needs: test
+ strategy:
+ fail-fast: false
+ matrix:
+ typescript: ['2.9', '3.9', '4.9', '5.9', '6.0', latest]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/download-artifact@v8
+ with:
+ name: dist
+ path: dist/
+ - uses: jdx/mise-action@v5
+ - run: node test/compat/check-types.mjs ${{ matrix.typescript }}
+
+ # Expected results and the javaVersion emulation must match real JVMs (see LOAD_ERAS in src/internal/java-version.ts)
+ java-reference:
+ name: Java reference
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - {id: jdk8, uses: actions/setup-java@v6, with: {distribution: zulu, java-version: '8'}}
+ - {id: jdk11_0_4, uses: actions/setup-java@v6, with: {distribution: zulu, java-version: '11.0.4'}}
+ - {id: jdk11_0_5, uses: actions/setup-java@v6, with: {distribution: zulu, java-version: '11.0.5'}}
+ - {id: jdk11, uses: actions/setup-java@v6, with: {distribution: zulu, java-version: '11'}}
+ - {id: jdk12, uses: actions/setup-java@v6, with: {distribution: zulu, java-version: '12'}}
+ - {id: jdk13, uses: actions/setup-java@v6, with: {distribution: zulu, java-version: '13'}}
+ - {id: jdk17, uses: actions/setup-java@v6, with: {distribution: zulu, java-version: '17'}}
+ - {id: jdk21, uses: actions/setup-java@v6, with: {distribution: zulu, java-version: '21'}}
+ - {id: jdk25, uses: actions/setup-java@v6, with: {distribution: zulu, java-version: '25'}}
+ - run: |
+ echo "JAVA_REFERENCE_JDKS=8=${{ steps.jdk8.outputs.path }}/bin/java,11.0.4=${{ steps.jdk11_0_4.outputs.path }}/bin/java,\
+ 11.0.5=${{ steps.jdk11_0_5.outputs.path }}/bin/java,11=${{ steps.jdk11.outputs.path }}/bin/java,\
+ 12=${{ steps.jdk12.outputs.path }}/bin/java,13=${{ steps.jdk13.outputs.path }}/bin/java,\
+ 17=${{ steps.jdk17.outputs.path }}/bin/java,21=${{ steps.jdk21.outputs.path }}/bin/java,\
+ 25=${{ steps.jdk25.outputs.path }}/bin/java" >> "$GITHUB_ENV"
+ - uses: jdx/mise-action@v5
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm run reference:check
+ - run: pnpm run reference:fuzz -- --runs 5000
diff --git a/.github/workflows/latest-jdks.yml b/.github/workflows/latest-jdks.yml
new file mode 100644
index 0000000..58a9ebd
--- /dev/null
+++ b/.github/workflows/latest-jdks.yml
@@ -0,0 +1,37 @@
+name: Latest JDKs
+
+# A new Java release could change Properties.load() (last change: JDK 13). This checks the committed Java results
+# against the latest GA and early-access JDKs: a failure means a new behavior era (see LOAD_ERAS).
+on:
+ schedule:
+ - cron: '49 1 20 * *' # monthly
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ java-reference:
+ name: Java reference (${{ matrix.jdk }})
+ strategy:
+ fail-fast: false
+ matrix:
+ jdk: [ga, ea]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - if: matrix.jdk == 'ga'
+ uses: oracle-actions/setup-java@v1
+ - if: matrix.jdk == 'ea'
+ uses: oracle-actions/setup-java@v1
+ with: {website: jdk.java.net, release: EA, version: stable}
+ - run: |
+ "$JAVA_HOME/bin/java" -version
+ echo "JAVA_REFERENCE_JDKS=${{ matrix.jdk }}=$JAVA_HOME/bin/java" >> "$GITHUB_ENV"
+ # javac for the reference program (--release 8), independent of the JDK under test
+ - uses: actions/setup-java@v6
+ with: {distribution: zulu, java-version: '25'}
+ - uses: jdx/mise-action@v5
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm run reference:check
+ - run: pnpm run reference:fuzz -- --runs 5000
diff --git a/.github/workflows/typedoc.yml b/.github/workflows/typedoc.yml
deleted file mode 100644
index 37709e1..0000000
--- a/.github/workflows/typedoc.yml
+++ /dev/null
@@ -1,27 +0,0 @@
----
-name: TypeDoc
-
-on:
- push:
- branches:
- - master
-
-jobs:
- typedoc:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Use Node.js v16.x
- uses: actions/setup-node@v2
- with:
- node-version: v16.x
- - name: Generate
- run: |
- npm i -g typedoc
- yarn install --frozen-lockfile
- typedoc
- - name: Deploy
- uses: JamesIves/github-pages-deploy-action@4.1.5
- with:
- branch: typedoc
- folder: ./typedoc
diff --git a/.gitignore b/.gitignore
index d771194..f14183c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,143 +1,16 @@
-### App
-/dist
-/typedoc
-
-### Node
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
+# Dependencies and outputs
node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Optional stylelint cache
-.stylelintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
+/dist/
+/coverage/
+/tools/java-reference/build/
*.tgz
+*.log
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-.env*.local
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-.parcel-cache
-
-# Next.js build output
-.next
-.next-*
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and not Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# TernJS port file
-.tern-port
-
-# Stores VSCode versions used for testing VSCode extensions
-.vscode-test
-
-# gyp build
-/build/
+# Local files, not shared
+.local/
+*.local.*
-### IDE
-# JetBrains IDE
+# Editors and OS
.idea/
-*.iml
-*.iws
-*.ipr
-
-# Eclipse
-.settings/
-.classpath
-.project
-
-# NetBeans
-nbproject/
-nbactions.xml
-nb-configuration.xml
-
-# VS Code
.vscode/
-*.code-workspace
-.history/
+.DS_Store
diff --git a/.prettierrc.js b/.prettierrc.js
deleted file mode 100644
index 5a93b9f..0000000
--- a/.prettierrc.js
+++ /dev/null
@@ -1,14 +0,0 @@
-module.exports = {
- trailingComma: 'es5',
- printWidth: 120,
- tabWidth: 4,
- semi: true,
- singleQuote: true,
- bracketSpacing: false,
- overrides: [
- {
- files: ['*.json', '*.yml'],
- options: {tabWidth: 2},
- },
- ],
-};
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 5766015..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-language: node_js
-node_js:
- - '16'
- - '14'
- - '12'
- - '10'
- - '8'
- - '6'
-install:
- - npm install -g yarn
- - npm install -g codecov
- - yarn install
-script:
- - yarn run test
- - '[[ "$(node -v)" == v6.* ]] && echo skip || codecov'
- - yarn run build
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..4e74be7
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+# Contributing
+
+The goal is to keep a simple project without unnecessary features: please open an issue to discuss a new feature
+before working on it.
+
+## Development
+
+Requirements: Node.js, [pnpm](https://pnpm.io/) and [Biome](https://biomejs.dev/). Their versions are pinned in
+`.config/mise.toml`: with [mise](https://mise.jdx.dev/), `mise install` installs them.
+
+```bash
+mise install # optional: installs the pinned tools
+pnpm install
+pnpm test # unit tests (with coverage)
+pnpm test -u # update the snapshots (test/**/*-snapshot.json) after an intended change
+pnpm run lint # Biome + type-check
+pnpm run test:compat # build, then check the package (test/compat/run-smoke.js also runs on Node.js >= 6)
+```
+
+## Java reference
+
+Java is the reference: the Java results of the test cases (`test/cases/*-expected.json`, `test/fixtures`) are generated
+and checked by running real JVMs, one or more per behavior era of `Properties.load()` (see `src/internal/java-version.ts`). The
+fuzzer compares `parse` and `stringify` with `javaVersion` to each JVM, on random inputs.
+It needs `javac` from a JDK 9+ on the `PATH`, and the JDKs to compare:
+
+```bash
+# JDKs to use (default: java from PATH): name=/path/to/bin/java,...
+export JAVA_REFERENCE_JDKS="8=/opt/jdk8/bin/java,11.0.4=/opt/jdk-11.0.4/bin/java,25=/opt/jdk25/bin/java"
+pnpm run reference:gen # regenerate
+pnpm run reference:check # verify
+pnpm run reference:fuzz # random inputs (builds first; --runs N, --seed N to replay a failure)
+```
+
+A monthly CI job (`.github/workflows/latest-jdks.yml`) also checks the latest GA and early-access JDKs, to catch a
+behavior change in a new Java release.
+
+## Compatibility
+
+Existing users must never break: the runtime behavior, the exports (including `java-props/dist/*` deep imports) and
+the supported environments (Node.js >= 6, ES2015, TypeScript >= 2.9) are checked in CI.
+
+Releases follow [SemVer](https://semver.org/).
diff --git a/README.md b/README.md
index 0389ca2..88dd6ec 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,20 @@
# java-props
[](https://www.npmjs.com/package/java-props)
-
-[](https://app.travis-ci.com/nathan818fr/node-java-props)
-[](https://codecov.io/gh/nathan818fr/node-java-props)
+[](https://github.com/nathan818fr/node-java-props/actions/workflows/ci.yml)
+[](LICENSE)
-Read/Parse Java .properties files (using EXACTLY the same [specification](https://docs.oracle.com/javase/10/docs/api/java/util/Properties.html#load%28java.io.Reader%29)) in Javascript (browser) and Node.js.
+Read and write Java `.properties` files in JavaScript (Node.js and browsers), with the syntax of Java's
+[`Properties.load()`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Properties.html#load(java.io.Reader)),
+or exactly the results of a given Java version.
-This module provides its own TypeScript declarations (.d.ts).
+- Full syntax: `=`, `:` or whitespace separators, `#` and `!` comments, continuation lines, escapes and `\uXXXX`,
+ LF, CRLF and CR line endings
+- With the `javaVersion` option: identical to `Properties.load()` and `store()` of that Java version, checked against
+ real JVMs (Java 8 to 25) on [edge cases](test/cases/parse-cases.mts) and random inputs
+- Any key is safe, including `__proto__`; malformed `\uXXXX` escapes throw, like in Java
+- No dependencies, TypeScript declarations included
+- Node.js >= 6, browsers (ES2015), CommonJS and ESM, TypeScript >= 2.9
## Installation
@@ -15,70 +22,192 @@ This module provides its own TypeScript declarations (.d.ts).
npm install java-props
```
-## Example
+## Usage
+
+```javascript
+import {parse, parseFile, readEntries, stringify} from 'java-props';
+// or: const {parse, parseFile, readEntries, stringify} = require('java-props');
+```
+
+The examples use `javaVersion: 21`, for exactly the results of Java 21 (see [Java versions](#java-versions)). Without
+it, java-props uses its legacy parser.
+
+### Read a file
```properties
-# file.properties
-a = Hello World
-b : Node.js\u00AE
-c value
-d=foo\
- bar
+# config.properties
+app.name = My App
+app.title : Caf\u00e9 \u2615
+app.greeting Hello, \
+ World!
+db.url=jdbc:postgresql://localhost:5432/app
+install.dir = C:\\Program Files\\App
+key\ with\ spaces = value
+! this is a comment too
```
```javascript
-const javaProps = require('java-props');
-
-javaProps
- .parseFile('./file.properties')
- .then((props) => {
- console.log(props);
- // { a: 'Hello World', b: 'Node.js®', c: 'value', d: 'foobar' }
- })
- .catch((err) => {
- console.error(err);
- });
+const props = await parseFile('config.properties', {javaVersion: 21});
+// {
+// 'app.name': 'My App',
+// 'app.title': 'Café ☕',
+// 'app.greeting': 'Hello, World!',
+// 'db.url': 'jdbc:postgresql://localhost:5432/app',
+// 'install.dir': 'C:\\Program Files\\App',
+// 'key with spaces': 'value'
+// }
```
-## Documentation
+### Parse a string
-For a detailed API reference, see:
-[node-java-props.nathan818.fr](https://node-java-props.nathan818.fr/modules.html)
+```javascript
+parse('a = 1\nb : 2', {javaVersion: 21});
+// { a: '1', b: '2' }
+```
-## Building
+### Read entries one by one
-This project uses TypeScript. To create javascript sources run:
+For the order of the entries, duplicate keys, the raw text, or positions:
-```sh
-yarn run build
+```javascript
+const reader = readEntries('b = 1\na = 2\nb = 3', {javaVersion: 21, positions: true});
+while (reader.next()) {
+ console.log(reader.key, reader.value, reader.startLine);
+}
+// b 1 1
+// a 2 2
+// b 3 3
```
-## Testing
+### Stringify
-To run the test suite, first install the dependencies, then run `yarn test`:
+```javascript
+stringify({'key with spaces': 'Café ☕', 'line.break': 'a\nb'}, {javaVersion: 21});
+```
-```bash
-yarn install
-yarn test
+```properties
+#Thu Jan 15 14:04:05 CET 2026
+key\ with\ spaces=Café ☕
+line.break=a\nb
```
-## Contributing
+Like `Properties.store(Writer)`, non-ASCII characters are written as-is (save it as UTF-8). With `escapeUnicode: true`,
+they are written as `\uXXXX`, like `store(OutputStream)`.
+
+## API
+
+### `parse(str, options?)`
+
+Parses a .properties string and returns its properties, as an object with a `null` prototype (any key is safe,
+including `__proto__`). Throws an `Error` on malformed `\uXXXX` escapes (where Java throws an
+`IllegalArgumentException`), and a `TypeError` or a `RangeError` on an invalid `javaVersion`.
+
+| Option | Default | Description |
+| ------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `javaVersion` | legacy parser | Parse exactly like `Properties.load()` of this [Java version](#java-versions): `21`, `'11.0.4'`, `'1.8.0_392'`, `'latest'`... A number is the cheapest form |
+
+### `parseFile(path, encoding?, options?)`, `parseFile(path, options?)`
+
+_Node.js only._ Reads and parses a .properties file, and returns a `Promise` of its properties. `path` is a file path
+(`string`, `Buffer` or `URL`) or a file descriptor.
-Contributions are welcome.
+| Option | Default | Description |
+| ------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
+| `javaVersion` | | See `parse` |
+| `encoding` | `'utf8'` | File encoding (also the second argument): `'latin1'` for files written by `Properties.store(OutputStream)` |
-The goal is to keep a simple project without unnecessary (non essential) features.
-It is recommended to open an issue before introducing new features to discuss them.
+### `readEntries(str, options?)`
-## Versioning
+Reads the entries of a .properties string one at a time, in order (duplicate keys included). Nothing is decoded until
+it is accessed, so reading only some entries, or only their raw text, is fast.
-We use [SemVer](http://semver.org/) for versioning.
-For the versions available, see the [tags on this repository](https://github.com/nathan818fr/node-java-props/tags).
+```javascript
+const reader = readEntries(str, {javaVersion: 21, positions: true});
+while (reader.next()) {
+ reader.key; // decoded key (throws an Error on malformed escapes, like parse)
+ reader.value; // decoded value
+ reader.rawKey; // key with its escape sequences
+ reader.rawValue; // value with its escape sequences
+ reader.line; // logical line (continuation lines joined)
+ reader.start; // with `positions`: offsets of the entry in `str` (start, end),
+ reader.end; // and line and column of its start (from 1)
+ reader.startLine;
+ reader.startColumn;
+}
+```
+
+| Option | Default | Description |
+| ------------- | ------- | ----------------------------------------------------- |
+| `javaVersion` | | See `parse` |
+| `positions` | `false` | Compute `start`, `end`, `startLine` and `startColumn` |
+
+### `stringify(props, options?)`
+
+Converts an object of string values (own enumerable properties only) to a .properties string, which Java reads back as
+the same keys and values. With options, throws a `TypeError` if a value is not a string.
+
+| Option | Default | With `javaVersion` | Description |
+| --------------------- | ------------- | ---------------------------- | ---------------------------------------------------------------------------- |
+| `javaVersion` | | | Write like `Properties.store(Writer)` of this Java version (see `parse`) |
+| `escapeUnicode` | `true` | `false` | Escape non-ASCII chars as `\uXXXX`, like `store(OutputStream)` |
+| `separator` | `': '` | `'='` | `'='`, `':'`, `' '`, `': '` or `' = '` |
+| `comments` | | | Comments written first, like the `comments` argument of `store()` |
+| `date` | `false` | `true` | Date comment: `true` (now), a `Date`, or a string (written as a comment) |
+| `timeZone` | system | system | Time zone of the date (IANA name, `'UTC'` or a fixed offset like `'+02:00'`) |
+| `order` | `'insertion'` | `'sorted'` | `'insertion'` (JavaScript order: integer-like keys first) or `'sorted'` |
+| `lineSeparator` | `'\n'` | system (`'\r\n'` on Windows) | `'\n'`, `'\r\n'` or `'\r'` |
+| `escapeValueSpecials` | `false` | `true` | Also escape `=`, `:`, `#` and `!` in values |
+
+Dates are written like Java's `Date.toString()`. Time zone names come from `Intl`, which doesn't always use Java's
+names: the output is exact for `'UTC'` and fixed offsets, and a `date` string makes it reproducible.
+
+### `escapeKey(key, options?)`, `escapeValue(value, options?)`
+
+Escape a key or a value, like `stringify`.
+
+| Option | Default | With `javaVersion` | Description |
+| --------------------- | ------- | ------------------ | -------------------------------------------------------------------------- |
+| `javaVersion` | | | Escape like `Properties.store(Writer)` of this Java version (see `parse`) |
+| `escapeUnicode` | `true` | `false` | Escape non-ASCII chars as `\uXXXX`, like `store(OutputStream)` |
+| `escapeValueSpecials` | `false` | `true` | `escapeValue`: also escape `=`, `:`, `#` and `!` (`escapeKey` always does) |
-## Authors
+
+Deprecated APIs
+
+The default export, and the functions of `java-props/dist/utils` (which keep the original behavior, without
+`javaVersion`), are deprecated. Their replacements:
+
+| Deprecated | Replacement |
+| ------------------------------------------------------ | ---------------------------------------------------------- |
+| Default export (`javaProps.default`) | Named exports (`parse`, `parseFile`, `stringify`) |
+| `rawParse(str, ({line, sepStart, valueStart}) => ...)` | `readEntries(str)`: `reader.line`, `rawKey` and `rawValue` |
+| `new LineReader(str).readLine()` | `readEntries(str)`: `reader.next()` and `reader.line` |
+| `rawParseLine(line)` | `reader.rawKey`, `reader.rawValue` |
+| `decodeLine(str)`, `convertLine(str)` | `reader.key`, `reader.value` |
+| `encodeLine(str)`, `encodeLine(str, true)` | `escapeValue(str)`, `escapeKey(str)` |
+
+
+
+## Java versions
+
+`Properties.load()` changed in a few Java versions, on unusual inputs only: some combinations of backslashes and
+comments, and truncated `\uXXXX` escapes. The `javaVersion` option picks the behavior:
+
+| `javaVersion` | Behavior |
+| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
+| not set | Legacy: the original java-props parser, close to Java 8 |
+| `6` to `8` | Java 6 to 8 |
+| `9`, `10`, `12`, `'11.0.0'` to `'11.0.4'` | Changed in Java 9: comments are skipped faster, which changed what some malformed lines read |
+| `11` (`'11.0.5'` and later) | Changed in Java 11.0.5, not in 12: [a rewritten line reader](https://bugs.openjdk.org/browse/JDK-8224202) |
+| `13` and later, `'latest'` | Changed in Java 13: [a truncated `\uXXXX` at the end of a line throws](https://bugs.openjdk.org/browse/JDK-8224240) |
+
+A major version alone means its latest update. For `stringify`, `javaVersion` only sets the defaults of the options,
+the same for every version.
+
+## Contributing
-- [Nathan Poirier](https://github.com/nathan818fr) <nathan@poirier.io>
+Contributions are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
## License
-This project is licensed under the MIT License.
-See the [LICENSE](https://github.com/nathan818fr/node-java-props/blob/master/LICENSE) file for details.
+[MIT](LICENSE)
diff --git a/biome.jsonc b/biome.jsonc
new file mode 100644
index 0000000..868d3de
--- /dev/null
+++ b/biome.jsonc
@@ -0,0 +1,49 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
+ "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
+ "files": {
+ // Also skips git-ignored files. Generated files keep their JSON.stringify formatting.
+ "includes": ["**", "!test/**/*-snapshot.json", "!test/cases/*-expected.json"]
+ },
+ "formatter": {
+ "indentStyle": "space",
+ "indentWidth": 4,
+ "lineWidth": 120,
+ "lineEnding": "lf"
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single",
+ "bracketSpacing": false,
+ "trailingCommas": "es5",
+ "semicolons": "always",
+ "arrowParentheses": "always"
+ }
+ },
+ "json": { "formatter": { "indentWidth": 2 } },
+ "linter": {
+ "rules": {
+ "preset": "recommended",
+ "style": { "useTemplate": "off" }
+ }
+ },
+ "assist": { "actions": { "source": { "organizeImports": "off" } } },
+ "overrides": [
+ {
+ // Code for Node.js >= 6 (check-esm.mjs: >= 12): no `node:` imports (Node >= 14.18), no `Object.hasOwn` (ES2022).
+ // The emitted .d.ts is read by TypeScript >= 2.9: no inline `type` imports (TS >= 4.5).
+ "includes": ["src/**", "test/compat/run-smoke.js", "test/compat/check-esm.mjs"],
+ "linter": {
+ "rules": {
+ "style": { "useNodejsImportProtocol": "off", "useImportType": "off" },
+ "suspicious": { "noPrototypeBuiltins": "off" }
+ }
+ }
+ },
+ {
+ // Fixture compiled by old TypeScript versions (2.9+): old syntax and unused variables on purpose.
+ "includes": ["test/compat/types/**"],
+ "linter": { "enabled": false }
+ }
+ ]
+}
diff --git a/jest.config.js b/jest.config.js
deleted file mode 100644
index f690be0..0000000
--- a/jest.config.js
+++ /dev/null
@@ -1,13 +0,0 @@
-module.exports = {
- globals: {
- 'ts-jest': {
- tsConfig: 'tsconfig.json',
- },
- },
- moduleFileExtensions: ['ts', 'js'],
- transform: {
- '^.+\\.(ts|tsx)$': 'ts-jest',
- },
- testMatch: ['**/test/**/*.test.(ts|js)'],
- testEnvironment: 'node',
-};
diff --git a/package.json b/package.json
index 39d8041..ecfacbe 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "java-props",
"version": "2.3.1",
- "description": "Read Java .properties files (using the same specification), without useless additional features.",
+ "description": "Read and write Java .properties files, with the syntax of Java's Properties.load().",
"author": "Nathan Poirier ",
"license": "MIT",
"keywords": [
@@ -13,33 +13,32 @@
],
"repository": {
"type": "git",
- "url": "https://github.com/nathan818fr/node-java-props.git"
+ "url": "git+https://github.com/nathan818fr/node-java-props.git"
},
"files": [
"dist/**"
],
"main": "dist/node-java-props.js",
"browser": "dist/java-props.js",
+ "sideEffects": false,
"scripts": {
- "clean": "rimraf ./dist",
- "build": "yarn run clean && yarn run build:ts",
- "build:ts": "tsc",
- "lint": "tslint -c ./tslint.json -p ./tsconfig.json",
- "format": "prettier -w ./src \"./test/**.ts\" \"*.md\" \"*.json\" \"*.js\" \"*.yml\"",
- "test": "jest --coverage --verbose",
- "test:watch": "yarn run test -- --watchAll",
- "release": "yarn run lint && yarn run build && np"
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
+ "build": "pnpm run clean && tsc -p tsconfig.build.json",
+ "lint": "biome check && tsc -p tsconfig.json",
+ "format": "biome check --write",
+ "test": "vitest run --coverage",
+ "test:watch": "vitest",
+ "pretest:compat": "pnpm run build",
+ "test:compat": "node test/compat/run-smoke.js && node test/compat/check-esm.mjs && node test/compat/check-package.mjs",
+ "reference:gen": "node tools/gen-expected.mts",
+ "reference:check": "node tools/gen-expected.mts --check",
+ "reference:fuzz": "pnpm run build && node tools/fuzz.mts"
},
"devDependencies": {
- "@types/jest": "^24.0.18",
- "@types/node": "^12.7.11",
- "jest": "^24.9.0",
- "rimraf": "^3.0.2",
- "ts-jest": "^24.3.0",
- "tslint": "^5.20.1",
- "typescript": "^3.9.10"
- },
- "resolutions": {
- "stack-utils": "1.0.2"
+ "@types/node": "^26.6.5",
+ "@vitest/coverage-v8": "^5.0.4",
+ "fast-check": "^4.10.2",
+ "typescript": "^7.0.2",
+ "vitest": "^5.0.4"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..615531b
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,978 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ devDependencies:
+ '@types/node':
+ specifier: ^26.6.5
+ version: 26.6.5
+ '@vitest/coverage-v8':
+ specifier: ^5.0.4
+ version: 5.0.4(vitest@5.0.4)
+ fast-check:
+ specifier: ^4.10.2
+ version: 4.10.2
+ typescript:
+ specifier: ^7.0.2
+ version: 7.0.2
+ vitest:
+ specifier: ^5.0.4
+ version: 5.0.4(@types/node@26.6.5)(@vitest/coverage-v8@5.0.4)(vite@8.3.4(@types/node@26.6.5))
+
+packages:
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.9':
+ resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@bcoe/v8-coverage@1.0.2':
+ resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
+ engines: {node: '>=18'}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@oxc-project/types@0.153.0':
+ resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==}
+
+ '@rolldown/binding-android-arm-eabi@1.2.13':
+ resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@rolldown/binding-android-arm64@1.2.13':
+ resolution: {integrity: sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.2.13':
+ resolution: {integrity: sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.2.13':
+ resolution: {integrity: sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.2.13':
+ resolution: {integrity: sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.13':
+ resolution: {integrity: sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.13':
+ resolution: {integrity: sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.2.13':
+ resolution: {integrity: sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.13':
+ resolution: {integrity: sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.13':
+ resolution: {integrity: sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.2.13':
+ resolution: {integrity: sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.2.13':
+ resolution: {integrity: sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.2.13':
+ resolution: {integrity: sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.13':
+ resolution: {integrity: sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.2.13':
+ resolution: {integrity: sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/node@26.6.5':
+ resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==}
+
+ '@typescript/typescript-aix-ppc64@7.0.2':
+ resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==}
+ engines: {node: '>=16.20.0'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@typescript/typescript-darwin-arm64@7.0.2':
+ resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==}
+ engines: {node: '>=16.20.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@typescript/typescript-darwin-x64@7.0.2':
+ resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==}
+ engines: {node: '>=16.20.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@typescript/typescript-freebsd-arm64@7.0.2':
+ resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==}
+ engines: {node: '>=16.20.0'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@typescript/typescript-freebsd-x64@7.0.2':
+ resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==}
+ engines: {node: '>=16.20.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@typescript/typescript-linux-arm64@7.0.2':
+ resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==}
+ engines: {node: '>=16.20.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@typescript/typescript-linux-arm@7.0.2':
+ resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==}
+ engines: {node: '>=16.20.0'}
+ cpu: [arm]
+ os: [linux]
+
+ '@typescript/typescript-linux-loong64@7.0.2':
+ resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==}
+ engines: {node: '>=16.20.0'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@typescript/typescript-linux-mips64el@7.0.2':
+ resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==}
+ engines: {node: '>=16.20.0'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@typescript/typescript-linux-ppc64@7.0.2':
+ resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==}
+ engines: {node: '>=16.20.0'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@typescript/typescript-linux-riscv64@7.0.2':
+ resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==}
+ engines: {node: '>=16.20.0'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@typescript/typescript-linux-s390x@7.0.2':
+ resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==}
+ engines: {node: '>=16.20.0'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@typescript/typescript-linux-x64@7.0.2':
+ resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==}
+ engines: {node: '>=16.20.0'}
+ cpu: [x64]
+ os: [linux]
+
+ '@typescript/typescript-netbsd-arm64@7.0.2':
+ resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==}
+ engines: {node: '>=16.20.0'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@typescript/typescript-netbsd-x64@7.0.2':
+ resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==}
+ engines: {node: '>=16.20.0'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@typescript/typescript-openbsd-arm64@7.0.2':
+ resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==}
+ engines: {node: '>=16.20.0'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@typescript/typescript-openbsd-x64@7.0.2':
+ resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==}
+ engines: {node: '>=16.20.0'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@typescript/typescript-sunos-x64@7.0.2':
+ resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==}
+ engines: {node: '>=16.20.0'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@typescript/typescript-win32-arm64@7.0.2':
+ resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==}
+ engines: {node: '>=16.20.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@typescript/typescript-win32-x64@7.0.2':
+ resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==}
+ engines: {node: '>=16.20.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@vitest/coverage-v8@5.0.4':
+ resolution: {integrity: sha512-P5YcQY8Y6RmoE9xRXI38rT8pssbcubEzqtYTM2XQoxOXMDn04nRaTI1SM8qCl9/NfW3ag9GlzdSXu7IwO5Ft8A==}
+ peerDependencies:
+ '@vitest/browser': 5.0.4
+ vitest: 5.0.4
+ peerDependenciesMeta:
+ '@vitest/browser':
+ optional: true
+
+ '@vitest/istanbul-lib-coverage@1.0.2':
+ resolution: {integrity: sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==}
+ engines: {node: '>=22'}
+
+ '@vitest/istanbul-lib-report@1.0.2':
+ resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==}
+ engines: {node: '>=22'}
+
+ '@vitest/mocker@5.0.4':
+ resolution: {integrity: sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==}
+ peerDependencies:
+ msw: ^2.4.9 || ^3.0.0
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/spy@5.0.4':
+ resolution: {integrity: sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ ast-v8-to-istanbul@1.0.7:
+ resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==}
+
+ chai@6.3.0:
+ resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==}
+ engines: {node: '>=18'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ es-module-lexer@2.3.2:
+ resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+ engines: {node: '>=12.0.0'}
+
+ fast-check@4.10.2:
+ resolution: {integrity: sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==}
+ engines: {node: '>=12.17.0'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ js-tokens@10.0.0:
+ resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
+
+ lightningcss-android-arm64@1.33.0:
+ resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.33.0:
+ resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.33.0:
+ resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.33.0:
+ resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.33.0:
+ resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.33.0:
+ resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
+ engines: {node: '>= 12.0.0'}
+
+ magic-string@1.4.3:
+ resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==}
+
+ magicast@0.5.5:
+ resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==}
+
+ nanoid@3.3.20:
+ resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ obug@3.0.0:
+ resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==}
+ engines: {node: '>=12.20.0'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.7:
+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.30:
+ resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ pure-rand@8.4.2:
+ resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==}
+
+ rolldown@1.2.13:
+ resolution: {integrity: sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
+ engines: {node: '>=0.10.0'}
+
+ std-env@4.3.0:
+ resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
+
+ tinybench@6.2.2:
+ resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
+ engines: {node: '>=20.0.0'}
+
+ tinyexec@1.3.1:
+ resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.2.0:
+ resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==}
+ engines: {node: '>=14.0.0'}
+
+ typescript@7.0.2:
+ resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
+ engines: {node: '>=16.20.0'}
+ hasBin: true
+
+ undici-types@8.9.0:
+ resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
+
+ vite@8.3.4:
+ resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.7.1
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@5.0.4:
+ resolution: {integrity: sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==}
+ engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 5.0.4
+ '@vitest/browser-preview': 5.0.4
+ '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
+ '@vitest/coverage-istanbul': 5.0.4
+ '@vitest/coverage-v8': 5.0.4
+ '@vitest/ui': 5.0.4
+ happy-dom: '*'
+ jsdom: '*'
+ vite: ^6.4.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ why-is-node-running@3.2.1:
+ resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
+ engines: {node: '>=20.11'}
+ hasBin: true
+
+snapshots:
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/parser@7.29.9':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@bcoe/v8-coverage@1.0.2': {}
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ '@oxc-project/types@0.153.0': {}
+
+ '@rolldown/binding-android-arm-eabi@1.2.13':
+ optional: true
+
+ '@rolldown/binding-android-arm64@1.2.13':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.2.13':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.2.13':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.2.13':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.13':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.13':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.2.13':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.13':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.13':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.2.13':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.2.13':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.2.13':
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.13':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.2.13':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/node@26.6.5':
+ dependencies:
+ undici-types: 8.9.0
+
+ '@typescript/typescript-aix-ppc64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-darwin-arm64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-darwin-x64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-freebsd-arm64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-freebsd-x64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-linux-arm64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-linux-arm@7.0.2':
+ optional: true
+
+ '@typescript/typescript-linux-loong64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-linux-mips64el@7.0.2':
+ optional: true
+
+ '@typescript/typescript-linux-ppc64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-linux-riscv64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-linux-s390x@7.0.2':
+ optional: true
+
+ '@typescript/typescript-linux-x64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-netbsd-arm64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-netbsd-x64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-openbsd-arm64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-openbsd-x64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-sunos-x64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-win32-arm64@7.0.2':
+ optional: true
+
+ '@typescript/typescript-win32-x64@7.0.2':
+ optional: true
+
+ '@vitest/coverage-v8@5.0.4(vitest@5.0.4)':
+ dependencies:
+ '@bcoe/v8-coverage': 1.0.2
+ '@vitest/istanbul-lib-coverage': 1.0.2
+ '@vitest/istanbul-lib-report': 1.0.2
+ ast-v8-to-istanbul: 1.0.7
+ magicast: 0.5.5
+ obug: 3.0.0
+ std-env: 4.3.0
+ tinyrainbow: 3.2.0
+ vitest: 5.0.4(@types/node@26.6.5)(@vitest/coverage-v8@5.0.4)(vite@8.3.4(@types/node@26.6.5))
+
+ '@vitest/istanbul-lib-coverage@1.0.2': {}
+
+ '@vitest/istanbul-lib-report@1.0.2':
+ dependencies:
+ '@vitest/istanbul-lib-coverage': 1.0.2
+
+ '@vitest/mocker@5.0.4(vite@8.3.4(@types/node@26.6.5))':
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ '@vitest/spy': 5.0.4
+ estree-walker: 3.0.3
+ magic-string: 1.4.3
+ optionalDependencies:
+ vite: 8.3.4(@types/node@26.6.5)
+
+ '@vitest/spy@5.0.4': {}
+
+ assertion-error@2.0.1: {}
+
+ ast-v8-to-istanbul@1.0.7:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ estree-walker: 3.0.3
+ js-tokens: 10.0.0
+
+ chai@6.3.0: {}
+
+ detect-libc@2.1.2: {}
+
+ es-module-lexer@2.3.2: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ expect-type@1.4.0: {}
+
+ fast-check@4.10.2:
+ dependencies:
+ pure-rand: 8.4.2
+
+ fdir@6.5.0(picomatch@4.0.7):
+ optionalDependencies:
+ picomatch: 4.0.7
+
+ fsevents@2.3.3:
+ optional: true
+
+ js-tokens@10.0.0: {}
+
+ lightningcss-android-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.33.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.33.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ optional: true
+
+ lightningcss@1.33.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.33.0
+ lightningcss-darwin-arm64: 1.33.0
+ lightningcss-darwin-x64: 1.33.0
+ lightningcss-freebsd-x64: 1.33.0
+ lightningcss-linux-arm-gnueabihf: 1.33.0
+ lightningcss-linux-arm64-gnu: 1.33.0
+ lightningcss-linux-arm64-musl: 1.33.0
+ lightningcss-linux-x64-gnu: 1.33.0
+ lightningcss-linux-x64-musl: 1.33.0
+ lightningcss-win32-arm64-msvc: 1.33.0
+ lightningcss-win32-x64-msvc: 1.33.0
+
+ magic-string@1.4.3:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ magicast@0.5.5:
+ dependencies:
+ '@babel/parser': 7.29.9
+ '@babel/types': 7.29.8
+ source-map-js: 1.2.2
+
+ nanoid@3.3.20: {}
+
+ obug@3.0.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.7: {}
+
+ postcss@8.5.30:
+ dependencies:
+ nanoid: 3.3.20
+ picocolors: 1.1.1
+ source-map-js: 1.2.2
+
+ pure-rand@8.4.2: {}
+
+ rolldown@1.2.13:
+ dependencies:
+ '@oxc-project/types': 0.153.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm-eabi': 1.2.13
+ '@rolldown/binding-android-arm64': 1.2.13
+ '@rolldown/binding-darwin-arm64': 1.2.13
+ '@rolldown/binding-darwin-x64': 1.2.13
+ '@rolldown/binding-freebsd-x64': 1.2.13
+ '@rolldown/binding-linux-arm-gnueabihf': 1.2.13
+ '@rolldown/binding-linux-arm64-gnu': 1.2.13
+ '@rolldown/binding-linux-arm64-musl': 1.2.13
+ '@rolldown/binding-linux-ppc64-gnu': 1.2.13
+ '@rolldown/binding-linux-s390x-gnu': 1.2.13
+ '@rolldown/binding-linux-x64-gnu': 1.2.13
+ '@rolldown/binding-linux-x64-musl': 1.2.13
+ '@rolldown/binding-openharmony-arm64': 1.2.13
+ '@rolldown/binding-win32-arm64-msvc': 1.2.13
+ '@rolldown/binding-win32-x64-msvc': 1.2.13
+
+ source-map-js@1.2.2: {}
+
+ std-env@4.3.0: {}
+
+ tinybench@6.2.2: {}
+
+ tinyexec@1.3.1: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+
+ tinyrainbow@3.2.0: {}
+
+ typescript@7.0.2:
+ optionalDependencies:
+ '@typescript/typescript-aix-ppc64': 7.0.2
+ '@typescript/typescript-darwin-arm64': 7.0.2
+ '@typescript/typescript-darwin-x64': 7.0.2
+ '@typescript/typescript-freebsd-arm64': 7.0.2
+ '@typescript/typescript-freebsd-x64': 7.0.2
+ '@typescript/typescript-linux-arm': 7.0.2
+ '@typescript/typescript-linux-arm64': 7.0.2
+ '@typescript/typescript-linux-loong64': 7.0.2
+ '@typescript/typescript-linux-mips64el': 7.0.2
+ '@typescript/typescript-linux-ppc64': 7.0.2
+ '@typescript/typescript-linux-riscv64': 7.0.2
+ '@typescript/typescript-linux-s390x': 7.0.2
+ '@typescript/typescript-linux-x64': 7.0.2
+ '@typescript/typescript-netbsd-arm64': 7.0.2
+ '@typescript/typescript-netbsd-x64': 7.0.2
+ '@typescript/typescript-openbsd-arm64': 7.0.2
+ '@typescript/typescript-openbsd-x64': 7.0.2
+ '@typescript/typescript-sunos-x64': 7.0.2
+ '@typescript/typescript-win32-arm64': 7.0.2
+ '@typescript/typescript-win32-x64': 7.0.2
+
+ undici-types@8.9.0: {}
+
+ vite@8.3.4(@types/node@26.6.5):
+ dependencies:
+ lightningcss: 1.33.0
+ picomatch: 4.0.7
+ postcss: 8.5.30
+ rolldown: 1.2.13
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 26.6.5
+ fsevents: 2.3.3
+
+ vitest@5.0.4(@types/node@26.6.5)(@vitest/coverage-v8@5.0.4)(vite@8.3.4(@types/node@26.6.5)):
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/mocker': 5.0.4(vite@8.3.4(@types/node@26.6.5))
+ chai: 6.3.0
+ es-module-lexer: 2.3.2
+ expect-type: 1.4.0
+ magic-string: 1.4.3
+ obug: 3.0.0
+ picomatch: 4.0.7
+ std-env: 4.3.0
+ tinybench: 6.2.2
+ tinyexec: 1.3.1
+ tinyglobby: 0.2.17
+ vite: 8.3.4(@types/node@26.6.5)
+ why-is-node-running: 3.2.1
+ optionalDependencies:
+ '@types/node': 26.6.5
+ '@vitest/coverage-v8': 5.0.4(vitest@5.0.4)
+ transitivePeerDependencies:
+ - msw
+
+ why-is-node-running@3.2.1: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..1562b6f
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+# pnpm settings (single-package repository)
+minimumReleaseAge: 10080 # minutes (7 days): don't install freshly published versions
diff --git a/src/internal/entry-reader.ts b/src/internal/entry-reader.ts
new file mode 100644
index 0000000..a0e744a
--- /dev/null
+++ b/src/internal/entry-reader.ts
@@ -0,0 +1,116 @@
+// Implementation of the EntryReader returned by readEntries() (see java-props.ts for the documented interface).
+import {JavaLineReader, javaDecode, splitLine} from './java-load';
+import {LoadEra} from './java-version';
+import {LegacyLineReader, legacyDecode} from './legacy-load';
+
+export class EntryReaderImpl {
+ private current: string | undefined; // logical line of the current entry
+ private sepStart = 0;
+ private valueStart = 0;
+ private decodedKey: string | undefined;
+ private decodedValue: string | undefined;
+ private readonly str: string;
+ private readonly era: LoadEra | undefined;
+ private readonly java: JavaLineReader | undefined;
+ private readonly legacy: LegacyLineReader | undefined;
+ private readonly track: boolean;
+ // Line counting for startLine/startColumn, done lazily up to the entries' starts (always forward)
+ private counted = 0;
+ private countedLines = 1;
+ private countedLineStart = 0;
+ private countedAfterCR = false;
+
+ public constructor(str: string, era: LoadEra | undefined, track: boolean) {
+ this.str = str;
+ this.era = era;
+ this.track = track;
+ if (era === undefined) {
+ this.legacy = new LegacyLineReader(str, track);
+ } else {
+ this.java = new JavaLineReader(str, era, track);
+ }
+ }
+
+ public next(): boolean {
+ this.decodedKey = undefined;
+ this.decodedValue = undefined;
+ this.current = this.java ? this.java.readLine() : (this.legacy as LegacyLineReader).readLine();
+ if (this.current === undefined) return false;
+ const split = splitLine(this.current);
+ this.sepStart = split.sepStart;
+ this.valueStart = split.valueStart;
+ return true;
+ }
+
+ public get line(): string {
+ return this.current as string;
+ }
+
+ public get rawKey(): string {
+ return (this.current === undefined ? undefined : this.current.substring(0, this.sepStart)) as string;
+ }
+
+ public get rawValue(): string {
+ return (this.current === undefined ? undefined : this.current.substring(this.valueStart)) as string;
+ }
+
+ public get key(): string {
+ if (this.decodedKey === undefined && this.current !== undefined) {
+ // Java can read past the key: into the rest of the line
+ this.decodedKey = this.decode(this.rawKey, this.sepStart);
+ }
+ return this.decodedKey as string;
+ }
+
+ public get value(): string {
+ if (this.decodedValue === undefined && this.current !== undefined) {
+ this.decodedValue = this.decode(this.rawValue, this.current.length);
+ }
+ return this.decodedValue as string;
+ }
+
+ public get start(): number {
+ return this.position(this.java ? this.java.start : (this.legacy as LegacyLineReader).start);
+ }
+
+ public get end(): number {
+ return this.position(this.java ? this.java.end : (this.legacy as LegacyLineReader).end);
+ }
+
+ public get startLine(): number {
+ return this.position(this.countLines() ? this.countedLines : 0);
+ }
+
+ public get startColumn(): number {
+ return this.position(this.countLines() ? this.start - this.countedLineStart + 1 : 0);
+ }
+
+ private position(value: number): number {
+ return (this.track && this.current !== undefined ? value : undefined) as number;
+ }
+
+ // Counts the line terminators (CR, LF, CRLF) up to the start of the current entry
+ private countLines(): boolean {
+ if (!this.track || this.current === undefined) return false;
+ const str = this.str;
+ const start = this.start;
+ for (let i = this.counted; i < start; i++) {
+ const c = str[i];
+ if (c === '\r' || (c === '\n' && !this.countedAfterCR)) this.countedLines++;
+ if (c === '\r' || c === '\n') this.countedLineStart = i + 1;
+ this.countedAfterCR = c === '\r';
+ }
+ if (start > this.counted) this.counted = start;
+ return true;
+ }
+
+ // Decodes a key or a value, followed in the line by what starts at `followedFrom`. In Java mode, decoding can also
+ // read past the line: leftovers of Java's line buffer (still the one of this entry). It only reads past `raw` after
+ // a backslash.
+ private decode(raw: string, followedFrom: number): string {
+ if (!this.java) return legacyDecode(raw);
+ const followedBy =
+ raw.indexOf('\\') < 0 ? '' : (this.current as string).substring(followedFrom) + this.java.tail();
+ return javaDecode(raw, followedBy, this.era as LoadEra);
+ }
+}
diff --git a/src/internal/java-load.ts b/src/internal/java-load.ts
new file mode 100644
index 0000000..db25896
--- /dev/null
+++ b/src/internal/java-load.ts
@@ -0,0 +1,303 @@
+import {LoadEra} from './java-version';
+
+// Exact ports of Properties.LineReader.readLine() and Properties.loadConvert(), for each load era.
+
+/**
+ * Properties.LineReader.readLine() of a load era. Like Java, it keeps a single line buffer: lines overwrite the start
+ * of it, so it can hold stale content of longer previous lines after the current one (or '\0' where nothing was ever
+ * written), that Java's decoding reads in some malformed inputs (see {@link tail}).
+ */
+export class JavaLineReader {
+ public pos = 0;
+ /** With `track`: offset of the first char of the last line (of its continuation backslash, if empty) */
+ public start = -1;
+ /** With `track`: offset of the end of the last line (its line terminator, or the end of the input) */
+ public end = -1;
+ // Java's line buffer, as what each line wrote to it, from the longest to the last one: as lines overwrite its start,
+ // only longer previous lines still show after a line
+ private written: string[] = [];
+ private text = ''; // the last line
+ private limit = 0; // end of the current 8192-char read chunk (Java reads its input by chunks)
+ private readonly rewritten: boolean; // JDK-8224202 (JDK 11.0.5+ and 13+)
+ private readonly str: string;
+ private readonly era: LoadEra;
+ private readonly track: boolean;
+
+ public constructor(str: string, era: LoadEra, track: boolean) {
+ this.str = str;
+ this.era = era;
+ this.track = track;
+ this.rewritten = era === 'jdk11' || era === 'jdk13';
+ }
+
+ /** The next logical line, or `undefined` at the end of the input */
+ public readLine(): string | undefined {
+ return this.rewritten ? this.readLine11() : this.readLine8();
+ }
+
+ /**
+ * What Java's decoding can read past the last line: the next 5 chars of the buffer when one of the last 5 chars of
+ * the line is a backslash (`\` + `uXXXX`), else nothing. It stops at the end of what was ever written: there, Java
+ * reads a '\0', which stops any read.
+ */
+ public tail(): string {
+ const len = this.text.length;
+ if (this.text.lastIndexOf('\\') < Math.max(0, len - 5)) return '';
+ let out = '';
+ let i = this.written.length - 1;
+ for (let at = len; at < len + 5; at++) {
+ while (i >= 0 && this.written[i].length <= at) i--; // the last line that wrote at `at`
+ if (i < 0) break;
+ out += this.written[i][at];
+ }
+ return out;
+ }
+
+ // A line wrote `written` to the buffer
+ private write(written: string): void {
+ const lines = this.written;
+ while (lines.length > 0 && lines[lines.length - 1].length <= written.length) lines.pop();
+ if (written !== '') lines.push(written);
+ }
+
+ // The line is the `len` first chars of what it wrote to the buffer (the last one can be a removed backslash)
+ private found(written: string, len: number): string {
+ this.write(written);
+ this.text = len === written.length ? written : written.substring(0, len);
+ return this.text;
+ }
+
+ // Like Reader.read() on a StringReader: false at the end of the input
+ private fill(): boolean {
+ if (this.pos >= this.str.length) return false;
+ this.limit = Math.min(this.pos + 8192, this.str.length);
+ return true;
+ }
+
+ // JDK 6 to 12, and 11.0.0 to 11.0.4
+ private readLine8(): string | undefined {
+ const str = this.str;
+ let len = 0;
+ let skipWhiteSpace = true;
+ let isCommentLine = false;
+ let isNewLine = true;
+ let appendedLineBegin = false;
+ let precedingBackslash = false;
+ let skipLF = false;
+ // What the line wrote to the buffer, as slices of the input: text + str.substring(segStart, segEnd). Its `len`
+ // first chars are the line, then a removed continuation backslash can follow (until overwritten).
+ let text = '';
+ let segStart = 0;
+ let segEnd = 0;
+ while (true) {
+ if (this.pos >= this.limit && !this.fill()) {
+ if (len === 0 || isCommentLine) return undefined;
+ this.end = str.length;
+ return this.found(text + str.substring(segStart, segEnd), precedingBackslash ? len - 1 : len);
+ }
+ let c = str[this.pos++];
+ if (skipLF) {
+ skipLF = false;
+ if (c === '\n') continue;
+ }
+ if (skipWhiteSpace) {
+ if (c === ' ' || c === '\t' || c === '\f') continue;
+ if (!appendedLineBegin && (c === '\r' || c === '\n')) continue;
+ skipWhiteSpace = false;
+ appendedLineBegin = false;
+ }
+ if (isNewLine) {
+ isNewLine = false;
+ if (c === '#' || c === '!') {
+ isCommentLine = true;
+ if (this.era === 'jdk8') continue;
+ // JDK 9+: skip up to a line break or a backslash (in the current chunk), then go on with it
+ while (this.pos < this.limit) {
+ c = str[this.pos++];
+ if (c === '\n' || c === '\r' || c === '\\') break;
+ }
+ }
+ }
+ if (c !== '\n' && c !== '\r') {
+ // Comment lines are written to the buffer too, and their backslashes count
+ if (this.track && len === 0 && !isCommentLine) this.start = this.pos - 1;
+ if (this.pos - 1 !== segEnd) {
+ text += str.substring(segStart, segStart + len - text.length); // (without a removed backslash)
+ segStart = this.pos - 1;
+ }
+ segEnd = this.pos;
+ len++;
+ precedingBackslash = c === '\\' ? !precedingBackslash : false;
+ } else {
+ if (isCommentLine || len === 0) {
+ this.write(text + str.substring(segStart, segEnd));
+ isCommentLine = false;
+ isNewLine = true;
+ skipWhiteSpace = true;
+ len = 0;
+ text = '';
+ segStart = segEnd = 0;
+ continue;
+ }
+ this.end = this.pos - 1;
+ if (this.pos >= this.limit && !this.fill()) {
+ return this.found(text + str.substring(segStart, segEnd), precedingBackslash ? len - 1 : len);
+ }
+ if (!precedingBackslash) return this.found(text + str.substring(segStart, segEnd), len);
+ len -= 1;
+ skipWhiteSpace = true;
+ appendedLineBegin = true;
+ precedingBackslash = false;
+ if (c === '\r') skipLF = true;
+ }
+ }
+ }
+
+ // JDK 11.0.5+ and 13+ (JDK-8224202)
+ private readLine11(): string | undefined {
+ const str = this.str;
+ let len = 0;
+ let skipWhiteSpace = true;
+ let appendedLineBegin = false;
+ let precedingBackslash = false;
+ let text = '';
+ let segStart = 0;
+ let segEnd = 0;
+ while (true) {
+ if (this.pos >= str.length) {
+ if (len === 0) return undefined;
+ this.end = str.length;
+ return this.found(text + str.substring(segStart, segEnd), precedingBackslash ? len - 1 : len);
+ }
+ let c = str[this.pos++];
+ if (skipWhiteSpace) {
+ if (c === ' ' || c === '\t' || c === '\f') continue;
+ if (!appendedLineBegin && (c === '\r' || c === '\n')) continue;
+ skipWhiteSpace = false;
+ appendedLineBegin = false;
+ }
+ if (len === 0 && (c === '#' || c === '!')) {
+ // Comment: skipped up to the end of the line, without touching the buffer
+ do {
+ if (this.pos >= str.length) return undefined;
+ c = str[this.pos++];
+ } while (c !== '\r' && c !== '\n');
+ skipWhiteSpace = true;
+ continue;
+ }
+ if (c !== '\n' && c !== '\r') {
+ if (this.track && len === 0) this.start = this.pos - 1;
+ if (this.pos - 1 !== segEnd) {
+ text += str.substring(segStart, segStart + len - text.length); // (without a removed backslash)
+ segStart = this.pos - 1;
+ }
+ segEnd = this.pos;
+ len++;
+ precedingBackslash = c === '\\' ? !precedingBackslash : false;
+ } else {
+ if (len === 0) {
+ skipWhiteSpace = true;
+ continue;
+ }
+ this.end = this.pos - 1;
+ if (this.pos >= str.length) {
+ return this.found(text + str.substring(segStart, segEnd), precedingBackslash ? len - 1 : len);
+ }
+ if (!precedingBackslash) return this.found(text + str.substring(segStart, segEnd), len);
+ len -= 1;
+ skipWhiteSpace = true;
+ appendedLineBegin = true;
+ precedingBackslash = false;
+ if (c === '\r' && str[this.pos] === '\n') this.pos++;
+ }
+ }
+ }
+}
+
+/**
+ * Properties.loadConvert() of a load era, on `str`. Like Java, an escape at the end of `str` reads on into `followedBy`,
+ * then '\0' (JDK 6 to 12 and 11.0.5+; JDK 13+ checks the bounds of `\uXXXX`).
+ */
+export function javaDecode(str: string, followedBy: string, era: LoadEra): string {
+ const end = str.length;
+ let off = str.indexOf('\\');
+ // No escape: nothing to decode (Java only reads past the end after a backslash). JDK 11.0.5+ also returns the
+ // string as-is when its first backslash is the last char.
+ if (off < 0 || ((era === 'jdk11' || era === 'jdk13') && off === end - 1)) return str;
+ const after = followedBy + '\0'; // a '\0' always stops a read before the end of `after`
+ const at = (i: number) => (i < end ? str[i] : after[i - end]);
+ let out = '';
+ let start = 0; // start of the text to copy as-is
+ while (off >= 0 && off < end) {
+ out += str.substring(start, off);
+ off++; // backslash
+ let c = at(off++);
+ if (c === 'u') {
+ if (era === 'jdk13' && off > end - 4) throw malformedUnicode();
+ let value = 0;
+ for (let i = 0; i < 4; i++) {
+ const digit = parseInt(at(off++), 16);
+ if (Number.isNaN(digit)) throw malformedUnicode();
+ value = (value << 4) + digit;
+ }
+ c = String.fromCharCode(value);
+ } else if (c === 't') {
+ c = '\t';
+ } else if (c === 'r') {
+ c = '\r';
+ } else if (c === 'n') {
+ c = '\n';
+ } else if (c === 'f') {
+ c = '\f';
+ }
+ out += c;
+ start = off;
+ off = start < end ? str.indexOf('\\', start) : -1;
+ }
+ return start < end ? out + str.substring(start) : out;
+}
+
+function malformedUnicode(): Error {
+ return new Error('Malformed \\uxxxx encoding.');
+}
+
+/** Key/value boundaries of a logical line, like Properties.load0() (the same in every era, and in the original behavior) */
+export function splitLine(line: string): {line: string; sepStart: number; valueStart: number} {
+ let keyLen = 0;
+ let valueStart = line.length;
+ let hasSep = false;
+ let backslash = false;
+
+ const lineLen = line.length;
+ let pos = 0;
+ for (; pos < lineLen; pos++) {
+ const c = line[pos];
+ if ((c === '=' || c === ':') && !backslash) {
+ valueStart = keyLen + 1;
+ hasSep = true;
+ break;
+ } else if ((c === ' ' || c === '\t' || c === '\f') && !backslash) {
+ valueStart = keyLen + 1;
+ break;
+ }
+ if (c === '\\') {
+ backslash = !backslash;
+ } else {
+ backslash = false;
+ }
+ keyLen++;
+ }
+ while (valueStart < lineLen) {
+ const c = line[valueStart];
+ if (c !== ' ' && c !== '\t' && c !== '\f') {
+ if (!hasSep && (c === '=' || c === ':')) {
+ hasSep = true;
+ } else {
+ break;
+ }
+ }
+ valueStart++;
+ }
+
+ return {line, sepStart: keyLen, valueStart};
+}
diff --git a/src/internal/java-store.ts b/src/internal/java-store.ts
new file mode 100644
index 0000000..8edb510
--- /dev/null
+++ b/src/internal/java-store.ts
@@ -0,0 +1,221 @@
+// What Properties.store() writes: escaped keys and values, comments, the date comment, and the line separator.
+import {EscapeOptions, StringifyOptions} from '../java-props';
+import {loadEra} from './java-version';
+
+/** The settings of an escapeKey()/escapeValue() output, see {@link escapeSettings} */
+export interface EscapeSettings {
+ escapeValueSpecials: boolean;
+ escapeUnicode: boolean;
+}
+
+/** The settings of a stringify() output, see {@link storeSettings} */
+export interface StoreSettings extends EscapeSettings {
+ separator: string;
+ lineSeparator: string;
+ comments: string | undefined;
+ date: boolean | Date | string;
+ timeZone: string | undefined;
+ order: 'insertion' | 'sorted';
+}
+
+/**
+ * Resolves the options of escapeKey()/escapeValue(): each option, else its default: like `Properties.store(Writer)`
+ * with `javaVersion`, else like the original output of java-props.
+ *
+ * @throws TypeError | RangeError if `options.javaVersion` is invalid
+ */
+export function escapeSettings(options: EscapeOptions = {}): EscapeSettings {
+ const store = isStore(options);
+ return {
+ escapeValueSpecials: pick(options.escapeValueSpecials, store, true, false),
+ escapeUnicode: pick(options.escapeUnicode, store, false, true),
+ };
+}
+
+/**
+ * Resolves the options of stringify(), like {@link escapeSettings}.
+ *
+ * @throws TypeError | RangeError if `options.javaVersion` is invalid
+ */
+export function storeSettings(options: StringifyOptions = {}): StoreSettings {
+ const store = isStore(options);
+ const {escapeValueSpecials, escapeUnicode} = escapeSettings(options);
+ return {
+ escapeValueSpecials,
+ escapeUnicode,
+ separator: pick(options.separator, store, '=', ': '),
+ lineSeparator: pick(options.lineSeparator, store, systemLineSeparator(), '\n'),
+ comments: options.comments,
+ date: pick(options.date, store, true, false),
+ timeZone: options.timeZone,
+ order: pick<'insertion' | 'sorted'>(options.order, store, 'sorted', 'insertion'),
+ };
+}
+
+// With `javaVersion`, the defaults are like Properties.store() (every version writes the same: only validated)
+function isStore(options: EscapeOptions): boolean {
+ if (options.javaVersion === undefined) return false;
+ loadEra(options.javaVersion);
+ return true;
+}
+
+function pick(value: T | undefined, store: boolean, storeDefault: T, originalDefault: T): T {
+ return value !== undefined ? value : store ? storeDefault : originalDefault;
+}
+
+/** `System.lineSeparator()`: `'\r\n'` on Windows, `'\n'` elsewhere (and in browsers) */
+export function systemLineSeparator(): string {
+ return typeof process !== 'undefined' && process.platform === 'win32' ? '\r\n' : '\n';
+}
+
+/** `Properties.writeComments()`: comment lines, `\uXXXX` escapes above U+00FF */
+export function writeComments(comments: string, nl: string): string {
+ let out = '#';
+ let last = 0;
+ for (let current = 0; current < comments.length; current++) {
+ const code = comments.charCodeAt(current);
+ if (code > 0xff || code === 0x0a || code === 0x0d) {
+ out += comments.substring(last, current);
+ if (code > 0xff) {
+ out += '\\u' + hex4(code);
+ } else {
+ out += nl;
+ if (code === 0x0d && current !== comments.length - 1 && comments[current + 1] === '\n') {
+ current++;
+ }
+ const next = comments[current + 1];
+ if (current === comments.length - 1 || (next !== '#' && next !== '!')) {
+ out += '#';
+ }
+ }
+ last = current + 1;
+ }
+ }
+ return out + comments.substring(last) + nl;
+}
+
+/**
+ * The date comment of `Properties.store()`: `Date.toString()` of a date (`true` = now), or a string written like the
+ * `java.properties.date` system property of Java 18+ (an empty string means now, like in Java).
+ *
+ * @throws RangeError if `timeZone` is invalid, or is not `'UTC'` nor a fixed offset without `Intl` support
+ */
+export function dateComment(date: true | Date | string, timeZone: string | undefined, nl: string): string {
+ if (typeof date === 'string' && date !== '') return writeComments(date, nl);
+ return '#' + javaDateString(date instanceof Date ? date : new Date(), timeZone) + nl;
+}
+
+const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+const ZONE_NAME = /^[A-Z]{2,5}$/;
+
+// Date.toString() of Java: "EEE MMM dd HH:mm:ss zzz yyyy"
+function javaDateString(date: Date, timeZone: string | undefined): string {
+ const {offset, zone} = zoneAt(date, timeZone);
+ const d = new Date(Math.floor(date.getTime() / 1000) * 1000 + offset * 60000);
+ return (
+ `${DAYS[d.getUTCDay()]} ${MONTHS[d.getUTCMonth()]} ${pad2(d.getUTCDate())} ` +
+ `${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())} ${zone} ${d.getUTCFullYear()}`
+ );
+}
+
+// Offset (in minutes) and short name of a time zone at a date
+function zoneAt(date: Date, timeZone: string | undefined): {offset: number; zone: string} {
+ const fixed = timeZone !== undefined ? /^([+-])(\d\d):(\d\d)$/.exec(timeZone) : null;
+ if (fixed) {
+ const offset = (fixed[1] === '-' ? -1 : 1) * (Number(fixed[2]) * 60 + Number(fixed[3]));
+ return {offset, zone: `GMT${fixed[1]}${fixed[2]}:${fixed[3]}`};
+ }
+ if (timeZone === 'UTC') return {offset: 0, zone: 'UTC'};
+ const formatToParts = typeof Intl !== 'undefined' && Intl.DateTimeFormat.prototype.formatToParts;
+ if (!formatToParts) {
+ if (timeZone !== undefined) throw new RangeError(`Unsupported timeZone ${timeZone}: no Intl support`);
+ const offset = -date.getTimezoneOffset();
+ return {offset, zone: gmtName(offset)};
+ }
+ const parts = (locale: string) =>
+ new Intl.DateTimeFormat(locale, {
+ timeZone,
+ hour12: false,
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ timeZoneName: 'short',
+ }).formatToParts(date);
+ const us = parts('en-US');
+ const value = (p: Intl.DateTimeFormatPart[], type: string) =>
+ (p.find((x) => x.type === type) as Intl.DateTimeFormatPart).value;
+ const field = (type: string) => Number(value(us, type));
+ const local = Date.UTC(
+ field('year'),
+ field('month') - 1,
+ field('day'),
+ field('hour') % 24,
+ field('minute'),
+ field('second')
+ );
+ const offset = Math.round((local - Math.floor(date.getTime() / 1000) * 1000) / 60000);
+ // Java's names are not available: en-US names (EST...), then en-GB ones (CET...), then Java's "GMT+01:00" format
+ let zone = value(us, 'timeZoneName');
+ if (!ZONE_NAME.test(zone)) zone = value(parts('en-GB'), 'timeZoneName');
+ return {offset, zone: ZONE_NAME.test(zone) ? zone : gmtName(offset)};
+}
+
+// Java's name of a custom time zone ("GMT+01:00")
+function gmtName(offset: number): string {
+ const abs = Math.abs(offset);
+ return `GMT${offset < 0 ? '-' : '+'}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`;
+}
+
+function pad2(n: number): string {
+ return n < 10 ? '0' + n : String(n);
+}
+
+function hex4(code: number): string {
+ const hex = code.toString(16).toUpperCase();
+ return '0000'.substring(hex.length) + hex;
+}
+
+/** `Properties.saveConvert()`, with the escaping of `=:#!` as an option (Java always escapes them) */
+export function saveConvert(
+ str: string,
+ escapeSpace: boolean,
+ escapeSpecials: boolean,
+ escapeUnicode: boolean
+): string {
+ let out = '';
+ let start = 0; // start of the text to copy as-is
+ for (let i = 0; i < str.length; i++) {
+ const c = str[i];
+ const code = str.charCodeAt(i);
+ let escaped: string;
+ if (code > 61 && code < 127) {
+ if (c !== '\\') continue;
+ escaped = '\\\\';
+ } else if (c === ' ') {
+ if (i !== 0 && !escapeSpace) continue;
+ escaped = '\\ ';
+ } else if (c === '\t') {
+ escaped = '\\t';
+ } else if (c === '\n') {
+ escaped = '\\n';
+ } else if (c === '\r') {
+ escaped = '\\r';
+ } else if (c === '\f') {
+ escaped = '\\f';
+ } else if (c === '=' || c === ':' || c === '#' || c === '!') {
+ if (!escapeSpecials) continue;
+ escaped = '\\' + c;
+ } else if ((code < 0x20 || code > 0x7e) && escapeUnicode) {
+ escaped = '\\u' + hex4(code);
+ } else {
+ continue;
+ }
+ out += str.substring(start, i) + escaped;
+ start = i + 1;
+ }
+ return start === 0 ? str : out + str.substring(start);
+}
diff --git a/src/internal/java-version.ts b/src/internal/java-version.ts
new file mode 100644
index 0000000..922ade7
--- /dev/null
+++ b/src/internal/java-version.ts
@@ -0,0 +1,65 @@
+/**
+ * Behavior eras of Properties.load(), each named after its first JDK:
+ * - jdk8: the original implementation.
+ * - jdk9: comments are skipped quickly up to their first backslash, so only the rest of a comment is written to the
+ * line buffer (and read by later decoding).
+ * - jdk11: JDK-8224202 "Speed up Properties.load", a rewrite of LineReader that also changed its behavior: `#`/`!`
+ * after an empty continuation line starts a comment, and nothing leaks from comment lines anymore.
+ * - jdk13: jdk11 + JDK-8224240: a truncated `\uXXXX` at the end of a line throws, instead of decoding stale content.
+ */
+export const LOAD_ERAS = ['jdk8', 'jdk9', 'jdk11', 'jdk13'] as const;
+export type LoadEra = (typeof LOAD_ERAS)[number];
+
+const ACCEPTED = 'a major version (6 or more), a java.version string (e.g. "11.0.4", "1.8.0_392") or "latest"';
+
+/**
+ * Behavior era of Properties.load() for a Java version: a major version (`8`, `17`...), a `java.version` string
+ * (`'11.0.4'`, `'1.8.0_392'`, `'21.0.2+13-LTS'`...) or `'latest'`. A major version alone means its latest update.
+ *
+ * @throws TypeError if `javaVersion` is neither a number nor a string
+ * @throws RangeError if `javaVersion` is not a valid Java version, or is older than Java 6
+ */
+export function loadEra(javaVersion: number | string): LoadEra {
+ if (javaVersion !== cachedVersion) {
+ cachedEra = resolveEra(javaVersion);
+ cachedVersion = javaVersion;
+ }
+ return cachedEra;
+}
+
+let cachedVersion: number | string | undefined; // escapeKey/escapeValue resolve it on each call
+let cachedEra: LoadEra;
+
+function resolveEra(javaVersion: number | string): LoadEra {
+ const [major, update] = parseVersion(javaVersion);
+ if (major >= 13) return 'jdk13';
+ if (major === 11 && update >= 5) return 'jdk11'; // JDK-8224202 was backported to 11.0.5 (not to 12)
+ if (major >= 9) return 'jdk9';
+ if (major >= 6) return 'jdk8';
+ throw invalid(javaVersion);
+}
+
+// [major, update] of a Java version. A missing update means the latest one, and 'latest' the latest version.
+function parseVersion(javaVersion: number | string): [number, number] {
+ if (javaVersion === 'latest') return [Infinity, Infinity];
+ if (typeof javaVersion === 'number') {
+ if (!Number.isInteger(javaVersion)) throw invalid(javaVersion);
+ return [javaVersion, Infinity];
+ }
+ if (typeof javaVersion !== 'string') {
+ throw new TypeError(`Invalid javaVersion type (${typeof javaVersion}): expected ${ACCEPTED}`);
+ }
+ // "11.0.4", "21.0.2+13-LTS", "17-ea" (Java 9+), or "1.8.0_392" (up to Java 8: "1.")
+ const m = /^(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:\.\d+)*(?:_\d+)?(?:[-+].*)?$/.exec(javaVersion);
+ if (!m) throw invalid(javaVersion);
+ const [, first, second, update] = m;
+ if (first === '1') {
+ if (second === undefined) throw invalid(javaVersion);
+ return [Number(second), Infinity];
+ }
+ return [Number(first), update === undefined ? Infinity : Number(update)];
+}
+
+function invalid(javaVersion: number | string): RangeError {
+ return new RangeError(`Invalid javaVersion ${JSON.stringify(javaVersion)}: expected ${ACCEPTED}`);
+}
diff --git a/src/internal/legacy-load.ts b/src/internal/legacy-load.ts
new file mode 100644
index 0000000..5e09024
--- /dev/null
+++ b/src/internal/legacy-load.ts
@@ -0,0 +1,130 @@
+// The original reading of java-props (a port of JDK 8's, with differences kept to not break existing code).
+
+const UNICODE_PATTERN = /^[0-9a-fA-F]{4}$/;
+
+/**
+ * Reads the logical lines of a string, like java-props always did: skips comments, blank lines and leading whitespace,
+ * and joins continuation lines. With `track`, it also gives the range of each line in the input.
+ */
+export class LegacyLineReader {
+ public pos = 0;
+ /** With `track`: offset of the first char of the last line (of its continuation backslash, if empty) */
+ public start = -1;
+ /** With `track`: offset of the end of the last line (its line terminator, or the end of the input) */
+ public end = -1;
+ private readonly str: string;
+ private readonly track: boolean;
+
+ public constructor(str: string, track: boolean) {
+ this.str = str;
+ this.track = track;
+ }
+
+ /** The next logical line, or `undefined` at the end of the input */
+ public readLine(): string | undefined {
+ const str = this.str;
+ let skipWhiteSpace = true;
+ let commentLine = false;
+ let newLine = true;
+ let appendedLineBegin = false;
+ let backslash = false;
+ let skipLF = false;
+ // The line (of `len` chars), as slices of the input: text + str.substring(segStart, segEnd)
+ let len = 0;
+ let text = '';
+ let segStart = 0;
+ let segEnd = 0;
+ while (this.pos < str.length) {
+ const c = str[this.pos++];
+ if (skipLF) {
+ skipLF = false;
+ if (c === '\n') continue;
+ }
+ if (skipWhiteSpace) {
+ if (c === ' ' || c === '\t' || c === '\f') continue;
+ if (!appendedLineBegin && (c === '\r' || c === '\n')) continue;
+ skipWhiteSpace = false;
+ appendedLineBegin = false;
+ }
+ if (newLine) {
+ newLine = false;
+ if (c === '#' || c === '!') {
+ commentLine = true;
+ continue;
+ }
+ }
+ if (c !== '\n' && c !== '\r') {
+ if (this.track && len === 0 && !commentLine) this.start = this.pos - 1;
+ len++;
+ if (this.pos - 1 !== segEnd) {
+ text += str.substring(segStart, segEnd);
+ segStart = this.pos - 1;
+ }
+ segEnd = this.pos;
+ backslash = c === '\\' ? !backslash : false;
+ } else {
+ if (commentLine || len === 0) {
+ commentLine = false;
+ newLine = true;
+ skipWhiteSpace = true;
+ len = 0;
+ text = '';
+ segStart = segEnd = 0;
+ continue;
+ }
+ if (!backslash) {
+ this.end = this.pos - 1;
+ return text + str.substring(segStart, segEnd);
+ }
+ len--;
+ segEnd--;
+ skipWhiteSpace = true;
+ appendedLineBegin = true;
+ backslash = false;
+ if (c === '\r') skipLF = true;
+ }
+ }
+ if (backslash && len > 0) {
+ // (`backslash` can be left over from a comment line)
+ len--;
+ segEnd--;
+ }
+ if (len === 0 || commentLine) return undefined;
+ this.end = str.length;
+ return text + str.substring(segStart, segEnd);
+ }
+}
+
+/** Decodes the escape sequences of a key or a value, like java-props always did */
+export function legacyDecode(line: string): string {
+ let off = line.indexOf('\\');
+ if (off < 0) return line;
+ let out = '';
+ let start = 0; // start of the text to copy as-is
+ while (off >= 0) {
+ out += line.substring(start, off);
+ off++; // backslash
+ const c = line[off];
+ if (c === 'u') {
+ // Up to 4 chars of the same line (originally: `\\u(.{0,4})`), that must be hex digits
+ off++;
+ let end = off;
+ while (end < off + 4 && end < line.length && !isLineTerminator(line[end])) end++;
+ const unicode = line.substring(off, end);
+ if (!UNICODE_PATTERN.test(unicode)) throw new Error('Malformed \\uxxxx encoding.');
+ out += String.fromCharCode(parseInt(unicode, 16));
+ off = end;
+ } else if (c !== undefined && !isLineTerminator(c)) {
+ out += c === 't' ? '\t' : c === 'r' ? '\r' : c === 'n' ? '\n' : c === 'f' ? '\f' : c;
+ off++;
+ } // else, the backslash is dropped
+ start = off;
+ off = line.indexOf('\\', start);
+ }
+ return out + line.substring(start);
+}
+
+// The chars that `.` doesn't match in a regex
+function isLineTerminator(c: string): boolean {
+ return c === '\n' || c === '\r' || c === '\u2028' || c === '\u2029';
+}
diff --git a/src/java-props.ts b/src/java-props.ts
index 22e502a..f6c3961 100644
--- a/src/java-props.ts
+++ b/src/java-props.ts
@@ -1,57 +1,218 @@
-import {decodeLine, encodeLine, rawParse} from './utils';
+import {EntryReaderImpl} from './internal/entry-reader';
+import {dateComment, escapeSettings, saveConvert, storeSettings, writeComments} from './internal/java-store';
+import {loadEra} from './internal/java-version';
export interface Properties {
[key: string]: string;
}
+export interface ParseOptions {
+ /**
+ * Parse exactly like `Properties.load()` of this Java version: a major version (`8`, `17`...), a `java.version`
+ * string (`'11.0.4'`, `'1.8.0_392'`...) or `'latest'`. Without it, java-props keeps its original behavior, which
+ * differs from Java on a few unusual inputs. It is resolved once per call: a number is the cheapest form.
+ */
+ javaVersion?: number | string;
+}
+
/**
- * Parses a .properties string and returns the result as an object.
+ * Parses a .properties string.
*
- * @param str The string to parse as .properties
- * @return The result as an object
+ * @param str The .properties content
+ * @param options See {@link ParseOptions}
+ * @return The properties, as an object with a `null` prototype
+ * @throws Error on malformed `\uXXXX` escapes (where Java throws an `IllegalArgumentException`)
+ * @throws TypeError | RangeError if `options.javaVersion` is invalid
*
* @example ```javascript
- * const props = javaProps.parse('foo=Hello\nbar=World');
- * console.log(props.foo + ' ' + props.bar);
- * // "Hello World"
+ * parse('foo=Hello\nbar=World');
+ * // {foo: 'Hello', bar: 'World'}
* ```
*/
-export function parse(str: string): Properties {
+export function parse(str: string, options?: ParseOptions): Properties {
const result: Properties = Object.create(null);
- rawParse(str, (res) => {
- const key = decodeLine(res.line.substring(0, res.sepStart));
- result[key] = decodeLine(res.line.substring(res.valueStart));
- });
+ const reader = readEntries(str, options);
+ while (reader.next()) {
+ result[reader.key] = reader.value;
+ }
return result;
}
+export interface ReadEntriesOptions extends ParseOptions {
+ /** Also give the position of each entry in the input: see {@link EntryPositions} */
+ positions?: boolean;
+}
+
+/**
+ * Reads the entries of a .properties string, one at a time, in order (duplicate keys included). Its properties are
+ * computed on first access, and are only valid after {@link EntryReader.next} returned `true`.
+ */
+export interface EntryReader {
+ /** Goes to the next entry, and returns `false` at the end of the input */
+ next(): boolean;
+ /** The logical line of the entry (continuation lines joined), with its escape sequences */
+ readonly line: string;
+ /** The key, with its escape sequences */
+ readonly rawKey: string;
+ /** The value, with its escape sequences */
+ readonly rawValue: string;
+ /** The decoded key. Throws an `Error` on malformed `\uXXXX` escapes (where Java throws). */
+ readonly key: string;
+ /** The decoded value. Throws an `Error` on malformed `\uXXXX` escapes (where Java throws). */
+ readonly value: string;
+}
+
+/** Position of the current entry in the input (with the `positions` option) */
+export interface EntryPositions {
+ /** Offset of the entry's first char (or of its continuation backslash, for an empty entry) */
+ readonly start: number;
+ /** Offset of the end of the entry: its line terminator, or the end of the input */
+ readonly end: number;
+ /** Line of `start` (from 1; CR, LF and CRLF end a line, like in Java) */
+ readonly startLine: number;
+ /** Column of `start` (from 1, in UTF-16 code units) */
+ readonly startColumn: number;
+}
+
+/**
+ * Reads the entries of a .properties string one at a time: for order, duplicate keys, raw text, positions, or fast
+ * partial reads (nothing is decoded until accessed).
+ *
+ * @param str The .properties content
+ * @param options See {@link ReadEntriesOptions}
+ * @throws TypeError | RangeError if `options.javaVersion` is invalid
+ *
+ * @example ```javascript
+ * const reader = readEntries('a=1\nb=2');
+ * while (reader.next()) {
+ * console.log(reader.key, reader.value);
+ * }
+ * ```
+ */
+export function readEntries(str: string, options: ReadEntriesOptions & {positions: true}): EntryReader & EntryPositions;
+export function readEntries(str: string, options?: ReadEntriesOptions): EntryReader;
+export function readEntries(str: string, options?: ReadEntriesOptions): EntryReader {
+ const era = options && options.javaVersion !== undefined ? loadEra(options.javaVersion) : undefined;
+ return new EntryReaderImpl(str, era, !!options && !!options.positions);
+}
+
+export interface EscapeOptions {
+ /**
+ * Escape like `Properties.store()` of this Java version (same forms as {@link ParseOptions.javaVersion}): it sets
+ * the defaults of the other options.
+ */
+ javaVersion?: number | string;
+ /** Also escape `=`, `:`, `#` and `!` in values, like `store()` (default: `false`, or `true` with `javaVersion`) */
+ escapeValueSpecials?: boolean;
+ /**
+ * Escape the characters outside of printable ASCII as `\uXXXX`, like `store(OutputStream)` (default: `true`, or
+ * `false` with `javaVersion`, like `store(Writer)`)
+ */
+ escapeUnicode?: boolean;
+}
+
+/**
+ * Options of {@link stringify}. Without `javaVersion`, they default to the original output of java-props; with it,
+ * they default to the output of `Properties.store(Writer)`.
+ */
+export interface StringifyOptions extends EscapeOptions {
+ /** Separator between keys and values (default: `': '`, or `'='` with `javaVersion`) */
+ separator?: '=' | ':' | ' ' | ': ' | ' = ';
+ /** Comments written at the start, like the `comments` argument of `Properties.store()` */
+ comments?: string;
+ /**
+ * Date comment, written after `comments` (default: `false`, or `true` with `javaVersion`): `true` (the current date)
+ * or a `Date`, written like Java's `Date.toString()`; or a string, written as a comment like the
+ * `java.properties.date` system property of Java 18+.
+ */
+ date?: boolean | Date | string;
+ /**
+ * IANA time zone of the date comment (default: the system time zone, like the JVM). Time zone names can differ from
+ * Java's own names: use a fixed offset (`'+02:00'`), `'UTC'`, or a `date` string for an exact output.
+ */
+ timeZone?: string;
+ /** Line separator (default: `'\n'`, or the system one with `javaVersion`, like Java: `'\r\n'` on Windows) */
+ lineSeparator?: '\n' | '\r\n' | '\r';
+ /**
+ * Order of the properties (default: `'insertion'`, or `'sorted'` with `javaVersion`, like Java 18+). JavaScript
+ * objects list integer-like keys first, in ascending order.
+ */
+ order?: 'insertion' | 'sorted';
+}
+
/**
- * Converts a JavaScript object (associating keys to string values) to a .properties string.
+ * Converts an object of string values (its own enumerable properties) to a .properties string, which Java reads back
+ * as the same keys and values.
*
- * @param props The JavaScript object to convert
- * @return The .properties string corresponding to the given JavaScript object
+ * @param props The properties
+ * @param options See {@link StringifyOptions}
+ * @return The .properties content
+ * @throws TypeError if a value is not a string, or if `options.javaVersion` has an invalid type
+ * @throws RangeError if `options.javaVersion` or `options.timeZone` is invalid
*
* @example ```javascript
- * const str = javaProps.stringify({'foo': 'Hello', 'bar': 'World'});
- * console.log(str);
- * // "foo: Hello\nbar: World\n"
+ * stringify({foo: 'Hello', bar: 'World'});
+ * // 'foo: Hello\nbar: World\n'
* ```
*/
-export function stringify(props: Properties): string {
+export function stringify(props: Properties, options?: StringifyOptions): string {
+ const settings = storeSettings(options);
+ const nl = settings.lineSeparator;
let str = '';
+ if (settings.comments !== undefined) {
+ str += writeComments(settings.comments, nl);
+ }
+ if (settings.date !== false) {
+ str += dateComment(settings.date, settings.timeZone, nl);
+ }
+ const keys: string[] = [];
for (const key in props) {
- if (Object.prototype.hasOwnProperty.call(props, key)) {
- const value = props[key];
- str += encodeLine(key, true) + ': ' + encodeLine(value) + '\n';
+ if (Object.prototype.hasOwnProperty.call(props, key)) keys.push(key);
+ }
+ if (settings.order === 'sorted') keys.sort();
+ for (const key of keys) {
+ const value = props[key];
+ if (typeof value !== 'string') {
+ throw new TypeError(`The value of ${JSON.stringify(key)} is not a string (${typeof value})`);
}
+ const escapedKey = saveConvert(key, true, true, settings.escapeUnicode);
+ const escapedValue = saveConvert(value, false, settings.escapeValueSpecials, settings.escapeUnicode);
+ str += escapedKey + settings.separator + escapedValue + nl;
}
return str;
}
/**
- * @deprecated
+ * Escapes a key: spaces, separators, comment chars, and by default the characters outside of printable ASCII.
+ *
+ * @throws TypeError if `key` is not a string, or if `options.javaVersion` has an invalid type
+ * @throws RangeError if `options.javaVersion` is invalid
+ */
+export function escapeKey(key: string, options?: EscapeOptions): string {
+ return escapeText(key, true, options);
+}
+
+/**
+ * Escapes a value: its leading space, and by default the characters outside of printable ASCII.
+ *
+ * @throws TypeError if `value` is not a string, or if `options.javaVersion` has an invalid type
+ * @throws RangeError if `options.javaVersion` is invalid
*/
-export default {
+export function escapeValue(value: string, options?: EscapeOptions): string {
+ return escapeText(value, false, options);
+}
+
+function escapeText(str: string, isKey: boolean, options: EscapeOptions | undefined): string {
+ if (typeof str !== 'string') throw new TypeError(`Expected a string, got ${typeof str}`);
+ const settings = escapeSettings(options);
+ return saveConvert(str, isKey, isKey || settings.escapeValueSpecials, settings.escapeUnicode);
+}
+
+const _default = {
parse,
stringify,
};
+/**
+ * @deprecated Use the named exports.
+ */
+export default _default;
diff --git a/src/node-java-props.ts b/src/node-java-props.ts
index 2512b7e..e57d95b 100644
--- a/src/node-java-props.ts
+++ b/src/node-java-props.ts
@@ -1,58 +1,73 @@
+///
import fs from 'fs';
-import {Properties, parse, stringify} from './java-props';
+import {loadEra} from './internal/java-version';
+import {ParseOptions, Properties, parse, stringify} from './java-props';
export * from './java-props';
+export interface ParseFileOptions extends ParseOptions {
+ /** File encoding (default: `'utf8'`). Use `'latin1'` for files written by `Properties.store(OutputStream)`. */
+ encoding?: string;
+}
+
/**
- * Parses a .properties file and returns the result as an object.
+ * Reads and parses a .properties file.
*
- * @param path Filename or file descriptor
- * @param encoding File encoding (default: utf8)
- * @return The result as an object
+ * @param path A file path, or a file descriptor
+ * @param encoding File encoding (default: `'utf8'`)
+ * @param options See {@link ParseOptions}
+ * @return A promise of the properties (see {@link parse}). It rejects if the file can't be read, if the encoding is
+ * unknown, or if {@link parse} throws.
*
* @example ```javascript
- * javaProps.parseFile('./foobar.properties').then((props) => {
- * console.log(props.foo + ' ' + props.bar);
- * // "Hello World"
- * }).catch((err) => {
- * console.error(err);
- * });
- * ```
- * *- or with async/await -*
- * ```javascript
- * async function fct() {
- * try {
- * const props = await javaProps.parseFile('./foobar.properties');
- * console.log(props.foo + ' ' + props.bar);
- * // "Hello World"
- * } catch (err) {
- * console.error(err);
- * }
- * }
+ * await parseFile('./config.properties');
+ * await parseFile('./config.properties', {encoding: 'latin1', javaVersion: 21});
* ```
*/
-export function parseFile(path: string | Buffer | URL | number, encoding?: string): Promise {
+export function parseFile(
+ path: string | Buffer | URL | number,
+ encoding?: string,
+ options?: ParseOptions
+): Promise;
+/**
+ * Reads and parses a .properties file (see the other signature).
+ *
+ * @param path A file path, or a file descriptor
+ * @param options The file encoding, and the options of {@link parse}
+ */
+export function parseFile(path: string | Buffer | URL | number, options?: ParseFileOptions): Promise;
+export function parseFile(
+ path: string | Buffer | URL | number,
+ encodingOrOptions?: string | ParseFileOptions,
+ parseOptions?: ParseOptions
+): Promise {
+ const fileOptions =
+ typeof encodingOrOptions === 'object' && encodingOrOptions !== null ? encodingOrOptions : undefined;
+ const encoding = fileOptions ? fileOptions.encoding : (encodingOrOptions as string | undefined);
+ const options = fileOptions || parseOptions;
return new Promise((resolve, reject) => {
- fs.readFile(path, {encoding: encoding || 'utf8', flag: 'r'}, (err, data) => {
+ // Invalid options reject before reading the file
+ if (options && options.javaVersion !== undefined) loadEra(options.javaVersion);
+ // `encoding` accepts any string: unknown encodings reject
+ fs.readFile(path, {encoding: (encoding || 'utf8') as BufferEncoding}, (err, data) => {
if (err) {
return reject(err);
}
try {
- const res = parse(data);
- return resolve(res);
+ return resolve(parse(data, options));
} catch (err) {
- /* istanbul ignore next */
return reject(err);
}
});
});
}
-/**
- * @deprecated
- */
-export default {
+const _default = {
parse,
parseFile,
stringify,
};
+/**
+ * @deprecated Use the named exports.
+ */
+export default _default;
diff --git a/src/typedoc.d.ts b/src/typedoc.d.ts
deleted file mode 100644
index 1caa89f..0000000
--- a/src/typedoc.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-export {Properties, parse, parseFile, stringify} from './node-java-props';
diff --git a/src/utils.ts b/src/utils.ts
index 7733b4d..12a86fd 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,119 +1,73 @@
-const DECODE_PATTERN = /(?:\\u(.{0,4})|\\(.?))/g;
-const UNICODE_PATTERN = /^[0-9a-fA-F]{4}$/;
-
-const ENCODE_PATTERN = /(?:[\u0000-\u001F\\\u007F-\uFFFF])/g;
-const ENCODE_KEY_PATTERN = /(?:[\u0000-\u0020!#:=\\\u007F-\uFFFF])/g; // ENCODE_PATTERN with separators + comments
+/*
+ * Deprecated low-level API (`java-props/dist/utils`, public since 2.3.0): the original behavior of java-props only,
+ * frozen. Replaced by readEntries(), escapeKey() and escapeValue() of the main module.
+ */
+import {splitLine} from './internal/java-load';
+import {saveConvert} from './internal/java-store';
+import {LegacyLineReader, legacyDecode} from './internal/legacy-load';
+/**
+ * @deprecated Use {@link readEntries}.
+ */
export interface ParsedLine {
line: string;
sepStart: number;
valueStart: number;
}
+/**
+ * Splits a .properties string into logical lines, and calls `consumeFn` with the key/value boundaries of each one.
+ *
+ * @deprecated Use {@link readEntries}: its `rawKey` and `rawValue`.
+ */
export function rawParse(str: string, consumeFn: (res: ParsedLine) => void): void {
- const lr = new LineReader(str);
- let line;
- while ((line = lr.readLine()) !== undefined) {
- consumeFn(rawParseLine(line));
+ const reader = new LegacyLineReader(str, false);
+ for (let line = reader.readLine(); line !== undefined; line = reader.readLine()) {
+ consumeFn(splitLine(line));
}
}
+/**
+ * Finds the key/value boundaries of a logical line (as returned by {@link LineReader.readLine}).
+ * The key is `line.substring(0, sepStart)` and the value is `line.substring(valueStart)`, both still escaped.
+ *
+ * @deprecated Use {@link readEntries}: its `rawKey` and `rawValue`.
+ */
export function rawParseLine(line: string): ParsedLine {
- let keyLen = 0;
- let valueStart = line.length;
- let hasSep = false;
- let backslash = false;
-
- const lineLen = line.length;
- let pos = 0;
- for (; pos < lineLen; pos++) {
- const c = line[pos];
- if ((c === '=' || c === ':') && !backslash) {
- valueStart = keyLen + 1;
- hasSep = true;
- break;
- } else if ((c === ' ' || c === '\t' || c === '\f') && !backslash) {
- valueStart = keyLen + 1;
- break;
- }
- if (c === '\\') {
- backslash = !backslash;
- } else {
- backslash = false;
- }
- keyLen++;
- }
- while (valueStart < lineLen) {
- const c = line[valueStart];
- if (c !== ' ' && c !== '\t' && c !== '\f') {
- if (!hasSep && (c === '=' || c === ':')) {
- hasSep = true;
- } else {
- break;
- }
- }
- valueStart++;
- }
-
- return {line, sepStart: keyLen, valueStart};
+ return splitLine(line);
}
+/**
+ * Decodes the escape sequences of a key or a value (`\t`, `\uXXXX`, `\=`, ...).
+ *
+ * @throws Error on malformed `\uXXXX` sequences
+ * @deprecated Use {@link readEntries}: its `key` and `value`.
+ */
export function decodeLine(line: string): string {
- return line.replace(DECODE_PATTERN, (_, unicode, char) => {
- if (unicode !== undefined) {
- if (!unicode.match(UNICODE_PATTERN)) {
- throw new Error('Malformed \\uxxxx encoding.');
- }
- const charVal = parseInt(unicode, 16);
- return String.fromCharCode(charVal);
- } else if (char === 't') {
- return '\t';
- } else if (char === 'r') {
- return '\r';
- } else if (char === 'n') {
- return '\n';
- } else if (char === 'f') {
- return '\f';
- } else {
- return char;
- }
- });
+ return legacyDecode(line);
}
+/**
+ * Escapes a value, or a key if `isKey` is true (keys also escape spaces, separators and comment chars).
+ *
+ * @deprecated Use {@link escapeKey} or {@link escapeValue}.
+ */
export function encodeLine(line: string, isKey?: boolean): string {
- let str = line.replace(isKey ? ENCODE_KEY_PATTERN : ENCODE_PATTERN, (c) => {
- if (c === '\t') {
- return '\\t';
- } else if (c === '\r') {
- return '\\r';
- } else if (c === '\n') {
- return '\\n';
- } else if (c === '\f') {
- return '\\f';
- } else if (c >= ' ' && c <= '~') {
- return '\\' + c;
- } else {
- const code = c.charCodeAt(0);
- if (code < 16) return '\\u000' + code.toString(16).toUpperCase();
- if (code < 256) return '\\u00' + code.toString(16).toUpperCase();
- if (code < 4096) return '\\u0' + code.toString(16).toUpperCase();
- return '\\u' + code.toString(16).toUpperCase();
- }
- });
- if (!isKey) {
- const c = str.charAt(0);
- if (c === ' ' || c === '\t' || c === '\f') {
- str = '\\' + str;
- }
- }
- return str;
+ // The original implementation called line.replace(): the same TypeError for anything but a string
+ if (typeof line !== 'string') (line as unknown as {replace(): void}).replace();
+ return saveConvert(line, !!isKey, !!isKey, true);
}
/**
- * @deprecated Use {@link #decodeLine}.
+ * @deprecated Use {@link readEntries}: its `key` and `value`.
*/
export const convertLine = decodeLine;
+/**
+ * Reads logical lines: skips comments, blank lines and leading whitespace, and joins continuation lines.
+ *
+ * @deprecated Use {@link readEntries}: its `line`.
+ */
export class LineReader {
public readonly str: string;
public readonly strLen: number;
@@ -124,75 +78,14 @@ export class LineReader {
this.strLen = str.length;
}
- public readLine(): string {
- let skipWhiteSpace = true;
- let commentLine = false;
- let newLine = true;
- let appendedLineBegin = false;
- let backslash = false;
- let skipLF = false;
- let line = '';
- while (this.pos < this.strLen) {
- const c = this.str[this.pos++];
- if (skipLF) {
- skipLF = false;
- if (c === '\n') {
- continue;
- }
- }
- if (skipWhiteSpace) {
- if (c === ' ' || c === '\t' || c === '\f') {
- continue;
- }
- if (!appendedLineBegin && (c === '\r' || c === '\n')) {
- continue;
- }
- skipWhiteSpace = false;
- appendedLineBegin = false;
- }
- if (newLine) {
- newLine = false;
- if (c === '#' || c === '!') {
- commentLine = true;
- continue;
- }
- }
-
- if (c !== '\n' && c !== '\r') {
- line += c;
- if (c === '\\') {
- backslash = !backslash;
- } else {
- backslash = false;
- }
- } else {
- // reached EOL
- if (commentLine || line === '') {
- commentLine = false;
- newLine = true;
- skipWhiteSpace = true;
- line = '';
- continue;
- }
- if (backslash) {
- line = line.substring(0, line.length - 1);
- skipWhiteSpace = true;
- appendedLineBegin = true;
- backslash = false;
- if (c === '\r') {
- skipLF = true;
- }
- } else {
- return line;
- }
- }
- }
- if (backslash) {
- line = line.substring(0, line.length - 1);
- }
- if (line === '' || commentLine) {
- return undefined;
- }
+ /**
+ * @return The next logical line, or `undefined` at the end of the input
+ */
+ public readLine(): string | undefined {
+ const reader = new LegacyLineReader(this.str, false);
+ reader.pos = this.pos;
+ const line = reader.readLine();
+ this.pos = reader.pos;
return line;
}
}
diff --git a/test/api.test.ts b/test/api.test.ts
deleted file mode 100644
index 2d5992b..0000000
--- a/test/api.test.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Definitions
- */
-
-const apis: any = {};
-
-// common
-import CommonJavaProps from '../src/java-props';
-import * as CommonJavaPropsW from '../src/java-props';
-
-const CommonJavaPropsR = require('../src/java-props');
-
-apis.common = {
- modules: [CommonJavaProps, CommonJavaPropsW, CommonJavaPropsR],
- methods: ['parse', 'stringify'],
-};
-
-// node
-import NodeJavaProps from '../src/node-java-props';
-import * as NodeJavaPropsW from '../src/node-java-props';
-
-const NodeJavaPropsR = require('../src/node-java-props');
-
-apis.node = {
- parent: apis.common,
- modules: [NodeJavaProps, NodeJavaPropsW, NodeJavaPropsR],
- methods: ['parseFile'],
-};
-
-/*
- * Tests
- */
-
-const testExport = (method: string, modules: any[], _first?: any) => {
- let first = _first;
- it('export ' + method, () => {
- for (const module of modules) {
- expect(module[method]).toBeDefined();
- if (first !== undefined) {
- expect(module[method]).toBe(first);
- } else {
- first = module[method];
- }
- }
- });
-};
-
-for (const apiName in apis) {
- if (!apis.hasOwnProperty(apiName)) {
- continue;
- }
- const api = apis[apiName];
-
- describe(apiName + ' api', () => {
- if (api.parent) {
- for (const method of api.parent.methods) {
- testExport(method, api.modules, api.parent.modules[0][method]);
- }
- }
- for (const method of api.methods) {
- testExport(method, api.modules);
- }
- });
-}
diff --git a/test/cases/parse-cases.mts b/test/cases/parse-cases.mts
new file mode 100644
index 0000000..cd12185
--- /dev/null
+++ b/test/cases/parse-cases.mts
@@ -0,0 +1,204 @@
+// Edge cases for parse(). Java results: parse-expected.json (`pnpm run reference:gen`), ours: parse-snapshot.json.
+// Inputs are JS strings: '\\' is a single backslash in the .properties content.
+
+export interface ParseCase {
+ name: string;
+ input: string;
+}
+
+const cases: ParseCase[] = [];
+const add = (group: string, entries: Record) => {
+ for (const [name, input] of Object.entries(entries)) cases.push({name: `${group}: ${name}`, input});
+};
+
+add('empty', {
+ 'empty string': '',
+ 'only LF': '\n',
+ 'only CR': '\r',
+ 'only CRLF': '\r\n',
+ 'only whitespace': ' \t\f ',
+ 'whitespace lines': ' \n\t\n\f\r\n',
+});
+
+add('line terminators', {
+ LF: 'a=1\nb=2\n',
+ CR: 'a=1\rb=2\r',
+ CRLF: 'a=1\r\nb=2\r\n',
+ LFCR: 'a=1\n\rb=2\n\r',
+ mixed: 'a=1\nb=2\rc=3\r\nd=4',
+ 'no final terminator': 'a=1\nb=2',
+ 'blank lines between': 'a=1\n\n\r\n\r\rb=2',
+ 'U+2028 and U+2029 are not terminators': 'a=1\u2028b=2\u2029c=3\n',
+ 'NEL (U+0085) is not a terminator': 'a=1\u0085b=2',
+ 'vertical tab is not whitespace': '\u000Ba=1',
+});
+
+add('separators', {
+ equals: 'key=value',
+ colon: 'key:value',
+ space: 'key value',
+ tab: 'key\tvalue',
+ 'form feed': 'key\fvalue',
+ 'spaces around equals': 'key = value',
+ 'spaces around colon': 'key \t: \fvalue',
+ 'equals then colon': 'key=:value',
+ 'colon then equals': 'key:=value',
+ 'two equals': 'key==value',
+ 'space then two equals': 'key = = value',
+ 'space then colon then space': 'key : value',
+ 'whitespace only separator, then equals in value': 'key value=x',
+ 'no separator (key only)': 'key',
+ 'key only with trailing spaces': 'key ',
+ 'key then separator, no value': 'key=',
+ 'key then separator and spaces, no value': 'key = ',
+ 'empty key (equals)': '=value',
+ 'empty key (colon)': ':value',
+ 'empty key, empty value': '=',
+ 'leading whitespace before empty key': ' =value',
+ 'escaped separator in key': 'a\\=b=c',
+ 'escaped colon in key': 'a\\:b:c',
+ 'escaped space in key': 'a\\ b c',
+ 'escaped tab in key': 'a\\\tb c',
+ 'double backslash before separator': 'a\\\\=b',
+ 'triple backslash before separator': 'a\\\\\\=b=c',
+ 'separator chars in value': 'k=a=b:c d',
+ 'value with leading escaped space': 'k=\\ v',
+ 'value trailing whitespace kept': 'k=v \t\f',
+});
+
+add('comments', {
+ hash: '# comment\nk=v',
+ bang: '! comment\nk=v',
+ 'indented hash': ' # comment\nk=v',
+ 'indented bang with tab': '\t! comment\nk=v',
+ 'comment at EOF without terminator': 'k=v\n# comment',
+ 'hash inside key is not a comment': 'a#b=c',
+ 'hash inside value': 'k=v # not a comment',
+ 'escaped hash key': '\\#k=v',
+ 'comment ending with backslash does not continue': '# comment \\\nk=v',
+ 'comment ending with two backslashes': '# comment \\\\\nk=v',
+ 'comment ending with backslash then CRLF': '# comment \\\r\nk=v',
+ 'comment ending with backslash at EOF': 'k=v\n# comment \\',
+ 'only comments': '#a\n!b\n # c',
+ 'comment with CR terminator': '# c\rk=v',
+});
+
+add('continuation', {
+ simple: 'k=a\\\nb',
+ 'leading whitespace of next line skipped': 'k=a\\\n \t\fb',
+ CRLF: 'k=a\\\r\nb',
+ CR: 'k=a\\\rb',
+ 'LFCR is two terminators': 'k=a\\\n\rb=c',
+ multiple: 'k=a\\\nb\\\nc',
+ 'in key': 'ke\\\ny=v',
+ 'in separator area': 'k\\\n = v',
+ 'escaped backslash at EOL is not a continuation': 'k=a\\\\\nb=c',
+ 'three backslashes at EOL': 'k=a\\\\\\\nb',
+ 'to an empty line': 'k=a\\\n\nb=c',
+ 'to a whitespace-only line': 'k=a\\\n \nb=c',
+ 'to a comment-looking line': 'k=a\\\n# not a comment',
+ 'to a bang-looking line': 'k=a\\\n!x',
+ 'at EOF': 'k=v\\',
+ 'at EOF after LF': 'k=v\\\n',
+ 'twice at EOF': 'k=v\\\n\\\n',
+ 'continued line starting with escaped space': 'k=a\\\n\\ b',
+ 'CR then LF on continued line start': 'k=a\\\r\n\r\nb=c',
+});
+
+add('lone continuation backslash', {
+ alone: '\\',
+ 'alone with LF': '\\\n',
+ 'alone with CRLF': '\\\r\n',
+ 'after property': 'a=1\n\\',
+ 'after property, with LF': 'a=1\n\\\n',
+ indented: ' \\',
+ 'followed by whitespace line': '\\\n ',
+ 'followed by empty line': '\\\n\n',
+ 'followed by property': '\\\nk=v',
+});
+
+// Comment or key? Changed by JDK-8224202 (see LOAD_ERAS in src/internal/java-version.ts).
+add('empty continuation then comment char', {
+ hash: '\\\n#foo=bar\nbaz=1',
+ 'bang with indentation': ' \\\n!x=1',
+ 'hash after CRLF': '\\\r\n#c\r\nk=v',
+ 'hash with leading whitespace on next line': '\\\n #c\nk=v',
+ 'double continuation then hash': '\\\n\\\n#c\nk=v',
+});
+
+// JDK 8 keeps state from comment lines (fixed by JDK-8224202), java-props counts backslashes differently.
+add('comment ending with backslash, then backslashes', {
+ 'comment ending with backslash, then backslash-only line': '#comment \\\n\\\\',
+ 'bang comment, CRLF, lone backslash': '!\\\r\n\\',
+ 'comment backslash, then key with trailing escaped backslash': '#\\\nk=\\\\',
+ 'comment with 3 backslashes, then backslashes': '#\\\\\\\n\\\\\\\\',
+});
+
+// Java reads stale chars of its reused line buffer (changed in JDK 9, partly fixed by JDK-8224202, then JDK-8224240).
+add('stale line buffer', {
+ 'lone backslash decode after comment': '#\\v\\\r\n\t\\',
+ 'truncated unicode after longer line': 'abcdef12\na=\\u00',
+ 'truncated unicode after longer non-hex line': 'abcdef1z\na=\\u00',
+ 'truncated unicode in key after longer line': 'xxxxxx0041\n\\u00',
+ 'comment chars before a backslash (skipped since JDK 9)': '#ab\\\n\\',
+ 'comment ending with backslash (skipped since JDK 9)': '#a\\\n\\',
+});
+
+add('escapes', {
+ 'standard escapes': 'k=\\t\\n\\r\\f',
+ 'unknown escapes drop the backslash': 'k=\\a\\b\\c\\d\\e\\g\\z\\0\\9',
+ 'escaped backslash': 'k=\\\\',
+ 'escaped quote chars': 'k=\\"\\\'',
+ 'escapes in key': '\\t\\n\\r\\f\\a=v',
+ 'escaped separators in value': 'k=\\=\\:\\ \\#\\!',
+ 'backslash before non-ASCII': 'k=\\é\\❤',
+ 'backslash before U+2028': 'k=\\\u2028',
+ 'backslash before NUL': 'k=\\\u0000',
+});
+
+add('unicode escapes', {
+ 'lowercase hex': 'k=\\u00e9',
+ 'uppercase hex': 'k=\\u00E9',
+ 'mixed case hex': 'k=\\uAbCd',
+ 'in key': '\\u0041=v',
+ NUL: 'k=\\u0000',
+ 'surrogate pair': 'k=\\uD83D\\uDC51',
+ 'lone high surrogate': 'k=\\uD83D',
+ 'lone low surrogate': 'k=\\uDC51',
+ 'escaped separator via unicode is not a separator': 'k\\u003Dx=v',
+ 'unicode escaped backslash': 'k=\\u005Cn',
+ 'followed by more chars': 'k=\\u00410',
+ 'uppercase U is not an escape': 'k=\\U0041',
+ 'escaped u': 'k=\\\\u0041',
+ 'malformed: non-hex': 'k=\\u00g1',
+ 'malformed: too short at end': 'k=\\u12',
+ 'malformed: empty at end': 'k=\\u',
+ 'malformed: in key': '\\uZZZZ=v',
+ 'malformed: short then separator': 'k\\u12=v',
+ 'malformed: spans continuation': 'k=\\u00\\\n41',
+ 'malformed: plus sign': 'k=\\u+123',
+ 'malformed: fullwidth digits': 'k=\\u\uFF10\uFF10\uFF14\uFF11',
+});
+
+add('raw characters', {
+ 'non-ASCII key and value': 'clé=valeur é ❤',
+ 'emoji (surrogate pair)': 'k=👑',
+ 'lone surrogate': 'k=\uD83D',
+ 'NUL char': 'k=a\u0000b',
+ 'control chars': 'k=\u0001\u001F\u007F',
+ 'BOM is not stripped': '\uFEFFk=v',
+ 'BOM on second line': 'a=1\n\uFEFFk=v',
+ 'non-breaking space is not whitespace': '\u00A0k=v',
+ 'fullwidth equals is not a separator': 'k\uFF1Dv',
+});
+
+add('keys', {
+ 'duplicate keys: last wins': 'k=1\nk=2\nk=3',
+ 'duplicate via escapes': 'k=1\n\\k=2',
+ 'proto-like keys': '__proto__=a\nconstructor=b\nhasOwnProperty=c\ntoString=d',
+ 'numeric keys': '2=b\n10=c\n1=a\n-1=d\n01=e',
+ 'long key and value': `${'k'.repeat(300)}=${'v'.repeat(10000)}`,
+ 'long line before short lines': `${'x'.repeat(200)}=y\na=\\`,
+});
+
+export default cases;
diff --git a/test/cases/parse-expected.json b/test/cases/parse-expected.json
new file mode 100644
index 0000000..1408b53
--- /dev/null
+++ b/test/cases/parse-expected.json
@@ -0,0 +1,3396 @@
+{
+ "empty: empty string": {
+ "input": "",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "empty: only LF": {
+ "input": "\n",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "empty: only CR": {
+ "input": "\r",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "empty: only CRLF": {
+ "input": "\r\n",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "empty: only whitespace": {
+ "input": " \t\f ",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "empty: whitespace lines": {
+ "input": " \n\t\n\f\r\n",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "line terminators: LF": {
+ "input": "a=1\nb=2\n",
+ "jdk8": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ }
+ },
+ "line terminators: CR": {
+ "input": "a=1\rb=2\r",
+ "jdk8": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ }
+ },
+ "line terminators: CRLF": {
+ "input": "a=1\r\nb=2\r\n",
+ "jdk8": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ }
+ },
+ "line terminators: LFCR": {
+ "input": "a=1\n\rb=2\n\r",
+ "jdk8": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ }
+ },
+ "line terminators: mixed": {
+ "input": "a=1\nb=2\rc=3\r\nd=4",
+ "jdk8": {
+ "props": {
+ "a": "1",
+ "b": "2",
+ "c": "3",
+ "d": "4"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1",
+ "b": "2",
+ "c": "3",
+ "d": "4"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1",
+ "b": "2",
+ "c": "3",
+ "d": "4"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1",
+ "b": "2",
+ "c": "3",
+ "d": "4"
+ }
+ }
+ },
+ "line terminators: no final terminator": {
+ "input": "a=1\nb=2",
+ "jdk8": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ }
+ },
+ "line terminators: blank lines between": {
+ "input": "a=1\n\n\r\n\r\rb=2",
+ "jdk8": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ }
+ },
+ "line terminators: U+2028 and U+2029 are not terminators": {
+ "input": "a=1
b=2
c=3\n",
+ "jdk8": {
+ "props": {
+ "a": "1
b=2
c=3"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1
b=2
c=3"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1
b=2
c=3"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1
b=2
c=3"
+ }
+ }
+ },
+ "line terminators: NEL (U+0085) is not a terminator": {
+ "input": "a=1
b=2",
+ "jdk8": {
+ "props": {
+ "a": "1
b=2"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1
b=2"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1
b=2"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1
b=2"
+ }
+ }
+ },
+ "line terminators: vertical tab is not whitespace": {
+ "input": "\u000ba=1",
+ "jdk8": {
+ "props": {
+ "\u000ba": "1"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "\u000ba": "1"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "\u000ba": "1"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "\u000ba": "1"
+ }
+ }
+ },
+ "separators: equals": {
+ "input": "key=value",
+ "jdk8": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "value"
+ }
+ }
+ },
+ "separators: colon": {
+ "input": "key:value",
+ "jdk8": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "value"
+ }
+ }
+ },
+ "separators: space": {
+ "input": "key value",
+ "jdk8": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "value"
+ }
+ }
+ },
+ "separators: tab": {
+ "input": "key\tvalue",
+ "jdk8": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "value"
+ }
+ }
+ },
+ "separators: form feed": {
+ "input": "key\fvalue",
+ "jdk8": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "value"
+ }
+ }
+ },
+ "separators: spaces around equals": {
+ "input": "key = value",
+ "jdk8": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "value"
+ }
+ }
+ },
+ "separators: spaces around colon": {
+ "input": "key \t: \fvalue",
+ "jdk8": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "value"
+ }
+ }
+ },
+ "separators: equals then colon": {
+ "input": "key=:value",
+ "jdk8": {
+ "props": {
+ "key": ":value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": ":value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": ":value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": ":value"
+ }
+ }
+ },
+ "separators: colon then equals": {
+ "input": "key:=value",
+ "jdk8": {
+ "props": {
+ "key": "=value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "=value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "=value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "=value"
+ }
+ }
+ },
+ "separators: two equals": {
+ "input": "key==value",
+ "jdk8": {
+ "props": {
+ "key": "=value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "=value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "=value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "=value"
+ }
+ }
+ },
+ "separators: space then two equals": {
+ "input": "key = = value",
+ "jdk8": {
+ "props": {
+ "key": "= value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "= value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "= value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "= value"
+ }
+ }
+ },
+ "separators: space then colon then space": {
+ "input": "key : value",
+ "jdk8": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "value"
+ }
+ }
+ },
+ "separators: whitespace only separator, then equals in value": {
+ "input": "key value=x",
+ "jdk8": {
+ "props": {
+ "key": "value=x"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "value=x"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "value=x"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "value=x"
+ }
+ }
+ },
+ "separators: no separator (key only)": {
+ "input": "key",
+ "jdk8": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": ""
+ }
+ }
+ },
+ "separators: key only with trailing spaces": {
+ "input": "key ",
+ "jdk8": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": ""
+ }
+ }
+ },
+ "separators: key then separator, no value": {
+ "input": "key=",
+ "jdk8": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": ""
+ }
+ }
+ },
+ "separators: key then separator and spaces, no value": {
+ "input": "key = ",
+ "jdk8": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": ""
+ }
+ }
+ },
+ "separators: empty key (equals)": {
+ "input": "=value",
+ "jdk8": {
+ "props": {
+ "": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": "value"
+ }
+ }
+ },
+ "separators: empty key (colon)": {
+ "input": ":value",
+ "jdk8": {
+ "props": {
+ "": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": "value"
+ }
+ }
+ },
+ "separators: empty key, empty value": {
+ "input": "=",
+ "jdk8": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": ""
+ }
+ }
+ },
+ "separators: leading whitespace before empty key": {
+ "input": " =value",
+ "jdk8": {
+ "props": {
+ "": "value"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "": "value"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": "value"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": "value"
+ }
+ }
+ },
+ "separators: escaped separator in key": {
+ "input": "a\\=b=c",
+ "jdk8": {
+ "props": {
+ "a=b": "c"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a=b": "c"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a=b": "c"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a=b": "c"
+ }
+ }
+ },
+ "separators: escaped colon in key": {
+ "input": "a\\:b:c",
+ "jdk8": {
+ "props": {
+ "a:b": "c"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a:b": "c"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a:b": "c"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a:b": "c"
+ }
+ }
+ },
+ "separators: escaped space in key": {
+ "input": "a\\ b c",
+ "jdk8": {
+ "props": {
+ "a b": "c"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a b": "c"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a b": "c"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a b": "c"
+ }
+ }
+ },
+ "separators: escaped tab in key": {
+ "input": "a\\\tb c",
+ "jdk8": {
+ "props": {
+ "a\tb": "c"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a\tb": "c"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a\tb": "c"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a\tb": "c"
+ }
+ }
+ },
+ "separators: double backslash before separator": {
+ "input": "a\\\\=b",
+ "jdk8": {
+ "props": {
+ "a\\": "b"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a\\": "b"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a\\": "b"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a\\": "b"
+ }
+ }
+ },
+ "separators: triple backslash before separator": {
+ "input": "a\\\\\\=b=c",
+ "jdk8": {
+ "props": {
+ "a\\=b": "c"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a\\=b": "c"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a\\=b": "c"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a\\=b": "c"
+ }
+ }
+ },
+ "separators: separator chars in value": {
+ "input": "k=a=b:c d",
+ "jdk8": {
+ "props": {
+ "k": "a=b:c d"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "a=b:c d"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "a=b:c d"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "a=b:c d"
+ }
+ }
+ },
+ "separators: value with leading escaped space": {
+ "input": "k=\\ v",
+ "jdk8": {
+ "props": {
+ "k": " v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": " v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": " v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": " v"
+ }
+ }
+ },
+ "separators: value trailing whitespace kept": {
+ "input": "k=v \t\f",
+ "jdk8": {
+ "props": {
+ "k": "v \t\f"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v \t\f"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v \t\f"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v \t\f"
+ }
+ }
+ },
+ "comments: hash": {
+ "input": "# comment\nk=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comments: bang": {
+ "input": "! comment\nk=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comments: indented hash": {
+ "input": " # comment\nk=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comments: indented bang with tab": {
+ "input": "\t! comment\nk=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comments: comment at EOF without terminator": {
+ "input": "k=v\n# comment",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comments: hash inside key is not a comment": {
+ "input": "a#b=c",
+ "jdk8": {
+ "props": {
+ "a#b": "c"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a#b": "c"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a#b": "c"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a#b": "c"
+ }
+ }
+ },
+ "comments: hash inside value": {
+ "input": "k=v # not a comment",
+ "jdk8": {
+ "props": {
+ "k": "v # not a comment"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v # not a comment"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v # not a comment"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v # not a comment"
+ }
+ }
+ },
+ "comments: escaped hash key": {
+ "input": "\\#k=v",
+ "jdk8": {
+ "props": {
+ "#k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "#k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "#k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "#k": "v"
+ }
+ }
+ },
+ "comments: comment ending with backslash does not continue": {
+ "input": "# comment \\\nk=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comments: comment ending with two backslashes": {
+ "input": "# comment \\\\\nk=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comments: comment ending with backslash then CRLF": {
+ "input": "# comment \\\r\nk=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comments: comment ending with backslash at EOF": {
+ "input": "k=v\n# comment \\",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comments: only comments": {
+ "input": "#a\n!b\n # c",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "comments: comment with CR terminator": {
+ "input": "# c\rk=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "continuation: simple": {
+ "input": "k=a\\\nb",
+ "jdk8": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "ab"
+ }
+ }
+ },
+ "continuation: leading whitespace of next line skipped": {
+ "input": "k=a\\\n \t\fb",
+ "jdk8": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "ab"
+ }
+ }
+ },
+ "continuation: CRLF": {
+ "input": "k=a\\\r\nb",
+ "jdk8": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "ab"
+ }
+ }
+ },
+ "continuation: CR": {
+ "input": "k=a\\\rb",
+ "jdk8": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "ab"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "ab"
+ }
+ }
+ },
+ "continuation: LFCR is two terminators": {
+ "input": "k=a\\\n\rb=c",
+ "jdk8": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ }
+ },
+ "continuation: multiple": {
+ "input": "k=a\\\nb\\\nc",
+ "jdk8": {
+ "props": {
+ "k": "abc"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "abc"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "abc"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "abc"
+ }
+ }
+ },
+ "continuation: in key": {
+ "input": "ke\\\ny=v",
+ "jdk8": {
+ "props": {
+ "key": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "key": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "key": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "key": "v"
+ }
+ }
+ },
+ "continuation: in separator area": {
+ "input": "k\\\n = v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "continuation: escaped backslash at EOL is not a continuation": {
+ "input": "k=a\\\\\nb=c",
+ "jdk8": {
+ "props": {
+ "b": "c",
+ "k": "a\\"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "b": "c",
+ "k": "a\\"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "b": "c",
+ "k": "a\\"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "b": "c",
+ "k": "a\\"
+ }
+ }
+ },
+ "continuation: three backslashes at EOL": {
+ "input": "k=a\\\\\\\nb",
+ "jdk8": {
+ "props": {
+ "k": "a\\b"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "a\\b"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "a\\b"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "a\\b"
+ }
+ }
+ },
+ "continuation: to an empty line": {
+ "input": "k=a\\\n\nb=c",
+ "jdk8": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ }
+ },
+ "continuation: to a whitespace-only line": {
+ "input": "k=a\\\n \nb=c",
+ "jdk8": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ }
+ },
+ "continuation: to a comment-looking line": {
+ "input": "k=a\\\n# not a comment",
+ "jdk8": {
+ "props": {
+ "k": "a# not a comment"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "a# not a comment"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "a# not a comment"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "a# not a comment"
+ }
+ }
+ },
+ "continuation: to a bang-looking line": {
+ "input": "k=a\\\n!x",
+ "jdk8": {
+ "props": {
+ "k": "a!x"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "a!x"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "a!x"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "a!x"
+ }
+ }
+ },
+ "continuation: at EOF": {
+ "input": "k=v\\",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "continuation: at EOF after LF": {
+ "input": "k=v\\\n",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "continuation: twice at EOF": {
+ "input": "k=v\\\n\\\n",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "continuation: continued line starting with escaped space": {
+ "input": "k=a\\\n\\ b",
+ "jdk8": {
+ "props": {
+ "k": "a b"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "a b"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "a b"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "a b"
+ }
+ }
+ },
+ "continuation: CR then LF on continued line start": {
+ "input": "k=a\\\r\n\r\nb=c",
+ "jdk8": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ }
+ },
+ "lone continuation backslash: alone": {
+ "input": "\\",
+ "jdk8": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": ""
+ }
+ }
+ },
+ "lone continuation backslash: alone with LF": {
+ "input": "\\\n",
+ "jdk8": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": ""
+ }
+ }
+ },
+ "lone continuation backslash: alone with CRLF": {
+ "input": "\\\r\n",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "lone continuation backslash: after property": {
+ "input": "a=1\n\\",
+ "jdk8": {
+ "props": {
+ "": "",
+ "a": "1"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "": "",
+ "a": "1"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": "",
+ "a": "1"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": "",
+ "a": "1"
+ }
+ }
+ },
+ "lone continuation backslash: after property, with LF": {
+ "input": "a=1\n\\\n",
+ "jdk8": {
+ "props": {
+ "": "",
+ "a": "1"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "": "",
+ "a": "1"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": "",
+ "a": "1"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": "",
+ "a": "1"
+ }
+ }
+ },
+ "lone continuation backslash: indented": {
+ "input": " \\",
+ "jdk8": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": ""
+ }
+ }
+ },
+ "lone continuation backslash: followed by whitespace line": {
+ "input": "\\\n ",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "lone continuation backslash: followed by empty line": {
+ "input": "\\\n\n",
+ "jdk8": {
+ "props": {}
+ },
+ "jdk9": {
+ "props": {}
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "lone continuation backslash: followed by property": {
+ "input": "\\\nk=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "empty continuation then comment char: hash": {
+ "input": "\\\n#foo=bar\nbaz=1",
+ "jdk8": {
+ "props": {
+ "#foo": "bar",
+ "baz": "1"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "#foo": "bar",
+ "baz": "1"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "baz": "1"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "baz": "1"
+ }
+ }
+ },
+ "empty continuation then comment char: bang with indentation": {
+ "input": " \\\n!x=1",
+ "jdk8": {
+ "props": {
+ "!x": "1"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "!x": "1"
+ }
+ },
+ "jdk11": {
+ "props": {}
+ },
+ "jdk13": {
+ "props": {}
+ }
+ },
+ "empty continuation then comment char: hash after CRLF": {
+ "input": "\\\r\n#c\r\nk=v",
+ "jdk8": {
+ "props": {
+ "#c": "",
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "#c": "",
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "empty continuation then comment char: hash with leading whitespace on next line": {
+ "input": "\\\n #c\nk=v",
+ "jdk8": {
+ "props": {
+ "#c": "",
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "#c": "",
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "empty continuation then comment char: double continuation then hash": {
+ "input": "\\\n\\\n#c\nk=v",
+ "jdk8": {
+ "props": {
+ "#c": "",
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "#c": "",
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "comment ending with backslash, then backslashes: comment ending with backslash, then backslash-only line": {
+ "input": "#comment \\\n\\\\",
+ "jdk8": {
+ "props": {
+ "\\": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "\\": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "\\": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "\\": ""
+ }
+ }
+ },
+ "comment ending with backslash, then backslashes: bang comment, CRLF, lone backslash": {
+ "input": "!\\\r\n\\",
+ "jdk8": {
+ "props": {
+ "\u0000": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "\u0000": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": ""
+ }
+ }
+ },
+ "comment ending with backslash, then backslashes: comment backslash, then key with trailing escaped backslash": {
+ "input": "#\\\nk=\\\\",
+ "jdk8": {
+ "props": {
+ "k": "\\"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\\"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\\"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\\"
+ }
+ }
+ },
+ "comment ending with backslash, then backslashes: comment with 3 backslashes, then backslashes": {
+ "input": "#\\\\\\\n\\\\\\\\",
+ "jdk8": {
+ "props": {
+ "\\\\": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "\\\\": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "\\\\": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "\\\\": ""
+ }
+ }
+ },
+ "stale line buffer: lone backslash decode after comment": {
+ "input": "#\\v\\\r\n\t\\",
+ "jdk8": {
+ "props": {
+ "v": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "v": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": ""
+ }
+ }
+ },
+ "stale line buffer: truncated unicode after longer line": {
+ "input": "abcdef12\na=\\u00",
+ "jdk8": {
+ "props": {
+ "a": "\u0012",
+ "abcdef12": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "\u0012",
+ "abcdef12": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "\u0012",
+ "abcdef12": ""
+ }
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "stale line buffer: truncated unicode after longer non-hex line": {
+ "input": "abcdef1z\na=\\u00",
+ "jdk8": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk9": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk11": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "stale line buffer: truncated unicode in key after longer line": {
+ "input": "xxxxxx0041\n\\u00",
+ "jdk8": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk9": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk11": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "stale line buffer: comment chars before a backslash (skipped since JDK 9)": {
+ "input": "#ab\\\n\\",
+ "jdk8": {
+ "props": {
+ "b": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "\u0000": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": ""
+ }
+ }
+ },
+ "stale line buffer: comment ending with backslash (skipped since JDK 9)": {
+ "input": "#a\\\n\\",
+ "jdk8": {
+ "props": {
+ "\\": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "\u0000": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "": ""
+ }
+ }
+ },
+ "escapes: standard escapes": {
+ "input": "k=\\t\\n\\r\\f",
+ "jdk8": {
+ "props": {
+ "k": "\t\n\r\f"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\t\n\r\f"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\t\n\r\f"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\t\n\r\f"
+ }
+ }
+ },
+ "escapes: unknown escapes drop the backslash": {
+ "input": "k=\\a\\b\\c\\d\\e\\g\\z\\0\\9",
+ "jdk8": {
+ "props": {
+ "k": "abcdegz09"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "abcdegz09"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "abcdegz09"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "abcdegz09"
+ }
+ }
+ },
+ "escapes: escaped backslash": {
+ "input": "k=\\\\",
+ "jdk8": {
+ "props": {
+ "k": "\\"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\\"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\\"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\\"
+ }
+ }
+ },
+ "escapes: escaped quote chars": {
+ "input": "k=\\\"\\'",
+ "jdk8": {
+ "props": {
+ "k": "\"'"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\"'"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\"'"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\"'"
+ }
+ }
+ },
+ "escapes: escapes in key": {
+ "input": "\\t\\n\\r\\f\\a=v",
+ "jdk8": {
+ "props": {
+ "\t\n\r\fa": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "\t\n\r\fa": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "\t\n\r\fa": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "\t\n\r\fa": "v"
+ }
+ }
+ },
+ "escapes: escaped separators in value": {
+ "input": "k=\\=\\:\\ \\#\\!",
+ "jdk8": {
+ "props": {
+ "k": "=: #!"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "=: #!"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "=: #!"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "=: #!"
+ }
+ }
+ },
+ "escapes: backslash before non-ASCII": {
+ "input": "k=\\é\\❤",
+ "jdk8": {
+ "props": {
+ "k": "é❤"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "é❤"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "é❤"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "é❤"
+ }
+ }
+ },
+ "escapes: backslash before U+2028": {
+ "input": "k=\\
",
+ "jdk8": {
+ "props": {
+ "k": "
"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "
"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "
"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "
"
+ }
+ }
+ },
+ "escapes: backslash before NUL": {
+ "input": "k=\\\u0000",
+ "jdk8": {
+ "props": {
+ "k": "\u0000"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\u0000"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\u0000"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\u0000"
+ }
+ }
+ },
+ "unicode escapes: lowercase hex": {
+ "input": "k=\\u00e9",
+ "jdk8": {
+ "props": {
+ "k": "é"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "é"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "é"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "é"
+ }
+ }
+ },
+ "unicode escapes: uppercase hex": {
+ "input": "k=\\u00E9",
+ "jdk8": {
+ "props": {
+ "k": "é"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "é"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "é"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "é"
+ }
+ }
+ },
+ "unicode escapes: mixed case hex": {
+ "input": "k=\\uAbCd",
+ "jdk8": {
+ "props": {
+ "k": "ꯍ"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "ꯍ"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "ꯍ"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "ꯍ"
+ }
+ }
+ },
+ "unicode escapes: in key": {
+ "input": "\\u0041=v",
+ "jdk8": {
+ "props": {
+ "A": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "A": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "A": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "A": "v"
+ }
+ }
+ },
+ "unicode escapes: NUL": {
+ "input": "k=\\u0000",
+ "jdk8": {
+ "props": {
+ "k": "\u0000"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\u0000"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\u0000"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\u0000"
+ }
+ }
+ },
+ "unicode escapes: surrogate pair": {
+ "input": "k=\\uD83D\\uDC51",
+ "jdk8": {
+ "props": {
+ "k": "👑"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "👑"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "👑"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "👑"
+ }
+ }
+ },
+ "unicode escapes: lone high surrogate": {
+ "input": "k=\\uD83D",
+ "jdk8": {
+ "props": {
+ "k": "\ud83d"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\ud83d"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\ud83d"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\ud83d"
+ }
+ }
+ },
+ "unicode escapes: lone low surrogate": {
+ "input": "k=\\uDC51",
+ "jdk8": {
+ "props": {
+ "k": "\udc51"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\udc51"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\udc51"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\udc51"
+ }
+ }
+ },
+ "unicode escapes: escaped separator via unicode is not a separator": {
+ "input": "k\\u003Dx=v",
+ "jdk8": {
+ "props": {
+ "k=x": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k=x": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k=x": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k=x": "v"
+ }
+ }
+ },
+ "unicode escapes: unicode escaped backslash": {
+ "input": "k=\\u005Cn",
+ "jdk8": {
+ "props": {
+ "k": "\\n"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\\n"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\\n"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\\n"
+ }
+ }
+ },
+ "unicode escapes: followed by more chars": {
+ "input": "k=\\u00410",
+ "jdk8": {
+ "props": {
+ "k": "A0"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "A0"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "A0"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "A0"
+ }
+ }
+ },
+ "unicode escapes: uppercase U is not an escape": {
+ "input": "k=\\U0041",
+ "jdk8": {
+ "props": {
+ "k": "U0041"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "U0041"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "U0041"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "U0041"
+ }
+ }
+ },
+ "unicode escapes: escaped u": {
+ "input": "k=\\\\u0041",
+ "jdk8": {
+ "props": {
+ "k": "\\u0041"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\\u0041"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\\u0041"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\\u0041"
+ }
+ }
+ },
+ "unicode escapes: malformed: non-hex": {
+ "input": "k=\\u00g1",
+ "jdk8": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk9": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk11": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "unicode escapes: malformed: too short at end": {
+ "input": "k=\\u12",
+ "jdk8": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk9": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk11": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "unicode escapes: malformed: empty at end": {
+ "input": "k=\\u",
+ "jdk8": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk9": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk11": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "unicode escapes: malformed: in key": {
+ "input": "\\uZZZZ=v",
+ "jdk8": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk9": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk11": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "unicode escapes: malformed: short then separator": {
+ "input": "k\\u12=v",
+ "jdk8": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk9": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk11": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "unicode escapes: malformed: spans continuation": {
+ "input": "k=\\u00\\\n41",
+ "jdk8": {
+ "props": {
+ "k": "A"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "A"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "A"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "A"
+ }
+ }
+ },
+ "unicode escapes: malformed: plus sign": {
+ "input": "k=\\u+123",
+ "jdk8": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk9": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk11": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "unicode escapes: malformed: fullwidth digits": {
+ "input": "k=\\u0041",
+ "jdk8": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk9": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk11": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "jdk13": {
+ "throws": "java.lang.IllegalArgumentException",
+ "message": "Malformed \\uxxxx encoding."
+ }
+ },
+ "raw characters: non-ASCII key and value": {
+ "input": "clé=valeur é ❤",
+ "jdk8": {
+ "props": {
+ "clé": "valeur é ❤"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "clé": "valeur é ❤"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "clé": "valeur é ❤"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "clé": "valeur é ❤"
+ }
+ }
+ },
+ "raw characters: emoji (surrogate pair)": {
+ "input": "k=👑",
+ "jdk8": {
+ "props": {
+ "k": "👑"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "👑"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "👑"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "👑"
+ }
+ }
+ },
+ "raw characters: lone surrogate": {
+ "input": "k=\ud83d",
+ "jdk8": {
+ "props": {
+ "k": "\ud83d"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\ud83d"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\ud83d"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\ud83d"
+ }
+ }
+ },
+ "raw characters: NUL char": {
+ "input": "k=a\u0000b",
+ "jdk8": {
+ "props": {
+ "k": "a\u0000b"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "a\u0000b"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "a\u0000b"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "a\u0000b"
+ }
+ }
+ },
+ "raw characters: control chars": {
+ "input": "k=\u0001\u001f",
+ "jdk8": {
+ "props": {
+ "k": "\u0001\u001f"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "\u0001\u001f"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "\u0001\u001f"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "\u0001\u001f"
+ }
+ }
+ },
+ "raw characters: BOM is not stripped": {
+ "input": "k=v",
+ "jdk8": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "v"
+ }
+ }
+ },
+ "raw characters: BOM on second line": {
+ "input": "a=1\nk=v",
+ "jdk8": {
+ "props": {
+ "a": "1",
+ "k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "1",
+ "k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "1",
+ "k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "1",
+ "k": "v"
+ }
+ }
+ },
+ "raw characters: non-breaking space is not whitespace": {
+ "input": " k=v",
+ "jdk8": {
+ "props": {
+ " k": "v"
+ }
+ },
+ "jdk9": {
+ "props": {
+ " k": "v"
+ }
+ },
+ "jdk11": {
+ "props": {
+ " k": "v"
+ }
+ },
+ "jdk13": {
+ "props": {
+ " k": "v"
+ }
+ }
+ },
+ "raw characters: fullwidth equals is not a separator": {
+ "input": "k=v",
+ "jdk8": {
+ "props": {
+ "k=v": ""
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k=v": ""
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k=v": ""
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k=v": ""
+ }
+ }
+ },
+ "keys: duplicate keys: last wins": {
+ "input": "k=1\nk=2\nk=3",
+ "jdk8": {
+ "props": {
+ "k": "3"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "3"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "3"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "3"
+ }
+ }
+ },
+ "keys: duplicate via escapes": {
+ "input": "k=1\n\\k=2",
+ "jdk8": {
+ "props": {
+ "k": "2"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "k": "2"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "k": "2"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "k": "2"
+ }
+ }
+ },
+ "keys: proto-like keys": {
+ "input": "__proto__=a\nconstructor=b\nhasOwnProperty=c\ntoString=d",
+ "jdk8": {
+ "props": {
+ "__proto__": "a",
+ "constructor": "b",
+ "hasOwnProperty": "c",
+ "toString": "d"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "__proto__": "a",
+ "constructor": "b",
+ "hasOwnProperty": "c",
+ "toString": "d"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "__proto__": "a",
+ "constructor": "b",
+ "hasOwnProperty": "c",
+ "toString": "d"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "__proto__": "a",
+ "constructor": "b",
+ "hasOwnProperty": "c",
+ "toString": "d"
+ }
+ }
+ },
+ "keys: numeric keys": {
+ "input": "2=b\n10=c\n1=a\n-1=d\n01=e",
+ "jdk8": {
+ "props": {
+ "1": "a",
+ "2": "b",
+ "10": "c",
+ "-1": "d",
+ "01": "e"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "1": "a",
+ "2": "b",
+ "10": "c",
+ "-1": "d",
+ "01": "e"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "1": "a",
+ "2": "b",
+ "10": "c",
+ "-1": "d",
+ "01": "e"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "1": "a",
+ "2": "b",
+ "10": "c",
+ "-1": "d",
+ "01": "e"
+ }
+ }
+ },
+ "keys: long key and value": {
+ "input": "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk=vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
+ "jdk8": {
+ "props": {
+ "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"
+ }
+ }
+ },
+ "keys: long line before short lines": {
+ "input": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=y\na=\\",
+ "jdk8": {
+ "props": {
+ "a": "",
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": "y"
+ }
+ },
+ "jdk9": {
+ "props": {
+ "a": "",
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": "y"
+ }
+ },
+ "jdk11": {
+ "props": {
+ "a": "",
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": "y"
+ }
+ },
+ "jdk13": {
+ "props": {
+ "a": "",
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": "y"
+ }
+ }
+ }
+}
diff --git a/test/cases/parse-snapshot.json b/test/cases/parse-snapshot.json
new file mode 100644
index 0000000..19371b0
--- /dev/null
+++ b/test/cases/parse-snapshot.json
@@ -0,0 +1,881 @@
+{
+ "empty: empty string": {
+ "input": "",
+ "props": {}
+ },
+ "empty: only LF": {
+ "input": "\n",
+ "props": {}
+ },
+ "empty: only CR": {
+ "input": "\r",
+ "props": {}
+ },
+ "empty: only CRLF": {
+ "input": "\r\n",
+ "props": {}
+ },
+ "empty: only whitespace": {
+ "input": " \t\f ",
+ "props": {}
+ },
+ "empty: whitespace lines": {
+ "input": " \n\t\n\f\r\n",
+ "props": {}
+ },
+ "line terminators: LF": {
+ "input": "a=1\nb=2\n",
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "line terminators: CR": {
+ "input": "a=1\rb=2\r",
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "line terminators: CRLF": {
+ "input": "a=1\r\nb=2\r\n",
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "line terminators: LFCR": {
+ "input": "a=1\n\rb=2\n\r",
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "line terminators: mixed": {
+ "input": "a=1\nb=2\rc=3\r\nd=4",
+ "props": {
+ "a": "1",
+ "b": "2",
+ "c": "3",
+ "d": "4"
+ }
+ },
+ "line terminators: no final terminator": {
+ "input": "a=1\nb=2",
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "line terminators: blank lines between": {
+ "input": "a=1\n\n\r\n\r\rb=2",
+ "props": {
+ "a": "1",
+ "b": "2"
+ }
+ },
+ "line terminators: U+2028 and U+2029 are not terminators": {
+ "input": "a=1
b=2
c=3\n",
+ "props": {
+ "a": "1
b=2
c=3"
+ }
+ },
+ "line terminators: NEL (U+0085) is not a terminator": {
+ "input": "a=1
b=2",
+ "props": {
+ "a": "1
b=2"
+ }
+ },
+ "line terminators: vertical tab is not whitespace": {
+ "input": "\u000ba=1",
+ "props": {
+ "\u000ba": "1"
+ }
+ },
+ "separators: equals": {
+ "input": "key=value",
+ "props": {
+ "key": "value"
+ }
+ },
+ "separators: colon": {
+ "input": "key:value",
+ "props": {
+ "key": "value"
+ }
+ },
+ "separators: space": {
+ "input": "key value",
+ "props": {
+ "key": "value"
+ }
+ },
+ "separators: tab": {
+ "input": "key\tvalue",
+ "props": {
+ "key": "value"
+ }
+ },
+ "separators: form feed": {
+ "input": "key\fvalue",
+ "props": {
+ "key": "value"
+ }
+ },
+ "separators: spaces around equals": {
+ "input": "key = value",
+ "props": {
+ "key": "value"
+ }
+ },
+ "separators: spaces around colon": {
+ "input": "key \t: \fvalue",
+ "props": {
+ "key": "value"
+ }
+ },
+ "separators: equals then colon": {
+ "input": "key=:value",
+ "props": {
+ "key": ":value"
+ }
+ },
+ "separators: colon then equals": {
+ "input": "key:=value",
+ "props": {
+ "key": "=value"
+ }
+ },
+ "separators: two equals": {
+ "input": "key==value",
+ "props": {
+ "key": "=value"
+ }
+ },
+ "separators: space then two equals": {
+ "input": "key = = value",
+ "props": {
+ "key": "= value"
+ }
+ },
+ "separators: space then colon then space": {
+ "input": "key : value",
+ "props": {
+ "key": "value"
+ }
+ },
+ "separators: whitespace only separator, then equals in value": {
+ "input": "key value=x",
+ "props": {
+ "key": "value=x"
+ }
+ },
+ "separators: no separator (key only)": {
+ "input": "key",
+ "props": {
+ "key": ""
+ }
+ },
+ "separators: key only with trailing spaces": {
+ "input": "key ",
+ "props": {
+ "key": ""
+ }
+ },
+ "separators: key then separator, no value": {
+ "input": "key=",
+ "props": {
+ "key": ""
+ }
+ },
+ "separators: key then separator and spaces, no value": {
+ "input": "key = ",
+ "props": {
+ "key": ""
+ }
+ },
+ "separators: empty key (equals)": {
+ "input": "=value",
+ "props": {
+ "": "value"
+ }
+ },
+ "separators: empty key (colon)": {
+ "input": ":value",
+ "props": {
+ "": "value"
+ }
+ },
+ "separators: empty key, empty value": {
+ "input": "=",
+ "props": {
+ "": ""
+ }
+ },
+ "separators: leading whitespace before empty key": {
+ "input": " =value",
+ "props": {
+ "": "value"
+ }
+ },
+ "separators: escaped separator in key": {
+ "input": "a\\=b=c",
+ "props": {
+ "a=b": "c"
+ }
+ },
+ "separators: escaped colon in key": {
+ "input": "a\\:b:c",
+ "props": {
+ "a:b": "c"
+ }
+ },
+ "separators: escaped space in key": {
+ "input": "a\\ b c",
+ "props": {
+ "a b": "c"
+ }
+ },
+ "separators: escaped tab in key": {
+ "input": "a\\\tb c",
+ "props": {
+ "a\tb": "c"
+ }
+ },
+ "separators: double backslash before separator": {
+ "input": "a\\\\=b",
+ "props": {
+ "a\\": "b"
+ }
+ },
+ "separators: triple backslash before separator": {
+ "input": "a\\\\\\=b=c",
+ "props": {
+ "a\\=b": "c"
+ }
+ },
+ "separators: separator chars in value": {
+ "input": "k=a=b:c d",
+ "props": {
+ "k": "a=b:c d"
+ }
+ },
+ "separators: value with leading escaped space": {
+ "input": "k=\\ v",
+ "props": {
+ "k": " v"
+ }
+ },
+ "separators: value trailing whitespace kept": {
+ "input": "k=v \t\f",
+ "props": {
+ "k": "v \t\f"
+ }
+ },
+ "comments: hash": {
+ "input": "# comment\nk=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "comments: bang": {
+ "input": "! comment\nk=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "comments: indented hash": {
+ "input": " # comment\nk=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "comments: indented bang with tab": {
+ "input": "\t! comment\nk=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "comments: comment at EOF without terminator": {
+ "input": "k=v\n# comment",
+ "props": {
+ "k": "v"
+ }
+ },
+ "comments: hash inside key is not a comment": {
+ "input": "a#b=c",
+ "props": {
+ "a#b": "c"
+ }
+ },
+ "comments: hash inside value": {
+ "input": "k=v # not a comment",
+ "props": {
+ "k": "v # not a comment"
+ }
+ },
+ "comments: escaped hash key": {
+ "input": "\\#k=v",
+ "props": {
+ "#k": "v"
+ }
+ },
+ "comments: comment ending with backslash does not continue": {
+ "input": "# comment \\\nk=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "comments: comment ending with two backslashes": {
+ "input": "# comment \\\\\nk=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "comments: comment ending with backslash then CRLF": {
+ "input": "# comment \\\r\nk=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "comments: comment ending with backslash at EOF": {
+ "input": "k=v\n# comment \\",
+ "props": {
+ "k": "v"
+ }
+ },
+ "comments: only comments": {
+ "input": "#a\n!b\n # c",
+ "props": {}
+ },
+ "comments: comment with CR terminator": {
+ "input": "# c\rk=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "continuation: simple": {
+ "input": "k=a\\\nb",
+ "props": {
+ "k": "ab"
+ }
+ },
+ "continuation: leading whitespace of next line skipped": {
+ "input": "k=a\\\n \t\fb",
+ "props": {
+ "k": "ab"
+ }
+ },
+ "continuation: CRLF": {
+ "input": "k=a\\\r\nb",
+ "props": {
+ "k": "ab"
+ }
+ },
+ "continuation: CR": {
+ "input": "k=a\\\rb",
+ "props": {
+ "k": "ab"
+ }
+ },
+ "continuation: LFCR is two terminators": {
+ "input": "k=a\\\n\rb=c",
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "continuation: multiple": {
+ "input": "k=a\\\nb\\\nc",
+ "props": {
+ "k": "abc"
+ }
+ },
+ "continuation: in key": {
+ "input": "ke\\\ny=v",
+ "props": {
+ "key": "v"
+ }
+ },
+ "continuation: in separator area": {
+ "input": "k\\\n = v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "continuation: escaped backslash at EOL is not a continuation": {
+ "input": "k=a\\\\\nb=c",
+ "props": {
+ "b": "c",
+ "k": "a\\"
+ }
+ },
+ "continuation: three backslashes at EOL": {
+ "input": "k=a\\\\\\\nb",
+ "props": {
+ "k": "a\\b"
+ }
+ },
+ "continuation: to an empty line": {
+ "input": "k=a\\\n\nb=c",
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "continuation: to a whitespace-only line": {
+ "input": "k=a\\\n \nb=c",
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "continuation: to a comment-looking line": {
+ "input": "k=a\\\n# not a comment",
+ "props": {
+ "k": "a# not a comment"
+ }
+ },
+ "continuation: to a bang-looking line": {
+ "input": "k=a\\\n!x",
+ "props": {
+ "k": "a!x"
+ }
+ },
+ "continuation: at EOF": {
+ "input": "k=v\\",
+ "props": {
+ "k": "v"
+ }
+ },
+ "continuation: at EOF after LF": {
+ "input": "k=v\\\n",
+ "props": {
+ "k": "v"
+ }
+ },
+ "continuation: twice at EOF": {
+ "input": "k=v\\\n\\\n",
+ "props": {
+ "k": "v"
+ }
+ },
+ "continuation: continued line starting with escaped space": {
+ "input": "k=a\\\n\\ b",
+ "props": {
+ "k": "a b"
+ }
+ },
+ "continuation: CR then LF on continued line start": {
+ "input": "k=a\\\r\n\r\nb=c",
+ "props": {
+ "b": "c",
+ "k": "a"
+ }
+ },
+ "lone continuation backslash: alone": {
+ "input": "\\",
+ "props": {}
+ },
+ "lone continuation backslash: alone with LF": {
+ "input": "\\\n",
+ "props": {}
+ },
+ "lone continuation backslash: alone with CRLF": {
+ "input": "\\\r\n",
+ "props": {}
+ },
+ "lone continuation backslash: after property": {
+ "input": "a=1\n\\",
+ "props": {
+ "a": "1"
+ }
+ },
+ "lone continuation backslash: after property, with LF": {
+ "input": "a=1\n\\\n",
+ "props": {
+ "a": "1"
+ }
+ },
+ "lone continuation backslash: indented": {
+ "input": " \\",
+ "props": {}
+ },
+ "lone continuation backslash: followed by whitespace line": {
+ "input": "\\\n ",
+ "props": {}
+ },
+ "lone continuation backslash: followed by empty line": {
+ "input": "\\\n\n",
+ "props": {}
+ },
+ "lone continuation backslash: followed by property": {
+ "input": "\\\nk=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "empty continuation then comment char: hash": {
+ "input": "\\\n#foo=bar\nbaz=1",
+ "props": {
+ "#foo": "bar",
+ "baz": "1"
+ }
+ },
+ "empty continuation then comment char: bang with indentation": {
+ "input": " \\\n!x=1",
+ "props": {
+ "!x": "1"
+ }
+ },
+ "empty continuation then comment char: hash after CRLF": {
+ "input": "\\\r\n#c\r\nk=v",
+ "props": {
+ "#c": "",
+ "k": "v"
+ }
+ },
+ "empty continuation then comment char: hash with leading whitespace on next line": {
+ "input": "\\\n #c\nk=v",
+ "props": {
+ "#c": "",
+ "k": "v"
+ }
+ },
+ "empty continuation then comment char: double continuation then hash": {
+ "input": "\\\n\\\n#c\nk=v",
+ "props": {
+ "#c": "",
+ "k": "v"
+ }
+ },
+ "comment ending with backslash, then backslashes: comment ending with backslash, then backslash-only line": {
+ "input": "#comment \\\n\\\\",
+ "props": {
+ "": ""
+ }
+ },
+ "comment ending with backslash, then backslashes: bang comment, CRLF, lone backslash": {
+ "input": "!\\\r\n\\",
+ "props": {
+ "": ""
+ }
+ },
+ "comment ending with backslash, then backslashes: comment backslash, then key with trailing escaped backslash": {
+ "input": "#\\\nk=\\\\",
+ "props": {
+ "k": "\\"
+ }
+ },
+ "comment ending with backslash, then backslashes: comment with 3 backslashes, then backslashes": {
+ "input": "#\\\\\\\n\\\\\\\\",
+ "props": {
+ "\\": ""
+ }
+ },
+ "stale line buffer: lone backslash decode after comment": {
+ "input": "#\\v\\\r\n\t\\",
+ "props": {
+ "": ""
+ }
+ },
+ "stale line buffer: truncated unicode after longer line": {
+ "input": "abcdef12\na=\\u00",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "stale line buffer: truncated unicode after longer non-hex line": {
+ "input": "abcdef1z\na=\\u00",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "stale line buffer: truncated unicode in key after longer line": {
+ "input": "xxxxxx0041\n\\u00",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "stale line buffer: comment chars before a backslash (skipped since JDK 9)": {
+ "input": "#ab\\\n\\",
+ "props": {
+ "": ""
+ }
+ },
+ "stale line buffer: comment ending with backslash (skipped since JDK 9)": {
+ "input": "#a\\\n\\",
+ "props": {
+ "": ""
+ }
+ },
+ "escapes: standard escapes": {
+ "input": "k=\\t\\n\\r\\f",
+ "props": {
+ "k": "\t\n\r\f"
+ }
+ },
+ "escapes: unknown escapes drop the backslash": {
+ "input": "k=\\a\\b\\c\\d\\e\\g\\z\\0\\9",
+ "props": {
+ "k": "abcdegz09"
+ }
+ },
+ "escapes: escaped backslash": {
+ "input": "k=\\\\",
+ "props": {
+ "k": "\\"
+ }
+ },
+ "escapes: escaped quote chars": {
+ "input": "k=\\\"\\'",
+ "props": {
+ "k": "\"'"
+ }
+ },
+ "escapes: escapes in key": {
+ "input": "\\t\\n\\r\\f\\a=v",
+ "props": {
+ "\t\n\r\fa": "v"
+ }
+ },
+ "escapes: escaped separators in value": {
+ "input": "k=\\=\\:\\ \\#\\!",
+ "props": {
+ "k": "=: #!"
+ }
+ },
+ "escapes: backslash before non-ASCII": {
+ "input": "k=\\é\\❤",
+ "props": {
+ "k": "é❤"
+ }
+ },
+ "escapes: backslash before U+2028": {
+ "input": "k=\\
",
+ "props": {
+ "k": "
"
+ }
+ },
+ "escapes: backslash before NUL": {
+ "input": "k=\\\u0000",
+ "props": {
+ "k": "\u0000"
+ }
+ },
+ "unicode escapes: lowercase hex": {
+ "input": "k=\\u00e9",
+ "props": {
+ "k": "é"
+ }
+ },
+ "unicode escapes: uppercase hex": {
+ "input": "k=\\u00E9",
+ "props": {
+ "k": "é"
+ }
+ },
+ "unicode escapes: mixed case hex": {
+ "input": "k=\\uAbCd",
+ "props": {
+ "k": "ꯍ"
+ }
+ },
+ "unicode escapes: in key": {
+ "input": "\\u0041=v",
+ "props": {
+ "A": "v"
+ }
+ },
+ "unicode escapes: NUL": {
+ "input": "k=\\u0000",
+ "props": {
+ "k": "\u0000"
+ }
+ },
+ "unicode escapes: surrogate pair": {
+ "input": "k=\\uD83D\\uDC51",
+ "props": {
+ "k": "👑"
+ }
+ },
+ "unicode escapes: lone high surrogate": {
+ "input": "k=\\uD83D",
+ "props": {
+ "k": "\ud83d"
+ }
+ },
+ "unicode escapes: lone low surrogate": {
+ "input": "k=\\uDC51",
+ "props": {
+ "k": "\udc51"
+ }
+ },
+ "unicode escapes: escaped separator via unicode is not a separator": {
+ "input": "k\\u003Dx=v",
+ "props": {
+ "k=x": "v"
+ }
+ },
+ "unicode escapes: unicode escaped backslash": {
+ "input": "k=\\u005Cn",
+ "props": {
+ "k": "\\n"
+ }
+ },
+ "unicode escapes: followed by more chars": {
+ "input": "k=\\u00410",
+ "props": {
+ "k": "A0"
+ }
+ },
+ "unicode escapes: uppercase U is not an escape": {
+ "input": "k=\\U0041",
+ "props": {
+ "k": "U0041"
+ }
+ },
+ "unicode escapes: escaped u": {
+ "input": "k=\\\\u0041",
+ "props": {
+ "k": "\\u0041"
+ }
+ },
+ "unicode escapes: malformed: non-hex": {
+ "input": "k=\\u00g1",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "unicode escapes: malformed: too short at end": {
+ "input": "k=\\u12",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "unicode escapes: malformed: empty at end": {
+ "input": "k=\\u",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "unicode escapes: malformed: in key": {
+ "input": "\\uZZZZ=v",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "unicode escapes: malformed: short then separator": {
+ "input": "k\\u12=v",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "unicode escapes: malformed: spans continuation": {
+ "input": "k=\\u00\\\n41",
+ "props": {
+ "k": "A"
+ }
+ },
+ "unicode escapes: malformed: plus sign": {
+ "input": "k=\\u+123",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "unicode escapes: malformed: fullwidth digits": {
+ "input": "k=\\u0041",
+ "throws": "Error",
+ "message": "Malformed \\uxxxx encoding."
+ },
+ "raw characters: non-ASCII key and value": {
+ "input": "clé=valeur é ❤",
+ "props": {
+ "clé": "valeur é ❤"
+ }
+ },
+ "raw characters: emoji (surrogate pair)": {
+ "input": "k=👑",
+ "props": {
+ "k": "👑"
+ }
+ },
+ "raw characters: lone surrogate": {
+ "input": "k=\ud83d",
+ "props": {
+ "k": "\ud83d"
+ }
+ },
+ "raw characters: NUL char": {
+ "input": "k=a\u0000b",
+ "props": {
+ "k": "a\u0000b"
+ }
+ },
+ "raw characters: control chars": {
+ "input": "k=\u0001\u001f",
+ "props": {
+ "k": "\u0001\u001f"
+ }
+ },
+ "raw characters: BOM is not stripped": {
+ "input": "k=v",
+ "props": {
+ "k": "v"
+ }
+ },
+ "raw characters: BOM on second line": {
+ "input": "a=1\nk=v",
+ "props": {
+ "a": "1",
+ "k": "v"
+ }
+ },
+ "raw characters: non-breaking space is not whitespace": {
+ "input": " k=v",
+ "props": {
+ " k": "v"
+ }
+ },
+ "raw characters: fullwidth equals is not a separator": {
+ "input": "k=v",
+ "props": {
+ "k=v": ""
+ }
+ },
+ "keys: duplicate keys: last wins": {
+ "input": "k=1\nk=2\nk=3",
+ "props": {
+ "k": "3"
+ }
+ },
+ "keys: duplicate via escapes": {
+ "input": "k=1\n\\k=2",
+ "props": {
+ "k": "2"
+ }
+ },
+ "keys: proto-like keys": {
+ "input": "__proto__=a\nconstructor=b\nhasOwnProperty=c\ntoString=d",
+ "props": {
+ "__proto__": "a",
+ "constructor": "b",
+ "hasOwnProperty": "c",
+ "toString": "d"
+ }
+ },
+ "keys: numeric keys": {
+ "input": "2=b\n10=c\n1=a\n-1=d\n01=e",
+ "props": {
+ "1": "a",
+ "2": "b",
+ "10": "c",
+ "-1": "d",
+ "01": "e"
+ }
+ },
+ "keys: long key and value": {
+ "input": "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk=vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
+ "props": {
+ "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"
+ }
+ },
+ "keys: long line before short lines": {
+ "input": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=y\na=\\",
+ "props": {
+ "a": "",
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": "y"
+ }
+ }
+}
diff --git a/test/cases/stringify-cases.mts b/test/cases/stringify-cases.mts
new file mode 100644
index 0000000..68d1324
--- /dev/null
+++ b/test/cases/stringify-cases.mts
@@ -0,0 +1,66 @@
+// Cases for stringify() with javaVersion. Java results: stringify-expected.json (`pnpm run reference:gen`).
+// Strings are JS strings: '\\' is a single backslash.
+
+export interface StringifyCase {
+ name: string;
+ props: Record;
+ comments?: string;
+}
+
+export interface DateCase {
+ name: string;
+ /** Epoch time in milliseconds */
+ time: number;
+ /** Time zone id for Java's TimeZone.getTimeZone() */
+ javaTimeZone: string;
+ /** Same time zone, for the `timeZone` option */
+ timeZone: string;
+}
+
+export const stringifyCases: StringifyCase[] = [
+ {name: 'empty', props: {}},
+ {name: 'simple', props: {a: '1', b: 'two words'}},
+ {name: 'order', props: {b: '1', a: '2', '10': '3', '2': '4', B: '5', é: '6', '': '7'}},
+ {name: 'separators and comment chars', props: {'k=:#!': 'v=:#!', '=': ':', '#': '!'}},
+ {name: 'spaces', props: {' k e y ': ' v a l u e ', ' ': ' ', tab: '\t', ff: '\f'}},
+ {name: 'line terminators', props: {'a\nb': 'c\rd', 'e\r\n': '\n\n'}},
+ {name: 'backslashes', props: {'\\': '\\\\', 'a\\b': 'c\\'}},
+ {name: 'control chars', props: {'\u0000': '\u0001\u001f', del: '\u007f', vt: '\u000b'}},
+ {name: 'non-ASCII', props: {clé: 'é ÿ', Ā: '❤ 👑', bom: '\ufeff', nbsp: '\u00a0', lone: '\ud83d', ls: '\u2028'}},
+ {name: 'proto-like keys', props: {['__proto__']: 'a', constructor: 'b'}},
+ {name: 'comments: simple', props: {k: 'v'}, comments: 'Generated file'},
+ {name: 'comments: empty', props: {}, comments: ''},
+ {name: 'comments: multi-line', props: {}, comments: 'line 1\nline 2\r\nline 3\rline 4'},
+ {name: 'comments: line starting with # or !', props: {}, comments: 'a\n# b\n! c\n # d'},
+ {name: 'comments: trailing line terminators', props: {}, comments: 'a\n'},
+ {name: 'comments: CRLF and CR at the end', props: {}, comments: 'a\r\n\r'},
+ {name: 'comments: non-ASCII', props: {}, comments: 'é ÿ Ā ❤ 👑 \u0000 \t'},
+]
+ // `__proto__` must be a data key
+ .map((c) => ({...c, props: Object.assign(Object.create(null), c.props)}));
+
+// Jan 15 and Jul 15 2026, 13:04:05.678 UTC
+const WINTER = Date.UTC(2026, 0, 15, 13, 4, 5, 678);
+const SUMMER = Date.UTC(2026, 6, 15, 13, 4, 5, 678);
+
+// Time zones where the date comment is exact (see the `timeZone` option)
+export const dateCases: DateCase[] = [
+ {name: 'UTC', time: WINTER, javaTimeZone: 'UTC', timeZone: 'UTC'},
+ {name: 'fixed positive offset', time: WINTER, javaTimeZone: 'GMT+02:00', timeZone: '+02:00'},
+ {name: 'fixed negative offset', time: WINTER, javaTimeZone: 'GMT-09:30', timeZone: '-09:30'},
+ {name: 'fixed zero offset', time: WINTER, javaTimeZone: 'GMT+00:00', timeZone: '+00:00'},
+ {name: 'day change', time: Date.UTC(2026, 11, 31, 23, 30), javaTimeZone: 'GMT+01:00', timeZone: '+01:00'},
+ {name: 'single digit day', time: Date.UTC(2026, 1, 3, 1, 2, 3), javaTimeZone: 'UTC', timeZone: 'UTC'},
+ {name: 'America/New_York winter', time: WINTER, javaTimeZone: 'America/New_York', timeZone: 'America/New_York'},
+ {name: 'America/New_York summer', time: SUMMER, javaTimeZone: 'America/New_York', timeZone: 'America/New_York'},
+ {name: 'Europe/Paris winter', time: WINTER, javaTimeZone: 'Europe/Paris', timeZone: 'Europe/Paris'},
+ {name: 'Europe/Paris summer', time: SUMMER, javaTimeZone: 'Europe/Paris', timeZone: 'Europe/Paris'},
+ {name: 'Europe/London winter', time: WINTER, javaTimeZone: 'Europe/London', timeZone: 'Europe/London'},
+ {name: 'Europe/London summer', time: SUMMER, javaTimeZone: 'Europe/London', timeZone: 'Europe/London'},
+ {
+ name: 'America/Sao_Paulo (no name in Java)',
+ time: WINTER,
+ javaTimeZone: 'America/Sao_Paulo',
+ timeZone: 'America/Sao_Paulo',
+ },
+];
diff --git a/test/cases/stringify-expected.json b/test/cases/stringify-expected.json
new file mode 100644
index 0000000..7ad8120
--- /dev/null
+++ b/test/cases/stringify-expected.json
@@ -0,0 +1,205 @@
+{
+ "cases": {
+ "empty": {
+ "props": {},
+ "writer": "",
+ "stream": ""
+ },
+ "simple": {
+ "props": {
+ "a": "1",
+ "b": "two words"
+ },
+ "writer": "a=1\nb=two words\n",
+ "stream": "a=1\nb=two words\n"
+ },
+ "order": {
+ "props": {
+ "2": "4",
+ "10": "3",
+ "b": "1",
+ "a": "2",
+ "B": "5",
+ "é": "6",
+ "": "7"
+ },
+ "writer": "=7\n10=3\n2=4\nB=5\na=2\nb=1\né=6\n",
+ "stream": "=7\n10=3\n2=4\nB=5\na=2\nb=1\n\\u00E9=6\n"
+ },
+ "separators and comment chars": {
+ "props": {
+ "k=:#!": "v=:#!",
+ "=": ":",
+ "#": "!"
+ },
+ "writer": "\\#=\\!\n\\==\\:\nk\\=\\:\\#\\!=v\\=\\:\\#\\!\n",
+ "stream": "\\#=\\!\n\\==\\:\nk\\=\\:\\#\\!=v\\=\\:\\#\\!\n"
+ },
+ "spaces": {
+ "props": {
+ " k e y ": " v a l u e ",
+ " ": " ",
+ "tab": "\t",
+ "ff": "\f"
+ },
+ "writer": "\\ \\ =\\ \n\\ k\\ e\\ y\\ =\\ v a l u e \nff=\\f\ntab=\\t\n",
+ "stream": "\\ \\ =\\ \n\\ k\\ e\\ y\\ =\\ v a l u e \nff=\\f\ntab=\\t\n"
+ },
+ "line terminators": {
+ "props": {
+ "a\nb": "c\rd",
+ "e\r\n": "\n\n"
+ },
+ "writer": "a\\nb=c\\rd\ne\\r\\n=\\n\\n\n",
+ "stream": "a\\nb=c\\rd\ne\\r\\n=\\n\\n\n"
+ },
+ "backslashes": {
+ "props": {
+ "\\": "\\\\",
+ "a\\b": "c\\"
+ },
+ "writer": "\\\\=\\\\\\\\\na\\\\b=c\\\\\n",
+ "stream": "\\\\=\\\\\\\\\na\\\\b=c\\\\\n"
+ },
+ "control chars": {
+ "props": {
+ "\u0000": "\u0001\u001f",
+ "del": "",
+ "vt": "\u000b"
+ },
+ "writer": "\u0000=\u0001\u001f\ndel=\nvt=\u000b\n",
+ "stream": "\\u0000=\\u0001\\u001F\ndel=\\u007F\nvt=\\u000B\n"
+ },
+ "non-ASCII": {
+ "props": {
+ "clé": "é ÿ",
+ "Ā": "❤ 👑",
+ "bom": "",
+ "nbsp": " ",
+ "lone": "\ud83d",
+ "ls": "
"
+ },
+ "writer": "bom=\nclé=é ÿ\nlone=\ud83d\nls=
\nnbsp= \nĀ=❤ 👑\n",
+ "stream": "bom=\\uFEFF\ncl\\u00E9=\\u00E9 \\u00FF\nlone=\\uD83D\nls=\\u2028\nnbsp=\\u00A0\n\\u0100=\\u2764 \\uD83D\\uDC51\n"
+ },
+ "proto-like keys": {
+ "props": {
+ "__proto__": "a",
+ "constructor": "b"
+ },
+ "writer": "__proto__=a\nconstructor=b\n",
+ "stream": "__proto__=a\nconstructor=b\n"
+ },
+ "comments: simple": {
+ "props": {
+ "k": "v"
+ },
+ "comments": "Generated file",
+ "writer": "#Generated file\nk=v\n",
+ "stream": "#Generated file\nk=v\n"
+ },
+ "comments: empty": {
+ "props": {},
+ "comments": "",
+ "writer": "#\n",
+ "stream": "#\n"
+ },
+ "comments: multi-line": {
+ "props": {},
+ "comments": "line 1\nline 2\r\nline 3\rline 4",
+ "writer": "#line 1\n#line 2\n#line 3\n#line 4\n",
+ "stream": "#line 1\n#line 2\n#line 3\n#line 4\n"
+ },
+ "comments: line starting with # or !": {
+ "props": {},
+ "comments": "a\n# b\n! c\n # d",
+ "writer": "#a\n# b\n! c\n# # d\n",
+ "stream": "#a\n# b\n! c\n# # d\n"
+ },
+ "comments: trailing line terminators": {
+ "props": {},
+ "comments": "a\n",
+ "writer": "#a\n#\n",
+ "stream": "#a\n#\n"
+ },
+ "comments: CRLF and CR at the end": {
+ "props": {},
+ "comments": "a\r\n\r",
+ "writer": "#a\n#\n#\n",
+ "stream": "#a\n#\n#\n"
+ },
+ "comments: non-ASCII": {
+ "props": {},
+ "comments": "é ÿ Ā ❤ 👑 \u0000 \t",
+ "writer": "#é ÿ \\u0100 \\u2764 \\uD83D\\uDC51 \u0000 \t\n",
+ "stream": "#é ÿ \\u0100 \\u2764 \\uD83D\\uDC51 \u0000 \t\n"
+ }
+ },
+ "dates": {
+ "UTC": {
+ "time": 1768482245678,
+ "timeZone": "UTC",
+ "java": "Thu Jan 15 13:04:05 UTC 2026"
+ },
+ "fixed positive offset": {
+ "time": 1768482245678,
+ "timeZone": "+02:00",
+ "java": "Thu Jan 15 15:04:05 GMT+02:00 2026"
+ },
+ "fixed negative offset": {
+ "time": 1768482245678,
+ "timeZone": "-09:30",
+ "java": "Thu Jan 15 03:34:05 GMT-09:30 2026"
+ },
+ "fixed zero offset": {
+ "time": 1768482245678,
+ "timeZone": "+00:00",
+ "java": "Thu Jan 15 13:04:05 GMT+00:00 2026"
+ },
+ "day change": {
+ "time": 1798759800000,
+ "timeZone": "+01:00",
+ "java": "Fri Jan 01 00:30:00 GMT+01:00 2027"
+ },
+ "single digit day": {
+ "time": 1770080523000,
+ "timeZone": "UTC",
+ "java": "Tue Feb 03 01:02:03 UTC 2026"
+ },
+ "America/New_York winter": {
+ "time": 1768482245678,
+ "timeZone": "America/New_York",
+ "java": "Thu Jan 15 08:04:05 EST 2026"
+ },
+ "America/New_York summer": {
+ "time": 1784120645678,
+ "timeZone": "America/New_York",
+ "java": "Wed Jul 15 09:04:05 EDT 2026"
+ },
+ "Europe/Paris winter": {
+ "time": 1768482245678,
+ "timeZone": "Europe/Paris",
+ "java": "Thu Jan 15 14:04:05 CET 2026"
+ },
+ "Europe/Paris summer": {
+ "time": 1784120645678,
+ "timeZone": "Europe/Paris",
+ "java": "Wed Jul 15 15:04:05 CEST 2026"
+ },
+ "Europe/London winter": {
+ "time": 1768482245678,
+ "timeZone": "Europe/London",
+ "java": "Thu Jan 15 13:04:05 GMT 2026"
+ },
+ "Europe/London summer": {
+ "time": 1784120645678,
+ "timeZone": "Europe/London",
+ "java": "Wed Jul 15 14:04:05 BST 2026"
+ },
+ "America/Sao_Paulo (no name in Java)": {
+ "time": 1768482245678,
+ "timeZone": "America/Sao_Paulo",
+ "java": "Thu Jan 15 10:04:05 GMT-03:00 2026"
+ }
+ }
+}
diff --git a/test/compat/api-snapshot.json b/test/compat/api-snapshot.json
new file mode 100644
index 0000000..1bc1c40
--- /dev/null
+++ b/test/compat/api-snapshot.json
@@ -0,0 +1,40 @@
+{
+ "main": [
+ "default: object",
+ "escapeKey: function",
+ "escapeValue: function",
+ "parse: function",
+ "parseFile: function",
+ "readEntries: function",
+ "stringify: function"
+ ],
+ "mainDefault": [
+ "parse: function",
+ "parseFile: function",
+ "stringify: function"
+ ],
+ "browser": [
+ "default: object",
+ "escapeKey: function",
+ "escapeValue: function",
+ "parse: function",
+ "readEntries: function",
+ "stringify: function"
+ ],
+ "browserDefault": [
+ "parse: function",
+ "stringify: function"
+ ],
+ "utils": [
+ "LineReader: function",
+ "convertLine: function",
+ "decodeLine: function",
+ "encodeLine: function",
+ "rawParse: function",
+ "rawParseLine: function"
+ ],
+ "lineReader": [
+ "constructor",
+ "readLine"
+ ]
+}
diff --git a/test/compat/check-esm.mjs b/test/compat/check-esm.mjs
new file mode 100644
index 0000000..3184b71
--- /dev/null
+++ b/test/compat/check-esm.mjs
@@ -0,0 +1,38 @@
+// ESM consumers of the built package (dist/). Runs on Node.js >= 12.20 (named imports from CommonJS).
+// Keep this file in Node 12 syntax (no top-level await).
+import assert from 'assert';
+import mainDefault, {
+ escapeKey,
+ escapeValue,
+ parse,
+ parseFile,
+ readEntries,
+ stringify,
+} from '../../dist/node-java-props.js';
+import browserDefault, * as browser from '../../dist/java-props.js';
+import {decodeLine, encodeLine, LineReader, rawParse, rawParseLine, convertLine} from '../../dist/utils.js';
+
+// `import x from` gives module.exports (CommonJS), whose `default` is the deprecated default export
+assert.strictEqual(mainDefault.parse, parse);
+assert.strictEqual(mainDefault.default.parseFile, parseFile);
+assert.strictEqual(browserDefault.stringify, stringify);
+assert.strictEqual(browser.parse, parse);
+assert.strictEqual(convertLine, decodeLine);
+for (const fn of [encodeLine, rawParse, rawParseLine]) assert.strictEqual(typeof fn, 'function');
+assert.strictEqual(new LineReader('a=b').readLine(), 'a=b');
+
+assert.deepStrictEqual(Object.assign({}, parse('a=1\nb : 2')), {a: '1', b: '2'});
+assert.strictEqual(stringify({a: '1'}), 'a: 1\n');
+assert.strictEqual(escapeKey('a b') + escapeValue(' c'), 'a\\ b\\ c');
+const reader = readEntries('k=v', {javaVersion: 25});
+assert.ok(reader.next() && reader.key === 'k' && reader.value === 'v');
+parseFile(new URL('../fixtures/backslashes.properties', import.meta.url)).then(
+ (props) => {
+ assert.ok(Object.keys(props).length > 0);
+ console.log(`OK: ESM imports work on Node.js ${process.version}`);
+ },
+ (err) => {
+ console.error(err);
+ process.exitCode = 1;
+ }
+);
diff --git a/test/compat/check-package.mjs b/test/compat/check-package.mjs
new file mode 100644
index 0000000..036c00e
--- /dev/null
+++ b/test/compat/check-package.mjs
@@ -0,0 +1,49 @@
+// Checks the npm package (dev only: needs a recent Node.js, npm and a build).
+import assert from 'node:assert/strict';
+import {execFileSync} from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+
+const ROOT = path.join(import.meta.dirname, '../..');
+const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
+
+// Entry points. Users also import `java-props/dist/*` directly: every dist module is public.
+assert.equal(pkg.main, 'dist/node-java-props.js');
+assert.equal(pkg.browser, 'dist/java-props.js');
+const [pack] = JSON.parse(
+ execFileSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], {
+ cwd: ROOT,
+ shell: process.platform === 'win32', // npm.cmd
+ })
+);
+const files = pack.files.map((f) => f.path);
+const required = ['java-props', 'node-java-props', 'utils'].flatMap((name) =>
+ ['.js', '.d.ts', '.js.map'].map((ext) => `dist/${name}${ext}`)
+);
+assert.deepEqual(
+ required.filter((file) => !files.includes(file)),
+ [],
+ 'files missing from the package'
+);
+
+// Fields that would break existing users
+assert.equal(pkg.exports, undefined, '"exports" would block the `java-props/dist/*` imports');
+assert.ok(pkg.type === undefined || pkg.type === 'commonjs', 'the package must stay CommonJS');
+assert.equal(pkg.engines, undefined, '"engines" could reject installs on old Node.js versions');
+assert.equal(pkg.dependencies, undefined, 'no runtime dependencies');
+
+// The browser entry, and every module it requires, must not require Node.js modules
+const requires = (file) =>
+ [...fs.readFileSync(path.join(ROOT, file), 'utf8').matchAll(/require\("([^"]+)"\)/g)].map((m) => m[1]);
+const seen = new Set();
+const visit = (file) => {
+ if (seen.has(file)) return;
+ seen.add(file);
+ for (const dep of requires(file)) {
+ assert.ok(dep.startsWith('./') || dep.startsWith('../'), `${file} requires ${dep}`);
+ visit(path.join(path.dirname(file), `${dep}.js`));
+ }
+};
+visit(pkg.browser);
+
+console.log(`OK: package content (${files.length} files)`);
diff --git a/test/compat/check-types.mjs b/test/compat/check-types.mjs
new file mode 100644
index 0000000..c23d888
--- /dev/null
+++ b/test/compat/check-types.mjs
@@ -0,0 +1,64 @@
+// Compiles test/compat/types/consumer.ts against the package declarations with a given TypeScript version.
+// Usage: node test/compat/check-types.mjs (needs a build)
+import {execFileSync} from 'node:child_process';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const ROOT = path.join(import.meta.dirname, '../..');
+const tsVersion = process.argv[2];
+if (!tsVersion) throw new Error('Usage: check-types.mjs ');
+
+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'java-props-types-'));
+try {
+ // Fake installed package (old TypeScript versions only use "main" when the .js file exists)
+ const installed = path.join(tmp, 'node_modules/java-props');
+ fs.mkdirSync(installed, {recursive: true});
+ fs.copyFileSync(path.join(ROOT, 'package.json'), path.join(installed, 'package.json'));
+ fs.cpSync(path.join(ROOT, 'dist'), path.join(installed, 'dist'), {recursive: true});
+ fs.cpSync(path.join(import.meta.dirname, 'types/stub'), path.join(tmp, 'types'), {recursive: true});
+ fs.copyFileSync(path.join(import.meta.dirname, 'types/consumer.ts'), path.join(tmp, 'consumer.ts'));
+
+ const tsc = (args) =>
+ execFileSync('pnpm', ['--silent', 'dlx', '--package', `typescript@${tsVersion}`, 'tsc', ...args], {
+ cwd: tmp,
+ encoding: 'utf8',
+ });
+ const major = Number(
+ tsc(['--version'])
+ .replace(/^Version /, '')
+ .split('.')[0]
+ );
+
+ const base = {noEmit: true, noImplicitAny: true, strictNullChecks: true, typeRoots: ['./types'], types: ['node']};
+ const configs = {
+ node10: {...base, module: 'commonjs', moduleResolution: 'node', target: 'es6', lib: ['es6']},
+ nodenext: {...base, module: 'nodenext', moduleResolution: 'nodenext', target: 'es2022', lib: ['es2022']},
+ bundler: {...base, module: 'preserve', moduleResolution: 'bundler', target: 'es2022', lib: ['es2022']},
+ };
+ // node10 resolution is deprecated in TypeScript 6 and removed in 7; module "preserve" needs TypeScript 5.4+
+ if (major === 6) configs.node10.ignoreDeprecations = '6.0';
+ const names =
+ major >= 7
+ ? ['nodenext', 'bundler']
+ : major === 6
+ ? ['node10', 'nodenext', 'bundler']
+ : major === 5
+ ? ['node10', 'nodenext']
+ : ['node10'];
+ for (const name of names) {
+ fs.writeFileSync(
+ path.join(tmp, 'tsconfig.json'),
+ JSON.stringify({compilerOptions: configs[name], files: ['consumer.ts']})
+ );
+ try {
+ tsc(['-p', '.']);
+ } catch (err) {
+ console.error(err.stdout || err);
+ throw new Error(`TypeScript ${tsVersion} (${name}) failed`);
+ }
+ console.log(`OK: TypeScript ${tsVersion} (${name})`);
+ }
+} finally {
+ fs.rmSync(tmp, {recursive: true, force: true});
+}
diff --git a/test/compat/run-smoke.js b/test/compat/run-smoke.js
new file mode 100644
index 0000000..2ba5304
--- /dev/null
+++ b/test/compat/run-smoke.js
@@ -0,0 +1,199 @@
+// Smoke test of the built package (dist/). Runs on every supported Node.js version (>= 6), so: Node 6 syntax (no
+// async/await, object spread, Object.entries/values, optional catch binding or trailing commas in calls), and no
+// import of test/support (hence its own copies of a few helpers).
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '../..');
+const FIXTURES_DIR = path.join(ROOT, 'test/fixtures');
+
+const main = require(ROOT); // package.json "main"
+const browser = require(path.join(ROOT, require(path.join(ROOT, 'package.json')).browser));
+const utils = require(path.join(ROOT, 'dist/utils'));
+
+let count = 0;
+function check(name, fn) {
+ try {
+ fn();
+ count++;
+ } catch (err) {
+ console.error('FAILED: ' + name);
+ throw err;
+ }
+}
+
+function sortedEntries(obj) {
+ return Object.keys(obj)
+ .sort()
+ .map((key) => [key, obj[key]]);
+}
+
+// API: same as the snapshot of the sources (test/unit/api.test.ts)
+function shape(mod) {
+ return Object.keys(mod)
+ .filter((key) => key !== '__esModule')
+ .map((key) => key + ': ' + typeof mod[key])
+ .sort();
+}
+const api = JSON.parse(fs.readFileSync(path.join(__dirname, 'api-snapshot.json'), 'utf8'));
+check('API shape', () => {
+ assert.deepStrictEqual(
+ {
+ main: shape(main),
+ mainDefault: shape(main.default),
+ browser: shape(browser),
+ browserDefault: shape(browser.default),
+ utils: shape(utils),
+ lineReader: Object.getOwnPropertyNames(utils.LineReader.prototype).sort(),
+ },
+ api
+ );
+ assert.strictEqual(main.parse, browser.parse);
+ assert.strictEqual(main.stringify, browser.stringify);
+ assert.strictEqual(main.default.parse, main.parse);
+ assert.strictEqual(main.default.parseFile, main.parseFile);
+ assert.strictEqual(browser.default.stringify, browser.stringify);
+ assert.strictEqual(utils.convertLine, utils.decodeLine);
+});
+
+// Fixtures
+fs.readdirSync(FIXTURES_DIR)
+ .filter((file) => /\.properties$/.test(file))
+ .forEach((file) => {
+ check('fixture ' + file, () => {
+ const input = fs.readFileSync(path.join(FIXTURES_DIR, file), 'utf8');
+ const expected = JSON.parse(
+ fs.readFileSync(
+ path.join(FIXTURES_DIR, file.replace(/(\.crlf)?\.properties$/, '.expected.json')),
+ 'utf8'
+ )
+ );
+ const props = main.parse(input);
+ assert.deepStrictEqual(sortedEntries(props), sortedEntries(expected));
+ assert.deepStrictEqual(sortedEntries(main.parse(main.stringify(props))), sortedEntries(expected));
+ });
+ });
+
+// Edge cases: same results as the sources (test/unit/parse-cases.test.ts)
+const cases = JSON.parse(fs.readFileSync(path.join(ROOT, 'test/cases/parse-snapshot.json'), 'utf8'));
+Object.keys(cases).forEach((name) => {
+ check('case ' + name, () => {
+ const c = cases[name];
+ let res;
+ try {
+ res = {props: sortedEntries(main.parse(c.input))};
+ } catch (err) {
+ res = {throws: err.message};
+ }
+ assert.deepStrictEqual(res, c.throws ? {throws: c.message} : {props: sortedEntries(c.props)});
+ });
+});
+
+// Edge cases with javaVersion: same results as Java (test/unit/parse-cases.test.ts)
+const ERA_VERSIONS = {jdk8: 8, jdk9: 9, jdk11: '11.0.5', jdk13: 13};
+const javaCases = JSON.parse(fs.readFileSync(path.join(ROOT, 'test/cases/parse-expected.json'), 'utf8'));
+Object.keys(ERA_VERSIONS).forEach((era) => {
+ check('cases with javaVersion ' + ERA_VERSIONS[era], () => {
+ Object.keys(javaCases).forEach((name) => {
+ const c = javaCases[name];
+ let res;
+ try {
+ res = {props: sortedEntries(main.parse(c.input, {javaVersion: ERA_VERSIONS[era]}))};
+ } catch (err) {
+ res = {throws: err.message};
+ }
+ const java = c[era];
+ assert.deepStrictEqual(
+ res,
+ java.throws ? {throws: java.message} : {props: sortedEntries(java.props)},
+ name
+ );
+ });
+ });
+});
+
+// stringify with javaVersion: same output as Java (test/unit/java-props.test.ts)
+const storeCases = JSON.parse(fs.readFileSync(path.join(ROOT, 'test/cases/stringify-expected.json'), 'utf8'));
+check('stringify with javaVersion', () => {
+ Object.keys(storeCases.cases).forEach((name) => {
+ const c = storeCases.cases[name];
+ const options = {javaVersion: 25, comments: c.comments, date: false, lineSeparator: '\n'};
+ assert.strictEqual(main.stringify(c.props, options), c.writer, name);
+ options.escapeUnicode = true;
+ assert.strictEqual(main.stringify(c.props, options), c.stream, name);
+ });
+ // Exact time zones only: names of the others depend on the Intl support of each Node.js version
+ Object.keys(storeCases.dates).forEach((name) => {
+ const d = storeCases.dates[name];
+ if (d.timeZone !== 'UTC' && !/^[+-]/.test(d.timeZone)) return;
+ const options = {javaVersion: 25, date: new Date(d.time), timeZone: d.timeZone, lineSeparator: '\n'};
+ assert.strictEqual(main.stringify({}, options), '#' + d.java + '\n', name);
+ });
+});
+
+// readEntries and escapeKey/escapeValue
+check('readEntries', () => {
+ const reader = main.readEntries('# c\nb=\\u00e9\r\na : 1\\\n 2', {positions: true});
+ const entries = [];
+ while (reader.next()) {
+ entries.push([
+ reader.key,
+ reader.value,
+ reader.rawValue,
+ reader.start,
+ reader.end,
+ reader.startLine,
+ reader.startColumn,
+ ]);
+ }
+ assert.deepStrictEqual(entries, [
+ ['b', '\u00e9', '\\u00e9', 4, 12, 2, 1],
+ ['a', '12', '12', 14, 24, 3, 1],
+ ]);
+ assert.strictEqual(browser.readEntries, main.readEntries);
+ assert.strictEqual(main.escapeKey('k =é'), 'k\\ \\=\\u00E9');
+ assert.strictEqual(main.escapeValue(' v=é', {javaVersion: 17}), '\\ v\\=é');
+});
+
+// stringify
+check('stringify', () => {
+ assert.strictEqual(main.stringify({a: 'b c', ' k=': ' é\n'}), 'a: b c\n\\ k\\=: \\ \\u00E9\\n\n');
+ const nullProto = Object.create(null);
+ nullProto.k = 'v';
+ assert.strictEqual(main.stringify(nullProto), 'k: v\n');
+});
+
+// parseFile
+const file = path.join(FIXTURES_DIR, 'syntax.properties');
+const expected = JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, 'syntax.expected.json'), 'utf8'));
+main.parseFile(file)
+ .then((props) => {
+ check('parseFile', () => {
+ assert.deepStrictEqual(sortedEntries(props), sortedEntries(expected));
+ });
+ return main.parseFile(file, 'utf8');
+ })
+ .then((props) => {
+ check('parseFile with encoding', () => {
+ assert.deepStrictEqual(sortedEntries(props), sortedEntries(expected));
+ });
+ return main.parseFile(path.join(FIXTURES_DIR, 'nonexistent.properties')).then(
+ () => {
+ throw new Error('parseFile must reject');
+ },
+ (err) => {
+ check('parseFile rejection', () => {
+ assert.strictEqual(err.code, 'ENOENT');
+ });
+ }
+ );
+ })
+ .then(() => {
+ console.log('OK: ' + count + ' checks passed on Node.js ' + process.version);
+ })
+ .catch((err) => {
+ console.error(err);
+ process.exitCode = 1;
+ });
diff --git a/test/compat/types/consumer.ts b/test/compat/types/consumer.ts
new file mode 100644
index 0000000..cdb71b1
--- /dev/null
+++ b/test/compat/types/consumer.ts
@@ -0,0 +1,76 @@
+// Uses every public declaration. Compiled by TypeScript 2.9+ (check-types.mjs): keep it in TypeScript 2.9 syntax.
+import javaProps = require('java-props');
+import defaultProps from 'java-props';
+import {parse, parseFile, stringify, Properties, ParseOptions, ParseFileOptions, StringifyOptions} from 'java-props';
+import {readEntries, escapeKey, escapeValue, EntryReader, EntryPositions, EscapeOptions} from 'java-props';
+import * as browser from 'java-props/dist/java-props';
+import browserDefault from 'java-props/dist/java-props';
+import {
+ LineReader,
+ ParsedLine,
+ rawParse,
+ rawParseLine,
+ decodeLine,
+ encodeLine,
+ convertLine,
+} from 'java-props/dist/utils';
+
+const p1: Properties = javaProps.parse('a=b');
+const p2: Properties = parse('a=b');
+const s: string = stringify(p1) + javaProps.stringify(p2) + browser.stringify(p2) + browserDefault.stringify(p2);
+const p3: Properties = defaultProps.parse(s) || browser.parse(s) || browserDefault.parse(s);
+const value: string = p3['a'];
+
+const promises: Array> = [
+ parseFile('file'),
+ parseFile('file', 'latin1'),
+ parseFile(Buffer.from('file')),
+ parseFile(new URL('file:///file')),
+ parseFile(3),
+ javaProps.parseFile('file'),
+ defaultProps.parseFile('file'),
+];
+
+const lr = new LineReader('a=b');
+const line: string | undefined = lr.readLine();
+const pos: number = lr.pos + lr.strLen;
+const str: string = lr.str;
+rawParse(s, (res: ParsedLine) => {
+ const n: number = res.sepStart + res.valueStart;
+ const l: string = res.line;
+});
+const parsed: ParsedLine = rawParseLine('a=b');
+const decoded: string = decodeLine('a') + encodeLine('a') + encodeLine('a', true) + convertLine('a');
+
+// Options
+const parseOptions: ParseOptions = {javaVersion: 8};
+const fileOptions: ParseFileOptions = {javaVersion: '11.0.4', encoding: 'latin1'};
+const stringifyOptions: StringifyOptions = {
+ javaVersion: 'latest',
+ separator: ' = ',
+ escapeValueSpecials: true,
+ escapeUnicode: false,
+ comments: 'c',
+ date: new Date(),
+ timeZone: 'UTC',
+ lineSeparator: '\r\n',
+ order: 'sorted',
+};
+const escapeOptions: EscapeOptions = {javaVersion: 17, escapeUnicode: false};
+const p4: Properties = parse('a=b', parseOptions) || javaProps.parse('a=b', {javaVersion: 17});
+const s2: string =
+ stringify(p4, stringifyOptions) + stringify(p4, {date: false}) + escapeKey('a', escapeOptions) + escapeValue('b');
+const promises2: Array> = [
+ parseFile('file', 'latin1', parseOptions),
+ parseFile('file', fileOptions),
+ parseFile('file', {javaVersion: 8}),
+ defaultProps.parseFile('file', fileOptions),
+];
+
+// readEntries
+const reader: EntryReader = readEntries(s2, parseOptions);
+while (reader.next()) {
+ const entry: string = reader.key + reader.value + reader.rawKey + reader.rawValue + reader.line;
+}
+const positioned: EntryReader & EntryPositions = readEntries(s2, {javaVersion: 8, positions: true});
+const position: number = positioned.start + positioned.end + positioned.startLine + positioned.startColumn;
diff --git a/test/compat/types/stub/node/index.d.ts b/test/compat/types/stub/node/index.d.ts
new file mode 100644
index 0000000..2a164f8
--- /dev/null
+++ b/test/compat/types/stub/node/index.d.ts
@@ -0,0 +1,7 @@
+// Minimal stand-in for @types/node: enough for the java-props declarations, and parseable by any TypeScript version.
+declare class Buffer {
+ static from(str: string): Buffer;
+}
+declare class URL {
+ constructor(url: string);
+}
diff --git a/test/data/testData1.dos.properties b/test/data/testData1.dos.properties
deleted file mode 100644
index fd33463..0000000
--- a/test/data/testData1.dos.properties
+++ /dev/null
@@ -1,35 +0,0 @@
-# testcases
-key0=abcd\
-
-\
-key1=value1
-
-key2 \= abcde
-key3
-
- #key4=abcde
-key5 ===Arial,ANSI_CHARSET
-key6= WingDings,SYMBOL_CHARSET \\\
- abc
-key7 Symbol,SYMBOL_CHARSET \
-
- keyabcdef
-
-key8notassign abcdef
-key9\ Term=ABCDE
-
-\\:key10=bar
-
-\\\\:key11=bar2
-
-\\\:key12=bar2
-
-key13dialog.3=
-key14_asdfa
-key15 \ abcdef
-key16\ b= \ abcdef
-key17=\
-#bar\
-baz
-
-
diff --git a/test/data/testData1.properties b/test/data/testData1.properties
deleted file mode 100644
index 2be6432..0000000
--- a/test/data/testData1.properties
+++ /dev/null
@@ -1,35 +0,0 @@
-# testcases
-key0=abcd\
-
-\
-key1=value1
-
-key2 \= abcde
-key3
-
- #key4=abcde
-key5 ===Arial,ANSI_CHARSET
-key6= WingDings,SYMBOL_CHARSET \\\
- abc
-key7 Symbol,SYMBOL_CHARSET \
-
- keyabcdef
-
-key8notassign abcdef
-key9\ Term=ABCDE
-
-\\:key10=bar
-
-\\\\:key11=bar2
-
-\\\:key12=bar2
-
-key13dialog.3=
-key14_asdfa
-key15 \ abcdef
-key16\ b= \ abcdef
-key17=\
-#bar\
-baz
-
-
diff --git a/test/data/testData1.properties-result.json b/test/data/testData1.properties-result.json
deleted file mode 100644
index 23d683f..0000000
--- a/test/data/testData1.properties-result.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "key0": "abcd",
- "key1": "value1",
- "key2": "= abcde",
- "key3": "",
- "key5": "==Arial,ANSI_CHARSET",
- "key6": "WingDings,SYMBOL_CHARSET \\abc",
- "key7": "Symbol,SYMBOL_CHARSET ",
- "keyabcdef": "",
- "key8notassign": "abcdef",
- "key9 Term": "ABCDE",
- "\\": "key10=bar",
- "\\\\": "key11=bar2",
- "\\:key12": "bar2",
- "key13dialog.3": "",
- "key14_asdfa": "",
- "key15": " abcdef",
- "key16 b": " abcdef",
- "key17": "#barbaz"
-}
diff --git a/test/data/testData2.dos.properties b/test/data/testData2.dos.properties
deleted file mode 100644
index fb97374..0000000
--- a/test/data/testData2.dos.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-#
-key1=-monotype-timesnewroman-regular-r---*-%d-*-*-p-*-iso8859-1serif.1a-monotype-timesnewroman-regular-r-normal--*-%d-*-*-p-*-iso8859-2serif.2a-b&h-LucidaBrightLat4-Normal-r-normal--*-%d-*-*-p-*-iso8859-4serif.3a-monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-5serif.4a-monotype-timesnewromangreek-regular-r-normal--*-%d-*-*-p-*-iso8859-7serif.5a-monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-9serif.6a-monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-15serif.7a-hanyi-ming-medium-r-normal--*-%d-*-*-m-*-big5-1serif.8a-sun-song-medium-r-normal--*-%d-*-*-c-*-gb2312.1980-0serif.9a-ricoh-hgminchol-medium-r-normal--*-%d-*-*-m-*-jisx0201.1976-0serif.10a-ricoh-hgminchol-medium-r-normal--*-%d-*-*-m-*-jisx0208.1983-0serif.11a-ricoh-heiseimin-w3-r-normal--*-%d-*-*-m-*-jisx0212.1990-0serif.12a-hanyang-myeongjo-medium-r-normal--*-%d-*-*-m-*-ksc5601.1992-3serif.13a-urw-itczapfdingbats-medium-r-normal--*-%d-*-*-p-*-sun-fontspecificserif.14a-*-symbol-medium-r-normal--*-%d-*-*-p-*-sun-fontspecificbserif.italic.0=-monotype-timesbnewbroman-regular-i---*-%d-*-*-p-*-iso8859-1bserif.italic.1=-monotype-timesbnewbroman-regular-i-normal-italic-*-%d-*-*-p-*-iso8859-2
-
-key2=-b&h-LucidaBrightLat4-normal-i-normal-Italic-*-%d-*-*-p-*-iso8859-4
diff --git a/test/data/testData2.properties b/test/data/testData2.properties
deleted file mode 100644
index b2d70ea..0000000
--- a/test/data/testData2.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-#
-key1=-monotype-timesnewroman-regular-r---*-%d-*-*-p-*-iso8859-1serif.1a-monotype-timesnewroman-regular-r-normal--*-%d-*-*-p-*-iso8859-2serif.2a-b&h-LucidaBrightLat4-Normal-r-normal--*-%d-*-*-p-*-iso8859-4serif.3a-monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-5serif.4a-monotype-timesnewromangreek-regular-r-normal--*-%d-*-*-p-*-iso8859-7serif.5a-monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-9serif.6a-monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-15serif.7a-hanyi-ming-medium-r-normal--*-%d-*-*-m-*-big5-1serif.8a-sun-song-medium-r-normal--*-%d-*-*-c-*-gb2312.1980-0serif.9a-ricoh-hgminchol-medium-r-normal--*-%d-*-*-m-*-jisx0201.1976-0serif.10a-ricoh-hgminchol-medium-r-normal--*-%d-*-*-m-*-jisx0208.1983-0serif.11a-ricoh-heiseimin-w3-r-normal--*-%d-*-*-m-*-jisx0212.1990-0serif.12a-hanyang-myeongjo-medium-r-normal--*-%d-*-*-m-*-ksc5601.1992-3serif.13a-urw-itczapfdingbats-medium-r-normal--*-%d-*-*-p-*-sun-fontspecificserif.14a-*-symbol-medium-r-normal--*-%d-*-*-p-*-sun-fontspecificbserif.italic.0=-monotype-timesbnewbroman-regular-i---*-%d-*-*-p-*-iso8859-1bserif.italic.1=-monotype-timesbnewbroman-regular-i-normal-italic-*-%d-*-*-p-*-iso8859-2
-
-key2=-b&h-LucidaBrightLat4-normal-i-normal-Italic-*-%d-*-*-p-*-iso8859-4
diff --git a/test/data/testData2.properties-result.json b/test/data/testData2.properties-result.json
deleted file mode 100644
index 2f39e6e..0000000
--- a/test/data/testData2.properties-result.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "key1": "-monotype-timesnewroman-regular-r---*-%d-*-*-p-*-iso8859-1serif.1a-monotype-timesnewroman-regular-r-normal--*-%d-*-*-p-*-iso8859-2serif.2a-b&h-LucidaBrightLat4-Normal-r-normal--*-%d-*-*-p-*-iso8859-4serif.3a-monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-5serif.4a-monotype-timesnewromangreek-regular-r-normal--*-%d-*-*-p-*-iso8859-7serif.5a-monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-9serif.6a-monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-15serif.7a-hanyi-ming-medium-r-normal--*-%d-*-*-m-*-big5-1serif.8a-sun-song-medium-r-normal--*-%d-*-*-c-*-gb2312.1980-0serif.9a-ricoh-hgminchol-medium-r-normal--*-%d-*-*-m-*-jisx0201.1976-0serif.10a-ricoh-hgminchol-medium-r-normal--*-%d-*-*-m-*-jisx0208.1983-0serif.11a-ricoh-heiseimin-w3-r-normal--*-%d-*-*-m-*-jisx0212.1990-0serif.12a-hanyang-myeongjo-medium-r-normal--*-%d-*-*-m-*-ksc5601.1992-3serif.13a-urw-itczapfdingbats-medium-r-normal--*-%d-*-*-p-*-sun-fontspecificserif.14a-*-symbol-medium-r-normal--*-%d-*-*-p-*-sun-fontspecificbserif.italic.0=-monotype-timesbnewbroman-regular-i---*-%d-*-*-p-*-iso8859-1bserif.italic.1=-monotype-timesbnewbroman-regular-i-normal-italic-*-%d-*-*-p-*-iso8859-2",
- "key2": "-b&h-LucidaBrightLat4-normal-i-normal-Italic-*-%d-*-*-p-*-iso8859-4"
-}
diff --git a/test/data/testData3.dos.properties b/test/data/testData3.dos.properties
deleted file mode 100644
index dd6118d..0000000
--- a/test/data/testData3.dos.properties
+++ /dev/null
@@ -1,29 +0,0 @@
-key1=\
--monotype-times new roman-regular-r---*-%d-*-*-p-*-iso8859-1, \
--monotype-times new roman-regular-r-normal--*-%d-*-*-p-*-iso8859-2, \
--b&h-LucidaBrightLat4-Normal-r-normal--*-%d-*-*-p-*-iso8859-4, \
--monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-5, \
--monotype-times new roman greek-regular-r-normal--*-%d-*-*-p-*-iso8859-7, \
--monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-9, \
--monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-15, \
--hanyi-ming-medium-r-normal--*-%d-*-*-m-*-big5-1, \
--sun-song-medium-r-normal--*-%d-*-*-c-*-gb2312.1980-0, \
--ricoh-hg gothic b-medium-r-normal--*-%d-*-*-m-*-jisx0201.1976-0, \
--ricoh-hg gothic b-medium-r-normal-*-*-%d-*-*-m-*-jisx0208.1983-0, \
--ricoh-heiseimin-w3-r-normal--*-%d-*-*-m-*-jisx0212.1990-0, \
--hanyang-myeongjo-medium-r-normal--*-%d-*-*-m-*-ksc5601.1992-3
-
-key2=\
--monotype-times new roman-regular-i---*-%d-*-*-p-*-iso8859-1, \
--monotype-times new roman-regular-i-normal-italic-*-%d-*-*-p-*-iso8859-2, \
--b&h-LucidaBrightLat4-normal-i-normal-Italic-*-%d-*-*-p-*-iso8859-4, \
--monotype-times-regular-i-normal-italic-*-%d-*-*-p-*-iso8859-5, \
--monotype-times new roman greek-regular-i-normal--*-%d-*-*-p-*-iso8859-7, \
--monotype-times-regular-i-normal-italic-*-%d-*-*-p-*-iso8859-9, \
--monotype-times-regular-i-normal--*-%d-*-*-p-*-iso8859-15, \
--hanyi-ming-medium-r-normal--*-%d-*-*-m-*-big5-1, \
--sun-song-medium-r-normal--*-%d-*-*-c-*-gb2312.1980-0, \
--ricoh-hg gothic b-medium-r-normal--*-%d-*-*-m-*-jisx0201.1976-0, \
--ricoh-hg gothic b-medium-r-normal-*-*-%d-*-*-m-*-jisx0208.1983-0, \
--ricoh-heiseimin-w3-r-normal--*-%d-*-*-m-*-jisx0212.1990-0, \
--hanyang-myeongjo-medium-r-normal--*-%d-*-*-m-*-ksc5601.1992-3
diff --git a/test/data/testData3.properties-result.json b/test/data/testData3.properties-result.json
deleted file mode 100644
index 901dfaf..0000000
--- a/test/data/testData3.properties-result.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "key1": "-monotype-times new roman-regular-r---*-%d-*-*-p-*-iso8859-1, -monotype-times new roman-regular-r-normal--*-%d-*-*-p-*-iso8859-2, -b&h-LucidaBrightLat4-Normal-r-normal--*-%d-*-*-p-*-iso8859-4, -monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-5, -monotype-times new roman greek-regular-r-normal--*-%d-*-*-p-*-iso8859-7, -monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-9, -monotype-times-regular-r-normal--*-%d-*-*-p-*-iso8859-15, -hanyi-ming-medium-r-normal--*-%d-*-*-m-*-big5-1, -sun-song-medium-r-normal--*-%d-*-*-c-*-gb2312.1980-0, -ricoh-hg gothic b-medium-r-normal--*-%d-*-*-m-*-jisx0201.1976-0, -ricoh-hg gothic b-medium-r-normal-*-*-%d-*-*-m-*-jisx0208.1983-0, -ricoh-heiseimin-w3-r-normal--*-%d-*-*-m-*-jisx0212.1990-0, -hanyang-myeongjo-medium-r-normal--*-%d-*-*-m-*-ksc5601.1992-3",
- "key2": "-monotype-times new roman-regular-i---*-%d-*-*-p-*-iso8859-1, -monotype-times new roman-regular-i-normal-italic-*-%d-*-*-p-*-iso8859-2, -b&h-LucidaBrightLat4-normal-i-normal-Italic-*-%d-*-*-p-*-iso8859-4, -monotype-times-regular-i-normal-italic-*-%d-*-*-p-*-iso8859-5, -monotype-times new roman greek-regular-i-normal--*-%d-*-*-p-*-iso8859-7, -monotype-times-regular-i-normal-italic-*-%d-*-*-p-*-iso8859-9, -monotype-times-regular-i-normal--*-%d-*-*-p-*-iso8859-15, -hanyi-ming-medium-r-normal--*-%d-*-*-m-*-big5-1, -sun-song-medium-r-normal--*-%d-*-*-c-*-gb2312.1980-0, -ricoh-hg gothic b-medium-r-normal--*-%d-*-*-m-*-jisx0201.1976-0, -ricoh-hg gothic b-medium-r-normal-*-*-%d-*-*-m-*-jisx0208.1983-0, -ricoh-heiseimin-w3-r-normal--*-%d-*-*-m-*-jisx0212.1990-0, -hanyang-myeongjo-medium-r-normal--*-%d-*-*-m-*-ksc5601.1992-3"
-}
diff --git a/test/fixtures/README.md b/test/fixtures/README.md
new file mode 100644
index 0000000..790a311
--- /dev/null
+++ b/test/fixtures/README.md
@@ -0,0 +1,11 @@
+# Test fixtures
+
+Each `x.properties` file (and its CRLF variant `x.crlf.properties`, if any) is parsed and compared to
+`x.expected.json`. The expected results are checked against real JVMs by `pnpm run reference:check`.
+
+The `.properties` files are byte-exact (see `.gitattributes`): some use CRLF or mixed line endings on purpose.
+
+- `syntax`: separators, escapes, comments, continuation lines, keys without value.
+- `backslashes`: trailing backslashes.
+- `multiline`: values and keys spanning several lines (continuation lines).
+- `mixed-line-endings`: LF, CRLF and CR in the same file.
diff --git a/test/data/testData4.properties-result.json b/test/fixtures/backslashes.expected.json
similarity index 100%
rename from test/data/testData4.properties-result.json
rename to test/fixtures/backslashes.expected.json
diff --git a/test/data/testData4.properties b/test/fixtures/backslashes.properties
similarity index 100%
rename from test/data/testData4.properties
rename to test/fixtures/backslashes.properties
diff --git a/test/fixtures/mixed-line-endings.expected.json b/test/fixtures/mixed-line-endings.expected.json
new file mode 100644
index 0000000..03b1e11
--- /dev/null
+++ b/test/fixtures/mixed-line-endings.expected.json
@@ -0,0 +1,10 @@
+{
+ "continued.cr": "ab",
+ "continued.crlf": "ab",
+ "continued.lf": "ab",
+ "continued.mixed": "abcd",
+ "cr": "3",
+ "crlf": "2",
+ "last": "end",
+ "lf": "1"
+}
diff --git a/test/fixtures/mixed-line-endings.properties b/test/fixtures/mixed-line-endings.properties
new file mode 100644
index 0000000..aed7b9e
--- /dev/null
+++ b/test/fixtures/mixed-line-endings.properties
@@ -0,0 +1,14 @@
+# LF, CRLF and CR line terminators in one file
+lf=1
+crlf=2
+cr=3
continued.lf=a\
+ b
+continued.crlf=a\
+ b
+continued.cr=a\
b
continued.mixed=a\
+ b\
c\
+ d
+
+
+# comment
! comment
+last=end
\ No newline at end of file
diff --git a/test/fixtures/multiline.crlf.properties b/test/fixtures/multiline.crlf.properties
new file mode 100644
index 0000000..b5d2279
--- /dev/null
+++ b/test/fixtures/multiline.crlf.properties
@@ -0,0 +1,34 @@
+# Values and keys spanning several lines
+greeting = Hello, \
+ World!
+path=/usr/local/bin:\
+ /usr/bin:\
+ /bin
+
+indented.key\
+ .continued = value
+ indented.entry = first \
+ second
+whitespace.kinds = one\
+ two
+unicode.across.lines = caf\
+ \u00e9 au lait
+escape.across.lines = tab\
+ \tseparated
+
+# An even number of backslashes doesn't continue the line
+windows.dir = C:\\Program Files\\
+after.windows.dir = not part of the previous value
+odd.backslashes = a\\\
+ b
+
+# Continuation lines are never comments
+hash.continuation = start\
+ # not a comment
+bang.continuation = start\
+ ! not a comment
+
+key\ with\ spaces = value with trailing spaces
+empty.value =
+only.key
+last = end
diff --git a/test/fixtures/multiline.expected.json b/test/fixtures/multiline.expected.json
new file mode 100644
index 0000000..ee12d3c
--- /dev/null
+++ b/test/fixtures/multiline.expected.json
@@ -0,0 +1,18 @@
+{
+ "after.windows.dir": "not part of the previous value",
+ "bang.continuation": "start! not a comment",
+ "empty.value": "",
+ "escape.across.lines": "tab\tseparated",
+ "greeting": "Hello, World!",
+ "hash.continuation": "start# not a comment",
+ "indented.entry": "first second",
+ "indented.key.continued": "value",
+ "key with spaces": "value with trailing spaces ",
+ "last": "end",
+ "odd.backslashes": "a\\b",
+ "only.key": "",
+ "path": "/usr/local/bin:/usr/bin:/bin",
+ "unicode.across.lines": "café au lait",
+ "whitespace.kinds": "onetwo",
+ "windows.dir": "C:\\Program Files\\"
+}
diff --git a/test/fixtures/multiline.properties b/test/fixtures/multiline.properties
new file mode 100644
index 0000000..4edc24a
--- /dev/null
+++ b/test/fixtures/multiline.properties
@@ -0,0 +1,34 @@
+# Values and keys spanning several lines
+greeting = Hello, \
+ World!
+path=/usr/local/bin:\
+ /usr/bin:\
+ /bin
+
+indented.key\
+ .continued = value
+ indented.entry = first \
+ second
+whitespace.kinds = one\
+ two
+unicode.across.lines = caf\
+ \u00e9 au lait
+escape.across.lines = tab\
+ \tseparated
+
+# An even number of backslashes doesn't continue the line
+windows.dir = C:\\Program Files\\
+after.windows.dir = not part of the previous value
+odd.backslashes = a\\\
+ b
+
+# Continuation lines are never comments
+hash.continuation = start\
+ # not a comment
+bang.continuation = start\
+ ! not a comment
+
+key\ with\ spaces = value with trailing spaces
+empty.value =
+only.key
+last = end
diff --git a/test/data/testData0.properties-result.json b/test/fixtures/syntax.expected.json
similarity index 100%
rename from test/data/testData0.properties-result.json
rename to test/fixtures/syntax.expected.json
diff --git a/test/data/testData0.properties b/test/fixtures/syntax.properties
similarity index 100%
rename from test/data/testData0.properties
rename to test/fixtures/syntax.properties
diff --git a/test/java-props.test.ts b/test/java-props.test.ts
deleted file mode 100644
index b745f77..0000000
--- a/test/java-props.test.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-import fs from 'fs';
-import JavaProps from '../src/java-props';
-import {decodeLine, encodeLine} from '../src/utils';
-
-const dataFiles = [
- 'testData0.properties',
- 'testData1.properties',
- 'testData1.dos.properties',
- 'testData2.properties',
- 'testData2.dos.properties',
- 'testData3.dos.properties',
- 'testData4.properties',
-];
-
-describe('parse', () => {
- for (const file of dataFiles) {
- it('validate ' + file, async () => {
- const res = JSON.parse(
- fs.readFileSync(__dirname + '/data/' + file.replace('.dos', '') + '-result.json', 'utf8')
- );
- const props = JavaProps.parse(fs.readFileSync(__dirname + '/data/' + file, 'utf8'));
- expect(props).toEqual(res);
- });
- }
-
- it('must escape crlf', () => {
- expect(JavaProps.parse('abc=123\\\r\ndef=456')).toEqual({abc: '123def=456'});
- expect(JavaProps.parse('abc=123\\\rdef=456')).toEqual({abc: '123def=456'});
- });
-
- it('must ignore escaped eof', () => {
- expect(JavaProps.parse('\\')).toEqual({});
- expect(JavaProps.parse('key=value\\')).toEqual({key: 'value'});
- });
-
- it('must ignore empty last line', () => {
- expect(JavaProps.parse('')).toEqual({});
- expect(JavaProps.parse('\n')).toEqual({});
- expect(JavaProps.parse('\n ')).toEqual({});
- expect(JavaProps.parse('key=value\n')).toEqual({key: 'value'});
- expect(JavaProps.parse('key=value\n ')).toEqual({key: 'value'});
- expect(JavaProps.parse('key=value\n# comment')).toEqual({key: 'value'});
- });
-});
-
-describe('stringify', () => {
- const expectConvertBack = (props: any) => {
- const newProps = JavaProps.parse(JavaProps.stringify(props));
- expect(newProps).toEqual(props);
- };
-
- for (const file of dataFiles) {
- it('convert back ' + file, async () => {
- expectConvertBack(JavaProps.parse(fs.readFileSync(__dirname + '/data/' + file, 'utf8')));
- });
- }
-
- it('must handle spacings', async () => {
- expectConvertBack({' key': 'value'});
- expectConvertBack({key: ' value'});
- expectConvertBack({'key ': 'value'});
- expectConvertBack({key: 'value '});
- expectConvertBack({' ke:y= ': ' = \\nval\\ue\n '});
- });
-
- it('must skip non-own properties', async () => {
- const obj = {key: 'value'};
- (Object.prototype as any).notAKey = 'x';
- expect(JavaProps.stringify(obj)).toBe('key: value\n');
- });
-
- it('must handle null-prototype objects', async () => {
- const obj = Object.create(null);
- obj.key = 'value';
- expect(JavaProps.stringify(obj)).toBe('key: value\n');
- });
-});
-
-const converts: any = {
- t: '\t',
- r: '\r',
- n: '\n',
- f: '\f',
-};
-
-describe('utils.decodeLine', () => {
- for (const char in converts) {
- if (converts.hasOwnProperty(char)) {
- const repl = converts[char];
- it('must decode \\' + char, () => {
- expect(decodeLine('abc\\' + char + 'def\\' + char + 'ghi')).toBe('abc' + repl + 'def' + repl + 'ghi');
- });
- }
- }
-
- it('must remove useless escapes', () => {
- expect(decodeLine('a\\bc\\def\\\\')).toBe('abcdef\\');
- });
-
- it('must replace \\uxxxx', () => {
- expect(decodeLine('\\u0058 \\u2764')).toBe('X ❤');
- expect(decodeLine('\\u00580')).toBe('X0');
- expect(decodeLine('\\u0058A\\u2764Test')).toBe('XA❤Test');
- expect(decodeLine('\\u002F \\u002f')).toBe('/ /');
- });
-
- it('must throw on malformed \\uxxxx', () => {
- expect(() => decodeLine('\\u')).toThrow();
- expect(() => decodeLine('\\u58')).toThrow();
- expect(() => decodeLine('\\u27g4')).toThrow();
- });
-
- it('must not replace escaped', () => {
- expect(decodeLine('test\\nand\\\\n \\\\\\n')).toBe('test\nand\\n \\\n');
- expect(decodeLine('abc \\\\u0000')).toBe('abc \\u0000');
- expect(decodeLine('abc \\\\uxxxx')).toBe('abc \\uxxxx');
- });
-});
-
-const origEncodeLine = encodeLine;
-for (const isKey of [false, true]) {
- const encodeLine = (line: string) => origEncodeLine(line, isKey);
- describe('utils.encodeLine (isKey=' + isKey + ')', () => {
- for (const char in converts) {
- if (converts.hasOwnProperty(char)) {
- const repl = converts[char];
- it('must encode \\' + char, () => {
- expect(encodeLine('abc' + repl + 'def' + repl + 'ghi')).toBe(
- 'abc\\' + char + 'def\\' + char + 'ghi'
- );
- });
- }
- }
-
- it('must escape special chars', () => {
- expect(encodeLine('!# :=\\')).toBe(isKey ? '\\!\\#\\ \\:\\=\\\\' : '!# :=\\\\');
- });
-
- if (!isKey) {
- it('must escape beginning space', () => {
- expect(encodeLine(' abc def ghi jkl ')).toBe('\\ abc def ghi jkl ');
- });
- }
-
- it('must not escape normal chars', () => {
- let str = '';
- for (let cc = 33; cc < 127; ++cc) {
- const c = String.fromCharCode(cc);
- if (c === '\\' || (isKey && (c === '!' || c === '#' || c === ' ' || c === ':' || c === '='))) {
- continue;
- }
- str += c;
- }
- expect(encodeLine(str)).toBe(str);
- });
-
- it('must encode others unicode chars', () => {
- expect(encodeLine('\0')).toBe('\\u0000');
- expect(encodeLine('\u0014')).toBe('\\u0014');
- expect(encodeLine('\u007F')).toBe('\\u007F');
- expect(encodeLine('é')).toBe('\\u00E9');
- expect(encodeLine('👑')).toBe('\\uD83D\\uDC51');
- expect(encodeLine(String.fromCharCode(15))).toBe('\\u000F');
- expect(encodeLine(String.fromCharCode(255))).toBe('\\u00FF');
- expect(encodeLine(String.fromCharCode(4095))).toBe('\\u0FFF');
- expect(encodeLine(String.fromCharCode(65535))).toBe('\\uFFFF');
- });
- });
-}
diff --git a/test/node-java-props.test.ts b/test/node-java-props.test.ts
deleted file mode 100644
index 5d4fcd0..0000000
--- a/test/node-java-props.test.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import fs from 'fs';
-import JavaProps from '../src/node-java-props';
-
-describe('parseFile', () => {
- const file = 'testData0.properties';
- it('read and validate ' + file, async () => {
- const res = JSON.parse(fs.readFileSync(__dirname + '/data/' + file + '-result.json', 'utf8'));
- const props = await JavaProps.parseFile(__dirname + '/data/' + file, 'utf8');
- expect(props).toEqual(res);
- });
-
- it('must throw on read error', async () => {
- let err;
- try {
- await JavaProps.parseFile(__dirname + '/nonexistent-file.properties');
- } catch (e) {
- err = e;
- }
- expect(err).toBeDefined();
- });
-});
diff --git a/test/support/arbitraries.mts b/test/support/arbitraries.mts
new file mode 100644
index 0000000..1699fc3
--- /dev/null
+++ b/test/support/arbitraries.mts
@@ -0,0 +1,26 @@
+// fast-check arbitraries for .properties content.
+import fc from 'fast-check';
+
+// Pieces of .properties text that matter to the parser, and some that don't (backslash weighted)
+const PIECES = [
+ ...['\\', '\\', '\\', '\\u', '\\u00', '\\u0041', '\\\n', '\\\r\n'],
+ ...['\n', '\r', '\r\n', '\n#', '\n!', ' ', '\t', '\f', '=', ':', '#', '!'],
+ ...['a', 'k', 'v', 'u', '0', 'F', 'g', 'n', 't', 'é'],
+ ...['\u0000', '\u007F', '\u00A0', '\uFEFF', '\uD83D', '\uDC51'],
+];
+export const text = (maxPieces: number) =>
+ fc.array(fc.constantFrom(...PIECES), {maxLength: maxPieces}).map((pieces) => pieces.join(''));
+
+/** Text with long lines too, to exceed Java's initial line buffer (1024 chars) */
+export const longText = fc
+ .array(
+ fc.oneof(
+ text(24),
+ fc.constantFrom('a', '\\\\', '=').map((c) => c.repeat(1000))
+ ),
+ {maxLength: 6}
+ )
+ .map((parts) => parts.join(''));
+
+/** Properties with tricky keys and values */
+export const props = fc.dictionary(text(8), text(8));
diff --git a/test/support/data.mts b/test/support/data.mts
new file mode 100644
index 0000000..5f77751
--- /dev/null
+++ b/test/support/data.mts
@@ -0,0 +1,46 @@
+// Test fixtures (test/fixtures) and the Java results of the edge cases (test/cases).
+import fs from 'node:fs';
+import path from 'node:path';
+import type {LoadEra} from '../../src/internal/java-version.ts';
+import type {LoadResult} from './load-result.mts';
+
+export const FIXTURES_DIR = path.join(import.meta.dirname, '../fixtures');
+export const PARSE_EXPECTED_FILE = path.join(import.meta.dirname, '../cases/parse-expected.json');
+export const STRINGIFY_EXPECTED_FILE = path.join(import.meta.dirname, '../cases/stringify-expected.json');
+
+/** The .properties files of test/fixtures. */
+export function fixtureFiles(): string[] {
+ return fs
+ .readdirSync(FIXTURES_DIR)
+ .filter((file) => file.endsWith('.properties'))
+ .sort();
+}
+
+export function readFixture(file: string): string {
+ return fs.readFileSync(path.join(FIXTURES_DIR, file), 'utf8');
+}
+
+/**
+ * Expected result of a fixture: `x.expected.json`, for `x.properties` and its CRLF variant `x.crlf.properties`.
+ */
+export function readFixtureResult(file: string): Record {
+ return JSON.parse(readFixture(file.replace(/(\.crlf)?\.properties$/, '.expected.json')));
+}
+
+export type ParseExpected = Record>;
+
+/** Reads parse-expected.json (not imported as a module: Vite rejects lone surrogate escapes like "\ud83d"). */
+export function readParseExpected(): ParseExpected {
+ return JSON.parse(fs.readFileSync(PARSE_EXPECTED_FILE, 'utf8'));
+}
+
+export interface StringifyExpected {
+ /** Inputs, and the outputs of store(Writer) and store(OutputStream) without the date line */
+ cases: Record; comments?: string; writer: string; stream: string}>;
+ /** Inputs (`timeZone` option), and the Date.toString() of Java */
+ dates: Record;
+}
+
+export function readStringifyExpected(): StringifyExpected {
+ return JSON.parse(fs.readFileSync(STRINGIFY_EXPECTED_FILE, 'utf8'));
+}
diff --git a/test/support/load-result.mts b/test/support/load-result.mts
new file mode 100644
index 0000000..e767fcd
--- /dev/null
+++ b/test/support/load-result.mts
@@ -0,0 +1,19 @@
+// Results of Properties.load() / parse().
+
+export type LoadResult = {props: Record} | {throws: string; message: string};
+
+/** Copies properties into a null-prototype object (so "__proto__" stays a data key), sorted by key. */
+export function sortedProps(props: Record): Record {
+ const res: Record = Object.create(null);
+ for (const key of Object.keys(props).sort()) res[key] = props[key];
+ return res;
+}
+
+/** Runs a JS parse function and captures its result, or the error it throws. */
+export function captureLoad(parse: () => Record): LoadResult {
+ try {
+ return {props: sortedProps(parse())};
+ } catch (err) {
+ return {throws: (err as Error).name, message: (err as Error).message};
+ }
+}
diff --git a/test/unit/api.test.ts b/test/unit/api.test.ts
new file mode 100644
index 0000000..f0d870e
--- /dev/null
+++ b/test/unit/api.test.ts
@@ -0,0 +1,34 @@
+// Snapshot of the public API (export names and types): removing or retyping an export breaks users.
+// The built dist/ is checked against the same snapshot by test/compat/run-smoke.js.
+import {describe, expect, it} from 'vitest';
+import CommonJavaProps, * as CommonJavaPropsNs from '../../src/java-props';
+import NodeJavaProps, * as NodeJavaPropsNs from '../../src/node-java-props';
+import * as Utils from '../../src/utils';
+
+const shape = (mod: object) =>
+ Object.entries(mod)
+ .filter(([key]) => key !== '__esModule')
+ .map(([key, value]) => `${key}: ${typeof value}`)
+ .sort();
+
+describe('exports', () => {
+ it('match the API snapshot', async () => {
+ const api = {
+ main: shape(NodeJavaPropsNs),
+ mainDefault: shape(NodeJavaProps),
+ browser: shape(CommonJavaPropsNs),
+ browserDefault: shape(CommonJavaProps),
+ utils: shape(Utils),
+ lineReader: Object.getOwnPropertyNames(Utils.LineReader.prototype).sort(),
+ };
+ await expect(`${JSON.stringify(api, null, 2)}\n`).toMatchFileSnapshot('../compat/api-snapshot.json');
+ });
+
+ it('share the same functions', () => {
+ for (const mod of [CommonJavaProps, CommonJavaPropsNs, NodeJavaProps, NodeJavaPropsNs]) {
+ expect(mod.parse).toBe(CommonJavaPropsNs.parse);
+ expect(mod.stringify).toBe(CommonJavaPropsNs.stringify);
+ }
+ expect(NodeJavaProps.parseFile).toBe(NodeJavaPropsNs.parseFile);
+ });
+});
diff --git a/test/unit/java-props.test.ts b/test/unit/java-props.test.ts
new file mode 100644
index 0000000..c2a0481
--- /dev/null
+++ b/test/unit/java-props.test.ts
@@ -0,0 +1,180 @@
+import {afterEach, describe, expect, it, vi} from 'vitest';
+import JavaProps, {parse, type Properties, stringify} from '../../src/java-props';
+import {dateCases, stringifyCases} from '../cases/stringify-cases.mts';
+import {fixtureFiles, readFixture, readFixtureResult, readStringifyExpected} from '../support/data.mts';
+
+const fixtures = fixtureFiles();
+
+describe('parse', () => {
+ it('has fixtures', () => {
+ expect(fixtures.length).toBeGreaterThan(0);
+ });
+
+ for (const file of fixtures) {
+ it(`parses ${file}`, () => {
+ expect(JavaProps.parse(readFixture(file))).toEqual(readFixtureResult(file));
+ });
+ }
+
+ it('joins continuation lines ending with CRLF or CR', () => {
+ expect(JavaProps.parse('abc=123\\\r\ndef=456')).toEqual({abc: '123def=456'});
+ expect(JavaProps.parse('abc=123\\\rdef=456')).toEqual({abc: '123def=456'});
+ });
+
+ it('ignores a continuation backslash at the end (Java returns an empty key)', () => {
+ expect(JavaProps.parse('\\')).toEqual({});
+ expect(JavaProps.parse('key=value\\')).toEqual({key: 'value'});
+ });
+
+ it('ignores a trailing blank line or comment', () => {
+ expect(JavaProps.parse('')).toEqual({});
+ expect(JavaProps.parse('\n')).toEqual({});
+ expect(JavaProps.parse('\n ')).toEqual({});
+ expect(JavaProps.parse('key=value\n')).toEqual({key: 'value'});
+ expect(JavaProps.parse('key=value\n ')).toEqual({key: 'value'});
+ expect(JavaProps.parse('key=value\n# comment')).toEqual({key: 'value'});
+ });
+
+ it('returns a null-prototype object', () => {
+ const props = JavaProps.parse('__proto__=a\nconstructor=b');
+ expect(Object.getPrototypeOf(props)).toBe(null);
+ expect(Object.keys(props)).toEqual(['__proto__', 'constructor']);
+ expect(Object.getOwnPropertyDescriptor(props, '__proto__')?.value).toBe('a');
+ });
+
+ it('throws on malformed \\uXXXX', () => {
+ expect(() => JavaProps.parse('k=\\u12')).toThrow(new Error('Malformed \\uxxxx encoding.'));
+ });
+});
+
+describe('stringify', () => {
+ const expectConvertBack = (props: Properties) => {
+ expect(JavaProps.parse(JavaProps.stringify(props))).toEqual(props);
+ };
+
+ for (const file of fixtures) {
+ it(`round-trips ${file}`, () => {
+ expectConvertBack(JavaProps.parse(readFixture(file)));
+ });
+ }
+
+ it('round-trips leading and trailing spaces', () => {
+ expectConvertBack({' key': 'value'});
+ expectConvertBack({key: ' value'});
+ expectConvertBack({'key ': 'value'});
+ expectConvertBack({key: 'value '});
+ expectConvertBack({' ke:y= ': ' = \\nval\\ue\n '});
+ });
+
+ it('writes "key: value" lines', () => {
+ expect(JavaProps.stringify({a: '1', b: '2'})).toBe('a: 1\nb: 2\n');
+ expect(JavaProps.stringify({})).toBe('');
+ expect(JavaProps.stringify({'': ''})).toBe(': \n');
+ });
+
+ it('skips inherited properties', () => {
+ const proto = {inherited: 'x'};
+ const obj = Object.create(proto);
+ obj.key = 'value';
+ expect(JavaProps.stringify(obj)).toBe('key: value\n');
+ });
+
+ it('accepts null-prototype objects', () => {
+ const obj = Object.create(null);
+ obj.key = 'value';
+ expect(JavaProps.stringify(obj)).toBe('key: value\n');
+ });
+});
+
+describe('stringify with javaVersion', () => {
+ const expected = readStringifyExpected();
+ const base = {javaVersion: 25, date: false, lineSeparator: '\n'} as const;
+
+ for (const c of stringifyCases) {
+ it(`writes like Properties.store(): ${c.name}`, () => {
+ const options = {...base, comments: c.comments};
+ expect(stringify(c.props, options)).toBe(expected.cases[c.name].writer);
+ expect(stringify(c.props, {...options, escapeUnicode: true})).toBe(expected.cases[c.name].stream);
+ });
+ }
+
+ for (const d of dateCases) {
+ it(`writes the date like Java: ${d.name}`, () => {
+ const options = {...base, date: new Date(d.time), timeZone: d.timeZone};
+ expect(stringify({}, options)).toBe(`#${expected.dates[d.name].java}\n`);
+ });
+ }
+});
+
+describe('stringify options', () => {
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.unstubAllEnvs();
+ });
+
+ // Replaces a property for the duration of `fn`
+ const withProperty = (obj: object, name: string, value: unknown, fn: () => void) => {
+ const descriptor = Object.getOwnPropertyDescriptor(obj, name) as PropertyDescriptor;
+ Object.defineProperty(obj, name, {value, configurable: true});
+ try {
+ fn();
+ } finally {
+ Object.defineProperty(obj, name, descriptor);
+ }
+ };
+
+ it('keep the historical format by default', () => {
+ const props = {b: '1', a: ' x=y '};
+ expect(stringify(props, {})).toBe(stringify(props));
+ expect(stringify(props, {order: 'sorted', separator: ' = ', lineSeparator: '\r\n'})).toBe(
+ 'a = \\ x=y \r\nb = 1\r\n'
+ );
+ expect(stringify(props, {escapeValueSpecials: true, escapeUnicode: false})).toBe('b: 1\na: \\ x\\=y \n');
+ expect(stringify(Object.create({inherited: 'x'}), {javaVersion: 25, date: false})).toBe('');
+ });
+
+ it('write a date string as a comment', () => {
+ expect(stringify({}, {date: 'Mon\nTue'})).toBe('#Mon\n#Tue\n');
+ });
+
+ it('write the current date by default with javaVersion', () => {
+ vi.useFakeTimers({now: Date.UTC(2026, 0, 15, 13, 4, 5)});
+ const expected = '#Thu Jan 15 13:04:05 UTC 2026\nk=v\n';
+ expect(stringify({k: 'v'}, {javaVersion: 25, timeZone: 'UTC', lineSeparator: '\n'})).toBe(expected);
+ expect(stringify({k: 'v'}, {javaVersion: 25, timeZone: 'UTC', lineSeparator: '\n', date: ''})).toBe(expected);
+ });
+
+ it('use the system line separator with javaVersion', () => {
+ withProperty(process, 'platform', 'win32', () => {
+ expect(stringify({k: 'v'}, {javaVersion: 25, date: false})).toBe('k=v\r\n');
+ });
+ withProperty(process, 'platform', 'linux', () => {
+ expect(stringify({k: 'v'}, {javaVersion: 25, date: false})).toBe('k=v\n');
+ });
+ });
+
+ it('fall back to the GMT offset without Intl.DateTimeFormat#formatToParts', () => {
+ vi.stubEnv('TZ', 'Etc/GMT-2');
+ const date = new Date(Date.UTC(2026, 0, 15, 13, 4, 5));
+ withProperty(Intl.DateTimeFormat.prototype, 'formatToParts', undefined, () => {
+ expect(stringify({}, {date})).toBe('#Thu Jan 15 15:04:05 GMT+02:00 2026\n');
+ expect(() => stringify({}, {date, timeZone: 'Europe/Paris'})).toThrow(RangeError);
+ });
+ });
+
+ it('reject invalid options', () => {
+ expect(() => stringify({k: 1 as unknown as string}, {javaVersion: 25})).toThrow(TypeError);
+ expect(() => stringify({}, {javaVersion: 'nope'})).toThrow(RangeError);
+ expect(() => stringify({}, {date: true, timeZone: 'Nowhere/Nope'})).toThrow(RangeError);
+ });
+});
+
+describe('parse options', () => {
+ it('reject an invalid javaVersion', () => {
+ expect(() => parse('a=b', {javaVersion: 5})).toThrow(RangeError);
+ });
+
+ it('ignore an undefined javaVersion', () => {
+ expect(parse('\\', {javaVersion: undefined})).toEqual(parse('\\'));
+ });
+});
diff --git a/test/unit/java-version.test.ts b/test/unit/java-version.test.ts
new file mode 100644
index 0000000..72616e8
--- /dev/null
+++ b/test/unit/java-version.test.ts
@@ -0,0 +1,38 @@
+import {describe, expect, it} from 'vitest';
+import {type LoadEra, loadEra} from '../../src/internal/java-version';
+
+describe('loadEra', () => {
+ it('maps Java versions to load eras', () => {
+ const eras: [number | string, LoadEra][] = [
+ [6, 'jdk8'],
+ ['1.6.0_45', 'jdk8'],
+ [8, 'jdk8'],
+ ['1.8.0_392', 'jdk8'],
+ [9, 'jdk9'],
+ ['9.0.7.1', 'jdk9'],
+ ['10.0.2', 'jdk9'],
+ ['11.0.4', 'jdk9'],
+ [11, 'jdk11'],
+ ['11', 'jdk11'],
+ ['11.0', 'jdk11'],
+ ['11.0.5', 'jdk11'],
+ ['11.0.32.1', 'jdk11'],
+ [12, 'jdk9'],
+ [13, 'jdk13'],
+ ['17-ea', 'jdk13'],
+ ['21.0.2+13-LTS', 'jdk13'],
+ [99, 'jdk13'],
+ ['latest', 'jdk13'],
+ ];
+ for (const [version, era] of eras) expect(loadEra(version), String(version)).toBe(era);
+ });
+
+ it('rejects invalid versions', () => {
+ for (const version of [null, true, {}]) {
+ expect(() => loadEra(version as unknown as number)).toThrow(TypeError);
+ }
+ for (const version of [5, 8.5, Number.NaN, '', 'abc', '1', '1.5.0', '5', '8.x', ' 8']) {
+ expect(() => loadEra(version), String(version)).toThrow(RangeError);
+ }
+ });
+});
diff --git a/test/unit/node-java-props.test.ts b/test/unit/node-java-props.test.ts
new file mode 100644
index 0000000..4b1c7b2
--- /dev/null
+++ b/test/unit/node-java-props.test.ts
@@ -0,0 +1,79 @@
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import {pathToFileURL} from 'node:url';
+import {afterAll, beforeAll, describe, expect, it} from 'vitest';
+import JavaProps from '../../src/node-java-props';
+import {FIXTURES_DIR, readFixtureResult} from '../support/data.mts';
+
+const file = path.join(FIXTURES_DIR, 'syntax.properties');
+const expected = readFixtureResult('syntax.properties');
+
+describe('parseFile', () => {
+ let tmpDir: string;
+ beforeAll(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'java-props-'));
+ });
+ afterAll(() => {
+ fs.rmSync(tmpDir, {recursive: true, force: true});
+ });
+
+ it('reads a file path (utf8 by default)', async () => {
+ expect(await JavaProps.parseFile(file)).toEqual(expected);
+ expect(await JavaProps.parseFile(file, 'utf8')).toEqual(expected);
+ });
+
+ it('reads a Buffer path, a file URL and a file descriptor', async () => {
+ expect(await JavaProps.parseFile(Buffer.from(file))).toEqual(expected);
+ expect(await JavaProps.parseFile(pathToFileURL(file))).toEqual(expected);
+ const fd = fs.openSync(file, 'r');
+ try {
+ expect(await JavaProps.parseFile(fd)).toEqual(expected);
+ } finally {
+ fs.closeSync(fd);
+ }
+ });
+
+ it('uses the given encoding', async () => {
+ const latin1File = path.join(tmpDir, 'latin1.properties');
+ fs.writeFileSync(latin1File, Buffer.from('k=\u00e9\n', 'latin1'));
+ expect(await JavaProps.parseFile(latin1File, 'latin1')).toEqual({k: '\u00e9'});
+ expect(await JavaProps.parseFile(latin1File)).toEqual({k: '\ufffd'});
+ });
+
+ it('keeps the UTF-8 BOM (like Java)', async () => {
+ const bomFile = path.join(tmpDir, 'bom.properties');
+ fs.writeFileSync(bomFile, '\ufeffk=v\n');
+ expect(await JavaProps.parseFile(bomFile)).toEqual({'\ufeffk': 'v'});
+ });
+
+ it('accepts parse options', async () => {
+ const badEnd = path.join(tmpDir, 'bad-end.properties');
+ fs.writeFileSync(badEnd, Buffer.from('k=é\\', 'latin1'));
+ expect(await JavaProps.parseFile(badEnd, 'latin1', {javaVersion: 8})).toEqual({k: 'é'});
+ expect(await JavaProps.parseFile(badEnd, {encoding: 'latin1', javaVersion: 8})).toEqual({k: 'é'});
+ expect(await JavaProps.parseFile(file, {javaVersion: 25})).toEqual(expected);
+ });
+
+ it('rejects on read error', async () => {
+ await expect(JavaProps.parseFile(path.join(FIXTURES_DIR, 'nonexistent.properties'))).rejects.toMatchObject({
+ code: 'ENOENT',
+ });
+ });
+
+ it('rejects on invalid options, without reading the file', async () => {
+ const missing = path.join(FIXTURES_DIR, 'nonexistent.properties');
+ await expect(JavaProps.parseFile(missing, {javaVersion: 'nope'})).rejects.toThrow(RangeError);
+ await expect(JavaProps.parseFile(missing, 'utf8', {javaVersion: 5})).rejects.toThrow(RangeError);
+ });
+
+ it('rejects on invalid encoding', async () => {
+ await expect(JavaProps.parseFile(file, 'not-an-encoding')).rejects.toThrow();
+ });
+
+ it('rejects on parse error', async () => {
+ const badFile = path.join(tmpDir, 'bad.properties');
+ fs.writeFileSync(badFile, 'k=\\u12\n');
+ await expect(JavaProps.parseFile(badFile)).rejects.toThrow('Malformed \\uxxxx encoding.');
+ });
+});
diff --git a/test/unit/parse-cases.test.ts b/test/unit/parse-cases.test.ts
new file mode 100644
index 0000000..5926808
--- /dev/null
+++ b/test/unit/parse-cases.test.ts
@@ -0,0 +1,123 @@
+// Runs the edge cases of test/cases/parse-cases.mts: their results are snapshotted in parse-snapshot.json, and compared
+// to the results of real JVMs (parse-expected.json, generated by tools/gen-expected.mts).
+import {describe, expect, it} from 'vitest';
+import {parse} from '../../src/java-props';
+import cases from '../cases/parse-cases.mts';
+import {readParseExpected} from '../support/data.mts';
+import {LOAD_ERAS, type LoadEra} from '../../src/internal/java-version';
+import {captureLoad, type LoadResult} from '../support/load-result.mts';
+
+const expected = readParseExpected();
+const results: Record = {};
+for (const c of cases) results[c.name] = captureLoad(() => parse(c.input));
+
+// Cases where parse() differs from Java, kept on purpose so that existing code keeps working.
+// Exception classes are not compared (Error vs IllegalArgumentException).
+const JAVA_DIVERGENCES: Record = {
+ jdk8: [
+ 'lone continuation backslash: alone',
+ 'lone continuation backslash: alone with LF',
+ 'lone continuation backslash: after property',
+ 'lone continuation backslash: after property, with LF',
+ 'lone continuation backslash: indented',
+ 'comment ending with backslash, then backslashes: comment ending with backslash, then backslash-only line',
+ 'comment ending with backslash, then backslashes: bang comment, CRLF, lone backslash',
+ 'comment ending with backslash, then backslashes: comment with 3 backslashes, then backslashes',
+ 'stale line buffer: lone backslash decode after comment',
+ 'stale line buffer: truncated unicode after longer line',
+ 'stale line buffer: comment chars before a backslash (skipped since JDK 9)',
+ 'stale line buffer: comment ending with backslash (skipped since JDK 9)',
+ ],
+ jdk9: [
+ 'lone continuation backslash: alone',
+ 'lone continuation backslash: alone with LF',
+ 'lone continuation backslash: after property',
+ 'lone continuation backslash: after property, with LF',
+ 'lone continuation backslash: indented',
+ 'comment ending with backslash, then backslashes: comment ending with backslash, then backslash-only line',
+ 'comment ending with backslash, then backslashes: bang comment, CRLF, lone backslash',
+ 'comment ending with backslash, then backslashes: comment with 3 backslashes, then backslashes',
+ 'stale line buffer: lone backslash decode after comment',
+ 'stale line buffer: truncated unicode after longer line',
+ 'stale line buffer: comment chars before a backslash (skipped since JDK 9)',
+ 'stale line buffer: comment ending with backslash (skipped since JDK 9)',
+ ],
+ jdk11: [
+ 'lone continuation backslash: alone',
+ 'lone continuation backslash: alone with LF',
+ 'lone continuation backslash: after property',
+ 'lone continuation backslash: after property, with LF',
+ 'lone continuation backslash: indented',
+ 'empty continuation then comment char: hash',
+ 'empty continuation then comment char: bang with indentation',
+ 'empty continuation then comment char: hash after CRLF',
+ 'empty continuation then comment char: hash with leading whitespace on next line',
+ 'empty continuation then comment char: double continuation then hash',
+ 'comment ending with backslash, then backslashes: comment ending with backslash, then backslash-only line',
+ 'comment ending with backslash, then backslashes: comment with 3 backslashes, then backslashes',
+ 'stale line buffer: truncated unicode after longer line',
+ ],
+ jdk13: [
+ 'lone continuation backslash: alone',
+ 'lone continuation backslash: alone with LF',
+ 'lone continuation backslash: after property',
+ 'lone continuation backslash: after property, with LF',
+ 'lone continuation backslash: indented',
+ 'empty continuation then comment char: hash',
+ 'empty continuation then comment char: bang with indentation',
+ 'empty continuation then comment char: hash after CRLF',
+ 'empty continuation then comment char: hash with leading whitespace on next line',
+ 'empty continuation then comment char: double continuation then hash',
+ 'comment ending with backslash, then backslashes: comment ending with backslash, then backslash-only line',
+ 'comment ending with backslash, then backslashes: comment with 3 backslashes, then backslashes',
+ ],
+};
+
+// A version of each era: parse() with this javaVersion must return exactly what Java returns
+const ERA_VERSIONS: Record = {jdk8: 8, jdk9: 9, jdk11: '11.0.5', jdk13: 13};
+
+// Same properties, or both throw (same message for IllegalArgumentException, Java's only expected exception)
+const sameAsJava = (js: LoadResult, java: LoadResult) => {
+ if ('throws' in java) {
+ return 'throws' in js && (java.throws !== 'java.lang.IllegalArgumentException' || js.message === java.message);
+ }
+ return sameOutcome(js, java);
+};
+
+const sameOutcome = (a: LoadResult, b: LoadResult) => {
+ if ('throws' in a || 'throws' in b) return 'throws' in a && 'throws' in b && a.message === b.message;
+ return JSON.stringify(Object.entries(a.props).sort()) === JSON.stringify(Object.entries(b.props).sort());
+};
+
+describe('parse cases', () => {
+ it('match the snapshot', async () => {
+ const snapshot = Object.fromEntries(cases.map((c) => [c.name, {input: c.input, ...results[c.name]}]));
+ await expect(`${JSON.stringify(snapshot, null, 2)}\n`).toMatchFileSnapshot('../cases/parse-snapshot.json');
+ });
+
+ it('all have Java results', () => {
+ expect(Object.keys(expected).sort()).toEqual(cases.map((c) => c.name).sort());
+ });
+
+ for (const era of LOAD_ERAS) {
+ it(`match Java (${era}) with javaVersion: ${ERA_VERSIONS[era]}`, () => {
+ const mismatches = cases
+ .filter(
+ (c) =>
+ !sameAsJava(
+ captureLoad(() => parse(c.input, {javaVersion: ERA_VERSIONS[era]})),
+ expected[c.name][era]
+ )
+ )
+ .map((c) => c.name);
+ expect(mismatches).toEqual([]);
+ });
+
+ it(`only diverge from Java (${era}) on the known cases`, () => {
+ const divergences = cases
+ .filter((c) => !sameOutcome(results[c.name], expected[c.name][era]))
+ .map((c) => c.name);
+ expect(divergences).toEqual(JAVA_DIVERGENCES[era]);
+ });
+ }
+});
diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts
new file mode 100644
index 0000000..e71dee4
--- /dev/null
+++ b/test/unit/properties.test.ts
@@ -0,0 +1,37 @@
+// Property-based tests (fast-check): random inputs, shrunk to a minimal one on failure.
+import fc from 'fast-check';
+import {describe, expect, it} from 'vitest';
+import {parse, stringify} from '../../src/java-props';
+import {props, text} from '../support/arbitraries.mts';
+
+describe('properties', () => {
+ it('parse only throws on malformed \\uXXXX', () => {
+ fc.assert(
+ fc.property(text(24), (input) => {
+ try {
+ parse(input);
+ } catch (err) {
+ expect((err as Error).message).toBe('Malformed \\uxxxx encoding.');
+ }
+ })
+ );
+ });
+
+ it('stringify output is parsed back to the same properties', () => {
+ fc.assert(
+ fc.property(fc.dictionary(text(8), text(8)), (props) => {
+ expect({...parse(stringify(props))}).toEqual({...props});
+ })
+ );
+ });
+
+ it('stringify output is parsed back to the same properties, with every javaVersion', () => {
+ for (const javaVersion of [8, 9, '11.0.5', 13]) {
+ fc.assert(
+ fc.property(props, (p) => {
+ expect({...parse(stringify(p, {javaVersion}), {javaVersion})}).toEqual({...p});
+ })
+ );
+ }
+ });
+});
diff --git a/test/unit/read-entries.test.ts b/test/unit/read-entries.test.ts
new file mode 100644
index 0000000..8d4222e
--- /dev/null
+++ b/test/unit/read-entries.test.ts
@@ -0,0 +1,181 @@
+import fc from 'fast-check';
+import {describe, expect, it} from 'vitest';
+import {escapeKey, escapeValue, readEntries} from '../../src/java-props';
+import {decodeLine, encodeLine, rawParse} from '../../src/utils';
+import {text} from '../support/arbitraries.mts';
+
+const entries = (str: string, javaVersion?: number) => {
+ const res: string[][] = [];
+ const reader = readEntries(str, {javaVersion});
+ while (reader.next()) res.push([reader.rawKey, reader.rawValue, reader.key, reader.value]);
+ return res;
+};
+
+describe('readEntries', () => {
+ it('reads entries in order, duplicates included', () => {
+ expect(entries('b=1\n# c\na : 2\\\n 3\nb=\\u00e9')).toEqual([
+ ['b', '1', 'b', '1'],
+ ['a', '23', 'a', '23'],
+ ['b', '\\u00e9', 'b', 'é'],
+ ]);
+ });
+
+ it('gives the logical line', () => {
+ const reader = readEntries('k = a\\\n b');
+ expect(reader.next()).toBe(true);
+ expect(reader.line).toBe('k = ab');
+ expect(reader.next()).toBe(false);
+ });
+
+ it('has no entry before next() and at the end', () => {
+ const reader = readEntries('k=v', {positions: true});
+ expect([reader.line, reader.rawKey, reader.key, reader.value, reader.start, reader.startLine]).toEqual(
+ Array(6).fill(undefined)
+ );
+ reader.next();
+ reader.next();
+ expect([reader.line, reader.rawKey, reader.rawValue, reader.key, reader.end, reader.startColumn]).toEqual(
+ Array(6).fill(undefined)
+ );
+ });
+
+ it('decodes only on access', () => {
+ const reader = readEntries('k=\\u12\nk2=v');
+ expect(reader.next()).toBe(true);
+ expect(reader.rawValue).toBe('\\u12');
+ expect(() => reader.value).toThrow('Malformed \\uxxxx encoding.');
+ expect(reader.next()).toBe(true);
+ expect(reader.value).toBe('v');
+ });
+
+ it('decodes like Java, leftovers of its line buffer included (see parse-cases.mts)', () => {
+ // Java 8 to 12 read the "12" left by the first line, Java 13+ throws
+ const reader = readEntries('abcdef12\na=\\u00', {javaVersion: 8});
+ reader.next();
+ reader.next();
+ expect(reader.value).toBe('\u0012');
+ const reader13 = readEntries('abcdef12\na=\\u00', {javaVersion: 13});
+ reader13.next();
+ reader13.next();
+ expect(() => reader13.value).toThrow('Malformed \\uxxxx encoding.');
+ // Java 8 reads the comment's leftover "\\", Java 9 a never written '\0'
+ expect(entries('#a\\\n\\', 8)).toEqual([['\\', '', '\\', '']]);
+ expect(entries('#a\\\n\\', 9)).toEqual([['\\', '', '\0', '']]);
+ });
+
+ it('rejects an invalid javaVersion', () => {
+ expect(() => readEntries('', {javaVersion: 'x'})).toThrow(RangeError);
+ });
+
+ it('reads like the deprecated utils without javaVersion', () => {
+ const outcome = (fn: () => string[][]) => {
+ try {
+ return fn();
+ } catch (err) {
+ return (err as Error).message;
+ }
+ };
+ fc.assert(
+ fc.property(text(30), (input) => {
+ const deprecated = () => {
+ const res: string[][] = [];
+ rawParse(input, ({line, sepStart, valueStart}) => {
+ const [rawKey, rawValue] = [line.substring(0, sepStart), line.substring(valueStart)];
+ res.push([rawKey, rawValue, decodeLine(rawKey), decodeLine(rawValue)]);
+ });
+ return res;
+ };
+ expect(outcome(() => entries(input))).toEqual(outcome(deprecated));
+ })
+ );
+ });
+});
+
+describe('readEntries positions', () => {
+ const positions = (str: string, javaVersion?: number | string) => {
+ const res: number[][] = [];
+ const reader = readEntries(str, {javaVersion, positions: true});
+ while (reader.next()) res.push([reader.start, reader.end, reader.startLine, reader.startColumn]);
+ return res;
+ };
+
+ it('give the range, line and column of each entry', () => {
+ const input = '# c\r\n a=1\r\nb = 2\\\r\n 3\n\n\tc\rd';
+ const expected = [
+ [7, 10, 2, 3],
+ [12, 24, 3, 1],
+ [27, 28, 6, 2],
+ [29, 30, 7, 1],
+ ];
+ for (const javaVersion of [undefined, 8, 9, '11.0.5', 13]) {
+ expect(positions(input, javaVersion), String(javaVersion)).toEqual(expected);
+ }
+ });
+
+ it('start an entry at its first char, after an empty continuation line', () => {
+ for (const javaVersion of [undefined, 8, 13]) {
+ expect(positions('\\\nk=v', javaVersion)).toEqual([[2, 5, 2, 1]]);
+ }
+ });
+
+ it('start an empty entry at its continuation backslash (Java only)', () => {
+ expect(positions('a=1\n \\', 8)).toEqual([
+ [0, 3, 1, 1],
+ [6, 7, 2, 3],
+ ]);
+ expect(positions('a=1\n \\')).toEqual([[0, 3, 1, 1]]);
+ });
+
+ it('skip comments, even those written to Java 8 line buffer', () => {
+ for (const javaVersion of [undefined, 8, 9, 13]) {
+ expect(positions('#ab\\c\nk=v', javaVersion)).toEqual([[6, 9, 2, 1]]);
+ }
+ });
+
+ it('are consistent with the line', () => {
+ fc.assert(
+ fc.property(text(30), fc.constantFrom(undefined, 8, 9, '11.0.5', 13), (input, javaVersion) => {
+ const reader = readEntries(input, {javaVersion, positions: true});
+ let previousEnd = -1;
+ while (reader.next()) {
+ const {start, end, line} = reader;
+ expect(start).toBeGreaterThan(previousEnd);
+ expect(end).toBeGreaterThan(start);
+ expect(end).toBeLessThanOrEqual(input.length);
+ if (line !== '') expect(input[start]).toBe(line[0]);
+ // startLine/startColumn: like a naive count
+ const before = input.substring(0, start).split(/\r\n|\r|\n/);
+ expect([reader.startLine, reader.startColumn]).toEqual([
+ before.length,
+ before[before.length - 1].length + 1,
+ ]);
+ previousEnd = end;
+ }
+ })
+ );
+ });
+});
+
+describe('escapeKey / escapeValue', () => {
+ it('escape like the deprecated encodeLine by default', () => {
+ fc.assert(
+ fc.property(text(8), (s) => {
+ expect(escapeKey(s)).toBe(encodeLine(s, true));
+ expect(escapeValue(s)).toBe(encodeLine(s));
+ })
+ );
+ });
+
+ it('escape like Properties.store() with javaVersion', () => {
+ expect(escapeKey('a=b é', {javaVersion: 17})).toBe('a\\=b\\ é');
+ expect(escapeValue(' a=b é', {javaVersion: 17})).toBe('\\ a\\=b é');
+ expect(escapeValue(' a=b é', {javaVersion: 17, escapeUnicode: true, escapeValueSpecials: false})).toBe(
+ '\\ a=b \\u00E9'
+ );
+ });
+
+ it('reject invalid arguments', () => {
+ expect(() => escapeKey(1 as unknown as string)).toThrow(TypeError);
+ expect(() => escapeValue('', {javaVersion: 'x'})).toThrow(RangeError);
+ });
+});
diff --git a/test/unit/types.test-d.ts b/test/unit/types.test-d.ts
new file mode 100644
index 0000000..51a4a2a
--- /dev/null
+++ b/test/unit/types.test-d.ts
@@ -0,0 +1,106 @@
+// Public types (vitest --typecheck). Compatibility with old TypeScript versions: test/compat/check-types.mjs.
+import {describe, expectTypeOf, it} from 'vitest';
+import javaProps, {
+ type EntryPositions,
+ type EntryReader,
+ type EscapeOptions,
+ escapeKey,
+ escapeValue,
+ type ParseFileOptions,
+ type ParseOptions,
+ type Properties,
+ parse,
+ parseFile,
+ type ReadEntriesOptions,
+ readEntries,
+ type StringifyOptions,
+ stringify,
+} from '../../src/node-java-props';
+import {
+ convertLine,
+ decodeLine,
+ encodeLine,
+ LineReader,
+ type ParsedLine,
+ rawParse,
+ rawParseLine,
+} from '../../src/utils';
+
+type Path = string | Buffer | URL | number;
+
+describe('types', () => {
+ it('main', () => {
+ expectTypeOf().toEqualTypeOf<{[key: string]: string}>();
+ expectTypeOf().toEqualTypeOf<{javaVersion?: number | string}>();
+ expectTypeOf().toEqualTypeOf<{javaVersion?: number | string; encoding?: string}>();
+ expectTypeOf(parse).toEqualTypeOf<(str: string, options?: ParseOptions) => Properties>();
+ expectTypeOf(stringify).toEqualTypeOf<(props: Properties, options?: StringifyOptions) => string>();
+ expectTypeOf(parseFile).toEqualTypeOf<{
+ (path: Path, encoding?: string, options?: ParseOptions): Promise;
+ (path: Path, options?: ParseFileOptions): Promise;
+ }>();
+ expectTypeOf(javaProps).toEqualTypeOf<{
+ parse: typeof parse;
+ parseFile: typeof parseFile;
+ stringify: typeof stringify;
+ }>();
+ });
+
+ it('readEntries', () => {
+ expectTypeOf().toEqualTypeOf<{javaVersion?: number | string; positions?: boolean}>();
+ expectTypeOf().toEqualTypeOf<{
+ next(): boolean;
+ readonly line: string;
+ readonly rawKey: string;
+ readonly rawValue: string;
+ readonly key: string;
+ readonly value: string;
+ }>();
+ expectTypeOf().toEqualTypeOf<{
+ readonly start: number;
+ readonly end: number;
+ readonly startLine: number;
+ readonly startColumn: number;
+ }>();
+ expectTypeOf(readEntries('')).toEqualTypeOf();
+ expectTypeOf(readEntries('', {javaVersion: 8})).toEqualTypeOf();
+ expectTypeOf(readEntries('', {positions: true})).toEqualTypeOf();
+ });
+
+ it('escape and stringify options', () => {
+ expectTypeOf().toEqualTypeOf<{
+ javaVersion?: number | string;
+ escapeValueSpecials?: boolean;
+ escapeUnicode?: boolean;
+ }>();
+ expectTypeOf(escapeKey).toEqualTypeOf<(key: string, options?: EscapeOptions) => string>();
+ expectTypeOf(escapeValue).toEqualTypeOf<(value: string, options?: EscapeOptions) => string>();
+ expectTypeOf().toEqualTypeOf<{
+ javaVersion?: number | string;
+ separator?: '=' | ':' | ' ' | ': ' | ' = ';
+ escapeValueSpecials?: boolean;
+ escapeUnicode?: boolean;
+ comments?: string;
+ date?: boolean | Date | string;
+ timeZone?: string;
+ lineSeparator?: '\n' | '\r\n' | '\r';
+ order?: 'insertion' | 'sorted';
+ }>();
+ });
+
+ it('utils (deprecated: the 2.3.1 declarations, with a fixed readLine)', () => {
+ expectTypeOf().toEqualTypeOf<{line: string; sepStart: number; valueStart: number}>();
+ expectTypeOf(rawParse).toEqualTypeOf<(str: string, consumeFn: (res: ParsedLine) => void) => void>();
+ expectTypeOf(rawParseLine).toEqualTypeOf<(line: string) => ParsedLine>();
+ expectTypeOf(decodeLine).toEqualTypeOf<(line: string) => string>();
+ expectTypeOf(convertLine).toEqualTypeOf(decodeLine);
+ expectTypeOf(encodeLine).toEqualTypeOf<(line: string, isKey?: boolean) => string>();
+ expectTypeOf(LineReader).constructorParameters.toEqualTypeOf<[str: string]>();
+ expectTypeOf().toEqualTypeOf<() => string | undefined>();
+ expectTypeOf>().toEqualTypeOf<{
+ readonly str: string;
+ readonly strLen: number;
+ pos: number;
+ }>();
+ });
+});
diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts
new file mode 100644
index 0000000..434f841
--- /dev/null
+++ b/test/unit/utils.test.ts
@@ -0,0 +1,183 @@
+import {describe, expect, it} from 'vitest';
+import {
+ convertLine,
+ decodeLine,
+ encodeLine,
+ LineReader,
+ type ParsedLine,
+ rawParse,
+ rawParseLine,
+} from '../../src/utils';
+
+const converts: Record = {
+ t: '\t',
+ r: '\r',
+ n: '\n',
+ f: '\f',
+};
+
+describe('utils.decodeLine', () => {
+ for (const [char, repl] of Object.entries(converts)) {
+ it(`decodes \\${char}`, () => {
+ expect(decodeLine(`abc\\${char}def\\${char}ghi`)).toBe(`abc${repl}def${repl}ghi`);
+ });
+ }
+
+ it('drops the backslash of other escapes', () => {
+ expect(decodeLine('a\\bc\\def\\\\')).toBe('abcdef\\');
+ });
+
+ it('decodes \\uXXXX', () => {
+ expect(decodeLine('\\u0058 \\u2764')).toBe('X ❤');
+ expect(decodeLine('\\u00580')).toBe('X0');
+ expect(decodeLine('\\u0058A\\u2764Test')).toBe('XA❤Test');
+ expect(decodeLine('\\u002F \\u002f')).toBe('/ /');
+ expect(decodeLine('\\uD83D\\uDC51')).toBe('👑');
+ });
+
+ it('throws on malformed \\uXXXX', () => {
+ expect(() => decodeLine('\\u')).toThrow('Malformed \\uxxxx encoding.');
+ expect(() => decodeLine('\\u58')).toThrow('Malformed \\uxxxx encoding.');
+ expect(() => decodeLine('\\u27g4')).toThrow('Malformed \\uxxxx encoding.');
+ });
+
+ it('decodes escaped backslashes', () => {
+ expect(decodeLine('test\\nand\\\\n \\\\\\n')).toBe('test\nand\\n \\\n');
+ expect(decodeLine('abc \\\\u0000')).toBe('abc \\u0000');
+ expect(decodeLine('abc \\\\uxxxx')).toBe('abc \\uxxxx');
+ });
+
+ it('drops a trailing lone backslash', () => {
+ expect(decodeLine('abc\\')).toBe('abc');
+ });
+
+ it('has the deprecated convertLine alias', () => {
+ expect(convertLine).toBe(decodeLine);
+ });
+});
+
+for (const isKey of [false, true]) {
+ const encode = (line: string) => encodeLine(line, isKey);
+ describe(`utils.encodeLine (isKey=${isKey})`, () => {
+ for (const [char, repl] of Object.entries(converts)) {
+ it(`encodes \\${char}`, () => {
+ expect(encode(`abc${repl}def${repl}ghi`)).toBe(`abc\\${char}def\\${char}ghi`);
+ });
+ }
+
+ it('escapes special chars', () => {
+ expect(encode('!# :=\\')).toBe(isKey ? '\\!\\#\\ \\:\\=\\\\' : '!# :=\\\\');
+ });
+
+ if (!isKey) {
+ it('escapes leading whitespace', () => {
+ expect(encode(' abc def ghi jkl ')).toBe('\\ abc def ghi jkl ');
+ expect(encode('\tabc')).toBe('\\tabc');
+ });
+ }
+
+ it('keeps printable ASCII chars', () => {
+ let str = '';
+ for (let cc = 33; cc < 127; ++cc) {
+ const c = String.fromCharCode(cc);
+ if (c === '\\' || (isKey && (c === '!' || c === '#' || c === ' ' || c === ':' || c === '='))) {
+ continue;
+ }
+ str += c;
+ }
+ expect(encode(str)).toBe(str);
+ });
+
+ it('encodes other chars as \\uXXXX', () => {
+ expect(encode('\0')).toBe('\\u0000');
+ expect(encode('\u0014')).toBe('\\u0014');
+ expect(encode('\u007F')).toBe('\\u007F');
+ expect(encode('é')).toBe('\\u00E9');
+ expect(encode('👑')).toBe('\\uD83D\\uDC51');
+ expect(encode(String.fromCharCode(15))).toBe('\\u000F');
+ expect(encode(String.fromCharCode(16))).toBe('\\u0010');
+ expect(encode(String.fromCharCode(255))).toBe('\\u00FF');
+ expect(encode(String.fromCharCode(256))).toBe('\\u0100');
+ expect(encode(String.fromCharCode(4095))).toBe('\\u0FFF');
+ expect(encode(String.fromCharCode(4096))).toBe('\\u1000');
+ expect(encode(String.fromCharCode(65535))).toBe('\\uFFFF');
+ });
+ });
+}
+
+describe('utils.encodeLine', () => {
+ it('defaults to value encoding', () => {
+ expect(encodeLine(' a=b')).toBe(encodeLine(' a=b', false));
+ });
+
+ it('throws a TypeError for anything but a string', () => {
+ for (const value of [undefined, null, 5]) {
+ expect(() => encodeLine(value as unknown as string)).toThrow(TypeError);
+ }
+ expect(encodeLine(new String('a b') as unknown as string, true)).toBe('a\\ b');
+ });
+});
+
+describe('utils.LineReader', () => {
+ const readAll = (str: string) => {
+ const lr = new LineReader(str);
+ const lines: string[] = [];
+ for (let line = lr.readLine(); line !== undefined; line = lr.readLine()) lines.push(line);
+ return lines;
+ };
+
+ it('exposes its state', () => {
+ const lr = new LineReader('a=1\nb=2');
+ expect(lr.str).toBe('a=1\nb=2');
+ expect(lr.strLen).toBe(7);
+ expect(lr.pos).toBe(0);
+ expect(lr.readLine()).toBe('a=1');
+ expect(lr.pos).toBe(4);
+ expect(lr.readLine()).toBe('b=2');
+ expect(lr.readLine()).toBe(undefined);
+ expect(lr.readLine()).toBe(undefined);
+ });
+
+ it('reads logical lines', () => {
+ expect(readAll(' a = 1 \r\n# c\n\n\tb\\\n c\\\r\n d\\\r e\n! c\\\nf')).toEqual(['a = 1 ', 'bcde', 'f']);
+ });
+
+ it('keeps escapes', () => {
+ expect(readAll('a\\=\\u0041\\\\\nb')).toEqual(['a\\=\\u0041\\\\', 'b']);
+ });
+});
+
+describe('utils.rawParseLine', () => {
+ const split = (line: string) => {
+ const res = rawParseLine(line);
+ expect(res.line).toBe(line);
+ return [line.substring(0, res.sepStart), line.substring(res.valueStart)];
+ };
+
+ it('finds key and value', () => {
+ expect(split('key=value')).toEqual(['key', 'value']);
+ expect(split('key : value')).toEqual(['key', 'value']);
+ expect(split('key value')).toEqual(['key', 'value']);
+ expect(split('key =:value')).toEqual(['key', ':value']);
+ expect(split('k\\=ey\\ \\:=value')).toEqual(['k\\=ey\\ \\:', 'value']);
+ expect(split('key')).toEqual(['key', '']);
+ expect(split('=value')).toEqual(['', 'value']);
+ expect(split('')).toEqual(['', '']);
+ });
+
+ it('returns offsets', () => {
+ expect(rawParseLine('ab = cd')).toEqual({line: 'ab = cd', sepStart: 2, valueStart: 5});
+ });
+});
+
+describe('utils.rawParse', () => {
+ it('calls consumeFn for each logical line', () => {
+ const res: ParsedLine[] = [];
+ rawParse('a=1\n# c\nb : 2\\\n 3\nc', (line) => res.push(line));
+ expect(res).toEqual([
+ {line: 'a=1', sepStart: 1, valueStart: 2},
+ {line: 'b : 23', sepStart: 1, valueStart: 4},
+ {line: 'c', sepStart: 1, valueStart: 1},
+ ]);
+ });
+});
diff --git a/tools/fuzz.mts b/tools/fuzz.mts
new file mode 100644
index 0000000..1eaafdf
--- /dev/null
+++ b/tools/fuzz.mts
@@ -0,0 +1,84 @@
+// Differential fuzzing of the built package (dist/) against real JVMs:
+// - parse(input, {javaVersion}) vs Properties.load() of that JDK;
+// - stringify(props, {javaVersion}) vs Properties.store() (Writer and OutputStream), date line excluded.
+//
+// Usage: node tools/fuzz.mts [--runs N] [--seed N] (needs a build: `pnpm run reference:fuzz` builds first)
+// JDKs: JAVA_REFERENCE_JDKS, like tools/gen-expected.mts.
+import {createRequire} from 'node:module';
+import fc from 'fast-check';
+import {longText, props, text} from '../test/support/arbitraries.mts';
+import {captureLoad, type LoadResult} from '../test/support/load-result.mts';
+import {jvmLoad, jvmStore, listJdks} from './java-reference/reference.mts';
+
+const arg = (name: string, def: number) => {
+ const i = process.argv.indexOf(name);
+ return i >= 0 ? Number(process.argv[i + 1]) : def;
+};
+const runs = arg('--runs', 20000);
+const seed = arg('--seed', Date.now() % 1e9);
+console.log(`runs: ${runs}, seed: ${seed}`);
+
+const {parse, stringify} = createRequire(import.meta.url)('../dist/node-java-props.js');
+
+const inputs = fc.sample(fc.oneof({arbitrary: text(30), weight: 9}, {arbitrary: longText, weight: 1}), {
+ numRuns: runs,
+ seed,
+});
+const stores = fc.sample(fc.record({props, comments: fc.option(text(8), {nil: undefined})}), {
+ numRuns: Math.ceil(runs / 4),
+ seed,
+});
+
+// Same properties, or both throw (same message for IllegalArgumentException)
+const sameAsJava = (js: LoadResult, java: LoadResult) => {
+ if ('throws' in java) {
+ return 'throws' in js && (java.throws !== 'java.lang.IllegalArgumentException' || js.message === java.message);
+ }
+ return (
+ !('throws' in js) &&
+ JSON.stringify(Object.entries(js.props).sort()) === JSON.stringify(Object.entries(java.props).sort())
+ );
+};
+
+let failures = 0;
+const report = (what: string, details: unknown) => {
+ if (++failures <= 10) console.error(`MISMATCH ${what}\n${JSON.stringify(details, null, 2)}`);
+};
+
+for (const jdk of listJdks()) {
+ const major = Number(jdk.version.replace(/^1\./, '').split(/\D/)[0]);
+ const before = failures;
+
+ jvmLoad(jdk, inputs).forEach((java, i) => {
+ const js = captureLoad(() => parse(inputs[i], {javaVersion: jdk.version}));
+ if (!sameAsJava(js, java)) report(`parse (${jdk.name})`, {input: inputs[i], js, java});
+ });
+
+ const requests = stores.flatMap((s) => [s, {...s, stream: true}, {props: {}, comments: s.comments}]);
+ const outputs = jvmStore(jdk, requests);
+ stores.forEach((s, i) => {
+ const dateLine = outputs[i * 3 + 2].split('\n').length - 2;
+ for (const [k, stream] of [
+ [0, false],
+ [1, true],
+ ] as const) {
+ const java = outputs[i * 3 + k].split('\n').filter((_, n) => n !== dateLine);
+ const js = stringify(s.props, {
+ javaVersion: jdk.version,
+ comments: s.comments,
+ date: false,
+ lineSeparator: '\n',
+ escapeUnicode: stream,
+ }).split('\n');
+ // Before Java 18, store() writes in hash order
+ const same =
+ major >= 18
+ ? js.join('\n') === java.join('\n')
+ : JSON.stringify(js.sort()) === JSON.stringify(java.sort());
+ if (!same) report(`stringify (${jdk.name}, ${stream ? 'OutputStream' : 'Writer'})`, {...s, js, java});
+ }
+ });
+
+ console.log(`${jdk.name} (${jdk.version}): ${failures === before ? 'OK' : `${failures - before} mismatches`}`);
+}
+process.exit(failures ? 1 : 0);
diff --git a/tools/gen-expected.mts b/tools/gen-expected.mts
new file mode 100644
index 0000000..0c68770
--- /dev/null
+++ b/tools/gen-expected.mts
@@ -0,0 +1,149 @@
+// Generates the Java results of the test cases, by running real JVMs:
+// - test/cases/parse-expected.json: Properties.load() for each case of parse-cases.mts (one key per era, see LOAD_ERAS
+// in src/internal/java-version.ts);
+// - test/cases/stringify-expected.json: Properties.store() (Java 18+, without the date line) and Date.toString() for
+// the cases of stringify-cases.mts.
+// Also checks the test/fixtures files against every JVM, and that every JVM writes the same store() lines.
+//
+// Usage: node tools/gen-expected.mts [--check]
+// JDKs: JAVA_REFERENCE_JDKS="name=/path/to/bin/java,..." (default: `java` from PATH). Results without a matching JDK
+// (load era, Java 18+) keep their committed values.
+// --check: fail if a committed file is outdated, without writing it.
+import fs from 'node:fs';
+import path from 'node:path';
+import {LOAD_ERAS, loadEra} from '../src/internal/java-version.ts';
+import cases from '../test/cases/parse-cases.mts';
+import {dateCases, stringifyCases} from '../test/cases/stringify-cases.mts';
+import {
+ fixtureFiles,
+ PARSE_EXPECTED_FILE,
+ readFixture,
+ readFixtureResult,
+ readParseExpected,
+ STRINGIFY_EXPECTED_FILE,
+ type StringifyExpected,
+} from '../test/support/data.mts';
+import {type LoadResult, sortedProps} from '../test/support/load-result.mts';
+import {type Jdk, jvmDate, jvmLoad, jvmStore, listJdks} from './java-reference/reference.mts';
+
+const ROOT = path.join(import.meta.dirname, '..');
+const check = process.argv.includes('--check');
+
+const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
+let failed = false;
+const fail = (msg: string) => {
+ console.error(`ERROR: ${msg}`);
+ failed = true;
+};
+
+// Run every JDK, grouped by era
+const jdks = listJdks();
+const byEra = new Map();
+const inputs = cases.map((c) => c.input);
+const fixtures = fixtureFiles();
+const fixtureInputs = fixtures.map(readFixture);
+for (const jdk of jdks) {
+ const era = loadEra(jdk.version);
+ console.log(`${jdk.name}: java.version=${jdk.version} -> era ${era}`);
+ const results = jvmLoad(jdk, [...inputs, ...fixtureInputs]);
+ if (!byEra.has(era)) byEra.set(era, []);
+ byEra.get(era)?.push({jdk, results});
+}
+
+// Fixtures: every JVM must match the committed .expected.json
+fixtures.forEach((file, i) => {
+ const expected = {props: sortedProps(readFixtureResult(file))};
+ for (const runs of byEra.values()) {
+ for (const {jdk, results} of runs) {
+ if (!same(results[inputs.length + i], expected)) fail(`fixture ${file}: ${jdk.name} result differs`);
+ }
+ }
+});
+
+// Cases
+const previous: Record> = fs.existsSync(PARSE_EXPECTED_FILE) ? readParseExpected() : {};
+const expected: Record> = {};
+const names = new Set();
+cases.forEach((c, i) => {
+ if (names.has(c.name)) fail(`duplicate case name: ${c.name}`);
+ names.add(c.name);
+ const entry: Record = {input: c.input};
+ for (const era of LOAD_ERAS) {
+ const runs = byEra.get(era);
+ if (!runs) {
+ if (previous[c.name]?.[era]) entry[era] = previous[c.name][era];
+ continue;
+ }
+ const [first, ...others] = runs;
+ for (const other of others) {
+ if (!same(first.results[i], other.results[i])) {
+ fail(`${c.name}: ${first.jdk.name} and ${other.jdk.name} disagree (same era ${era}): new era?`);
+ }
+ }
+ entry[era] = first.results[i];
+ }
+ expected[c.name] = entry;
+});
+
+for (const era of LOAD_ERAS)
+ if (!byEra.has(era)) console.warn(`WARNING: no JDK for era ${era}, keeping committed results`);
+output(PARSE_EXPECTED_FILE, expected);
+
+// Stringify cases: store() of Java 18+ (sorted keys), date line removed
+const major = (jdk: Jdk) => Number(jdk.version.replace(/^1\./, '').split(/\D/)[0]);
+const storeJdks = jdks.filter((jdk) => major(jdk) >= 18).sort((a, b) => major(b) - major(a));
+if (storeJdks.length === 0) {
+ console.warn('WARNING: no JDK 18+, keeping committed stringify results');
+} else {
+ const storeExpected: StringifyExpected = {cases: {}, dates: {}};
+ const lines = (out: string) => out.replace(/\r\n/g, '\n').split('\n');
+ for (const jdk of [storeJdks[0], ...jdks.filter((j) => j !== storeJdks[0])]) {
+ const requests = stringifyCases.flatMap((c) => [
+ {props: c.props, comments: c.comments},
+ {props: c.props, comments: c.comments, stream: true},
+ {props: {}, comments: c.comments}, // to find the date line
+ ]);
+ const outputs = jvmStore(jdk, requests);
+ stringifyCases.forEach((c, i) => {
+ const dateLine = lines(outputs[i * 3 + 2]).length - 2;
+ const [writer, stream] = [outputs[i * 3], outputs[i * 3 + 1]].map((out) =>
+ lines(out)
+ .filter((_, n) => n !== dateLine)
+ .join('\n')
+ );
+ if (jdk === storeJdks[0]) {
+ storeExpected.cases[c.name] = {props: c.props, comments: c.comments, writer, stream};
+ } else {
+ // Same lines (in hash order before Java 18)
+ const ref = storeExpected.cases[c.name];
+ const sameLines = (a: string, b: string) => same(a.split('\n').sort(), b.split('\n').sort());
+ if (!sameLines(writer, ref.writer) || !sameLines(stream, ref.stream)) {
+ fail(`stringify ${c.name}: ${jdk.name} and ${storeJdks[0].name} write different lines`);
+ }
+ }
+ });
+ }
+ // Time zone names change across Java versions: the newest JDK is the reference
+ const dates = jvmDate(
+ storeJdks[0],
+ dateCases.map((d) => ({time: d.time, timeZone: d.javaTimeZone}))
+ );
+ dateCases.forEach((d, i) => {
+ storeExpected.dates[d.name] = {time: d.time, timeZone: d.timeZone, java: dates[i]};
+ });
+ output(STRINGIFY_EXPECTED_FILE, storeExpected);
+}
+process.exit(failed ? 1 : 0);
+
+function output(file: string, value: unknown) {
+ const json = `${JSON.stringify(value, null, 2)}\n`;
+ const name = path.relative(ROOT, file);
+ if (!check) {
+ fs.writeFileSync(file, json);
+ console.log(`written ${name}`);
+ } else if (fs.existsSync(file) && fs.readFileSync(file, 'utf8') === json) {
+ console.log(`${name} is up to date`);
+ } else {
+ fail(`${name} is outdated: run "pnpm run reference:gen"`);
+ }
+}
diff --git a/tools/java-reference/JavaReference.java b/tools/java-reference/JavaReference.java
new file mode 100644
index 0000000..5e39256
--- /dev/null
+++ b/tools/java-reference/JavaReference.java
@@ -0,0 +1,109 @@
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Date;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TimeZone;
+import java.util.TreeMap;
+
+/**
+ * Runs java.util.Properties for reference.mts. Java 8 compatible (compiled with --release 8, run on every tested JDK).
+ *
+ * Protocol: one request per line on stdin, one response per line on stdout. Strings are sent as hex UTF-16 code
+ * units (4 digits per char), so that any char gets through (line terminators, lone surrogates...):
+ * - "version" -> ""
+ * - "load " -> "ok :,..." (sorted by key), or "throw "
+ * - "store :,..." -> "ok ": the output of
+ * store(Writer) or store(OutputStream) (decoded as ISO-8859-1), date line included
+ * - "date