audit(docs/29): 全项目审计——修分片载入卸载截断高危、Sidebar memo 击穿与 Rust 资源/搜索/恢复上限等 20 余处

三线并行审计(编辑器核心/Rust 后端/服务与组件)+ App 编排层自审。
高危:分片载入未收尾时窗格卸载回写部分序列化,干净标签会被静默
截断——cleanup 补 muted 守卫,create 完成回调补取消守卫。
性能:gitSync/outline/收起回调稳定化保住 Sidebar memo;光标行列
rAF 合并;图片显示解析加缓存;导出图片并发解析;chunkLoader
偏移前缀表;围栏切点只认顶格(列表内嵌围栏等价回归实测)。
Rust:资源命令 async 化 + 32MB 上限;搜索 junction 越界校验;
recovery/workspace 条数与单条上限;启动坏库改名重建 + WAL;
validate 补 ext:: 黑名单与 ref 形态;init 先种 .gitignore
(含 .*.tmp-* 防崩溃残留被提交);游离 HEAD 不再猜分支。
验证:vitest 122+2、tsc、构建通过;Rust 过 rustfmt 解析、
validate 纯函数抽出实跑断言、命令对账 28=28。
This commit is contained in:
Muse
2026-11-14 18:00:31 +08:00
parent 527ca5ecc5
commit fb2315a2f3
28 changed files with 657 additions and 140 deletions

185
docs/29-全项目审计.md Normal file
View File

