Skip to content

Commit

Permalink
fix: do not coerce undefined to null for JSON serialization (#2)
Browse files Browse the repository at this point in the history
Removes the CID decoder and disallows `undefined` when decoding
CBOR as these do not round-trip to JSON.

Expands the test suite to be explicit about the types that are
not supported.
  • Loading branch information
achingbrain authored Feb 29, 2024
1 parent 8fb4a76 commit d36ce29
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 18 deletions.
19 changes: 1 addition & 18 deletions packages/verified-fetch/src/utils/dag-cbor-to-safe-json.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,20 @@
import { decode } from 'cborg'
import { encode } from 'cborg/json'
import { CID } from 'multiformats/cid'
import type { TagDecoder } from 'cborg'

// https://github.com/ipfs/go-ipfs/issues/3570#issuecomment-273931692
const CID_CBOR_TAG = 0x2A

function cidDecoder (bytes: Uint8Array): CID {
if (bytes[0] !== 0) {
throw new Error('Invalid CID for CBOR tag 42; expected leading 0x00')
}

return CID.decode(bytes.subarray(1)) // ignore leading 0x00
}

/**
* Take a `DAG-CBOR` encoded `Uint8Array`, deserialize it as an object and
* re-serialize it in a form that can be passed to `JSON.serialize` and then
* `JSON.parse` without losing any data.
*/
export function dagCborToSafeJSON (buf: Uint8Array): string {
const tags: TagDecoder[] = []
tags[CID_CBOR_TAG] = cidDecoder

const obj = decode(buf, {
allowIndefinite: false,
coerceUndefinedToNull: true,
coerceUndefinedToNull: false,
allowNaN: false,
allowInfinity: false,
strict: true,
useMaps: false,
rejectDuplicateMapKeys: true,
tags,

// this is different to `DAG-CBOR` - the reason we disallow BigInts is
// because we are about to re-encode to `JSON` which does not support
Expand Down
79 changes: 79 additions & 0 deletions packages/verified-fetch/test/accept-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,59 @@ import * as ipldDagJson from '@ipld/dag-json'
import { stop } from '@libp2p/interface'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { expect } from 'aegir/chai'
import * as cborg from 'cborg'
import { marshal } from 'ipns'
import { CID } from 'multiformats/cid'
import * as raw from 'multiformats/codecs/raw'
import { sha256 } from 'multiformats/hashes/sha2'
import { VerifiedFetch } from '../src/verified-fetch.js'
import { createHelia } from './fixtures/create-offline-helia.js'
import type { Helia } from '@helia/interface'

interface Codec {
encode(obj: any): Uint8Array
decode(obj: Uint8Array): any
}

interface AcceptCborTestArgs {
obj: any
type: string
codec?: Codec
}

describe('accept header', () => {
let helia: Helia
let verifiedFetch: VerifiedFetch

function shouldNotAcceptCborWith ({ obj, type, codec = ipldDagCbor }: AcceptCborTestArgs): void {
it(`should return 406 Not Acceptable if CBOR ${type} field is encountered`, async () => {
const buf = codec.encode(obj)
const rawCid = CID.createV1(raw.code, await sha256.digest(buf))
await helia.blockstore.put(rawCid, buf)
const dagCborCid = CID.createV1(ipldDagCbor.code, rawCid.multihash)

const resp = await verifiedFetch.fetch(dagCborCid, {
headers: {
accept: 'application/json'
}
})

expect(resp.status).to.equal(406)
expect(resp.statusText).to.equal('Not Acceptable')

const resp2 = await verifiedFetch.fetch(dagCborCid, {
headers: {
accept: 'application/octet-stream'
}
})

expect(resp2.status).to.equal(200)

const out = codec.decode(new Uint8Array(await resp2.arrayBuffer()))
expect(out).to.deep.equal(obj, 'could not round-trip as application/octet-stream')
})
}

beforeEach(async () => {
helia = await createHelia()
verifiedFetch = new VerifiedFetch({
Expand Down Expand Up @@ -246,4 +290,39 @@ describe('accept header', () => {

expect(buf).to.equalBytes(marshal(record))
})

shouldNotAcceptCborWith({
obj: {
hello: 'world',
invalid: undefined
},
type: 'undefined',
// `undefined` is not supported by the IPLD data model so we have to encode
// using cborg and not @ipld/dag-cbor
codec: cborg
})

shouldNotAcceptCborWith({
obj: {
hello: 'world',
invalid: BigInt(Number.MAX_SAFE_INTEGER) + 10n
},
type: 'BigInt'
})

shouldNotAcceptCborWith({
obj: {
hello: 'world',
invalid: Uint8Array.from([0, 1, 2, 3])
},
type: 'Uint8Array'
})

shouldNotAcceptCborWith({
obj: {
hello: 'world',
invalid: CID.parse('QmbxpRxwKXxnJQjnPqm1kzDJSJ8YgkLxH23mcZURwPHjGv')
},
type: 'CID'
})
})

0 comments on commit d36ce29

Please sign in to comment.