269 lines
9.1 KiB
JavaScript
269 lines
9.1 KiB
JavaScript
|
|
"use strict";
|
||
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
|
exports.diff = exports.decode = exports.table = void 0;
|
||
|
|
const base_1 = require("@scure/base");
|
||
|
|
const P = require("./index.js");
|
||
|
|
const UNKNOWN = '(???)';
|
||
|
|
const bold = '\x1b[1m';
|
||
|
|
const gray = '\x1b[90m';
|
||
|
|
const reset = '\x1b[0m';
|
||
|
|
const red = '\x1b[31m';
|
||
|
|
const green = '\x1b[32m';
|
||
|
|
const yellow = '\x1b[33m';
|
||
|
|
class DebugReader extends P.Reader {
|
||
|
|
constructor() {
|
||
|
|
super(...arguments);
|
||
|
|
this.debugLst = [];
|
||
|
|
}
|
||
|
|
get lastElm() {
|
||
|
|
if (this.debugLst.length)
|
||
|
|
return this.debugLst[this.debugLst.length - 1];
|
||
|
|
return { start: 0, end: 0, path: '' };
|
||
|
|
}
|
||
|
|
fieldPathPush(s) {
|
||
|
|
const last = this.lastElm;
|
||
|
|
if (last.end === undefined)
|
||
|
|
last.end = this.pos;
|
||
|
|
else if (last.end !== this.pos) {
|
||
|
|
this.debugLst.push({
|
||
|
|
path: [...this.fieldPath, UNKNOWN].join('/'),
|
||
|
|
start: last.end,
|
||
|
|
end: this.pos,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
this.cur = { path: [...this.fieldPath, s].join('/'), start: this.pos };
|
||
|
|
super.fieldPathPush(s);
|
||
|
|
}
|
||
|
|
fieldPathPop() {
|
||
|
|
// happens if pop after pop (exit from nested structure)
|
||
|
|
if (!this.cur) {
|
||
|
|
const last = this.lastElm;
|
||
|
|
if (last.end === undefined)
|
||
|
|
last.end = this.pos;
|
||
|
|
else if (last.end !== this.pos) {
|
||
|
|
this.debugLst.push({ start: last.end, end: this.pos, path: last.path + `/${UNKNOWN}` });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
this.cur.end = this.pos;
|
||
|
|
const lastItem = this.path[this.path.length - 1];
|
||
|
|
const lastField = this.fieldPath[this.fieldPath.length - 1];
|
||
|
|
if (lastItem && lastField !== undefined)
|
||
|
|
this.cur.value = lastItem[lastField];
|
||
|
|
this.debugLst.push(this.cur);
|
||
|
|
this.cur = undefined;
|
||
|
|
}
|
||
|
|
super.fieldPathPop();
|
||
|
|
}
|
||
|
|
finishDebug() {
|
||
|
|
const end = this.data.length;
|
||
|
|
if (this.cur)
|
||
|
|
this.debugLst.push({ end, ...this.cur });
|
||
|
|
const last = this.lastElm;
|
||
|
|
if (!last || last.end !== end)
|
||
|
|
this.debugLst.push({ start: this.pos, end, path: UNKNOWN });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function toBytes(data) {
|
||
|
|
if (P.isBytes(data))
|
||
|
|
return data;
|
||
|
|
if (typeof data !== 'string')
|
||
|
|
throw new Error('PD: data should be string or Uint8Array');
|
||
|
|
try {
|
||
|
|
return base_1.base64.decode(data);
|
||
|
|
}
|
||
|
|
catch (e) { }
|
||
|
|
try {
|
||
|
|
return base_1.hex.decode(data);
|
||
|
|
}
|
||
|
|
catch (e) { }
|
||
|
|
throw new Error(`PD: data has unknown string format: ${data}`);
|
||
|
|
}
|
||
|
|
function mapData(lst, data) {
|
||
|
|
let end = 0;
|
||
|
|
const res = [];
|
||
|
|
for (const elm of lst) {
|
||
|
|
if (elm.start !== end)
|
||
|
|
throw new Error(`PD: elm start=${elm.start} after prev elm end=${end}`);
|
||
|
|
if (elm.end === undefined)
|
||
|
|
throw new Error(`PD: elm.end is undefined=${elm}`);
|
||
|
|
res.push({ path: elm.path, data: data.slice(elm.start, elm.end), value: elm.value });
|
||
|
|
end = elm.end;
|
||
|
|
}
|
||
|
|
if (end !== data.length)
|
||
|
|
throw new Error('PD: not all data mapped');
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
function chrWidth(s) {
|
||
|
|
/*
|
||
|
|
It is almost impossible to find out real characters width in terminal since it depends on terminal itself, current unicode version and moon's phase.
|
||
|
|
So, we just stripping ANSI, tabs and unicode supplimental characters. Emoji support requires big tables (and have no guarantee to work), so we ignore it for now.
|
||
|
|
Also, no support for full width unicode characters for now.
|
||
|
|
*/
|
||
|
|
return s
|
||
|
|
.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g, '')
|
||
|
|
.replace('\t', ' ')
|
||
|
|
.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, ' ').length;
|
||
|
|
}
|
||
|
|
function wrap(s, padding = 0) {
|
||
|
|
// @ts-ignore
|
||
|
|
const limit = process.stdout.columns - 3 - padding;
|
||
|
|
if (chrWidth(s) <= limit)
|
||
|
|
return s;
|
||
|
|
while (chrWidth(s) > limit)
|
||
|
|
s = s.slice(0, -1);
|
||
|
|
return `${s}${reset}...`;
|
||
|
|
}
|
||
|
|
function table(data) {
|
||
|
|
let res = [];
|
||
|
|
const str = (v) => (v === undefined ? '' : '' + v);
|
||
|
|
const pad = (s, width) => `${s}${''.padEnd(Math.max(0, width - chrWidth(s)), ' ')}`;
|
||
|
|
let widths = {};
|
||
|
|
for (let elm of data) {
|
||
|
|
for (let k in elm) {
|
||
|
|
widths[k] = Math.max(widths[k] || 0, chrWidth(str(k)), str(elm[k])
|
||
|
|
.split('\n')
|
||
|
|
.reduce((a, b) => Math.max(a, chrWidth(b)), 0));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const columns = Object.keys(widths);
|
||
|
|
if (!data.length || !columns.length)
|
||
|
|
throw new Error('No data');
|
||
|
|
const padding = ` ${reset}${gray}│${reset} `;
|
||
|
|
res.push(wrap(` ${columns.map((c) => `${bold}${pad(c, widths[c])}`).join(padding)}${reset}`, 3));
|
||
|
|
for (let idx = 0; idx < data.length; idx++) {
|
||
|
|
const elm = data[idx];
|
||
|
|
const row = columns.map((i) => str(elm[i]).split('\n'));
|
||
|
|
let message = [...Array(Math.max(...row.map((i) => i.length))).keys()]
|
||
|
|
.map((line) => row.map((c, i) => pad(str(c[line]), widths[columns[i]])))
|
||
|
|
.map((line, _) => wrap(` ${line.join(padding)} `, 1))
|
||
|
|
.join('\n');
|
||
|
|
res.push(message);
|
||
|
|
}
|
||
|
|
for (let i = 0; i < res.length; i++) {
|
||
|
|
const border = columns
|
||
|
|
.map((c) => ''.padEnd(widths[c], '─'))
|
||
|
|
.join(`─${i === res.length - 1 ? '┴' : '┼'}─`);
|
||
|
|
res[i] += wrap(`\n${reset}${gray}─${border}─${reset}`);
|
||
|
|
}
|
||
|
|
// @ts-ignore
|
||
|
|
console.log(res.join('\n'));
|
||
|
|
}
|
||
|
|
exports.table = table;
|
||
|
|
function fmtData(data, perLine = 8) {
|
||
|
|
const res = [];
|
||
|
|
for (let i = 0; i < data.length; i += perLine) {
|
||
|
|
res.push(base_1.hex.encode(data.slice(i, i + perLine)));
|
||
|
|
}
|
||
|
|
return res.map((i) => `${bold}${i}${reset}`).join('\n');
|
||
|
|
}
|
||
|
|
function fmtValue(value) {
|
||
|
|
if (P.isBytes(value))
|
||
|
|
return `b(${green}${base_1.hex.encode(value)}${reset} len=${value.length})`;
|
||
|
|
if (typeof value === 'string')
|
||
|
|
return `s(${green}"${value}"${reset} len=${value.length})`;
|
||
|
|
if (typeof value === 'number' || typeof value === 'bigint')
|
||
|
|
return `n(${value})`;
|
||
|
|
// console.log('fmt', value);
|
||
|
|
// if (Object.prototype.toString.call(value) === '[object Object]') return inspect(value);
|
||
|
|
return '' + value;
|
||
|
|
}
|
||
|
|
function decode(coder, data, forcePrint = false) {
|
||
|
|
data = toBytes(data);
|
||
|
|
const r = new DebugReader(data);
|
||
|
|
let res, e;
|
||
|
|
try {
|
||
|
|
res = coder.decodeStream(r);
|
||
|
|
r.finish();
|
||
|
|
}
|
||
|
|
catch (_e) {
|
||
|
|
e = _e;
|
||
|
|
}
|
||
|
|
r.finishDebug();
|
||
|
|
if (e || forcePrint) {
|
||
|
|
// @ts-ignore
|
||
|
|
console.log('==== DECODED BEFORE ERROR ====');
|
||
|
|
table(mapData(r.debugLst, data).map((elm) => ({
|
||
|
|
Data: fmtData(elm.data),
|
||
|
|
Len: elm.data.length,
|
||
|
|
Path: `${green}${elm.path}${reset}`,
|
||
|
|
Value: fmtValue(elm.value),
|
||
|
|
})));
|
||
|
|
// @ts-ignore
|
||
|
|
console.log('==== /DECODED BEFORE ERROR ====');
|
||
|
|
}
|
||
|
|
if (e)
|
||
|
|
throw e;
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
exports.decode = decode;
|
||
|
|
function getMap(coder, data) {
|
||
|
|
data = toBytes(data);
|
||
|
|
const r = new DebugReader(data);
|
||
|
|
coder.decodeStream(r);
|
||
|
|
r.finish();
|
||
|
|
r.finishDebug();
|
||
|
|
return mapData(r.debugLst, data);
|
||
|
|
}
|
||
|
|
function diffData(a, e) {
|
||
|
|
const len = Math.max(a.length, e.length);
|
||
|
|
let outA = '', outE = '';
|
||
|
|
const charHex = (n) => n.toString(16).padStart(2, '0');
|
||
|
|
for (let i = 0; i < len; i++) {
|
||
|
|
const [aI, eI] = [a[i], e[i]];
|
||
|
|
if (i && !(i % 8)) {
|
||
|
|
if (aI !== undefined)
|
||
|
|
outA += '\n';
|
||
|
|
if (eI !== undefined)
|
||
|
|
outE += '\n';
|
||
|
|
}
|
||
|
|
if (aI !== undefined)
|
||
|
|
outA += aI === eI ? charHex(aI) : `${yellow}${charHex(aI)}${reset}`;
|
||
|
|
if (eI !== undefined)
|
||
|
|
outE += aI === eI ? charHex(eI) : `${yellow}${charHex(eI)}${reset}`;
|
||
|
|
}
|
||
|
|
return [outA, outE];
|
||
|
|
}
|
||
|
|
function diffPath(a, e) {
|
||
|
|
if (a === e)
|
||
|
|
return a;
|
||
|
|
return `A: ${red}${a}${reset}\nE: ${green}${e}${reset}`;
|
||
|
|
}
|
||
|
|
function diffLength(a, e) {
|
||
|
|
const [aLen, eLen] = [a.length, e.length];
|
||
|
|
if (aLen === eLen)
|
||
|
|
return aLen;
|
||
|
|
return `A: ${red}${aLen}${reset}\nE: ${green}${eLen}${reset}`;
|
||
|
|
}
|
||
|
|
function diffValue(a, e) {
|
||
|
|
const [aV, eV] = [a, e].map(fmtValue);
|
||
|
|
if (aV === eV)
|
||
|
|
return aV;
|
||
|
|
return `A: ${red}${aV}${reset}\nE: ${green}${eV}${reset}`;
|
||
|
|
}
|
||
|
|
function diff(coder, actual, expected, skipSame = true) {
|
||
|
|
// @ts-ignore
|
||
|
|
console.log('==== DIFF ====');
|
||
|
|
const [_actual, _expected] = [actual, expected].map((i) => getMap(coder, i));
|
||
|
|
const len = Math.max(_actual.length, _expected.length);
|
||
|
|
const data = [];
|
||
|
|
const DEF = { data: P.EMPTY, path: '' };
|
||
|
|
for (let i = 0; i < len; i++) {
|
||
|
|
const [a, e] = [_actual[i] || DEF, _expected[i] || DEF];
|
||
|
|
if (P.equalBytes(a.data, e.data) && skipSame)
|
||
|
|
continue;
|
||
|
|
const [adata, edata] = diffData(a.data, e.data);
|
||
|
|
data.push({
|
||
|
|
'Data (A)': adata,
|
||
|
|
'Data (E)': edata,
|
||
|
|
Len: diffLength(a.data, e.data),
|
||
|
|
Path: diffPath(a.path, e.path),
|
||
|
|
Value: diffValue(a.value, e.value),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
table(data);
|
||
|
|
// @ts-ignore
|
||
|
|
console.log('==== /DIFF ====');
|
||
|
|
}
|
||
|
|
exports.diff = diff;
|