UNPKG

@substrate-system/bencode

Version:
964 lines (952 loc) 25.7 kB
// test/encoding-length.test.ts import fs from "fs"; import path, { dirname } from "path"; // node_modules/@substrate-system/tapzero/dist/index.js var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); function equal(a, b) { if (a === b) return true; if (a && b && typeof a == "object" && typeof b == "object") { if (a.constructor !== b.constructor) return false; var length, i, keys; if (Array.isArray(a)) { length = a.length; if (length != b.length) return false; for (i = length; i-- !== 0; ) if (!equal(a[i], b[i])) return false; return true; } if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); keys = Object.keys(a); length = keys.length; if (length !== Object.keys(b).length) return false; for (i = length; i-- !== 0; ) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; for (i = length; i-- !== 0; ) { var key = keys[i]; if (!equal(a[key], b[key])) return false; } return true; } return a !== a && b !== b; } __name(equal, "equal"); var NEW_LINE_REGEX = /\n/g; var OBJ_TO_STRING = Object.prototype.toString; var AT_REGEX = new RegExp( // non-capturing group for 'at ' "^(?:[^\\s]*\\s*\\bat\\s+)(?:(.*)\\s+\\()?((?:\\/|[a-zA-Z]:\\\\)[^:\\)]+:(\\d+)(?::(\\d+))?)\\)$" ); var CACHED_FILE; var Test = class { static { __name(this, "Test"); } /** * @constructor * @param {string} name * @param {TestFn} fn * @param {TestRunner} runner */ constructor(name, fn, runner) { this.name = name; this._planned = null; this._actual = null; this.fn = fn; this.runner = runner; this._result = { pass: 0, fail: 0 }; this.done = false; this.strict = runner.strict; } /** * @param {string} msg * @returns {void} */ comment(msg) { this.runner.report("# " + msg); } /** * Plan the number of assertions. * * @param {number} n * @returns {void} */ plan(n) { this._planned = n; } /** * @template T * @param {T} actual * @param {T} expected * @param {string} [msg] * @returns {void} */ deepEqual(actual, expected, msg) { if (this.strict && !msg) throw new Error("tapzero msg required"); this._assert( equal(actual, expected), actual, expected, msg || "should be equivalent", "deepEqual" ); } /** * @template T * @param {T} actual * @param {T} expected * @param {string} [msg] * @returns {void} */ notDeepEqual(actual, expected, msg) { if (this.strict && !msg) throw new Error("tapzero msg required"); this._assert( !equal(actual, expected), actual, expected, msg || "should not be equivalent", "notDeepEqual" ); } /** * @template T * @param {T} actual * @param {T} expected * @param {string} [msg] * @returns {void} */ equal(actual, expected, msg) { if (this.strict && !msg) throw new Error("tapzero msg required"); this._assert( // eslint-disable-next-line eqeqeq actual == expected, actual, expected, msg || "should be equal", "equal" ); } /** * @param {unknown} actual * @param {unknown} expected * @param {string} [msg] * @returns {void} */ notEqual(actual, expected, msg) { if (this.strict && !msg) throw new Error("tapzero msg required"); this._assert( // eslint-disable-next-line eqeqeq actual != expected, actual, expected, msg || "should not be equal", "notEqual" ); } /** * @param {string} [msg] * @returns {void} */ fail(msg) { if (this.strict && !msg) throw new Error("tapzero msg required"); this._assert( false, "fail called", "fail not called", msg || "fail called", "fail" ); } /** * @param {unknown} actual * @param {string} [msg] * @returns {void} */ ok(actual, msg) { if (this.strict && !msg) throw new Error("tapzero msg required"); this._assert( !!actual, actual, "truthy value", msg || "should be truthy", "ok" ); } /** * @param {Error | null | undefined} err * @param {string} [msg] * @returns {void} */ ifError(err, msg) { if (this.strict && !msg) throw new Error("tapzero msg required"); this._assert( !err, err, "no error", msg || String(err), "ifError" ); } /** * @param {Function} fn * @param {RegExp | any} [expected] * @param {string} [message] * @returns {Promise<void>} */ async throws(fn, expected, message) { if (typeof expected === "string") { message = expected; expected = void 0; } if (this.strict && !message) throw new Error("tapzero msg required"); let caught = void 0; try { await fn(); } catch (err) { caught = /** @type {Error} */ err; } let pass = !!caught; if (expected instanceof RegExp) { pass = !!(caught && expected.test(caught.message)); } else if (expected) { throw new Error(`t.throws() not implemented for expected: ${typeof expected}`); } this._assert(pass, caught, expected, message || "should throw", "throws"); } /** * @param {boolean} pass * @param {unknown} actual * @param {unknown} expected * @param {string} description * @param {string} operator * @returns {void} */ _assert(pass, actual, expected, description, operator) { if (this.done) { throw new Error( "assertion occurred after test was finished: " + this.name ); } if (this._planned !== null) { this._actual = (this._actual || 0) + 1; if (this._actual > this._planned) { throw new Error(`More tests than planned in TEST *${this.name}*`); } } const report = this.runner.report; const prefix = pass ? "ok" : "not ok"; const id = this.runner.nextId(); report(`${prefix} ${id} ${description}`); if (pass) { this._result.pass++; return; } const atErr = new Error(description); let err = atErr; if (actual && OBJ_TO_STRING.call(actual) === "[object Error]") { err = /** @type {Error} */ actual; actual = err.message; } this._result.fail++; report(" ---"); report(` operator: ${operator}`); let ex = toJSON(expected); let ac = toJSON(actual); if (Math.max(ex.length, ac.length) > 65) { ex = ex.replace(NEW_LINE_REGEX, "\n "); ac = ac.replace(NEW_LINE_REGEX, "\n "); report(` expected: |- ${ex}`); report(` actual: |- ${ac}`); } else { report(` expected: ${ex}`); report(` actual: ${ac}`); } const at = findAtLineFromError(atErr); if (at) { report(` at: ${at}`); } report(" stack: |-"); const st = (err.stack || "").split("\n"); for (const line of st) { report(` ${line}`); } report(" ..."); } /** * @returns {Promise<{ * pass: number, * fail: number * }>} */ async run() { this.runner.report("# " + this.name); const maybeP = this.fn(this); if (maybeP && typeof maybeP.then === "function") { await maybeP; } this.done = true; if (this._planned !== null) { if (this._planned > (this._actual || 0)) { throw new Error( `Test ended before the planned number planned: ${this._planned} actual: ${this._actual || 0} ` ); } } return this._result; } }; function getTapZeroFileName() { if (CACHED_FILE) return CACHED_FILE; const e = new Error("temp"); const lines = (e.stack || "").split("\n"); for (const line of lines) { const m = AT_REGEX.exec(line); if (!m) { continue; } let fileName = m[2]; if (m[4] && fileName.endsWith(`:${m[4]}`)) { fileName = fileName.slice(0, fileName.length - m[4].length - 1); } if (m[3] && fileName.endsWith(`:${m[3]}`)) { fileName = fileName.slice(0, fileName.length - m[3].length - 1); } CACHED_FILE = fileName; break; } return CACHED_FILE || ""; } __name(getTapZeroFileName, "getTapZeroFileName"); function findAtLineFromError(e) { const lines = (e.stack || "").split("\n"); const dir = getTapZeroFileName(); for (const line of lines) { const m = AT_REGEX.exec(line); if (!m) { continue; } if (m[2].slice(0, dir.length) === dir) { continue; } return `${m[1] || "<anonymous>"} (${m[2]})`; } return ""; } __name(findAtLineFromError, "findAtLineFromError"); var TestRunner = class { static { __name(this, "TestRunner"); } /** * @constructor * @param {(lines: string) => void} [report] */ constructor(report) { this.report = report || printLine; this.tests = []; this.onlyTests = []; this.scheduled = false; this._id = 0; this.completed = false; this.rethrowExceptions = true; this.strict = false; this._onFinishCallback = void 0; } /** * @returns {string} */ nextId() { return String(++this._id); } /** * @param {string} name * @param {TestFn} fn * @param {boolean} only * @returns {void} */ add(name, fn, only2) { if (this.completed) { throw new Error("Cannot add() a test case after tests completed."); } const t = new Test(name, fn, this); const arr = only2 ? this.onlyTests : this.tests; arr.push(t); if (!this.scheduled) { this.scheduled = true; setTimeout(() => { const promise = this.run(); if (this.rethrowExceptions) { promise.then(null, rethrowImmediate); } }, 0); } } /** * @returns {Promise<void>} */ async run() { const ts = this.onlyTests.length > 0 ? this.onlyTests : this.tests; this.report("TAP version 13"); let total = 0; let success = 0; let fail = 0; for (const test2 of ts) { const result = await test2.run(); total += result.fail + result.pass; success += result.pass; fail += result.fail; } this.completed = true; this.report(""); this.report(`1..${total}`); this.report(`# tests ${total}`); this.report(`# pass ${success}`); if (fail) { this.report(`# fail ${fail}`); } else { this.report(""); this.report("# ok"); } if (this._onFinishCallback) { this._onFinishCallback({ total, success, fail }); } else { if (typeof process !== "undefined" && typeof process.exit === "function" && typeof process.on === "function" && Reflect.get(process, "browser") !== true) { process.on("exit", function(code) { if (typeof code === "number" && code !== 0) { return; } if (fail) { process.exit(1); } }); } } } /** * @param {(result: { total: number, success: number, fail: number }) => void} callback * @returns {void} */ onFinish(callback) { if (typeof callback === "function") { this._onFinishCallback = callback; } else throw new Error("onFinish() expects a function"); } }; function printLine(line) { console.log(line); } __name(printLine, "printLine"); var GLOBAL_TEST_RUNNER = new TestRunner(); function only(name, fn) { if (!fn) return; GLOBAL_TEST_RUNNER.add(name, fn, true); } __name(only, "only"); function skip(_name, _fn) { } __name(skip, "skip"); function setStrict(strict) { GLOBAL_TEST_RUNNER.strict = strict; } __name(setStrict, "setStrict"); function test(name, fn) { if (!fn) return; GLOBAL_TEST_RUNNER.add(name, fn, false); } __name(test, "test"); test.only = only; test.skip = skip; function rethrowImmediate(err) { setTimeout(rethrow, 0); function rethrow() { throw err; } __name(rethrow, "rethrow"); } __name(rethrowImmediate, "rethrowImmediate"); function toJSON(thing) { const replacer = /* @__PURE__ */ __name((_k, v) => v === void 0 ? "_tz_undefined_tz_" : v, "replacer"); const json = JSON.stringify(thing, replacer, " ") || "undefined"; return json.replace(/"_tz_undefined_tz_"/g, "undefined"); } __name(toJSON, "toJSON"); // test/encoding-length.test.ts import { fileURLToPath } from "url"; // node_modules/@substrate-system/uint8-util/dist/util.js var __defProp2 = Object.defineProperty; var __name2 = (target, value) => __defProp2(target, "name", { value, configurable: true }); var alphabet = "0123456789abcdef"; var encodeLookup = []; var decodeLookup = []; for (let i = 0; i < 256; i++) { encodeLookup[i] = alphabet[i >> 4 & 15] + alphabet[i & 15]; if (i < 16) { if (i < 10) { decodeLookup[48 + i] = i; } else { decodeLookup[97 - 10 + i] = i; } } } var arr2hex = /* @__PURE__ */ __name2((data) => { const length = data.length; let string = ""; let i = 0; while (i < length) { string += encodeLookup[data[i++]]; } return string; }, "arr2hex"); var concat = /* @__PURE__ */ __name2((chunks, size = 0) => { const length = chunks.length || 0; if (!size) { let i2 = length; while (i2--) size += chunks[i2].length; } const b = new Uint8Array(size); let offset = size; let i = length; while (i--) { offset -= chunks[i].length; b.set(chunks[i], offset); } return b; }, "concat"); // node_modules/@substrate-system/uint8-util/dist/node/index.js var __defProp3 = Object.defineProperty; var __name3 = (target, value) => __defProp3(target, "name", { value, configurable: true }); var decoder = new TextDecoder(); var arr2text = /* @__PURE__ */ __name3((data, enc) => { if (data.byteLength > 1024) { if (!enc) return decoder.decode(data); const dec = new TextDecoder(enc); return dec.decode(data); } return Buffer.from(data).toString(enc || "utf8"); }, "arr2text"); var text2arr = /* @__PURE__ */ __name3((str) => new Uint8Array(Buffer.from(str, "utf8")), "text2arr"); // src/util.ts function digitCount(value) { const sign = value < 0 ? 1 : 0; value = Math.abs(Number(value || 1)); return Math.floor(Math.log10(value)) + 1 + sign; } function getType(value) { if (ArrayBuffer.isView(value)) return "arraybufferview"; if (Array.isArray(value)) return "array"; if (value instanceof Number) return "number"; if (value instanceof Boolean) return "boolean"; if (value instanceof Set) return "set"; if (value instanceof Map) return "map"; if (value instanceof String) return "string"; if (value instanceof ArrayBuffer) return "arraybuffer"; return typeof value; } // src/encode.ts function encode(data, buffer, offset) { const buffers = []; let result = null; encode._encode(buffers, data); result = concat(buffers); encode.bytes = result.length; if (ArrayBuffer.isView(buffer)) { buffer.set(result, offset); return buffer; } return result; } encode.bytes = -1; encode._floatConversionDetected = false; encode._encode = function(buffers, data) { if (data == null) { return; } switch (getType(data)) { case "object": encode.dict(buffers, data); break; case "map": encode.dictMap(buffers, data); break; case "array": encode.list(buffers, data); break; case "set": encode.listSet(buffers, data); break; case "string": encode.string(buffers, data); break; case "number": encode.number(buffers, data); break; case "boolean": encode.number(buffers, data); break; case "arraybufferview": encode.buffer(buffers, new Uint8Array(data.buffer, data.byteOffset, data.byteLength)); break; case "arraybuffer": encode.buffer(buffers, new Uint8Array(data)); break; } }; var buffE = new Uint8Array([101]); var buffD = new Uint8Array([100]); var buffL = new Uint8Array([108]); encode.buffer = function(buffers, data) { buffers.push(text2arr(data.length + ":"), data); }; encode.string = function(buffers, data) { buffers.push(text2arr(text2arr(data).byteLength + ":" + data)); }; encode.number = function(buffers, data) { if (Number.isInteger(data)) return buffers.push(text2arr("i" + BigInt(data) + "e")); const maxLo = 2147483648; const hi = data / maxLo << 0; const lo = data % maxLo << 0; const val = hi * maxLo + lo; buffers.push(text2arr("i" + val + "e")); if (val !== data && !encode._floatConversionDetected) { encode._floatConversionDetected = true; console.warn( 'WARNING: Possible data corruption detected with value "' + data + '":', 'Bencoding only defines support for integers, value was converted to "' + val + '"' ); console.trace(); } }; encode.dict = function(buffers, data) { buffers.push(buffD); let j = 0; let k; const keys = Object.keys(data).sort(); const kl = keys.length; for (; j < kl; j++) { k = keys[j]; if (data[k] == null) continue; encode.string(buffers, k); encode._encode(buffers, data[k]); } buffers.push(buffE); }; encode.dictMap = function(buffers, data) { buffers.push(buffD); const keys = Array.from(data.keys()).sort(); for (const key of keys) { if (data.get(key) == null) { continue; } if (ArrayBuffer.isView(key)) { encode._encode(buffers, key); } else { encode.string(buffers, String(key)); } encode._encode(buffers, data.get(key)); } buffers.push(buffE); }; encode.list = function(buffers, data) { let i = 0; const c = data.length; buffers.push(buffL); for (; i < c; i++) { if (data[i] == null) continue; encode._encode(buffers, data[i]); } buffers.push(buffE); }; encode.listSet = function(buffers, data) { buffers.push(buffL); for (const item of data) { if (item == null) continue; encode._encode(buffers, item); } buffers.push(buffE); }; var encode_default = encode; // src/encoding-length.ts function listLength(list) { let length = 1 + 1; for (const value of list) { length += encodingLength(value); } return length; } function mapLength(map) { let length = 1 + 1; for (const [key, value] of map) { const keyLength = text2arr(key).byteLength; length += digitCount(keyLength) + 1 + keyLength; length += encodingLength(value); } return length; } function objectLength(value) { let length = 1 + 1; const keys = Object.keys(value); for (let i = 0; i < keys.length; i++) { const keyLength = text2arr(keys[i]).byteLength; length += digitCount(keyLength) + 1 + keyLength; length += encodingLength(value[keys[i]]); } return length; } function stringLength(value) { const length = text2arr(value).byteLength; return digitCount(length) + 1 + length; } function arrayBufferLength(value) { const length = value.byteLength - value.byteOffset; return digitCount(length) + 1 + length; } function encodingLength(value) { const length = 0; if (value == null) return length; const type = getType(value); switch (type) { case "arraybufferview": return arrayBufferLength(value); case "string": return stringLength(value); case "array": case "set": return listLength(value); case "number": return 1 + digitCount(Math.floor(value)) + 1; case "bigint": return 1 + value.toString().length + 1; case "object": return objectLength(value); case "map": return mapLength(value); default: throw new TypeError(`Unsupported value of type "${type}"`); } } var encoding_length_default = encodingLength; // src/decode.ts var INTEGER_START = 105; var STRING_DELIM = 58; var DICTIONARY_START = 100; var LIST_START = 108; var END_OF_TYPE = 101; function getIntFromBuffer(buffer, start, end) { let sum = 0; let sign = 1; for (let i = start; i < end; i++) { const num = buffer[i]; if (num < 58 && num >= 48) { sum = sum * 10 + (num - 48); continue; } if (i === start && num === 43) { continue; } if (i === start && num === 45) { sign = -1; continue; } if (num === 46) { break; } throw new Error("not a number: buffer[" + i + "] = " + num); } return sum * sign; } var decode = function decode2(data, start, end, encoding) { if (data == null || data.length === 0) { return null; } if (typeof start !== "number" && encoding == null) { encoding = start; start = void 0; } if (typeof end !== "number" && encoding == null) { encoding = end; end = void 0; } decode2.position = 0; decode2.encoding = encoding || null; decode2.data = !ArrayBuffer.isView(data) ? text2arr(data) : new Uint8Array( data.slice( start, end ) ); decode2.bytes = decode2.data.length; return decode2.next(); }; decode.bytes = 0; decode.position = 0; decode.data = null; decode.encoding = null; decode.next = function() { switch (decode.data[decode.position]) { case DICTIONARY_START: return decode.dictionary(); case LIST_START: return decode.list(); case INTEGER_START: return decode.integer(); default: return decode.buffer(); } }; decode.find = function(chr) { if (!decode.data?.length) return null; let i = decode.position; const c = decode.data.length; const d = decode.data; while (i < c) { if (d[i] === chr) return i; i++; } throw new Error( 'Invalid data: Missing delimiter "' + String.fromCharCode(chr) + '" [0x' + chr.toString(16) + "]" ); }; decode.dictionary = function() { if (!decode.data) return null; decode.position++; const dict = {}; while (decode.data[decode.position] !== END_OF_TYPE) { const buffer = decode.buffer(); if (typeof buffer === "string") { dict[buffer] = decode.next(); continue; } let key = arr2text(buffer); if (key.includes("\uFFFD")) key = arr2hex(buffer); dict[key] = decode.next(); } decode.position++; return dict; }; decode.list = function() { decode.position++; const lst = []; while (decode.data[decode.position] !== END_OF_TYPE) { lst.push(decode.next()); } decode.position++; return lst; }; decode.integer = function() { const end = decode.find(END_OF_TYPE); const number = getIntFromBuffer(decode.data, decode.position + 1, end); if (!end) throw new Error("not end"); decode.position += end + 1 - decode.position; return number; }; decode.buffer = function() { const sep = decode.find(STRING_DELIM); const newIndex = (sep || 0) + 1; const length = getIntFromBuffer(decode.data, decode.position, sep); const end = newIndex + length; decode.position = end; return decode.encoding ? arr2text(decode.data.slice(newIndex, end)) : decode.data.slice(newIndex, end); }; var decode_default = decode; // src/index.ts var encodingLength2 = encoding_length_default; var src_default = { encode: encode_default, decode: decode_default, byteLength: encoding_length_default, encodingLength: encodingLength2 }; // test/encoding-length.test.ts var __filename = fileURLToPath(import.meta.url); var __dirname = dirname(__filename); var torrent = fs.readFileSync( path.join(__dirname, "..", "benchmark", "test.torrent") ); test("torrent", function(t) { t.plan(1); const value = src_default.decode(torrent); const length = src_default.encodingLength(value); t.equal(length, torrent.length); }); test("returns correct length for empty dictionaries", function(t) { t.plan(2); t.equal(src_default.encodingLength({}), 2); t.equal(src_default.encodingLength(/* @__PURE__ */ new Map()), 2); }); test("returns correct length for dictionaries", function(t) { t.plan(2); const obj = { a: 1, b: "str", c: { de: "f" } }; const map = /* @__PURE__ */ new Map([ ["a", 1], ["b", "str"], ["c", { de: "f" }] ]); t.equal(src_default.encodingLength(obj), 28); t.equal(src_default.encodingLength(map), 28); }); test("returns correct length for empty lists", function(t) { t.plan(2); t.equal(src_default.encodingLength([]), 2); t.equal(src_default.encodingLength(/* @__PURE__ */ new Set()), 2); }); test("returns correct length for lists", function(t) { t.plan(3); t.equal(src_default.encodingLength([1, 2, 3]), 11); t.equal(src_default.encodingLength([1, "string", [{ a: 1, b: 2 }]]), 29); t.equal(src_default.encodingLength(/* @__PURE__ */ new Set([1, "string", [{ a: 1, b: 2 }]])), 29); }); test("returns correct length for integers", function(t) { t.plan(2); t.equal(src_default.encodingLength(-0), 3); t.equal(src_default.encodingLength(-1), 4); }); test("returns integer part length for floating point numbers", function(t) { t.plan(1); t.equal(src_default.encodingLength(100.25), 1 + 1 + 3); }); test("returns correct length for BigInts", function(t) { t.plan(1); t.equal(src_default.encodingLength(3402823669209385e23), 1 + 1 + 39); }); test("returns zero for undefined or null values", function(t) { t.plan(2); t.equal(src_default.encodingLength(null), 0); t.equal(src_default.encodingLength(void 0), 0); }); /*! Bundled license information: @substrate-system/uint8-util/dist/util.js: (* Common package for dealing with hex/string/uint8 conversions (and sha1 hashing) * * @author Jimmy Wärting <jimmy@warting.se> (https://jimmy.warting.se/opensource) * @license MIT *) */