multiformats
Version:
Interface for multihash, multicodec, multibase and CID
348 lines (317 loc) • 8.87 kB
JavaScript
import basex from '../../vendor/base-x.js'
import { coerce } from '../bytes.js'
// Linter can't see that API is used in types.
// eslint-disable-next-line
import * as API from './interface.js'
/**
* Class represents both BaseEncoder and MultibaseEncoder meaning it
* can be used to encode to multibase or base encode without multibase
* prefix.
*
* @class
* @template {string} Base
* @template {string} Prefix
* @implements {API.MultibaseEncoder<Prefix>}
* @implements {API.BaseEncoder}
*/
class Encoder {
/**
* @param {Base} name
* @param {Prefix} prefix
* @param {(bytes:Uint8Array) => string} baseEncode
*/
constructor (name, prefix, baseEncode) {
this.name = name
this.prefix = prefix
this.baseEncode = baseEncode
}
/**
* @param {Uint8Array} bytes
* @returns {API.Multibase<Prefix>}
*/
encode (bytes) {
if (bytes instanceof Uint8Array) {
return `${this.prefix}${this.baseEncode(bytes)}`
} else {
throw Error('Unknown type, must be binary type')
}
}
}
/**
* @template {string} Prefix
*/
/**
* Class represents both BaseDecoder and MultibaseDecoder so it could be used
* to decode multibases (with matching prefix) or just base decode strings
* with corresponding base encoding.
*
* @class
* @template {string} Base
* @template {string} Prefix
* @implements {API.MultibaseDecoder<Prefix>}
* @implements {API.UnibaseDecoder<Prefix>}
* @implements {API.BaseDecoder}
*/
class Decoder {
/**
* @param {Base} name
* @param {Prefix} prefix
* @param {(text:string) => Uint8Array} baseDecode
*/
constructor (name, prefix, baseDecode) {
this.name = name
this.prefix = prefix
/* c8 ignore next 3 */
if (prefix.codePointAt(0) === undefined) {
throw new Error('Invalid prefix character')
}
/** @private */
this.prefixCodePoint = /** @type {number} */ (prefix.codePointAt(0))
this.baseDecode = baseDecode
}
/**
* @param {string} text
*/
decode (text) {
if (typeof text === 'string') {
if (text.codePointAt(0) !== this.prefixCodePoint) {
throw Error(`Unable to decode multibase string ${JSON.stringify(text)}, ${this.name} decoder only supports inputs prefixed with ${this.prefix}`)
}
return this.baseDecode(text.slice(this.prefix.length))
} else {
throw Error('Can only multibase decode strings')
}
}
/**
* @template {string} OtherPrefix
* @param {API.UnibaseDecoder<OtherPrefix>|ComposedDecoder<OtherPrefix>} decoder
* @returns {ComposedDecoder<Prefix|OtherPrefix>}
*/
or (decoder) {
return or(this, decoder)
}
}
/**
* @template {string} Prefix
* @typedef {Record<Prefix, API.UnibaseDecoder<Prefix>>} Decoders
*/
/**
* @template {string} Prefix
* @implements {API.MultibaseDecoder<Prefix>}
* @implements {API.CombobaseDecoder<Prefix>}
*/
class ComposedDecoder {
/**
* @param {Decoders<Prefix>} decoders
*/
constructor (decoders) {
this.decoders = decoders
}
/**
* @template {string} OtherPrefix
* @param {API.UnibaseDecoder<OtherPrefix>|ComposedDecoder<OtherPrefix>} decoder
* @returns {ComposedDecoder<Prefix|OtherPrefix>}
*/
or (decoder) {
return or(this, decoder)
}
/**
* @param {string} input
* @returns {Uint8Array}
*/
decode (input) {
const prefix = /** @type {Prefix} */ (input[0])
const decoder = this.decoders[prefix]
if (decoder) {
return decoder.decode(input)
} else {
throw RangeError(`Unable to decode multibase string ${JSON.stringify(input)}, only inputs prefixed with ${Object.keys(this.decoders)} are supported`)
}
}
}
/**
* @template {string} L
* @template {string} R
* @param {API.UnibaseDecoder<L>|API.CombobaseDecoder<L>} left
* @param {API.UnibaseDecoder<R>|API.CombobaseDecoder<R>} right
* @returns {ComposedDecoder<L|R>}
*/
export const or = (left, right) => new ComposedDecoder(/** @type {Decoders<L|R>} */({
...(left.decoders || { [/** @type API.UnibaseDecoder<L> */(left).prefix]: left }),
...(right.decoders || { [/** @type API.UnibaseDecoder<R> */(right).prefix]: right })
}))
/**
* @class
* @template {string} Base
* @template {string} Prefix
* @implements {API.MultibaseCodec<Prefix>}
* @implements {API.MultibaseEncoder<Prefix>}
* @implements {API.MultibaseDecoder<Prefix>}
* @implements {API.BaseCodec}
* @implements {API.BaseEncoder}
* @implements {API.BaseDecoder}
*/
export class Codec {
/**
* @param {Base} name
* @param {Prefix} prefix
* @param {(bytes:Uint8Array) => string} baseEncode
* @param {(text:string) => Uint8Array} baseDecode
*/
constructor (name, prefix, baseEncode, baseDecode) {
this.name = name
this.prefix = prefix
this.baseEncode = baseEncode
this.baseDecode = baseDecode
this.encoder = new Encoder(name, prefix, baseEncode)
this.decoder = new Decoder(name, prefix, baseDecode)
}
/**
* @param {Uint8Array} input
*/
encode (input) {
return this.encoder.encode(input)
}
/**
* @param {string} input
*/
decode (input) {
return this.decoder.decode(input)
}
}
/**
* @template {string} Base
* @template {string} Prefix
* @param {object} options
* @param {Base} options.name
* @param {Prefix} options.prefix
* @param {(bytes:Uint8Array) => string} options.encode
* @param {(input:string) => Uint8Array} options.decode
* @returns {Codec<Base, Prefix>}
*/
export const from = ({ name, prefix, encode, decode }) =>
new Codec(name, prefix, encode, decode)
/**
* @template {string} Base
* @template {string} Prefix
* @param {object} options
* @param {Base} options.name
* @param {Prefix} options.prefix
* @param {string} options.alphabet
* @returns {Codec<Base, Prefix>}
*/
export const baseX = ({ prefix, name, alphabet }) => {
const { encode, decode } = basex(alphabet, name)
return from({
prefix,
name,
encode,
/**
* @param {string} text
*/
decode: text => coerce(decode(text))
})
}
/**
* @param {string} string
* @param {string} alphabet
* @param {number} bitsPerChar
* @param {string} name
* @returns {Uint8Array}
*/
const decode = (string, alphabet, bitsPerChar, name) => {
// Build the character lookup table:
/** @type {Record<string, number>} */
const codes = {}
for (let i = 0; i < alphabet.length; ++i) {
codes[alphabet[i]] = i
}
// Count the padding bytes:
let end = string.length
while (string[end - 1] === '=') {
--end
}
// Allocate the output:
const out = new Uint8Array((end * bitsPerChar / 8) | 0)
// Parse the data:
let bits = 0 // Number of bits currently in the buffer
let buffer = 0 // Bits waiting to be written out, MSB first
let written = 0 // Next byte to write
for (let i = 0; i < end; ++i) {
// Read one character from the string:
const value = codes[string[i]]
if (value === undefined) {
throw new SyntaxError(`Non-${name} character`)
}
// Append the bits to the buffer:
buffer = (buffer << bitsPerChar) | value
bits += bitsPerChar
// Write out some bits if the buffer has a byte's worth:
if (bits >= 8) {
bits -= 8
out[written++] = 0xff & (buffer >> bits)
}
}
// Verify that we have received just enough bits:
if (bits >= bitsPerChar || 0xff & (buffer << (8 - bits))) {
throw new SyntaxError('Unexpected end of data')
}
return out
}
/**
* @param {Uint8Array} data
* @param {string} alphabet
* @param {number} bitsPerChar
* @returns {string}
*/
const encode = (data, alphabet, bitsPerChar) => {
const pad = alphabet[alphabet.length - 1] === '='
const mask = (1 << bitsPerChar) - 1
let out = ''
let bits = 0 // Number of bits currently in the buffer
let buffer = 0 // Bits waiting to be written out, MSB first
for (let i = 0; i < data.length; ++i) {
// Slurp data into the buffer:
buffer = (buffer << 8) | data[i]
bits += 8
// Write out as much as we can:
while (bits > bitsPerChar) {
bits -= bitsPerChar
out += alphabet[mask & (buffer >> bits)]
}
}
// Partial character:
if (bits) {
out += alphabet[mask & (buffer << (bitsPerChar - bits))]
}
// Add padding characters until we hit a byte boundary:
if (pad) {
while ((out.length * bitsPerChar) & 7) {
out += '='
}
}
return out
}
/**
* RFC4648 Factory
*
* @template {string} Base
* @template {string} Prefix
* @param {object} options
* @param {Base} options.name
* @param {Prefix} options.prefix
* @param {string} options.alphabet
* @param {number} options.bitsPerChar
*/
export const rfc4648 = ({ name, prefix, bitsPerChar, alphabet }) => {
return from({
prefix,
name,
encode (input) {
return encode(input, alphabet, bitsPerChar)
},
decode (input) {
return decode(input, alphabet, bitsPerChar, name)
}
})
}