1717 * then rebuilds the FTS index.
1818 */
1919import { dialog } from 'electron' ;
20- import { writeFile , readFile } from 'fs/promises' ;
20+ import { writeFile , readFile , stat } from 'fs/promises' ;
2121import { safeHandle } from './ipcUtils.js' ;
2222import { logger } from './logger.js' ;
2323import { getConnection } from './db/sqliteService.js' ;
@@ -28,13 +28,16 @@ import {
2828 buildWorldExport ,
2929 validateWorldImport ,
3030 WORLD_TABLE_WHITELIST ,
31+ WORLD_EXPORT_MAX_ROWS ,
3132 type WorldTableBundle ,
3233} from '../src/features/sovereignty/lib/worldExport.js' ;
3334
3435/** 快照单行 id(与 useWorldSnapshotSync 的 SNAPSHOT_ID 对齐) / Snapshot row id */
3536const SNAPSHOT_ID = 'latest' ;
3637/** 入籍记录导出上限(叙述层防超大响应;恢复层 tables.imports 不受限) */
3738const MAX_RECORDS = 5000 ;
39+ /** 恢复文件大小防御上限(防超大文件读入内存拖垮 JSON.parse) / Max import file bytes */
40+ const MAX_IMPORT_BYTES = 256 * 1024 * 1024 ;
3841
3942/** 恢复层表白名单(父表在前,满足外键约束;与 worldExport 白名单同源) */
4043const EXPORT_TABLES = WORLD_TABLE_WHITELIST ;
@@ -94,8 +97,13 @@ export function registerSovereigntyHandlers(): void {
9497 await writeFile ( result . filePath , JSON . stringify ( bundle , null , 2 ) , 'utf-8' ) ;
9598 const rowCount = tables . reduce ( ( s , t ) => s + t . rows . length , 0 ) ;
9699 const nodeCount = Array . isArray ( bundle . graph . nodes ) ? bundle . graph . nodes . length : 0 ;
100+ // 对称性提示:导出不受限(备份语义),但超过恢复上限时提醒用户
101+ const overLimit = rowCount > WORLD_EXPORT_MAX_ROWS ;
102+ if ( overLimit ) {
103+ logger . warn ( `[Sovereignty] Export exceeds restore limit (${ rowCount } > ${ WORLD_EXPORT_MAX_ROWS } rows); the file will not be restorable` ) ;
104+ }
97105 logger . info ( `[Sovereignty] World exported to ${ result . filePath } (${ rowCount } rows, ${ nodeCount } nodes)` ) ;
98- return { success : true , canceled : false , path : result . filePath } ;
106+ return { success : true , canceled : false , path : result . filePath , overLimit } ;
99107 } catch ( err ) {
100108 const msg = err instanceof Error ? err . message : String ( err ) ;
101109 logger . error ( '[Sovereignty] Export failed' , err ) ;
@@ -121,7 +129,14 @@ export function registerSovereigntyHandlers(): void {
121129 }
122130
123131 try {
124- const text = await readFile ( result . filePaths [ 0 ] , 'utf-8' ) ;
132+ // 大小防御:超大文件先拒绝,避免全量读入内存 + JSON.parse 内存峰值
133+ const fileStat = await stat ( result . filePaths [ 0 ] ) ;
134+ if ( fileStat . size > MAX_IMPORT_BYTES ) {
135+ return { success : false , error : '文件过大(超过 256MB),无法恢复' } ;
136+ }
137+ const rawText = await readFile ( result . filePaths [ 0 ] , 'utf-8' ) ;
138+ // 兼容 Windows 记事本保存的 UTF-8 BOM
139+ const text = rawText . charCodeAt ( 0 ) === 0xfeff ? rawText . slice ( 1 ) : rawText ;
125140 let parsed : unknown ;
126141 try {
127142 parsed = JSON . parse ( text ) ;
0 commit comments