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:
185
docs/29-全项目审计.md
Normal file
185
docs/29-全项目审计.md
Normal 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 文档后立刻连开多个标签挤掉其窗格,再回看该文档
|
||||
是否完整」一验。
|
||||
Reference in New Issue
Block a user