├── .clang-format ├── .codecov.yml ├── .editorconfig ├── .github ├── actions │ └── setup │ │ └── action.yml ├── prebuild.sh ├── renovate.json └── workflows │ ├── build.yml │ ├── codeql.yml │ ├── docs.yml │ ├── npm_package.yml │ ├── ossf-scorecard.yml │ ├── prebuilds.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .node-version ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── binding.d.ts ├── binding.gyp ├── binding.js ├── build_flags.gypi ├── ci └── check_symvers.js ├── deps ├── .npmignore └── zstd.gyp ├── eslint.config.js ├── jest.config.js ├── lib ├── compress.ts ├── decompress.ts ├── index.ts ├── simple.ts └── util.ts ├── package-lock.json ├── package.json ├── src ├── binding.cc ├── cctx.cc ├── cctx.h ├── cdict.cc ├── cdict.h ├── constants.cc ├── constants.h ├── dctx.cc ├── dctx.h ├── ddict.cc ├── ddict.h ├── object_wrap_helper.h └── util.h ├── tests ├── binding.ts ├── data │ └── minimal.dct ├── fast_check.ts ├── hl_compress.ts ├── hl_decompress.ts └── util.ts ├── tsconfig.emit.json ├── tsconfig.json ├── tsconfig.old-ts.json ├── tsdoc.json └── typedoc.config.js /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: Chromium 4 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: off 6 | patch: off 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: setup 2 | inputs: 3 | node-version: 4 | description: Node version override 5 | runs: 6 | using: composite 7 | steps: 8 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 9 | with: 10 | node-version: ${{ inputs.node-version }} 11 | node-version-file: ${{ (inputs.node-version == '' && '.node-version') || null }} 12 | cache: npm 13 | - run: npm ci --ignore-scripts 14 | shell: bash 15 | -------------------------------------------------------------------------------- /.github/prebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu -o pipefail 3 | cd "$(dirname "$0")/.." 4 | 5 | mkdir -p prebuilds 6 | platform="$(node -p process.platform)" 7 | arch="${1:-$(node -p process.arch)}" 8 | template="$(jq -r '"\(.name)-v\(.version)"' package.json)-napi-vNAPI_VER-${platform}-${arch}.tar.gz" 9 | for napi_ver in $(jq .binary.napi_versions[] package.json); do 10 | ./node_modules/.bin/node-gyp rebuild --copy_licenses=1 --napi_build_version="$napi_ver" --target_arch="$arch" 11 | tar c --format pax --numeric-owner build/Release/{*.node,LICENSE*,NOTICE*} | 12 | gzip -9 -n > "prebuilds/${template/NAPI_VER/${napi_ver}}" 13 | done 14 | if [[ "$(uname -s)" == "Linux" ]]; then 15 | echo "Checking symbol versions" 16 | node ./ci/check_symvers.js build/Release/binding.node 17 | fi 18 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:js-lib", "config:best-practices", "npm:unpublishSafe"], 4 | "assignAutomerge": true, 5 | "packageRules": [ 6 | { 7 | "matchUpdateTypes": ["minor", "patch", "digest"], 8 | "schedule": ["before 8am on saturday"] 9 | }, 10 | { 11 | "matchSourceUrls": ["https://github.com/eslint/eslint"], 12 | "groupName": "eslint monorepo" 13 | }, 14 | { 15 | "matchManagers": ["github-actions"], 16 | "matchPackageNames": ["actions/*", "codecov/*", "github/*", "ossf/*"], 17 | "matchUpdateTypes": ["minor", "patch", "pinDigest"], 18 | "groupName": "CI actions (non-major)" 19 | }, 20 | { 21 | "matchDepTypes": ["devDependencies"], 22 | "matchCurrentVersion": "!/^0/", 23 | "matchPackageNames": ["!typescript"], 24 | "matchUpdateTypes": ["minor"], 25 | "groupName": "dev dependencies (non-major)" 26 | }, 27 | { 28 | "matchDepTypes": ["devDependencies"], 29 | "matchUpdateTypes": ["patch", "digest"], 30 | "groupName": "dev dependencies (non-major)" 31 | }, 32 | { 33 | "matchDepTypes": ["devDependencies"], 34 | "automerge": true 35 | }, 36 | { 37 | "matchDepTypes": ["action"], 38 | "matchUpdateTypes": ["minor", "patch", "pinDigest"], 39 | "automerge": true 40 | }, 41 | { 42 | "matchDatasources": ["npm"], 43 | "matchPackageNames": ["@types/node", "node-addon-api"], 44 | "matchUpdateTypes": ["major"], 45 | "dependencyDashboardApproval": true 46 | }, 47 | { 48 | "matchManagers": ["nodenv"], 49 | "matchUpdateTypes": ["major"], 50 | "dependencyDashboardApproval": true 51 | }, 52 | { 53 | "matchManagers": ["github-actions"], 54 | "matchPackageNames": ["slsa-framework/slsa-github-generator"], 55 | "pinDigests": false 56 | } 57 | ], 58 | "reviewers": ["drakedevel"], 59 | "timezone": "America/Los_Angeles" 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: push 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | build-and-test: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node: [16, 18, '', 22, 23, 24] 13 | runs-on: ubuntu-24.04 14 | permissions: 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | with: 19 | submodules: recursive 20 | - uses: ./.github/actions/setup 21 | with: 22 | node-version: ${{ matrix.node }} 23 | - run: npm run build 24 | env: 25 | JOBS: 3 26 | ZSTD_NAPI_ENABLE_GCOV: ${{ (matrix.node == '' && 1) || null }} 27 | - run: npm run ${{ (matrix.node == '' && 'test-coverage') || 'test' }} 28 | - name: Submit coverage data to Codecov 29 | if: matrix.node == '' 30 | uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 31 | with: 32 | use_oidc: true 33 | 34 | npm-package: 35 | uses: ./.github/workflows/npm_package.yml 36 | permissions: 37 | actions: read 38 | contents: write 39 | id-token: write 40 | 41 | prebuilds: 42 | uses: ./.github/workflows/prebuilds.yml 43 | permissions: 44 | actions: read 45 | contents: write 46 | id-token: write 47 | 48 | other-ts-versions: 49 | runs-on: ubuntu-24.04 50 | steps: 51 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 52 | - uses: ./.github/actions/setup 53 | - run: | 54 | for version in 4.5 4.6 4.7 4.8 4.9 5.0 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8; do 55 | echo "=== Typescript ${version} ===" 56 | npx -y --package=typescript@$version -- tsc -p tsconfig.old-ts.json 57 | done 58 | 59 | lint: 60 | runs-on: ubuntu-24.04 61 | steps: 62 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 63 | - uses: ./.github/actions/setup 64 | - run: ./node_modules/.bin/tsc -p . 65 | - run: npm run lint 66 | - name: Check JS/TS is formatted with Prettier 67 | run: ./node_modules/.bin/prettier --check . 68 | - name: Check C++ is formatted with clang-format 69 | run: | 70 | clang-format-18 -i src/* 71 | git diff --stat --exit-code src/ 72 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | schedule: 8 | - cron: '27 13 * * 1' 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 360 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: [c-cpp, javascript-typescript] 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 30 | with: 31 | submodules: ${{ (matrix.language == 'c-cpp' && 'recursive') || '' }} 32 | 33 | - uses: ./.github/actions/setup 34 | 35 | - name: Pre-build libzstd 36 | if: matrix.language == 'c-cpp' 37 | run: | 38 | ./node_modules/.bin/node-gyp configure 39 | make -C build -j3 Release/obj.target/deps/zstd.a 40 | 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 43 | with: 44 | languages: ${{ matrix.language }} 45 | queries: security-extended,security-and-quality 46 | 47 | - name: Build 48 | if: matrix.language == 'c-cpp' 49 | run: ./node_modules/.bin/node-gyp build 50 | env: 51 | JOBS: 3 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 55 | with: 56 | category: '/language:${{matrix.language}}' 57 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs to Pages 2 | 3 | on: 4 | push: 5 | branches: [main, typedoc] 6 | 7 | permissions: 8 | contents: read 9 | 10 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 11 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 12 | concurrency: 13 | group: pages 14 | cancel-in-progress: false 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-24.04 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 22 | - uses: ./.github/actions/setup 23 | - run: npm run typedoc 24 | - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 25 | with: 26 | path: docs 27 | 28 | deploy: 29 | needs: build 30 | permissions: 31 | pages: write 32 | id-token: write 33 | environment: 34 | name: github-pages 35 | url: ${{ steps.deployment.outputs.page_url }} 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 39 | id: deployment 40 | -------------------------------------------------------------------------------- /.github/workflows/npm_package.yml: -------------------------------------------------------------------------------- 1 | name: npm-package 2 | on: 3 | workflow_call: 4 | inputs: 5 | provenance: 6 | type: boolean 7 | upload-assets: 8 | type: boolean 9 | outputs: 10 | package-name: 11 | value: ${{ jobs.build-provenance.outputs.package-name }} 12 | package-download-name: 13 | value: ${{ jobs.build-provenance.outputs.package-download-name }} 14 | package-download-sha256: 15 | value: ${{ jobs.build-provenance.outputs.package-download-sha256 }} 16 | provenance-name: 17 | value: ${{ jobs.build-provenance.outputs.provenance-name }} 18 | provenance-download-name: 19 | value: ${{ jobs.build-provenance.outputs.provenance-download-name }} 20 | provenance-download-sha256: 21 | value: ${{ jobs.build-provenance.outputs.provenance-download-sha256 }} 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | build-unsigned: 28 | runs-on: ubuntu-24.04 29 | if: '!inputs.provenance' 30 | outputs: 31 | artifact-id: ${{ steps.upload.outputs.artifact-id }} 32 | steps: 33 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | with: 35 | submodules: recursive 36 | - uses: ./.github/actions/setup 37 | - run: ./node_modules/.bin/tsc -p tsconfig.emit.json 38 | - id: npm_pack 39 | name: Build npm package 40 | run: echo "package=$(npm pack -s)" >> "$GITHUB_OUTPUT" 41 | - id: upload 42 | name: Upload NPM package 43 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 44 | with: 45 | name: npm-package 46 | path: ${{ steps.npm_pack.outputs.package }} 47 | 48 | build-provenance: 49 | uses: slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml@v2.1.0 50 | if: inputs.provenance 51 | permissions: 52 | actions: read 53 | contents: read 54 | id-token: write 55 | with: 56 | node-version-file: .node-version 57 | run-scripts: submodule-update,ci-ignore-scripts 58 | 59 | upload: 60 | if: inputs.upload-assets && !(failure() || cancelled()) 61 | needs: [build-provenance, build-unsigned] 62 | runs-on: ubuntu-24.04 63 | permissions: 64 | contents: write 65 | steps: 66 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 67 | 68 | - if: inputs.provenance 69 | uses: slsa-framework/slsa-github-generator/actions/nodejs/secure-package-download@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 70 | with: 71 | name: ${{ needs.build-provenance.outputs.package-download-name }} 72 | path: ${{ needs.build-provenance.outputs.package-name }} 73 | sha256: ${{ needs.build-provenance.outputs.package-download-sha256 }} 74 | 75 | - if: '!inputs.provenance' 76 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 77 | with: 78 | artifact-ids: ${{ needs.build-unsigned.outputs.artifact-id }} 79 | merge-multiple: true # There's only one artifact, but otherwise it winds up in a subdirectory 80 | 81 | - run: gh release upload '${{ github.ref_name }}' zstd-napi-*.tgz 82 | env: 83 | GH_TOKEN: ${{ github.token }} 84 | 85 | - if: inputs.provenance 86 | uses: slsa-framework/slsa-github-generator/actions/nodejs/secure-attestations-download@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 87 | with: 88 | name: ${{ needs.build-provenance.outputs.provenance-download-name }} 89 | path: attestations 90 | sha256: ${{ needs.build-provenance.outputs.provenance-download-sha256 }} 91 | 92 | - if: inputs.provenance 93 | run: gh release upload '${{ github.ref_name }}' 'attestations/${{ needs.build-provenance.outputs.provenance-download-name }}/${{ needs.build-provenance.outputs.provenance-name }}' 94 | env: 95 | GH_TOKEN: ${{ github.token }} 96 | -------------------------------------------------------------------------------- /.github/workflows/ossf-scorecard.yml: -------------------------------------------------------------------------------- 1 | name: OSSF Scorecard analysis workflow 2 | on: 3 | branch_protection_rule: 4 | schedule: 5 | - cron: '18 14 * * 5' 6 | push: 7 | branches: [main] 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | analysis: 13 | name: Scorecard analysis 14 | runs-on: ubuntu-latest 15 | permissions: 16 | security-events: write 17 | id-token: write 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | 25 | - name: Run analysis 26 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 27 | with: 28 | results_file: results.sarif 29 | results_format: sarif 30 | publish_results: true 31 | 32 | - name: Upload artifact 33 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 34 | with: 35 | name: SARIF file 36 | path: results.sarif 37 | retention-days: 5 38 | 39 | - name: Upload SARIF results to code scanning 40 | uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 41 | with: 42 | sarif_file: results.sarif 43 | -------------------------------------------------------------------------------- /.github/workflows/prebuilds.yml: -------------------------------------------------------------------------------- 1 | name: prebuilds 2 | on: 3 | workflow_call: 4 | inputs: 5 | provenance: 6 | type: boolean 7 | upload-assets: 8 | type: boolean 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | runs-on: ${{ 16 | (matrix.os == 'linux' && 'ubuntu-24.04') || 17 | (matrix.os == 'macos' && 'macos-14') || 18 | (matrix.os == 'windows' && 'windows-2022') }} 19 | container: 20 | image: ${{ matrix.os == 'linux' && 'debian:11-slim' || null }} 21 | strategy: 22 | matrix: 23 | os: [linux, macos, windows] 24 | cross: [''] 25 | include: 26 | - { os: windows, cross: ia32 } 27 | outputs: 28 | hash-linux: ${{ steps.output.outputs.hash-linux }} 29 | hash-macos: ${{ steps.output.outputs.hash-macos }} 30 | hash-windows: ${{ steps.output.outputs.hash-windows }} 31 | hash-windows-ia32: ${{ steps.output.outputs.hash-windows-ia32 }} 32 | id-linux: ${{ steps.output.outputs.id-linux }} 33 | id-macos: ${{ steps.output.outputs.id-macos }} 34 | id-windows: ${{ steps.output.outputs.id-windows }} 35 | id-windows-ia32: ${{ steps.output.outputs.id-windows-ia32 }} 36 | env: 37 | JOBS: 3 38 | steps: 39 | - name: Install system dependencies 40 | run: apt-get update && apt-get -y install g++ g++-aarch64-linux-gnu g++-arm-linux-gnueabihf git jq make python3 41 | if: runner.os == 'Linux' 42 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 43 | with: 44 | submodules: recursive 45 | - uses: ./.github/actions/setup 46 | - name: Build ${{ (runner.arch == 'ARM64' && 'arm64') || 'x64' }} 47 | run: bash .github/prebuild.sh 48 | if: matrix.cross == '' 49 | - name: Build arm cross 50 | run: bash .github/prebuild.sh arm 51 | if: runner.os == 'Linux' 52 | env: 53 | CC: arm-linux-gnueabihf-gcc 54 | CXX: arm-linux-gnueabihf-g++ 55 | - name: Build arm64 cross 56 | run: bash .github/prebuild.sh arm64 57 | if: runner.os == 'Linux' 58 | env: 59 | CC: aarch64-linux-gnu-gcc 60 | CXX: aarch64-linux-gnu-g++ 61 | - name: Build ia32 cross 62 | run: bash .github/prebuild.sh ia32 63 | if: matrix.cross == 'ia32' 64 | - name: Build x64 cross 65 | run: bash .github/prebuild.sh x64 66 | if: runner.os == 'macOS' 67 | - id: upload 68 | name: Upload prebuilds 69 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 70 | with: 71 | name: prebuilds-${{ matrix.os }}${{ matrix.cross != '' && '-' || '' }}${{ matrix.cross }} 72 | path: prebuilds/ 73 | - id: output 74 | run: | 75 | suffix='${{ matrix.os }}${{ matrix.cross != '' && '-' || '' }}${{ matrix.cross }}' 76 | echo "hash-${suffix}=$(openssl sha256 -r * | tr '*' ' ' | jq -Rrs @base64)" >> "$GITHUB_OUTPUT" 77 | echo "id-${suffix}=${{ steps.upload.outputs.artifact-id }}" >> "$GITHUB_OUTPUT" 78 | shell: bash 79 | working-directory: prebuilds 80 | 81 | upload-build: 82 | if: inputs.upload-assets 83 | needs: [build] 84 | runs-on: ubuntu-24.04 85 | permissions: 86 | contents: write 87 | steps: 88 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 89 | - id: artifacts 90 | run: echo "ids=$(jq -nr '[env.INPUTS|fromjson|with_entries(select(.key|startswith("id-"))).[]]|join(",")')" >> "$GITHUB_OUTPUT" 91 | env: 92 | INPUTS: ${{ toJSON(needs.build.outputs) }} 93 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 94 | with: 95 | artifact-ids: ${{ steps.artifacts.outputs.ids }} 96 | merge-multiple: true 97 | path: prebuilds 98 | - run: gh release upload '${{ github.ref_name }}' ./prebuilds/*.tar.gz 99 | env: 100 | GH_TOKEN: ${{ github.token }} 101 | 102 | hashes: 103 | needs: [build] 104 | if: inputs.provenance 105 | runs-on: ubuntu-24.04 106 | outputs: 107 | hashes: ${{ steps.combine.outputs.hashes }} 108 | steps: 109 | - id: combine 110 | run: echo "hashes=$(jq -nr '[env.INPUTS|fromjson|with_entries(select(.key|startswith("hash-")))|.[]|@base64d]|join("")|@base64')" >> "$GITHUB_OUTPUT" 111 | env: 112 | INPUTS: ${{ toJSON(needs.build.outputs) }} 113 | - run: echo "${{ steps.combine.outputs.hashes }}" 114 | 115 | provenance: 116 | needs: [hashes] 117 | if: inputs.provenance 118 | permissions: 119 | actions: read 120 | contents: write 121 | id-token: write 122 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 123 | with: 124 | base64-subjects: ${{ needs.hashes.outputs.hashes }} 125 | provenance-name: prebuilds.intoto.jsonl 126 | 127 | upload-provenance: 128 | if: inputs.provenance && inputs.upload-assets 129 | needs: [provenance] 130 | runs-on: ubuntu-24.04 131 | permissions: 132 | contents: write 133 | steps: 134 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 135 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 136 | with: 137 | name: ${{ needs.provenance.outputs.provenance-name }} 138 | - run: gh release upload '${{ github.ref_name }}' '${{ needs.provenance.outputs.provenance-name }}' 139 | env: 140 | GH_TOKEN: ${{ github.token }} 141 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - v*.*.* 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | sanity-check: 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | - name: Verify package.json has correct version for tag 16 | run: | 17 | pkg_json="$(jq -r .version package.json)" 18 | tag="${{ github.ref_name }}" 19 | tag_ver="$(sed -E 's/^v([^-]+)(-.*)?$/\1/' <<< "$tag")" 20 | if [[ "$pkg_json" != "$tag_ver" ]]; then 21 | echo "::error::package.json has version ${pkg_json}, expected ${tag_ver}" 22 | exit 1 23 | fi 24 | 25 | create-draft-release: 26 | needs: [sanity-check] 27 | runs-on: ubuntu-24.04 28 | permissions: 29 | contents: write 30 | steps: 31 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 32 | - run: gh release create ${{ github.ref_name }} -d -t ${{ github.ref_name }} --verify-tag 33 | env: 34 | GH_TOKEN: ${{ github.token }} 35 | 36 | npm-package: 37 | needs: [create-draft-release] 38 | uses: ./.github/workflows/npm_package.yml 39 | permissions: 40 | actions: read 41 | contents: write 42 | id-token: write 43 | with: 44 | provenance: true 45 | upload-assets: true 46 | 47 | prebuilds: 48 | needs: [create-draft-release] 49 | uses: ./.github/workflows/prebuilds.yml 50 | permissions: 51 | actions: read 52 | contents: write 53 | id-token: write 54 | with: 55 | provenance: true 56 | upload-assets: true 57 | 58 | npm-publish: 59 | if: "!contains(github.ref_name, '-')" 60 | environment: npm-publish 61 | needs: [npm-package, prebuilds] 62 | runs-on: ubuntu-24.04 63 | steps: 64 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 65 | with: 66 | registry-url: https://registry.npmjs.org 67 | - uses: slsa-framework/slsa-github-generator/actions/nodejs/publish@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 68 | with: 69 | node-auth-token: ${{ secrets.NPM_TOKEN }} 70 | package-name: ${{ needs.npm-package.outputs.package-name }} 71 | package-download-name: ${{ needs.npm-package.outputs.package-download-name }} 72 | package-download-sha256: ${{ needs.npm-package.outputs.package-download-sha256 }} 73 | provenance-name: ${{ needs.npm-package.outputs.provenance-name }} 74 | provenance-download-name: ${{ needs.npm-package.outputs.provenance-download-name }} 75 | provenance-download-sha256: ${{ needs.npm-package.outputs.provenance-download-sha256 }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /coverage/ 3 | /docs/ 4 | /dist/ 5 | /node_modules/ 6 | /prebuilds/ 7 | zstd-napi-*.tgz 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/zstd"] 2 | path = deps/zstd 3 | url = https://github.com/facebook/zstd.git 4 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.19.1 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | message=Release version %s 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | deps/zstd 3 | dist 4 | docs 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | }; 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ### Changed 6 | 7 | - Build GNU/Linux prebuilds on Debian 11 (binaries are still compatible with Glibc 2.28). 8 | 9 | ## [0.0.11] - 2025-05-11 10 | 11 | ### Changed 12 | 13 | - Import native binary with explicit `.node` file extension to improve bundler compatibility. ([#140]) 14 | 15 | [#140]: https://github.com/drakedevel/zstd-napi/issues/140 16 | 17 | ## [0.0.10] - 2025-03-15 18 | 19 | ### Changed 20 | 21 | - Build native library with basic hardening flags. 22 | 23 | ### Fixed 24 | 25 | - From-source builds using the published NPM package work again (broken in 0.0.9). ([#135]) 26 | 27 | [#135]: https://github.com/drakedevel/zstd-napi/issues/135 28 | 29 | ## [0.0.9] - 2025-02-21 30 | 31 | ### Changed 32 | 33 | - Build macOS prebuilds on macOS 14. 34 | - Upgrade Zstandard to 1.5.7. 35 | 36 | ### Fixed 37 | 38 | - From-source builds now work on Windows without requiring POSIX command-line utilities. ([#93]) 39 | - Native library no longer exposes unnecessary global symbols on GNU/Linux or macOS. 40 | 41 | [#93]: https://github.com/drakedevel/zstd-napi/issues/93 42 | 43 | ## [0.0.8] - 2024-04-07 44 | 45 | ### Added 46 | 47 | - API documentation. 48 | - High-level `compress` and `decompress` convenience functions. 49 | - Low-level bindings for `ZSTD_defaultCLevel` and `ZSTD_getDictID_fromCDict`. 50 | - Prebuilds for GNU/Linux on arm64 and armv7, Windows on ia32. 51 | - New `targetCBlockSize` compression parameter, stabilized in Zstandard 1.5.6. 52 | - Releases now include [SLSA Build L3](https://slsa.dev/spec/v1.0/levels#build-l3) provenance attestations. 53 | 54 | ### Changed 55 | 56 | - Build GNU/Linux prebuilds on Debian 10 (Glibc 2.28). 57 | - Class type definitions for the low-level bindings now have brands to prevent interchanging e.g. `CDict` and `DDict`. 58 | - Upgrade `node-addon-api` to 7.x. 59 | - Upgrade Zstandard to 1.5.6. 60 | 61 | ### Removed 62 | 63 | - Prebuilds for Node-API 6. 64 | - Support for Node-API < 8. 65 | - Support for GNU/Linux with Glibc < 2.28. 66 | - Support for TypeScript < 4.3. 67 | 68 | ### Fixed 69 | 70 | - Bindings use type tags (added in Node-API 8) to prevent passing an invalid native object type as a parameter. 71 | - Types no longer require `esModuleInterop` to be enabled. 72 | 73 | ## [0.0.7] - 2023-06-14 74 | 75 | ### Added 76 | 77 | - Prebuilds for macOS on arm64. 78 | 79 | ### Changed 80 | 81 | - Upgrade Zstandard to 1.5.5. 82 | 83 | ### Removed 84 | 85 | - Support for Node < 16. 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | zstd-napi 2 | Copyright 2020 Andrew Drake 3 | 4 | Licensed under the Apache License, Version 2.0. 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/zstd-napi)](https://www.npmjs.com/package/zstd-napi) 2 | [![codecov](https://codecov.io/github/drakedevel/zstd-napi/graph/badge.svg?token=Ry4jOq8sCE)](https://codecov.io/github/drakedevel/zstd-napi) 3 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/drakedevel/zstd-napi/badge)](https://securityscorecards.dev/viewer/?uri=github.com/drakedevel/zstd-napi) 4 | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8241/badge)](https://www.bestpractices.dev/projects/8241) 5 | 6 | # zstd-napi 7 | 8 | `zstd-napi` is a TypeScript binding to the native [Zstandard][zstd] compression library, using the [Node-API][node-api] addon interface. 9 | 10 | A strong emphasis has been placed on reliability and performance. It has been used for years in at least one production envionment where it is invoked more than a million times per second. 11 | 12 | [zstd]: https://github.com/facebook/zstd 13 | [node-api]: https://nodejs.org/docs/latest/api/n-api.html 14 | 15 | ## Getting Started 16 | 17 | To install, just run `npm install zstd-napi` (or the equivalent for your choice of package manager). By default, this will download a pre-built native module suitable for your platform from the corresponding [GitHub release][gh-release]. If a prebuild isn't available, it will try to build the module from source, which should work as long as you have a C++ compiler installed. 18 | 19 | Once installed, usage is as simple as: 20 | 21 | ```ts 22 | import * as zstd from 'zstd-napi'; 23 | zstd.compress(Buffer.from('your data here')); 24 | ``` 25 | 26 | See the [API documentation][api-docs] for more details! 27 | 28 | [api-docs]: https://drakedevel.github.io/zstd-napi/ 29 | [gh-release]: https://github.com/drakedevel/zstd-napi/releases 30 | 31 | ## Features 32 | 33 | The library exposes all of the functionality present in the stable Zstandard API, including dictionary compression, multithreading, advanced compression parameters, and more. 34 | 35 | Most of this functionality is available through a [high-level API][hl-api], which is the recommended way to use the library for nearly all applications. Both streaming and single-pass interfaces are available. 36 | 37 | The high-level API is built on top of the [low-level API][ll-api], which is exposed as `zstd-napi/binding`. This is a wrapper around the raw Zstandard API designed to be as thin as possible while preventing JavaScript code from doing anything memory-unsafe. Notably, buffer management is left to the caller. It may be useful as a building block for another library developer, or for applications with specialized use-cases. Most users will be happier using the high-level API. 38 | 39 | Currently, there's no support for the (unstable) Zstandard "experimental" API, but at least partial support is planned for the future. If there's functionality you'd like to use, please [file an issue][new-issue] and let me know! 40 | 41 | [hl-api]: https://drakedevel.github.io/zstd-napi/modules/index.html 42 | [ll-api]: https://drakedevel.github.io/zstd-napi/modules/binding.html 43 | [new-issue]: https://github.com/drakedevel/zstd-napi/issues/new/choose 44 | 45 | ## Support Policy 46 | 47 | ### Node.js 48 | 49 | All live Node.js verisons are supported on a first-class basis. Per the [release cycle][node-releases], this includes the Current version, the Active LTS version, and one or more Maintenance versions. In addition, one previous LTS version is supported on a best-effort basis. As of the latest `zstd-napi` release, this means: 50 | 51 | | Node.js Version | Support | 52 | | --------------- | :---------: | 53 | | 24 (Current) | ✅ | 54 | | 23 (Maint.) | ✅ | 55 | | 22 (Active LTS) | ✅ | 56 | | 20 (Maint. LTS) | ✅ | 57 | | 16, 18 | best-effort | 58 | 59 | Other versions may work, but they aren't regularly tested and may break at any time. 60 | 61 | [node-releases]: https://github.com/nodejs/release#release-schedule 62 | 63 | ### Platform 64 | 65 | All of the native code in this project is fairly portable, so in principle `zstd-napi` should work on any platform supported by both Node.js and Zstandard. 66 | 67 | Prebuilds are provided for all platforms with [Tier 1 support][tier-1] in any live version of Node.js. This includes GNU/Linux armv7, arm64, and x64, macOS arm64 and x64, and Windows x64 and x86. GNU/Linux prebuilds are compatible with glibc >= 2.28 and libstdc++ >= 6.0.25, which are the same versions required by official Node.js binaries since version 18. 68 | 69 | Please [file an issue][new-issue] if this library doesn't work on your platform! 70 | 71 | [tier-1]: https://github.com/nodejs/node/blob/main/BUILDING.md#platform-list 72 | 73 | ### Zstandard 74 | 75 | `zstd-napi`'s native component statically links Zstandard, currently version 1.5.7. Newer versions will be pulled in as they are released. 76 | 77 | ### Security Updates 78 | 79 | This project will make every effort to promptly release fixes for discovered security vulnerabilties. Whenever possible, patches will be released for previous release branches to allow affected users to easily upgrade without other breaking changes. 80 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Security vulnerabilities can be privately reported with GitHub's Security Advisories 6 | feature by clicking [here](https://github.com/drakedevel/zstd-napi/security/advisories/new). 7 | -------------------------------------------------------------------------------- /binding.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module (imported as `zstd-napi/binding`) exposes a thin (but safe!) 3 | * wrapper around the native Zstandard API. If you aren't trying to do something 4 | * weird, use the {@link "index" | high-level API} instead. 5 | * 6 | * Native API functions that are associated with a data structure are methods on 7 | * the wrapper class corresponding to that data structure. For example, many 8 | * compression functions take a `ZSTD_CCtx` and can therefore be found on the 9 | * {@link CCtx} class. 10 | * 11 | * The upstream 12 | * {@link https://facebook.github.io/zstd/zstd_manual.html | Zstandard manual} 13 | * is the best source for understanding how to use this API. While this 14 | * documentation tries to be helpful, it has only a small fraction of the 15 | * information. Documentation for structures and functions within this module 16 | * will mention the corresponding native names to enable cross-referencing. 17 | * (Alas, it appears not to be possible to _link_ to functions on that page...) 18 | * 19 | * @module 20 | */ 21 | 22 | /** 23 | * Magic number denoting the start of a Zstandard frame. 24 | * 25 | * Corresponds to `ZSTD_MAGICNUMBER`. 26 | */ 27 | export const MAGICNUMBER: number; 28 | 29 | /** 30 | * Magic number denoting the start of a Zstandard dictionary. 31 | * 32 | * Corresponds to `ZSTD_MAGIC_DICTIONARY`. 33 | */ 34 | export const MAGIC_DICTIONARY: number; 35 | 36 | /** 37 | * Corresponds to `ZSTD_MAGIC_SKIPPABLE_START`. 38 | * @experimental 39 | */ 40 | export const MAGIC_SKIPPABLE_START: number; 41 | 42 | /** 43 | * Corresponds to `ZSTD_MAGIC_SKIPPABLE_MASK`. 44 | * @experimental 45 | */ 46 | export const MAGIC_SKIPPABLE_MASK: number; 47 | 48 | /** 49 | * Parameters for Zstandard compression. 50 | * 51 | * @category Advanced API 52 | */ 53 | export enum CParameter { 54 | compressionLevel, 55 | windowLog, 56 | hashLog, 57 | chainLog, 58 | searchLog, 59 | minMatch, 60 | targetLength, 61 | strategy, 62 | targetCBlockSize, 63 | enableLongDistanceMatching, 64 | ldmHashLog, 65 | ldmMinMatch, 66 | ldmBucketSizeLog, 67 | ldmHashRateLog, 68 | contentSizeFlag, 69 | checksumFlag, 70 | dictIDFlag, 71 | nbWorkers, 72 | jobSize, 73 | overlapLog, 74 | } 75 | 76 | /** 77 | * Parameters for Zstandard decompression. 78 | * 79 | * Corresponds to `ZSTD_dParameter`. 80 | * 81 | * @category Advanced API 82 | */ 83 | export enum DParameter { 84 | windowLogMax, 85 | } 86 | 87 | /** 88 | * Identifies whether to flush or close the current frame. 89 | * 90 | * Corresponds to `ZSTD_EndDirective`. 91 | * 92 | * @category Streaming 93 | */ 94 | export enum EndDirective { 95 | /** Don't flush or end the frame */ 96 | continue, 97 | /** Flush all data written so far */ 98 | flush, 99 | /** Flush all data written so far and end the frame */ 100 | end, 101 | } 102 | 103 | /** 104 | * Identifies what parts of a (de)compression context to reset. 105 | * 106 | * Corresponds to `ZSTD_ResetDirective`. 107 | * 108 | * @category Advanced API 109 | */ 110 | export enum ResetDirective { 111 | /** Abort the current frame, but keep dictionary/parameters */ 112 | sessionOnly, 113 | /** Reset the dictionary/parameters (only works if not in a frame) */ 114 | parameters, 115 | /** Reset both the frame and dictionary/parameters */ 116 | sessionAndParameters, 117 | } 118 | 119 | /** 120 | * Compression strategies. 121 | * 122 | * Used as values for {@link CParameter.strategy}. 123 | * 124 | * Corresponds to `ZSTD_strategy`. 125 | * 126 | * @category Advanced API 127 | */ 128 | export enum Strategy { 129 | fast, 130 | dfast, 131 | greedy, 132 | lazy, 133 | lazy2, 134 | btlazy2, 135 | btopt, 136 | btultra, 137 | } 138 | 139 | /** 140 | * Composite return value for streaming (de)compression functions. 141 | * 142 | * These functions effectively return three values: 143 | * - `returnValue`: the function's normal return value 144 | * - `dstProduced`: the number of bytes written to the output buffer 145 | * - `srcProduced`: the number of bytes read from the input buffer 146 | * 147 | * @remarks 148 | * The latter two of these are out parameters in C, and the most efficient way 149 | * to map that to a JavaScript API is to return a composite value instead. We 150 | * use a tuple for performance reasons: Node-API (unlike V8) doesn't have an API 151 | * to efficiently construct objects with a fixed set of properties. 152 | * 153 | * @category Streaming 154 | */ 155 | type StreamResult = [ 156 | returnValue: number, 157 | dstProduced: number, 158 | srcConsumed: number, 159 | ]; 160 | 161 | /** 162 | * Compression context. 163 | * 164 | * Wraps `ZSTD_CCtx` (which is also `ZSTD_CStream`). The finalizer automatically 165 | * calls `ZSTD_freeCCtx` when this object is garbage collected. 166 | */ 167 | export class CCtx { 168 | /** 169 | * Creates a new compression context. 170 | * 171 | * Wraps `ZSTD_createCCtx`. 172 | */ 173 | constructor(); 174 | 175 | /** 176 | * Compresses `srcBuf` into `dstBuf` at compression level `level`. 177 | * 178 | * `dstBuf` must be large enough to fit the entire result. See 179 | * {@link compressBound} for a way to compute an upper bound on that size. 180 | * 181 | * This ignores any parameters set by {@link setParameter} and compresses 182 | * at the level specified by `level`. See {@link compress2} for a similar 183 | * function that respects those parameters. 184 | * 185 | * Wraps `ZSTD_compressCCtx`. 186 | * 187 | * @param dstBuf - Output buffer for compressed bytes 188 | * @param srcBuf - Data to compress 189 | * @param level - Compression level 190 | * @returns Number of compressed bytes written to `dstBuf` 191 | */ 192 | compress(dstBuf: Uint8Array, srcBuf: Uint8Array, level: number): number; 193 | 194 | /** 195 | * Compresses `srcBuf` into `dstBuf`, using `dictBuf` as a dictionary. 196 | * 197 | * Works like {@link CCtx.compress | compress}, except it uses a dictionary. 198 | * 199 | * Wraps `ZSTD_compress_usingDict`. 200 | * 201 | * @remarks 202 | * Loading the dictionary from a buffer is expensive. If the dictionary will 203 | * be used more than once, it's better to load it into a {@link CDict} once 204 | * and use {@link compressUsingCDict} instead. 205 | * 206 | * @param dstBuf - Output buffer for compressed bytes 207 | * @param srcBuf - Data to compress 208 | * @param dictBuf - Compression dictionary 209 | * @param level - Compression level 210 | * @returns Number of compressed bytes written to `dstBuf` 211 | */ 212 | compressUsingDict( 213 | dstBuf: Uint8Array, 214 | srcBuf: Uint8Array, 215 | dictBuf: Uint8Array, 216 | level: number, 217 | ): number; 218 | 219 | /** 220 | * Compresses `srcBuf` into `dstBuf` using the prepared dictionary `dict`. 221 | * 222 | * Works like {@link CCtx.compress | compress}, except it uses a dictionary. 223 | * The compression level is selected at dictionary load time. 224 | * 225 | * Wraps `ZSTD_compress_usingCDict`. 226 | * 227 | * @param dstBuf - Output buffer for compressed bytes 228 | * @param srcBuf - Data to compress 229 | * @param dict - Prepared dictionary 230 | * @returns Number of compressed bytes written to `dstBuf` 231 | */ 232 | compressUsingCDict( 233 | dstBuf: Uint8Array, 234 | srcBuf: Uint8Array, 235 | dict: CDict, 236 | ): number; 237 | 238 | /** 239 | * Set a compression parameter. 240 | * 241 | * Note that these parameters are only respected by the {@link compress2} 242 | * and {@link compressStream2} methods. 243 | * 244 | * Wraps `ZSTD_CCtx_setParameter`. 245 | * 246 | * @param param - Parameter to set 247 | * @param value - New parameter value 248 | */ 249 | setParameter(param: CParameter, value: number): void; 250 | 251 | /** 252 | * Set the uncompressed length of the next frame. 253 | * 254 | * Allows populating the header with the uncompressed size when using the 255 | * streaming compression interface. Compression will throw an error if this 256 | * commitment is not respected. 257 | * 258 | * Wraps `ZSTD_CCtx_setPledgedSrcSize`. 259 | */ 260 | setPledgedSrcSize(size: number): void; 261 | 262 | /** 263 | * Resets this compression context. 264 | * 265 | * The `reset` parameter controls what exactly is reset. 266 | * 267 | * Wraps `ZSTD_CCtx_reset`. 268 | */ 269 | reset(reset: ResetDirective): void; 270 | 271 | /** 272 | * Compresses `srcBuf` into `dstBuf`. 273 | * 274 | * Works like {@link CCtx.compress | compress}, except it respects the 275 | * configuration set on this object with other methods. 276 | * 277 | * Wraps `ZSTD_compress2`. 278 | * 279 | * @param dstBuf - Output buffer for compressed bytes 280 | * @param srcBuf - Data to compress 281 | * @returns Number of compressed bytes written to `dstBuf` 282 | */ 283 | compress2(dstBuf: Uint8Array, srcBuf: Uint8Array): number; 284 | 285 | /** 286 | * Compresses `srcBuf` into `dstBuf` with a streaming interface. 287 | * 288 | * The `endOp` parameter indicates whether to flush data or end the frame. 289 | * 290 | * May consume all or part of `srcBuf`, and may partially write `dstBuf`. 291 | * Returns a tuple with a bound on how many bytes are left to flush, how many 292 | * bytes were written, and how many bytes were consumed. 293 | * 294 | * Wraps `ZSTD_compressStream2`. 295 | * 296 | * @remarks 297 | * This function requires some care to use correctly, consult the {@link 298 | * https://facebook.github.io/zstd/zstd_manual.html | Zstandard manual} for 299 | * full usage information. 300 | * 301 | * @param dstBuf - Output buffer for compressed bytes 302 | * @param srcBuf - Data to compress 303 | * @param endOp - Whether to flush or end the frame 304 | * @returns Compression progress information 305 | */ 306 | compressStream2( 307 | dstBuf: Uint8Array, 308 | srcBuf: Uint8Array, 309 | endOp: EndDirective, 310 | ): StreamResult; 311 | 312 | /** 313 | * Load a compression dictionary from `dictBuf`. 314 | * 315 | * Note that this dictionary will only be used by the {@link compress2} and 316 | * {@link compressStream2} methods. 317 | * 318 | * Wraps `ZSTD_CCtx_loadDictionary`. 319 | */ 320 | loadDictionary(dictBuf: Uint8Array): void; 321 | 322 | private __brand: 'CCtx'; 323 | } 324 | 325 | /** 326 | * Prepared dictionary for compression. 327 | * 328 | * Wraps `ZSTD_CDict`. The finalizer automatically calls `ZSTD_freeCDict` when 329 | * this object is garbage collected. 330 | * 331 | * @category Dictionary 332 | */ 333 | export class CDict { 334 | /** 335 | * Load a dictionary for compression from the bytes in `dictBuf`. 336 | * 337 | * The compression level must be given in `level` and will override any 338 | * compression level set when this dictionary is used. 339 | * 340 | * Wraps `ZSTD_createCDict`. 341 | */ 342 | constructor(dictBuf: Uint8Array, level: number); 343 | 344 | /** 345 | * Returns the ID for this dictionary. 346 | * 347 | * Wraps `ZSTD_getDictID_fromCDict`. 348 | * 349 | * @returns The ID, or 0 if this is a non-standard/content-only dictionary 350 | */ 351 | getDictID(): number; 352 | 353 | private __brand: 'CDict'; 354 | } 355 | 356 | /** 357 | * Decompression context. 358 | * 359 | * Wraps `ZSTD_DCtx` (which is also `ZSTD_DStream`). The finalizer automatically 360 | * calls `ZSTD_freeDCtx` when this object is garbage collected. 361 | */ 362 | export class DCtx { 363 | /** 364 | * Creates a new decompression context. 365 | * 366 | * Wraps `ZSTD_createDCtx`. 367 | */ 368 | constructor(); 369 | 370 | /** 371 | * Decompresses `srcBuf` into `dstBuf`. 372 | * 373 | * `dstBuf` must be large enough to fit the entire result. `srcBuf` must end 374 | * on a frame boundary (no partial frames or other trailing data). 375 | * 376 | * Wraps `ZSTD_decompressDCtx`. 377 | * 378 | * @remarks 379 | * If the frame has the uncompressed size in the header, you can use 380 | * {@link getFrameContentSize} to determine how big the buffer needs to be. 381 | * If it's too large, or unknown, use {@link decompressStream} instead. 382 | * 383 | * @param dstBuf - Output buffer for decompressed bytes 384 | * @param srcBuf - Data to decompress 385 | * @returns Number of decompressed bytes written to `dstBuf` 386 | */ 387 | decompress(dstBuf: Uint8Array, srcBuf: Uint8Array): number; 388 | 389 | /** 390 | * Decompresses `srcBuf` into `dstBuf` with a streaming interface. 391 | * 392 | * May consume all or part of `srcBuf`, and may partially write `dstBuf`. 393 | * Returns a tuple with a bound on how many bytes are left to flush, how many 394 | * bytes were written, and how many bytes were consumed. 395 | * 396 | * Wraps `ZSTD_decompressStream`. 397 | * 398 | * @remarks 399 | * This function requires some care to use correctly, consult the {@link 400 | * https://facebook.github.io/zstd/zstd_manual.html | Zstandard manual} for 401 | * full usage information. 402 | * 403 | * @param dstBuf - Output buffer for decompressed bytes 404 | * @param srcBuf - Data to decompress 405 | * @returns Decompression progress information 406 | */ 407 | decompressStream(dstBuf: Uint8Array, srcBuf: Uint8Array): StreamResult; 408 | 409 | /** 410 | * Decompresses `srcBuf` into `dstBuf`, using `dictBuf` as a dictionary. 411 | * 412 | * Works like {@link DCtx.decompress | decompress}, except it uses the 413 | * provided dictionary instead of any set on this context. 414 | * 415 | * Wraps `ZSTD_decompress_usingDict`. 416 | * 417 | * @remarks 418 | * Loading the dictionary from a buffer is expensive. If the dictionary will 419 | * be used more than once, it's better to load it into a {@link DDict} once 420 | * and use {@link decompressUsingDDict} instead. 421 | * 422 | * @param dstBuf - Output buffer for decompressed bytes 423 | * @param srcBuf - Data to decompress 424 | * @param dictBuf - Compression dictionary 425 | * @returns Number of compressed bytes written to `dstBuf` 426 | */ 427 | decompressUsingDict( 428 | dstBuf: Uint8Array, 429 | srcBuf: Uint8Array, 430 | dictBuf: Uint8Array, 431 | ): number; 432 | 433 | /** 434 | * Decompresses `srcBuf` into `dstBuf` using the prepared dictionary `dict`. 435 | * 436 | * Works like {@link DCtx.decompress | decompress}, except it uses the 437 | * provided dictionary instead of any set on this context. 438 | * 439 | * Wraps `ZSTD_decompress_usingDDict`. 440 | * 441 | * @param dstBuf - Output buffer for compressed bytes 442 | * @param srcBuf - Data to compress 443 | * @param dict - Prepared dictionary 444 | * @returns Number of compressed bytes written to `dstBuf` 445 | */ 446 | decompressUsingDDict( 447 | dstBuf: Uint8Array, 448 | srcBuf: Uint8Array, 449 | dict: DDict, 450 | ): number; 451 | 452 | /** 453 | * Set a decompression parameter. 454 | * 455 | * Wraps `ZSTD_DCtx_setParameter`. 456 | * 457 | * @param param - Parameter to set 458 | * @param value - New parameter value 459 | */ 460 | setParameter(param: DParameter, value: number): void; 461 | 462 | /** 463 | * Resets this decompression context. 464 | * 465 | * The `reset` parameter controls what exactly is reset. 466 | * 467 | * Wraps `ZSTD_DCtx_reset`. 468 | */ 469 | reset(reset: ResetDirective): void; 470 | 471 | /** 472 | * Load a compression dictionary from `dictBuf`. 473 | * 474 | * This dictionary will be used by {@link DCtx.decompress | decompress} and 475 | * {@link decompressStream}. 476 | * 477 | * Wraps `ZSTD_DCtx_loadDictionary`. 478 | */ 479 | loadDictionary(dictBuf: Uint8Array): void; 480 | 481 | private __brand: 'DCtx'; 482 | } 483 | 484 | /** 485 | * Prepared dictionary for decompression. 486 | * 487 | * Wraps `ZSTD_DDict`. The finalizer automatically calls `ZSTD_freeDDict` when 488 | * this object is garbage collected. 489 | * 490 | * @category Dictionary 491 | */ 492 | export class DDict { 493 | /** 494 | * Load a dictionary for decompression from the bytes in `dictBuf`. 495 | * 496 | * Wraps `ZSTD_createDDict`. 497 | */ 498 | constructor(dictBuf: Uint8Array); 499 | 500 | /** 501 | * Returns the ID for this dictionary. 502 | * 503 | * Wraps `ZSTD_getDictID_fromDDict`. 504 | * 505 | * @returns The ID, or 0 if this is a non-standard/content-only dictionary 506 | */ 507 | getDictID(): number; 508 | 509 | private __brand: 'DDict'; 510 | } 511 | 512 | /** 513 | * Inclusive lower and upper bounds for a parameter. 514 | * 515 | * Corresponds to `ZSTD_bounds`. 516 | * 517 | * @category Advanced API 518 | */ 519 | export interface Bounds { 520 | /** Minimum allowed value */ 521 | lowerBound: number; 522 | /** Maximum allowed value */ 523 | upperBound: number; 524 | } 525 | 526 | /** 527 | * Returns the Zstandard library version as a number. 528 | * 529 | * Wraps `ZSTD_versionNumber`. 530 | */ 531 | export function versionNumber(): number; 532 | 533 | /** 534 | * Returns the Zstandard library version as a string. 535 | * 536 | * Wraps `ZSTD_versionString`. 537 | */ 538 | export function versionString(): string; 539 | 540 | /** 541 | * Compresses `srcBuf` into `dstBuf` at compression level `level`. 542 | * 543 | * `dstBuf` must be large enough to fit the entire result. See 544 | * {@link compressBound} for a way to compute an upper bound on that size. 545 | * 546 | * Wraps `ZSTD_compress`. 547 | * 548 | * @remarks 549 | * This function is here for completeness, creating a {@link CCtx} and reusing 550 | * it will give better performance than calling this repeatedly. The high-level 551 | * {@link index.compress | compress} function will take care of this for you. 552 | * 553 | * @param dstBuf - Output buffer for compressed bytes 554 | * @param srcBuf - Data to compress 555 | * @param level - Compression level 556 | * @returns Number of compressed bytes written to `dstBuf` 557 | * @category Simple API 558 | */ 559 | export function compress( 560 | dstBuf: Uint8Array, 561 | srcBuf: Uint8Array, 562 | level: number, 563 | ): number; 564 | 565 | /** 566 | * Decompresses `srcBuf` into `dstBuf`. 567 | * 568 | * `dstBuf` must be large enough to fit the entire result. `srcBuf` must end on 569 | * a frame boundary (no partial frames or other trailing data). 570 | * 571 | * Wraps `ZSTD_decompress`. 572 | * 573 | * @remarks 574 | * This function is here for completeness, creating a {@link DCtx} and reusing 575 | * it will give better performance than calling this repeatedly. The high-level 576 | * {@link index.decompress | decompress} function will take care of this for 577 | * you. 578 | * 579 | * @param dstBuf - Output buffer for decompressed bytes 580 | * @param srcBuf - Data to decompress 581 | * @returns Number of decompressed bytes written to `dstBuf` 582 | * @category Simple API 583 | */ 584 | export function decompress(dstBuf: Uint8Array, srcBuf: Uint8Array): number; 585 | 586 | /** 587 | * Returns the number of decompressed bytes in the provided frame. 588 | * 589 | * Wraps `ZSTD_getFrameContentSize`. 590 | * 591 | * @param frameBuf - Buffer with Zstandard frame (or frame header) 592 | * @returns Number of decompressed bytes, or `null` if unknown 593 | * @category Simple API 594 | */ 595 | export function getFrameContentSize(frameBuf: Uint8Array): number | null; 596 | 597 | /** 598 | * Returns the size of the first compressed frame in `frameBuf`. 599 | * 600 | * @param frameBuf - Buffer containing at least one complete Zstandard frame 601 | * @returns Size of the first frame in `frameBuf` 602 | * @category Simple API 603 | */ 604 | export function findFrameCompressedSize(frameBuf: Uint8Array): number; 605 | 606 | /** 607 | * Returns worst-case maximum compressed size for an input of `srcSize` bytes. 608 | * 609 | * @category Simple API 610 | */ 611 | export function compressBound(srcSize: number): number; 612 | 613 | /** 614 | * Returns the minimum valid compression level. 615 | * 616 | * @category Simple API 617 | */ 618 | export function minCLevel(): number; 619 | 620 | /** 621 | * Returns the maximum valid compression level. 622 | * 623 | * @category Simple API 624 | */ 625 | export function maxCLevel(): number; 626 | 627 | /** 628 | * Returns the default compression level. 629 | * 630 | * @category Simple API 631 | */ 632 | export function defaultCLevel(): number; 633 | 634 | /** 635 | * Get upper and lower bounds for a compression parameter. 636 | * 637 | * Wraps `ZSTD_cParam_getBounds`. 638 | * 639 | * @category Advanced API 640 | */ 641 | export function cParamGetBounds(param: CParameter): Bounds; 642 | 643 | /** 644 | * Get upper and lower bounds for a decompression parameter. 645 | * 646 | * Wraps `ZSTD_dParam_getBounds`. 647 | * 648 | * @category Advanced API 649 | */ 650 | export function dParamGetBounds(param: DParameter): Bounds; 651 | 652 | /** 653 | * Returns the recommended size of a streaming compression input buffer. 654 | * 655 | * Wraps `ZSTD_CStreamInSize`. 656 | * 657 | * @category Streaming 658 | */ 659 | export function cStreamInSize(): number; 660 | 661 | /** 662 | * Returns the recommended size of a streaming compression output buffer. 663 | * 664 | * Wraps `ZSTD_CStreamOutSize`. 665 | * 666 | * @category Streaming 667 | */ 668 | export function cStreamOutSize(): number; 669 | 670 | /** 671 | * Returns the recommended size of a streaming decompression input buffer. 672 | * 673 | * Wraps `ZSTD_DStreamInSize`. 674 | * 675 | * @category Streaming 676 | */ 677 | export function dStreamInSize(): number; 678 | 679 | /** 680 | * Returns the recommended size of a streaming decompression input buffer. 681 | * 682 | * Wraps `ZSTD_DStreamOutSize`. 683 | * 684 | * @category Streaming 685 | */ 686 | export function dStreamOutSize(): number; 687 | 688 | /** 689 | * Returns the dictionary ID stored in the provided dictionary. 690 | * 691 | * Wraps `ZSTD_getDictID_fromDict`. 692 | * 693 | * @param dictBuf - Buffer containing the dictionary 694 | * @returns The dictionary ID, or 0 if the buffer does not contain a dictionary 695 | * @category Dictionary 696 | */ 697 | export function getDictIDFromDict(dictBuf: Uint8Array): number; 698 | 699 | /** 700 | * Returns the dictionary ID recorded in a Zstandard frame. 701 | * 702 | * @param frameBuf - Buffer containing a Zstandard frame 703 | * @returns The dictionary ID, or 0 if the frame header doesn't include one (or 704 | * if the buffer doesn't contain a valid frame header) 705 | * @category Dictionary 706 | */ 707 | export function getDictIDFromFrame(frameBuf: Uint8Array): number; 708 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'variables': { 3 | 'copy_licenses': 0, 4 | 'enable_gcov': 0, 5 | 'napi_build_version': 3, 6 | 'conditions': [ 7 | ['OS!="win"', { 8 | 'enable_gcov': ' parseInt(v ?? '0', 10)); 42 | const maxVersion = MAX_VERSIONS[lib]; 43 | if (!maxVersion) { 44 | console.error(`Unknown versioned library: ${sym}`); 45 | ok = false; 46 | continue; 47 | } 48 | if (cmpVersion(version, maxVersion) > 0) { 49 | console.error(`Version too new: ${sym}`); 50 | ok = false; 51 | } 52 | } 53 | if (count === 0) { 54 | console.error("Didn't find any versioned symbols"); 55 | ok = false; 56 | } 57 | if (!ok) { 58 | process.exit(1); 59 | } 60 | } 61 | 62 | main(); 63 | -------------------------------------------------------------------------------- /deps/.npmignore: -------------------------------------------------------------------------------- 1 | /** 2 | !/zstd.gyp 3 | !/zstd/COPYING 4 | !/zstd/LICENSE 5 | !/zstd/lib/*.h 6 | !/zstd/lib/common/* 7 | !/zstd/lib/compress/* 8 | !/zstd/lib/decompress/* 9 | -------------------------------------------------------------------------------- /deps/zstd.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'libzstd', 5 | 'includes': ['../build_flags.gypi'], 6 | 'type': 'static_library', 7 | 'sources': [ 8 | 'zstd/lib/common/debug.c', 9 | 'zstd/lib/common/entropy_common.c', 10 | 'zstd/lib/common/error_private.c', 11 | 'zstd/lib/common/fse_decompress.c', 12 | 'zstd/lib/common/pool.c', 13 | 'zstd/lib/common/threading.c', 14 | 'zstd/lib/common/xxhash.c', 15 | 'zstd/lib/common/zstd_common.c', 16 | 'zstd/lib/compress/fse_compress.c', 17 | 'zstd/lib/compress/hist.c', 18 | 'zstd/lib/compress/huf_compress.c', 19 | 'zstd/lib/compress/zstd_compress.c', 20 | 'zstd/lib/compress/zstd_compress_literals.c', 21 | 'zstd/lib/compress/zstd_compress_sequences.c', 22 | 'zstd/lib/compress/zstd_compress_superblock.c', 23 | 'zstd/lib/compress/zstd_double_fast.c', 24 | 'zstd/lib/compress/zstd_fast.c', 25 | 'zstd/lib/compress/zstd_lazy.c', 26 | 'zstd/lib/compress/zstd_ldm.c', 27 | 'zstd/lib/compress/zstd_opt.c', 28 | 'zstd/lib/compress/zstd_preSplit.c', 29 | 'zstd/lib/compress/zstdmt_compress.c', 30 | 'zstd/lib/decompress/huf_decompress.c', 31 | 'zstd/lib/decompress/huf_decompress_amd64.S', 32 | 'zstd/lib/decompress/zstd_ddict.c', 33 | 'zstd/lib/decompress/zstd_decompress_block.c', 34 | 'zstd/lib/decompress/zstd_decompress.c', 35 | ], 36 | 'cflags+': ['-fvisibility=hidden'], 37 | 'defines': [ 38 | 'XXH_NAMESPACE=ZSTD_', 39 | 'ZSTDERRORLIB_VISIBLE=', 40 | 'ZSTDLIB_VISIBLE=', 41 | 'ZSTD_MULTITHREAD', 42 | 'ZSTD_NO_TRACE', 43 | ], 44 | 'direct_dependent_settings': { 45 | 'include_dirs': ['zstd/lib'], 46 | }, 47 | 'conditions': [ 48 | ['OS=="mac"', { 49 | 'xcode_settings': { 50 | 'GCC_SYMBOLS_PRIVATE_EXTERN': 'YES', 51 | 'MACOSX_DEPLOYMENT_TARGET': '10.7', 52 | }, 53 | }], 54 | ['OS=="win"', { 55 | 'sources!': [ 56 | # MSVC doesn't support GAS assembly syntax 57 | 'zstd/lib/decompress/huf_decompress_amd64.S', 58 | ], 59 | }], 60 | ], 61 | }, 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const eslint = require('@eslint/js'); 2 | const eslintConfigPrettier = require('eslint-config-prettier'); 3 | const jest = require('eslint-plugin-jest'); 4 | const tsdoc = require('eslint-plugin-tsdoc'); 5 | const globals = require('globals'); 6 | const tseslint = require('typescript-eslint'); 7 | module.exports = tseslint.config( 8 | eslint.configs.recommended, 9 | { 10 | extends: [ 11 | tseslint.configs.recommendedTypeChecked, 12 | tseslint.configs.stylisticTypeChecked, 13 | ], 14 | files: ['**/*.ts'], 15 | }, 16 | eslintConfigPrettier, 17 | { 18 | ignores: ['coverage/', 'docs/', 'dist/', 'node_modules/'], 19 | }, 20 | { 21 | languageOptions: { 22 | ecmaVersion: 2022, 23 | sourceType: 'commonjs', 24 | globals: globals.node, 25 | parserOptions: { 26 | project: true, 27 | tsconfigRootDir: __dirname, 28 | }, 29 | }, 30 | }, 31 | { 32 | extends: [jest.configs['flat/style'], jest.configs['flat/recommended']], 33 | files: ['tests/**/*.{js,ts}'], 34 | rules: { 35 | '@typescript-eslint/ban-ts-comment': [ 36 | 'error', 37 | { 'ts-expect-error': 'allow-with-description' }, 38 | ], 39 | '@typescript-eslint/dot-notation': 'off', 40 | '@typescript-eslint/no-unsafe-assignment': 'off', 41 | '@typescript-eslint/no-unsafe-return': 'off', 42 | '@typescript-eslint/unbound-method': 'off', 43 | 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }], 44 | }, 45 | }, 46 | { 47 | files: ['**/*.ts'], 48 | plugins: { tsdoc }, 49 | rules: { 50 | 'tsdoc/syntax': 'error', 51 | }, 52 | }, 53 | ); 54 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { createDefaultPreset } = require('ts-jest'); 2 | module.exports = { 3 | ...createDefaultPreset(), 4 | collectCoverageFrom: ['binding.js', 'lib/**/*.{js,ts}'], 5 | // TODO: Remove after upgrading to Jest 30 6 | prettierPath: null, 7 | testMatch: ['**/tests/**/*.ts'], 8 | }; 9 | -------------------------------------------------------------------------------- /lib/compress.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert'; 2 | import { Transform, TransformCallback } from 'stream'; 3 | 4 | import * as binding from '../binding'; 5 | import { mapBoolean, mapEnum, mapNumber, mapParameters } from './util'; 6 | 7 | /** 8 | * Zstandard compression parameters. 9 | * 10 | * Most applications will only need the {@link compressionLevel} parameter. See 11 | * the {@link https://facebook.github.io/zstd/zstd_manual.html | Zstandard manual} 12 | * for a full description. 13 | */ 14 | export interface CompressParameters { 15 | /** 16 | * Compression level, where higher numbers compress better but are slower. 17 | * 18 | * Typical values range from 1 to 9, with a default of 3, but values up to 22 19 | * are allowed, as are negative values (see {@link binding.minCLevel}). Zero 20 | * is interpreted as "use the default". 21 | * 22 | * @category Basic parameters 23 | */ 24 | compressionLevel?: number | undefined; 25 | 26 | // Advanced compression options 27 | /** @category Advanced compression options */ 28 | windowLog?: number | undefined; 29 | /** @category Advanced compression options */ 30 | hashLog?: number | undefined; 31 | /** @category Advanced compression options */ 32 | chainLog?: number | undefined; 33 | /** @category Advanced compression options */ 34 | searchLog?: number | undefined; 35 | /** @category Advanced compression options */ 36 | minMatch?: number | undefined; 37 | /** @category Advanced compression options */ 38 | targetLength?: number | undefined; 39 | /** @category Advanced compression options */ 40 | strategy?: keyof typeof binding.Strategy | undefined; 41 | /** @category Advanced compression options */ 42 | targetCBlockSize?: number | undefined; 43 | 44 | // Long-distance matching options 45 | /** @category Long-distance matching */ 46 | enableLongDistanceMatching?: boolean | undefined; 47 | /** @category Long-distance matching */ 48 | ldmHashLog?: number | undefined; 49 | /** @category Long-distance matching */ 50 | ldmMinMatch?: number | undefined; 51 | /** @category Long-distance matching */ 52 | ldmBucketSizeLog?: number | undefined; 53 | /** @category Long-distance matching */ 54 | ldmHashRateLog?: number | undefined; 55 | 56 | // Frame parameters 57 | /** @category Frame parameters */ 58 | contentSizeFlag?: boolean | undefined; 59 | /** @category Frame parameters */ 60 | checksumFlag?: boolean | undefined; 61 | /** @category Frame parameters */ 62 | dictIDFlag?: boolean | undefined; 63 | 64 | // Multi-threading parameters 65 | /** @category Multi-threading parameters */ 66 | nbWorkers?: number | undefined; 67 | /** @category Multi-threading parameters */ 68 | jobSize?: number | undefined; 69 | /** @category Multi-threading parameters */ 70 | overlapLog?: number | undefined; 71 | } 72 | 73 | const PARAM_MAPPERS = { 74 | compressionLevel: mapNumber, 75 | 76 | // Advanced compression options 77 | windowLog: mapNumber, 78 | hashLog: mapNumber, 79 | chainLog: mapNumber, 80 | searchLog: mapNumber, 81 | minMatch: mapNumber, 82 | targetLength: mapNumber, 83 | strategy: mapEnum(binding.Strategy), 84 | targetCBlockSize: mapNumber, 85 | 86 | // Long-distance matching options 87 | enableLongDistanceMatching: mapBoolean, 88 | ldmHashLog: mapNumber, 89 | ldmMinMatch: mapNumber, 90 | ldmBucketSizeLog: mapNumber, 91 | ldmHashRateLog: mapNumber, 92 | 93 | // Frame parameters 94 | contentSizeFlag: mapBoolean, 95 | checksumFlag: mapBoolean, 96 | dictIDFlag: mapBoolean, 97 | 98 | // Multi-threading parameters 99 | nbWorkers: mapNumber, 100 | jobSize: mapNumber, 101 | overlapLog: mapNumber, 102 | }; 103 | 104 | function updateCCtxParameters( 105 | cctx: binding.CCtx, 106 | parameters: CompressParameters, 107 | ): void { 108 | const mapped = mapParameters(binding.CParameter, PARAM_MAPPERS, parameters); 109 | for (const [param, value] of mapped) { 110 | cctx.setParameter(param, value); 111 | } 112 | } 113 | 114 | /** 115 | * High-level interface for customized single-pass Zstandard compression. 116 | * 117 | * @example Basic usage 118 | * ``` 119 | * const cmp = new Compressor(); 120 | * const result = cmp.compress(Buffer.from('your data here')); 121 | * ``` 122 | * 123 | * @example Advanced usage 124 | * ``` 125 | * const cmp = new Compressor(); 126 | * cmp.setParameters({compressionLevel: 9}); 127 | * cmp.loadDictionary(fs.readFileSync('path/to/dictionary.dct')); 128 | * const result = cmp.compress(Buffer.from('your data here')); 129 | * ``` 130 | */ 131 | export class Compressor { 132 | private cctx = new binding.CCtx(); 133 | private scratchBuf: Buffer | null = null; 134 | private scratchLen = -1; 135 | 136 | /** 137 | * Compress the data in `buffer` with the configured dictionary/parameters. 138 | * 139 | * @param buffer - Data to compress 140 | * @returns A new Buffer containing the compressed data 141 | */ 142 | compress(buffer: Uint8Array): Buffer { 143 | let dest: Buffer; 144 | if (this.scratchBuf && buffer.length <= this.scratchLen) { 145 | dest = this.scratchBuf; 146 | } else { 147 | dest = Buffer.allocUnsafe(binding.compressBound(buffer.length)); 148 | } 149 | 150 | const length = this.cctx.compress2(dest, buffer); 151 | let result; 152 | if (length < 0.75 * dest.length) { 153 | // Destination buffer is too wasteful, trim by copying 154 | result = Buffer.from(dest.subarray(0, length)); 155 | 156 | // Save the old buffer for scratch if it's small enough 157 | if (dest.length <= 128 * 1024 && buffer.length > this.scratchLen) { 158 | this.scratchBuf = dest; 159 | this.scratchLen = buffer.length; 160 | } 161 | } else { 162 | // Destination buffer is about the right size, return it directly 163 | result = dest.subarray(0, length); 164 | 165 | // Make sure we don't re-use the scratch buffer if we're returning it 166 | if (Object.is(dest, this.scratchBuf)) { 167 | this.scratchBuf = null; 168 | this.scratchLen = -1; 169 | } 170 | } 171 | return result; 172 | } 173 | 174 | /** 175 | * Load a compression dictionary from the provided buffer. 176 | * 177 | * The loaded dictionary will be used for all future {@link compress} calls 178 | * until removed or replaced. Passing an empty buffer to this function will 179 | * remove a previously loaded dictionary. 180 | * 181 | * Set any parameters you want to set before loading a dictionary, since 182 | * parameters can't be changed while a dictionary is loaded. 183 | */ 184 | loadDictionary(data: Uint8Array): void { 185 | // TODO: Compression parameters get locked in on next compress operation, 186 | // and are cleared by setParameters. There should be some checks to ensure 187 | // users have a safe usage pattern. 188 | this.cctx.loadDictionary(data); 189 | } 190 | 191 | /** 192 | * Reset the compressor state to only the provided parameters. 193 | * 194 | * Any loaded dictionary will be cleared, and any parameters not specified 195 | * will be reset to their default values. 196 | */ 197 | setParameters(parameters: CompressParameters): void { 198 | this.cctx.reset(binding.ResetDirective.parameters); 199 | this.updateParameters(parameters); 200 | } 201 | 202 | /** 203 | * Modify compression parameters. 204 | * 205 | * Parameters not specified will be left at their current values. Changing 206 | * parameters is not possible while a dictionary is loaded. 207 | */ 208 | updateParameters(parameters: CompressParameters): void { 209 | updateCCtxParameters(this.cctx, parameters); 210 | } 211 | } 212 | 213 | const BUF_SIZE = binding.cStreamOutSize(); 214 | 215 | const dummyFlushBuffer = Buffer.alloc(0); 216 | const dummyEndBuffer = Buffer.alloc(0); 217 | 218 | /** 219 | * High-level interface for streaming Zstandard compression. 220 | * 221 | * Implements the standard Node stream transformer interface, so can be used 222 | * with `.pipe` or any other streaming interface. 223 | * 224 | * @example Basic usage 225 | * ``` 226 | * import { pipeline } from 'stream/promises'; 227 | * const cmp = new CompressStream(); 228 | * await pipeline( 229 | * fs.createReadStream('data.txt'), 230 | * new CompressStream(), 231 | * fs.createWriteStream('data.txt.zst'), 232 | * ); 233 | * ``` 234 | */ 235 | export class CompressStream extends Transform { 236 | private cctx = new binding.CCtx(); 237 | private buffer = Buffer.allocUnsafe(BUF_SIZE); 238 | 239 | // TODO: Allow user to specify a dictionary 240 | /** 241 | * Create a new streaming compressor with the specified parameters. 242 | * 243 | * @param parameters - Compression parameters 244 | */ 245 | constructor(parameters: CompressParameters = {}) { 246 | // TODO: autoDestroy doesn't really work on Transform, we should consider 247 | // calling .destroy ourselves when necessary. 248 | super({ autoDestroy: true }); 249 | updateCCtxParameters(this.cctx, parameters); 250 | } 251 | 252 | // TODO: Provide API to allow changing parameters mid-frame in MT mode 253 | // TODO: Provide API to allow changing parameters between frames 254 | 255 | /** 256 | * End the current Zstandard frame without ending the stream. 257 | * 258 | * Frames are compressed independently, so this can be used to create a 259 | * "seekable" archive, or to provide more resilience to data corruption by 260 | * isolating parts of the file from each other. 261 | * 262 | * The optional `callback` is invoked with the same semantics as it is for a 263 | * a stream write. 264 | */ 265 | endFrame(callback?: (error?: Error | null) => void): void { 266 | this.write(dummyEndBuffer, undefined, callback); 267 | } 268 | 269 | /** 270 | * Flush internal compression buffers to the stream. 271 | * 272 | * Ensures that a receiver can decompress all bytes written so far without 273 | * as much negative impact to compression as {@link endFrame}. 274 | * 275 | * The optional `callback` is invoked with the same semantics as it is for a 276 | * a stream write. 277 | */ 278 | flush(callback?: (error?: Error | null) => void): void { 279 | this.write(dummyFlushBuffer, undefined, callback); 280 | } 281 | 282 | private doCompress(chunk: Buffer, endType: binding.EndDirective): void { 283 | const flushing = endType !== binding.EndDirective.continue; 284 | for (;;) { 285 | const [ret, produced, consumed] = this.cctx.compressStream2( 286 | this.buffer, 287 | chunk, 288 | endType, 289 | ); 290 | if (produced > 0) { 291 | this.push(this.buffer.subarray(0, produced)); 292 | this.buffer = Buffer.allocUnsafe(Math.max(BUF_SIZE, ret)); 293 | } 294 | chunk = chunk.subarray(consumed); 295 | if (chunk.length == 0 && (!flushing || ret == 0)) return; 296 | } 297 | } 298 | 299 | /** @internal */ 300 | override _transform( 301 | chunk: unknown, 302 | _encoding: string, 303 | done: TransformCallback, 304 | ): void { 305 | try { 306 | // The Writable machinery is responsible for converting to a Buffer 307 | assert(chunk instanceof Buffer); 308 | 309 | // Handle flushes indicated by special dummy buffers 310 | let endType = binding.EndDirective.continue; 311 | if (Object.is(chunk, dummyFlushBuffer)) 312 | endType = binding.EndDirective.flush; 313 | else if (Object.is(chunk, dummyEndBuffer)) 314 | endType = binding.EndDirective.end; 315 | 316 | this.doCompress(chunk, endType); 317 | } catch (err) { 318 | done(err as Error); 319 | return; 320 | } 321 | done(); 322 | return; 323 | } 324 | 325 | /** @internal */ 326 | override _flush(done: TransformCallback): void { 327 | try { 328 | this.doCompress(dummyEndBuffer, binding.EndDirective.end); 329 | } catch (err) { 330 | done(err as Error); 331 | return; 332 | } 333 | done(); 334 | return; 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /lib/decompress.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert'; 2 | import { Transform, TransformCallback } from 'stream'; 3 | 4 | import * as binding from '../binding'; 5 | import { mapNumber, mapParameters } from './util'; 6 | 7 | /** 8 | * Zstandard decompression parameters. 9 | * 10 | * Most applications will not need to adjust these. See the 11 | * {@link https://facebook.github.io/zstd/zstd_manual.html | Zstandard manual} 12 | * for a full description. 13 | */ 14 | export interface DecompressParameters { 15 | windowLogMax?: number | undefined; 16 | } 17 | 18 | const PARAM_MAPPERS = { 19 | windowLogMax: mapNumber, 20 | }; 21 | 22 | function updateDCtxParameters( 23 | dctx: binding.DCtx, 24 | parameters: DecompressParameters, 25 | ): void { 26 | const mapped = mapParameters(binding.DParameter, PARAM_MAPPERS, parameters); 27 | for (const [param, value] of mapped) { 28 | dctx.setParameter(param, value); 29 | } 30 | } 31 | 32 | function getTotalContentSize(buffer: Uint8Array): number | null { 33 | let result = 0; 34 | let frame = buffer; 35 | while (frame.length > 0) { 36 | const contentSize = binding.getFrameContentSize(frame); 37 | if (contentSize === null) return null; 38 | result += contentSize; 39 | frame = frame.subarray(binding.findFrameCompressedSize(frame)); 40 | } 41 | return result; 42 | } 43 | 44 | const BUF_SIZE = binding.dStreamOutSize(); 45 | 46 | /** 47 | * High-level interface for customized single-pass Zstandard decompression. 48 | * 49 | * @example Basic usage 50 | * ``` 51 | * const dec = new Decompressor(); 52 | * const result = dec.decompress(compressedBuffer); 53 | * ``` 54 | * 55 | * @example Advanced usage 56 | * ``` 57 | * const dec = new Decompressor(); 58 | * dec.setParameters({windowLogMax: 24}); 59 | * dec.loadDictionary(fs.readFileSync('path/to/dictionary.dct')); 60 | * const result = dec.decompress(compressedBuffer); 61 | * ``` 62 | */ 63 | export class Decompressor { 64 | private dctx = new binding.DCtx(); 65 | 66 | /** 67 | * Decompress the data in `buffer` with the configured dictionary/parameters. 68 | * 69 | * @param buffer - Compressed data 70 | * @returns A new buffer with the uncompressed data 71 | */ 72 | decompress(buffer: Uint8Array): Buffer { 73 | // TODO: Default allocation limit, with option to override 74 | // TODO: Add a way to supply a known size or size hint 75 | 76 | // Find total uncompressed size of all frames in buffer 77 | const contentSize = getTotalContentSize(buffer); 78 | 79 | // Fast path if we have a content size 80 | if (contentSize !== null) { 81 | const result = Buffer.allocUnsafe(contentSize); 82 | const decompressedSize = this.dctx.decompress(result, buffer); 83 | assert.equal(decompressedSize, contentSize); 84 | return result; 85 | } 86 | 87 | // Fall back to streaming decompression 88 | const resultChunks: Buffer[] = []; 89 | let remainingInput = buffer; 90 | while (remainingInput.length > 0) { 91 | // With the complete input available, decompressStream will gladly fill 92 | // an arbitrarily large buffer. We can exploit this to save native calls 93 | // by using the input size as a heuristic lower bound on the content size. 94 | // 95 | // This is conservative: it won't over-allocate memory by more than the 96 | // max of the input length and BUF_SIZE. It doesn't perform well in the 97 | // face of extremely high compression ratios, but the worst case is 98 | // equivalent to always allocating BUF_SIZE. 99 | const chunkLen = Math.max(BUF_SIZE, remainingInput.length); 100 | const chunk = Buffer.allocUnsafe(chunkLen); 101 | const [, produced, consumed] = this.dctx.decompressStream( 102 | chunk, 103 | remainingInput, 104 | ); 105 | resultChunks.push(chunk.subarray(0, produced)); 106 | remainingInput = remainingInput.subarray(consumed); 107 | } 108 | 109 | // Concatenate the decompressed chunks (copies everything) 110 | return Buffer.concat(resultChunks); 111 | } 112 | 113 | /** 114 | * Load a compression dictionary from the provided buffer. 115 | * 116 | * The loaded dictionary will be used for all future {@link decompress} calls 117 | * until removed or replaced. Passing an empty buffer to this function will 118 | * remove a previously loaded dictionary. 119 | */ 120 | loadDictionary(data: Uint8Array): void { 121 | this.dctx.loadDictionary(data); 122 | } 123 | 124 | /** 125 | * Reset the decompressor state to only the provided parameters. 126 | * 127 | * Any loaded dictionary will be cleared, and any parameters not specified 128 | * will be reset to their default values. 129 | */ 130 | setParameters(parameters: DecompressParameters): void { 131 | this.dctx.reset(binding.ResetDirective.parameters); 132 | this.updateParameters(parameters); 133 | } 134 | 135 | /** 136 | * Modify decompression parameters. 137 | * 138 | * Parameters not specified will be left at their current values. 139 | */ 140 | updateParameters(parameters: DecompressParameters): void { 141 | updateDCtxParameters(this.dctx, parameters); 142 | } 143 | } 144 | 145 | /** 146 | * High-level interface for streaming Zstandard decompression. 147 | * 148 | * Implements the standard Node stream transformer interface, so can be used 149 | * with `.pipe` or any other streaming interface. 150 | * 151 | * @example Basic usage 152 | * ``` 153 | * import { pipeline } from 'stream/promises'; 154 | * await pipeline( 155 | * fs.createReadStream('data.txt.zst'), 156 | * new DecompressStream(), 157 | * fs.createWriteStream('data.txt'), 158 | * ); 159 | * ``` 160 | */ 161 | export class DecompressStream extends Transform { 162 | private dctx = new binding.DCtx(); 163 | private inFrame = false; 164 | 165 | // TODO: Allow user to specify a dictionary 166 | /** 167 | * Create a new streaming decompressor with the specified parameters. 168 | * 169 | * @param parameters - Decompression parameters 170 | */ 171 | constructor(parameters: DecompressParameters = {}) { 172 | // TODO: autoDestroy doesn't really work on Transform, we should consider 173 | // calling .destroy ourselves when necessary. 174 | super({ autoDestroy: true }); 175 | updateDCtxParameters(this.dctx, parameters); 176 | } 177 | 178 | /** @internal */ 179 | override _transform( 180 | chunk: unknown, 181 | _encoding: string, 182 | done: TransformCallback, 183 | ): void { 184 | // TODO: Optimize this by looking at the frame header 185 | try { 186 | // The Writable machinery is responsible for converting to a Buffer 187 | assert(chunk instanceof Buffer); 188 | let srcBuf = chunk; 189 | 190 | for (;;) { 191 | const dstBuf = Buffer.allocUnsafe(BUF_SIZE); 192 | const [ret, produced, consumed] = this.dctx.decompressStream( 193 | dstBuf, 194 | srcBuf, 195 | ); 196 | if (produced > 0) this.push(dstBuf.subarray(0, produced)); 197 | 198 | srcBuf = srcBuf.subarray(consumed); 199 | if (srcBuf.length === 0 && (produced < dstBuf.length || ret === 0)) { 200 | this.inFrame = ret !== 0; 201 | break; 202 | } 203 | } 204 | } catch (err) { 205 | done(err as Error); 206 | return; 207 | } 208 | done(); 209 | return; 210 | } 211 | 212 | /** @internal */ 213 | override _flush(done: TransformCallback): void { 214 | if (this.inFrame) { 215 | done(new Error('Stream ended in middle of compressed data frame')); 216 | return; 217 | } 218 | done(); 219 | return; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module (imported as `zstd-napi`) provides a high-level interface for 3 | * Zstandard compression and decompression. If you aren't sure what you need, 4 | * this is the right place to start! 5 | * 6 | * - The {@link compress} and {@link decompress} functions are the simplest, 7 | * single-pass (in-memory) interface. 8 | * - The {@link Compressor} and {@link Decompressor} classes provide a 9 | * single-pass interface with dictionary support. 10 | * - The {@link CompressStream} and {@link DecompressStream} classes provide 11 | * a streaming interface. 12 | * 13 | * If you're looking for low-level bindings to the native Zstandard library, 14 | * see the {@link "binding" | binding module}. 15 | * 16 | * @module index 17 | */ 18 | 19 | export { CompressStream, Compressor } from './compress'; 20 | export type { CompressParameters } from './compress'; 21 | 22 | export { DecompressStream, Decompressor } from './decompress'; 23 | export type { DecompressParameters } from './decompress'; 24 | 25 | export { compress, decompress } from './simple'; 26 | -------------------------------------------------------------------------------- /lib/simple.ts: -------------------------------------------------------------------------------- 1 | import { Compressor, CompressParameters } from './compress'; 2 | import { Decompressor, DecompressParameters } from './decompress'; 3 | 4 | let defaultCompressor: Compressor | undefined; 5 | let defaultDecompressor: Decompressor | undefined; 6 | 7 | /** 8 | * Compress `data` with Zstandard. 9 | * 10 | * Under the hood, this uses a shared, lazily-initialized {@link Compressor}, 11 | * which minimizes overhead. If you need dictionary support, create your own 12 | * instance of that class. 13 | * 14 | * @param data - Buffer containing data to compress 15 | * @param parameters - Optional compression parameters 16 | * @returns Compressed data 17 | */ 18 | export function compress( 19 | data: Uint8Array, 20 | parameters: CompressParameters = {}, 21 | ) { 22 | defaultCompressor ??= new Compressor(); 23 | defaultCompressor.setParameters(parameters); 24 | return defaultCompressor.compress(data); 25 | } 26 | 27 | /** 28 | * Decompress Zstandard-compressed `data`. 29 | * 30 | * Under the hood, this uses a shared, lazily-initialized {@link Decompressor}, 31 | * which minimizes overhead. If you need dictionary support, create your own 32 | * instance of that class. 33 | * 34 | * @param data - Buffer containing compressed data 35 | * @param parameters - Optional decompression parameters 36 | * @returns Decompressed data 37 | */ 38 | export function decompress( 39 | data: Uint8Array, 40 | parameters: DecompressParameters = {}, 41 | ) { 42 | defaultDecompressor ??= new Decompressor(); 43 | defaultDecompressor.setParameters(parameters); 44 | return defaultDecompressor.decompress(data); 45 | } 46 | -------------------------------------------------------------------------------- /lib/util.ts: -------------------------------------------------------------------------------- 1 | interface ParamMapper { 2 | validateInput(value: unknown): value is T; 3 | mapValue(value: T): number; 4 | } 5 | 6 | type ParamObject = { 7 | [key in keyof M]?: M[key] extends ParamMapper 8 | ? T | undefined 9 | : never; 10 | }; 11 | 12 | type StrKeys = Extract; 13 | type OnlyKeys = O & Record, never>; 14 | 15 | export const mapNumber: ParamMapper = { 16 | validateInput: (value): value is number => typeof value === 'number', 17 | mapValue: (value) => value, 18 | }; 19 | 20 | export function mapEnum, number>>( 21 | enumObj: E, 22 | ): ParamMapper> { 23 | return { 24 | validateInput: (value): value is StrKeys => 25 | typeof value === 'string' && value in enumObj, 26 | mapValue: (value) => enumObj[value], 27 | }; 28 | } 29 | 30 | export const mapBoolean: ParamMapper = { 31 | validateInput: (value): value is boolean => typeof value === 'boolean', 32 | mapValue: (value) => Number(value), 33 | }; 34 | 35 | function mapParameter

