import isPlainObject from "is-plain-obj"; import * as constants from "./constants"; import { encode, symKeys } from "./internal"; import { RubyClass, RubyFloat, RubyHash, RubyInteger, RubyModule, RubyObject, RubyRegexp, RubyStruct, } from "./ruby"; import type { ClassLike } from "./load"; export interface DumpOptions { /** * If true, convert string keys in JS objects to ruby symbols in Hash. * ```js * dump({ a: 1 }) // => ruby: { "a" => 1 } * dump({ a: 1 }, { hashStringKeysToSymbol: true }) // => ruby: { :a => 1 } * ``` */ hashStringKeysToSymbol?: boolean; /** * If set, use this known classes to encode ruby objects. * ```js * dump(new A()) // => Error "can't dump object [object Object]" * dump(new A(), { known: { A } }) // => ruby: # * ``` */ known?: { [klass: string]: ClassLike }; /** * If set, use this string for unknown classes to encode ruby objects. * ```js * dump(new A()) // => Error "can't dump object [object Object]" * dump(new A(), { unknown: () => "A" }) // => ruby: # * ``` */ unknown?: (obj: unknown) => string | null | undefined; } class Dumper { declare data_: Uint8Array; declare length_: number; declare objects_: Map; declare symbols_: Map; declare options_: DumpOptions; constructor(options: DumpOptions = {}) { this.data_ = new Uint8Array(16); this.length_ = 0; this.objects_ = new Map(); this.symbols_ = new Map(); this.options_ = options; } dump_(value: unknown) { w_byte(this, 4); w_byte(this, 8); w_object(this, value); this.objects_.clear(); this.symbols_.clear(); return this; } get_() { return this.data_.subarray(0, this.length_); } } const resize = (d: Dumper) => { var data = new Uint8Array(d.data_.byteLength << 1); data.set(d.data_); d.data_ = data; }; const w_byte = (d: Dumper, b: number) => { if (d.length_ >= d.data_.byteLength) resize(d); d.data_[d.length_++] = b; }; const w_buffer = (d: Dumper, arr: ArrayLike) => { while (d.length_ + arr.length >= d.data_.byteLength) resize(d); d.data_.set(arr, d.length_); d.length_ += arr.length; }; const w_bytes = (d: Dumper, arr: ArrayLike) => { w_long(d, arr.length); w_buffer(d, arr); }; const w_string = (d: Dumper, s: string) => { w_bytes(d, encode(s)); }; const w_long = (d: Dumper, n: number) => { var buf = new Uint8Array(5); var i = ruby_marshal_write_long(n, buf); w_buffer(d, buf.subarray(0, i)); }; const ruby_marshal_write_long = (x: number, buf: Uint8Array) => { if (x === 0) { buf[0] = 0; return 1; } if (0 < x && x < 123) { buf[0] = x + 5; return 1; } if (-124 < x && x < 0) { buf[0] = (x - 5) & 0xff; return 1; } for (var i = 1; i < 5; ++i) { buf[i] = x & 0xff; x >>= 8; if (x === 0) { buf[0] = i; break; } if (x === -1) { buf[0] = -i; break; } } return i + 1; }; // prettier-ignore const w_float = (d: Dumper, f: number) => { w_string( d, f !== f ? "nan" : Number.isFinite(f) ? (Object.is(f, -0) ? "-0" : f.toString()) : f < 0 ? "-inf" : "inf" ); }; const w_symbol = (d: Dumper, sym: symbol) => { if (d.symbols_.has(sym)) { w_byte(d, constants.T_SYMLINK); w_long(d, d.symbols_.get(sym)!); } else { w_byte(d, constants.T_SYMBOL); w_bytes(d, encode(Symbol.keyFor(sym)!)); d.symbols_.set(sym, d.symbols_.size); } }; const w_extended = (d: Dumper, e: symbol[]) => { for (var sym of e) { w_byte(d, constants.T_EXTENDED); w_symbol(d, sym); } }; const w_class = (d: Dumper, type: number, a: RubyObject | RubyStruct) => { if (a[constants.S_EXTENDS]) w_extended(d, a[constants.S_EXTENDS]); w_byte(d, type); w_symbol(d, a.class); }; const w_uclass = (d: Dumper, a: RubyObject) => { if (a[constants.S_EXTENDS]) w_extended(d, a[constants.S_EXTENDS]); if (a.wrapped) { w_byte(d, constants.T_UCLASS); w_symbol(d, a.class); } }; // also used for struct.members, both printing sym keys const w_ivar = (d: Dumper, a: {}) => { // prettier-ignore var keys = symKeys(a), n = keys.length, i, k; if (n > 0) { w_long(d, n); for (i = 0; i < n; ++i) { w_symbol(d, (k = keys[i])); w_object(d, a[k]); } } else { w_long(d, 0); } }; const w_bignum = (d: Dumper, a: number) => { w_byte(d, constants.T_BIGNUM); w_byte(d, a < 0 ? constants.B_NEGATIVE : constants.B_POSITIVE); var buffer: number[] = []; a = Math.abs(a); do { buffer.push(a & 0xff); a = Math.floor(a / 256); } while (a); if (buffer.length & 1) buffer.push(0); w_long(d, buffer.length >> 1); w_buffer(d, buffer); }; const w_remember = (d: Dumper, obj: unknown) => { if (!d.objects_.has(obj)) { d.objects_.set(obj, d.objects_.size); } }; const w_known = (d: Dumper, obj: {}, klass: string) => { w_remember(d, obj); if (obj[constants.S_EXTENDS]) w_extended(d, obj[constants.S_EXTENDS]); w_byte(d, constants.T_OBJECT); w_symbol(d, Symbol.for(klass)); w_ivar(d, obj); }; const w_object = (d: Dumper, obj: unknown) => { var str2sym = d.options_.hashStringKeysToSymbol; var known = d.options_.known || {}; var unknown = d.options_.unknown; if (obj === void 0) { throw new TypeError("can't dump undefined"); } else if (obj === null) { w_byte(d, constants.T_NIL); } else if (obj === true) { w_byte(d, constants.T_TRUE); } else if (obj === false) { w_byte(d, constants.T_FALSE); } else if (typeof obj === "number") { if (Number.isInteger(obj)) { if (-0x40000000 <= obj && obj < 0x40000000) { w_byte(d, constants.T_FIXNUM); w_long(d, obj); } else { w_remember(d, obj); w_bignum(d, obj); } } else { w_remember(d, obj); w_byte(d, constants.T_FLOAT); w_float(d, obj); } } else if (obj instanceof RubyInteger) { var i = obj.value; if (-0x40000000 <= i && i < 0x40000000) { w_byte(d, constants.T_FIXNUM); w_long(d, i); } else { w_remember(d, i); w_bignum(d, i); } } else if (obj instanceof RubyFloat) { var i = obj.value; w_remember(d, i); w_byte(d, constants.T_FLOAT); w_float(d, i); } else if (typeof obj === "symbol") { w_symbol(d, obj); } else if (d.objects_.has(obj)) { w_byte(d, constants.T_LINK); w_long(d, d.objects_.get(obj)!); } else if (obj instanceof RubyObject) { w_remember(d, obj); if (obj.data !== void 0) { w_class(d, constants.T_DATA, obj); w_object(d, obj.data); } else if (obj.wrapped !== void 0) { w_uclass(d, obj); w_object(d, obj.wrapped); } else if (obj.userDefined) { var has_ivar = symKeys(obj).length > 0; if (has_ivar) w_byte(d, constants.T_IVAR); w_class(d, constants.T_USERDEF, obj); w_bytes(d, obj.userDefined); if (has_ivar) w_ivar(d, obj); } else if (obj.userMarshal !== void 0) { w_class(d, constants.T_USERMARSHAL, obj); w_object(d, obj.userMarshal); } else { w_class(d, constants.T_OBJECT, obj); w_ivar(d, obj); } } else if (obj instanceof RubyStruct) { w_remember(d, obj); w_class(d, constants.T_STRUCT, obj); w_ivar(d, obj.members); } else if (Array.isArray(obj)) { w_remember(d, obj); w_byte(d, constants.T_ARRAY); w_long(d, obj.length); for (var i = 0; i < obj.length; ++i) { w_object(d, obj[i]); } } else if (obj instanceof RegExp) { w_remember(d, obj); w_byte(d, constants.T_REGEXP); w_string(d, obj.source); var options = 0; if (obj.flags.includes("i")) options |= constants.RE_IGNORECASE; if (obj.flags.includes("m")) options |= constants.RE_MULTILINE; w_byte(d, options); } else if (obj instanceof RubyRegexp) { w_remember(d, obj); w_byte(d, constants.T_REGEXP); w_string(d, obj.source); w_byte(d, obj.options); } else if (typeof obj === "string") { w_remember(d, obj); w_byte(d, constants.T_IVAR); w_byte(d, constants.T_STRING); w_string(d, obj); w_long(d, 1); w_symbol(d, constants._E); w_byte(d, constants.T_TRUE); } else if (obj instanceof Uint8Array) { w_remember(d, obj); w_byte(d, constants.T_STRING); w_bytes(d, obj); } else if (obj instanceof RubyClass) { w_remember(d, obj); w_byte(d, constants.T_CLASS); w_string(d, obj.name); } else if (obj instanceof RubyModule) { w_remember(d, obj); w_byte(d, obj.old ? constants.T_MODULE_OLD : constants.T_MODULE); w_string(d, obj.name); } else if (obj instanceof RubyHash) { w_remember(d, obj); var def = obj.default; w_byte(d, def === void 0 ? constants.T_HASH : constants.T_HASH_DEF); w_long(d, obj.entries.length); for (var i = 0, n = obj.entries.length, k, v; i < n; ++i) { [k, v] = obj.entries[i]; w_object(d, k); w_object(d, v); } if (def !== void 0) w_object(d, def); } else if (obj instanceof Map) { w_remember(d, obj); var def = obj[constants.S_DEFAULT] as unknown; w_byte(d, def === void 0 ? constants.T_HASH : constants.T_HASH_DEF); w_long(d, obj.size); for (var [k, v] of obj) { w_object(d, k); w_object(d, v); } if (def !== void 0) w_object(d, def); } else if (isPlainObject(obj)) { w_remember(d, obj); var def = obj[constants.S_DEFAULT]; w_byte(d, def === void 0 ? constants.T_HASH : constants.T_HASH_DEF); var keys = (Object.keys(obj) as (string | symbol)[]).concat(symKeys(obj)); w_long(d, keys.length); for (var i = 0, n = keys.length, k; i < n; ++i) { k = keys[i]; w_object(d, str2sym && typeof k === "string" ? Symbol.for(k) : k); w_object(d, obj[k]); } if (def !== void 0) w_object(d, def); } else { var proto = Object.getPrototypeOf(obj); for (var klass in known) { if (proto === known[klass].prototype) { w_known(d, obj, klass); return; } } if (unknown) { var s = unknown(obj); if (s) { w_known(d, obj, s); return; } } throw new TypeError("can't dump " + typeof obj + " " + obj); } }; /** * Dump a value into marshal buffer. * ```js * dump(null) // => Uint8Array [ 4, 8, 48 ] * ``` */ export function dump(value: unknown, options?: DumpOptions): Uint8Array { return new Dumper(options).dump_(value).get_(); } export function dumpAll(value: unknown[], options?: DumpOptions): Uint8Array { var d = new Dumper(options); for (var i = 0; i < value.length; ++i) { d.dump_(value[i]); } return d.get_(); }