@substrate-system/bencode
Version:
Bencode de/encoder
964 lines (952 loc) • 25.7 kB
JavaScript
// 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
*)
*/