Skip to content

Commit

Permalink
feat: add createNode() and createLink() factories
Browse files Browse the repository at this point in the history
to match DAGNode and DAGLink constructors of old
  • Loading branch information
rvagg committed Jun 24, 2021
1 parent 291d34c commit 5084446
Show file tree
Hide file tree
Showing 4 changed files with 371 additions and 201 deletions.
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

0 comments on commit 5084446

Please sign in to comment.