Skip to content

Commit ce9fa07

Browse files
committed
feat: represent CIDs as array buffer view
1 parent 3d53343 commit ce9fa07

File tree

3 files changed

+279
-174
lines changed

3 files changed

+279
-174
lines changed

cid.js

Lines changed: 190 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import * as bytes from 'multiformats/bytes.js'
2-
3-
const readonly = (object, key, value) => {
4-
Object.defineProperty(object, key, {
5-
value,
6-
writable: false,
7-
enumerable: true
8-
})
9-
}
1+
import * as Bytes from 'multiformats/bytes.js'
2+
3+
const property = (value, { writable = false, enumerable = true, configurable = false } = {}) => ({
4+
value,
5+
writable,
6+
enumerable,
7+
configurable
8+
})
109

1110
// ESM does not support importing package.json where this version info
1211
// should come from. To workaround it version is copied here.
@@ -38,86 +37,180 @@ if (cid) {
3837
}
3938
`
4039

40+
/**
41+
* @param {import('./index').Multiformats} multiformats
42+
*/
4143
export default multiformats => {
4244
const { multibase, varint, multihash } = multiformats
43-
const parse = buff => {
44-
const [code, length] = varint.decode(buff)
45-
return [code, buff.slice(length)]
45+
46+
/**
47+
* @param {number} version
48+
* @param {number} codec
49+
* @param {Uint8Array} multihash
50+
* @returns {Uint8Array}
51+
*/
52+
const encodeCID = (version, codec, multihash) => {
53+
const versionBytes = varint.encode(version)
54+
const codecBytes = varint.encode(codec)
55+
const bytes = new Uint8Array(versionBytes.byteLength + codecBytes.byteLength + multihash.byteLength)
56+
bytes.set(versionBytes, 0)
57+
bytes.set(codecBytes, versionBytes.byteLength)
58+
bytes.set(multihash, versionBytes.byteLength + codecBytes.byteLength)
59+
return bytes
4660
}
47-
const encode = (version, codec, multihash) => {
48-
return Uint8Array.from([
49-
...varint.encode(version),
50-
...varint.encode(codec),
51-
...multihash
52-
])
61+
62+
/**
63+
* Takes `Uint8Array` representation of `CID` and returns
64+
* `[version, codec, multihash]`. Throws error if bytes passed do not
65+
* correspond to vaild `CID`.
66+
* @param {Uint8Array} bytes
67+
* @returns {[number, number, Uint8Array]}
68+
*/
69+
const decodeCID = (bytes) => {
70+
const [version, offset] = varint.decode(bytes)
71+
switch (version) {
72+
// CIDv0
73+
case 18: {
74+
return [0, 0x70, bytes]
75+
}
76+
// CIDv1
77+
case 1: {
78+
const [code, length] = varint.decode(bytes.subarray(offset))
79+
return [1, code, decodeMultihash(bytes.subarray(offset + length))]
80+
}
81+
default: {
82+
throw new RangeError(`Invalid CID version ${version}`)
83+
}
84+
}
5385
}
5486

5587
const cidSymbol = Symbol.for('@ipld/js-cid/CID')
5688

57-
class CID {
58-
constructor (cid, ...args) {
59-
Object.defineProperty(this, '_baseCache', {
60-
value: new Map(),
61-
writable: false,
62-
enumerable: false
63-
})
64-
readonly(this, 'asCID', this)
65-
if (cid != null && cid[cidSymbol] === true) {
66-
readonly(this, 'version', cid.version)
67-
readonly(this, 'multihash', bytes.coerce(cid.multihash))
68-
readonly(this, 'buffer', bytes.coerce(cid.buffer))
69-
if (cid.code) readonly(this, 'code', cid.code)
70-
else readonly(this, 'code', multiformats.get(cid.codec).code)
71-
return
89+
/**
90+
* Create CID from the string encoded CID.
91+
* @param {string} string
92+
* @returns {CID}
93+
*/
94+
const fromString = (string) => {
95+
switch (string[0]) {
96+
// V0
97+
case 'Q': {
98+
const cid = new CID(multibase.get('base58btc').decode(string))
99+
cid._baseCache.set('base58btc', string)
100+
return cid
72101
}
73-
if (args.length > 0) {
74-
if (typeof args[0] !== 'number') throw new Error('String codecs are no longer supported')
75-
readonly(this, 'version', cid)
76-
readonly(this, 'code', args.shift())
77-
if (this.version === 0 && this.code !== 112) {
78-
throw new Error('Version 0 CID must be 112 codec (dag-cbor)')
79-
}
80-
this._multihash = args.shift()
81-
if (args.length) throw new Error('No longer supported, cannot specify base encoding in instantiation')
82-
if (this.version === 0) readonly(this, 'buffer', this.multihash)
83-
else readonly(this, 'buffer', encode(this.version, this.code, this.multihash))
84-
return
102+
default: {
103+
// CID v1
104+
const cid = new CID(multibase.decode(string))
105+
cid._baseCache.set(multibase.encoding(string).name, string)
106+
return cid
85107
}
86-
if (typeof cid === 'string') {
87-
if (cid.startsWith('Q')) {
88-
readonly(this, 'version', 0)
89-
readonly(this, 'code', 0x70)
90-
const { decode } = multibase.get('base58btc')
91-
this._multihash = decode(cid)
92-
readonly(this, 'buffer', this.multihash)
93-
return
108+
}
109+
}
110+
111+
/**
112+
* Takes a hashCID multihash and validates the digest. Returns it back if
113+
* all good otherwise throws error.
114+
* @param {Uint8Array} hash
115+
* @returns {Uint8Array}
116+
*/
117+
const decodeMultihash = (hash) => {
118+
const { digest, length } = multihash.decode(hash)
119+
if (digest.length !== length) {
120+
throw new Error('Given multihash has incorrect length')
121+
}
122+
123+
return hash
124+
}
125+
126+
/**
127+
* @implements {ArrayBufferView}
128+
*/
129+
class CID {
130+
/**
131+
* Creates new CID from the given value that is either CID, string or an
132+
* Uint8Array.
133+
* @param {CID|string|Uint8Array} value
134+
*/
135+
static from (value) {
136+
if (typeof value === 'string') {
137+
return fromString(value)
138+
} else if (value instanceof Uint8Array) {
139+
return new CID(value)
140+
} else {
141+
const cid = CID.asCID(value)
142+
if (cid) {
143+
// If we got the same CID back we create a copy.
144+
if (cid === value) {
145+
return new CID(cid.bytes)
146+
} else {
147+
return cid
148+
}
149+
} else {
150+
throw new TypeError(`Can not create CID from given value ${value}`)
94151
}
95-
const { name } = multibase.encoding(cid)
96-
this._baseCache.set(name, cid)
97-
cid = multibase.decode(cid)
98152
}
99-
cid = bytes.coerce(cid)
100-
readonly(this, 'buffer', cid)
101-
let code
102-
;[code, cid] = parse(cid)
103-
if (code === 18) {
104-
// CIDv0
105-
readonly(this, 'version', 0)
106-
readonly(this, 'code', 0x70)
107-
this._multihash = this.buffer
108-
return
153+
}
154+
155+
/**
156+
* Creates new CID with a given version, codec and a multihash.
157+
* @param {number} version
158+
* @param {number} code
159+
* @param {Uint8Array} multihash
160+
*/
161+
static create (version, code, multihash) {
162+
if (typeof code !== 'number') {
163+
throw new Error('String codecs are no longer supported')
164+
}
165+
166+
switch (version) {
167+
case 0: {
168+
if (code !== 112) {
169+
throw new Error('Version 0 CID must be 112 codec (dag-cbor)')
170+
} else {
171+
return new CID(multihash)
172+
}
173+
}
174+
case 1: {
175+
// TODO: Figure out why we check digest here but not in v 0
176+
return new CID(encodeCID(version, code, decodeMultihash(multihash)))
177+
}
178+
default: {
179+
throw new Error('Invalid version')
180+
}
109181
}
110-
if (code > 1) throw new Error(`Invalid CID version ${code}`)
111-
readonly(this, 'version', code)
112-
;[code, cid] = parse(cid)
113-
readonly(this, 'code', code)
114-
this._multihash = cid
115182
}
116183

117-
set _multihash (hash) {
118-
const { length, digest } = multihash.decode(hash)
119-
if (digest.length !== length) throw new Error('Incorrect length')
120-
readonly(this, 'multihash', hash)
184+
/**
185+
*
186+
* @param {ArrayBuffer|Uint8Array} buffer
187+
* @param {number} [byteOffset=0]
188+
* @param {number} [byteLength=buffer.byteLength]
189+
*/
190+
constructor (buffer, byteOffset = 0, byteLength = buffer.byteLength) {
191+
const bytes = buffer instanceof Uint8Array
192+
? Bytes.coerce(buffer) // Just in case it's a node Buffer
193+
: new Uint8Array(buffer, byteOffset, byteLength)
194+
195+
const [version, code, multihash] = decodeCID(bytes)
196+
Object.defineProperties(this, {
197+
// ArrayBufferView
198+
buffer: property(bytes.buffer, { enumerable: false }),
199+
byteOffset: property(bytes.byteOffset, { enumerable: false }),
200+
byteLength: property(bytes.byteLength, { enumerable: false }),
201+
202+
// CID fields
203+
version: property(version),
204+
code: property(code),
205+
multihash: property(multihash),
206+
asCID: property(this),
207+
208+
// Legacy
209+
bytes: property(bytes, { enumerable: false }),
210+
211+
// Internal
212+
_baseCache: property(new Map(), { enumerable: false })
213+
})
121214
}
122215

123216
get codec () {
@@ -143,11 +236,11 @@ export default multiformats => {
143236
throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0')
144237
}
145238

146-
return new CID(0, this.code, this.multihash)
239+
return CID.create(0, this.code, this.multihash)
147240
}
148241

149242
toV1 () {
150-
return new CID(1, this.code, this.multihash)
243+
return CID.create(1, this.code, this.multihash)
151244
}
152245

153246
get toBaseEncodedString () {
@@ -159,17 +252,25 @@ export default multiformats => {
159252
}
160253

161254
toString (base) {
162-
if (this.version === 0) {
255+
const { version, bytes } = this
256+
if (version === 0) {
163257
if (base && base !== 'base58btc') {
164258
throw new Error(`Cannot string encode V0 in ${base} encoding`)
165259
}
166260
const { encode } = multibase.get('base58btc')
167-
return encode(this.buffer)
261+
return encode(bytes)
262+
}
263+
264+
base = base || 'base32'
265+
const { _baseCache } = this
266+
const string = _baseCache.get(base)
267+
if (string == null) {
268+
const string = multibase.encode(bytes, base)
269+
_baseCache.set(base, string)
270+
return string
271+
} else {
272+
return string
168273
}
169-
if (!base) base = 'base32'
170-
if (this._baseCache.has(base)) return this._baseCache.get(base)
171-
this._baseCache.set(base, multibase.encode(this.buffer, base))
172-
return this._baseCache.get(base)
173274
}
174275

175276
toJSON () {
@@ -183,17 +284,13 @@ export default multiformats => {
183284
equals (other) {
184285
return this.code === other.code &&
185286
this.version === other.version &&
186-
bytes.equals(this.multihash, other.multihash)
287+
Bytes.equals(this.multihash, other.multihash)
187288
}
188289

189290
get [Symbol.toStringTag] () {
190291
return 'CID'
191292
}
192293

193-
get [cidSymbol] () {
194-
return true
195-
}
196-
197294
/**
198295
* Takes any input `value` and returns a `CID` instance if it was
199296
* a `CID` otherwise returns `null`. If `value` is instanceof `CID`
@@ -217,12 +314,14 @@ export default multiformats => {
217314
// API.
218315
} else if (value != null && value.asCID === value) {
219316
const { version, code, multihash } = value
220-
return new CID(version, code, multihash)
317+
return CID.create(version, code, multihash)
221318
// If value is a CID from older implementation that used to be tagged via
222319
// symbol we still rebase it to the this `CID` implementation by
223320
// delegating that to a constructor.
224321
} else if (value != null && value[cidSymbol] === true) {
225-
return new CID(value)
322+
const { version, multihash } = value
323+
const code = value.code || multiformats.get(value.codec).code
324+
return new CID(encodeCID(version, code, multihash))
226325
// Otherwise value is not a CID (or an incompatible version of it) in
227326
// which case we return `null`.
228327
} else {
@@ -232,7 +331,7 @@ export default multiformats => {
232331

233332
static isCID (value) {
234333
deprecate(/^0\.0/, IS_CID_DEPRECATION)
235-
return !!(value && value[cidSymbol])
334+
return !!(value && (value[cidSymbol] || value.asCID === value))
236335
}
237336
}
238337

0 commit comments

Comments
 (0)