'use strict'; const { StringDecoder } = require('string_decoder'); const { types } = require('util'); const BUFFER_BYTES = 64 * 1024; // 读取时直接构造容器和正文,不先拼接整份 JSON 原文。 function createJsonReader(fs, filename) { const fd = fs.openSync(filename, 'r'); const buffer = Buffer.allocUnsafe(BUFFER_BYTES); let index = 0; let length = 0; let eof = false; let closed = false; function available() { if (index < length) return true; if (eof) return false; length = fs.readSync(fd, buffer, 0, buffer.length, null); index = 0; eof = length === 0; return !eof; } function whitespace() { while (available()) { const ch = buffer[index]; if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13) break; index += 1; } } function peek() { whitespace(); return available() ? buffer[index] : -1; } function invalid() { throw new SyntaxError('持久化 JSON 结构无效或意外结束'); } function consume(expected) { if (peek() !== expected.charCodeAt(0)) invalid(); index += 1; } function string() { consume('"'); const parts = []; let pending = []; let pendingChars = 0; let decoder = new StringDecoder('utf8'); function append(text) { if (!text) return; pending.push(text); pendingChars += text.length; if (pendingChars >= 8192) { parts.push(pending.join('')); pending = []; pendingChars = 0; } } while (available()) { const start = index; while (index < length && buffer[index] >= 32 && buffer[index] !== 34 && buffer[index] !== 92) index += 1; if (index > start) append(decoder.write(buffer.subarray(start, index))); if (index === length) continue; const ch = buffer[index++]; if (ch < 32) invalid(); // 转义字符与引号都是 ASCII,先结束前一段 UTF-8 解码。 append(decoder.end()); if (ch === 34) { parts.push(pending.join('')); return parts.join(''); } decoder = new StringDecoder('utf8'); if (!available()) invalid(); const escaped = buffer[index++]; const simple = { 34: '"', 92: '\\', 47: '/', 98: '\b', 102: '\f', 110: '\n', 114: '\r', 116: '\t' }; if (Object.prototype.hasOwnProperty.call(simple, escaped)) append(simple[escaped]); else if (escaped === 117) { let hex = ''; for (let count = 0; count < 4; count += 1) { if (!available()) invalid(); hex += String.fromCharCode(buffer[index++]); } if (!/^[0-9a-fA-F]{4}$/.test(hex)) invalid(); append(String.fromCharCode(Number.parseInt(hex, 16))); } else invalid(); } invalid(); } function object(field) { consume('{'); if (peek() === 125) { index += 1; return; } while (true) { if (peek() !== 34) invalid(); const key = string(); consume(':'); field(key); if (peek() === 125) { index += 1; return; } consume(','); } } function array(item) { consume('['); if (peek() === 93) { index += 1; return; } while (true) { item(value()); if (peek() === 93) { index += 1; return; } consume(','); } } function value() { const first = peek(); if (first === 34) return string(); if (first === 123) { const result = {}; object(key => Object.defineProperty(result, key, { value: value(), enumerable: true, writable: true, configurable: true, })); return result; } if (first === 91) { const result = []; array(item => result.push(item)); return result; } const parts = []; while (available()) { const start = index; while (index < length && ![44, 125, 93, 32, 9, 10, 13].includes(buffer[index])) index += 1; if (index > start) parts.push(buffer.toString('utf8', start, index)); if (index < length) break; } const token = parts.join(''); if (token === 'null') return null; if (token === 'true') return true; if (token === 'false') return false; if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(token)) invalid(); return Number(token); } return { value, object, array, end() { if (peek() !== -1) invalid(); }, close() { if (!closed) { closed = true; fs.closeSync(fd); } }, }; } function readJsonSync(fs, filename) { const reader = createJsonReader(fs, filename); try { const result = reader.value(); reader.end(); return result; } finally { reader.close(); } } // 与历史持久化既有 replacer 保持一致;toJSON 在项目类型转换之前执行。 function jsonValue(value, key) { if (value !== null && (typeof value === 'object' || typeof value === 'function' || typeof value === 'bigint')) { const toJSON = value.toJSON; if (typeof toJSON === 'function') value = toJSON.call(value, key); } if (typeof value === 'bigint') return String(value); if (value instanceof Map) value = Object.fromEntries(value); else if (value instanceof Set) value = [...value]; if (value && typeof value === 'object') { if (types.isNumberObject(value)) value = Number(value); else if (types.isStringObject(value)) value = String(value); else if (types.isBooleanObject(value)) value = Boolean.prototype.valueOf.call(value); else if (types.isBigIntObject(value)) throw new TypeError('JSON 不支持包装的 bigint'); } if (typeof value === 'number' && !Number.isFinite(value)) return null; if (value === 0) return 0; if (value === undefined || typeof value === 'function' || typeof value === 'symbol') return undefined; return value; } function cloneJsonValue(value) { const ancestors = new Set(); function clone(item, key) { item = jsonValue(item, key); if (item === null || typeof item !== 'object') return item; if (ancestors.has(item)) throw new TypeError('JSON 不支持循环引用'); ancestors.add(item); try { if (Array.isArray(item)) { const result = []; const length = item.length; for (let index = 0; index < length; index += 1) result.push(clone(item[index], String(index)) ?? null); return result; } const result = {}; for (const name of Object.keys(item)) { const next = clone(item[name], name); if (next !== undefined) Object.defineProperty(result, name, { value: next, enumerable: true, writable: true, configurable: true, }); } return result; } finally { ancestors.delete(item); } } return clone(value, ''); } function writeJsonSync(fs, fd, value) { const ancestors = new Set(); const output = Buffer.allocUnsafe(BUFFER_BYTES); let used = 0; function flush() { let offset = 0; while (offset < used) { const written = fs.writeSync(fd, output, offset, used - offset, null); if (!Number.isInteger(written) || written <= 0) throw new Error('持久化 JSON 写入未取得进展'); offset += written; } used = 0; } function append(text) { const bytes = Buffer.from(text, 'utf8'); for (let offset = 0; offset < bytes.length;) { const count = Math.min(output.length - used, bytes.length - offset); bytes.copy(output, used, offset, offset + count); offset += count; used += count; if (used === output.length) flush(); } } function string(text) { append('"'); for (let start = 0; start < text.length;) { let end = Math.min(text.length, start + 8192); // 不能在分段转义处拆开 UTF-16 代理对,否则会改变实际 JSON 字节。 if (end < text.length && text.charCodeAt(end - 1) >= 0xd800 && text.charCodeAt(end - 1) <= 0xdbff && text.charCodeAt(end) >= 0xdc00 && text.charCodeAt(end) <= 0xdfff) end -= 1; append(JSON.stringify(text.slice(start, end)).slice(1, -1)); start = end; } append('"'); } function write(item, key, arrayItem = false, prepared = false) { if (!prepared) item = jsonValue(item, key); if (item === undefined) { if (arrayItem) { append('null'); return true; } return false; } if (item === null) append('null'); else if (typeof item === 'string') string(item); else if (typeof item !== 'object') append(String(item)); else { if (ancestors.has(item)) throw new TypeError('JSON 不支持循环引用'); ancestors.add(item); try { if (Array.isArray(item)) { append('['); const length = item.length; for (let index = 0; index < length; index += 1) { if (index) append(','); write(item[index], String(index), true); } append(']'); } else { append('{'); let first = true; for (const name of Object.keys(item)) { // 每个字段只求值一次,避免 getter 或 toJSON 重复执行。 const next = jsonValue(item[name], name); if (next === undefined) continue; if (!first) append(','); first = false; string(name); append(':'); write(next, name, false, true); } append('}'); } } finally { ancestors.delete(item); } } return true; } if (!write(value, '')) throw new TypeError('顶层值不可持久化为 JSON'); flush(); } // 合并输入已是规范 JSON 值;直接比较结构,避免为长正文制造序列化副本。 function jsonValueEqual(left, right) { if (left === right) return true; if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') return false; if (Array.isArray(left) !== Array.isArray(right)) return false; if (Array.isArray(left)) return left.length === right.length && left.every((item, index) => jsonValueEqual(item, right[index])); const keys = Object.keys(left); return keys.length === Object.keys(right).length && keys.every(key => Object.prototype.hasOwnProperty.call(right, key) && jsonValueEqual(left[key], right[key])); } module.exports = { createJsonReader, readJsonSync, writeJsonSync, cloneJsonValue, jsonValueEqual };