@@ -0,0 +1,185 @@
# docs/29 — 全项目审计(性能 + 逻辑,三线并行 + App 编排层自审)
时间:2026-11-14 晚。范围:src/editor/ 全部、EditorPane/SourceEditor、
src/services/、src/components/、src/hooks/、src/utils/、App.tsx 编排层、
src-tauri/src/ 全部(1289 行)。先说结论:发现 **1 个高危数据问题**
(已修)、中低问题一批(已修 20 余处),其余记录待办;三线均未再
发现新的高危。搜索结果上限(每文件 20 条/共 300 文件)顺带核实存在。
## 已修复
### 高危
- **H1 分片载入未完成时窗格卸载,部分文档被静默回写并截断**:
EditorPane 卸载 cleanup 无条件回写最终序列化,而实时回写路径有
muted(载入中)守卫、唯独卸载路径漏了。载入中被保活淘汰(打开
第 7 个标签)或关标签时,回写的是已追加的部分文本;
syncEditorContent 对干净标签会把 savedContent 一起对齐到部分
文本——不标脏、无提示,下一次保存即把截断内容写入磁盘。修复:
cleanup 在 muted 时跳过回写(状态里本就是磁盘完整原文);载入
失败路径已主动解除 muted 并如实回写,不受守卫影响。同处顺带修:
`editor.create()` 完成回调无取消守卫,卸载后仍会把已销毁编辑器
的 API 注册回 App → 补 cancelled 检查并销毁。回归测试见
tests/editor-pane-unload.test.tsx(对照组);中途卸载的交错在
jsdom 里无法自动化复现(React act 会抽干异步追加,三种卸载方式
实测均如此),该守卫靠代码复核保证:muted 仅在追加完成/失败的
收尾回调里解除。
### 性能(中)
- **P1 Sidebar 的 memo 被三个不稳定 props 击穿,击键时整棵文件树
重渲染**:gitSync 内联对象、onCollapseSidebar 内联回调、随正文
变化重建的 outline 数组。三处稳定化:gitSyncUi 用 useMemo、
collapseSidebar 用 useCallback、非大纲模式传模块级空数组常量。
两条审计线独立撞到同一问题,属实。
- **P2 光标行列每次选区变化从文档头全量遍历**:Milkdown
selectionUpdated 每事务同步分发,600K 文档光标在文末时单次上万
节点。改为 rAF 合并:闭包缓存最新一次计算、逐帧最多算一次
(行列与表格上下文同帧合并)。
- **P3 图片显示解析无缓存**:同一图片出现 N 次/切标签重挂/epoch
重建都重复读盘转 base64。App 层加(文档路径|src)→Promise 缓存,
上限 60 条 FIFO。
- **P4 表格工具栏滚动监听无节流**:每次 scroll 都量 DOM + setState。
改 rAF 合并 + 位置未变不 setState。打字机模式逐键重启平滑滚动
改 behavior:'auto' 防抖动。
- **P5 分片切分的行偏移 O(切点数×行数)**:改行偏移前缀表 O(1) 查表。
- **P6 导出 embed 图片串行逐张解析**:改 Promise.all 并发;
exportHtml 高亮器初始化失败后 rejected Promise 被永久缓存、
整会话高亮失效 → 失败时清缓存,下次导出重试。
- **P7(Rust)list_dir 排序比较器内每次比较分配两个小写字符串**:
改 sort_by_cached_key 预计算键。
### 逻辑/健壮性(中)
- **L1(Rust)资源复制/保存命令是同步命令且无大小上限**:
copy_to_assets 对任意文件整读(冻 UI 线程 + 内存整载),
save_bytes_to_assets 同样无上限。两命令改 async + spawn_blocking,
统一 32MB 上限(与 read_file_base64 对齐)。写入侧补 64MB 上限,
与读侧对称。
- **L2(Rust)工作区搜索跟随 junction/符号链接逃出工作区**:去重
只防环,指向外部的 junction 是「新目录」会被整扫、耗尽配额并
越界读文件。scan_dir 加 canonical 包含校验,越界即跳过。顺带:
元数据取不到时由 fail-open 改为跳过;隐藏文件与隐藏目录同口径
跳过。
- **L3(Rust)recovery 草稿表无任何上限**:单条可达数十 MB、条数
无限增长、启动时全量正文过 IPC。recovery_save 加单条 10MB 上限
(超限静默跳过,草稿是副本、.md 是正本,不报错刷屏)+ 只保留
最近 20 条;workspaces 表同样只留最近 20 条。
- **L4(Rust)启动期数据库失败直接 panic 且循环复现**:DB 损坏/
磁盘异常时用户连界面都进不去。改为:打开/建表失败时把坏库
(含 -wal/-shm)改名 `.broken-<时间戳>` 备份后新建空库继续
启动;正文在磁盘 .md 中不受影响。连接另设 WAL + busy_timeout
3000(多开写操作排队而非直接 SQLITE_BUSY)。
- **L5 路径比较未做大小写归一(Windows 语义)**:pathsEqual/
isPathInside 大小写敏感,盘符/目录大小写不一致时 DnD 守卫漏判、
活动文件高亮消失、恢复键/Git 设置键/最近列表键同根裂键。新增
pathCompareKey(Windows 风格路径折叠小写,POSIX 保持敏感),
pathsEqual/isPathInside/文件树高亮/recoveryKey/Git 设置键与
租约键/legacy recent 去重共用。注意:升级后旧恢复记录键与新键
不一致,旧草稿可能多提示一次、随后被 20 条修剪清掉,无数据
损失。顺带修 parentDirOf('C:/a.md') 返回盘符相对路径 'C:' →
返回 'C:/'。
- **L6 文件树与标签页右键菜单无视口夹取**:靠边弹出溢出屏幕、尾
部条目够不到(编辑区菜单已有夹取,这两处漏了)。抽共享
clampMenuPosition(utils/menuPos.ts),三处菜单共用。
- **L7 逻辑小项打包**:①工作区搜索无乱序保护,慢的旧请求会覆盖
新结果 → 序号守卫;②换工作区时文件树短暂显示旧目录(expanded
按绝对路径残留)→ FileTree 按 root 加 key 重挂;③TabBar 拖拽
dragover 以浏览器节律重复 dispatch 同一移动 → (dragId:下标)
去重;④Dropdown 给 useDismiss 传内联 onClose,打开期间每次渲染
重注册 5 个全局监听 → useCallback 稳定;⑤Git 租约 check-then-
write 双窗口同启可同时取得 → 写入后回读确认持有者;⑥
windowClaims 注册表坏条目(keys 非数组)可让启动扫描抛错 →
prune 时整条丢弃坏条目;⑦store 的 UI 偏好 Web 模式只写不读 →
loadUiPref Web 分支读 localStorage;⑧多行粘贴尾随换行多建一个
空段落 → split 后丢弃末尾空项。
### 本轮新功能面(Git 同步)复审补强
- validate_branch 补:尾随 `.`、开头 `/`、连续 `//`、`@{`;
validate_remote_url 补 ext::/fd:: 可执行 transport 黑名单
(fetch 时会执行命令,用户自填场景威胁弱,属防患)。两函数已
抽出在沙箱实跑断言(合法/非法各一组,全过)。
- git_init_repo:.gitignore 播种提前到绑远程之前,部分失败不再
留「有仓库无忽略规则」的半成品;已有 origin 改 set-url 不再
报错。默认 .gitignore 补 `.*.tmp-*`:原子写临时文件在崩溃
残留时原本会被 `git add -A` 提交推送。
- git_sync_now 游离 HEAD 时不再猜分支名 "main"(会把成果推错
地方),直接报错提示先切回分支。
### 一致性
- FindBar 与表格工具栏的原生 title 提示迁到自绘 Tooltip(跟随
主题,与大王既定偏好对齐;路径类完整信息 title 保留);FindBar
开关按钮补 aria-pressed。
- GitSyncModal 补 Esc/点背幕关闭(忙碌时禁用),与 Stage5 弹窗
范式对齐。
- extractOutline 围栏判定改按字符+长度跟踪:四反引号围栏内的
三反引号行不再误翻转状态(与分片切分的围栏跟踪口径一致)。
### 已实测澄清(非问题)
- **分片切分会把「列表内缩进围栏」误判为顶层切点**(审计推导项):
已构造 >60K 含列表内嵌围栏的文档实测——旧逻辑下分片序列化与
整篇**确实不等**,属实。修复:围栏作切点时只认顶格;回归测试
已加入 tests/chunk-load.test.ts(关掉修复即失败,判别有效)。
- **替换文本含换行符**:实测 replaceAll('line1\nline2') 序列化为
段落内软换行,不拆段、不丢内容。语义与「粘贴多行建多段」不
同,属可接受差异,维持现状。
## 记录待办(本轮未改,附理由)
- D1(中)非 UTF-8 文件首次保存被静默转码为 UTF-8(GBK/UTF-16
文档存一次即变 UTF-8):编辑器内部统一 UTF-8 是既定设计,但
落盘编码变更应至少首次提示一次。需前端交互拍板,未擅自加。
- D2(中)专注模式跨块时全量重建装饰集:大文档每次回车重建与
顶层块数等量的装饰对象。改增量方案需重构插件,单独立项。
- D3(低)源码模式每次击键物化全文回写 App(无渲染模式的 200ms
防抖等价物):加防抖要先理清源码模式下保存读的是哪份状态,
贸然加会存旧文本,单独立项处理。
- D4(低)查找索引每次 findAll 全量重建(600K 约 5MB 瞬时):
可按 doc 引用缓存,未改。
- D5(低)专注模式开关是模块级全局变量,多窗格/隐藏解析编辑器
共享:目前靠各窗格写同一 App 值维持一致,「按窗格模式」时
需改为注入式。
- D6(低)文件树刷新时全部展开层级同时重拉(IPC burst)、拖拽
悬停整树重渲染:TreeLevel 未 memo 化,属结构性优化,单独立项。
- D7(低,Rust)git_status 串行拉起 6–7 个 git 子进程(每次约
百毫秒级):可改 `git status --porcelain=v2 --branch` 一次拿
全,解析重写风险大于当前收益,未改。
- D8(低,Rust)rename_path:仅大小写不同的改名被 exists 预检
误判冲突;跨卷移动直接报错而非复制+删除。前端入口目前多在
同盘同树内,影响有限,未改。
- D9(低,Rust)schema 无版本号/迁移机制:今后给表加列时
CREATE TABLE IF NOT EXISTS 不会迁移老库(配合 L4 的坏库重建,
最坏情况是偏好重置、正文无损)。加列前须先落 user_version。
- 沿袭:docs/28 的 B1–B4(空仓变基、git 子进程无超时、设置与
同步交错、SSH 主机密钥)与 C1(CSP=null)状态未变。
## 复核通过(无问题,不展开)
- Rust:原子写主链(同目录临时文件 + sync_all + rename,失败
清理)、编码探测顺序与 BOM 处理、sanitize_file_name 对
Windows 非法字符/保留名的处理、SQL 全部参数化、搜索四重
上限、git 同步链错误路径(变基失败先 abort 再报错)。
- 前端:useDismiss/MenuBar 监听清理完整;windowClaims 心跳
10s/过期 45s 与 Git 租约 10s 续租/25s TTL 配比合理;
exportHtml 代码块替换的降序 splice 与路径重写逻辑无误;
SourceEditor replaceAll 倒序 changes 数组经 CM6 实测正确。
- App 编排层:外部修改检测基线在打开/保存/重载各点维护完整,
不会误报自身写入;本轮另给 4 秒轮询加了防重叠(上轮未完跳过
本轮)。自动保存与手动保存并发同标签经原子写 + 幂等成立,
安全。菜单定义随击键重建属小开销,可接受。
## 验证边界
- vitest 122+2(新增:EditorPane 卸载对照、列表内嵌围栏等价、
路径/大纲新例、recoveryKey 新契约),tsc 与生产构建通过。
- Rust:沙箱仍无法跑完整 Tauri 构建(缺 GTK/WebKit 系统库);
四个改动文件已过 rustfmt 语法解析,validate_branch/
validate_remote_url 已抽出实跑断言全过;命令数与注册数对账
28=28 不变(两个命令改 async 不影响注册)。async 化、WAL、
坏库重建、搜索包含校验的实效待 Windows 构建与真机确认。
- H1 的守卫交错无法在 jsdom 自动化复现(见上),真机侧可用
「打开 >60K 文档后立刻连开多个标签挤掉其窗格,再回看该文档
是否完整」一验。