Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ An implementation of the [DAG-PB spec](https://github.com/ipld/specs/blob/master
## Example

```js
import CID from 'multiformats/cid'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import * as dagPB from '@ipld/dag-pb'

Expand Down Expand Up @@ -47,7 +47,7 @@ The DAG-PB encoding is very strict about the Data Model forms that are passed in
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.

```js
import CID from 'multiformats/cid'
import { CID } from 'multiformats/cid'
import { prepare } from '@ipld/dag-pb'

console.log(prepare({ Data: 'some data' }))
Expand All @@ -66,6 +66,61 @@ Some features of `prepare()`:
* `Links` array is always present, even if empty
* `Links` array is properly sorted

## `createNode()` & `createLink()`

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.

* `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.
* `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()`.

```js
import { CID, bytes } from 'multiformats'
import * as Block from 'multiformats/block'
import { sha256 as hasher } from 'multiformats/hashes/sha2'
import * as codec from '@ipld/dag-pb'

const { createLink, createNode } = codec

async function run () {
const cid1 = CID.parse('QmWDtUQj38YLW8v3q4A6LwPn4vYKEbuKWpgSm6bjKW6Xfe')
const cid2 = CID.parse('bafyreifepiu23okq5zuyvyhsoiazv2icw2van3s7ko6d3ixl5jx2yj2yhu')

const links = [createLink('link1', 100, cid1), createLink('link2', 200, cid2)]
const value = createNode(Uint8Array.from([0, 1, 2, 3, 4]), links)
console.log(value)

const block = await Block.encode({ value, codec, hasher })
console.log(block.cid)
console.log(`Encoded: ${bytes.toHex(block.bytes).replace(/(.{80})/g, '$1\n ')}`)
}

run().catch((err) => console.error(err))
```

Results in:

```
{
Data: Uint8Array(5) [ 0, 1, 2, 3, 4 ],
Links: [
{
Hash: CID(QmWDtUQj38YLW8v3q4A6LwPn4vYKEbuKWpgSm6bjKW6Xfe),
Name: 'link1',
Tsize: 100
},
{
Hash: CID(bafyreifepiu23okq5zuyvyhsoiazv2icw2van3s7ko6d3ixl5jx2yj2yhu),
Name: 'link2',
Tsize: 200
}
]
}
CID(bafybeihsp53wkzsaif76mjv564cawzqyjwianosamlvf6sht2m25ttyxiy)
Encoded: 122d0a2212207521fe19c374a97759226dc5c0c8e674e73950e81b211f7dd3b6b30883a08a511205
6c696e6b31186412300a2401711220a47a29adb950ee698ae0f272019ae902b6aa06ee5f53bc3da2
ebea6fac27583d12056c696e6b3218c8010a050001020304
```

## License

Licensed under either of
Expand Down
198 changes: 3 additions & 195 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CID } from 'multiformats/cid'
import { decodeNode } from './pb-decode.js'
import { encodeNode } from './pb-encode.js'
import { prepare, validate, createNode, createLink } from './util.js'

/**
* @template T
Expand All @@ -15,201 +16,6 @@ import { encodeNode } from './pb-encode.js'
export const name = 'dag-pb'
export const code = 0x70

const pbNodeProperties = ['Data', 'Links']
const pbLinkProperties = ['Hash', 'Name', 'Tsize']

const textEncoder = new TextEncoder()

/**
* @param {PBLink} a
* @param {PBLink} b
* @returns {number}
*/
function linkComparator (a, b) {
if (a === b) {
return 0
}

const abuf = a.Name ? textEncoder.encode(a.Name) : []
const bbuf = b.Name ? textEncoder.encode(b.Name) : []

let x = abuf.length
let y = bbuf.length

for (let i = 0, len = Math.min(x, y); i < len; ++i) {
if (abuf[i] !== bbuf[i]) {
x = abuf[i]
y = bbuf[i]
break
}
}

return x < y ? -1 : y < x ? 1 : 0
}

/**
* @param {any} node
* @param {string[]} properties
* @returns {boolean}
*/
function hasOnlyProperties (node, properties) {
return !Object.keys(node).some((p) => !properties.includes(p))
}

/**
* Converts a CID, or a PBLink-like object to a PBLink
*
* @param {any} link
* @returns {PBLink}
*/
function asLink (link) {
if (typeof link.asCID === 'object') {
const Hash = CID.asCID(link)
if (!Hash) {
throw new TypeError('Invalid DAG-PB form')
}
return { Hash }
}

if (typeof link !== 'object' || Array.isArray(link)) {
throw new TypeError('Invalid DAG-PB form')
}

const pbl = {}

if (link.Hash) {
let cid = CID.asCID(link.Hash)
try {
if (!cid) {
if (typeof link.Hash === 'string') {
cid = CID.parse(link.Hash)
} else if (link.Hash instanceof Uint8Array) {
cid = CID.decode(link.Hash)
}
}
} catch (e) {
throw new TypeError(`Invalid DAG-PB form: ${e.message}`)
}

if (cid) {
pbl.Hash = cid
}
}

if (!pbl.Hash) {
throw new TypeError('Invalid DAG-PB form')
}

if (typeof link.Name === 'string') {
pbl.Name = link.Name
}

if (typeof link.Tsize === 'number') {
pbl.Tsize = link.Tsize
}

return pbl
}

/**
* @param {any} node
* @returns {PBNode}
*/
export function prepare (node) {
if (node instanceof Uint8Array || typeof node === 'string') {
node = { Data: node }
}

if (typeof node !== 'object' || Array.isArray(node)) {
throw new TypeError('Invalid DAG-PB form')
}

/** @type {PBNode} */
const pbn = {}

if (node.Data) {
if (typeof node.Data === 'string') {
pbn.Data = textEncoder.encode(node.Data)
} else if (node.Data instanceof Uint8Array) {
pbn.Data = node.Data
}
}

if (node.Links && Array.isArray(node.Links) && node.Links.length) {
pbn.Links = node.Links.map(asLink)
pbn.Links.sort(linkComparator)
} else {
pbn.Links = []
}

return pbn
}

/**
* @param {PBNode} node
*/
export function validate (node) {
/*
type PBLink struct {
Hash optional Link
Name optional String
Tsize optional Int
}

type PBNode struct {
Links [PBLink]
Data optional Bytes
}
*/
if (!node || typeof node !== 'object' || Array.isArray(node)) {
throw new TypeError('Invalid DAG-PB form')
}

if (!hasOnlyProperties(node, pbNodeProperties)) {
throw new TypeError('Invalid DAG-PB form (extraneous properties)')
}

if (node.Data !== undefined && !(node.Data instanceof Uint8Array)) {
throw new TypeError('Invalid DAG-PB form (Data must be a Uint8Array)')
}

if (!Array.isArray(node.Links)) {
throw new TypeError('Invalid DAG-PB form (Links must be an array)')
}

for (let i = 0; i < node.Links.length; i++) {
const link = node.Links[i]
if (!link || typeof link !== 'object' || Array.isArray(link)) {
throw new TypeError('Invalid DAG-PB form (bad link object)')
}

if (!hasOnlyProperties(link, pbLinkProperties)) {
throw new TypeError('Invalid DAG-PB form (extraneous properties on link object)')
}

if (!link.Hash) {
throw new TypeError('Invalid DAG-PB form (link must have a Hash)')
}

// @ts-ignore private property for TS
if (link.Hash.asCID !== link.Hash) {
throw new TypeError('Invalid DAG-PB form (link Hash must be a CID)')
}

if (link.Name !== undefined && typeof link.Name !== 'string') {
throw new TypeError('Invalid DAG-PB form (link Name must be a string)')
}

if (link.Tsize !== undefined && (typeof link.Tsize !== 'number' || link.Tsize % 1 !== 0)) {
throw new TypeError('Invalid DAG-PB form (link Tsize must be an integer)')
}

if (i > 0 && linkComparator(link, node.Links[i - 1]) === -1) {
throw new TypeError('Invalid DAG-PB form (links must be sorted by Name bytes)')
}
}
}

/**
* @param {PBNode} node
* @returns {ByteView<PBNode>}
Expand Down Expand Up @@ -274,3 +80,5 @@ export function decode (bytes) {

return node
}

export { prepare, validate, createNode, createLink }
Loading