/** @deprecated Buggy legacy library, don't use it. Use utils/urlJSON instead. */ const CODES = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-."; // Not used symbols _!~*'() const NUM_CODES = "0123456789.e+-"; const NUM_CODES_SIZE = Math.ceil(Math.log(NUM_CODES.length) / Math.log(2)); const MAX_INT_SIZE = Math.log(Number.MAX_SAFE_INTEGER) / Math.log(2); function NumberToBytes(val) { return new Uint8Array(new Float64Array([val]).buffer, 0, 8); } function NumberFromBytes(val) { return new Float64Array(new Uint8Array(val).buffer, 0, 1)[0]; } function PackJSONBuffer(domain = CODES) { this.domain = domain; this.bufferCellSize = Math.floor(Math.log(domain.length) / Math.log(2)); this.clear(); } Object.defineProperty(PackJSONBuffer.prototype, "lastCell", { get() { return this.buffer[this.buffer.length - 1]; }, set(val) { this.buffer[this.buffer.length - 1] = val; }, }); PackJSONBuffer.prototype.MAX_INT_CHUNK_SIZE = 30; PackJSONBuffer.prototype.pushChunk = function (size, val) { if (this.readonly) throw Error("Cannot push the chunk. The value is readonly"); while (size > 0) { if (this.avaliableBufferCellSize === 0) { this.buffer.push(0); this.avaliableBufferCellSize = this.bufferCellSize; } if (this.avaliableBufferCellSize >= size) { this.lastCell |= ((1 << this.bufferCellSize) - 1) & (val << (this.avaliableBufferCellSize -= size)); size = 0; } else { this.lastCell |= ((1 << this.bufferCellSize) - 1) & (val >> (size -= this.avaliableBufferCellSize)); this.avaliableBufferCellSize = 0; } } }; PackJSONBuffer.prototype.readChunk = function (size) { if (size > this.MAX_INT_CHUNK_SIZE) throw Error(`Unsupported size of a chunk. Couldn't be greater than ${this.MAX_INT_CHUNK_SIZE}`); let chunk = 0; let cellIdx; let cellPos; let cellValueSize; let cellValue; while (size > 0) { cellPos = this.pos % this.bufferCellSize; cellIdx = (this.pos - cellPos) / this.bufferCellSize; cellValueSize = this.bufferCellSize - cellPos; cellValue = this.buffer[cellIdx] & ((1 << cellValueSize) - 1); chunk = (chunk << Math.min(cellValueSize, size)) | (cellValue >> Math.max(cellValueSize - size, 0)); this.pos += Math.min(cellValueSize, size); size -= cellValueSize; } return chunk; }; PackJSONBuffer.prototype.seek = function (pos) { this.pos = pos; }; PackJSONBuffer.prototype.readBytes = function (length) { return new Uint8Array(length).map(() => this.readChunk(8)); }; PackJSONBuffer.prototype.clear = function () { this.buffer = []; this.avaliableBufferCellSize = 0; this.readonly = false; this.pos = 0; }; PackJSONBuffer.prototype.toString = function () { return this.buffer.map((domainIdx) => this.domain[domainIdx]).join(""); }; PackJSONBuffer.prototype.fromString = function (string) { this.buffer = string.split("").map((char) => this.domain.indexOf(char)); this.readonly = true; this.pos = 0; }; PackJSONBuffer.fromString = (string, domain) => new PackJSONBuffer(domain).fromString(string); /* body:= [...value] value := 0 spec [0:stop]|[1:true]|[2:false][3:null] 1 number [0:float]|[1:int]|[2:string] ? 2 string [type][[0:[...[char]|[[.][string_code]]]|[1:...[string_code]]|[2:...[[0:isChar][char]|[1:isCode][string_code]]]] 3 array [...[value]][0:stop] 4 object [...[[value:key][value:value]]][0:stop] 5 definitions 6 const [value] definitions:= 1 dictionary [...bodyValues] (getting by idx) */ const CODE_SIZE = 3; const SPEC_CODE = 0; const SPEC_LITERALS = [undefined, true, false, null]; const SPEC_SIZE = (SPEC_LITERALS.length - 1).toString(2).length; const NUMBER_CODE = 1; const NUMBER_TYPE_SIZE = 2; const NUMBER_FLOAT_TYPE = 0; const NUMBER_INTEGER_TYPE = 1; const NUMBER_STRING_TYPE = 2; const STRING_CODE = 2; const STRING_LEN_BLOCK_SIZE = 4; const STRING_TYPE_SIZE = 3; // with a future potential const INFREQUENT_CODES_STRING_TYPE = 0; const ONLY_CODES_STRING_TYPE = 1; const MARKED_CHARS_STRING_TYPE = 2; const ARRAY_CODE = 3; const OBJECT_CODE = 4; const DEFINITION_CODE = 5; const DEFINITION_TYPE_SIZE = 2; const DICT_DEFINITION_TYPE = 1; const CONST_CODE = 6; const PackJSON = function (domain) { this.buffer = new PackJSONBuffer(domain); }; PackJSON.prototype.stringify = function (json) { this.buffer.clear(); this.makeDictionaries(json); this.encode(json); return this.buffer.toString(); }; PackJSON.prototype.parse = function (string) { this.buffer.fromString(string); this.definitions = []; return this.decode(); }; PackJSON.prototype.makeDictionaries = function (json) { this.sharedValuesCount = 0; this.sharedNumbersDict = {}; this.sharedStringsDict = {}; this.tmpSharedValuesSet = new Set(); this.collectObjectWords(json); this.encodeDefinitions(); }; PackJSON.prototype.collectObjectWords = function (value) { switch (typeof value) { case "number": { if (this.tmpSharedValuesSet.has(value) && this.sharedStringsDict[value] === undefined) { this.sharedNumbersDict[value] = this.sharedValuesCount++; } this.tmpSharedValuesSet.add(value); break; } case "string": { if (this.tmpSharedValuesSet.has(value) && this.sharedStringsDict[value] === undefined) { this.sharedStringsDict[value] = this.sharedValuesCount++; } this.tmpSharedValuesSet.add(value); break; } case "object": { if (value === null) return; if (Array.isArray(value)) { value.forEach((v) => this.collectObjectWords(v)); } else { for (const [key, val] of Object.entries(value)) { this.collectObjectWords(key); this.collectObjectWords(val); } } break; } } }; PackJSON.prototype.encode = function (value) { const type = typeof value; switch (type) { case "boolean": { this.encodeSpec(value); break; } case "number": { this.encodeNumber(value); break; } case "string": { this.encodeString(value); break; } case "object": { if (value === null) { this.encodeSpec(value); } else if (Array.isArray(value)) { this.encodeArray(value); } else { this.encodeObject(value); } break; } } }; PackJSON.prototype.decode = function () { const code = this.buffer.readChunk(CODE_SIZE); switch (code) { case SPEC_CODE: { return this.decodeSpec(); } case NUMBER_CODE: { return this.decodeNumber(); } case STRING_CODE: { return this.decodeString(); } case ARRAY_CODE: { return this.decodeArray(); } case OBJECT_CODE: { return this.decodeObject(); } case DEFINITION_CODE: { return this.decodeDefinitions(); } case CONST_CODE: { return this.decodeConst(); } } }; PackJSON.prototype.encodeSpec = function (value) { this.buffer.pushChunk(CODE_SIZE, SPEC_CODE); this.buffer.pushChunk(SPEC_SIZE, SPEC_LITERALS.indexOf(value)); }; PackJSON.prototype.decodeSpec = function () { return SPEC_LITERALS[this.buffer.readChunk(SPEC_SIZE)]; }; PackJSON.prototype.encodeNumber = function (value) { if (this.definitions?.indexOf(value) > -1) { return this.encodeConst(this.sharedNumbersDict[value]); } this.buffer.pushChunk(CODE_SIZE, NUMBER_CODE); if (Number.isInteger(value)) { this.buffer.pushChunk(NUMBER_TYPE_SIZE, NUMBER_INTEGER_TYPE); this.buffer.pushChunk(1, value < 0); value = Math.abs(value); this.buffer.pushChunk(MAX_INT_SIZE.toString(2).length, value.toString(2).length); value .toString(32) .split("") .forEach((b32, idx) => { const val = Number.parseInt(b32, 32); this.buffer.pushChunk(idx ? 5 : val.toString(2).length, val); }); } else { const stringValue = JSON.stringify(value); if (stringValue.length * NUM_CODES_SIZE < 64) { this.buffer.pushChunk(NUMBER_TYPE_SIZE, NUMBER_STRING_TYPE); this.buffer.pushChunk((64 / NUM_CODES_SIZE - 1).toString(2).length, stringValue.length); stringValue.split("").forEach((ch) => { this.buffer.pushChunk(NUM_CODES_SIZE, NUM_CODES.indexOf(ch)); }); } else { const bytes = NumberToBytes(value); this.buffer.pushChunk(NUMBER_TYPE_SIZE, NUMBER_FLOAT_TYPE); bytes.forEach((byte) => this.buffer.pushChunk(8, byte)); } } }; PackJSON.prototype.decodeNumber = function () { const type = this.buffer.readChunk(NUMBER_TYPE_SIZE); switch (type) { case NUMBER_INTEGER_TYPE: { const sign = this.buffer.readChunk(1); const size = this.buffer.readChunk(MAX_INT_SIZE.toString(2).length); const b32 = Array.apply(null, new Array(Math.ceil(size / 5))) .map((v, idx) => this.buffer.readChunk(idx ? 5 : size % 5 || 5).toString(32)) .join(""); return (sign ? -1 : 1) * Number.parseInt(b32, 32); } case NUMBER_STRING_TYPE: { const length = this.buffer.readChunk((64 / NUM_CODES_SIZE - 1).toString(2).length); return JSON.parse( Array.apply(null, new Array(length)) .map(() => NUM_CODES[this.buffer.readChunk(NUM_CODES_SIZE)]) .join(""), ); } case NUMBER_FLOAT_TYPE: { const bytes = this.buffer.readBytes(8); return NumberFromBytes(bytes); } } }; PackJSON.prototype.encodeString = function (value) { if (this.definitions?.indexOf(value) > -1) { return this.encodeConst(this.sharedStringsDict[value]); } value = this.packInConstants(value); this.buffer.pushChunk(CODE_SIZE, STRING_CODE); const knownCharsCount = value.split("").filter((ch) => { const idx = CODES.indexOf(ch); return idx > -1 && idx < CODES.length - 1; }).length; const unknownCharsCount = value.length - knownCharsCount; const potentialInfrequentCharsStringSize = knownCharsCount * 6 + unknownCharsCount * (6 + 16); const potentialOnlyCodesStringSize = value.length * 16; const potentialMarkedCharsStringSize = knownCharsCount * 7 + unknownCharsCount * 17; const minSize = Math.min( potentialInfrequentCharsStringSize, potentialOnlyCodesStringSize, potentialMarkedCharsStringSize, ); switch (minSize) { case potentialInfrequentCharsStringSize: { this.buffer.pushChunk(STRING_TYPE_SIZE, INFREQUENT_CODES_STRING_TYPE); this.encodeStringLen(value); value.split("").forEach((ch) => { const idx = CODES.indexOf(ch); if (idx > -1 && idx < CODES.length - 1) { this.buffer.pushChunk(6, idx); } else { this.buffer.pushChunk(6, CODES.length - 1); this.buffer.pushChunk(16, ch.charCodeAt(0)); } }); break; } case potentialOnlyCodesStringSize: { this.buffer.pushChunk(STRING_TYPE_SIZE, ONLY_CODES_STRING_TYPE); this.encodeStringLen(value); value.split("").forEach((ch) => { this.buffer.pushChunk(16, ch.charCodeAt(0)); }); break; } case potentialMarkedCharsStringSize: { this.buffer.pushChunk(STRING_TYPE_SIZE, MARKED_CHARS_STRING_TYPE); this.encodeStringLen(value); value.split("").forEach((ch) => { const idx = CODES.indexOf(ch); if (idx > -1) { this.buffer.pushChunk(1, 0); this.buffer.pushChunk(6, idx); } else { this.buffer.pushChunk(1, 1); this.buffer.pushChunk(16, ch.charCodeAt(0)); } }); break; } } }; PackJSON.prototype.encodeStringLen = function (value) { const stringLengthParts = value.length.toString(1 << STRING_LEN_BLOCK_SIZE).split(""); stringLengthParts.forEach((lenBlock, idx) => { this.buffer.pushChunk(STRING_LEN_BLOCK_SIZE, Number.parseInt(lenBlock, 1 << STRING_LEN_BLOCK_SIZE)); this.buffer.pushChunk(1, idx === stringLengthParts.length - 1); // stop chain marker }); }; PackJSON.prototype.decodeString = function () { const value = this._decodeString(); return this.resolveConstants(value); }; PackJSON.prototype._decodeString = function () { const stingType = this.buffer.readChunk(STRING_TYPE_SIZE); switch (stingType) { case INFREQUENT_CODES_STRING_TYPE: { const length = this.decodeStringLen(); return Array.apply(null, new Array(length)) .map(() => { const idx = this.buffer.readChunk(6); if (idx > -1 && idx < CODES.length - 2) { return CODES[idx]; } if (idx === CODES.length - 1) { return String.fromCharCode(this.buffer.readChunk(16)); } }) .join(""); } case ONLY_CODES_STRING_TYPE: { const length = this.decodeStringLen(); return Array.apply(null, new Array(length)) .map(() => String.fromCharCode(this.buffer.readChunk(16))) .join(""); } case MARKED_CHARS_STRING_TYPE: { const length = this.decodeStringLen(); return Array.apply(null, new Array(length)) .map(() => { const isCode = this.buffer.readChunk(1); if (!isCode) { return CODES[this.buffer.readChunk(6)]; } return String.fromCharCode(this.buffer.readChunk(16)); }) .join(""); } } }; PackJSON.prototype.decodeStringLen = function () { const stringLengthParts = []; let shouldStop = false; do { stringLengthParts.push(this.buffer.readChunk(STRING_LEN_BLOCK_SIZE).toString(1 << STRING_LEN_BLOCK_SIZE)); shouldStop = this.buffer.readChunk(1); } while (!shouldStop); return Number.parseInt(stringLengthParts.join(""), 1 << STRING_LEN_BLOCK_SIZE); }; PackJSON.prototype.packInConstants = function (value) { const re = /\./g; value = value.replace(re, ".-"); this.definitions.forEach((definition, idx) => { const re = new RegExp(definition, "g"); value = value.replace(re, `.${idx}`); }); return value; }; PackJSON.prototype.resolveConstants = function (value) { this.definitions.forEach((definition, idx) => { const re = new RegExp(`\\.${idx}`, "g"); value = value.replace(re, definition); }); const re = /\.-/g; value = value.replace(re, "."); return value; }; PackJSON.prototype.pushStopCode = function () { this.buffer.pushChunk(CODE_SIZE, SPEC_CODE); this.buffer.pushChunk(SPEC_SIZE, 0); }; PackJSON.prototype.encodeArray = function (value) { this.buffer.pushChunk(CODE_SIZE, ARRAY_CODE); const len = value.length; for (let i = 0; i < len; i++) { this.encode(value[i]); } this.pushStopCode(); }; PackJSON.prototype.decodeArray = function () { const res = []; while (!(this.buffer.readChunk(CODE_SIZE + SPEC_SIZE) === 0)) { this.buffer.seek(this.buffer.pos - (CODE_SIZE + SPEC_SIZE)); res.push(this.decode()); } return res; }; PackJSON.prototype.encodeObject = function (value) { this.buffer.pushChunk(CODE_SIZE, OBJECT_CODE); for (const [key, val] of Object.entries(value)) { this.encode(key); this.encode(val); } this.pushStopCode(); }; PackJSON.prototype.decodeObject = function () { const res = {}; while (!(this.buffer.readChunk(CODE_SIZE + SPEC_SIZE) === 0)) { this.buffer.seek(this.buffer.pos - (CODE_SIZE + SPEC_SIZE)); res[this.decode()] = this.decode(); } return res; }; PackJSON.prototype.encodeDefinitions = function () { const definitions = []; this.definitions = []; [this.sharedNumbersDict, this.sharedStringsDict].forEach((dictionary) => { Object.entries(dictionary).forEach(([value, idx]) => { definitions[idx] = value; }); }); if (!definitions.length) return; this.buffer.pushChunk(CODE_SIZE, DEFINITION_CODE); this.buffer.pushChunk(DEFINITION_TYPE_SIZE, DICT_DEFINITION_TYPE); this.definitionIndexSize = Math.ceil(Math.log(definitions.length) / Math.log(2)); definitions.forEach((definition) => { this.encode(definition); }); this.definitions = definitions; this.pushStopCode(); }; PackJSON.prototype.decodeDefinitions = function () { this.buffer.readChunk(DEFINITION_TYPE_SIZE); while (!(this.buffer.readChunk(CODE_SIZE + SPEC_SIZE) === 0)) { this.buffer.seek(this.buffer.pos - (CODE_SIZE + SPEC_SIZE)); this.definitions.push(this.decode()); } this.definitionIndexSize = Math.ceil(Math.log(this.definitions.length) / Math.log(2)); // decode next as if there weren't any definitions return this.decode(); }; PackJSON.prototype.encodeConst = function (idx) { this.buffer.pushChunk(CODE_SIZE, CONST_CODE); this.buffer.pushChunk(this.definitionIndexSize, idx); }; PackJSON.prototype.decodeConst = function () { const definitionIndex = this.buffer.readChunk(this.definitionIndexSize); return this.definitions[definitionIndex]; }; const packJSON = new PackJSON(); export { PackJSON, packJSON };