Files
cc-web/lib/json-persistence.js

306 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 };