import type { JsonObject } from '../types/json.js' import { createHttpError } from './http.js' const LARGE_INTEGER_PATTERN = /(:\s*)(-?\d{16,})(\s*[,}\]])/g type ParseJsonObjectOptions = { preserveLargeIntegers?: boolean throwOnError?: boolean } export function parseJsonObject( rawText: unknown, { preserveLargeIntegers = false, throwOnError = false }: ParseJsonObjectOptions = {}, ): JsonObject { const normalized = String(rawText || '').trim() if (!normalized) { return {} } try { const prepared = preserveLargeIntegers ? wrapLargeIntegerLiterals(normalized) : normalized const parsed = JSON.parse(prepared) return isPlainObject(parsed) ? parsed : {} } catch (error) { if (!throwOnError) { return {} } throw createHttpError('json 参数不是合法 JSON', { statusCode: 400, errorCode: 'invalid_json_payload', cause: error, }) } } export function wrapLargeIntegerLiterals(rawText: unknown): string { return String(rawText || '').replace(LARGE_INTEGER_PATTERN, (_match, prefix, digits, suffix) => { if (!shouldPreserveIntegerToken(digits)) { return `${prefix}${digits}${suffix}` } return `${prefix}"${digits}"${suffix}` }) } function shouldPreserveIntegerToken(rawDigits: unknown): boolean { const digits = String(rawDigits || '').trim() const unsigned = digits.startsWith('-') ? digits.slice(1) : digits if (!/^\d+$/.test(unsigned)) { return false } if (unsigned.length > 16) { return true } if (unsigned.length < 16) { return false } return unsigned > '9007199254740991' } export function sanitizeWellFormedUnicode(value: unknown): string { if (value === null || value === undefined) return '' const str = String(value) if (!str) return '' // 1. Remove null bytes (\u0000) which PostgreSQL text/jsonb columns strictly forbid const withoutNull = str.replace(/\0/g, '') // 2. Convert lone surrogates into replacement character U+FFFD const toWellFormedFn = (withoutNull as { toWellFormed?: () => string }).toWellFormed return typeof toWellFormedFn === 'function' ? toWellFormedFn.call(withoutNull) : withoutNull.replace( /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?(value: T): T { if (value === null || value === undefined) { return value } if (typeof value === 'string') { return sanitizeWellFormedUnicode(value) as unknown as T } if (Array.isArray(value)) { return value.map((item) => deepSanitizeUnicode(item)) as unknown as T } if (isPlainObject(value)) { const res: Record = {} for (const [k, v] of Object.entries(value)) { const cleanKey = sanitizeWellFormedUnicode(k) res[cleanKey] = deepSanitizeUnicode(v) } return res as unknown as T } return value } export function safeJsonStringify(value: unknown, fallback = '{}'): string { if (value === undefined || value === null) { return fallback } if (typeof value === 'string') { const trimmed = value.trim() if (!trimmed) return fallback try { const parsed = JSON.parse(trimmed) const sanitized = deepSanitizeUnicode(parsed) return JSON.stringify(sanitized) } catch { return JSON.stringify(sanitizeWellFormedUnicode(value)) } } try { const sanitized = deepSanitizeUnicode(value) return JSON.stringify(sanitized) } catch { return fallback } } function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' }