Skip to content

Commit

Permalink
fix: Array buffer view (#29)
Browse files Browse the repository at this point in the history
* feat: use ArrayBuffer.isView for type detection

* chore: use asCID instead of deprecated isCID

* chore: add jsdoc comments

* feat: represent CIDs as array buffer view
  • Loading branch information
Gozala authored Aug 4, 2020
1 parent 3e7fbad commit 5d5fcc1
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 190 deletions.
13 changes: 3 additions & 10 deletions bytes.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,17 @@ const equals = (aa, bb) => {
return true
}

const TypedArray = Object.getPrototypeOf(Int8Array)
const isTypedArray = obj => obj instanceof TypedArray

const coerce = o => {
if (o instanceof Uint8Array && o.constructor.name === 'Uint8Array') return o
if (o instanceof ArrayBuffer) return new Uint8Array(o)
if (o instanceof DataView || isTypedArray(o)) {
if (ArrayBuffer.isView(o)) {
return new Uint8Array(o.buffer, o.byteOffset, o.byteLength)
}
throw new Error('Unknown type, must be binary type')
}

const isBinary = o => {
if (o instanceof DataView) return true
if (o instanceof ArrayBuffer) return true
if (isTypedArray(o)) return true
return false
}
const isBinary = o =>
o instanceof ArrayBuffer || ArrayBuffer.isView(o)

const fromString = str => (new TextEncoder()).encode(str)
const toString = b => (new TextDecoder()).decode(b)
Expand Down
281 changes: 190 additions & 91 deletions cid.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as bytes from 'multiformats/bytes.js'

const readonly = (object, key, value) => {
Object.defineProperty(object, key, {
value,
writable: false,
enumerable: true
})
}
import * as Bytes from 'multiformats/bytes.js'

const property = (value, { writable = false, enumerable = true, configurable = false } = {}) => ({
value,
writable,
enumerable,
configurable
})

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

/**
* @param {import('./index').Multiformats} multiformats
*/
export default multiformats => {
const { multibase, varint, multihash } = multiformats
const parse = buff => {
const [code, length] = varint.decode(buff)
return [code, buff.slice(length)]

/**
* @param {number} version
* @param {number} codec
* @param {Uint8Array} multihash
* @returns {Uint8Array}
*/
const encodeCID = (version, codec, multihash) => {
const versionBytes = varint.encode(version)
const codecBytes = varint.encode(codec)
const bytes = new Uint8Array(versionBytes.byteLength + codecBytes.byteLength + multihash.byteLength)
bytes.set(versionBytes, 0)
bytes.set(codecBytes, versionBytes.byteLength)
bytes.set(multihash, versionBytes.byteLength + codecBytes.byteLength)
return bytes
}
const encode = (version, codec, multihash) => {
return Uint8Array.from([
...varint.encode(version),
...varint.encode(codec),
...multihash
])

/**
* Takes `Uint8Array` representation of `CID` and returns
* `[version, codec, multihash]`. Throws error if bytes passed do not
* correspond to vaild `CID`.
* @param {Uint8Array} bytes
* @returns {[number, number, Uint8Array]}
*/
const decodeCID = (bytes) => {
const [version, offset] = varint.decode(bytes)
switch (version) {
// CIDv0
case 18: {
return [0, 0x70, bytes]
}
// CIDv1
case 1: {
const [code, length] = varint.decode(bytes.subarray(offset))
return [1, code, decodeMultihash(bytes.subarray(offset + length))]
}
default: {
throw new RangeError(`Invalid CID version ${version}`)
}
}
}

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

class CID {
constructor (cid, ...args) {
Object.defineProperty(this, '_baseCache', {
value: new Map(),
writable: false,
enumerable: false
})
readonly(this, 'asCID', this)
if (cid != null && cid[cidSymbol] === true) {
readonly(this, 'version', cid.version)
readonly(this, 'multihash', bytes.coerce(cid.multihash))
readonly(this, 'buffer', bytes.coerce(cid.buffer))
if (cid.code) readonly(this, 'code', cid.code)
else readonly(this, 'code', multiformats.get(cid.codec).code)
return
/**
* Create CID from the string encoded CID.
* @param {string} string
* @returns {CID}
*/
const fromString = (string) => {
switch (string[0]) {
// V0
case 'Q': {
const cid = new CID(multibase.get('base58btc').decode(string))
cid._baseCache.set('base58btc', string)
return cid
}
if (args.length > 0) {
if (typeof args[0] !== 'number') throw new Error('String codecs are no longer supported')
readonly(this, 'version', cid)
readonly(this, 'code', args.shift())
if (this.version === 0 && this.code !== 112) {
throw new Error('Version 0 CID must be 112 codec (dag-cbor)')
}
this._multihash = args.shift()
if (args.length) throw new Error('No longer supported, cannot specify base encoding in instantiation')
if (this.version === 0) readonly(this, 'buffer', this.multihash)
else readonly(this, 'buffer', encode(this.version, this.code, this.multihash))
return
default: {
// CID v1
const cid = new CID(multibase.decode(string))
cid._baseCache.set(multibase.encoding(string).name, string)
return cid
}
if (typeof cid === 'string') {
if (cid.startsWith('Q')) {
readonly(this, 'version', 0)
readonly(this, 'code', 0x70)
const { decode } = multibase.get('base58btc')
this._multihash = decode(cid)
readonly(this, 'buffer', this.multihash)
return
}
}

/**
* Takes a hashCID multihash and validates the digest. Returns it back if
* all good otherwise throws error.
* @param {Uint8Array} hash
* @returns {Uint8Array}
*/
const decodeMultihash = (hash) => {
const { digest, length } = multihash.decode(hash)
if (digest.length !== length) {
throw new Error('Given multihash has incorrect length')
}

return hash
}

/**
* @implements {ArrayBufferView}
*/
class CID {
/**
* Creates new CID from the given value that is either CID, string or an
* Uint8Array.
* @param {CID|string|Uint8Array} value
*/
static from (value) {
if (typeof value === 'string') {
return fromString(value)
} else if (value instanceof Uint8Array) {
return new CID(value)
} else {
const cid = CID.asCID(value)
if (cid) {
// If we got the same CID back we create a copy.
if (cid === value) {
return new CID(cid.bytes)
} else {
return cid
}
} else {
throw new TypeError(`Can not create CID from given value ${value}`)
}
const { name } = multibase.encoding(cid)
this._baseCache.set(name, cid)
cid = multibase.decode(cid)
}
cid = bytes.coerce(cid)
readonly(this, 'buffer', cid)
let code
;[code, cid] = parse(cid)
if (code === 18) {
// CIDv0
readonly(this, 'version', 0)
readonly(this, 'code', 0x70)
this._multihash = this.buffer
return
}

/**
* Creates new CID with a given version, codec and a multihash.
* @param {number} version
* @param {number} code
* @param {Uint8Array} multihash
*/
static create (version, code, multihash) {
if (typeof code !== 'number') {
throw new Error('String codecs are no longer supported')
}

switch (version) {
case 0: {
if (code !== 112) {
throw new Error('Version 0 CID must be 112 codec (dag-cbor)')
} else {
return new CID(multihash)
}
}
case 1: {
// TODO: Figure out why we check digest here but not in v 0
return new CID(encodeCID(version, code, decodeMultihash(multihash)))
}
default: {
throw new Error('Invalid version')
}
}
if (code > 1) throw new Error(`Invalid CID version ${code}`)
readonly(this, 'version', code)
;[code, cid] = parse(cid)
readonly(this, 'code', code)
this._multihash = cid
}

set _multihash (hash) {
const { length, digest } = multihash.decode(hash)
if (digest.length !== length) throw new Error('Incorrect length')
readonly(this, 'multihash', hash)
/**
*
* @param {ArrayBuffer|Uint8Array} buffer
* @param {number} [byteOffset=0]
* @param {number} [byteLength=buffer.byteLength]
*/
constructor (buffer, byteOffset = 0, byteLength = buffer.byteLength) {
const bytes = buffer instanceof Uint8Array
? Bytes.coerce(buffer) // Just in case it's a node Buffer
: new Uint8Array(buffer, byteOffset, byteLength)

const [version, code, multihash] = decodeCID(bytes)
Object.defineProperties(this, {
// ArrayBufferView
buffer: property(bytes.buffer, { enumerable: false }),
byteOffset: property(bytes.byteOffset, { enumerable: false }),
byteLength: property(bytes.byteLength, { enumerable: false }),

// CID fields
version: property(version),
code: property(code),
multihash: property(multihash),
asCID: property(this),

// Legacy
bytes: property(bytes, { enumerable: false }),

// Internal
_baseCache: property(new Map(), { enumerable: false })
})
}

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

return new CID(0, this.code, this.multihash)
return CID.create(0, this.code, this.multihash)
}

toV1 () {
return new CID(1, this.code, this.multihash)
return CID.create(1, this.code, this.multihash)
}

get toBaseEncodedString () {
Expand All @@ -159,17 +252,25 @@ export default multiformats => {
}

toString (base) {
if (this.version === 0) {
const { version, bytes } = this
if (version === 0) {
if (base && base !== 'base58btc') {
throw new Error(`Cannot string encode V0 in ${base} encoding`)
}
const { encode } = multibase.get('base58btc')
return encode(this.buffer)
return encode(bytes)
}

base = base || 'base32'
const { _baseCache } = this
const string = _baseCache.get(base)
if (string == null) {
const string = multibase.encode(bytes, base)
_baseCache.set(base, string)
return string
} else {
return string
}
if (!base) base = 'base32'
if (this._baseCache.has(base)) return this._baseCache.get(base)
this._baseCache.set(base, multibase.encode(this.buffer, base))
return this._baseCache.get(base)
}

toJSON () {
Expand All @@ -183,17 +284,13 @@ export default multiformats => {
equals (other) {
return this.code === other.code &&
this.version === other.version &&
bytes.equals(this.multihash, other.multihash)
Bytes.equals(this.multihash, other.multihash)
}

get [Symbol.toStringTag] () {
return 'CID'
}

get [cidSymbol] () {
return true
}

/**
* Takes any input `value` and returns a `CID` instance if it was
* a `CID` otherwise returns `null`. If `value` is instanceof `CID`
Expand All @@ -217,12 +314,14 @@ export default multiformats => {
// API.
} else if (value != null && value.asCID === value) {
const { version, code, multihash } = value
return new CID(version, code, multihash)
return CID.create(version, code, multihash)
// If value is a CID from older implementation that used to be tagged via
// symbol we still rebase it to the this `CID` implementation by
// delegating that to a constructor.
} else if (value != null && value[cidSymbol] === true) {
return new CID(value)
const { version, multihash } = value
const code = value.code || multiformats.get(value.codec).code
return new CID(encodeCID(version, code, multihash))
// Otherwise value is not a CID (or an incompatible version of it) in
// which case we return `null`.
} else {
Expand All @@ -232,7 +331,7 @@ export default multiformats => {

static isCID (value) {
deprecate(/^0\.0/, IS_CID_DEPRECATION)
return !!(value && value[cidSymbol])
return !!(value && (value[cidSymbol] || value.asCID === value))
}
}

Expand Down
Loading

0 comments on commit 5d5fcc1

Please sign in to comment.