Skip to content

Commit 09a6573

Browse files
committed
feat: CID.inspectBytes() and CID.decodeFirst()
1 parent 6ccdfcb commit 09a6573

4 files changed

Lines changed: 137 additions & 47 deletions

File tree

src/cid.js

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as varint from './varint.js'
22
import * as Digest from './hashes/digest.js'
33
import { base58btc } from './bases/base58.js'
44
import { base32 } from './bases/base32.js'
5+
import { coerce } from './bytes.js'
56

67
/**
78
* @typedef {import('./hashes/interface').MultihashDigest} MultihashDigest
@@ -271,64 +272,80 @@ export default class CID {
271272
* An error will be thrown if the bytes provided do not contain a valid
272273
* binary representation of a CID.
273274
*
274-
* @param {Uint8Array} cid
275+
* @param {Uint8Array} bytes
275276
* @returns {CID}
276277
*/
277278
static decode (bytes) {
278-
const [version, offset] = varint.decode(bytes)
279-
switch (version) {
280-
// CIDv0
281-
case 18: {
282-
const multihash = Digest.decode(bytes)
283-
return CID.createV0(multihash)
284-
}
285-
// CIDv1
286-
case 1: {
287-
const [code, length] = varint.decode(bytes.subarray(offset))
288-
const digest = Digest.decode(bytes.subarray(offset + length))
289-
return CID.createV1(code, digest)
290-
}
291-
default: {
292-
throw new RangeError(`Invalid CID version ${version}`)
293-
}
279+
const [cid, remaining] = CID.decodeFirst(bytes)
280+
if (remaining.length) {
281+
throw new Error('Incorrect length')
282+
}
283+
return cid
284+
}
285+
286+
/**
287+
* Decoded a CID from its binary representation at the begining of a byte
288+
* array.
289+
*
290+
* Returns an array with the first element containing the CID and the second
291+
* element containing the remainder of the original byte array. The remainder
292+
* will be a zero-length byte array if the provided bytes only contained a
293+
* binary CID representation.
294+
*
295+
* @param {Uint8Array} bytes
296+
* @returns {[CID, Uint8Array]}
297+
*/
298+
static decodeFirst (bytes) {
299+
const specs = CID.inspectBytes(bytes)
300+
const prefixSize = specs.size - specs.multihashSize
301+
const multihashBytes = coerce(bytes.subarray(prefixSize, prefixSize + specs.multihashSize))
302+
if (multihashBytes.byteLength !== specs.multihashSize) {
303+
throw new Error('Incorrect length')
294304
}
305+
const digestBytes = multihashBytes.subarray(specs.multihashSize - specs.digestSize)
306+
const digest = new Digest.Digest(specs.multihashCode, specs.digestSize, digestBytes, multihashBytes)
307+
const cid = specs.version === 0 ? CID.createV0(digest) : CID.createV1(specs.codec, digest)
308+
return [cid, bytes.subarray(specs.size)]
295309
}
296310

297311
/*
298-
* Determine the length of the binary representation of a CID given the
299-
* initial bytes.
312+
* Inspect the initial bytes of a CID to determine its properties.
300313
*
301-
* Involves decoding up to 3 varints. Typically this will require only 3 to 5
314+
* Involves decoding up to 4 varints. Typically this will require only 4 to 6
302315
* bytes but for larger multicodec code values and larger multihash digest
303-
* lengths these varints can be quite large.
316+
* lengths these varints can be quite large. It is recommended that at least
317+
* 10 bytes be made available in the `initialBytes` argument for a complete
318+
* inspection.
304319
*
305320
* @param {Uint8Array} initialBytes
306-
* @returns {number}
321+
* @returns {{ version:number, codec:number, multihashCode:number, digestSize:number, multihashSize:number, size:number }}
307322
*/
308-
static encodedLength (initialBytes) {
323+
static inspectBytes (initialBytes) {
309324
let offset = 0
310325
const next = () => {
311326
const [i, length] = varint.decode(initialBytes.subarray(offset))
312327
offset += length
313328
return i
314329
}
315330

316-
const version = next()
317-
switch (version) {
318-
// CIDv0
319-
case 18:
320-
return 36
321-
// CIDv1
322-
case 1: {
323-
next() // codec
324-
next() // multihash code
325-
const mhLength = next() // multihash length
326-
return offset + mhLength
327-
}
328-
default: {
329-
throw new RangeError(`Invalid CID version ${version}`)
330-
}
331+
let version = next()
332+
let codec = DAG_PB_CODE
333+
if (version === 18) { // CIDv0
334+
version = 0
335+
offset = 0
336+
} else if (version === 1) {
337+
codec = next()
338+
} else if (version !== 1) {
339+
throw new RangeError(`Invalid CID version ${version}`)
331340
}
341+
342+
const prefixSize = offset
343+
const multihashCode = next() // multihash code
344+
const digestSize = next() // multihash length
345+
const size = offset + digestSize
346+
const multihashSize = size - prefixSize
347+
348+
return { version, codec, multihashCode, digestSize, multihashSize, size }
332349
}
333350

334351
/**

src/codecs/aes.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,17 @@ const mkcrypto = ({ name, code, ivsize }) => {
1919
const decrypt = async ({ key, value }) => {
2020
let { bytes, iv } = value
2121
bytes = await aes.decrypt(bytes, key, { name: name.toUpperCase(), iv, tagLength: 16 })
22-
const cidLength = CID.encodedLength(bytes)
23-
const cid = CID.decode(bytes.subarray(0, cidLength))
24-
bytes = bytes.subarray(cid.bytes.length)
25-
return { cid, bytes }
22+
const [cid, remainder] = CID.decodeFirst(bytes)
23+
return { cid, bytes: remainder }
2624
}
25+
2726
const encrypt = async ({ key, cid, bytes }) => {
2827
const iv = random.getRandomBytes(ivsize)
2928
const msg = concat([cid.bytes, bytes])
3029
bytes = await aes.encrypt(msg, key, { name: name.toUpperCase(), iv, tagLength: 16 })
3130
return { bytes, iv }
3231
}
32+
3333
return { encode, decode, encrypt, decrypt, code, name: name.toLowerCase() }
3434
}
3535

test/fixtures/invalid-multihash.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,9 @@ export default [{
2828
size: 32,
2929
hex: '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7',
3030
message: 'Incorrect length'
31+
}, {
32+
code: 0x12,
33+
size: 32,
34+
hex: '1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad0000',
35+
message: 'Incorrect length'
3136
}]

test/test-cid.js

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22

33
import OLDCID from 'cids'
44
import assert from 'assert'
5-
import { toHex, equals } from '../src/bytes.js'
5+
import { fromHex, toHex, equals } from '../src/bytes.js'
66
import { varint, CID } from 'multiformats'
77
import { base58btc } from 'multiformats/bases/base58'
88
import { base32 } from 'multiformats/bases/base32'
99
import { base64 } from 'multiformats/bases/base64'
1010
import { sha256, sha512 } from 'multiformats/hashes/sha2'
1111
import util from 'util'
1212
import { Buffer } from 'buffer'
13+
import invalidMultihash from './fixtures/invalid-multihash.js'
14+
1315
const test = it
1416

15-
const same = (x, y) => {
16-
if (x instanceof Uint8Array && y instanceof Uint8Array) {
17-
if (Buffer.compare(Buffer.from(x), Buffer.from(y)) === 0) return
17+
const same = (actual, expected) => {
18+
if (actual instanceof Uint8Array && expected instanceof Uint8Array) {
19+
if (Buffer.compare(Buffer.from(actual), Buffer.from(expected)) === 0) return
1820
}
19-
return assert.deepStrictEqual(x, y)
21+
return assert.deepStrictEqual(actual, expected)
2022
}
2123

2224
// eslint-disable-next-line no-unused-vars
@@ -119,6 +121,35 @@ describe('CID', () => {
119121
const newCid = CID.asCID(oldCid)
120122
same(newCid.toString(), cidStr)
121123
})
124+
125+
test('inspect bytes', () => {
126+
const byts = fromHex('1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')
127+
const inspected = CID.inspectBytes(byts.subarray(0, 10)) // should only need the first few bytes
128+
same({
129+
version: 0,
130+
codec: 0x70,
131+
multihashCode: 0x12,
132+
multihashSize: 34,
133+
digestSize: 32,
134+
size: 34
135+
}, inspected)
136+
})
137+
138+
describe('decodeFirst', () => {
139+
test('no remainder', () => {
140+
const byts = fromHex('1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')
141+
const [cid, remainder] = CID.decodeFirst(byts)
142+
same(cid.toString(), 'QmatYkNGZnELf8cAGdyJpUca2PyY4szai3RHyyWofNY1pY')
143+
same(remainder.byteLength, 0)
144+
})
145+
146+
test('remainder', () => {
147+
const byts = fromHex('1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad0102030405')
148+
const [cid, remainder] = CID.decodeFirst(byts)
149+
same(cid.toString(), 'QmatYkNGZnELf8cAGdyJpUca2PyY4szai3RHyyWofNY1pY')
150+
same(toHex(remainder), '0102030405')
151+
})
152+
})
122153
})
123154

124155
describe('v1', () => {
@@ -282,6 +313,13 @@ describe('CID', () => {
282313
const name = `CID.create(${version}, ${code}, ${mh})`
283314
test(name, async () => await testThrowAny(() => CID.create(version, code, hash)))
284315
}
316+
317+
test('invalid fixtures', async () => {
318+
for (const test of invalidMultihash) {
319+
const buff = fromHex(`0171${test.hex}`)
320+
assert.throws(() => CID.decode(buff), new RegExp(test.message))
321+
}
322+
})
285323
})
286324

287325
describe('idempotence', () => {
@@ -482,6 +520,35 @@ describe('CID', () => {
482520
})
483521
})
484522

523+
test('inspect bytes', () => {
524+
const byts = fromHex('01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')
525+
const inspected = CID.inspectBytes(byts.subarray(0, 10)) // should only need the first few bytes
526+
same({
527+
version: 1,
528+
codec: 0x71,
529+
multihashCode: 0x12,
530+
multihashSize: 34,
531+
digestSize: 32,
532+
size: 36
533+
}, inspected)
534+
535+
describe('decodeFirst', () => {
536+
test('no remainder', () => {
537+
const byts = fromHex('01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')
538+
const [cid, remainder] = CID.decodeFirst(byts)
539+
same(cid.toString(), 'bafyreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu')
540+
same(remainder.byteLength, 0)
541+
})
542+
543+
test('remainder', () => {
544+
const byts = fromHex('01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad0102030405')
545+
const [cid, remainder] = CID.decodeFirst(byts)
546+
same(cid.toString(), 'bafyreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu')
547+
same(toHex(remainder), '0102030405')
548+
})
549+
})
550+
})
551+
485552
test('new CID from old CID', async () => {
486553
const hash = await sha256.digest(Buffer.from('abc'))
487554
const cid = CID.asCID(new OLDCID(1, 'raw', Buffer.from(hash.bytes)))
@@ -527,6 +594,7 @@ describe('CID', () => {
527594
const encoded = varint.encodeTo(2, new Uint8Array(32))
528595
await testThrow(() => CID.decode(encoded), 'Invalid CID version 2')
529596
})
597+
530598
test('buffer', async () => {
531599
const hash = await sha256.digest(Buffer.from('abc'))
532600
const cid = CID.create(1, 112, hash)

0 commit comments

Comments
 (0)