PHP WebShell
Текущая директория: /usr/lib/node_modules/bitgo/node_modules/micro-packed
Просмотр файла: index.ts
import { base64, bytes as baseBytes, hex as baseHex, str as baseStr, utf8 } from '@scure/base';
import type { Coder as BaseCoder } from '@scure/base';
/**
* TODO:
* - Holes, simplify pointers. Hole is some sized element which is skipped at encoding,
* but later other elements can write to it by path
* - Composite / tuple keys for dict
* - Web UI for easier debugging. We can wrap every coder to something that would write
* start & end positions to; and we can colorize specific bytes used by specific coder
*/
// Useful default values
export const EMPTY = /* @__PURE__ */ new Uint8Array(); // Empty bytes array
export const NULL = /* @__PURE__ */ new Uint8Array([0]); // NULL
// Non constant-time equality check.
export function equalBytes(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
}
export function isBytes(a: unknown): a is Bytes {
return (
a instanceof Uint8Array ||
(a != null && typeof a === 'object' && a.constructor.name === 'Uint8Array')
);
}
/**
* Copies several Uint8Arrays into one.
*/
export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
let sum = 0;
for (let i = 0; i < arrays.length; i++) {
const a = arrays[i];
if (!isBytes(a)) throw new Error('Uint8Array expected');
sum += a.length;
}
const res = new Uint8Array(sum);
for (let i = 0, pad = 0; i < arrays.length; i++) {
const a = arrays[i];
res.set(a, pad);
pad += a.length;
}
return res;
}
// Types
export type Bytes = Uint8Array;
export type Option<T> = T | undefined;
export interface Coder<F, T> {
encode(from: F): T;
decode(to: T): F;
}
export interface BytesCoder<T> extends Coder<T, Bytes> {
size?: number; // Size hint element
encode: (data: T) => Bytes;
decode: (data: Bytes) => T;
}
export interface BytesCoderStream<T> {
size?: number;
encodeStream: (w: Writer, value: T) => void;
decodeStream: (r: Reader) => T;
}
export type CoderType<T> = BytesCoderStream<T> & BytesCoder<T>;
export type Sized<T> = CoderType<T> & { size: number };
export type UnwrapCoder<T> = T extends CoderType<infer U> ? U : T;
// NOTE: we can't have terminator separate function, since it won't know about boundaries
// E.g. array of U16LE ([1,2,3]) would be [1, 0, 2, 0, 3, 0]
// But terminator will find array at index '1', which happens to be inside of an element itself
export type Length = CoderType<number> | CoderType<bigint> | number | Bytes | string | null;
type ArrLike<T> = Array<T> | ReadonlyArray<T>;
// prettier-ignore
export type TypedArray =
| Uint8Array | Int8Array | Uint8ClampedArray
| Uint16Array | Int16Array
| Uint32Array | Int32Array;
// as const returns readonly stuff, remove readonly property
export type Writable<T> = T extends {}
? T extends TypedArray
? T
: {
-readonly [P in keyof T]: Writable<T[P]>;
}
: T;
export type Values<T> = T[keyof T];
export type NonUndefinedKey<T, K extends keyof T> = T[K] extends undefined ? never : K;
export type NullableKey<T, K extends keyof T> = T[K] extends NonNullable<T[K]> ? never : K;
// Opt: value !== undefined, but value === T|undefined
export type OptKey<T, K extends keyof T> = NullableKey<T, K> & NonUndefinedKey<T, K>;
export type ReqKey<T, K extends keyof T> = T[K] extends NonNullable<T[K]> ? K : never;
export type OptKeys<T> = Pick<T, { [K in keyof T]: OptKey<T, K> }[keyof T]>;
export type ReqKeys<T> = Pick<T, { [K in keyof T]: ReqKey<T, K> }[keyof T]>;
export type StructInput<T extends Record<string, any>> = { [P in keyof ReqKeys<T>]: T[P] } & {
[P in keyof OptKeys<T>]?: T[P];
};
export type StructRecord<T extends Record<string, any>> = {
[P in keyof T]: CoderType<T[P]>;
};
export type StructOut = Record<string, any>;
export type PadFn = (i: number) => number;
// Utils
// Small bitset structure to store position of ranges that have been read.
// Possible can be even more efficient by using some interval trees, but would be more complex
// Needs O(N/8) memory for parsing.
// Purpose: if there are pointers in parsed structure,
// they can cause read of two distinct ranges:
// [0-32, 64-128], which means 'pos' is not enough to handle them
const _bitset = {
BITS: 32,
FULL_MASK: -1 >>> 0, // 1<<32 will overflow
len: (len: number) => Math.ceil(len / 32),
create: (len: number) => new Uint32Array(_bitset.len(len)),
clean: (bs: Uint32Array) => bs.fill(0),
debug: (bs: Uint32Array) => Array.from(bs).map((i) => (i >>> 0).toString(2).padStart(32, '0')),
checkLen: (bs: Uint32Array, len: number) => {
if (_bitset.len(len) === bs.length) return;
throw new Error(`bitSet: wrong length=${bs.length}. Expected: ${_bitset.len(len)}`);
},
chunkLen: (bsLen: number, pos: number, len: number) => {
if (pos < 0) throw new Error(`bitset: wrong pos=${pos}`);
if (pos + len > bsLen) throw new Error(`bitSet: wrong range=${pos}/${len} of ${bsLen}`);
},
set: (bs: Uint32Array, chunk: number, value: number, allowRewrite = true) => {
if (!allowRewrite && (bs[chunk] & value) !== 0) return false;
bs[chunk] |= value;
return true;
},
pos: (pos: number, i: number) => ({
chunk: Math.floor((pos + i) / 32),
mask: 1 << (32 - ((pos + i) % 32) - 1),
}),
indices: (bs: Uint32Array, len: number, invert = false) => {
_bitset.checkLen(bs, len);
const { FULL_MASK, BITS } = _bitset;
const left = BITS - (len % BITS);
const lastMask = left ? (FULL_MASK >>> left) << left : FULL_MASK;
const res = [];
for (let i = 0; i < bs.length; i++) {
let c = bs[i];
if (invert) c = ~c; // allows to gen unset elements
// apply mask to last element, so we won't iterate non-existent items
if (i === bs.length - 1) c &= lastMask;
if (c === 0) continue; // fast-path
for (let j = 0; j < BITS; j++) {
const m = 1 << (BITS - j - 1);
if (c & m) res.push(i * BITS + j);
}
}
return res;
},
range: (arr: number[]) => {
const res = [];
let cur;
for (const i of arr) {
if (cur === undefined || i !== cur.pos + cur.length) res.push((cur = { pos: i, length: 1 }));
else cur.length += 1;
}
return res;
},
rangeDebug: (bs: Uint32Array, len: number, invert = false) =>
`[${_bitset
.range(_bitset.indices(bs, len, invert))
.map((i) => `(${i.pos}/${i.length})`)
.join(', ')}]`,
setRange: (bs: Uint32Array, bsLen: number, pos: number, len: number, allowRewrite = true) => {
_bitset.chunkLen(bsLen, pos, len);
const { FULL_MASK, BITS } = _bitset;
// Try to set range with maximum efficiency:
// - first chunk is always '0000[1111]' (only right ones)
// - middle chunks are set to '[1111 1111]' (all ones)
// - last chunk is always '[1111]0000' (only left ones)
// - max operations: (N/32) + 2 (first and last)
const first = pos % BITS ? Math.floor(pos / BITS) : undefined;
const lastPos = pos + len;
const last = lastPos % BITS ? Math.floor(lastPos / BITS) : undefined;
// special case, whole range inside single chunk
if (first !== undefined && first === last)
return _bitset.set(
bs,
first,
(FULL_MASK >>> (BITS - len)) << (BITS - len - pos),
allowRewrite
);
if (first !== undefined) {
if (!_bitset.set(bs, first, FULL_MASK >>> pos % BITS, allowRewrite)) return false; // first chunk
}
// middle chunks
const start = first !== undefined ? first + 1 : pos / BITS;
const end = last !== undefined ? last : lastPos / BITS;
for (let i = start; i < end; i++)
if (!_bitset.set(bs, i, FULL_MASK, allowRewrite)) return false;
if (last !== undefined && first !== last)
if (!_bitset.set(bs, last, FULL_MASK << (BITS - (lastPos % BITS)), allowRewrite))
return false; // last chunk
return true;
},
};
export type ReaderOpts = {
// If there are remaining unparsed bytes, the decoding is probably wrong.
// Or, unnecessary information was added. Perhaps, to fingerprint something.
allowUnreadBytes?: boolean;
// The check enforces parser termination.
// If pointers can read same region of memory multiple times,
// you can cause combinatorial explosion by creating
// array of pointers to same address and cause DoS.
allowMultipleReads?: boolean;
};
export class Reader {
pos = 0;
bitBuf = 0;
bitPos = 0;
private bs: Uint32Array | undefined;
constructor(
readonly data: Bytes,
readonly opts: ReaderOpts = {},
public path: StructOut[] = [],
public fieldPath: string[] = [],
private parent: Reader | undefined = undefined,
public parentOffset: number = 0
) {}
enablePtr(): void {
if (this.parent) return this.parent.enablePtr();
if (this.bs) return;
this.bs = _bitset.create(this.data.length);
_bitset.setRange(this.bs, this.data.length, 0, this.pos, this.opts.allowMultipleReads);
}
private markBytesBS(pos: number, len: number): boolean {
if (this.parent) return this.parent.markBytesBS(this.parentOffset + pos, len);
if (!len) return true;
if (!this.bs) return true;
return _bitset.setRange(this.bs, this.data.length, pos, len, false);
}
private markBytes(len: number): boolean {
const pos = this.pos;
this.pos += len;
const res = this.markBytesBS(pos, len);
if (!this.opts.allowMultipleReads && !res)
throw this.err(`multiple read pos=${this.pos} len=${len}`);
return res;
}
err(msg: string) {
return new Error(`Reader(${this.fieldPath.join('/')}): ${msg}`);
}
// read bytes by absolute offset
absBytes(n: number) {
if (n > this.data.length) throw new Error('absBytes: Unexpected end of buffer');
return this.data.subarray(n);
}
// return reader using offset
offsetReader(n: number) {
return new Reader(this.absBytes(n), this.opts, this.path, this.fieldPath, this, n);
}
bytes(n: number, peek = false) {
if (this.bitPos) throw this.err('readBytes: bitPos not empty');
if (!Number.isFinite(n)) throw this.err(`readBytes: wrong length=${n}`);
if (this.pos + n > this.data.length) throw this.err('readBytes: Unexpected end of buffer');
const slice = this.data.subarray(this.pos, this.pos + n);
if (!peek) this.markBytes(n);
return slice;
}
byte(peek = false): number {
if (this.bitPos) throw this.err('readByte: bitPos not empty');
if (this.pos + 1 > this.data.length) throw this.err('readBytes: Unexpected end of buffer');
const data = this.data[this.pos];
if (!peek) this.markBytes(1);
return data;
}
get leftBytes(): number {
return this.data.length - this.pos;
}
isEnd(): boolean {
return this.pos >= this.data.length && !this.bitPos;
}
length(len: Length): number {
let byteLen;
if (isCoder(len)) byteLen = Number(len.decodeStream(this));
else if (typeof len === 'number') byteLen = len;
else if (typeof len === 'string') byteLen = getPath(this.path, len.split('/'));
if (typeof byteLen === 'bigint') byteLen = Number(byteLen);
if (typeof byteLen !== 'number') throw this.err(`Wrong length: ${byteLen}`);
return byteLen;
}
// bits are read in BE mode (left to right): (0b1000_0000).readBits(1) == 1
bits(bits: number) {
if (bits > 32) throw this.err('BitReader: cannot read more than 32 bits in single call');
let out = 0;
while (bits) {
if (!this.bitPos) {
this.bitBuf = this.byte();
this.bitPos = 8;
}
const take = Math.min(bits, this.bitPos);
this.bitPos -= take;
out = (out << take) | ((this.bitBuf >> this.bitPos) & (2 ** take - 1));
this.bitBuf &= 2 ** this.bitPos - 1;
bits -= take;
}
// Fix signed integers
return out >>> 0;
}
find(needle: Bytes, pos = this.pos) {
if (!isBytes(needle)) throw this.err(`find: needle is not bytes! ${needle}`);
if (this.bitPos) throw this.err('findByte: bitPos not empty');
if (!needle.length) throw this.err(`find: needle is empty`);
// indexOf should be faster than full equalBytes check
for (let idx = pos; (idx = this.data.indexOf(needle[0], idx)) !== -1; idx++) {
if (idx === -1) return;
const leftBytes = this.data.length - idx;
if (leftBytes < needle.length) return;
if (equalBytes(needle, this.data.subarray(idx, idx + needle.length))) return idx;
}
return;
}
finish() {
if (this.opts.allowUnreadBytes) return;
if (this.bitPos) {
throw this.err(
`${this.bitPos} bits left after unpack: ${baseHex.encode(this.data.slice(this.pos))}`
);
}
if (this.bs && !this.parent) {
const notRead = _bitset.indices(this.bs, this.data.length, true);
if (notRead.length) {
const formatted = _bitset
.range(notRead)
.map(
({ pos, length }) =>
`(${pos}/${length})[${baseHex.encode(this.data.subarray(pos, pos + length))}]`
)
.join(', ');
throw this.err(`unread byte ranges: ${formatted} (total=${this.data.length})`);
} else return; // all bytes read, everything is ok
}
// Default: no pointers enabled
if (!this.isEnd()) {
throw this.err(
`${this.leftBytes} bytes ${this.bitPos} bits left after unpack: ${baseHex.encode(
this.data.slice(this.pos)
)}`
);
}
}
fieldPathPush(s: string) {
this.fieldPath.push(s);
}
fieldPathPop() {
this.fieldPath.pop();
}
}
export class Writer {
private buffers: Bytes[] = [];
pos: number = 0;
ptrs: { pos: number; ptr: CoderType<number>; buffer: Bytes }[] = [];
bitBuf = 0;
bitPos = 0;
constructor(
public path: StructOut[] = [],
public fieldPath: string[] = []
) {}
err(msg: string) {
return new Error(`Writer(${this.fieldPath.join('/')}): ${msg}`);
}
bytes(b: Bytes) {
if (this.bitPos) throw this.err('writeBytes: ends with non-empty bit buffer');
this.buffers.push(b);
this.pos += b.length;
}
byte(b: number) {
if (this.bitPos) throw this.err('writeByte: ends with non-empty bit buffer');
this.buffers.push(new Uint8Array([b]));
this.pos++;
}
get buffer(): Bytes {
if (this.bitPos) throw this.err('buffer: ends with non-empty bit buffer');
let buf = concatBytes(...this.buffers);
for (let ptr of this.ptrs) {
const pos = buf.length;
buf = concatBytes(buf, ptr.buffer);
const val = ptr.ptr.encode(pos);
for (let i = 0; i < val.length; i++) buf[ptr.pos + i] = val[i];
}
return buf;
}
length(len: Length, value: number) {
if (len === null) return;
if (isCoder(len)) return len.encodeStream(this, value);
let byteLen;
if (typeof len === 'number') byteLen = len;
else if (typeof len === 'string') byteLen = getPath(this.path, len.split('/'));
if (typeof byteLen === 'bigint') byteLen = Number(byteLen);
if (byteLen === undefined || byteLen !== value)
throw this.err(`Wrong length: ${byteLen} len=${len} exp=${value}`);
}
bits(value: number, bits: number) {
if (bits > 32) throw this.err('writeBits: cannot write more than 32 bits in single call');
if (value >= 2 ** bits) throw this.err(`writeBits: value (${value}) >= 2**bits (${bits})`);
while (bits) {
const take = Math.min(bits, 8 - this.bitPos);
this.bitBuf = (this.bitBuf << take) | (value >> (bits - take));
this.bitPos += take;
bits -= take;
value &= 2 ** bits - 1;
if (this.bitPos === 8) {
this.bitPos = 0;
this.buffers.push(new Uint8Array([this.bitBuf]));
this.pos++;
}
}
}
fieldPathPush(s: string) {
this.fieldPath.push(s);
}
fieldPathPop() {
this.fieldPath.pop();
}
}
// Immutable LE<->BE
const swap = (b: Bytes): Bytes => Uint8Array.from(b).reverse();
export function checkBounds(p: Writer | Reader, value: bigint, bits: bigint, signed: boolean) {
if (signed) {
// [-(2**(32-1)), 2**(32-1)-1]
const signBit = 2n ** (bits - 1n);
if (value < -signBit || value >= signBit) throw p.err('sInt: value out of bounds');
} else {
// [0, 2**32-1]
if (0n > value || value >= 2n ** bits) throw p.err('uInt: value out of bounds');
}
}
// Wrap stream encoder into generic encoder
export function wrap<T>(inner: BytesCoderStream<T>): BytesCoderStream<T> & BytesCoder<T> {
return {
...inner,
encode: (value: T): Bytes => {
const w = new Writer();
inner.encodeStream(w, value);
return w.buffer;
},
decode: (data: Bytes, opts: ReaderOpts = {}): T => {
const r = new Reader(data, opts);
const res = inner.decodeStream(r);
r.finish();
return res;
},
};
}
function getPath(objPath: Record<string, any>[], path: string[]): Option<any> {
objPath = Array.from(objPath);
let i = 0;
for (; i < path.length; i++) {
if (path[i] === '..') objPath.pop();
else break;
}
let cur = objPath.pop();
for (; i < path.length; i++) {
if (!cur || cur[path[i]] === undefined) return undefined;
cur = cur[path[i]];
}
return cur;
}
export function isCoder<T>(elm: any): elm is CoderType<T> {
return (
elm !== null &&
typeof elm === 'object' &&
typeof (elm as CoderType<T>).encode === 'function' &&
typeof (elm as CoderType<T>).encodeStream === 'function' &&
typeof (elm as CoderType<T>).decode === 'function' &&
typeof (elm as CoderType<T>).decodeStream === 'function'
);
}
// Coders (like in @scure/base) for common operations
// TODO:
// - move to base? very generic converters, not releated to base and packed
// - encode/decode -> from/to? coder->convert?
function dict<T>(): BaseCoder<[string, T][], Record<string, T>> {
return {
encode: (from: [string, T][]): Record<string, T> => {
const to: Record<string, T> = {};
for (const [name, value] of from) {
if (to[name] !== undefined)
throw new Error(`coders.dict: same key(${name}) appears twice in struct`);
to[name] = value;
}
return to;
},
decode: (to: Record<string, T>): [string, T][] => Object.entries(to),
};
}
// Safely converts bigint to number
// Sometimes pointers / tags use u64 or other big numbers which cannot be represented by number,
// but we still can use them since real value will be smaller than u32
const number: BaseCoder<bigint, number> = {
encode: (from: bigint): number => {
if (from > BigInt(Number.MAX_SAFE_INTEGER))
throw new Error(`coders.number: element bigger than MAX_SAFE_INTEGER=${from}`);
return Number(from);
},
decode: (to: number): bigint => {
if (!Number.isSafeInteger(to)) throw new Error('coders.number: element is not safe integer');
return BigInt(to);
},
};
// TODO: replace map with this?
type Enum = { [k: string]: number | string } & { [k: number]: string };
// Doesn't return numeric keys, so it's fine
type EnumKeys<T extends Enum> = keyof T;
function tsEnum<T extends Enum>(e: T): BaseCoder<number, EnumKeys<T>> {
return {
encode: (from: number): string => e[from],
decode: (to: string): number => e[to] as number,
};
}
function decimal(precision: number) {
const decimalMask = 10n ** BigInt(precision);
return {
encode: (from: bigint): string => {
let s = (from < 0n ? -from : from).toString(10);
let sep = s.length - precision;
if (sep < 0) {
s = s.padStart(s.length - sep, '0');
sep = 0;
}
let i = s.length - 1;
for (; i >= sep && s[i] === '0'; i--);
let [int, frac] = [s.slice(0, sep), s.slice(sep, i + 1)];
if (!int) int = '0';
if (from < 0n) int = '-' + int;
if (!frac) return int;
return `${int}.${frac}`;
},
decode: (to: string): bigint => {
let neg = false;
if (to.startsWith('-')) {
neg = true;
to = to.slice(1);
}
let sep = to.indexOf('.');
sep = sep === -1 ? to.length : sep;
const [intS, fracS] = [to.slice(0, sep), to.slice(sep + 1)];
const int = BigInt(intS) * decimalMask;
const fracLen = Math.min(fracS.length, precision);
const frac = BigInt(fracS.slice(0, fracLen)) * 10n ** BigInt(precision - fracLen);
const value = int + frac;
return neg ? -value : value;
},
};
}
// TODO: export from @scure/base?
type BaseInput<F> = F extends BaseCoder<infer T, any> ? T : never;
type BaseOutput<F> = F extends BaseCoder<any, infer T> ? T : never;
/**
* Allows to split big conditional coders into a small one; also sort of parser combinator:
*
* `encode = [Ae, Be]; decode = [Ad, Bd]`
* ->
* `match([{encode: Ae, decode: Ad}, {encode: Be; decode: Bd}])`
*
* 1. It is easier to reason: encode/decode of specific part are closer to each other
* 2. Allows composable coders and ability to add conditions on runtime
* @param lst
* @returns
*/
function match<
L extends BaseCoder<unknown | undefined, unknown | undefined>[],
I = { [K in keyof L]: NonNullable<BaseInput<L[K]>> }[number],
O = { [K in keyof L]: NonNullable<BaseOutput<L[K]>> }[number],
>(lst: L): BaseCoder<I, O> {
return {
encode: (from: I): O => {
for (const c of lst) {
const elm = c.encode(from);
if (elm !== undefined) return elm as O;
}
throw new Error(`match/encode: cannot find match in ${from}`);
},
decode: (to: O): I => {
for (const c of lst) {
const elm = c.decode(to);
if (elm !== undefined) return elm as I;
}
throw new Error(`match/decode: cannot find match in ${to}`);
},
};
}
// Reverse direction of coder
const reverse = <F, T>(coder: Coder<F, T>): Coder<T, F> => ({
encode: coder.decode,
decode: coder.encode,
});
export const coders = { dict, number, tsEnum, decimal, match, reverse };
// PackedCoders
export const bits = (len: number): CoderType<number> =>
wrap({
encodeStream: (w: Writer, value: number) => w.bits(value, len),
decodeStream: (r: Reader): number => r.bits(len),
});
// unsized bigint should be wrapped in container (bytes/etc)
// 0n = new Uint8Array([])
// 1n = new Uint8Array([1n])
// Please open issue, if you need different behavior for zero.
export const bigint = (size: number, le = false, signed = false, sized = true): CoderType<bigint> =>
wrap({
size: sized ? size : undefined,
encodeStream: (w: Writer, value: bigint) => {
if (typeof value !== 'bigint') throw w.err(`bigint: invalid value: ${value}`);
let _value = BigInt(value);
const bLen = BigInt(size);
checkBounds(w, _value, 8n * bLen, !!signed);
const signBit = 2n ** (8n * bLen - 1n);
if (signed && _value < 0) _value = _value | signBit;
let b = [];
for (let i = 0; i < size; i++) {
b.push(Number(_value & 255n));
_value >>= 8n;
}
let res = new Uint8Array(b).reverse();
if (!sized) {
let pos = 0;
for (pos = 0; pos < res.length; pos++) if (res[pos] !== 0) break;
res = res.subarray(pos); // remove leading zeros
}
w.bytes(le ? res.reverse() : res);
},
decodeStream: (r: Reader): bigint => {
const bLen = BigInt(size);
// TODO: for le we can read until first zero?
const value = r.bytes(sized ? size : Math.min(size, r.leftBytes));
const b = le ? value : swap(value);
const signBit = 2n ** (8n * bLen - 1n);
let res = 0n;
for (let i = 0; i < b.length; i++) res |= BigInt(b[i]) << (8n * BigInt(i));
if (signed && res & signBit) res = (res ^ signBit) - signBit;
checkBounds(r, res, 8n * bLen, !!signed);
return res;
},
});
export const U256LE = /* @__PURE__ */ bigint(32, true);
export const U256BE = /* @__PURE__ */ bigint(32, false);
export const I256LE = /* @__PURE__ */ bigint(32, true, true);
export const I256BE = /* @__PURE__ */ bigint(32, false, true);
export const U128LE = /* @__PURE__ */ bigint(16, true);
export const U128BE = /* @__PURE__ */ bigint(16, false);
export const I128LE = /* @__PURE__ */ bigint(16, true, true);
export const I128BE = /* @__PURE__ */ bigint(16, false, true);
export const U64LE = /* @__PURE__ */ bigint(8, true);
export const U64BE = /* @__PURE__ */ bigint(8, false);
export const I64LE = /* @__PURE__ */ bigint(8, true, true);
export const I64BE = /* @__PURE__ */ bigint(8, false, true);
// TODO: we can speed-up if integers are used. Unclear if it's worth to increase code size.
// Also, numbers can't use >= 32 bits.
export const int = (size: number, le = false, signed = false, sized = true): CoderType<number> => {
if (size > 6) throw new Error('int supports size up to 6 bytes (48 bits), for other use bigint');
return apply(bigint(size, le, signed, sized), coders.number);
};
export const U32LE = /* @__PURE__ */ int(4, true);
export const U32BE = /* @__PURE__ */ int(4, false);
export const I32LE = /* @__PURE__ */ int(4, true, true);
export const I32BE = /* @__PURE__ */ int(4, false, true);
export const U16LE = /* @__PURE__ */ int(2, true);
export const U16BE = /* @__PURE__ */ int(2, false);
export const I16LE = /* @__PURE__ */ int(2, true, true);
export const I16BE = /* @__PURE__ */ int(2, false, true);
export const U8 = /* @__PURE__ */ int(1, false);
export const I8 = /* @__PURE__ */ int(1, false, true);
export const bool: CoderType<boolean> = /* @__PURE__ */ wrap({
size: 1,
encodeStream: (w: Writer, value: boolean) => w.byte(value ? 1 : 0),
decodeStream: (r: Reader): boolean => {
const value = r.byte();
if (value !== 0 && value !== 1) throw r.err(`bool: invalid value ${value}`);
return value === 1;
},
});
// Can be done w array, but specific implementation should be
// faster: no need to create js array of numbers.
export const bytes = (len: Length, le = false): CoderType<Bytes> =>
wrap({
size: typeof len === 'number' ? len : undefined,
encodeStream: (w: Writer, value: Bytes) => {
if (!isBytes(value)) throw w.err(`bytes: invalid value ${value}`);
if (!isBytes(len)) w.length(len, value.length);
w.bytes(le ? swap(value) : value);
if (isBytes(len)) w.bytes(len);
},
decodeStream: (r: Reader): Bytes => {
let bytes: Bytes;
if (isBytes(len)) {
const tPos = r.find(len);
if (!tPos) throw r.err(`bytes: cannot find terminator`);
bytes = r.bytes(tPos - r.pos);
r.bytes(len.length);
} else bytes = r.bytes(len === null ? r.leftBytes : r.length(len));
return le ? swap(bytes) : bytes;
},
});
export const string = (len: Length, le = false): CoderType<string> => {
const inner = bytes(len, le);
return wrap({
size: inner.size,
encodeStream: (w: Writer, value: string) => inner.encodeStream(w, utf8.decode(value)),
decodeStream: (r: Reader): string => utf8.encode(inner.decodeStream(r)),
});
};
export const cstring = /* @__PURE__ */ string(NULL);
export const hex = (len: Length, le = false, withZero = false): CoderType<string> => {
const inner = bytes(len, le);
return wrap({
size: inner.size,
encodeStream: (w: Writer, value: string) => {
if (withZero && !value.startsWith('0x'))
throw new Error('hex(withZero=true).encode input should start with 0x');
const bytes = baseHex.decode(withZero ? value.slice(2) : value);
return inner.encodeStream(w, bytes);
},
decodeStream: (r: Reader): string =>
(withZero ? '0x' : '') + baseHex.encode(inner.decodeStream(r)),
});
};
// Interoperability with base
export function apply<T, F>(inner: CoderType<T>, b: BaseCoder<T, F>): CoderType<F> {
if (!isCoder(inner)) throw new Error(`apply: invalid inner value ${inner}`);
return wrap({
size: inner.size,
encodeStream: (w: Writer, value: F) => {
let innerValue;
try {
innerValue = b.decode(value);
} catch (e) {
throw w.err('' + e);
}
return inner.encodeStream(w, innerValue);
},
decodeStream: (r: Reader): F => {
const innerValue = inner.decodeStream(r);
try {
return b.encode(innerValue);
} catch (e) {
throw r.err('' + e);
}
},
});
}
// Additional check of values both on encode and decode steps.
// E.g. to force uint32 to be 1..10
export function validate<T>(inner: CoderType<T>, fn: (elm: T) => T): CoderType<T> {
if (!isCoder(inner)) throw new Error(`validate: invalid inner value ${inner}`);
return wrap({
size: inner.size,
encodeStream: (w: Writer, value: T) => inner.encodeStream(w, fn(value)),
decodeStream: (r: Reader): T => fn(inner.decodeStream(r)),
});
}
export function lazy<T>(fn: () => CoderType<T>): CoderType<T> {
return wrap({
encodeStream: (w: Writer, value: T) => fn().encodeStream(w, value),
decodeStream: (r: Reader): T => fn().decodeStream(r),
});
}
// TODO: export from base? Must support 0x in micro-base
type baseFmt =
| 'utf8'
| 'hex'
| 'base16'
| 'base32'
| 'base64'
| 'base64url'
| 'base58'
| 'base58xmr';
export const bytesFormatted = (len: Length, fmt: baseFmt, le = false) => {
const inner = bytes(len, le);
return wrap({
size: inner.size,
encodeStream: (w: Writer, value: string) => inner.encodeStream(w, baseBytes(fmt, value)),
decodeStream: (r: Reader): string => baseStr(fmt, inner.decodeStream(r)),
});
};
// Returns true if some marker exists, otherwise false. Xor argument flips behaviour
export const flag = (flagValue: Bytes, xor = false): CoderType<boolean> =>
wrap({
size: flagValue.length,
encodeStream: (w: Writer, value: boolean) => {
if (!!value !== xor) w.bytes(flagValue);
},
decodeStream: (r: Reader): boolean => {
let hasFlag = r.leftBytes >= flagValue.length;
if (hasFlag) {
hasFlag = equalBytes(r.bytes(flagValue.length, true), flagValue);
// Found flag, advance cursor position
if (hasFlag) r.bytes(flagValue.length);
}
// hasFlag ^ xor
return hasFlag !== xor;
},
});
// Decode/encode only if flag found
export function flagged<T>(
path: string | BytesCoderStream<boolean>,
inner: BytesCoderStream<T>,
def?: T
): CoderType<Option<T>> {
if (!isCoder(inner)) throw new Error(`flagged: invalid inner value ${inner}`);
return wrap({
encodeStream: (w: Writer, value: Option<T>) => {
if (typeof path === 'string') {
if (getPath(w.path, path.split('/'))) inner.encodeStream(w, value);
else if (def) inner.encodeStream(w, def);
} else {
path.encodeStream(w, !!value);
if (!!value) inner.encodeStream(w, value);
else if (def) inner.encodeStream(w, def);
}
},
decodeStream: (r: Reader): Option<T> => {
let hasFlag = false;
if (typeof path === 'string') hasFlag = getPath(r.path, path.split('/'));
else hasFlag = path.decodeStream(r);
// If there is a flag -- decode and return value
if (hasFlag) return inner.decodeStream(r);
else if (def) inner.decodeStream(r);
return;
},
});
}
export function optional<T>(
flag: BytesCoderStream<boolean>,
inner: BytesCoderStream<T>,
def?: T
): CoderType<Option<T>> {
if (!isCoder(flag) || !isCoder(inner))
throw new Error(`optional: invalid flag or inner value flag=${flag} inner=${inner}`);
return wrap({
size: def !== undefined && flag.size && inner.size ? flag.size + inner.size : undefined,
encodeStream: (w: Writer, value: Option<T>) => {
flag.encodeStream(w, !!value);
if (value) inner.encodeStream(w, value);
else if (def !== undefined) inner.encodeStream(w, def);
},
decodeStream: (r: Reader): Option<T> => {
if (flag.decodeStream(r)) return inner.decodeStream(r);
else if (def !== undefined) inner.decodeStream(r);
return;
},
});
}
export function magic<T>(inner: CoderType<T>, constant: T, check = true): CoderType<undefined> {
if (!isCoder(inner)) throw new Error(`flagged: invalid inner value ${inner}`);
return wrap({
size: inner.size,
encodeStream: (w: Writer, _value: undefined) => inner.encodeStream(w, constant),
decodeStream: (r: Reader): undefined => {
const value = inner.decodeStream(r);
if (
(check && typeof value !== 'object' && value !== constant) ||
(isBytes(constant) && !equalBytes(constant, value as any))
) {
throw r.err(`magic: invalid value: ${value} !== ${constant}`);
}
return;
},
});
}
export const magicBytes = (constant: Bytes | string): CoderType<undefined> => {
const c = typeof constant === 'string' ? utf8.decode(constant) : constant;
return magic(bytes(c.length), c);
};
export function constant<T>(c: T): CoderType<T> {
return wrap({
encodeStream: (_w: Writer, value: T) => {
if (value !== c) throw new Error(`constant: invalid value ${value} (exp: ${c})`);
},
decodeStream: (_r: Reader): T => c,
});
}
function sizeof(fields: CoderType<any>[]): Option<number> {
let size: Option<number> = 0;
for (let f of fields) {
if (f.size === undefined) return;
if (!Number.isSafeInteger(f.size)) throw new Error(`sizeof: wrong element size=${size}`);
size += f.size;
}
return size;
}
export function struct<T extends Record<string, any>>(
fields: StructRecord<T>
): CoderType<StructInput<T>> {
if (Array.isArray(fields)) throw new Error('Packed.Struct: got array instead of object');
return wrap({
size: sizeof(Object.values(fields)),
encodeStream: (w: Writer, value: StructInput<T>) => {
if (typeof value !== 'object' || value === null)
throw w.err(`struct: invalid value ${value}`);
w.path.push(value);
for (let name in fields) {
w.fieldPathPush(name);
let field = fields[name];
field.encodeStream(w, (value as T)[name]);
w.fieldPathPop();
}
w.path.pop();
},
decodeStream: (r: Reader): StructInput<T> => {
let res: Partial<T> = {};
r.path.push(res);
for (let name in fields) {
r.fieldPathPush(name);
res[name] = fields[name].decodeStream(r);
r.fieldPathPop();
}
r.path.pop();
return res as T;
},
});
}
export function tuple<
T extends ArrLike<CoderType<any>>,
O = Writable<{ [K in keyof T]: UnwrapCoder<T[K]> }>,
>(fields: T): CoderType<O> {
if (!Array.isArray(fields))
throw new Error(`Packed.Tuple: got ${typeof fields} instead of array`);
return wrap({
size: sizeof(fields),
encodeStream: (w: Writer, value: O) => {
if (!Array.isArray(value)) throw w.err(`tuple: invalid value ${value}`);
w.path.push(value);
for (let i = 0; i < fields.length; i++) {
w.fieldPathPush('' + i);
fields[i].encodeStream(w, value[i]);
w.fieldPathPop();
}
w.path.pop();
},
decodeStream: (r: Reader): O => {
let res: any = [];
r.path.push(res);
for (let i = 0; i < fields.length; i++) {
r.fieldPathPush('' + i);
res.push(fields[i].decodeStream(r));
r.fieldPathPop();
}
r.path.pop();
return res;
},
});
}
type PrefixLength = string | number | CoderType<number> | CoderType<bigint>;
export function prefix<T>(len: PrefixLength, inner: CoderType<T>): CoderType<T> {
if (!isCoder(inner)) throw new Error(`prefix: invalid inner value ${inner}`);
if (isBytes(len)) throw new Error(`prefix: len cannot be Uint8Array`);
const b = bytes(len);
return wrap({
size: typeof len === 'number' ? len : undefined,
encodeStream: (w: Writer, value: T) => {
const wChild = new Writer(w.path, w.fieldPath);
inner.encodeStream(wChild, value);
b.encodeStream(w, wChild.buffer);
},
decodeStream: (r: Reader): T => {
const data = b.decodeStream(r);
const ir = new Reader(data, r.opts, r.path, r.fieldPath);
const res = inner.decodeStream(ir);
ir.finish();
return res;
},
});
}
export function array<T>(len: Length, inner: CoderType<T>): CoderType<T[]> {
if (!isCoder(inner)) throw new Error(`array: invalid inner value ${inner}`);
return wrap({
size: typeof len === 'number' && inner.size ? len * inner.size : undefined,
encodeStream: (w: Writer, value: T[]) => {
if (!Array.isArray(value)) throw w.err(`array: invalid value ${value}`);
if (!isBytes(len)) w.length(len, value.length);
w.path.push(value);
for (let i = 0; i < value.length; i++) {
w.fieldPathPush('' + i);
const elm = value[i];
const startPos = w.pos;
inner.encodeStream(w, elm);
if (isBytes(len)) {
// Terminator is bigger than elm size, so skip
if (len.length > w.pos - startPos) continue;
const data = w.buffer.subarray(startPos, w.pos);
// There is still possible case when multiple elements create terminator,
// but it is hard to catch here, will be very slow
if (equalBytes(data.subarray(0, len.length), len))
throw w.err(`array: inner element encoding same as separator. elm=${elm} data=${data}`);
}
w.fieldPathPop();
}
w.path.pop();
if (isBytes(len)) w.bytes(len);
},
decodeStream: (r: Reader): T[] => {
let res: T[] = [];
if (len === null) {
let i = 0;
r.path.push(res);
while (!r.isEnd()) {
r.fieldPathPush('' + i++);
res.push(inner.decodeStream(r));
r.fieldPathPop();
if (inner.size && r.leftBytes < inner.size) break;
}
r.path.pop();
} else if (isBytes(len)) {
let i = 0;
r.path.push(res);
while (true) {
if (equalBytes(r.bytes(len.length, true), len)) {
// Advance cursor position if terminator found
r.bytes(len.length);
break;
}
r.fieldPathPush('' + i++);
res.push(inner.decodeStream(r));
r.fieldPathPop();
}
r.path.pop();
} else {
r.fieldPathPush('arrayLen');
const length = r.length(len);
r.fieldPathPop();
r.path.push(res);
for (let i = 0; i < length; i++) {
r.fieldPathPush('' + i);
res.push(inner.decodeStream(r));
r.fieldPathPop();
}
r.path.pop();
}
return res;
},
});
}
export function map<T>(inner: CoderType<T>, variants: Record<string, T>): CoderType<string> {
if (!isCoder(inner)) throw new Error(`map: invalid inner value ${inner}`);
const variantNames: Map<T, string> = new Map();
for (const k in variants) variantNames.set(variants[k], k);
return wrap({
size: inner.size,
encodeStream: (w: Writer, value: string) => {
if (typeof value !== 'string') throw w.err(`map: invalid value ${value}`);
if (!(value in variants)) throw w.err(`Map: unknown variant: ${value}`);
inner.encodeStream(w, variants[value]);
},
decodeStream: (r: Reader): string => {
const variant = inner.decodeStream(r);
const name = variantNames.get(variant);
if (name === undefined)
throw r.err(`Enum: unknown value: ${variant} ${Array.from(variantNames.keys())}`);
return name;
},
});
}
export function tag<
T extends Values<{
[P in keyof Variants]: { TAG: P; data: UnwrapCoder<Variants[P]> };
}>,
TagValue extends string | number,
Variants extends Record<TagValue, CoderType<any>>,
>(tag: CoderType<TagValue>, variants: Variants): CoderType<T> {
if (!isCoder(tag)) throw new Error(`tag: invalid tag value ${tag}`);
return wrap({
size: tag.size,
encodeStream: (w: Writer, value: T) => {
const { TAG, data } = value;
const dataType = variants[TAG];
if (!dataType) throw w.err(`Tag: invalid tag ${TAG.toString()}`);
tag.encodeStream(w, TAG as any);
dataType.encodeStream(w, data);
},
decodeStream: (r: Reader): T => {
const TAG = tag.decodeStream(r);
const dataType = variants[TAG];
if (!dataType) throw r.err(`Tag: invalid tag ${TAG}`);
return { TAG, data: dataType.decodeStream(r) } as any;
},
});
}
// Takes {name: [value, coder]}
export function mappedTag<
T extends Values<{
[P in keyof Variants]: { TAG: P; data: UnwrapCoder<Variants[P][1]> };
}>,
TagValue extends string | number,
Variants extends Record<string, [TagValue, CoderType<any>]>,
>(tagCoder: CoderType<TagValue>, variants: Variants): CoderType<T> {
if (!isCoder(tagCoder)) throw new Error(`mappedTag: invalid tag value ${tag}`);
const mapValue: Record<string, TagValue> = {};
const tagValue: Record<string, CoderType<any>> = {};
for (const key in variants) {
const v = variants[key];
mapValue[key] = v[0];
tagValue[key] = v[1];
}
return tag(map(tagCoder, mapValue), tagValue) as any as CoderType<T>;
}
export function bitset<Names extends readonly string[]>(
names: Names,
pad = false
): CoderType<Record<Names[number], boolean>> {
return wrap({
encodeStream: (w: Writer, value: Record<Names[number], boolean>) => {
if (typeof value !== 'object' || value === null)
throw w.err(`bitset: invalid value ${value}`);
for (let i = 0; i < names.length; i++) w.bits(+(value as any)[names[i]], 1);
if (pad && names.length % 8) w.bits(0, 8 - (names.length % 8));
},
decodeStream: (r: Reader): Record<Names[number], boolean> => {
let out: Record<string, boolean> = {};
for (let i = 0; i < names.length; i++) out[names[i]] = !!r.bits(1);
if (pad && names.length % 8) r.bits(8 - (names.length % 8));
return out;
},
});
}
export const ZeroPad: PadFn = (_) => 0;
function padLength(blockSize: number, len: number): number {
if (len % blockSize === 0) return 0;
return blockSize - (len % blockSize);
}
export function padLeft<T>(
blockSize: number,
inner: CoderType<T>,
padFn: Option<PadFn>
): CoderType<T> {
if (!isCoder(inner)) throw new Error(`padLeft: invalid inner value ${inner}`);
const _padFn = padFn || ZeroPad;
if (!inner.size) throw new Error('padLeft with dynamic size argument is impossible');
return wrap({
size: inner.size + padLength(blockSize, inner.size),
encodeStream: (w: Writer, value: T) => {
const padBytes = padLength(blockSize, inner.size!);
for (let i = 0; i < padBytes; i++) w.byte(_padFn(i));
inner.encodeStream(w, value);
},
decodeStream: (r: Reader): T => {
r.bytes(padLength(blockSize, inner.size!));
return inner.decodeStream(r);
},
});
}
export function padRight<T>(
blockSize: number,
inner: CoderType<T>,
padFn: Option<PadFn>
): CoderType<T> {
if (!isCoder(inner)) throw new Error(`padRight: invalid inner value ${inner}`);
const _padFn = padFn || ZeroPad;
return wrap({
size: inner.size ? inner.size + padLength(blockSize, inner.size) : undefined,
encodeStream: (w: Writer, value: T) => {
const pos = w.pos;
inner.encodeStream(w, value);
const padBytes = padLength(blockSize, w.pos - pos);
for (let i = 0; i < padBytes; i++) w.byte(_padFn(i));
},
decodeStream: (r: Reader): T => {
const start = r.pos;
const res = inner.decodeStream(r);
r.bytes(padLength(blockSize, r.pos - start));
return res;
},
});
}
// Pointers are scoped, next pointer in dereference chain is offseted by previous one.
// Not too generic, but, works fine for now.
export function pointer<T>(
ptr: CoderType<number>,
inner: CoderType<T>,
sized = false
): CoderType<T> {
if (!isCoder(ptr)) throw new Error(`pointer: invalid ptr value ${ptr}`);
if (!isCoder(inner)) throw new Error(`pointer: invalid inner value ${inner}`);
if (!ptr.size) throw new Error('Pointer: unsized ptr');
return wrap({
size: sized ? ptr.size : undefined,
encodeStream: (w: Writer, value: T) => {
// TODO: by some reason it encodes array of pointers as [(ptr,val), (ptr, val)]
// instead of [ptr, ptr][val, val]
const start = w.pos;
ptr.encodeStream(w, 0);
w.ptrs.push({ pos: start, ptr, buffer: inner.encode(value) });
},
decodeStream: (r: Reader): T => {
const ptrVal = ptr.decodeStream(r);
r.enablePtr();
return inner.decodeStream(r.offsetReader(ptrVal));
},
});
}
// lineLen: gpg=64, ssh=70
export function base64armor<T>(
name: string,
lineLen: number,
inner: Coder<T, Bytes>,
checksum?: (data: Bytes) => Bytes
): Coder<T, string> {
const markBegin = `-----BEGIN ${name.toUpperCase()}-----`;
const markEnd = `-----END ${name.toUpperCase()}-----`;
return {
encode(value: T) {
const data = inner.encode(value);
const encoded = base64.encode(data);
let lines = [];
for (let i = 0; i < encoded.length; i += lineLen) {
const s = encoded.slice(i, i + lineLen);
if (s.length) lines.push(`${encoded.slice(i, i + lineLen)}\n`);
}
let body = lines.join('');
if (checksum) body += `=${base64.encode(checksum(data))}\n`;
return `${markBegin}\n\n${body}${markEnd}\n`;
},
decode(s: string): T {
let lines = s.replace(markBegin, '').replace(markEnd, '').trim().split('\n');
lines = lines.map((l) => l.replace('\r', '').trim());
const last = lines.length - 1;
if (checksum && lines[last].startsWith('=')) {
const body = base64.decode(lines.slice(0, -1).join(''));
const cs = lines[last].slice(1);
const realCS = base64.encode(checksum(body));
if (realCS !== cs)
throw new Error(`Base64Armor: invalid checksum ${cs} instead of ${realCS}`);
return inner.decode(body);
}
return inner.decode(base64.decode(lines.join('')));
},
};
}
// Does nothing at all
export const nothing = /* @__PURE__ */ magic(/* @__PURE__ */ bytes(0), EMPTY);
export function debug<T>(inner: CoderType<T>): CoderType<T> {
if (!isCoder(inner)) throw new Error(`debug: invalid inner value ${inner}`);
const log = (name: string, rw: Reader | Writer, value: any) => {
// @ts-ignore
console.log(`DEBUG/${name}(${rw.fieldPath.join('/')}):`, { type: typeof value, value });
return value;
};
return wrap({
size: inner.size,
encodeStream: (w: Writer, value: T) => inner.encodeStream(w, log('encode', w, value)),
decodeStream: (r: Reader): T => log('decode', r, inner.decodeStream(r)),
});
}
// Internal methods for test purposes only
export const _TEST = /* @__PURE__ */ { _bitset };
Выполнить команду
Для локальной разработки. Не используйте в интернете!