├── .github ├── dependabot.yml └── workflows │ ├── generated-pr.yml │ ├── js-test-and-release.yml │ └── stale.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── dag-pb.proto ├── example-prepare.js ├── example.js ├── package.json ├── src ├── index.js ├── interface.ts ├── pb-decode.js ├── pb-encode.js └── util.js ├── test ├── test-basics.spec.js ├── test-compat.spec.js ├── test-edges.spec.js ├── test-forms.spec.js ├── test-pb.spec.js └── ts-use │ ├── .gitignore │ ├── package.json │ ├── src │ └── main.ts │ └── tsconfig.json └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | commit-message: 10 | prefix: "deps" 11 | prefix-development: "deps(dev)" 12 | -------------------------------------------------------------------------------- /.github/workflows/generated-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Generated PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/js-test-and-release.yml: -------------------------------------------------------------------------------- 1 | name: test & maybe release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | id-token: write 13 | packages: write 14 | pull-requests: write 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | js-test-and-release: 22 | uses: ipdxco/unified-github-workflows/.github/workflows/js-test-and-release.yml@v1.0 23 | secrets: 24 | DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} 25 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | UCI_GITHUB_TOKEN: ${{ secrets.UCI_GITHUB_TOKEN }} 28 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | coverage/ 4 | .coverage/ 5 | package-lock.json 6 | .docs 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.1.5](https://github.com/ipld/js-dag-pb/compare/v4.1.4...v4.1.5) (2025-05-21) 2 | 3 | ### Trivial Changes 4 | 5 | * don't commit package-lock.json ([b66416a](https://github.com/ipld/js-dag-pb/commit/b66416a5b2a2398c4e412021b1b9fc0818d6c406)) 6 | * remove package-lock.json ([d6aa27c](https://github.com/ipld/js-dag-pb/commit/d6aa27cc1606e3c98cf7e708635bdebe9f73e81c)) 7 | 8 | ### Dependencies 9 | 10 | * **dev:** bump aegir from 46.0.5 to 47.0.6 ([#106](https://github.com/ipld/js-dag-pb/issues/106)) ([8e3e8bb](https://github.com/ipld/js-dag-pb/commit/8e3e8bb222b9ac87d1abc61d26fdc446c93d1e64)) 11 | 12 | ## [4.1.4](https://github.com/ipld/js-dag-pb/compare/v4.1.3...v4.1.4) (2025-05-08) 13 | 14 | ### Trivial Changes 15 | 16 | * **test:** correctly name compatibility test case ([#102](https://github.com/ipld/js-dag-pb/issues/102)) ([af3a132](https://github.com/ipld/js-dag-pb/commit/af3a132075233728ec24d266918d32db64359812)) 17 | * **test:** fix wrongly named test case ([#101](https://github.com/ipld/js-dag-pb/issues/101)) ([345439f](https://github.com/ipld/js-dag-pb/commit/345439fca0001a6e2252cd949924152d0841cd6c)) 18 | 19 | ### Dependencies 20 | 21 | * **dev:** bump aegir from 45.2.1 to 46.0.0 ([#104](https://github.com/ipld/js-dag-pb/issues/104)) ([7b80582](https://github.com/ipld/js-dag-pb/commit/7b805822daeb4a102d63ea546a2d77ae7560638d)) 22 | 23 | ## [4.1.3](https://github.com/ipld/js-dag-pb/compare/v4.1.2...v4.1.3) (2024-10-29) 24 | 25 | ### Dependencies 26 | 27 | * **dev:** bump aegir from 44.1.4 to 45.0.0 ([02613e0](https://github.com/ipld/js-dag-pb/commit/02613e0914d79cd9aba3d2c89eb0c59c597336ab)) 28 | 29 | ## [4.1.2](https://github.com/ipld/js-dag-pb/compare/v4.1.1...v4.1.2) (2024-06-24) 30 | 31 | ### Dependencies 32 | 33 | * **dev:** bump aegir from 43.0.3 to 44.0.0 ([6510fa3](https://github.com/ipld/js-dag-pb/commit/6510fa34634864c0c0af4eb1795f07b87217bb0a)) 34 | 35 | ## [4.1.1](https://github.com/ipld/js-dag-pb/compare/v4.1.0...v4.1.1) (2024-06-01) 36 | 37 | 38 | ### Dependencies 39 | 40 | * **dev:** bump aegir from 42.2.11 to 43.0.1 ([1426d0a](https://github.com/ipld/js-dag-pb/commit/1426d0acd1ee4fadc9fcd877c0af6f893d18b750)) 41 | 42 | ## [4.1.0](https://github.com/ipld/js-dag-pb/compare/v4.0.8...v4.1.0) (2024-02-16) 43 | 44 | 45 | ### Features 46 | 47 | * support decoding ArrayBuffers ([#95](https://github.com/ipld/js-dag-pb/issues/95)) ([3d4eaf8](https://github.com/ipld/js-dag-pb/commit/3d4eaf8995ec4fef1899da62e7025d5fe5eaecbc)) 48 | 49 | ## [4.0.8](https://github.com/ipld/js-dag-pb/compare/v4.0.7...v4.0.8) (2024-01-10) 50 | 51 | 52 | ### Dependencies 53 | 54 | * **dev:** bump aegir from 41.3.5 to 42.1.0 ([63242a2](https://github.com/ipld/js-dag-pb/commit/63242a2ef309a2b88297c37ba47d2909d603674b)) 55 | 56 | ## [4.0.7](https://github.com/ipld/js-dag-pb/compare/v4.0.6...v4.0.7) (2023-12-28) 57 | 58 | 59 | ### Dependencies 60 | 61 | * bump multiformats from 12.1.3 to 13.0.0 ([#92](https://github.com/ipld/js-dag-pb/issues/92)) ([c06bed1](https://github.com/ipld/js-dag-pb/commit/c06bed10eca0bd35cad96e42e6e92af41bdf48a8)) 62 | 63 | ## [4.0.6](https://github.com/ipld/js-dag-pb/compare/v4.0.5...v4.0.6) (2023-10-03) 64 | 65 | 66 | ### Trivial Changes 67 | 68 | * add or force update .github/workflows/js-test-and-release.yml ([0041dec](https://github.com/ipld/js-dag-pb/commit/0041dec6884c574b1b1e2f67716c4ee228f28d2f)) 69 | * delete templates [skip ci] ([#89](https://github.com/ipld/js-dag-pb/issues/89)) ([d171c19](https://github.com/ipld/js-dag-pb/commit/d171c1958f4bac5aed8b938c9e3870a84e38cb2c)) 70 | 71 | 72 | ### Dependencies 73 | 74 | * **dev:** bump aegir from 40.0.13 to 41.0.0 ([66b0fc1](https://github.com/ipld/js-dag-pb/commit/66b0fc1f0f158d3bbbca51040c36da4a51031063)) 75 | 76 | ## [4.0.5](https://github.com/ipld/js-dag-pb/compare/v4.0.4...v4.0.5) (2023-08-08) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * address new typescript linting error ([9e3f768](https://github.com/ipld/js-dag-pb/commit/9e3f7685000dfeb03e8ff01507eccffa6c322d6a)) 82 | 83 | 84 | ### Dependencies 85 | 86 | * **dev:** bump aegir from 39.0.13 to 40.0.8 ([3ec2876](https://github.com/ipld/js-dag-pb/commit/3ec2876a923ce9f952efb842ea658105fa320dc7)) 87 | 88 | ## [4.0.4](https://github.com/ipld/js-dag-pb/compare/v4.0.3...v4.0.4) (2023-06-19) 89 | 90 | 91 | ### Dependencies 92 | 93 | * bump multiformats from 11.0.2 to 12.0.1 ([31dc397](https://github.com/ipld/js-dag-pb/commit/31dc39709b5432cb7809cde529d9d39eacd835a0)) 94 | 95 | ## [4.0.3](https://github.com/ipld/js-dag-pb/compare/v4.0.2...v4.0.3) (2023-05-11) 96 | 97 | 98 | ### Bug Fixes 99 | 100 | * update import paths to include extension ([#82](https://github.com/ipld/js-dag-pb/issues/82)) ([9fe97e3](https://github.com/ipld/js-dag-pb/commit/9fe97e3d0bd8c4628b7111f6e2931def88f69366)) 101 | 102 | 103 | ### Dependencies 104 | 105 | * **dev:** bump aegir from 38.1.8 to 39.0.5 ([#81](https://github.com/ipld/js-dag-pb/issues/81)) ([e2de671](https://github.com/ipld/js-dag-pb/commit/e2de671bb545be0a0d8be01f7f18b33182f4f70d)) 106 | 107 | ## [4.0.2](https://github.com/ipld/js-dag-pb/compare/v4.0.1...v4.0.2) (2023-02-14) 108 | 109 | 110 | ### Documentation 111 | 112 | * publish tsdocs for this module ([#75](https://github.com/ipld/js-dag-pb/issues/75)) ([69176a2](https://github.com/ipld/js-dag-pb/commit/69176a2887b1a337dfd30e734fc8445c928ec615)) 113 | 114 | ## [4.0.1](https://github.com/ipld/js-dag-pb/compare/v4.0.0...v4.0.1) (2023-02-13) 115 | 116 | 117 | ### Dependencies 118 | 119 | * **dev:** bump aegir from 37.12.1 to 38.1.2 ([#73](https://github.com/ipld/js-dag-pb/issues/73)) ([d01086a](https://github.com/ipld/js-dag-pb/commit/d01086a64a83bef2dba02ff3d5dcd4f0b60a0012)) 120 | 121 | ## [4.0.0](https://github.com/ipld/js-dag-pb/compare/v3.0.2...v4.0.0) (2023-01-06) 122 | 123 | 124 | ### ⚠ BREAKING CHANGES 125 | 126 | * improve consistency and fidelity of error conditions & messages 127 | 128 | ### Bug Fixes 129 | 130 | * improve consistency and fidelity of error conditions & messages ([bbbff4a](https://github.com/ipld/js-dag-pb/commit/bbbff4ab7b8924b3144a93980da8f1edae2ce421)) 131 | 132 | ## [3.0.2](https://github.com/ipld/js-dag-pb/compare/v3.0.1...v3.0.2) (2023-01-03) 133 | 134 | 135 | ### Dependencies 136 | 137 | * bump multiformats from 10.0.3 to 11.0.0 ([0bf1cb2](https://github.com/ipld/js-dag-pb/commit/0bf1cb2fb53b61b6fbd0c22c06c631ce07996e5b)) 138 | 139 | ## [3.0.1](https://github.com/ipld/js-dag-pb/compare/v3.0.0...v3.0.1) (2022-11-26) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * **norelease:** minor comment typo ([#65](https://github.com/ipld/js-dag-pb/issues/65)) ([89722b0](https://github.com/ipld/js-dag-pb/commit/89722b08a76fa02c6fb7ec456c0a251b81bc2da2)) 145 | 146 | ## [3.0.0](https://github.com/ipld/js-dag-pb/compare/v2.1.18...v3.0.0) (2022-10-19) 147 | 148 | 149 | ### ⚠ BREAKING CHANGES 150 | 151 | * publish as esm-only (#56) 152 | 153 | ### Features 154 | 155 | * publish as esm-only ([#56](https://github.com/ipld/js-dag-pb/issues/56)) ([27e1076](https://github.com/ipld/js-dag-pb/commit/27e1076b4161259d8073efa50dbb247e0effecc9)) 156 | 157 | 158 | ### Trivial Changes 159 | 160 | * **no-release:** bump @types/mocha from 9.1.1 to 10.0.0 ([#58](https://github.com/ipld/js-dag-pb/issues/58)) ([6475261](https://github.com/ipld/js-dag-pb/commit/64752610be4cedb8d54c258231625897ab405d9d)) 161 | * **no-release:** bump actions/setup-node from 3.4.1 to 3.5.0 ([#57](https://github.com/ipld/js-dag-pb/issues/57)) ([72521b4](https://github.com/ipld/js-dag-pb/commit/72521b4af008180226acd31002f2aadbb20b73d2)) 162 | * **no-release:** bump actions/setup-node from 3.5.0 to 3.5.1 ([#60](https://github.com/ipld/js-dag-pb/issues/60)) ([eaa7039](https://github.com/ipld/js-dag-pb/commit/eaa7039497e729acb86faadb151ca4b574b8990c)) 163 | 164 | ## [2.1.18](https://github.com/ipld/js-dag-pb/compare/v2.1.17...v2.1.18) (2022-08-26) 165 | 166 | 167 | ### Trivial Changes 168 | 169 | * **deps-dev:** bump typescript from 4.7.4 to 4.8.2 ([#55](https://github.com/ipld/js-dag-pb/issues/55)) ([770bc6d](https://github.com/ipld/js-dag-pb/commit/770bc6d44d08b081115c780e727da554b8df07cb)) 170 | * **no-release:** bump actions/setup-node from 3.2.0 to 3.3.0 ([#51](https://github.com/ipld/js-dag-pb/issues/51)) ([422f91e](https://github.com/ipld/js-dag-pb/commit/422f91ea722efdd119b25a8c41087ef9a61f2252)) 171 | * **no-release:** bump actions/setup-node from 3.3.0 to 3.4.0 ([#53](https://github.com/ipld/js-dag-pb/issues/53)) ([3f63ff8](https://github.com/ipld/js-dag-pb/commit/3f63ff88a0c2e6838bca6987da0fbb576acd4e19)) 172 | * **no-release:** bump actions/setup-node from 3.4.0 to 3.4.1 ([#54](https://github.com/ipld/js-dag-pb/issues/54)) ([63c6ed2](https://github.com/ipld/js-dag-pb/commit/63c6ed2cbea62b5d09dd105f6263e851e7a54e0f)) 173 | 174 | ### [2.1.17](https://github.com/ipld/js-dag-pb/compare/v2.1.16...v2.1.17) (2022-05-25) 175 | 176 | 177 | ### Trivial Changes 178 | 179 | * **deps-dev:** bump typescript from 4.6.4 to 4.7.2 ([#50](https://github.com/ipld/js-dag-pb/issues/50)) ([756ba25](https://github.com/ipld/js-dag-pb/commit/756ba256749d29978630c2984f4cbccc1c809057)) 180 | * **no-release:** bump actions/checkout from 2.4.0 to 3 ([#42](https://github.com/ipld/js-dag-pb/issues/42)) ([ceee519](https://github.com/ipld/js-dag-pb/commit/ceee519459e427843412dabd7ac78d4f0b5551f0)) 181 | * **no-release:** bump actions/setup-node from 3.0.0 to 3.1.0 ([#43](https://github.com/ipld/js-dag-pb/issues/43)) ([0c4bcb9](https://github.com/ipld/js-dag-pb/commit/0c4bcb9a1587e343325c1694419408d6482fe8f9)) 182 | * **no-release:** bump actions/setup-node from 3.1.0 to 3.1.1 ([#44](https://github.com/ipld/js-dag-pb/issues/44)) ([3c44c80](https://github.com/ipld/js-dag-pb/commit/3c44c8095f2720f1fb4f8a584f6054c163514d35)) 183 | * **no-release:** bump actions/setup-node from 3.1.1 to 3.2.0 ([#49](https://github.com/ipld/js-dag-pb/issues/49)) ([0fa8119](https://github.com/ipld/js-dag-pb/commit/0fa8119e40b18d9954d0e6c8516aac2e28897b1f)) 184 | * **no-release:** bump mocha from 9.2.2 to 10.0.0 ([#47](https://github.com/ipld/js-dag-pb/issues/47)) ([26392b8](https://github.com/ipld/js-dag-pb/commit/26392b8fb6e8e56ea7bd9da11bf9add378d7f9f9)) 185 | * **no-release:** bump polendina from 2.0.15 to 3.0.0 ([#46](https://github.com/ipld/js-dag-pb/issues/46)) ([e38782c](https://github.com/ipld/js-dag-pb/commit/e38782c84cd5f319379a96b0feb722dae4595de4)) 186 | * **no-release:** bump polendina from 3.0.0 to 3.1.0 ([#48](https://github.com/ipld/js-dag-pb/issues/48)) ([1b16aaf](https://github.com/ipld/js-dag-pb/commit/1b16aafdcb6f81276845239dba68846037a81197)) 187 | * **no-release:** bump standard from 16.0.4 to 17.0.0 ([#45](https://github.com/ipld/js-dag-pb/issues/45)) ([c334cad](https://github.com/ipld/js-dag-pb/commit/c334cadffae73338185aa8a111cd0b9ad4e0e6a4)) 188 | 189 | ### [2.1.16](https://github.com/ipld/js-dag-pb/compare/v2.1.15...v2.1.16) (2022-03-02) 190 | 191 | 192 | ### Trivial Changes 193 | 194 | * **deps-dev:** bump typescript from 4.5.5 to 4.6.2 ([#41](https://github.com/ipld/js-dag-pb/issues/41)) ([59b3cc4](https://github.com/ipld/js-dag-pb/commit/59b3cc4d00db9ebe1133f733a551528db566acb2)) 195 | * **no-release:** bump actions/setup-node from 2.5.0 to 2.5.1 ([#38](https://github.com/ipld/js-dag-pb/issues/38)) ([80cf03c](https://github.com/ipld/js-dag-pb/commit/80cf03c2fca9ad941a4c71f8fb8088860681dc10)) 196 | * **no-release:** bump actions/setup-node from 2.5.1 to 3.0.0 ([#40](https://github.com/ipld/js-dag-pb/issues/40)) ([a77b15f](https://github.com/ipld/js-dag-pb/commit/a77b15f60420445c78425ce419e95c2ac3dfcd72)) 197 | 198 | ### [2.1.15](https://github.com/ipld/js-dag-pb/compare/v2.1.14...v2.1.15) (2021-12-09) 199 | 200 | 201 | ### Trivial Changes 202 | 203 | * use semantic-release, update testing ([8735b23](https://github.com/ipld/js-dag-pb/commit/8735b238e70a446000a012f97799be539c6f916b)) 204 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is dual licensed under MIT and Apache-2.0. 2 | 3 | MIT: https://www.opensource.org/licenses/mit 4 | Apache-2.0: https://www.apache.org/licenses/license-2.0 5 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ipld/dag-pb 2 | 3 | [![codecov](https://img.shields.io/codecov/c/github/ipld/js-dag-pb.svg?style=flat-square)](https://codecov.io/gh/ipld/js-dag-pb) 4 | [![CI](https://img.shields.io/github/actions/workflow/status/ipld/js-dag-pb/js-test-and-release.yml?branch=master\&style=flat-square)](https://github.com/ipld/js-dag-pb/actions/workflows/js-test-and-release.yml?query=branch%3Amaster) 5 | 6 | > JS implementation of DAG-PB 7 | 8 | ## Table of contents 9 | 10 | - [Install](#install) 11 | - [Browser ` 33 | ``` 34 | 35 | ## Overview 36 | 37 | An implementation of the [DAG-PB spec](https://github.com/ipld/specs/blob/master/block-layer/codecs/dag-pb.md) for JavaScript designed for use with [multiformats](https://github.com/multiformats/js-multiformats) or via the higher-level `Block` abstraction in [@ipld/block](https://github.com/ipld/js-block). 38 | 39 | ## Example 40 | 41 | ```js 42 | import { CID } from 'multiformats/cid' 43 | import { sha256 } from 'multiformats/hashes/sha2' 44 | import * as dagPB from '@ipld/dag-pb' 45 | 46 | async function run () { 47 | const bytes = dagPB.encode({ 48 | Data: new TextEncoder().encode('Some data as a string'), 49 | Links: [] 50 | }) 51 | 52 | // also possible if you `import dagPB, { prepare } from '@ipld/dag-pb'` 53 | // const bytes = dagPB.encode(prepare('Some data as a string')) 54 | // const bytes = dagPB.encode(prepare(new TextEncoder().encode('Some data as a string'))) 55 | 56 | const hash = await sha256.digest(bytes) 57 | const cid = CID.create(1, dagPB.code, hash) 58 | 59 | console.log(cid, '=>', Buffer.from(bytes).toString('hex')) 60 | 61 | const decoded = dagPB.decode(bytes) 62 | 63 | console.log(decoded) 64 | console.log(`decoded "Data": ${new TextDecoder().decode(decoded.Data)}`) 65 | } 66 | 67 | run().catch((err) => { 68 | console.error(err) 69 | process.exit(1) 70 | }) 71 | ``` 72 | 73 | ## Usage 74 | 75 | `@ipld/dag-pb` is designed to be used within multiformats but can be used separately. `encode()`, `decode()`, `validate()` and `prepare()` functions are available if you pass in a `multiformats` object to the default export function. Each of these can operate independently as required. 76 | 77 | ### `prepare()` 78 | 79 | The DAG-PB encoding is very strict about the Data Model forms that are passed in. The objects *must* exactly resemble what they would if they were to undergo a round-trip of encode & decode. Therefore, extraneous or mistyped properties are not acceptable and will be rejected. See the [DAG-PB spec](https://github.com/ipld/specs/blob/master/block-layer/codecs/dag-pb.md) for full details of the acceptable schema and additional constraints. 80 | 81 | Due to this strictness, a `prepare()` function is made available which simplifies construction and allows for more flexible input forms. Prior to encoding objects, call `prepare()` to receive a new object that strictly conforms to the schema. 82 | 83 | ```js 84 | import { CID } from 'multiformats/cid' 85 | import { prepare } from '@ipld/dag-pb' 86 | 87 | console.log(prepare({ Data: 'some data' })) 88 | // ->{ Data: Uint8Array(9) [115, 111, 109, 101, 32, 100, 97, 116, 97] } 89 | console.log(prepare({ Links: [CID.parse('bafkqabiaaebagba')] })) 90 | // -> { Links: [ { Hash: CID(bafkqabiaaebagba) } ] } 91 | ``` 92 | 93 | Some features of `prepare()`: 94 | 95 | - Extraneous properties are omitted 96 | - String values for `Data` are converted 97 | - Strings are converted to `{ Data: bytes }` (as are `Uint8Array`s) 98 | - Multiple ways of finding CIDs in the `Links` array are attempted, including interpreting the whole link element as a CID, reading a `Uint8Array` as a CID 99 | - Ensuring that properties are of the correct type (link `Name` is a `string` and `Tsize` is a `number`) 100 | - `Links` array is always present, even if empty 101 | - `Links` array is properly sorted 102 | 103 | ## `createNode()` & `createLink()` 104 | 105 | These utility exports are available to make transition from the older [ipld-dag-pb](https://github.com/ipld/js-ipld-dag-pb) library which used `DAGNode` and `DAGLink` objects with constructors. `createNode()` mirrors the `new DAGNode()` API while `createLink()` mirrors `new DAGLink()` API. 106 | 107 | - `createNode(data: Uint8Array, links: PBLink[]|void): PBNode`: create a correctly formed `PBNode` object from a `Uint8Array` and an optional array of correctly formed `PBLink` objects. The returned object will be suitable for passing to `encode()` and using `prepare()` on it should result in a noop. 108 | - `createLink(name: string, size: number, cid: CID): PBLink`: create a correctly formed `PBLink` object from a name, size and CID. The returned object will be suitable for attaching to a `PBNode`'s `Links` array, or in an array for the second argument to `createNode()`. 109 | 110 | ```js 111 | import { CID, bytes } from 'multiformats' 112 | import * as Block from 'multiformats/block' 113 | import { sha256 as hasher } from 'multiformats/hashes/sha2' 114 | import * as codec from '@ipld/dag-pb' 115 | 116 | const { createLink, createNode } = codec 117 | 118 | async function run () { 119 | const cid1 = CID.parse('QmWDtUQj38YLW8v3q4A6LwPn4vYKEbuKWpgSm6bjKW6Xfe') 120 | const cid2 = CID.parse('bafyreifepiu23okq5zuyvyhsoiazv2icw2van3s7ko6d3ixl5jx2yj2yhu') 121 | 122 | const links = [createLink('link1', 100, cid1), createLink('link2', 200, cid2)] 123 | const value = createNode(Uint8Array.from([0, 1, 2, 3, 4]), links) 124 | console.log(value) 125 | 126 | const block = await Block.encode({ value, codec, hasher }) 127 | console.log(block.cid) 128 | console.log(`Encoded: ${bytes.toHex(block.bytes).replace(/(.{80})/g, '$1\n ')}`) 129 | } 130 | 131 | run().catch((err) => console.error(err)) 132 | ``` 133 | 134 | Results in: 135 | 136 | ``` 137 | { 138 | Data: Uint8Array(5) [ 0, 1, 2, 3, 4 ], 139 | Links: [ 140 | { 141 | Hash: CID(QmWDtUQj38YLW8v3q4A6LwPn4vYKEbuKWpgSm6bjKW6Xfe), 142 | Name: 'link1', 143 | Tsize: 100 144 | }, 145 | { 146 | Hash: CID(bafyreifepiu23okq5zuyvyhsoiazv2icw2van3s7ko6d3ixl5jx2yj2yhu), 147 | Name: 'link2', 148 | Tsize: 200 149 | } 150 | ] 151 | } 152 | CID(bafybeihsp53wkzsaif76mjv564cawzqyjwianosamlvf6sht2m25ttyxiy) 153 | Encoded: 122d0a2212207521fe19c374a97759226dc5c0c8e674e73950e81b211f7dd3b6b30883a08a511205 154 | 6c696e6b31186412300a2401711220a47a29adb950ee698ae0f272019ae902b6aa06ee5f53bc3da2 155 | ebea6fac27583d12056c696e6b3218c8010a050001020304 156 | ``` 157 | 158 | ## API Docs 159 | 160 | - 161 | 162 | ## License 163 | 164 | Licensed under either of 165 | 166 | - Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) 167 | - MIT ([LICENSE-MIT](LICENSE-MIT) / ) 168 | 169 | ## Contribute 170 | 171 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 172 | -------------------------------------------------------------------------------- /dag-pb.proto: -------------------------------------------------------------------------------- 1 | // An IPFS MerkleDAG Link 2 | message PBLink { 3 | 4 | // multihash of the target object 5 | optional bytes Hash = 1; 6 | 7 | // utf string name. should be unique per object 8 | optional string Name = 2; 9 | 10 | // cumulative size of target object 11 | optional uint64 Tsize = 3; 12 | } 13 | 14 | // An IPFS MerkleDAG Node 15 | message PBNode { 16 | 17 | // refs to other objects 18 | repeated PBLink Links = 2; 19 | 20 | // opaque user data 21 | optional bytes Data = 1; 22 | } 23 | -------------------------------------------------------------------------------- /example-prepare.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { prepare } from '@ipld/dag-pb' 4 | import { CID } from 'multiformats/cid' 5 | 6 | console.log(prepare({ Data: 'some data' })) 7 | // ->{ Data: Uint8Array(9) [115, 111, 109, 101, 32, 100, 97, 116, 97] } 8 | console.log(prepare({ Links: [CID.parse('bafkqabiaaebagba')] })) 9 | // -> { Links: [ { Hash: CID(bafkqabiaaebagba) } ] } 10 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import * as dagPB from '@ipld/dag-pb' 4 | import { CID } from 'multiformats/cid' 5 | import { sha256 } from 'multiformats/hashes/sha2' 6 | 7 | async function run () { 8 | const bytes = dagPB.encode({ 9 | Data: new TextEncoder().encode('Some data as a string'), 10 | Links: [] 11 | }) 12 | 13 | // also possible if you `import dagPB, { prepare } from '@ipld/dag-pb'` 14 | // const bytes = dagPB.encode(prepare('Some data as a string')) 15 | // const bytes = dagPB.encode(prepare(new TextEncoder().encode('Some data as a string'))) 16 | 17 | const hash = await sha256.digest(bytes) 18 | const cid = CID.create(1, dagPB.code, hash) 19 | 20 | console.log(cid, '=>', Buffer.from(bytes).toString('hex')) 21 | 22 | const decoded = dagPB.decode(bytes) 23 | 24 | console.log(decoded) 25 | console.log(`decoded "Data": ${new TextDecoder().decode(decoded.Data)}`) 26 | } 27 | 28 | run().catch((err) => { 29 | console.error(err) 30 | process.exit(1) 31 | }) 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ipld/dag-pb", 3 | "version": "4.1.5", 4 | "description": "JS implementation of DAG-PB", 5 | "author": "Rod (http://r.va.gg/)", 6 | "license": "Apache-2.0 OR MIT", 7 | "homepage": "https://github.com/ipld/js-dag-pb#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/ipld/js-dag-pb.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/ipld/js-dag-pb/issues" 14 | }, 15 | "keywords": [ 16 | "ipfs", 17 | "ipld", 18 | "multiformats" 19 | ], 20 | "engines": { 21 | "node": ">=16.0.0", 22 | "npm": ">=7.0.0" 23 | }, 24 | "type": "module", 25 | "types": "./dist/src/index.d.ts", 26 | "typesVersions": { 27 | "*": { 28 | "*": [ 29 | "*", 30 | "dist/*", 31 | "dist/src/*", 32 | "dist/src/*/index" 33 | ], 34 | "src/*": [ 35 | "*", 36 | "dist/*", 37 | "dist/src/*", 38 | "dist/src/*/index" 39 | ] 40 | } 41 | }, 42 | "files": [ 43 | "src", 44 | "dist", 45 | "!dist/test", 46 | "!**/*.tsbuildinfo" 47 | ], 48 | "exports": { 49 | ".": { 50 | "types": "./dist/src/index.d.ts", 51 | "import": "./src/index.js" 52 | } 53 | }, 54 | "eslintConfig": { 55 | "extends": "ipfs", 56 | "parserOptions": { 57 | "sourceType": "module" 58 | }, 59 | "ignorePatterns": [ 60 | "dist", 61 | "test/ts-use" 62 | ] 63 | }, 64 | "release": { 65 | "branches": [ 66 | "master" 67 | ], 68 | "plugins": [ 69 | [ 70 | "@semantic-release/commit-analyzer", 71 | { 72 | "preset": "conventionalcommits", 73 | "releaseRules": [ 74 | { 75 | "breaking": true, 76 | "release": "major" 77 | }, 78 | { 79 | "revert": true, 80 | "release": "patch" 81 | }, 82 | { 83 | "type": "feat", 84 | "release": "minor" 85 | }, 86 | { 87 | "type": "fix", 88 | "release": "patch" 89 | }, 90 | { 91 | "type": "docs", 92 | "release": "patch" 93 | }, 94 | { 95 | "type": "test", 96 | "release": "patch" 97 | }, 98 | { 99 | "type": "deps", 100 | "release": "patch" 101 | }, 102 | { 103 | "scope": "no-release", 104 | "release": false 105 | } 106 | ] 107 | } 108 | ], 109 | [ 110 | "@semantic-release/release-notes-generator", 111 | { 112 | "preset": "conventionalcommits", 113 | "presetConfig": { 114 | "types": [ 115 | { 116 | "type": "feat", 117 | "section": "Features" 118 | }, 119 | { 120 | "type": "fix", 121 | "section": "Bug Fixes" 122 | }, 123 | { 124 | "type": "chore", 125 | "section": "Trivial Changes" 126 | }, 127 | { 128 | "type": "docs", 129 | "section": "Documentation" 130 | }, 131 | { 132 | "type": "deps", 133 | "section": "Dependencies" 134 | }, 135 | { 136 | "type": "test", 137 | "section": "Tests" 138 | } 139 | ] 140 | } 141 | } 142 | ], 143 | "@semantic-release/changelog", 144 | "@semantic-release/npm", 145 | "@semantic-release/github", 146 | [ 147 | "@semantic-release/git", 148 | { 149 | "assets": [ 150 | "CHANGELOG.md", 151 | "package.json" 152 | ] 153 | } 154 | ] 155 | ] 156 | }, 157 | "scripts": { 158 | "clean": "aegir clean", 159 | "docs": "aegir docs", 160 | "lint": "aegir lint", 161 | "build": "aegir build", 162 | "release": "aegir release", 163 | "test": "aegir lint && aegir test", 164 | "test:ts": "npm run test --prefix test/ts-use", 165 | "test:node": "aegir test -t node --cov", 166 | "test:chrome": "aegir test -t browser --cov", 167 | "test:chrome-webworker": "aegir test -t webworker", 168 | "test:firefox": "aegir test -t browser -- --browser firefox", 169 | "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", 170 | "test:electron-main": "aegir test -t electron-main", 171 | "dep-check": "aegir dep-check -i @ipld/dag-pb" 172 | }, 173 | "dependencies": { 174 | "multiformats": "^13.1.0" 175 | }, 176 | "devDependencies": { 177 | "aegir": "^47.0.6" 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { CID } from 'multiformats/cid' 2 | import { decodeNode } from './pb-decode.js' 3 | import { encodeNode } from './pb-encode.js' 4 | import { prepare, validate, createNode, createLink, toByteView } from './util.js' 5 | 6 | /** 7 | * @template T 8 | * @typedef {import('multiformats/codecs/interface').ByteView} ByteView 9 | */ 10 | 11 | /** 12 | * @template T 13 | * @typedef {import('multiformats/codecs/interface').ArrayBufferView} ArrayBufferView 14 | */ 15 | 16 | /** 17 | * @typedef {import('./interface.js').PBLink} PBLink 18 | * @typedef {import('./interface.js').PBNode} PBNode 19 | */ 20 | 21 | export const name = 'dag-pb' 22 | export const code = 0x70 23 | 24 | /** 25 | * @param {PBNode} node 26 | * @returns {ByteView} 27 | */ 28 | export function encode (node) { 29 | validate(node) 30 | 31 | const pbn = {} 32 | if (node.Links) { 33 | pbn.Links = node.Links.map((l) => { 34 | const link = {} 35 | if (l.Hash) { 36 | link.Hash = l.Hash.bytes // cid -> bytes 37 | } 38 | if (l.Name !== undefined) { 39 | link.Name = l.Name 40 | } 41 | if (l.Tsize !== undefined) { 42 | link.Tsize = l.Tsize 43 | } 44 | return link 45 | }) 46 | } 47 | if (node.Data) { 48 | pbn.Data = node.Data 49 | } 50 | 51 | return encodeNode(pbn) 52 | } 53 | 54 | /** 55 | * @param {ByteView | ArrayBufferView} bytes 56 | * @returns {PBNode} 57 | */ 58 | export function decode (bytes) { 59 | const buf = toByteView(bytes) 60 | const pbn = decodeNode(buf) 61 | 62 | const node = {} 63 | 64 | if (pbn.Data) { 65 | node.Data = pbn.Data 66 | } 67 | 68 | if (pbn.Links) { 69 | node.Links = pbn.Links.map((l) => { 70 | const link = {} 71 | try { 72 | link.Hash = CID.decode(l.Hash) 73 | } catch { 74 | // ignore parse fail 75 | } 76 | if (!link.Hash) { 77 | throw new Error('Invalid Hash field found in link, expected CID') 78 | } 79 | if (l.Name !== undefined) { 80 | link.Name = l.Name 81 | } 82 | if (l.Tsize !== undefined) { 83 | link.Tsize = l.Tsize 84 | } 85 | return link 86 | }) 87 | } 88 | 89 | return node 90 | } 91 | 92 | export { prepare, validate, createNode, createLink } 93 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | import type { CID } from 'multiformats/cid' 2 | 3 | /* 4 | PBNode and PBLink match the DAG-PB logical format, as described at: 5 | https://github.com/ipld/specs/blob/master/block-layer/codecs/dag-pb.md#logical-format 6 | */ 7 | 8 | export interface PBLink { 9 | Name?: string 10 | Tsize?: number 11 | Hash: CID 12 | } 13 | 14 | export interface PBNode { 15 | Data?: Uint8Array 16 | Links: PBLink[] 17 | } 18 | 19 | // Raw versions of PBNode and PBLink used internally to deal with the underlying 20 | // encode/decode byte interface. 21 | // A future iteration could make pb-encode.js and pb-decode.js aware of PBNode 22 | // and PBLink specifics (including CID and optionals). 23 | 24 | export interface RawPBLink { 25 | Name: string 26 | Tsize: number 27 | Hash: Uint8Array 28 | } 29 | 30 | export interface RawPBNode { 31 | Data: Uint8Array 32 | Links: RawPBLink[] 33 | } 34 | -------------------------------------------------------------------------------- /src/pb-decode.js: -------------------------------------------------------------------------------- 1 | const textDecoder = new TextDecoder() 2 | 3 | /** 4 | * @typedef {import('./interface.js').RawPBLink} RawPBLink 5 | */ 6 | 7 | /** 8 | * @typedef {import('./interface.js').RawPBNode} RawPBNode 9 | */ 10 | 11 | /** 12 | * @param {Uint8Array} bytes 13 | * @param {number} offset 14 | * @returns {[number, number]} 15 | */ 16 | function decodeVarint (bytes, offset) { 17 | let v = 0 18 | 19 | for (let shift = 0; ; shift += 7) { 20 | /* c8 ignore next 3 */ 21 | if (shift >= 64) { 22 | throw new Error('protobuf: varint overflow') 23 | } 24 | /* c8 ignore next 3 */ 25 | if (offset >= bytes.length) { 26 | throw new Error('protobuf: unexpected end of data') 27 | } 28 | 29 | const b = bytes[offset++] 30 | v += shift < 28 ? (b & 0x7f) << shift : (b & 0x7f) * (2 ** shift) 31 | if (b < 0x80) { 32 | break 33 | } 34 | } 35 | return [v, offset] 36 | } 37 | 38 | /** 39 | * @param {Uint8Array} bytes 40 | * @param {number} offset 41 | * @returns {[Uint8Array, number]} 42 | */ 43 | function decodeBytes (bytes, offset) { 44 | let byteLen 45 | ;[byteLen, offset] = decodeVarint(bytes, offset) 46 | const postOffset = offset + byteLen 47 | 48 | /* c8 ignore next 3 */ 49 | if (byteLen < 0 || postOffset < 0) { 50 | throw new Error('protobuf: invalid length') 51 | } 52 | /* c8 ignore next 3 */ 53 | if (postOffset > bytes.length) { 54 | throw new Error('protobuf: unexpected end of data') 55 | } 56 | 57 | return [bytes.subarray(offset, postOffset), postOffset] 58 | } 59 | 60 | /** 61 | * @param {Uint8Array} bytes 62 | * @param {number} index 63 | * @returns {[number, number, number]} 64 | */ 65 | function decodeKey (bytes, index) { 66 | let wire 67 | ;[wire, index] = decodeVarint(bytes, index) 68 | // [wireType, fieldNum, newIndex] 69 | return [wire & 0x7, wire >> 3, index] 70 | } 71 | 72 | /** 73 | * @param {Uint8Array} bytes 74 | * @returns {RawPBLink} 75 | */ 76 | function decodeLink (bytes) { 77 | /** @type {RawPBLink} */ 78 | const link = {} 79 | const l = bytes.length 80 | let index = 0 81 | 82 | while (index < l) { 83 | let wireType, fieldNum 84 | ;[wireType, fieldNum, index] = decodeKey(bytes, index) 85 | 86 | if (fieldNum === 1) { 87 | if (link.Hash) { 88 | throw new Error('protobuf: (PBLink) duplicate Hash section') 89 | } 90 | if (wireType !== 2) { 91 | throw new Error(`protobuf: (PBLink) wrong wireType (${wireType}) for Hash`) 92 | } 93 | if (link.Name !== undefined) { 94 | throw new Error('protobuf: (PBLink) invalid order, found Name before Hash') 95 | } 96 | if (link.Tsize !== undefined) { 97 | throw new Error('protobuf: (PBLink) invalid order, found Tsize before Hash') 98 | } 99 | 100 | [link.Hash, index] = decodeBytes(bytes, index) 101 | } else if (fieldNum === 2) { 102 | if (link.Name !== undefined) { 103 | throw new Error('protobuf: (PBLink) duplicate Name section') 104 | } 105 | if (wireType !== 2) { 106 | throw new Error(`protobuf: (PBLink) wrong wireType (${wireType}) for Name`) 107 | } 108 | if (link.Tsize !== undefined) { 109 | throw new Error('protobuf: (PBLink) invalid order, found Tsize before Name') 110 | } 111 | 112 | let byts 113 | ;[byts, index] = decodeBytes(bytes, index) 114 | link.Name = textDecoder.decode(byts) 115 | } else if (fieldNum === 3) { 116 | if (link.Tsize !== undefined) { 117 | throw new Error('protobuf: (PBLink) duplicate Tsize section') 118 | } 119 | if (wireType !== 0) { 120 | throw new Error(`protobuf: (PBLink) wrong wireType (${wireType}) for Tsize`) 121 | } 122 | 123 | [link.Tsize, index] = decodeVarint(bytes, index) 124 | } else { 125 | throw new Error(`protobuf: (PBLink) invalid fieldNumber, expected 1, 2 or 3, got ${fieldNum}`) 126 | } 127 | } 128 | 129 | /* c8 ignore next 3 */ 130 | if (index > l) { 131 | throw new Error('protobuf: (PBLink) unexpected end of data') 132 | } 133 | 134 | return link 135 | } 136 | 137 | /** 138 | * @param {Uint8Array} bytes 139 | * @returns {RawPBNode} 140 | */ 141 | export function decodeNode (bytes) { 142 | const l = bytes.length 143 | let index = 0 144 | /** @type {RawPBLink[]|void} */ 145 | let links = undefined // eslint-disable-line no-undef-init 146 | let linksBeforeData = false 147 | /** @type {Uint8Array|void} */ 148 | let data = undefined // eslint-disable-line no-undef-init 149 | 150 | while (index < l) { 151 | let wireType, fieldNum 152 | ;[wireType, fieldNum, index] = decodeKey(bytes, index) 153 | 154 | if (wireType !== 2) { 155 | throw new Error(`protobuf: (PBNode) invalid wireType, expected 2, got ${wireType}`) 156 | } 157 | 158 | if (fieldNum === 1) { 159 | if (data) { 160 | throw new Error('protobuf: (PBNode) duplicate Data section') 161 | } 162 | 163 | [data, index] = decodeBytes(bytes, index) 164 | if (links) { 165 | linksBeforeData = true 166 | } 167 | } else if (fieldNum === 2) { 168 | if (linksBeforeData) { // interleaved Links/Data/Links 169 | throw new Error('protobuf: (PBNode) duplicate Links section') 170 | } else if (!links) { 171 | links = [] 172 | } 173 | let byts 174 | ;[byts, index] = decodeBytes(bytes, index) 175 | links.push(decodeLink(byts)) 176 | } else { 177 | throw new Error(`protobuf: (PBNode) invalid fieldNumber, expected 1 or 2, got ${fieldNum}`) 178 | } 179 | } 180 | 181 | /* c8 ignore next 3 */ 182 | if (index > l) { 183 | throw new Error('protobuf: (PBNode) unexpected end of data') 184 | } 185 | 186 | /** @type {RawPBNode} */ 187 | const node = {} 188 | if (data) { 189 | node.Data = data 190 | } 191 | node.Links = links || [] 192 | return node 193 | } 194 | -------------------------------------------------------------------------------- /src/pb-encode.js: -------------------------------------------------------------------------------- 1 | const textEncoder = new TextEncoder() 2 | const maxInt32 = 2 ** 32 3 | const maxUInt32 = 2 ** 31 4 | 5 | /** 6 | * @typedef {import('./interface.js').RawPBLink} RawPBLink 7 | */ 8 | 9 | /** 10 | * @typedef {import('./interface.js').RawPBNode} RawPBNode 11 | */ 12 | 13 | // the encoders work backward from the end of the bytes array 14 | 15 | /** 16 | * encodeLink() is passed a slice of the parent byte array that ends where this 17 | * link needs to end, so it packs to the right-most part of the passed `bytes` 18 | * 19 | * @param {RawPBLink} link 20 | * @param {Uint8Array} bytes 21 | * @returns {number} 22 | */ 23 | function encodeLink (link, bytes) { 24 | let i = bytes.length 25 | 26 | if (typeof link.Tsize === 'number') { 27 | if (link.Tsize < 0) { 28 | throw new Error('Tsize cannot be negative') 29 | } 30 | if (!Number.isSafeInteger(link.Tsize)) { 31 | throw new Error('Tsize too large for encoding') 32 | } 33 | i = encodeVarint(bytes, i, link.Tsize) - 1 34 | bytes[i] = 0x18 35 | } 36 | 37 | if (typeof link.Name === 'string') { 38 | const nameBytes = textEncoder.encode(link.Name) 39 | i -= nameBytes.length 40 | bytes.set(nameBytes, i) 41 | i = encodeVarint(bytes, i, nameBytes.length) - 1 42 | bytes[i] = 0x12 43 | } 44 | 45 | if (link.Hash) { 46 | i -= link.Hash.length 47 | bytes.set(link.Hash, i) 48 | i = encodeVarint(bytes, i, link.Hash.length) - 1 49 | bytes[i] = 0xa 50 | } 51 | 52 | return bytes.length - i 53 | } 54 | 55 | /** 56 | * Encodes a PBNode into a new byte array of precisely the correct size 57 | * 58 | * @param {RawPBNode} node 59 | * @returns {Uint8Array} 60 | */ 61 | export function encodeNode (node) { 62 | const size = sizeNode(node) 63 | const bytes = new Uint8Array(size) 64 | let i = size 65 | 66 | if (node.Data) { 67 | i -= node.Data.length 68 | bytes.set(node.Data, i) 69 | i = encodeVarint(bytes, i, node.Data.length) - 1 70 | bytes[i] = 0xa 71 | } 72 | 73 | if (node.Links) { 74 | for (let index = node.Links.length - 1; index >= 0; index--) { 75 | const size = encodeLink(node.Links[index], bytes.subarray(0, i)) 76 | i -= size 77 | i = encodeVarint(bytes, i, size) - 1 78 | bytes[i] = 0x12 79 | } 80 | } 81 | 82 | return bytes 83 | } 84 | 85 | /** 86 | * work out exactly how many bytes this link takes up 87 | * 88 | * @param {RawPBLink} link 89 | * @returns 90 | */ 91 | function sizeLink (link) { 92 | let n = 0 93 | 94 | if (link.Hash) { 95 | const l = link.Hash.length 96 | n += 1 + l + sov(l) 97 | } 98 | 99 | if (typeof link.Name === 'string') { 100 | const l = textEncoder.encode(link.Name).length 101 | n += 1 + l + sov(l) 102 | } 103 | 104 | if (typeof link.Tsize === 'number') { 105 | n += 1 + sov(link.Tsize) 106 | } 107 | 108 | return n 109 | } 110 | 111 | /** 112 | * Work out exactly how many bytes this node takes up 113 | * 114 | * @param {RawPBNode} node 115 | * @returns {number} 116 | */ 117 | function sizeNode (node) { 118 | let n = 0 119 | 120 | if (node.Data) { 121 | const l = node.Data.length 122 | n += 1 + l + sov(l) 123 | } 124 | 125 | if (node.Links) { 126 | for (const link of node.Links) { 127 | const l = sizeLink(link) 128 | n += 1 + l + sov(l) 129 | } 130 | } 131 | 132 | return n 133 | } 134 | 135 | /** 136 | * @param {Uint8Array} bytes 137 | * @param {number} offset 138 | * @param {number} v 139 | * @returns {number} 140 | */ 141 | function encodeVarint (bytes, offset, v) { 142 | offset -= sov(v) 143 | const base = offset 144 | 145 | while (v >= maxUInt32) { 146 | bytes[offset++] = (v & 0x7f) | 0x80 147 | v /= 128 148 | } 149 | 150 | while (v >= 128) { 151 | bytes[offset++] = (v & 0x7f) | 0x80 152 | v >>>= 7 153 | } 154 | 155 | bytes[offset] = v 156 | 157 | return base 158 | } 159 | 160 | /** 161 | * size of varint 162 | * 163 | * @param {number} x 164 | * @returns {number} 165 | */ 166 | function sov (x) { 167 | if (x % 2 === 0) { 168 | x++ 169 | } 170 | return Math.floor((len64(x) + 6) / 7) 171 | } 172 | 173 | /** 174 | * golang math/bits, how many bits does it take to represent this integer? 175 | * 176 | * @param {number} x 177 | * @returns {number} 178 | */ 179 | function len64 (x) { 180 | let n = 0 181 | if (x >= maxInt32) { 182 | x = Math.floor(x / maxInt32) 183 | n = 32 184 | } 185 | if (x >= (1 << 16)) { 186 | x >>>= 16 187 | n += 16 188 | } 189 | if (x >= (1 << 8)) { 190 | x >>>= 8 191 | n += 8 192 | } 193 | return n + len8tab[x] 194 | } 195 | 196 | // golang math/bits 197 | const len8tab = [ 198 | 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 199 | 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 200 | 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 201 | 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 202 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 203 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 204 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 205 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 206 | 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 207 | 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 208 | 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 209 | 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 210 | 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 211 | 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 212 | 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 213 | 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 214 | ] 215 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import { CID } from 'multiformats/cid' 2 | 3 | /* eslint-disable complexity, no-nested-ternary */ 4 | 5 | /** 6 | * @typedef {import('./interface.js').PBLink} PBLink 7 | * @typedef {import('./interface.js').PBNode} PBNode 8 | */ 9 | 10 | /** 11 | * @template T 12 | * @typedef {import('multiformats/codecs/interface').ByteView} ByteView 13 | */ 14 | 15 | /** 16 | * @template T 17 | * @typedef {import('multiformats/codecs/interface').ArrayBufferView} ArrayBufferView 18 | */ 19 | 20 | const pbNodeProperties = ['Data', 'Links'] 21 | const pbLinkProperties = ['Hash', 'Name', 'Tsize'] 22 | 23 | const textEncoder = new TextEncoder() 24 | 25 | /** 26 | * @param {PBLink} a 27 | * @param {PBLink} b 28 | * @returns {number} 29 | */ 30 | function linkComparator (a, b) { 31 | if (a === b) { 32 | return 0 33 | } 34 | 35 | const abuf = a.Name ? textEncoder.encode(a.Name) : [] 36 | const bbuf = b.Name ? textEncoder.encode(b.Name) : [] 37 | 38 | let x = abuf.length 39 | let y = bbuf.length 40 | 41 | for (let i = 0, len = Math.min(x, y); i < len; ++i) { 42 | if (abuf[i] !== bbuf[i]) { 43 | x = abuf[i] 44 | y = bbuf[i] 45 | break 46 | } 47 | } 48 | 49 | return x < y ? -1 : y < x ? 1 : 0 50 | } 51 | 52 | /** 53 | * @param {any} node 54 | * @param {string[]} properties 55 | * @returns {boolean} 56 | */ 57 | function hasOnlyProperties (node, properties) { 58 | return !Object.keys(node).some((p) => !properties.includes(p)) 59 | } 60 | 61 | /** 62 | * Converts a CID, or a PBLink-like object to a PBLink 63 | * 64 | * @param {any} link 65 | * @returns {PBLink} 66 | */ 67 | function asLink (link) { 68 | if (typeof link.asCID === 'object') { 69 | const Hash = CID.asCID(link) 70 | if (!Hash) { 71 | throw new TypeError('Invalid DAG-PB form') 72 | } 73 | return { Hash } 74 | } 75 | 76 | if (typeof link !== 'object' || Array.isArray(link)) { 77 | throw new TypeError('Invalid DAG-PB form') 78 | } 79 | 80 | const pbl = {} 81 | 82 | if (link.Hash) { 83 | let cid = CID.asCID(link.Hash) 84 | try { 85 | if (!cid) { 86 | if (typeof link.Hash === 'string') { 87 | cid = CID.parse(link.Hash) 88 | } else if (link.Hash instanceof Uint8Array) { 89 | cid = CID.decode(link.Hash) 90 | } 91 | } 92 | } catch (/** @type {any} */ e) { 93 | throw new TypeError(`Invalid DAG-PB form: ${e.message}`) 94 | } 95 | 96 | if (cid) { 97 | pbl.Hash = cid 98 | } 99 | } 100 | 101 | if (!pbl.Hash) { 102 | throw new TypeError('Invalid DAG-PB form') 103 | } 104 | 105 | if (typeof link.Name === 'string') { 106 | pbl.Name = link.Name 107 | } 108 | 109 | if (typeof link.Tsize === 'number') { 110 | pbl.Tsize = link.Tsize 111 | } 112 | 113 | return pbl 114 | } 115 | 116 | /** 117 | * @param {any} node 118 | * @returns {PBNode} 119 | */ 120 | export function prepare (node) { 121 | if (node instanceof Uint8Array || typeof node === 'string') { 122 | node = { Data: node } 123 | } 124 | 125 | if (typeof node !== 'object' || Array.isArray(node)) { 126 | throw new TypeError('Invalid DAG-PB form') 127 | } 128 | 129 | /** @type {PBNode} */ 130 | const pbn = {} 131 | 132 | if (node.Data !== undefined) { 133 | if (typeof node.Data === 'string') { 134 | pbn.Data = textEncoder.encode(node.Data) 135 | } else if (node.Data instanceof Uint8Array) { 136 | pbn.Data = node.Data 137 | } else { 138 | throw new TypeError('Invalid DAG-PB form') 139 | } 140 | } 141 | 142 | if (node.Links !== undefined) { 143 | if (Array.isArray(node.Links)) { 144 | pbn.Links = node.Links.map(asLink) 145 | pbn.Links.sort(linkComparator) 146 | } else { 147 | throw new TypeError('Invalid DAG-PB form') 148 | } 149 | } else { 150 | pbn.Links = [] 151 | } 152 | 153 | return pbn 154 | } 155 | 156 | /** 157 | * @param {PBNode} node 158 | */ 159 | export function validate (node) { 160 | /* 161 | type PBLink struct { 162 | Hash optional Link 163 | Name optional String 164 | Tsize optional Int 165 | } 166 | 167 | type PBNode struct { 168 | Links [PBLink] 169 | Data optional Bytes 170 | } 171 | */ 172 | // @ts-ignore private property for TS 173 | if (!node || typeof node !== 'object' || Array.isArray(node) || node instanceof Uint8Array || (node['/'] && node['/'] === node.bytes)) { 174 | throw new TypeError('Invalid DAG-PB form') 175 | } 176 | 177 | if (!hasOnlyProperties(node, pbNodeProperties)) { 178 | throw new TypeError('Invalid DAG-PB form (extraneous properties)') 179 | } 180 | 181 | if (node.Data !== undefined && !(node.Data instanceof Uint8Array)) { 182 | throw new TypeError('Invalid DAG-PB form (Data must be bytes)') 183 | } 184 | 185 | if (!Array.isArray(node.Links)) { 186 | throw new TypeError('Invalid DAG-PB form (Links must be a list)') 187 | } 188 | 189 | for (let i = 0; i < node.Links.length; i++) { 190 | const link = node.Links[i] 191 | // @ts-ignore private property for TS 192 | if (!link || typeof link !== 'object' || Array.isArray(link) || link instanceof Uint8Array || (link['/'] && link['/'] === link.bytes)) { 193 | throw new TypeError('Invalid DAG-PB form (bad link)') 194 | } 195 | 196 | if (!hasOnlyProperties(link, pbLinkProperties)) { 197 | throw new TypeError('Invalid DAG-PB form (extraneous properties on link)') 198 | } 199 | 200 | if (link.Hash === undefined) { 201 | throw new TypeError('Invalid DAG-PB form (link must have a Hash)') 202 | } 203 | 204 | // @ts-ignore private property for TS 205 | if (link.Hash == null || !link.Hash['/'] || link.Hash['/'] !== link.Hash.bytes) { 206 | throw new TypeError('Invalid DAG-PB form (link Hash must be a CID)') 207 | } 208 | 209 | if (link.Name !== undefined && typeof link.Name !== 'string') { 210 | throw new TypeError('Invalid DAG-PB form (link Name must be a string)') 211 | } 212 | 213 | if (link.Tsize !== undefined) { 214 | if (typeof link.Tsize !== 'number' || link.Tsize % 1 !== 0) { 215 | throw new TypeError('Invalid DAG-PB form (link Tsize must be an integer)') 216 | } 217 | if (link.Tsize < 0) { 218 | throw new TypeError('Invalid DAG-PB form (link Tsize cannot be negative)') 219 | } 220 | } 221 | 222 | if (i > 0 && linkComparator(link, node.Links[i - 1]) === -1) { 223 | throw new TypeError('Invalid DAG-PB form (links must be sorted by Name bytes)') 224 | } 225 | } 226 | } 227 | 228 | /** 229 | * @param {Uint8Array} data 230 | * @param {PBLink[]} [links] 231 | * @returns {PBNode} 232 | */ 233 | export function createNode (data, links = []) { 234 | return prepare({ Data: data, Links: links }) 235 | } 236 | 237 | /** 238 | * @param {string} name 239 | * @param {number} size 240 | * @param {CID} cid 241 | * @returns {PBLink} 242 | */ 243 | export function createLink (name, size, cid) { 244 | return asLink({ Hash: cid, Name: name, Tsize: size }) 245 | } 246 | 247 | /** 248 | * @template T 249 | * @param {ByteView | ArrayBufferView} buf 250 | * @returns {ByteView} 251 | */ 252 | export function toByteView (buf) { 253 | if (buf instanceof ArrayBuffer) { 254 | return new Uint8Array(buf, 0, buf.byteLength) 255 | } 256 | 257 | return buf 258 | } 259 | -------------------------------------------------------------------------------- /test/test-basics.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { assert } from 'aegir/chai' 4 | import { bytes } from 'multiformats' 5 | import { CID } from 'multiformats/cid' 6 | import { sha256 } from 'multiformats/hashes/sha2' 7 | import { encode, decode, code, prepare, createNode, createLink } from '../src/index.js' 8 | 9 | /** 10 | * @typedef {import('../src/index.js').PBLink} PBLink 11 | */ 12 | 13 | /** 14 | * @param {PBLink[]} links 15 | * @returns 16 | */ 17 | function linkCidsToStrings (links) { 18 | return links.map((l) => { 19 | return { 20 | Name: l.Name, 21 | Tsize: l.Tsize, 22 | Hash: l.Hash.toString() 23 | } 24 | }) 25 | } 26 | 27 | describe('Basics', () => { 28 | it('prepare & encode an empty node', () => { 29 | const prepared = prepare({}) 30 | assert.deepEqual(prepared, { Links: [] }) 31 | const result = encode(prepared) 32 | assert.instanceOf(result, Uint8Array) 33 | assert.strictEqual(result.length, 0) 34 | }) 35 | 36 | it('prepare & encode a node with data', () => { 37 | const data = Uint8Array.from([0, 1, 2, 3, 4]) 38 | const prepared = prepare({ Data: data }) 39 | assert.deepEqual(prepared, { Data: data, Links: [] }) 40 | const result = encode(prepared) 41 | assert.instanceOf(result, Uint8Array) 42 | 43 | const node = decode(result) 44 | assert.deepEqual(node.Data, data) 45 | }) 46 | 47 | it('prepare & encode a node with data using an ArrayBuffer', () => { 48 | const data = Uint8Array.from([0, 1, 2, 3, 4]) 49 | const prepared = prepare({ Data: data }) 50 | assert.deepEqual(prepared, { Data: data, Links: [] }) 51 | const result = encode(prepared) 52 | assert.instanceOf(result, Uint8Array) 53 | 54 | const node = decode(result.buffer) 55 | assert.deepEqual(node.Data, data) 56 | }) 57 | 58 | it('prepare & encode a node with links', () => { 59 | const links = [ 60 | { Hash: CID.parse('QmWDtUQj38YLW8v3q4A6LwPn4vYKEbuKWpgSm6bjKW6Xfe') } 61 | ] 62 | const prepared = prepare({ Links: links }) 63 | assert.deepEqual(prepared, { Links: [{ Hash: links[0].Hash }] }) 64 | const result = encode(prepared) 65 | assert.instanceOf(result, Uint8Array) 66 | 67 | const node = decode(result) 68 | // @ts-ignore chaiSubset 69 | assert.containSubset(linkCidsToStrings(node.Links), linkCidsToStrings([{ 70 | Hash: CID.parse('QmWDtUQj38YLW8v3q4A6LwPn4vYKEbuKWpgSm6bjKW6Xfe') 71 | }])) 72 | }) 73 | 74 | it('prepare & encode a node with links as plain objects', () => { 75 | const links = [{ 76 | Hash: CID.parse('QmWDtUQj38YLW8v3q4A6LwPn4vYKEbuKWpgSm6bjKW6Xfe') 77 | }] 78 | const prepared = prepare({ Links: links }) 79 | assert.deepEqual(prepared, { Links: [{ Hash: links[0].Hash }] }) 80 | const result = encode(prepared) 81 | assert.instanceOf(result, Uint8Array) 82 | 83 | const node = decode(result) 84 | // @ts-ignore chaiSubset 85 | assert.containSubset(linkCidsToStrings(node.Links), linkCidsToStrings(links)) 86 | }) 87 | 88 | it('ignore invalid properties when preparing', () => { 89 | const prepared = prepare({ foo: 'bar' }) 90 | assert.deepEqual(prepared, { Links: [] }) 91 | const result = encode(prepared) 92 | assert.strictEqual(result.length, 0) 93 | }) 94 | 95 | it('prepare & create a node with string data', () => { 96 | const data = 'some data' 97 | const prepared = prepare({ Data: data }) 98 | assert.deepEqual(prepared, { Data: new TextEncoder().encode(data), Links: [] }) 99 | const serialized = encode(prepared) 100 | const deserialized = decode(serialized) 101 | assert.deepEqual(deserialized.Data, new TextEncoder().encode('some data')) 102 | }) 103 | 104 | it('prepare & create a node with bare string', () => { 105 | const data = 'some data' 106 | const prepared = prepare(data) 107 | assert.deepEqual(prepared, { Data: new TextEncoder().encode(data), Links: [] }) 108 | const serialized = encode(prepared) 109 | const deserialized = decode(serialized) 110 | assert.deepEqual(deserialized.Data, new TextEncoder().encode('some data')) 111 | }) 112 | 113 | it('prepare & create a node with links (& sorting)', () => { 114 | const origLinks = [{ 115 | Name: 'some other link', 116 | Hash: CID.parse('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V'), 117 | Tsize: 8 118 | }, { 119 | Name: 'some link', 120 | Hash: CID.parse('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39U'), 121 | Tsize: 100000000 122 | }] 123 | 124 | const someData = new TextEncoder().encode('some data') 125 | 126 | const node = { 127 | Data: someData, 128 | Links: origLinks.map(({ Name, Tsize, Hash }) => { 129 | return { Name, Tsize, Hash } 130 | }) 131 | } 132 | 133 | const prepared = prepare(node) 134 | assert.deepEqual(prepared.Links.map((l) => l.Name), [ 135 | 'some link', 136 | 'some other link' 137 | ]) 138 | const byts = encode(prepared) 139 | const expectedBytes = '12340a2212208ab7a6c5e74737878ac73863cb76739d15d4666de44e5756bf55a2f9e9ab5f431209736f6d65206c696e6b1880c2d72f12370a2212208ab7a6c5e74737878ac73863cb76739d15d4666de44e5756bf55a2f9e9ab5f44120f736f6d65206f74686572206c696e6b18080a09736f6d652064617461' 140 | assert.strictEqual(bytes.toHex(byts), expectedBytes) 141 | const reconstituted = decode(byts) 142 | 143 | // check sorting 144 | assert.deepEqual(reconstituted.Links.map((l) => l.Name), [ 145 | 'some link', 146 | 'some other link' 147 | ]) 148 | }) 149 | 150 | it('prepare & create a node with stable sorted links', () => { 151 | const links = [{ 152 | Name: '', 153 | Hash: CID.parse('QmUGhP2X8xo9dsj45vqx1H6i5WqPqLqmLQsHTTxd3ke8mp'), 154 | Tsize: 262158 155 | }, { 156 | Name: '', 157 | Hash: CID.parse('QmP7SrR76KHK9A916RbHG1ufy2TzNABZgiE23PjZDMzZXy'), 158 | Tsize: 262158 159 | }, { 160 | Name: '', 161 | Hash: CID.parse('QmQg1v4o9xdT3Q14wh4S7dxZkDjyZ9ssFzFzyep1YrVJBY'), 162 | Tsize: 262158 163 | }, { 164 | Name: '', 165 | Hash: CID.parse('QmdP6fartWRrydZCUjHgrJ4XpxSE4SAoRsWJZ1zJ4MWiuf'), 166 | Tsize: 262158 167 | }, { 168 | Name: '', 169 | Hash: CID.parse('QmNNjUStxtMC1WaSZYiDW6CmAUrvd5Q2e17qnxPgVdwrwW'), 170 | Tsize: 262158 171 | }, { 172 | Name: '', 173 | Hash: CID.parse('QmWJwqZBJWerHsN1b7g4pRDYmzGNnaMYuD3KSbnpaxsB2h'), 174 | Tsize: 262158 175 | }, { 176 | Name: '', 177 | Hash: CID.parse('QmRXPSdysBS3dbUXe6w8oXevZWHdPQWaR2d3fggNsjvieL'), 178 | Tsize: 262158 179 | }, { 180 | Name: '', 181 | Hash: CID.parse('QmTUZAXfws6zrhEksnMqLxsbhXZBQs4FNiarjXSYQqVrjC'), 182 | Tsize: 262158 183 | }, { 184 | Name: '', 185 | Hash: CID.parse('QmNNk7dTdh8UofwgqLNauq6N78DPc6LKK2yBs1MFdx7Mbg'), 186 | Tsize: 262158 187 | }, { 188 | Name: '', 189 | Hash: CID.parse('QmW5mrJfyqh7B4ywSvraZgnWjS3q9CLiYURiJpCX3aro5i'), 190 | Tsize: 262158 191 | }, { 192 | Name: '', 193 | Hash: CID.parse('QmTFHZL5CkgNz19MdPnSuyLAi6AVq9fFp81zmPpaL2amED'), 194 | Tsize: 262158 195 | }] 196 | 197 | const node = { Data: new TextEncoder().encode('some data'), Links: links } 198 | const prepared = prepare(node) 199 | assert.deepEqual(prepared, node) 200 | const reconstituted = decode(encode(node)) 201 | 202 | // check sorting 203 | assert.deepEqual(reconstituted.Links.map((l) => l.Hash), links.map(l => l.Hash)) 204 | }) 205 | 206 | it('prepare & create with empty link name', () => { 207 | const node = { 208 | Data: new TextEncoder().encode('hello'), 209 | Links: [ 210 | CID.parse('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39U') 211 | ] 212 | } 213 | const expected = { Data: node.Data, Links: [{ Hash: node.Links[0] }] } 214 | const prepared = prepare(node) 215 | assert.deepEqual(prepared, expected) 216 | const reconstituted = decode(encode(prepared)) 217 | assert.deepEqual(reconstituted, expected) 218 | }) 219 | 220 | it('prepare & create with undefined link name', () => { 221 | const node = { 222 | Data: new TextEncoder().encode('hello'), 223 | Links: [ 224 | { Tsize: 10, Hash: CID.parse('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39U') } 225 | ] 226 | } 227 | const prepared = prepare(node) 228 | assert.deepEqual(prepared, node) 229 | const reconstituted = decode(encode(prepared)) 230 | assert.deepEqual(reconstituted, node) 231 | }) 232 | 233 | it('prepare & create a node with bytes only', () => { 234 | const node = new TextEncoder().encode('hello') 235 | const reconstituted = decode(encode(prepare(node))) 236 | assert.deepEqual(reconstituted, { Data: new TextEncoder().encode('hello'), Links: [] }) 237 | }) 238 | 239 | it('prepare & create an empty node', () => { 240 | const node = new Uint8Array(0) 241 | const prepared = prepare(node) 242 | assert.deepEqual(prepared, { Data: new Uint8Array(0), Links: [] }) 243 | const reconstituted = decode(encode(prepared)) 244 | assert.deepEqual(reconstituted, { Data: new Uint8Array(0), Links: [] }) 245 | }) 246 | 247 | it('prepare & create an empty node from object', () => { 248 | const prepared = prepare({}) 249 | assert.deepEqual(prepared, { Links: [] }) 250 | const reconstituted = decode(encode(prepared)) 251 | assert.deepEqual(reconstituted, { Links: [] }) 252 | }) 253 | 254 | it('fail to prepare & create a node with other data types', () => { 255 | const invalids = [ 256 | [], 257 | true, 258 | 100, 259 | () => { }, 260 | Symbol.for('nope') 261 | ] 262 | 263 | for (const invalid of invalids) { 264 | assert.throws(() => encode(prepare(invalid)), 'Invalid DAG-PB form') 265 | } 266 | }) 267 | 268 | it('fail to prepare & create a link with other data types', () => { 269 | const invalids = [ 270 | [], 271 | true, 272 | 100, 273 | () => { }, 274 | Symbol.for('nope'), 275 | { asCID: {} } 276 | ] 277 | 278 | for (const invalid of invalids) { 279 | assert.throws(() => encode(prepare({ Links: [invalid] })), 'Invalid DAG-PB form') 280 | if (!Array.isArray(invalid)) { 281 | assert.throws(() => encode(prepare({ Links: invalid })), 'Invalid DAG-PB form') 282 | } 283 | } 284 | }) 285 | 286 | it('fail to create link with bad CID hash', () => { 287 | assert.throws(() => prepare({ 288 | Links: [{ 289 | Hash: Uint8Array.from([0xf0, 1, 2, 3, 4]) // doesn't decode as CID 290 | }] 291 | }), 'Invalid DAG-PB form') 292 | }) 293 | 294 | it('deserialize go-ipfs block with unnamed links', async () => { 295 | const testBlockUnnamedLinks = bytes.fromHex('122b0a2212203f29086b59b9e046b362b4b19c9371e834a9f5a80597af83be6d8b7d1a5ad33b120018aed4e015122b0a221220ae1a5afd7c770507dddf17f92bba7a326974af8ae5277c198cf13206373f7263120018aed4e015122b0a22122022ab2ebf9c3523077bd6a171d516ea0e1be1beb132d853778bcc62cd208e77f1120018aed4e015122b0a22122040a77fe7bc69bbef2491f7633b7c462d0bce968868f88e2cbcaae9d0996997e8120018aed4e015122b0a2212206ae1979b14dd43966b0241ebe80ac2a04ad48959078dc5affa12860648356ef6120018aed4e015122b0a221220a957d1f89eb9a861593bfcd19e0637b5c957699417e2b7f23c88653a240836c4120018aed4e015122b0a221220345f9c2137a2cd76d7b876af4bfecd01f80b7dd125f375cb0d56f8a2f96de2c31200189bfec10f0a2b080218cbc1819201208080e015208080e015208080e015208080e015208080e015208080e01520cbc1c10f') 296 | 297 | const expectedLinks = [ 298 | { 299 | Name: '', 300 | Hash: CID.parse('QmSbCgdsX12C4KDw3PDmpBN9iCzS87a5DjgSCoW9esqzXk'), 301 | Tsize: 45623854 302 | }, 303 | { 304 | Name: '', 305 | Hash: CID.parse('Qma4GxWNhywSvWFzPKtEswPGqeZ9mLs2Kt76JuBq9g3fi2'), 306 | Tsize: 45623854 307 | }, 308 | { 309 | Name: '', 310 | Hash: CID.parse('QmQfyxyys7a1e3mpz9XsntSsTGc8VgpjPj5BF1a1CGdGNc'), 311 | Tsize: 45623854 312 | }, 313 | { 314 | Name: '', 315 | Hash: CID.parse('QmSh2wTTZT4N8fuSeCFw7wterzdqbE93j1XDhfN3vQHzDV'), 316 | Tsize: 45623854 317 | }, 318 | { 319 | Name: '', 320 | Hash: CID.parse('QmVXsSVjwxMsCwKRCUxEkGb4f4B98gXVy3ih3v4otvcURK'), 321 | Tsize: 45623854 322 | }, 323 | { 324 | Name: '', 325 | Hash: CID.parse('QmZjhH97MEYwQXzCqSQbdjGDhXWuwW4RyikR24pNqytWLj'), 326 | Tsize: 45623854 327 | }, 328 | { 329 | Name: '', 330 | Hash: CID.parse('QmRs6U5YirCqC7taTynz3x2GNaHJZ3jDvMVAzaiXppwmNJ'), 331 | Tsize: 32538395 332 | } 333 | ] 334 | 335 | const node = decode(testBlockUnnamedLinks) 336 | assert.deepEqual(node.Links, expectedLinks) 337 | 338 | // not a lot of point to this but we are testing that `code` is correct 339 | const hash = await sha256.digest(testBlockUnnamedLinks) 340 | const cid = CID.create(0, code, hash) 341 | assert.strictEqual(cid.toString(), 'QmQqy2SiEkKgr2cw5UbQ93TtLKEMsD8TdcWggR8q9JabjX') 342 | }) 343 | 344 | it('deserialize go-ipfs block with named links', async () => { 345 | const testBlockNamedLinks = bytes.fromHex('12390a221220b4397c02da5513563d33eef894bf68f2ccdf1bdfc14a976956ab3d1c72f735a0120e617564696f5f6f6e6c792e6d346118cda88f0b12310a221220025c13fcd1a885df444f64a4a82a26aea867b1148c68cb671e83589f971149321208636861742e74787418e40712340a2212205d44a305b9b328ab80451d0daa72a12a7bf2763c5f8bbe327597a31ee40d1e48120c706c61796261636b2e6d3375187412360a2212202539ed6e85f2a6f9097db9d76cffd49bf3042eb2e3e8e9af4a3ce842d49dea22120a7a6f6f6d5f302e6d70341897fb8592010a020801') 346 | 347 | const expectedLinks = [ 348 | { 349 | Name: 'audio_only.m4a', 350 | Hash: CID.parse('QmaUAwAQJNtvUdJB42qNbTTgDpzPYD1qdsKNtctM5i7DGB'), 351 | Tsize: 23319629 352 | }, 353 | { 354 | Name: 'chat.txt', 355 | Hash: CID.parse('QmNVrxbB25cKTRuKg2DuhUmBVEK9NmCwWEHtsHPV6YutHw'), 356 | Tsize: 996 357 | }, 358 | { 359 | Name: 'playback.m3u', 360 | Hash: CID.parse('QmUcjKzDLXBPmB6BKHeKSh6ZoFZjss4XDhMRdLYRVuvVfu'), 361 | Tsize: 116 362 | }, 363 | { 364 | Name: 'zoom_0.mp4', 365 | Hash: CID.parse('QmQqy2SiEkKgr2cw5UbQ93TtLKEMsD8TdcWggR8q9JabjX'), 366 | Tsize: 306281879 367 | } 368 | ] 369 | 370 | const node = decode(testBlockNamedLinks) 371 | assert.deepEqual(node.Links, expectedLinks) 372 | 373 | // not a lot of point to this but we are testing that `code` is correct 374 | const hash = await sha256.digest(testBlockNamedLinks) 375 | const cid = CID.create(0, code, hash) 376 | assert.strictEqual(cid.toString(), 'QmbSAC58x1tsuPBAoarwGuTQAgghKvdbKSBC8yp5gKCj5M') 377 | }) 378 | 379 | // Ref: https://github.com/ipld/specs/pull/360 380 | // Ref: https://github.com/ipld/go-codec-dagpb/pull/26 381 | it('deserialize ancient ipfs block with Data before Links', async () => { 382 | const outOfOrderNodeHex = '0a040802180612240a221220cf92fdefcdc34cac009c8b05eb662be0618db9de55ecd42785e9ec6712f8df6512240a221220cf92fdefcdc34cac009c8b05eb662be0618db9de55ecd42785e9ec6712f8df65' 383 | const outOfOrderNode = bytes.fromHex(outOfOrderNodeHex) 384 | const node = decode(outOfOrderNode) // should not throw 385 | const reencoded = encode(node) 386 | // we only care that it's different, i.e. this won't round-trip 387 | assert.notStrictEqual(bytes.toHex(reencoded), outOfOrderNodeHex) 388 | }) 389 | 390 | // this condition is introduced due to the laxity of the above case 391 | it('node with data between links', async () => { 392 | const doubleLinksNode = bytes.fromHex('12240a221220cf92fdefcdc34cac009c8b05eb662be0618db9de55ecd42785e9ec6712f8df650a040802180612240a221220cf92fdefcdc34cac009c8b05eb662be0618db9de55ecd42785e9ec6712f8df65') 393 | assert.throws(() => decode(doubleLinksNode), /PBNode.*duplicate Links section/) 394 | }) 395 | 396 | it('prepare & create with multihash bytes', () => { 397 | const linkHash = bytes.fromHex('12208ab7a6c5e74737878ac73863cb76739d15d4666de44e5756bf55a2f9e9ab5f43') 398 | const link = { 399 | Name: 'hello', 400 | Tsize: 3, 401 | Hash: linkHash 402 | } 403 | 404 | const node = { Data: new TextEncoder().encode('some data'), Links: [link] } 405 | const prepared = prepare(node) 406 | assert.strictEqual(prepared.Links[0].Hash.toString(), 'QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39U') 407 | const reconstituted = decode(encode(prepared)) 408 | 409 | assert.strictEqual(reconstituted.Links[0].Hash.toString(), 'QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39U') 410 | }) 411 | 412 | it('prepare & create with CID string', () => { 413 | const linkString = 'QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39U' 414 | const link = { 415 | Name: 'hello', 416 | Tsize: 3, 417 | Hash: linkString 418 | } 419 | 420 | const node = { Data: new TextEncoder().encode('some data'), Links: [link] } 421 | const prepared = prepare(node) 422 | assert.strictEqual(prepared.Links[0].Hash.toString(), linkString) 423 | const reconstituted = decode(encode(prepared)) 424 | 425 | assert.strictEqual(reconstituted.Links[0].Hash.toString(), linkString) 426 | }) 427 | 428 | it('fail to create without hash', () => { 429 | const node = { 430 | Data: new TextEncoder().encode('some data'), 431 | Links: [{ Name: 'hello', Tsize: 3 }] 432 | } 433 | assert.throws(() => prepare(node), 'Invalid DAG-PB form') 434 | }) 435 | }) 436 | 437 | describe('create*() utility functions', () => { 438 | const data = Uint8Array.from([0, 1, 2, 3, 4]) 439 | const aCid = CID.parse('QmWDtUQj38YLW8v3q4A6LwPn4vYKEbuKWpgSm6bjKW6Xfe') 440 | const links = [ 441 | { 442 | Name: 'foo', 443 | Hash: CID.parse('QmUGhP2X8xo9dsj45vqx1H6i5WqPqLqmLQsHTTxd3ke8mp'), 444 | Tsize: 262158 445 | }, { 446 | Name: 'boo', 447 | Hash: CID.parse('QmP7SrR76KHK9A916RbHG1ufy2TzNABZgiE23PjZDMzZXy'), 448 | Tsize: 262158 449 | }, { 450 | Name: 'yep', 451 | Hash: CID.parse('QmQg1v4o9xdT3Q14wh4S7dxZkDjyZ9ssFzFzyep1YrVJBY'), 452 | Tsize: 262158 453 | } 454 | ] 455 | const linksSorted = [links[1], links[0], links[2]] 456 | 457 | it('createNode()', () => { 458 | assert.deepStrictEqual(createNode(data), { Data: data, Links: [] }) 459 | assert.deepStrictEqual(createNode(data, []), { Data: data, Links: [] }) 460 | assert.deepStrictEqual(createNode(data, [links[0]]), { Data: data, Links: [links[0]] }) 461 | assert.deepStrictEqual(createNode(data, links), { Data: data, Links: linksSorted }) 462 | // @ts-ignore 463 | assert.deepStrictEqual(createNode(), { Links: [] }) 464 | }) 465 | 466 | it('createNode() errors', () => { 467 | const invalids = [ 468 | [], 469 | true, 470 | 100, 471 | () => { }, 472 | Symbol.for('nope') 473 | ] 474 | for (const invalid of invalids) { 475 | // @ts-ignore 476 | assert.throws(() => createNode(invalid)) 477 | } 478 | }) 479 | 480 | it('createLink()', () => { 481 | assert.deepStrictEqual(createLink('foo', 100, aCid), { Hash: aCid, Name: 'foo', Tsize: 100 }) 482 | for (const l of links) { 483 | assert.deepStrictEqual(createLink(l.Name, l.Tsize, l.Hash), l) 484 | } 485 | // Tsize isn't mandatory 486 | // @ts-ignore 487 | assert.deepStrictEqual(createLink('foo', undefined, aCid), { Hash: aCid, Name: 'foo' }) 488 | // neither is Name 489 | // @ts-ignore 490 | assert.deepStrictEqual(createLink(undefined, undefined, aCid), { Hash: aCid }) 491 | // but that's not really what this API is for ... 492 | }) 493 | 494 | it('createLink() errors', () => { 495 | const invalids = [ 496 | undefined, 497 | null, 498 | [], 499 | true, 500 | 100, 501 | () => { }, 502 | Symbol.for('nope'), 503 | {} 504 | ] 505 | for (const invalid1 of invalids) { 506 | for (const invalid2 of invalids) { 507 | for (const invalid3 of invalids) { 508 | // @ts-ignore 509 | assert.throws(() => createLink(invalid1, invalid2, invalid3)) 510 | } 511 | } 512 | } 513 | }) 514 | }) 515 | -------------------------------------------------------------------------------- /test/test-compat.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | // tests mirrored in go-merkledag/pb/compat_test.go 4 | 5 | import { assert } from 'aegir/chai' 6 | import { bytes } from 'multiformats' 7 | import { CID } from 'multiformats/cid' 8 | import { encode, decode } from '../src/index.js' 9 | import { decodeNode } from '../src/pb-decode.js' 10 | import { encodeNode } from '../src/pb-encode.js' 11 | 12 | // Hash is raw+identity 0x0001020304 CID(bafkqabiaaebagba) 13 | const acid = CID.decode(Uint8Array.from([1, 85, 0, 5, 0, 1, 2, 3, 4])) 14 | 15 | /** 16 | * @param {{node:any, expectedBytes:string, expectedForm:string}} testCase 17 | * @param {boolean} [bypass] 18 | */ 19 | function verifyRoundTrip (testCase, bypass) { 20 | const actualBytes = (bypass ? encodeNode : encode)(testCase.node) 21 | assert.strictEqual(bytes.toHex(actualBytes), testCase.expectedBytes) 22 | const roundTripNode = (bypass ? decodeNode : decode)(actualBytes) 23 | if (roundTripNode.Data) { 24 | // @ts-ignore this can't be a string, but we're making it so for ease of test 25 | roundTripNode.Data = bytes.toHex(roundTripNode.Data) 26 | } 27 | if (roundTripNode.Links) { 28 | for (const link of roundTripNode.Links) { 29 | if (link.Hash) { 30 | // @ts-ignore this can't be a string, but we're making it so for ease of test 31 | link.Hash = bytes.toHex(bypass ? link.Hash : link.Hash.bytes) 32 | } 33 | } 34 | } 35 | const actualForm = JSON.stringify(roundTripNode, null, 2) 36 | assert.strictEqual(actualForm, testCase.expectedForm) 37 | } 38 | 39 | describe('Compatibility', () => { 40 | it('empty', () => { 41 | verifyRoundTrip({ 42 | node: { Links: [] }, 43 | expectedBytes: '', 44 | expectedForm: `{ 45 | "Links": [] 46 | }` 47 | }) 48 | }) 49 | 50 | it('Data zero', () => { 51 | verifyRoundTrip({ 52 | node: { Data: new Uint8Array(0), Links: [] }, 53 | expectedBytes: '0a00', 54 | expectedForm: `{ 55 | "Data": "", 56 | "Links": [] 57 | }` 58 | }) 59 | }) 60 | 61 | it('Data some', () => { 62 | verifyRoundTrip({ 63 | node: { Data: Uint8Array.from([0, 1, 2, 3, 4]), Links: [] }, 64 | expectedBytes: '0a050001020304', 65 | expectedForm: `{ 66 | "Data": "0001020304", 67 | "Links": [] 68 | }` 69 | }) 70 | }) 71 | 72 | it('Links zero', () => { 73 | const testCase = { 74 | node: { Links: [] }, 75 | expectedBytes: '', 76 | expectedForm: `{ 77 | "Links": [] 78 | }` 79 | } 80 | verifyRoundTrip(testCase) 81 | }) 82 | 83 | it('Data some Links zero', () => { 84 | const testCase = { 85 | node: { Data: Uint8Array.from([0, 1, 2, 3, 4]), Links: [] }, 86 | expectedBytes: '0a050001020304', 87 | expectedForm: `{ 88 | "Data": "0001020304", 89 | "Links": [] 90 | }` 91 | } 92 | verifyRoundTrip(testCase) 93 | }) 94 | 95 | it('Links empty', () => { 96 | const testCase = { 97 | node: { Links: [{}] }, 98 | expectedBytes: '1200', 99 | expectedForm: `{ 100 | "Links": [ 101 | {} 102 | ] 103 | }` 104 | } 105 | assert.throws(() => verifyRoundTrip(testCase), /Hash/) 106 | // bypass straight to encode and it should verify the bytes 107 | verifyRoundTrip(testCase, true) 108 | }) 109 | 110 | it('Data some Links empty', () => { 111 | const testCase = { 112 | node: { Data: Uint8Array.from([0, 1, 2, 3, 4]), Links: [{}] }, 113 | expectedBytes: '12000a050001020304', 114 | expectedForm: `{ 115 | "Data": "0001020304", 116 | "Links": [ 117 | {} 118 | ] 119 | }` 120 | } 121 | assert.throws(() => verifyRoundTrip(testCase), /Hash/) 122 | // bypass straight to encode and it should verify the bytes 123 | verifyRoundTrip(testCase, true) 124 | }) 125 | 126 | // this is excluded from the spec, it must be a CID bytes 127 | it('Links Hash zero', () => { 128 | const testCase = { 129 | node: { Links: [{ Hash: new Uint8Array(0) }] }, 130 | expectedBytes: '12020a00', 131 | expectedForm: `{ 132 | "Links": [ 133 | { 134 | "Hash": "" 135 | } 136 | ] 137 | }` 138 | } 139 | assert.throws(() => verifyRoundTrip(testCase), /CID/) 140 | // bypass straight to encode and decode and it should verify the bytes, 141 | // the failure is on the way in _and_ out, so we have to bypass encode & decode 142 | verifyRoundTrip(testCase, true) 143 | // don't bypass decode and check the bad CID test there 144 | assert.throws(() => decode(bytes.fromHex(testCase.expectedBytes)), /CID/) 145 | }) 146 | 147 | it('Links Hash some', () => { 148 | verifyRoundTrip({ 149 | node: { Links: [{ Hash: acid }] }, 150 | expectedBytes: '120b0a09015500050001020304', 151 | expectedForm: `{ 152 | "Links": [ 153 | { 154 | "Hash": "015500050001020304" 155 | } 156 | ] 157 | }` 158 | }) 159 | }) 160 | 161 | it('Links Name zero', () => { 162 | const testCase = { 163 | node: { Links: [{ Name: '' }] }, 164 | expectedBytes: '12021200', 165 | expectedForm: `{ 166 | "Links": [ 167 | { 168 | "Name": "" 169 | } 170 | ] 171 | }` 172 | } 173 | assert.throws(() => verifyRoundTrip(testCase), /Hash/) 174 | // bypass straight to encode and it should verify the bytes 175 | verifyRoundTrip(testCase, true) 176 | }) 177 | 178 | // same as above but with a Hash 179 | it('Links Hash some Name zero', () => { 180 | verifyRoundTrip({ 181 | node: { Links: [{ Hash: acid, Name: '' }] }, 182 | expectedBytes: '120d0a090155000500010203041200', 183 | expectedForm: `{ 184 | "Links": [ 185 | { 186 | "Hash": "015500050001020304", 187 | "Name": "" 188 | } 189 | ] 190 | }` 191 | }) 192 | }) 193 | 194 | it('Links Name some', () => { 195 | const testCase = { 196 | node: { Links: [{ Name: 'some name' }] }, 197 | expectedBytes: '120b1209736f6d65206e616d65', 198 | expectedForm: `{ 199 | "Links": [ 200 | { 201 | "Name": "some name" 202 | } 203 | ] 204 | }` 205 | } 206 | assert.throws(() => verifyRoundTrip(testCase), /Hash/) 207 | // bypass straight to encode and it should verify the bytes 208 | verifyRoundTrip(testCase, true) 209 | }) 210 | 211 | // same as above but with a Hash 212 | it('Links Hash some Name some', () => { 213 | verifyRoundTrip({ 214 | node: { Links: [{ Hash: acid, Name: 'some name' }] }, 215 | expectedBytes: '12160a090155000500010203041209736f6d65206e616d65', 216 | expectedForm: `{ 217 | "Links": [ 218 | { 219 | "Hash": "015500050001020304", 220 | "Name": "some name" 221 | } 222 | ] 223 | }` 224 | }) 225 | }) 226 | 227 | it('Links Tsize zero', () => { 228 | const testCase = { 229 | node: { Links: [{ Tsize: 0 }] }, 230 | expectedBytes: '12021800', 231 | expectedForm: `{ 232 | "Links": [ 233 | { 234 | "Tsize": 0 235 | } 236 | ] 237 | }` 238 | } 239 | assert.throws(() => verifyRoundTrip(testCase), /Hash/) 240 | // bypass straight to encode and it should verify the bytes 241 | verifyRoundTrip(testCase, true) 242 | }) 243 | 244 | // same as above but with a Hash 245 | it('Links Hash some Tsize zero', () => { 246 | verifyRoundTrip({ 247 | node: { Links: [{ Hash: acid, Tsize: 0 }] }, 248 | expectedBytes: '120d0a090155000500010203041800', 249 | expectedForm: `{ 250 | "Links": [ 251 | { 252 | "Hash": "015500050001020304", 253 | "Tsize": 0 254 | } 255 | ] 256 | }` 257 | }) 258 | }) 259 | 260 | it('Links Tsize some', () => { 261 | const testCase = { 262 | node: { Links: [{ Tsize: 1010 }] }, 263 | expectedBytes: '120318f207', 264 | expectedForm: `{ 265 | "Links": [ 266 | { 267 | "Tsize": 1010 268 | } 269 | ] 270 | }` 271 | } 272 | assert.throws(() => verifyRoundTrip(testCase), /Hash/) 273 | // bypass straight to encode and it should verify the bytes 274 | verifyRoundTrip(testCase, true) 275 | }) 276 | 277 | // same as above but with a Hash 278 | it('Links Hash some Tsize some', () => { 279 | verifyRoundTrip({ 280 | node: { Links: [{ Hash: acid, Tsize: 9007199254740991 }] }, // MAX_SAFE_INTEGER 281 | expectedBytes: '12140a0901550005000102030418ffffffffffffff0f', 282 | expectedForm: `{ 283 | "Links": [ 284 | { 285 | "Hash": "015500050001020304", 286 | "Tsize": 9007199254740991 287 | } 288 | ] 289 | }` 290 | }) 291 | }) 292 | }) 293 | -------------------------------------------------------------------------------- /test/test-edges.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { assert } from 'aegir/chai' 4 | import { bytes } from 'multiformats' 5 | import { decodeNode } from '../src/pb-decode.js' 6 | import { encodeNode } from '../src/pb-encode.js' 7 | 8 | const acidBytes = Uint8Array.from([1, 85, 0, 5, 0, 1, 2, 3, 4]) 9 | 10 | describe('Edge cases', () => { 11 | it('fail to encode large int', () => { 12 | // sanity check maximum forms 13 | 14 | let form = { Links: [{ Hash: acidBytes, Tsize: Number.MAX_SAFE_INTEGER - 1 }] } 15 | let expected = '12140a0901550005000102030418feffffffffffff0f' 16 | assert.strictEqual( 17 | bytes.toHex( 18 | // @ts-ignore RawPBLink needs Name but we don't have one 19 | encodeNode(form)) 20 | , expected) 21 | // @ts-expect-error 22 | assert.deepEqual(decodeNode(bytes.fromHex(expected)), form) 23 | 24 | form = { Links: [{ Hash: acidBytes, Tsize: Number.MAX_SAFE_INTEGER }] } 25 | expected = '12140a0901550005000102030418ffffffffffffff0f' 26 | assert.strictEqual( 27 | bytes.toHex( 28 | // @ts-ignore RawPBLink needs Name but we don't have one 29 | encodeNode(form)) 30 | , expected) 31 | // @ts-expect-error 32 | assert.deepEqual(decodeNode(bytes.fromHex(expected)), form) 33 | 34 | // too big, we can decode but not encode, it's a tiny bit too hard to bother 35 | form = { Links: [{ Hash: acidBytes, Tsize: Number.MAX_SAFE_INTEGER + 1 }] } 36 | expected = '12140a09015500050001020304188080808080808010' 37 | assert.throws(() => { 38 | // @ts-ignore RawPBLink needs Name but we don't have one 39 | encodeNode(form) 40 | }, /too large/) 41 | // @ts-expect-error 42 | assert.deepEqual(decodeNode(bytes.fromHex(expected)), form) 43 | }) 44 | 45 | it('fail to encode negative large', () => { 46 | assert.throws(() => { 47 | encodeNode({ Links: [{ Hash: acidBytes, Name: 'yoik', Tsize: -1 }], Data: new Uint8Array(0) }) 48 | }, /negative/) 49 | }) 50 | 51 | it('encode awkward tsize values ', () => { 52 | // testing len64() to make sure we can properly calculate the encoded length 53 | // of various awkward values 54 | const cases = [6779297111, 5368709120, 4831838208, 4294967296, 3758096384, 55 | 3221225472, 2813203579, 2147483648, 1932735283, 1610612736, 1073741824] 56 | for (const Tsize of cases) { 57 | const node = { 58 | Links: [{ Hash: acidBytes, Name: 'big.bin', Tsize }], 59 | Data: new Uint8Array([8, 1]) 60 | } 61 | const encoded = encodeNode(node) 62 | assert.deepEqual(decodeNode(encoded), node) 63 | } 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/test-forms.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { assert } from 'aegir/chai' 4 | import { CID } from 'multiformats/cid' 5 | import { encode, validate } from '../src/index.js' 6 | 7 | /** 8 | * @typedef {import('../src/index.js').PBNode} PBNode 9 | */ 10 | 11 | const acid = CID.parse('bafkqabiaaebagba') 12 | 13 | describe('Forms (Data Model)', () => { 14 | it('validate good forms', () => { 15 | const doesntThrow = (/** @type {PBNode} */ good) => { 16 | validate(good) 17 | const byts = encode(good) 18 | assert.instanceOf(byts, Uint8Array) 19 | } 20 | 21 | doesntThrow({ Links: [] }) 22 | 23 | doesntThrow({ Data: Uint8Array.from([1, 2, 3]), Links: [] }) 24 | doesntThrow({ 25 | Links: [ 26 | { Hash: acid }, 27 | { Hash: acid, Name: 'bar' }, 28 | { Hash: acid, Name: 'foo' } 29 | ] 30 | }) 31 | doesntThrow({ 32 | Links: [ 33 | { Hash: acid }, 34 | { Hash: acid, Name: 'a' }, 35 | { Hash: acid, Name: 'a' } 36 | ] 37 | }) 38 | const l = { Hash: acid, Name: 'a' } 39 | doesntThrow({ Links: [l, l] }) 40 | }) 41 | 42 | it('validate fails bad forms', () => { 43 | const throws = (/** @type {any} */ bad) => { 44 | assert.throws(() => { 45 | // @ts-ignore because type checking is the point 46 | validate(bad) 47 | }) 48 | assert.throws(() => { 49 | // @ts-ignore because type checking is the point 50 | encode(bad) 51 | }) 52 | } 53 | 54 | for (const bad of [true, false, null, 0, 101, -101, 'blip', [], Infinity, Symbol.for('boop'), Uint8Array.from([1, 2, 3])]) { 55 | throws(bad) 56 | } 57 | 58 | throws({}) 59 | throws({ Data: null, Links: null }) 60 | throws({ Data: null, Links: [] }) 61 | throws({ Links: null }) 62 | 63 | // empty link 64 | throws({ Links: [{}] }) 65 | 66 | throws({ Data: acid.bytes, extraneous: true }) 67 | throws({ Links: [{ Hash: acid, extraneous: true }] }) 68 | 69 | // bad Data forms 70 | for (const bad of [true, false, 0, 101, -101, 'blip', Infinity, Symbol.for('boop'), []]) { 71 | throws({ Data: bad, Links: [] }) 72 | } 73 | 74 | // bad Link array forms 75 | for (const bad of [true, false, 0, 101, -101, 'blip', Infinity, Symbol.for('boop'), Uint8Array.from([1, 2, 3])]) { 76 | throws({ Links: bad }) 77 | } 78 | 79 | // bad Link forms 80 | for (const bad of [true, false, 0, 101, -101, 'blip', Infinity, Symbol.for('boop'), Uint8Array.from([1, 2, 3])]) { 81 | throws({ Links: [bad] }) 82 | } 83 | 84 | // bad Link.Hash forms 85 | for (const bad of [true, false, 0, 101, -101, [], {}, Infinity, Symbol.for('boop'), Uint8Array.from([1, 2, 3])]) { 86 | throws({ Links: [{ Hash: bad }] }) 87 | } 88 | 89 | // bad Link.Name forms 90 | for (const bad of [true, false, 0, 101, -101, [], {}, Infinity, Symbol.for('boop'), Uint8Array.from([1, 2, 3])]) { 91 | throws({ Links: [{ Hash: acid, Name: bad }] }) 92 | } 93 | 94 | // bad Link.Tsize forms 95 | for (const bad of [true, false, [], 'blip', {}, Symbol.for('boop'), Uint8Array.from([1, 2, 3])]) { 96 | throws({ Links: [{ Hash: acid, Tsize: bad }] }) 97 | } 98 | 99 | // bad sort 100 | throws({ 101 | Links: [ 102 | { Hash: acid }, 103 | { Hash: acid, Name: 'foo' }, 104 | { Hash: acid, Name: 'bar' } 105 | ] 106 | }) 107 | throws({ 108 | Links: [ 109 | { Hash: acid }, 110 | { Hash: acid, Name: 'aa' }, 111 | { Hash: acid, Name: 'a' } 112 | ] 113 | }) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /test/test-pb.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { assert } from 'aegir/chai' 4 | import { bytes } from 'multiformats' 5 | import { decodeNode } from '../src/pb-decode.js' 6 | 7 | describe('Protobuf format', () => { 8 | describe('PBNode', () => { 9 | it('bad wireType', () => { 10 | const block = bytes.fromHex('0a0500010203040') 11 | for (let i = 0; i < 8; i++) { 12 | if (i === 2) { // the valid case, length-delimited bytes 13 | continue 14 | } 15 | block[0] = (1 << 3) | i // field 1, wireType i 16 | assert.throws(() => decodeNode(block), /PBNode.*wireType/) 17 | } 18 | }) 19 | 20 | it('bad fieldNum', () => { 21 | const block = bytes.fromHex('0a0500010203040') 22 | for (let i = 0; i < 32; i++) { 23 | if (i === 1 || i === 2) { // the valid case, fields 1 and 2 24 | continue 25 | } 26 | block[0] = (i << 3) | 2 // field i, wireType 2 27 | assert.throws(() => decodeNode(block), /PBNode.*fieldNum/) 28 | } 29 | }) 30 | 31 | it('duplicate Data', () => { 32 | assert.throws(() => { 33 | decodeNode(bytes.fromHex('0a0500010203040a050001020304')) 34 | }, /PBNode.*duplicate Data/) 35 | }) 36 | }) 37 | 38 | describe('PBLink', () => { 39 | it('bad wireType for Hash', () => { 40 | const block = bytes.fromHex('120b0a09015500050001020304') 41 | for (let i = 0; i < 8; i++) { 42 | if (i === 2) { // the valid case, length-delimited bytes 43 | continue 44 | } 45 | block[2] = (1 << 3) | i // field 1, wireType i 46 | assert.throws(() => decodeNode(block), /PBLink.*wireType.*Hash/) 47 | } 48 | }) 49 | 50 | it('bad wireType for Name', () => { 51 | const block = bytes.fromHex('12160a090155000500010203041209736f6d65206e616d65') 52 | for (let i = 0; i < 8; i++) { 53 | if (i === 2) { // the valid case, length-delimited bytes 54 | continue 55 | } 56 | block[13] = (2 << 3) | i // field 2, wireType i 57 | assert.throws(() => decodeNode(block), /PBLink.*wireType.*Name/) 58 | } 59 | }) 60 | 61 | it('bad wireType for Tsize', () => { 62 | const block = bytes.fromHex('120e0a0901550005000102030418f207') 63 | for (let i = 0; i < 8; i++) { 64 | if (i === 0) { // the valid case, varint 65 | continue 66 | } 67 | block[13] = (3 << 3) | i // field 2, wireType i 68 | assert.throws(() => decodeNode(block), /PBLink.*wireType.*Tsize/) 69 | } 70 | }) 71 | 72 | it('bad fieldNum', () => { 73 | const block = bytes.fromHex('120b0a09015500050001020304') 74 | for (let i = 0; i < 32; i++) { 75 | if (i === 1 || i === 2 || i === 3) { // the valid case, fields 1, 2 and 3 76 | continue 77 | } 78 | block[2] = (i << 3) | 2 // field i, wireType 2 79 | assert.throws(() => decodeNode(block), /fieldNum/) 80 | } 81 | }) 82 | 83 | it('Name before Hash', () => { 84 | assert.throws(() => { 85 | decodeNode(bytes.fromHex('120d12000a09015500050001020304')) 86 | }, /PBLink.*Name before Hash/) 87 | }) 88 | 89 | it('Tsize before Hash', () => { 90 | assert.throws(() => { 91 | decodeNode(bytes.fromHex('120e18f2070a09015500050001020304')) 92 | }, /PBLink.*Tsize before Hash/) 93 | }) 94 | 95 | it('Tsize before Name', () => { 96 | assert.throws(() => { 97 | decodeNode(bytes.fromHex('120518f2071200')) 98 | }, /PBLink.*Tsize before Name/) 99 | }) 100 | 101 | it('duplicate Hash', () => { 102 | assert.throws(() => { 103 | decodeNode(bytes.fromHex('12160a090155000500010203040a09015500050001020304')) 104 | }, /PBLink.*duplicate Hash/) 105 | }) 106 | 107 | it('duplicate Name', () => { 108 | assert.throws(() => { 109 | decodeNode(bytes.fromHex('12210a090155000500010203041209736f6d65206e616d651209736f6d65206e616d65')) 110 | }, /PBLink.*duplicate Name/) 111 | }) 112 | 113 | it('duplicate Tsize', () => { 114 | assert.throws(() => { 115 | decodeNode(bytes.fromHex('12110a0901550005000102030418f20718f207')) 116 | }, /PBLink.*duplicate Tsize/) 117 | }) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /test/ts-use/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/main.js 3 | tsconfig.tsbuildinfo 4 | -------------------------------------------------------------------------------- /test/ts-use/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-use", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "@ipld/dag-pb": "file:../../", 7 | "multiformats": "file:../../node_modules/multiformats" 8 | }, 9 | "scripts": { 10 | "test": "npm install && npm_config_yes=true npx -p typescript tsc && node dist/src/main.js" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/ts-use/src/main.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { deepStrictEqual } from 'assert' 4 | import * as dagPB from '@ipld/dag-pb' 5 | import type { PBNode } from '@ipld/dag-pb' 6 | import type { BlockEncoder, BlockDecoder, BlockCodec } from 'multiformats/codecs/interface' 7 | 8 | const exampleNode:PBNode = { Data: Uint8Array.from([0, 1, 2, 3, 4]), Links: [] } 9 | const exampleBytes = [0x0a, 5, 0, 1, 2, 3, 4] 10 | 11 | const main = (): void => { 12 | // make sure we have a full codec 13 | useCodec(dagPB) 14 | } 15 | 16 | function useCodec (codec: BlockCodec<0x70, any>): void { 17 | // use only as a BlockEncoder 18 | useEncoder(codec) 19 | 20 | // use only as a BlockDecoder 21 | useDecoder(codec) 22 | 23 | // use with ArrayBuffer input type 24 | useDecoderWithArrayBuffer(codec) 25 | 26 | // use as a full BlockCodec which does both BlockEncoder & BlockDecoder 27 | useBlockCodec(codec) 28 | } 29 | 30 | function useEncoder (encoder: BlockEncoder): void { 31 | deepStrictEqual(encoder.code, 0x70) 32 | deepStrictEqual(encoder.name, 'dag-pb') 33 | deepStrictEqual(Array.from(encoder.encode(exampleNode)), exampleBytes) 34 | console.log('[TS] ✓ { encoder: BlockEncoder }') 35 | } 36 | 37 | function useDecoder (decoder: BlockDecoder): void { 38 | deepStrictEqual(decoder.code, 0x70) 39 | deepStrictEqual(decoder.decode(Uint8Array.from(exampleBytes)), exampleNode) 40 | console.log('[TS] ✓ { decoder: BlockDecoder }') 41 | } 42 | 43 | function useDecoderWithArrayBuffer (decoder: BlockDecoder): void { 44 | deepStrictEqual(decoder.code, 0x70) 45 | deepStrictEqual(decoder.decode(Uint8Array.from(exampleBytes).buffer), exampleNode) 46 | console.log('[TS] ✓ { decoder: BlockDecoder }') 47 | } 48 | 49 | function useBlockCodec (blockCodec: BlockCodec): void { 50 | deepStrictEqual(blockCodec.code, 0x70) 51 | deepStrictEqual(blockCodec.name, 'dag-pb') 52 | deepStrictEqual(Array.from(blockCodec.encode(exampleNode)), exampleBytes) 53 | deepStrictEqual(blockCodec.decode(Uint8Array.from(exampleBytes)), exampleNode) 54 | console.log('[TS] ✓ {}:BlockCodec') 55 | } 56 | 57 | main() 58 | 59 | export default main 60 | -------------------------------------------------------------------------------- /test/ts-use/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "aegir/src/config/tsconfig.aegir.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "aegir/src/config/tsconfig.aegir.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "emitDeclarationOnly": true 6 | }, 7 | "include": [ 8 | "src", 9 | "test" 10 | ], 11 | "exclude": [ 12 | "test/ts-use" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------