( 36 | name: string, 37 | mapper: ParamMapper

, 38 | value: unknown, 39 | ): number { 40 | if (!mapper.validateInput(value)) { 41 | throw new TypeError(`Invalid type for parameter: ${name}`); 42 | } 43 | return mapper.mapValue(value); 44 | } 45 | 46 | export function mapParameters< 47 | E, 48 | M extends Record, ParamMapper>, 49 | P extends ParamObject, 50 | >( 51 | paramEnum: E, 52 | mapper: OnlyKeys>, 53 | params: OnlyKeys>, 54 | ): Map { 55 | const result = new Map(); 56 | for (const [rawKey, value] of Object.entries(params)) { 57 | if (value !== undefined) { 58 | if (!(rawKey in mapper)) { 59 | throw new RangeError(`Invalid parameter name: ${rawKey}`); 60 | } 61 | const key = rawKey as StrKeys; 62 | result.set(paramEnum[key], mapParameter(key, mapper[key], value)); 63 | } 64 | } 65 | return result; 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zstd-napi", 3 | "version": "0.0.11", 4 | "description": "Zstandard (zstd) compression library bindings with Node-API", 5 | "keywords": [ 6 | "binding", 7 | "compression", 8 | "native", 9 | "zstandard", 10 | "zstd", 11 | "Node-API", 12 | "N-API" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/drakedevel/zstd-napi.git" 17 | }, 18 | "license": "Apache-2.0", 19 | "author": "Andrew Drake ", 20 | "main": "dist/index.js", 21 | "types": "dist/index.d.ts", 22 | "files": [ 23 | "NOTICE", 24 | "binding.d.ts", 25 | "binding.gyp", 26 | "binding.js", 27 | "build_flags.gypi", 28 | "deps", 29 | "dist", 30 | "src" 31 | ], 32 | "binary": { 33 | "napi_versions": [ 34 | 8 35 | ] 36 | }, 37 | "scripts": { 38 | "build": "node-gyp configure && node-gyp build", 39 | "ci-ignore-scripts": "npm ci --ignore-scripts", 40 | "clang-format": "clang-format -i src/*", 41 | "install": "prebuild-install -r napi || node-gyp rebuild", 42 | "lint": "eslint .", 43 | "prepare": "tsc -p tsconfig.emit.json", 44 | "prettier": "prettier -l --write .", 45 | "submodule-update": "git submodule update --init", 46 | "test": "jest", 47 | "test-coverage": "jest --coverage --coverageReporters=json", 48 | "typedoc": "typedoc" 49 | }, 50 | "dependencies": { 51 | "@types/node": "*", 52 | "node-addon-api": "^7.0.0", 53 | "prebuild-install": "^7.1.1" 54 | }, 55 | "devDependencies": { 56 | "@eslint/js": "9.25.1", 57 | "@fast-check/jest": "2.1.1", 58 | "@tsconfig/node20": "20.1.5", 59 | "@tsconfig/strictest": "2.0.5", 60 | "@types/eslint-config-prettier": "6.11.3", 61 | "@types/jest": "29.5.14", 62 | "eslint": "9.25.1", 63 | "eslint-config-prettier": "10.1.2", 64 | "eslint-plugin-jest": "28.11.0", 65 | "eslint-plugin-tsdoc": "0.4.0", 66 | "expect-type": "1.2.1", 67 | "globals": "16.0.0", 68 | "jest": "29.7.0", 69 | "node-gyp": "11.2.0", 70 | "prettier": "3.5.3", 71 | "ts-jest": "29.3.2", 72 | "typedoc": "0.28.4", 73 | "typescript": "5.8.3", 74 | "typescript-eslint": "8.31.1" 75 | }, 76 | "engines": { 77 | "node": "^12.22.0 || ^14.17.0 || ^15.12.0 || >=16" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/binding.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "cctx.h" 6 | #include "cdict.h" 7 | #include "constants.h" 8 | #include "dctx.h" 9 | #include "ddict.h" 10 | #include "util.h" 11 | 12 | using namespace Napi; 13 | 14 | // Version functions 15 | Value wrapVersionNumber(const CallbackInfo& info) { 16 | return Number::New(info.Env(), ZSTD_versionNumber()); 17 | } 18 | 19 | Value wrapVersionString(const CallbackInfo& info) { 20 | return String::New(info.Env(), ZSTD_versionString()); 21 | } 22 | 23 | // Simple API 24 | Value wrapCompress(const CallbackInfo& info) { 25 | Env env = info.Env(); 26 | checkArgCount(info, 3); 27 | Uint8Array dstBuf = info[0].As(); 28 | Uint8Array srcBuf = info[1].As(); 29 | int32_t level = info[2].ToNumber(); 30 | 31 | size_t result = ZSTD_compress(dstBuf.Data(), dstBuf.ByteLength(), 32 | srcBuf.Data(), srcBuf.ByteLength(), level); 33 | return convertZstdResult(env, result); 34 | } 35 | 36 | Value wrapDecompress(const CallbackInfo& info) { 37 | Env env = info.Env(); 38 | checkArgCount(info, 2); 39 | Uint8Array dstBuf = info[0].As(); 40 | Uint8Array srcBuf = info[1].As(); 41 | 42 | size_t result = ZSTD_decompress(dstBuf.Data(), dstBuf.ByteLength(), 43 | srcBuf.Data(), srcBuf.ByteLength()); 44 | return convertZstdResult(env, result); 45 | } 46 | 47 | Value wrapGetFrameContentSize(const CallbackInfo& info) { 48 | Env env = info.Env(); 49 | checkArgCount(info, 1); 50 | Uint8Array frameBuf = info[0].As(); 51 | 52 | unsigned long long size = 53 | ZSTD_getFrameContentSize(frameBuf.Data(), frameBuf.ByteLength()); 54 | if (size == ZSTD_CONTENTSIZE_UNKNOWN) 55 | return env.Null(); 56 | if (size == ZSTD_CONTENTSIZE_ERROR) 57 | throw Error::New(env, "Could not parse Zstandard header"); 58 | return Number::New(env, size); 59 | } 60 | 61 | Value wrapFindFrameCompressedSize(const CallbackInfo& info) { 62 | Env env = info.Env(); 63 | checkArgCount(info, 1); 64 | Uint8Array frameBuf = info[0].As(); 65 | 66 | return convertZstdResult(env, ZSTD_findFrameCompressedSize( 67 | frameBuf.Data(), frameBuf.ByteLength())); 68 | } 69 | 70 | // Helper functions 71 | Value wrapCompressBound(const CallbackInfo& info) { 72 | Env env = info.Env(); 73 | checkArgCount(info, 1); 74 | int64_t size = info[0].ToNumber(); 75 | 76 | return Number::New(env, ZSTD_compressBound(size)); 77 | } 78 | 79 | Value wrapMinCLevel(const CallbackInfo& info) { 80 | return Number::New(info.Env(), ZSTD_minCLevel()); 81 | } 82 | 83 | Value wrapMaxCLevel(const CallbackInfo& info) { 84 | return Number::New(info.Env(), ZSTD_maxCLevel()); 85 | } 86 | 87 | Value wrapDefaultCLevel(const CallbackInfo& info) { 88 | return Number::New(info.Env(), ZSTD_defaultCLevel()); 89 | } 90 | 91 | // Advanced compression 92 | static inline Value convertParamBounds(Env env, const ZSTD_bounds& bounds) { 93 | checkZstdError(env, bounds.error); 94 | Object result = Object::New(env); 95 | result["lowerBound"] = bounds.lowerBound; 96 | result["upperBound"] = bounds.upperBound; 97 | return result; 98 | } 99 | 100 | Value wrapCParamGetBounds(const CallbackInfo& info) { 101 | Env env = info.Env(); 102 | checkArgCount(info, 1); 103 | ZSTD_cParameter param = 104 | static_cast(info[0].ToNumber().Int32Value()); 105 | 106 | return convertParamBounds(env, ZSTD_cParam_getBounds(param)); 107 | } 108 | 109 | // Advanced decompression 110 | Value wrapDParamGetBounds(const CallbackInfo& info) { 111 | Env env = info.Env(); 112 | checkArgCount(info, 1); 113 | ZSTD_dParameter param = 114 | static_cast(info[0].ToNumber().Int32Value()); 115 | 116 | return convertParamBounds(env, ZSTD_dParam_getBounds(param)); 117 | } 118 | 119 | // Streaming compression 120 | Value wrapCStreamInSize(const CallbackInfo& info) { 121 | return Number::New(info.Env(), ZSTD_CStreamInSize()); 122 | } 123 | 124 | Value wrapCStreamOutSize(const CallbackInfo& info) { 125 | return Number::New(info.Env(), ZSTD_CStreamOutSize()); 126 | } 127 | 128 | // Streaming decompression 129 | Value wrapDStreamInSize(const CallbackInfo& info) { 130 | return Number::New(info.Env(), ZSTD_DStreamInSize()); 131 | } 132 | 133 | Value wrapDStreamOutSize(const CallbackInfo& info) { 134 | return Number::New(info.Env(), ZSTD_DStreamOutSize()); 135 | } 136 | 137 | // Dictionary helper functions 138 | Value wrapGetDictIDFromDict(const CallbackInfo& info) { 139 | Env env = info.Env(); 140 | checkArgCount(info, 1); 141 | Uint8Array dictBuf = info[0].As(); 142 | return Number::New( 143 | env, ZSTD_getDictID_fromDict(dictBuf.Data(), dictBuf.ByteLength())); 144 | } 145 | 146 | Value wrapGetDictIDFromFrame(const CallbackInfo& info) { 147 | Env env = info.Env(); 148 | checkArgCount(info, 1); 149 | Uint8Array frameBuf = info[0].As(); 150 | return Number::New( 151 | env, ZSTD_getDictID_fromFrame(frameBuf.Data(), frameBuf.ByteLength())); 152 | } 153 | 154 | // This is a copy of PropertyDescriptor::Function, except it uses the templated 155 | // version of Function::New instead of the heap-allocating one. Should be 156 | // replaced when added upstream (not yet added as of 7.x). 157 | template 158 | inline PropertyDescriptor propertyDescFunction( 159 | Env env, 160 | Object /*object*/, 161 | const char* utf8name, 162 | napi_property_attributes attributes = napi_default, 163 | void* data = nullptr) { 164 | return PropertyDescriptor({utf8name, nullptr, nullptr, nullptr, nullptr, 165 | Napi::Function::New(env, utf8name, data), 166 | attributes, nullptr}); 167 | } 168 | 169 | Object ModuleInit(Env env, Object exports) { 170 | CCtx::Init(env, exports); 171 | CDict::Init(env, exports); 172 | DCtx::Init(env, exports); 173 | DDict::Init(env, exports); 174 | 175 | createConstants(env, exports); 176 | createEnums(env, exports); 177 | 178 | exports.DefineProperties({ 179 | propertyDescFunction(env, exports, "versionNumber"), 180 | propertyDescFunction(env, exports, "versionString"), 181 | propertyDescFunction(env, exports, "compress"), 182 | propertyDescFunction(env, exports, "decompress"), 183 | propertyDescFunction(env, exports, 184 | "getFrameContentSize"), 185 | propertyDescFunction( 186 | env, exports, "findFrameCompressedSize"), 187 | propertyDescFunction(env, exports, "compressBound"), 188 | propertyDescFunction(env, exports, "minCLevel"), 189 | propertyDescFunction(env, exports, "maxCLevel"), 190 | propertyDescFunction(env, exports, "defaultCLevel"), 191 | propertyDescFunction(env, exports, 192 | "cParamGetBounds"), 193 | propertyDescFunction(env, exports, 194 | "dParamGetBounds"), 195 | propertyDescFunction(env, exports, "cStreamInSize"), 196 | propertyDescFunction(env, exports, "cStreamOutSize"), 197 | propertyDescFunction(env, exports, "dStreamInSize"), 198 | propertyDescFunction(env, exports, "dStreamOutSize"), 199 | propertyDescFunction(env, exports, 200 | "getDictIDFromDict"), 201 | propertyDescFunction(env, exports, 202 | "getDictIDFromFrame"), 203 | }); 204 | 205 | return exports; 206 | } 207 | 208 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, ModuleInit) 209 | -------------------------------------------------------------------------------- /src/cctx.cc: -------------------------------------------------------------------------------- 1 | #include "cctx.h" 2 | 3 | #include "cdict.h" 4 | 5 | using namespace Napi; 6 | 7 | const napi_type_tag CCtx::typeTag = {0xced16821986135b4, 0x11b09ca57b5ab06c}; 8 | 9 | void CCtx::Init(Napi::Env env, Napi::Object exports) { 10 | Function func = DefineClass( 11 | env, "CCtx", 12 | { 13 | InstanceMethod<&CCtx::wrapCompress>("compress"), 14 | InstanceMethod<&CCtx::wrapCompressUsingDict>("compressUsingDict"), 15 | InstanceMethod<&CCtx::wrapCompressUsingCDict>("compressUsingCDict"), 16 | InstanceMethod<&CCtx::wrapSetParameter>("setParameter"), 17 | InstanceMethod<&CCtx::wrapSetPledgedSrcSize>("setPledgedSrcSize"), 18 | InstanceMethod<&CCtx::wrapReset>("reset"), 19 | InstanceMethod<&CCtx::wrapCompress2>("compress2"), 20 | InstanceMethod<&CCtx::wrapCompressStream2>("compressStream2"), 21 | InstanceMethod<&CCtx::wrapLoadDictionary>("loadDictionary"), 22 | }); 23 | exports.Set("CCtx", func); 24 | } 25 | 26 | CCtx::CCtx(const Napi::CallbackInfo& info) : ObjectWrapHelper(info) { 27 | cctx.reset(ZSTD_createCCtx()); 28 | adjustMemory(info.Env()); 29 | } 30 | 31 | Napi::Value CCtx::wrapCompress(const Napi::CallbackInfo& info) { 32 | Napi::Env env = info.Env(); 33 | checkArgCount(info, 3); 34 | Uint8Array dstBuf = info[0].As(); 35 | Uint8Array srcBuf = info[1].As(); 36 | int32_t level = info[2].ToNumber(); 37 | 38 | size_t result = 39 | ZSTD_compressCCtx(cctx.get(), dstBuf.Data(), dstBuf.ByteLength(), 40 | srcBuf.Data(), srcBuf.ByteLength(), level); 41 | adjustMemory(env); 42 | return convertZstdResult(env, result); 43 | } 44 | 45 | Napi::Value CCtx::wrapCompressUsingDict(const Napi::CallbackInfo& info) { 46 | Napi::Env env = info.Env(); 47 | checkArgCount(info, 4); 48 | Uint8Array dstBuf = info[0].As(); 49 | Uint8Array srcBuf = info[1].As(); 50 | Uint8Array dictBuf = info[2].As(); 51 | int32_t level = info[3].ToNumber(); 52 | 53 | size_t result = ZSTD_compress_usingDict( 54 | cctx.get(), dstBuf.Data(), dstBuf.ByteLength(), srcBuf.Data(), 55 | srcBuf.ByteLength(), dictBuf.Data(), dictBuf.ByteLength(), level); 56 | adjustMemory(env); 57 | return convertZstdResult(env, result); 58 | } 59 | 60 | Napi::Value CCtx::wrapCompressUsingCDict(const Napi::CallbackInfo& info) { 61 | Napi::Env env = info.Env(); 62 | checkArgCount(info, 3); 63 | Uint8Array dstBuf = info[0].As(); 64 | Uint8Array srcBuf = info[1].As(); 65 | CDict* cdictObj = CDict::Unwrap(info[2].As()); 66 | 67 | size_t result = ZSTD_compress_usingCDict( 68 | cctx.get(), dstBuf.Data(), dstBuf.ByteLength(), srcBuf.Data(), 69 | srcBuf.ByteLength(), cdictObj->cdict.get()); 70 | adjustMemory(env); 71 | return convertZstdResult(env, result); 72 | } 73 | 74 | void CCtx::wrapSetParameter(const Napi::CallbackInfo& info) { 75 | Napi::Env env = info.Env(); 76 | checkArgCount(info, 2); 77 | ZSTD_cParameter param = 78 | static_cast(info[0].ToNumber().Int32Value()); 79 | int value = info[1].ToNumber(); 80 | 81 | size_t result = ZSTD_CCtx_setParameter(cctx.get(), param, value); 82 | adjustMemory(env); 83 | checkZstdError(env, result); 84 | } 85 | 86 | void CCtx::wrapSetPledgedSrcSize(const Napi::CallbackInfo& info) { 87 | Napi::Env env = info.Env(); 88 | checkArgCount(info, 1); 89 | unsigned long long srcSize = info[0].ToNumber().Int64Value(); 90 | 91 | size_t result = ZSTD_CCtx_setPledgedSrcSize(cctx.get(), srcSize); 92 | adjustMemory(env); 93 | checkZstdError(env, result); 94 | } 95 | 96 | void CCtx::wrapReset(const Napi::CallbackInfo& info) { 97 | Napi::Env env = info.Env(); 98 | checkArgCount(info, 1); 99 | ZSTD_ResetDirective reset = 100 | static_cast(info[0].ToNumber().Int32Value()); 101 | 102 | size_t result = ZSTD_CCtx_reset(cctx.get(), reset); 103 | adjustMemory(env); 104 | checkZstdError(env, result); 105 | } 106 | 107 | Napi::Value CCtx::wrapCompress2(const Napi::CallbackInfo& info) { 108 | Napi::Env env = info.Env(); 109 | checkArgCount(info, 2); 110 | Uint8Array dstBuf = info[0].As(); 111 | Uint8Array srcBuf = info[1].As(); 112 | 113 | size_t result = ZSTD_compress2(cctx.get(), dstBuf.Data(), dstBuf.ByteLength(), 114 | srcBuf.Data(), srcBuf.ByteLength()); 115 | adjustMemory(env); 116 | return convertZstdResult(env, result); 117 | } 118 | 119 | Napi::Value CCtx::wrapCompressStream2(const Napi::CallbackInfo& info) { 120 | Napi::Env env = info.Env(); 121 | checkArgCount(info, 3); 122 | Uint8Array dstBuf = info[0].As(); 123 | Uint8Array srcBuf = info[1].As(); 124 | ZSTD_EndDirective endOp = 125 | static_cast(info[2].ToNumber().Int32Value()); 126 | 127 | ZSTD_outBuffer zstdOut = makeZstdOutBuffer(dstBuf); 128 | ZSTD_inBuffer zstdIn = makeZstdInBuffer(srcBuf); 129 | size_t ret = ZSTD_compressStream2(cctx.get(), &zstdOut, &zstdIn, endOp); 130 | adjustMemory(env); 131 | return makeStreamResult(env, ret, zstdOut, zstdIn); 132 | } 133 | 134 | void CCtx::wrapLoadDictionary(const Napi::CallbackInfo& info) { 135 | Napi::Env env = info.Env(); 136 | checkArgCount(info, 1); 137 | Uint8Array dictBuf = info[0].As(); 138 | 139 | size_t result = ZSTD_CCtx_loadDictionary(cctx.get(), dictBuf.Data(), 140 | dictBuf.ByteLength()); 141 | adjustMemory(env); 142 | checkZstdError(env, result); 143 | } 144 | -------------------------------------------------------------------------------- /src/cctx.h: -------------------------------------------------------------------------------- 1 | #ifndef CCTX_H 2 | #define CCTX_H 3 | 4 | #include 5 | 6 | #include "object_wrap_helper.h" 7 | #include "util.h" 8 | #include "zstd.h" 9 | 10 | class CCtx : public ObjectWrapHelper { 11 | public: 12 | static const napi_type_tag typeTag; 13 | static void Init(Napi::Env env, Napi::Object exports); 14 | CCtx(const Napi::CallbackInfo& info); 15 | 16 | private: 17 | zstd_unique_ptr cctx; 18 | 19 | int64_t getCurrentSize() override { return ZSTD_sizeof_CCtx(cctx.get()); } 20 | 21 | Napi::Value wrapCompress(const Napi::CallbackInfo& info); 22 | Napi::Value wrapCompressUsingDict(const Napi::CallbackInfo& info); 23 | Napi::Value wrapCompressUsingCDict(const Napi::CallbackInfo& info); 24 | void wrapSetParameter(const Napi::CallbackInfo& info); 25 | void wrapSetPledgedSrcSize(const Napi::CallbackInfo& info); 26 | void wrapReset(const Napi::CallbackInfo& info); 27 | Napi::Value wrapCompress2(const Napi::CallbackInfo& info); 28 | Napi::Value wrapCompressStream2(const Napi::CallbackInfo& info); 29 | void wrapLoadDictionary(const Napi::CallbackInfo& info); 30 | }; 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/cdict.cc: -------------------------------------------------------------------------------- 1 | #include "cdict.h" 2 | 3 | using namespace Napi; 4 | 5 | const napi_type_tag CDict::typeTag = {0x9257fdef516e4f9c, 0x3efa685d51e7bb2b}; 6 | 7 | void CDict::Init(Napi::Env env, Napi::Object exports) { 8 | Function func = 9 | DefineClass(env, "CDict", 10 | { 11 | InstanceMethod<&CDict::wrapGetDictID>("getDictID"), 12 | }); 13 | exports.Set("CDict", func); 14 | } 15 | 16 | CDict::CDict(const Napi::CallbackInfo& info) : ObjectWrapHelper(info) { 17 | Napi::Env env = info.Env(); 18 | checkArgCount(info, 2); 19 | Uint8Array dictBuf = info[0].As(); 20 | int32_t level = info[1].ToNumber(); 21 | 22 | cdict.reset(ZSTD_createCDict(dictBuf.Data(), dictBuf.ByteLength(), level)); 23 | if (!cdict) 24 | throw Error::New(env, "Failed to create CDict"); 25 | adjustMemory(env); 26 | } 27 | 28 | Napi::Value CDict::wrapGetDictID(const Napi::CallbackInfo& info) { 29 | return Number::New(info.Env(), ZSTD_getDictID_fromCDict(cdict.get())); 30 | } 31 | -------------------------------------------------------------------------------- /src/cdict.h: -------------------------------------------------------------------------------- 1 | #ifndef CDICT_H 2 | #define CDICT_H 3 | 4 | #include 5 | 6 | #include "object_wrap_helper.h" 7 | #include "util.h" 8 | #include "zstd.h" 9 | 10 | class CDict : public ObjectWrapHelper { 11 | public: 12 | static const napi_type_tag typeTag; 13 | static void Init(Napi::Env env, Napi::Object exports); 14 | CDict(const Napi::CallbackInfo& info); 15 | 16 | private: 17 | friend class CCtx; 18 | zstd_unique_ptr cdict; 19 | 20 | int64_t getCurrentSize() { return ZSTD_sizeof_CDict(cdict.get()); } 21 | 22 | Napi::Value wrapGetDictID(const Napi::CallbackInfo& info); 23 | }; 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /src/constants.cc: -------------------------------------------------------------------------------- 1 | #include "constants.h" 2 | 3 | #include "zstd.h" 4 | 5 | using namespace Napi; 6 | 7 | void createConstants(Env env, Object exports) { 8 | #define C(v) exports[#v] = Number::New(env, ZSTD_##v) 9 | C(MAGICNUMBER); 10 | C(MAGIC_DICTIONARY); 11 | C(MAGIC_SKIPPABLE_START); 12 | C(MAGIC_SKIPPABLE_MASK); 13 | #undef C 14 | } 15 | 16 | void createEnums(Env env, Object exports) { 17 | #define ADD_ENUM_MEMBER(obj, pfx, name, jname) \ 18 | do { \ 19 | String vName = String::New(env, #jname); \ 20 | Number vValue = Number::New(env, pfx##name); \ 21 | (obj).Set(vName, vValue); \ 22 | (obj).Set(vValue, vName); \ 23 | } while (0) 24 | 25 | // ZSTD_strategy 26 | Object strategy = Object::New(env); 27 | #define E(name) ADD_ENUM_MEMBER(strategy, ZSTD_, name, name) 28 | E(fast); 29 | E(dfast); 30 | E(greedy); 31 | E(lazy); 32 | E(lazy2); 33 | E(btlazy2); 34 | E(btopt); 35 | E(btultra); 36 | #undef E 37 | exports["Strategy"] = strategy; 38 | 39 | // ZSTD_cParameter 40 | Object cParameter = Object::New(env); 41 | #define E(name) ADD_ENUM_MEMBER(cParameter, ZSTD_c_, name, name) 42 | E(compressionLevel); 43 | E(windowLog); 44 | E(hashLog); 45 | E(chainLog); 46 | E(searchLog); 47 | E(minMatch); 48 | E(targetLength); 49 | E(strategy); 50 | E(targetCBlockSize); 51 | E(enableLongDistanceMatching); 52 | E(ldmHashLog); 53 | E(ldmMinMatch); 54 | E(ldmBucketSizeLog); 55 | E(ldmHashRateLog); 56 | E(contentSizeFlag); 57 | E(checksumFlag); 58 | E(dictIDFlag); 59 | E(nbWorkers); 60 | E(jobSize); 61 | E(overlapLog); 62 | #undef E 63 | exports["CParameter"] = cParameter; 64 | 65 | // ZSTD_ResetDirective 66 | Object resetDirective = Object::New(env); 67 | #define E(name, jname) ADD_ENUM_MEMBER(resetDirective, ZSTD_reset_, name, jname) 68 | E(session_only, sessionOnly); 69 | E(parameters, parameters); 70 | E(session_and_parameters, sessionAndParameters); 71 | #undef E 72 | exports["ResetDirective"] = resetDirective; 73 | 74 | // ZSTD_dParameter 75 | Object dParameter = Object::New(env); 76 | #define E(name) ADD_ENUM_MEMBER(dParameter, ZSTD_d_, name, name) 77 | E(windowLogMax); 78 | #undef E 79 | exports["DParameter"] = dParameter; 80 | 81 | // ZSTD_EndDirective 82 | Object endDirective = Object::New(env); 83 | #define E(name) ADD_ENUM_MEMBER(endDirective, ZSTD_e_, name, name) 84 | E(continue); 85 | E(flush); 86 | E(end); 87 | #undef E 88 | exports["EndDirective"] = endDirective; 89 | 90 | #undef ADD_ENUM_MEMBER 91 | } 92 | -------------------------------------------------------------------------------- /src/constants.h: -------------------------------------------------------------------------------- 1 | #ifndef CONSTANTS_H 2 | #define CONSTANTS_H 3 | 4 | #include 5 | 6 | void createConstants(Napi::Env env, Napi::Object exports); 7 | void createEnums(Napi::Env env, Napi::Object exports); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /src/dctx.cc: -------------------------------------------------------------------------------- 1 | #include "dctx.h" 2 | 3 | #include "ddict.h" 4 | 5 | using namespace Napi; 6 | 7 | const napi_type_tag DCtx::typeTag = {0x1c73d7689d424a96, 0x45bc0e392233f8ca}; 8 | 9 | void DCtx::Init(Napi::Env env, Napi::Object exports) { 10 | Function func = DefineClass( 11 | env, "DCtx", 12 | { 13 | InstanceMethod<&DCtx::wrapDecompress>("decompress"), 14 | InstanceMethod<&DCtx::wrapDecompressStream>("decompressStream"), 15 | InstanceMethod<&DCtx::wrapDecompressUsingDict>("decompressUsingDict"), 16 | InstanceMethod<&DCtx::wrapDecompressUsingDDict>( 17 | "decompressUsingDDict"), 18 | InstanceMethod<&DCtx::wrapSetParameter>("setParameter"), 19 | InstanceMethod<&DCtx::wrapReset>("reset"), 20 | InstanceMethod<&DCtx::wrapLoadDictionary>("loadDictionary"), 21 | }); 22 | exports.Set("DCtx", func); 23 | } 24 | 25 | DCtx::DCtx(const Napi::CallbackInfo& info) : ObjectWrapHelper(info) { 26 | dctx.reset(ZSTD_createDCtx()); 27 | adjustMemory(info.Env()); 28 | } 29 | 30 | Napi::Value DCtx::wrapDecompress(const Napi::CallbackInfo& info) { 31 | Napi::Env env = info.Env(); 32 | checkArgCount(info, 2); 33 | Uint8Array dstBuf = info[0].As(); 34 | Uint8Array srcBuf = info[1].As(); 35 | 36 | size_t result = 37 | ZSTD_decompressDCtx(dctx.get(), dstBuf.Data(), dstBuf.ByteLength(), 38 | srcBuf.Data(), srcBuf.ByteLength()); 39 | adjustMemory(env); 40 | return convertZstdResult(env, result); 41 | } 42 | 43 | Napi::Value DCtx::wrapDecompressStream(const Napi::CallbackInfo& info) { 44 | Napi::Env env = info.Env(); 45 | checkArgCount(info, 2); 46 | Uint8Array dstBuf = info[0].As(); 47 | Uint8Array srcBuf = info[1].As(); 48 | 49 | ZSTD_outBuffer zstdOut = makeZstdOutBuffer(dstBuf); 50 | ZSTD_inBuffer zstdIn = makeZstdInBuffer(srcBuf); 51 | size_t ret = ZSTD_decompressStream(dctx.get(), &zstdOut, &zstdIn); 52 | adjustMemory(env); 53 | return makeStreamResult(env, ret, zstdOut, zstdIn); 54 | } 55 | 56 | Napi::Value DCtx::wrapDecompressUsingDict(const Napi::CallbackInfo& info) { 57 | Napi::Env env = info.Env(); 58 | checkArgCount(info, 3); 59 | Uint8Array dstBuf = info[0].As(); 60 | Uint8Array srcBuf = info[1].As(); 61 | Uint8Array dictBuf = info[2].As(); 62 | 63 | size_t result = ZSTD_decompress_usingDict( 64 | dctx.get(), dstBuf.Data(), dstBuf.ByteLength(), srcBuf.Data(), 65 | srcBuf.ByteLength(), dictBuf.Data(), dictBuf.ByteLength()); 66 | adjustMemory(env); 67 | return convertZstdResult(env, result); 68 | } 69 | 70 | Napi::Value DCtx::wrapDecompressUsingDDict(const Napi::CallbackInfo& info) { 71 | Napi::Env env = info.Env(); 72 | checkArgCount(info, 3); 73 | Uint8Array dstBuf = info[0].As(); 74 | Uint8Array srcBuf = info[1].As(); 75 | DDict* ddictObj = DDict::Unwrap(info[2].As()); 76 | 77 | size_t result = ZSTD_decompress_usingDDict( 78 | dctx.get(), dstBuf.Data(), dstBuf.ByteLength(), srcBuf.Data(), 79 | srcBuf.ByteLength(), ddictObj->ddict.get()); 80 | adjustMemory(env); 81 | return convertZstdResult(env, result); 82 | } 83 | 84 | void DCtx::wrapSetParameter(const Napi::CallbackInfo& info) { 85 | Napi::Env env = info.Env(); 86 | checkArgCount(info, 2); 87 | ZSTD_dParameter param = 88 | static_cast(info[0].ToNumber().Int32Value()); 89 | int value = info[1].ToNumber(); 90 | 91 | size_t result = ZSTD_DCtx_setParameter(dctx.get(), param, value); 92 | adjustMemory(env); 93 | checkZstdError(env, result); 94 | } 95 | 96 | void DCtx::wrapReset(const Napi::CallbackInfo& info) { 97 | Napi::Env env = info.Env(); 98 | checkArgCount(info, 1); 99 | ZSTD_ResetDirective reset = 100 | static_cast(info[0].ToNumber().Int32Value()); 101 | 102 | size_t result = ZSTD_DCtx_reset(dctx.get(), reset); 103 | adjustMemory(env); 104 | checkZstdError(env, result); 105 | } 106 | 107 | void DCtx::wrapLoadDictionary(const Napi::CallbackInfo& info) { 108 | Napi::Env env = info.Env(); 109 | checkArgCount(info, 1); 110 | Uint8Array dictBuf = info[0].As(); 111 | 112 | size_t result = ZSTD_DCtx_loadDictionary(dctx.get(), dictBuf.Data(), 113 | dictBuf.ByteLength()); 114 | adjustMemory(env); 115 | checkZstdError(env, result); 116 | } 117 | -------------------------------------------------------------------------------- /src/dctx.h: -------------------------------------------------------------------------------- 1 | #ifndef DCTX_H 2 | #define DCTX_H 3 | 4 | #include 5 | 6 | #include "object_wrap_helper.h" 7 | #include "util.h" 8 | #include "zstd.h" 9 | 10 | class DCtx : public ObjectWrapHelper { 11 | public: 12 | static const napi_type_tag typeTag; 13 | static void Init(Napi::Env env, Napi::Object exports); 14 | DCtx(const Napi::CallbackInfo& info); 15 | 16 | private: 17 | zstd_unique_ptr dctx; 18 | 19 | int64_t getCurrentSize() { return ZSTD_sizeof_DCtx(dctx.get()); } 20 | 21 | Napi::Value wrapDecompress(const Napi::CallbackInfo& info); 22 | Napi::Value wrapDecompressStream(const Napi::CallbackInfo& info); 23 | Napi::Value wrapDecompressUsingDict(const Napi::CallbackInfo& info); 24 | Napi::Value wrapDecompressUsingDDict(const Napi::CallbackInfo& info); 25 | void wrapSetParameter(const Napi::CallbackInfo& info); 26 | void wrapReset(const Napi::CallbackInfo& info); 27 | void wrapLoadDictionary(const Napi::CallbackInfo& info); 28 | }; 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /src/ddict.cc: -------------------------------------------------------------------------------- 1 | #include "ddict.h" 2 | 3 | using namespace Napi; 4 | 5 | const napi_type_tag DDict::typeTag = {0x5947459a9a933efa, 0xe3ce81af92835a95}; 6 | 7 | void DDict::Init(Napi::Env env, Napi::Object exports) { 8 | Function func = 9 | DefineClass(env, "DDict", 10 | { 11 | InstanceMethod<&DDict::wrapGetDictID>("getDictID"), 12 | }); 13 | exports.Set("DDict", func); 14 | } 15 | 16 | DDict::DDict(const Napi::CallbackInfo& info) : ObjectWrapHelper(info) { 17 | Napi::Env env = info.Env(); 18 | checkArgCount(info, 1); 19 | Uint8Array dictBuf = info[0].As(); 20 | 21 | ddict.reset(ZSTD_createDDict(dictBuf.Data(), dictBuf.ByteLength())); 22 | if (!ddict) 23 | throw Error::New(env, "Failed to create DDict"); 24 | adjustMemory(env); 25 | } 26 | 27 | Napi::Value DDict::wrapGetDictID(const Napi::CallbackInfo& info) { 28 | return Number::New(info.Env(), ZSTD_getDictID_fromDDict(ddict.get())); 29 | } 30 | -------------------------------------------------------------------------------- /src/ddict.h: -------------------------------------------------------------------------------- 1 | #ifndef DDICT_H 2 | #define DDICT_H 3 | 4 | #include 5 | 6 | #include "object_wrap_helper.h" 7 | #include "util.h" 8 | #include "zstd.h" 9 | 10 | class DDict : public ObjectWrapHelper { 11 | public: 12 | static const napi_type_tag typeTag; 13 | static void Init(Napi::Env env, Napi::Object exports); 14 | DDict(const Napi::CallbackInfo& info); 15 | 16 | private: 17 | friend class DCtx; 18 | zstd_unique_ptr ddict; 19 | 20 | int64_t getCurrentSize() { return ZSTD_sizeof_DDict(ddict.get()); } 21 | 22 | Napi::Value wrapGetDictID(const Napi::CallbackInfo& info); 23 | }; 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /src/object_wrap_helper.h: -------------------------------------------------------------------------------- 1 | #ifndef OBJECT_WRAP_HELPER_H 2 | #define OBJECT_WRAP_HELPER_H 3 | 4 | #include 5 | 6 | template 7 | class ObjectWrapHelper : public Napi::ObjectWrap { 8 | public: 9 | ObjectWrapHelper(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 10 | info.This().As().TypeTag(&T::typeTag); 11 | } 12 | virtual void Finalize(Napi::Env env) override; 13 | 14 | static T* Unwrap(Napi::Object wrapper) { 15 | // This check prevents memory unsafety should a user pass a wrapped object 16 | // of the wrong type as a function parameter 17 | if (!wrapper.CheckTypeTag(&T::typeTag)) { 18 | throw Napi::TypeError::New(wrapper.Env(), "Native object tag mismatch"); 19 | } 20 | return Napi::ObjectWrap::Unwrap(wrapper); 21 | } 22 | 23 | protected: 24 | void adjustMemory(Napi::Env env); 25 | 26 | private: 27 | int64_t lastSize = 0; 28 | 29 | virtual int64_t getCurrentSize() = 0; 30 | }; 31 | 32 | template 33 | void ObjectWrapHelper::Finalize(Napi::Env env) { 34 | Napi::MemoryManagement::AdjustExternalMemory(env, -lastSize); 35 | lastSize = 0; 36 | } 37 | 38 | template 39 | void ObjectWrapHelper::adjustMemory(Napi::Env env) { 40 | int64_t newSize = getCurrentSize(); 41 | if (newSize != lastSize) { 42 | Napi::MemoryManagement::AdjustExternalMemory(env, newSize - lastSize); 43 | lastSize = newSize; 44 | } 45 | } 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_H 2 | #define UTIL_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "zstd.h" 10 | 11 | static inline void checkArgCount(const Napi::CallbackInfo& info, size_t count) { 12 | if (info.Length() != count) { 13 | char errMsg[128]; 14 | snprintf(errMsg, sizeof(errMsg), "Expected %zd arguments, got %zd", count, 15 | info.Length()); 16 | throw Napi::TypeError::New(info.Env(), errMsg); 17 | } 18 | } 19 | 20 | static inline void checkZstdError(Napi::Env env, size_t ret) { 21 | if (ZSTD_isError(ret)) 22 | throw Napi::Error::New(env, ZSTD_getErrorName(ret)); 23 | } 24 | 25 | static inline Napi::Number convertZstdResult(Napi::Env env, size_t ret) { 26 | checkZstdError(env, ret); 27 | return Napi::Number::New(env, ret); 28 | } 29 | 30 | static inline ZSTD_inBuffer makeZstdInBuffer(Napi::Uint8Array& buf) { 31 | ZSTD_inBuffer result; 32 | result.src = buf.Data(); 33 | result.size = buf.ByteLength(); 34 | result.pos = 0; 35 | return result; 36 | } 37 | 38 | static inline ZSTD_outBuffer makeZstdOutBuffer(Napi::Uint8Array& buf) { 39 | ZSTD_outBuffer result; 40 | result.dst = buf.Data(); 41 | result.size = buf.ByteLength(); 42 | result.pos = 0; 43 | return result; 44 | } 45 | 46 | static inline Napi::Value makeStreamResult(Napi::Env env, 47 | size_t ret, 48 | ZSTD_outBuffer& outBuf, 49 | ZSTD_inBuffer& inBuf) { 50 | // NB: An array is slightly faster than constructing an object here, since 51 | // N-API doesn't expose the relevant V8 features to speed that up. 52 | Napi::Array result = Napi::Array::New(env, 3); 53 | result[uint32_t(0)] = convertZstdResult(env, ret); 54 | result[uint32_t(1)] = outBuf.pos; 55 | result[uint32_t(2)] = inBuf.pos; 56 | return result; 57 | } 58 | 59 | template 60 | using zstd_unique_ptr = 61 | std::unique_ptr>; 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /tests/binding.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { Worker } from 'worker_threads'; 4 | import * as binding from '../binding'; 5 | 6 | // Minimal dictionary (generated with zstd --train on random hex) 7 | const minDict = fs.readFileSync(path.join(__dirname, 'data', 'minimal.dct')); 8 | const minDictId = 598886516; 9 | 10 | function hex(data: string): Buffer { 11 | return Buffer.from(data, 'hex'); 12 | } 13 | 14 | // Minimal frame compressed with the minDict dictionary 15 | const minDictFrame = hex('28b52ffd237448b22300010000'); 16 | 17 | // Minimal frame with content size known (0 bytes) 18 | const minEmptyFrame = hex('28b52ffd2000010000'); 19 | 20 | // Minimal frame with content size unknown 21 | const minStreamFrame = hex('28b52ffd0000010000'); 22 | 23 | // Frame with content 'abc123' repeated five times 24 | const abcFrame = hex('28b52ffd201e650000306162633132330100014b11'); 25 | const abcDictFrame = hex('28b52ffd237448b2231e650000306162633132330100014b11'); 26 | const abcStreamFrame = hex('28b52ffd0058650000306162633132330100014b11'); 27 | const abcFrameContent = Buffer.from('abc123abc123abc123abc123abc123'); 28 | 29 | function expectCompress( 30 | input: Buffer, 31 | expected: Buffer, 32 | f: (dest: Buffer, src: Buffer) => number, 33 | ): void { 34 | const output = Buffer.alloc(binding.compressBound(input.length)); 35 | const len = f(output, input); 36 | expect(output.subarray(0, len).equals(expected)).toBe(true); 37 | } 38 | 39 | function expectDecompress( 40 | input: Buffer, 41 | expected: Buffer, 42 | f: (dest: Buffer, src: Buffer) => number, 43 | ): void { 44 | const output = Buffer.alloc(expected.length); 45 | const len = f(output, input); 46 | expect(output.subarray(0, len).equals(expected)).toBe(true); 47 | } 48 | 49 | describe('CCtx', () => { 50 | let cctx: binding.CCtx; 51 | 52 | beforeEach(() => { 53 | cctx = new binding.CCtx(); 54 | }); 55 | 56 | test('#compress works', () => { 57 | expectCompress(abcFrameContent, abcFrame, (dest, src) => 58 | cctx.compress(dest, src, 3), 59 | ); 60 | }); 61 | 62 | test('#compressUsingDict works', () => { 63 | expectCompress(abcFrameContent, abcDictFrame, (dest, src) => 64 | cctx.compressUsingDict(dest, src, minDict, 3), 65 | ); 66 | }); 67 | 68 | test('#compressUsingCDict works', () => { 69 | const cdict = new binding.CDict(minDict, 3); 70 | expectCompress(abcFrameContent, abcDictFrame, (dest, src) => 71 | cctx.compressUsingCDict(dest, src, cdict), 72 | ); 73 | }); 74 | 75 | test('#compressUsingCDict rejects invalid dictionary objects', () => { 76 | expect(() => { 77 | const ddict = new binding.DDict(minDict); 78 | // @ts-expect-error: testing invalid value 79 | cctx.compressUsingCDict(Buffer.alloc(0), Buffer.alloc(0), ddict); 80 | }).toThrowErrorMatchingInlineSnapshot(`"Native object tag mismatch"`); 81 | }); 82 | 83 | test('#setPledgedSrcSize works', () => { 84 | const srcBuf = Buffer.from('hello'); 85 | const dstBuf = Buffer.alloc(binding.compressBound(srcBuf.length)); 86 | cctx.setPledgedSrcSize(srcBuf.length + 1); 87 | cctx.compressStream2(dstBuf, srcBuf, binding.EndDirective.continue); 88 | expect(() => { 89 | cctx.compressStream2(dstBuf, Buffer.alloc(0), binding.EndDirective.end); 90 | }).toThrowErrorMatchingInlineSnapshot(`"Src size is incorrect"`); 91 | }); 92 | 93 | test('#reset works', () => { 94 | const srcBuf = Buffer.from('hello'); 95 | const dstBuf = Buffer.alloc(binding.compressBound(srcBuf.length * 2)); 96 | const [, dstProd] = cctx.compressStream2( 97 | dstBuf, 98 | srcBuf, 99 | binding.EndDirective.continue, 100 | ); 101 | expect(dstProd).toBe(0); 102 | cctx.reset(binding.ResetDirective.sessionAndParameters); 103 | const [, dstProd2] = cctx.compressStream2( 104 | dstBuf, 105 | srcBuf, 106 | binding.EndDirective.end, 107 | ); 108 | expectDecompress(dstBuf.subarray(0, dstProd2), srcBuf, binding.decompress); 109 | }); 110 | 111 | test('#compress2 works', () => { 112 | cctx.setParameter(binding.CParameter.contentSizeFlag, 0); 113 | cctx.setParameter(binding.CParameter.windowLog, 10); 114 | expectCompress(Buffer.alloc(0), minStreamFrame, (dst, src) => 115 | cctx.compress2(dst, src), 116 | ); 117 | }); 118 | 119 | test('#compressStream2 works', () => { 120 | const output = Buffer.alloc(abcStreamFrame.length); 121 | let [toFlush, dstProduced, srcConsumed] = cctx.compressStream2( 122 | output, 123 | abcFrameContent, 124 | binding.EndDirective.continue, 125 | ); 126 | expect(toFlush).toBe(0); 127 | expect(dstProduced).toBe(0); 128 | expect(srcConsumed).toBe(abcFrameContent.length); 129 | [toFlush, dstProduced, srcConsumed] = cctx.compressStream2( 130 | output, 131 | Buffer.alloc(0), 132 | binding.EndDirective.end, 133 | ); 134 | expect(toFlush).toBe(0); 135 | expect(dstProduced).toBe(output.length); 136 | expect(srcConsumed).toBe(0); 137 | expect(output.equals(abcStreamFrame)).toBe(true); 138 | }); 139 | 140 | test('#loadDictionary works', () => { 141 | cctx.loadDictionary(minDict); 142 | expectCompress(abcFrameContent, abcDictFrame, (dst, src) => 143 | cctx.compress2(dst, src), 144 | ); 145 | }); 146 | }); 147 | 148 | describe('CDict', () => { 149 | test('constructor errors on corrupt dictionary', () => { 150 | expect(() => { 151 | new binding.CDict(minDict.subarray(0, 32), 3); 152 | }).toThrow('Failed to create CDict'); 153 | }); 154 | 155 | test('#getDictID works', () => { 156 | const cdict = new binding.CDict(minDict, 3); 157 | expect(cdict.getDictID()).toBe(minDictId); 158 | }); 159 | }); 160 | 161 | describe('DCtx', () => { 162 | let dctx: binding.DCtx; 163 | 164 | beforeEach(() => { 165 | dctx = new binding.DCtx(); 166 | }); 167 | 168 | test('#decompress works', () => { 169 | expectDecompress(abcFrame, abcFrameContent, (output) => 170 | dctx.decompress(output, abcFrame), 171 | ); 172 | }); 173 | 174 | test('#decompressStream works', () => { 175 | const output = Buffer.alloc(abcFrameContent.length); 176 | let [inputHint, dstProduced, srcConsumed] = dctx.decompressStream( 177 | output, 178 | abcStreamFrame.subarray(0, 12), 179 | ); 180 | expect(inputHint).toBe(abcStreamFrame.length - 12); 181 | expect(dstProduced).toBe(0); 182 | expect(srcConsumed).toBe(12); 183 | [inputHint, dstProduced, srcConsumed] = dctx.decompressStream( 184 | output, 185 | abcStreamFrame.subarray(srcConsumed), 186 | ); 187 | expect(inputHint).toBe(0); 188 | expect(dstProduced).toBe(abcFrameContent.length); 189 | expect(srcConsumed).toBe(abcStreamFrame.length - 12); 190 | expect(output.equals(abcFrameContent)).toBe(true); 191 | }); 192 | 193 | test('#decompressUsingDict works', () => { 194 | expectDecompress(abcDictFrame, abcFrameContent, (output) => 195 | dctx.decompressUsingDict(output, abcDictFrame, minDict), 196 | ); 197 | }); 198 | 199 | test('#decompressUsingDDict works', () => { 200 | const ddict = new binding.DDict(minDict); 201 | expectDecompress(abcDictFrame, abcFrameContent, (output) => 202 | dctx.decompressUsingDDict(output, abcDictFrame, ddict), 203 | ); 204 | }); 205 | 206 | test('#decompressUsingDDict rejects invalid dictionary objects', () => { 207 | expect(() => { 208 | const cdict = new binding.CDict(minDict, 3); 209 | // @ts-expect-error: testing invalid value 210 | dctx.decompressUsingDDict(Buffer.alloc(0), Buffer.alloc(0), cdict); 211 | }).toThrowErrorMatchingInlineSnapshot(`"Native object tag mismatch"`); 212 | }); 213 | 214 | test('#setParameter works', () => { 215 | dctx.setParameter(binding.DParameter.windowLogMax, 10); 216 | const { upperBound } = binding.dParamGetBounds( 217 | binding.DParameter.windowLogMax, 218 | ); 219 | expect(() => { 220 | dctx.setParameter(binding.DParameter.windowLogMax, upperBound + 1); 221 | }).toThrowErrorMatchingInlineSnapshot(`"Parameter is out of bound"`); 222 | }); 223 | 224 | test('#reset works', () => { 225 | const dstBuf = Buffer.alloc(1); 226 | const [, , consumed1] = dctx.decompressStream( 227 | dstBuf, 228 | minEmptyFrame.subarray(0, 4), 229 | ); 230 | expect(consumed1).toBe(4); 231 | dctx.reset(binding.ResetDirective.sessionAndParameters); 232 | const [, , consumed2] = dctx.decompressStream(dstBuf, minEmptyFrame); 233 | expect(consumed2).toBe(minEmptyFrame.length); 234 | }); 235 | 236 | test('#loadDictionary works', () => { 237 | dctx.loadDictionary(minDict); 238 | expectDecompress(abcDictFrame, abcFrameContent, (dst, src) => 239 | dctx.decompress(dst, src), 240 | ); 241 | }); 242 | }); 243 | 244 | describe('DDict', () => { 245 | test('constructor errors on corrupt dictionary', () => { 246 | expect(() => { 247 | new binding.DDict(minDict.subarray(0, 32)); 248 | }).toThrow('Failed to create DDict'); 249 | }); 250 | 251 | test('#getDictID works', () => { 252 | const ddict = new binding.DDict(minDict); 253 | expect(ddict.getDictID()).toBe(minDictId); 254 | }); 255 | }); 256 | 257 | test('versionString works', () => { 258 | expect(binding.versionString()).toBe('1.5.7'); 259 | }); 260 | 261 | test('versionNumber works', () => { 262 | expect(binding.versionNumber()).toBe(10507); 263 | }); 264 | 265 | test('compress works', () => { 266 | expectCompress(abcFrameContent, abcFrame, (dest, src) => 267 | binding.compress(dest, src, 3), 268 | ); 269 | }); 270 | 271 | test('decompress works', () => { 272 | expectDecompress(abcFrame, abcFrameContent, (dest, src) => 273 | binding.decompress(dest, src), 274 | ); 275 | }); 276 | 277 | describe('getFrameContentSize', () => { 278 | test('works on normal frames', () => { 279 | expect(binding.getFrameContentSize(minEmptyFrame)).toBe(0); 280 | }); 281 | test('returns null when size is unknown', () => { 282 | expect(binding.getFrameContentSize(minStreamFrame)).toBeNull(); 283 | }); 284 | test('throws error when frame is corrupt', () => { 285 | expect(() => { 286 | binding.getFrameContentSize(minEmptyFrame.subarray(0, 4)); 287 | }).toThrowErrorMatchingInlineSnapshot(`"Could not parse Zstandard header"`); 288 | }); 289 | }); 290 | 291 | test('findFrameCompressedSize works', () => { 292 | expect(binding.findFrameCompressedSize(minEmptyFrame)).toBe( 293 | minEmptyFrame.length, 294 | ); 295 | }); 296 | 297 | test('compressBound works', () => { 298 | expect(binding.compressBound(0)).toBeGreaterThanOrEqual(minEmptyFrame.length); 299 | }); 300 | 301 | test('minCLevel works', () => { 302 | expect(binding.minCLevel()).toBeLessThan(0); 303 | }); 304 | 305 | test('maxCLevel works', () => { 306 | expect(binding.maxCLevel()).toBeGreaterThan(0); 307 | }); 308 | 309 | test('defaultCLevel works', () => { 310 | expect(binding.defaultCLevel()).toBe(3); 311 | }); 312 | 313 | function expectBounds(bounds: binding.Bounds): void { 314 | expect(bounds).toMatchObject({ 315 | lowerBound: expect.any(Number), 316 | upperBound: expect.any(Number), 317 | }); 318 | expect(bounds.lowerBound).toBeLessThanOrEqual(bounds.upperBound); 319 | } 320 | 321 | test('cParamGetBounds works', () => { 322 | for (const v of Object.values(binding.CParameter)) { 323 | if (typeof v === 'number') { 324 | expectBounds(binding.cParamGetBounds(v)); 325 | } 326 | } 327 | }); 328 | 329 | test('dParamGetBounds works', () => { 330 | for (const v of Object.values(binding.DParameter)) { 331 | if (typeof v === 'number') { 332 | expectBounds(binding.dParamGetBounds(v)); 333 | } 334 | } 335 | }); 336 | 337 | test('cStreamInSize works', () => { 338 | expect(binding.cStreamInSize()).toBeGreaterThan(0); 339 | }); 340 | 341 | test('cStreamOutSize works', () => { 342 | expect(binding.cStreamOutSize()).toBeGreaterThan(0); 343 | }); 344 | 345 | test('dStreamInSize works', () => { 346 | expect(binding.dStreamInSize()).toBeGreaterThan(0); 347 | }); 348 | 349 | test('dStreamOutSize works', () => { 350 | expect(binding.dStreamOutSize()).toBeGreaterThan(0); 351 | }); 352 | 353 | test('getDictIDFromDict works', () => { 354 | expect(binding.getDictIDFromDict(minDict)).toBe(minDictId); 355 | }); 356 | 357 | test('wrapGetDictIDFromFrame works', () => { 358 | expect(binding.getDictIDFromFrame(minDictFrame)).toBe(minDictId); 359 | }); 360 | 361 | test('loading from multiple threads works', async () => { 362 | async function runInWorker(): Promise { 363 | return new Promise((resolve, reject) => 364 | new Worker('require("./binding")', { eval: true }) 365 | .on('error', reject) 366 | .on('exit', resolve), 367 | ); 368 | } 369 | 370 | expect(await runInWorker()).toBe(0); 371 | expect(await runInWorker()).toBe(0); 372 | }); 373 | 374 | test('passing wrong argument count throws error', () => { 375 | expect(() => { 376 | // @ts-expect-error: deliberately passing wrong arguments 377 | binding.compressBound(); 378 | }).toThrowErrorMatchingInlineSnapshot(`"Expected 1 arguments, got 0"`); 379 | }); 380 | 381 | test('libzstd errors are propagated', () => { 382 | expect(() => { 383 | binding.compress(Buffer.alloc(0), Buffer.alloc(0), 3); 384 | }).toThrowErrorMatchingInlineSnapshot(`"Destination buffer is too small"`); 385 | }); 386 | -------------------------------------------------------------------------------- /tests/data/minimal.dct: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakedevel/zstd-napi/6858e169dfea61ab2c51e6c8e77dccf2c64f2827/tests/data/minimal.dct -------------------------------------------------------------------------------- /tests/fast_check.ts: -------------------------------------------------------------------------------- 1 | /* eslint jest/no-standalone-expect: 0 */ 2 | import { fc, it } from '@fast-check/jest'; 3 | import { strict as assert } from 'assert'; 4 | import * as events from 'events'; 5 | 6 | import { 7 | Compressor, 8 | CompressStream, 9 | Decompressor, 10 | DecompressStream, 11 | } from '../lib'; 12 | 13 | // Generate some deterministic random data up front to slice from. This is 14 | // much faster than generating data directly with fast-check, and allows for 15 | // some compression to actually happen. 16 | const dataPool: Buffer = (() => { 17 | const arrLen = (1024 * 1024) / 4; 18 | const [arr] = fc.sample( 19 | fc.uint32Array({ minLength: arrLen, maxLength: arrLen }).noBias(), 20 | { numRuns: 1, seed: 0 }, 21 | ); 22 | assert(arr); 23 | return Buffer.from(arr.buffer); 24 | })(); 25 | 26 | function arbSubarray(pool: Buffer) { 27 | return fc 28 | .nat(pool.length - 1) 29 | .noBias() 30 | .chain((start) => 31 | fc 32 | .nat(pool.length - start) 33 | .map((length) => pool.subarray(start, start + length)), 34 | ); 35 | } 36 | 37 | describe('single-pass API', () => { 38 | const arbData = fc.oneof( 39 | arbSubarray(dataPool), 40 | arbSubarray(dataPool).map((s) => Buffer.from(s.toString('base64'))), 41 | ); 42 | const arbOpts = fc.oneof( 43 | fc.constant(undefined), 44 | fc.constant({ contentSizeFlag: false }), 45 | ); 46 | 47 | it.prop([fc.array(fc.tuple(arbData, arbOpts))])('should roundtrip', (ops) => { 48 | // Run the ops through each of the compressor and decompressor. Delay 49 | // checking results as long as possible to allow bugs in buffer re-use 50 | // to be detected. 51 | const cmp = new Compressor(); 52 | const dec = new Decompressor(); 53 | const cmpResults = []; 54 | for (const [data, opts] of ops) { 55 | if (opts) { 56 | cmp.setParameters(opts); 57 | } 58 | cmpResults.push({ data, compressed: cmp.compress(data) }); 59 | } 60 | const decResults = []; 61 | for (const { data, compressed } of cmpResults) { 62 | const decompressed = dec.decompress(compressed); 63 | decResults.push({ data, decompressed }); 64 | } 65 | for (const { data, decompressed } of decResults) { 66 | expect(decompressed.equals(data)).toBe(true); 67 | } 68 | }); 69 | }); 70 | 71 | describe('streaming API', () => { 72 | // Generate a mixture of calls to the stream 73 | const arbOp = fc.oneof( 74 | { 75 | arbitrary: arbSubarray(dataPool).map((slice) => ({ write: slice })), 76 | weight: 10, 77 | }, 78 | fc.constant({ flush: {} }), 79 | fc.constant({ endFrame: {} }), 80 | ); 81 | 82 | it.prop([fc.array(arbOp, { maxLength: 25 })])( 83 | 'should roundtrip', 84 | async (ops) => { 85 | // Run each operation on the pipeline 86 | const cmp = new CompressStream(); 87 | const dec = new DecompressStream(); 88 | const input: Buffer[] = []; 89 | const output: Buffer[] = []; 90 | cmp.pipe(dec); 91 | dec.on('data', (chunk: Buffer) => output.push(chunk)); 92 | for (const op of ops) { 93 | if ('write' in op) { 94 | input.push(op.write); 95 | cmp.write(op.write); 96 | } else if ('flush' in op) { 97 | cmp.flush(); 98 | } else if ('endFrame' in op) { 99 | cmp.endFrame(); 100 | } 101 | } 102 | cmp.end(); 103 | await events.once(dec, 'end'); 104 | 105 | // Verify the output matches 106 | const actual = Buffer.concat(output); 107 | const expected = Buffer.concat(input); 108 | expect(actual.equals(expected)).toBe(true); 109 | }, 110 | ); 111 | }); 112 | -------------------------------------------------------------------------------- /tests/hl_compress.ts: -------------------------------------------------------------------------------- 1 | /* eslint jest/no-done-callback: 0 */ 2 | import { strict as assert } from 'assert'; 3 | import { randomBytes } from 'crypto'; 4 | import { expectTypeOf } from 'expect-type'; 5 | import * as binding from '../binding'; 6 | import { 7 | Compressor, 8 | CompressParameters, 9 | CompressStream, 10 | compress, 11 | decompress, 12 | } from '../lib'; 13 | 14 | const mockBinding: jest.Mocked = 15 | jest.createMockFromModule('../binding'); 16 | 17 | function expectDecompress(input: Buffer, expected: Buffer): void { 18 | const output = decompress(input); 19 | expect(output.equals(expected)).toBe(true); 20 | } 21 | 22 | describe('Compressor', () => { 23 | let compressor: Compressor; 24 | 25 | beforeEach(() => { 26 | jest.clearAllMocks(); 27 | compressor = new Compressor(); 28 | }); 29 | 30 | test('#compress compresses data', () => { 31 | const input = Buffer.from('hello'); 32 | const output = compressor.compress(input); 33 | expectDecompress(output, input); 34 | }); 35 | 36 | test('#compress scratch buffer can be re-used', () => { 37 | expect(compressor['scratchBuf']).toBeNull(); 38 | 39 | // Verify opportunistic buffer-saving 40 | const input1 = Buffer.from('hello'); 41 | const output1 = compressor.compress(input1); 42 | expectDecompress(output1, input1); 43 | expect(compressor['scratchBuf']).not.toBeNull(); 44 | const scratch1 = compressor['scratchBuf']; 45 | 46 | // Verify scratch buffer re-use 47 | const output2 = compressor.compress(input1); 48 | expectDecompress(output2, input1); 49 | expect(compressor['scratchBuf']).toBe(scratch1); 50 | 51 | // Verify scratch buffer is preserved if not used 52 | const input3 = randomBytes(128 * 1024 + 1); 53 | const output3 = compressor.compress(input3); 54 | expectDecompress(output3, input3); 55 | expect(compressor['scratchBuf']).toBe(scratch1); 56 | }); 57 | 58 | test('#compress scratch buffer can be stolen', () => { 59 | expect(compressor['scratchBuf']).toBeNull(); 60 | 61 | // Prime with highly-compressible data to ensure buffer is saved 62 | compressor.compress(Buffer.alloc(256)); 63 | expect(compressor['scratchBuf']).not.toBeNull(); 64 | const scratch = compressor['scratchBuf']; 65 | 66 | // Compress incompressible data to fill scratch buffer 67 | const input = randomBytes(256); 68 | const output = compressor.compress(input); 69 | expectDecompress(output, input); 70 | 71 | // Verify buffer was stolen 72 | expect(compressor['scratchBuf']).toBeNull(); 73 | expect(output.buffer).toBe(scratch?.buffer); 74 | }); 75 | 76 | test('#loadDictionary works', () => { 77 | compressor['cctx'] = new mockBinding.CCtx(); 78 | 79 | const dictBuf = Buffer.alloc(0); 80 | compressor.loadDictionary(dictBuf); 81 | expect(mockBinding.CCtx.prototype.loadDictionary).toHaveBeenCalledWith( 82 | dictBuf, 83 | ); 84 | }); 85 | 86 | test('#setParameters resets other parameters', () => { 87 | compressor['cctx'] = new mockBinding.CCtx(); 88 | 89 | compressor.setParameters({ compressionLevel: 0 }); 90 | expect(mockBinding.CCtx.prototype.reset).toHaveBeenCalledWith( 91 | binding.ResetDirective.parameters, 92 | ); 93 | expect(mockBinding.CCtx.prototype.setParameter).toHaveBeenCalledWith( 94 | binding.CParameter.compressionLevel, 95 | 0, 96 | ); 97 | }); 98 | 99 | test('#updateParameters does not reset parameters', () => { 100 | compressor['cctx'] = new mockBinding.CCtx(); 101 | 102 | compressor.updateParameters({ compressionLevel: 0 }); 103 | expect(mockBinding.CCtx.prototype.reset).not.toHaveBeenCalled(); 104 | expect(mockBinding.CCtx.prototype.setParameter).toHaveBeenCalled(); 105 | }); 106 | 107 | test('#updateParameters maps parameters correctly', () => { 108 | compressor['cctx'] = new mockBinding.CCtx(); 109 | 110 | // Set one parameter of each type 111 | compressor.updateParameters({ 112 | compressionLevel: 9, 113 | enableLongDistanceMatching: true, 114 | strategy: 'lazy', 115 | }); 116 | 117 | // Verify types got mapped correctly 118 | const setParam = mockBinding.CCtx.prototype.setParameter; 119 | expect(setParam).toHaveBeenCalledTimes(3); 120 | expect(setParam).toHaveBeenCalledWith( 121 | binding.CParameter.compressionLevel, 122 | 9, 123 | ); 124 | expect(setParam).toHaveBeenCalledWith( 125 | binding.CParameter.enableLongDistanceMatching, 126 | 1, 127 | ); 128 | expect(setParam).toHaveBeenCalledWith( 129 | binding.CParameter.strategy, 130 | binding.Strategy.lazy, 131 | ); 132 | }); 133 | 134 | test('#updateParameters rejects invalid parameter names/types', () => { 135 | compressor['cctx'] = new mockBinding.CCtx(); 136 | 137 | expect(() => { 138 | // @ts-expect-error: deliberately passing wrong arguments 139 | compressor.updateParameters({ invalidName: 42 }); 140 | }).toThrowErrorMatchingInlineSnapshot( 141 | `"Invalid parameter name: invalidName"`, 142 | ); 143 | expect(() => { 144 | // @ts-expect-error: deliberately passing wrong arguments 145 | compressor.updateParameters({ compressionLevel: 'invalid' }); 146 | }).toThrowErrorMatchingInlineSnapshot( 147 | `"Invalid type for parameter: compressionLevel"`, 148 | ); 149 | expect(mockBinding.CCtx.prototype.setParameter).not.toHaveBeenCalled(); 150 | }); 151 | 152 | test('#updateParameters ignores undefined values', () => { 153 | compressor['cctx'] = new mockBinding.CCtx(); 154 | 155 | compressor.updateParameters({ compressionLevel: undefined }); 156 | expect(mockBinding.CCtx.prototype.setParameter).not.toHaveBeenCalled(); 157 | }); 158 | }); 159 | 160 | describe('CompressParameters', () => { 161 | test('matches binding.CParameter', () => { 162 | expectTypeOf().toEqualTypeOf< 163 | keyof typeof binding.CParameter 164 | >(); 165 | }); 166 | }); 167 | 168 | describe('CompressStream', () => { 169 | let stream: CompressStream; 170 | let chunks: Buffer[]; 171 | const dataHandler = jest.fn((chunk: Buffer) => chunks.push(chunk)); 172 | const errorHandler = jest.fn(); 173 | 174 | beforeEach(() => { 175 | jest.clearAllMocks(); 176 | chunks = []; 177 | stream = new CompressStream(); 178 | stream.on('data', dataHandler); 179 | stream.on('error', errorHandler); 180 | }); 181 | 182 | afterEach(() => { 183 | // TODO: Determine if this is supposed to be legal 184 | // eslint-disable-next-line jest/no-standalone-expect 185 | expect(errorHandler).not.toHaveBeenCalled(); 186 | }); 187 | 188 | test('basic functionality works', (done) => { 189 | stream.on('end', () => { 190 | expectDecompress(Buffer.concat(chunks), Buffer.from('hello')); 191 | return done(); 192 | }); 193 | 194 | stream.end('hello'); 195 | }); 196 | 197 | test('#endFrame ends the frame at the correct point', (done) => { 198 | stream.on('end', () => { 199 | const result = Buffer.concat(chunks); 200 | 201 | // Verify two frames were emitted 202 | const firstFrameLen = binding.findFrameCompressedSize(result); 203 | expect(firstFrameLen).toBeLessThan(result.length); 204 | const firstFrame = result.subarray(0, firstFrameLen); 205 | const lastFrame = result.subarray(firstFrameLen); 206 | const lastFrameLen = binding.findFrameCompressedSize(lastFrame); 207 | expect(lastFrameLen).toBe(lastFrame.length); 208 | 209 | // Verify frame boundary is in the correct place 210 | expectDecompress(firstFrame, Buffer.from('hello')); 211 | expectDecompress(lastFrame, Buffer.from('world')); 212 | 213 | return done(); 214 | }); 215 | 216 | stream.write('hello'); 217 | stream.endFrame(); 218 | stream.end('world'); 219 | }); 220 | 221 | test('#endFrame flushes', (done) => { 222 | stream.write('hello', () => { 223 | // Verify nothing is written until we call flush 224 | expect(chunks).toHaveLength(0); 225 | stream.endFrame(() => { 226 | expect(chunks).toHaveLength(1); 227 | const [result] = chunks; 228 | assert.ok(result); 229 | 230 | // Verify exactly one frame was emitted 231 | const frameLen = binding.findFrameCompressedSize(result); 232 | expect(frameLen).toBe(result.length); 233 | 234 | // Verify data decompresses correctly 235 | expectDecompress(result, Buffer.from('hello')); 236 | 237 | return done(); 238 | }); 239 | }); 240 | }); 241 | 242 | test('#flush flushes but does not end frame', (done) => { 243 | stream.on('end', () => { 244 | const result = Buffer.concat(chunks); 245 | 246 | // One more chunk should have been emitted to end the frame 247 | // It should be an empty block (3 bytes) 248 | expect(chunks).toHaveLength(2); 249 | expect(chunks[1]).toHaveLength(3); 250 | 251 | // Verify only one frame was emitted 252 | const frameLen = binding.findFrameCompressedSize(result); 253 | expect(frameLen).toBe(result.length); 254 | 255 | // Verify data decompresses correctly 256 | expectDecompress(result, Buffer.from('hello')); 257 | 258 | return done(); 259 | }); 260 | 261 | stream.write('hello', () => { 262 | // Verify nothing is written until we call flush 263 | expect(chunks).toHaveLength(0); 264 | stream.flush(() => { 265 | expect(chunks).toHaveLength(1); 266 | stream.end(); 267 | }); 268 | }); 269 | }); 270 | 271 | test('#_transform correctly propagates errors', (done) => { 272 | mockBinding.CCtx.prototype.compressStream2.mockImplementationOnce(() => { 273 | throw new Error('Simulated error'); 274 | }); 275 | stream['cctx'] = new mockBinding.CCtx(); 276 | 277 | const writeCb = jest.fn(); 278 | stream.off('error', errorHandler); 279 | stream.on('error', (err) => { 280 | expect(err).toMatchObject({ message: 'Simulated error' }); 281 | expect(writeCb).toHaveBeenCalledWith(err); 282 | return done(); 283 | }); 284 | stream.write('', writeCb); 285 | }); 286 | 287 | test('#_flush correctly propagates errors', (done) => { 288 | mockBinding.CCtx.prototype.compressStream2.mockImplementationOnce(() => { 289 | throw new Error('Simulated error'); 290 | }); 291 | stream['cctx'] = new mockBinding.CCtx(); 292 | 293 | stream.off('error', errorHandler); 294 | stream.on('error', (err) => { 295 | expect(err).toMatchObject(new Error('Simulated error')); 296 | return done(); 297 | }); 298 | 299 | stream.end(); 300 | }); 301 | 302 | test('handles input larger than buffer size', (done) => { 303 | // Generate incompressible input that's larger than the buffer 304 | const input = randomBytes(binding.cStreamInSize() * 2 + 1); 305 | 306 | stream.on('end', () => { 307 | // Verify data decompresses correctly 308 | expectDecompress(Buffer.concat(chunks), input); 309 | return done(); 310 | }); 311 | 312 | stream.write(input, () => { 313 | // Verify we actually got two blocks out of one write 314 | expect(chunks).toHaveLength(2); 315 | stream.end(); 316 | }); 317 | }); 318 | }); 319 | 320 | describe('compress', () => { 321 | test('basic functionality works', () => { 322 | const input = Buffer.from('hello'); 323 | expectDecompress(compress(input), input); 324 | }); 325 | }); 326 | -------------------------------------------------------------------------------- /tests/hl_decompress.ts: -------------------------------------------------------------------------------- 1 | /* eslint jest/no-done-callback: 0 */ 2 | import { randomBytes } from 'crypto'; 3 | import { expectTypeOf } from 'expect-type'; 4 | import * as binding from '../binding'; 5 | import { 6 | Decompressor, 7 | DecompressParameters, 8 | DecompressStream, 9 | compress, 10 | decompress, 11 | } from '../lib'; 12 | 13 | const mockBinding: jest.Mocked = 14 | jest.createMockFromModule('../binding'); 15 | 16 | describe('Decompressor', () => { 17 | let decompressor: Decompressor; 18 | 19 | beforeEach(() => { 20 | jest.clearAllMocks(); 21 | decompressor = new Decompressor(); 22 | }); 23 | 24 | test('#decompress handles frames with content size', () => { 25 | const original = Buffer.from('hello'); 26 | const input = compress(Buffer.from('hello')); 27 | expect(binding.getFrameContentSize(input)).toBe(original.length); 28 | expect(decompressor.decompress(input).equals(original)).toBe(true); 29 | }); 30 | 31 | test('#decompress handles frames without content size', () => { 32 | const original = Buffer.from('hello'); 33 | const input = compress(Buffer.from('hello'), { contentSizeFlag: false }); 34 | expect(binding.getFrameContentSize(input)).toBeNull(); 35 | expect(decompressor.decompress(input).equals(original)).toBe(true); 36 | }); 37 | 38 | test('#decompress decompresses all frames in input', () => { 39 | const originals = [Buffer.from('hello'), Buffer.from(' world')]; 40 | const frames = originals.map((b) => 41 | compress(b, { contentSizeFlag: false }), 42 | ); 43 | const input = Buffer.concat(frames); 44 | const expected = Buffer.concat(originals); 45 | expect(decompressor.decompress(input).equals(expected)).toBe(true); 46 | }); 47 | 48 | test('#decompress handles large inputs without content size', () => { 49 | // 1MiB of random data with roughly 2x compression ratio 50 | const chunks = []; 51 | for (let i = 0; i < 1024; i++) { 52 | const chunk = randomBytes(1024); 53 | chunks.push(chunk, chunk); 54 | } 55 | const original = Buffer.concat(chunks); 56 | const input = compress(original, { contentSizeFlag: false }); 57 | expect(original.length / input.length).toBeCloseTo(2, 1); 58 | expect(decompressor.decompress(input).equals(original)).toBe(true); 59 | }); 60 | 61 | test('#decompress handles high-ratio inputs without content size', () => { 62 | // 1MiB of zeros, which has a very high compression ratio 63 | const original = Buffer.alloc(1024 * 1024); 64 | const input = compress(original, { contentSizeFlag: false }); 65 | expect(input.length).toBeLessThan(128); 66 | expect(decompressor.decompress(input).equals(original)).toBe(true); 67 | }); 68 | 69 | test('#loadDictionary works', () => { 70 | decompressor['dctx'] = new mockBinding.DCtx(); 71 | 72 | const dictBuf = Buffer.alloc(0); 73 | decompressor.loadDictionary(dictBuf); 74 | expect(mockBinding.DCtx.prototype.loadDictionary).toHaveBeenCalledWith( 75 | dictBuf, 76 | ); 77 | }); 78 | 79 | test('#setParameters resets other parameters', () => { 80 | decompressor['dctx'] = new mockBinding.DCtx(); 81 | 82 | decompressor.setParameters({ windowLogMax: 10 }); 83 | expect(mockBinding.DCtx.prototype.reset).toHaveBeenCalledWith( 84 | binding.ResetDirective.parameters, 85 | ); 86 | expect(mockBinding.DCtx.prototype.setParameter).toHaveBeenCalledWith( 87 | binding.DParameter.windowLogMax, 88 | 10, 89 | ); 90 | }); 91 | 92 | test('#updateParameters does not reset parameters', () => { 93 | decompressor['dctx'] = new mockBinding.DCtx(); 94 | 95 | decompressor.updateParameters({ windowLogMax: 0 }); 96 | expect(mockBinding.DCtx.prototype.reset).not.toHaveBeenCalled(); 97 | expect(mockBinding.DCtx.prototype.setParameter).toHaveBeenCalled(); 98 | }); 99 | 100 | test('#updateParameters rejects invalid parameter names', () => { 101 | decompressor['dctx'] = new mockBinding.DCtx(); 102 | 103 | expect(() => { 104 | // @ts-expect-error: testing invalid key 105 | decompressor.updateParameters({ invalidName: 42 }); 106 | }).toThrowErrorMatchingInlineSnapshot( 107 | `"Invalid parameter name: invalidName"`, 108 | ); 109 | expect(() => { 110 | // @ts-expect-error: testing invalid value type 111 | decompressor.updateParameters({ windowLogMax: 'invalid' }); 112 | }).toThrowErrorMatchingInlineSnapshot( 113 | `"Invalid type for parameter: windowLogMax"`, 114 | ); 115 | expect(mockBinding.DCtx.prototype.setParameter).not.toHaveBeenCalled(); 116 | }); 117 | 118 | test('#updateParameters ignores undefined values', () => { 119 | decompressor['dctx'] = new mockBinding.DCtx(); 120 | 121 | decompressor.updateParameters({ windowLogMax: undefined }); 122 | expect(mockBinding.DCtx.prototype.setParameter).not.toHaveBeenCalled(); 123 | }); 124 | }); 125 | 126 | describe('DecompressParameters', () => { 127 | test('matches binding.DParameter', () => { 128 | expectTypeOf().toEqualTypeOf< 129 | keyof typeof binding.DParameter 130 | >(); 131 | }); 132 | }); 133 | 134 | describe('DecompressStream', () => { 135 | let stream: DecompressStream; 136 | let chunks: Buffer[]; 137 | const dataHandler = jest.fn((chunk: Buffer) => chunks.push(chunk)); 138 | const errorHandler = jest.fn(); 139 | 140 | beforeEach(() => { 141 | jest.clearAllMocks(); 142 | chunks = []; 143 | stream = new DecompressStream(); 144 | stream.on('data', dataHandler); 145 | stream.on('error', errorHandler); 146 | }); 147 | 148 | afterEach(() => { 149 | // TODO: Determine if this is supposed to be legal 150 | // eslint-disable-next-line jest/no-standalone-expect 151 | expect(errorHandler).not.toHaveBeenCalled(); 152 | }); 153 | 154 | test('basic functionality works', (done) => { 155 | const original = Buffer.from('hello'); 156 | 157 | stream.on('end', () => { 158 | expect(Buffer.concat(chunks).equals(original)).toBe(true); 159 | return done(); 160 | }); 161 | 162 | stream.end(compress(original)); 163 | }); 164 | 165 | test('#_transform correctly propagates errors', (done) => { 166 | mockBinding.DCtx.prototype.decompressStream.mockImplementationOnce(() => { 167 | throw new Error('Simulated error'); 168 | }); 169 | stream['dctx'] = new mockBinding.DCtx(); 170 | 171 | const writeCb = jest.fn(); 172 | stream.off('error', errorHandler); 173 | stream.on('error', (err) => { 174 | expect(err).toMatchObject({ message: 'Simulated error' }); 175 | expect(writeCb).toHaveBeenCalledWith(err); 176 | return done(); 177 | }); 178 | stream.write('', writeCb); 179 | }); 180 | 181 | test('#_flush fails if in the middle of a frame', (done) => { 182 | const input = compress(Buffer.from('hello')); 183 | 184 | stream.off('error', errorHandler); 185 | stream.on('error', (err) => { 186 | expect(err).toMatchInlineSnapshot( 187 | `[Error: Stream ended in middle of compressed data frame]`, 188 | ); 189 | return done(); 190 | }); 191 | 192 | stream.end(input.subarray(0, input.length - 1)); 193 | }); 194 | 195 | test('flushes complete frames eagerly', (done) => { 196 | const orig1 = Buffer.from('hello'); 197 | const orig2 = Buffer.from(' world'); 198 | const original = Buffer.concat([orig1, orig2]); 199 | stream.write(compress(orig1), () => { 200 | expect(Buffer.concat(chunks).equals(orig1)).toBe(true); 201 | stream.write(compress(orig2), () => { 202 | expect(Buffer.concat(chunks).equals(original)).toBe(true); 203 | stream.end(); 204 | return done(); 205 | }); 206 | }); 207 | }); 208 | 209 | test('handles multiple frames in single write', (done) => { 210 | const orig1 = Buffer.from('hello'); 211 | const orig2 = Buffer.from(' world'); 212 | const original = Buffer.concat([orig1, orig2]); 213 | stream.on('end', () => { 214 | expect(Buffer.concat(chunks).equals(original)).toBe(true); 215 | return done(); 216 | }); 217 | 218 | stream.end(Buffer.concat([compress(orig1), compress(orig2)])); 219 | }); 220 | 221 | test('handles output that exactly matches buffer size', (done) => { 222 | const original = randomBytes(binding.dStreamOutSize()); 223 | 224 | stream.on('end', () => { 225 | expect(Buffer.concat(chunks).equals(original)).toBe(true); 226 | return done(); 227 | }); 228 | 229 | stream.write(compress(original), () => { 230 | // Verify we only got one block 231 | expect(chunks).toHaveLength(1); 232 | stream.end(); 233 | }); 234 | }); 235 | 236 | test('handles output larger than buffer size', (done) => { 237 | const original = randomBytes(binding.dStreamOutSize() + 1); 238 | 239 | stream.on('end', () => { 240 | expect(Buffer.concat(chunks).equals(original)).toBe(true); 241 | return done(); 242 | }); 243 | 244 | stream.write(compress(original), () => { 245 | // Verify we actually got two blocks out of one write 246 | expect(chunks).toHaveLength(2); 247 | stream.end(); 248 | }); 249 | }); 250 | }); 251 | 252 | describe('decompress', () => { 253 | test('basic functionality works', () => { 254 | const original = Buffer.from('hello'); 255 | expect(decompress(compress(original)).equals(original)).toBe(true); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /tests/util.ts: -------------------------------------------------------------------------------- 1 | import { mapNumber, mapParameters } from '../lib/util'; 2 | 3 | describe('mapParameters', () => { 4 | enum TestParameter { 5 | paramOne, 6 | } 7 | const goodMapper = { paramOne: mapNumber }; 8 | 9 | // eslint-disable-next-line jest/expect-expect -- type-only tests 10 | it('should reject mismatched mapper objects', () => { 11 | mapParameters(TestParameter, goodMapper, {}); 12 | 13 | // Extra property 14 | const mapperWithExtra = { ...goodMapper, paramTwo: mapNumber }; 15 | // @ts-expect-error: should reject extra mapper 16 | mapParameters(TestParameter, mapperWithExtra, {}); 17 | 18 | // Missing property 19 | // @ts-expect-error: should reject missing mapper 20 | mapParameters(TestParameter, {}, {}); 21 | }); 22 | 23 | it('should reject mismatched parameter objects', () => { 24 | // Extra property 25 | expect(() => { 26 | // @ts-expect-error: should reject extra parameter 27 | mapParameters(TestParameter, goodMapper, { paramTwo: 42 }); 28 | }).toThrow(); 29 | 30 | // Wrong property type 31 | expect(() => { 32 | // @ts-expect-error: should reject wrong parameter type 33 | mapParameters(TestParameter, goodMapper, { paramOne: true }); 34 | }).toThrow(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tsconfig.emit.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "noEmit": false, 6 | "outDir": "dist", 7 | "skipLibCheck": false 8 | }, 9 | "include": ["binding.d.ts", "lib/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@tsconfig/node20", "@tsconfig/strictest"], 3 | "compilerOptions": { 4 | "esModuleInterop": false, 5 | "noEmit": true 6 | }, 7 | "include": ["**/*.ts", "**/*.js", ".*.js"], 8 | "exclude": ["coverage", "dist", "docs"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.old-ts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/strictest", 3 | "compilerOptions": { 4 | "esModuleInterop": false, 5 | "module": "commonjs", 6 | "noEmit": true, 7 | "skipLibCheck": false, 8 | "target": "es2020" 9 | }, 10 | "include": ["binding.d.ts", "lib/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "extends": ["typedoc/tsdoc.json"] 4 | } 5 | -------------------------------------------------------------------------------- /typedoc.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entryPoints: ['lib', 'binding.d.ts'], 3 | excludeInternal: true, 4 | excludePrivate: true, 5 | excludeProtected: true, 6 | headings: { 7 | readme: false, 8 | }, 9 | includeVersion: true, 10 | sortEntryPoints: false, 11 | tsconfig: 'tsconfig.emit.json', 12 | validation: { 13 | notDocumented: true, 14 | }, 15 | requiredToBeDocumented: [ 16 | 'Accessor', 17 | 'Class', 18 | 'Constructor', 19 | 'Enum', 20 | //'EnumMember', 21 | 'Function', 22 | 'Interface', 23 | 'Method', 24 | 'Module', 25 | //'Property', 26 | 'TypeAlias', 27 | 'Variable', 28 | ], 29 | externalSymbolLinkMappings: { 30 | '@types/node': { 31 | 'internal.Transform': 32 | 'https://nodejs.org/docs/latest/api/stream.html#class-streamtransform', 33 | }, 34 | }, 35 | }; 36 | --------------------------------------------------------------------------------