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 [![npm version](https://img.shields.io/npm/v/java-props.svg)](https://www.npmjs.com/package/java-props) -![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg) -[![Build Status](https://app.travis-ci.com/nathan818fr/node-java-props.svg?branch=master)](https://app.travis-ci.com/nathan818fr/node-java-props) -[![codecov](https://codecov.io/gh/nathan818fr/node-java-props/branch/master/graph/badge.svg)](https://codecov.io/gh/nathan818fr/node-java-props) +[![CI](https://github.com/nathan818fr/node-java-props/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/nathan818fr/node-java-props/actions/workflows/ci.yml) +[![license](https://img.shields.io/npm/l/java-props.svg)](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