Skip to content

Commit d36ce29

Browse files
authored
fix: do not coerce undefined to null for JSON serialization (#2)
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.
1 parent 8fb4a76 commit d36ce29

File tree

2 files changed

+80
-18
lines changed

2 files changed

+80
-18
lines changed

packages/verified-fetch/src/utils/dag-cbor-to-safe-json.ts

+1-18
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,20 @@
11
import { decode } from 'cborg'
22
import { encode } from 'cborg/json'
3-
import { CID } from 'multiformats/cid'
4-
import type { TagDecoder } from 'cborg'
5-
6-
// https://github.com/ipfs/go-ipfs/issues/3570#issuecomment-273931692
7-
const CID_CBOR_TAG = 0x2A
8-
9-
function cidDecoder (bytes: Uint8Array): CID {
10-
if (bytes[0] !== 0) {
11-
throw new Error('Invalid CID for CBOR tag 42; expected leading 0x00')
12-
}
13-
14-
return CID.decode(bytes.subarray(1)) // ignore leading 0x00
15-
}
163

174
/**
185
* Take a `DAG-CBOR` encoded `Uint8Array`, deserialize it as an object and
196
* re-serialize it in a form that can be passed to `JSON.serialize` and then
207
* `JSON.parse` without losing any data.
218
*/
229
export function dagCborToSafeJSON (buf: Uint8Array): string {
23-
const tags: TagDecoder[] = []
24-
tags[CID_CBOR_TAG] = cidDecoder
25-
2610
const obj = decode(buf, {
2711
allowIndefinite: false,
28-
coerceUndefinedToNull: true,
12+
coerceUndefinedToNull: false,
2913
allowNaN: false,
3014
allowInfinity: false,
3115
strict: true,
3216
useMaps: false,
3317
rejectDuplicateMapKeys: true,
34-
tags,
3518

3619
// this is different to `DAG-CBOR` - the reason we disallow BigInts is
3720
// because we are about to re-encode to `JSON` which does not support

packages/verified-fetch/test/accept-header.spec.ts

+79
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,59 @@ import * as ipldDagJson from '@ipld/dag-json'
66
import { stop } from '@libp2p/interface'
77
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
88
import { expect } from 'aegir/chai'
9+
import * as cborg from 'cborg'
910
import { marshal } from 'ipns'
11+
import { CID } from 'multiformats/cid'
12+
import * as raw from 'multiformats/codecs/raw'
13+
import { sha256 } from 'multiformats/hashes/sha2'
1014
import { VerifiedFetch } from '../src/verified-fetch.js'
1115
import { createHelia } from './fixtures/create-offline-helia.js'
1216
import type { Helia } from '@helia/interface'
1317

18+
interface Codec {
19+
encode(obj: any): Uint8Array
20+
decode(obj: Uint8Array): any
21+
}
22+
23+
interface AcceptCborTestArgs {
24+
obj: any
25+
type: string
26+
codec?: Codec
27+
}
28+
1429
describe('accept header', () => {
1530
let helia: Helia
1631
let verifiedFetch: VerifiedFetch
1732

33+
function shouldNotAcceptCborWith ({ obj, type, codec = ipldDagCbor }: AcceptCborTestArgs): void {
34+
it(`should return 406 Not Acceptable if CBOR ${type} field is encountered`, async () => {
35+
const buf = codec.encode(obj)
36+
const rawCid = CID.createV1(raw.code, await sha256.digest(buf))
37+
await helia.blockstore.put(rawCid, buf)
38+
const dagCborCid = CID.createV1(ipldDagCbor.code, rawCid.multihash)
39+
40+
const resp = await verifiedFetch.fetch(dagCborCid, {
41+
headers: {
42+
accept: 'application/json'
43+
}
44+
})
45+
46+
expect(resp.status).to.equal(406)
47+
expect(resp.statusText).to.equal('Not Acceptable')
48+
49+
const resp2 = await verifiedFetch.fetch(dagCborCid, {
50+
headers: {
51+
accept: 'application/octet-stream'
52+
}
53+
})
54+
55+
expect(resp2.status).to.equal(200)
56+
57+
const out = codec.decode(new Uint8Array(await resp2.arrayBuffer()))
58+
expect(out).to.deep.equal(obj, 'could not round-trip as application/octet-stream')
59+
})
60+
}
61+
1862
beforeEach(async () => {
1963
helia = await createHelia()
2064
verifiedFetch = new VerifiedFetch({
@@ -246,4 +290,39 @@ describe('accept header', () => {
246290

247291
expect(buf).to.equalBytes(marshal(record))
248292
})
293+
294+
shouldNotAcceptCborWith({
295+
obj: {
296+
hello: 'world',
297+
invalid: undefined
298+
},
299+
type: 'undefined',
300+
// `undefined` is not supported by the IPLD data model so we have to encode
301+
// using cborg and not @ipld/dag-cbor
302+
codec: cborg
303+
})
304+
305+
shouldNotAcceptCborWith({
306+
obj: {
307+
hello: 'world',
308+
invalid: BigInt(Number.MAX_SAFE_INTEGER) + 10n
309+
},
310+
type: 'BigInt'
311+
})
312+
313+
shouldNotAcceptCborWith({
314+
obj: {
315+
hello: 'world',
316+
invalid: Uint8Array.from([0, 1, 2, 3])
317+
},
318+
type: 'Uint8Array'
319+
})
320+
321+
shouldNotAcceptCborWith({
322+
obj: {
323+
hello: 'world',
324+
invalid: CID.parse('QmbxpRxwKXxnJQjnPqm1kzDJSJ8YgkLxH23mcZURwPHjGv')
325+
},
326+
type: 'CID'
327+
})
249328
})

0 commit comments

Comments
 (0)