UNPKG

@jsonjoy.com/json-pack

Version:

High-performance JSON serialization library

484 lines 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EjsonDecoder = void 0; const values_1 = require("../bson/values"); const JsonDecoder_1 = require("../json/JsonDecoder"); const JsonDecoder_2 = require("../json/JsonDecoder"); class EjsonDecoder extends JsonDecoder_1.JsonDecoder { constructor(options = {}) { super(); this.options = options; } /** * Decode from string (for backward compatibility). * This method maintains the previous API but uses the binary decoder internally. */ decodeFromString(json) { const bytes = new TextEncoder().encode(json); return this.decode(bytes); } readAny() { this.skipWhitespace(); const reader = this.reader; const uint8 = reader.uint8; const char = uint8[reader.x]; switch (char) { case 34 /* " */: return this.readStr(); case 91 /* [ */: return this.readArr(); case 102 /* f */: return this.readFalse(); case 110 /* n */: return this.readNull(); case 116 /* t */: return this.readTrue(); case 123 /* { */: return this.readObjWithEjsonSupport(); default: if ((char >= 48 /* 0 */ && char <= 57) /* 9 */ || char === 45 /* - */) return this.readNum(); throw new Error('Invalid JSON'); } } readArr() { const reader = this.reader; if (reader.u8() !== 0x5b /* [ */) throw new Error('Invalid JSON'); const arr = []; const uint8 = reader.uint8; let first = true; while (true) { this.skipWhitespace(); const char = uint8[reader.x]; if (char === 0x5d /* ] */) return reader.x++, arr; if (char === 0x2c /* , */) reader.x++; else if (!first) throw new Error('Invalid JSON'); this.skipWhitespace(); arr.push(this.readAny()); // Arrays should process EJSON objects recursively first = false; } } readObjWithEjsonSupport() { const reader = this.reader; if (reader.u8() !== 0x7b /* { */) throw new Error('Invalid JSON'); const obj = {}; const uint8 = reader.uint8; let first = true; while (true) { this.skipWhitespace(); let char = uint8[reader.x]; if (char === 0x7d /* } */) { reader.x++; // Check if this is an EJSON type wrapper return this.transformEjsonObject(obj); } if (char === 0x2c /* , */) reader.x++; else if (!first) throw new Error('Invalid JSON'); this.skipWhitespace(); char = uint8[reader.x++]; if (char !== 0x22 /* " */) throw new Error('Invalid JSON'); const key = (0, JsonDecoder_2.readKey)(reader); if (key === '__proto__') throw new Error('Invalid JSON'); this.skipWhitespace(); if (reader.u8() !== 0x3a /* : */) throw new Error('Invalid JSON'); this.skipWhitespace(); // For EJSON type wrapper detection, we need to read nested objects as raw first obj[key] = this.readValue(); first = false; } } readValue() { this.skipWhitespace(); const reader = this.reader; const uint8 = reader.uint8; const char = uint8[reader.x]; switch (char) { case 34 /* " */: return this.readStr(); case 91 /* [ */: return this.readArr(); case 102 /* f */: return this.readFalse(); case 110 /* n */: return this.readNull(); case 116 /* t */: return this.readTrue(); case 123 /* { */: return this.readRawObj(); // Read as raw object first default: if ((char >= 48 /* 0 */ && char <= 57) /* 9 */ || char === 45 /* - */) return this.readNum(); throw new Error('Invalid JSON'); } } readRawObj() { const reader = this.reader; if (reader.u8() !== 0x7b /* { */) throw new Error('Invalid JSON'); const obj = {}; const uint8 = reader.uint8; let first = true; while (true) { this.skipWhitespace(); let char = uint8[reader.x]; if (char === 0x7d /* } */) { reader.x++; return obj; // Return raw object without transformation } if (char === 0x2c /* , */) reader.x++; else if (!first) throw new Error('Invalid JSON'); this.skipWhitespace(); char = uint8[reader.x++]; if (char !== 0x22 /* " */) throw new Error('Invalid JSON'); const key = (0, JsonDecoder_2.readKey)(reader); if (key === '__proto__') throw new Error('Invalid JSON'); this.skipWhitespace(); if (reader.u8() !== 0x3a /* : */) throw new Error('Invalid JSON'); this.skipWhitespace(); obj[key] = this.readValue(); first = false; } } transformEjsonObject(obj) { const keys = Object.keys(obj); // Helper function to validate exact key match const hasExactKeys = (expectedKeys) => { if (keys.length !== expectedKeys.length) return false; return expectedKeys.every((key) => keys.includes(key)); }; // Check if object has any special $ keys that indicate a type wrapper const specialKeys = keys.filter((key) => key.startsWith('$')); if (specialKeys.length > 0) { // ObjectId if (specialKeys.includes('$oid')) { if (!hasExactKeys(['$oid'])) { throw new Error('Invalid ObjectId format: extra keys not allowed'); } const oidStr = obj.$oid; if (typeof oidStr === 'string' && /^[0-9a-fA-F]{24}$/.test(oidStr)) { return this.parseObjectId(oidStr); } throw new Error('Invalid ObjectId format'); } // Int32 if (specialKeys.includes('$numberInt')) { if (!hasExactKeys(['$numberInt'])) { throw new Error('Invalid Int32 format: extra keys not allowed'); } const intStr = obj.$numberInt; if (typeof intStr === 'string') { const value = parseInt(intStr, 10); if (!Number.isNaN(value) && value >= -2147483648 && value <= 2147483647) { return new values_1.BsonInt32(value); } } throw new Error('Invalid Int32 format'); } // Int64 if (specialKeys.includes('$numberLong')) { if (!hasExactKeys(['$numberLong'])) { throw new Error('Invalid Int64 format: extra keys not allowed'); } const longStr = obj.$numberLong; if (typeof longStr === 'string') { const value = parseFloat(longStr); // Use parseFloat to handle large numbers better if (!Number.isNaN(value)) { return new values_1.BsonInt64(value); } } throw new Error('Invalid Int64 format'); } // Double if (specialKeys.includes('$numberDouble')) { if (!hasExactKeys(['$numberDouble'])) { throw new Error('Invalid Double format: extra keys not allowed'); } const doubleStr = obj.$numberDouble; if (typeof doubleStr === 'string') { if (doubleStr === 'Infinity') return new values_1.BsonFloat(Infinity); if (doubleStr === '-Infinity') return new values_1.BsonFloat(-Infinity); if (doubleStr === 'NaN') return new values_1.BsonFloat(NaN); const value = parseFloat(doubleStr); if (!Number.isNaN(value)) { return new values_1.BsonFloat(value); } } throw new Error('Invalid Double format'); } // Decimal128 if (specialKeys.includes('$numberDecimal')) { if (!hasExactKeys(['$numberDecimal'])) { throw new Error('Invalid Decimal128 format: extra keys not allowed'); } const decimalStr = obj.$numberDecimal; if (typeof decimalStr === 'string') { return new values_1.BsonDecimal128(new Uint8Array(16)); } throw new Error('Invalid Decimal128 format'); } // Binary if (specialKeys.includes('$binary')) { if (!hasExactKeys(['$binary'])) { throw new Error('Invalid Binary format: extra keys not allowed'); } const binaryObj = obj.$binary; if (typeof binaryObj === 'object' && binaryObj !== null) { const binaryKeys = Object.keys(binaryObj); if (binaryKeys.length === 2 && binaryKeys.includes('base64') && binaryKeys.includes('subType')) { const base64 = binaryObj.base64; const subType = binaryObj.subType; if (typeof base64 === 'string' && typeof subType === 'string') { const data = this.base64ToUint8Array(base64); const subtype = parseInt(subType, 16); return new values_1.BsonBinary(subtype, data); } } } throw new Error('Invalid Binary format'); } // UUID (special case of Binary) if (specialKeys.includes('$uuid')) { if (!hasExactKeys(['$uuid'])) { throw new Error('Invalid UUID format: extra keys not allowed'); } const uuidStr = obj.$uuid; if (typeof uuidStr === 'string' && this.isValidUuid(uuidStr)) { const data = this.uuidToBytes(uuidStr); return new values_1.BsonBinary(4, data); // Subtype 4 for UUID } throw new Error('Invalid UUID format'); } // Code if (specialKeys.includes('$code') && !specialKeys.includes('$scope')) { if (!hasExactKeys(['$code'])) { throw new Error('Invalid Code format: extra keys not allowed'); } const code = obj.$code; if (typeof code === 'string') { return new values_1.BsonJavascriptCode(code); } throw new Error('Invalid Code format'); } // CodeWScope if (specialKeys.includes('$code') && specialKeys.includes('$scope')) { if (!hasExactKeys(['$code', '$scope'])) { throw new Error('Invalid CodeWScope format: extra keys not allowed'); } const code = obj.$code; const scope = obj.$scope; if (typeof code === 'string' && typeof scope === 'object' && scope !== null) { return new values_1.BsonJavascriptCodeWithScope(code, this.transformEjsonObject(scope)); } throw new Error('Invalid CodeWScope format'); } // Symbol if (specialKeys.includes('$symbol')) { if (!hasExactKeys(['$symbol'])) { throw new Error('Invalid Symbol format: extra keys not allowed'); } const symbol = obj.$symbol; if (typeof symbol === 'string') { return new values_1.BsonSymbol(symbol); } throw new Error('Invalid Symbol format'); } // Timestamp if (specialKeys.includes('$timestamp')) { if (!hasExactKeys(['$timestamp'])) { throw new Error('Invalid Timestamp format: extra keys not allowed'); } const timestampObj = obj.$timestamp; if (typeof timestampObj === 'object' && timestampObj !== null) { const timestampKeys = Object.keys(timestampObj); if (timestampKeys.length === 2 && timestampKeys.includes('t') && timestampKeys.includes('i')) { const t = timestampObj.t; const i = timestampObj.i; if (typeof t === 'number' && typeof i === 'number' && t >= 0 && i >= 0) { return new values_1.BsonTimestamp(i, t); } } } throw new Error('Invalid Timestamp format'); } // Regular Expression if (specialKeys.includes('$regularExpression')) { if (!hasExactKeys(['$regularExpression'])) { throw new Error('Invalid RegularExpression format: extra keys not allowed'); } const regexObj = obj.$regularExpression; if (typeof regexObj === 'object' && regexObj !== null) { const regexKeys = Object.keys(regexObj); if (regexKeys.length === 2 && regexKeys.includes('pattern') && regexKeys.includes('options')) { const pattern = regexObj.pattern; const options = regexObj.options; if (typeof pattern === 'string' && typeof options === 'string') { return new RegExp(pattern, options); } } } throw new Error('Invalid RegularExpression format'); } // DBPointer if (specialKeys.includes('$dbPointer')) { if (!hasExactKeys(['$dbPointer'])) { throw new Error('Invalid DBPointer format: extra keys not allowed'); } const dbPointerObj = obj.$dbPointer; if (typeof dbPointerObj === 'object' && dbPointerObj !== null) { const dbPointerKeys = Object.keys(dbPointerObj); if (dbPointerKeys.length === 2 && dbPointerKeys.includes('$ref') && dbPointerKeys.includes('$id')) { const ref = dbPointerObj.$ref; const id = dbPointerObj.$id; if (typeof ref === 'string' && id !== undefined) { const transformedId = this.transformEjsonObject(id); if (transformedId instanceof values_1.BsonObjectId) { return new values_1.BsonDbPointer(ref, transformedId); } } } } throw new Error('Invalid DBPointer format'); } // Date if (specialKeys.includes('$date')) { if (!hasExactKeys(['$date'])) { throw new Error('Invalid Date format: extra keys not allowed'); } const dateValue = obj.$date; if (typeof dateValue === 'string') { // ISO-8601 format (relaxed) const date = new Date(dateValue); if (!Number.isNaN(date.getTime())) { return date; } } else if (typeof dateValue === 'object' && dateValue !== null) { // Canonical format with $numberLong const longObj = dateValue; const longKeys = Object.keys(longObj); if (longKeys.length === 1 && longKeys[0] === '$numberLong' && typeof longObj.$numberLong === 'string') { const timestamp = parseFloat(longObj.$numberLong); if (!Number.isNaN(timestamp)) { return new Date(timestamp); } } } throw new Error('Invalid Date format'); } // MinKey if (specialKeys.includes('$minKey')) { if (!hasExactKeys(['$minKey'])) { throw new Error('Invalid MinKey format: extra keys not allowed'); } if (obj.$minKey === 1) { return new values_1.BsonMinKey(); } throw new Error('Invalid MinKey format'); } // MaxKey if (specialKeys.includes('$maxKey')) { if (!hasExactKeys(['$maxKey'])) { throw new Error('Invalid MaxKey format: extra keys not allowed'); } if (obj.$maxKey === 1) { return new values_1.BsonMaxKey(); } throw new Error('Invalid MaxKey format'); } // Undefined if (specialKeys.includes('$undefined')) { if (!hasExactKeys(['$undefined'])) { throw new Error('Invalid Undefined format: extra keys not allowed'); } if (obj.$undefined === true) { return undefined; } throw new Error('Invalid Undefined format'); } } // DBRef (not a BSON type, but a convention) - special case, can have additional fields if (keys.includes('$ref') && keys.includes('$id')) { const ref = obj.$ref; const id = this.transformEjsonObject(obj.$id); const result = { $ref: ref, $id: id }; if (keys.includes('$db')) { result.$db = obj.$db; } // Add any other fields for (const key of keys) { if (key !== '$ref' && key !== '$id' && key !== '$db') { result[key] = this.transformEjsonObject(obj[key]); } } return result; } // Regular object - transform all properties const result = {}; for (const [key, val] of Object.entries(obj)) { if (typeof val === 'object' && val !== null && !Array.isArray(val)) { result[key] = this.transformEjsonObject(val); } else if (Array.isArray(val)) { result[key] = val.map((item) => typeof item === 'object' && item !== null && !Array.isArray(item) ? this.transformEjsonObject(item) : item); } else { result[key] = val; } } return result; } // Utility methods parseObjectId(hex) { // Parse 24-character hex string into ObjectId components const timestamp = parseInt(hex.slice(0, 8), 16); const process = parseInt(hex.slice(8, 18), 16); const counter = parseInt(hex.slice(18, 24), 16); return new values_1.BsonObjectId(timestamp, process, counter); } base64ToUint8Array(base64) { // Convert base64 string to Uint8Array const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } isValidUuid(uuid) { // UUID pattern: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx const uuidPattern = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; return uuidPattern.test(uuid); } uuidToBytes(uuid) { // Convert UUID string to 16-byte array const hex = uuid.replace(/-/g, ''); const bytes = new Uint8Array(16); for (let i = 0; i < 16; i++) { bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); } return bytes; } } exports.EjsonDecoder = EjsonDecoder; //# sourceMappingURL=EjsonDecoder.js.map