@jsonjoy.com/json-pack
Version:
High-performance JSON serialization library
484 lines • 21 kB
JavaScript
"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