├── .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 | [](https://codecov.io/gh/ipld/js-dag-pb)
4 | [](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 |
--------------------------------------------------------------------------------