76 lines
11 KiB
Markdown
76 lines
11 KiB
Markdown
# 代码审计报告(阶段 6 后全量只读审计)
|
||
|
||
审计日期:2026-11-13。方式:三路并行只读审计(编辑器内核 / App 状态与服务层 / Rust+样式)+ 主会话逐条复核关键项。未改任何代码。标注「已复核」的为主会话亲自对代码(及 ProseMirror 源码)验证过的结论;行号为审计时点位置。
|
||
|
||
## A. 逻辑错误——高危(丢数据 / 崩溃 / 功能失效)
|
||
|
||
1. **保存竞态会误标未保存内容并删恢复记录**(已复核)— `stores/tabs.ts` 的 `saved` reducer 写 `savedContent: t.content`(分发时刻的当前正文),而非实际写盘的那份快照。`App.tsx saveTab` 在 `await` 写盘期间用户继续输入 → 新输入被标「已保存」、标签变干净、恢复记录被删、后续自动保存跳过、关窗不提示。慢写(大文件)时窗口更宽。**可真实丢字。**
|
||
2. **外部修改自动重载被旧编辑器卸载回写覆盖**(已复核机制)— `reloadTabFromDisk` 先 dispatch `loadContent` 再 bump editorEpoch,旧 EditorPane 卸载 cleanup 无条件用旧编辑器内容 `onMarkdown` 回写 → 刚载入的磁盘新内容被旧正文盖掉且标脏。自动重载实际上从未生效过,看到的仍是旧内容。
|
||
3. **外部重载的检查—执行竞态** — 轮询判「不脏」后 `await readTextFile` 期间用户开始输入,`loadContent` 整表覆盖,数个击键被静默吞掉。与第 2 条独立。
|
||
4. **多窗口 `?file=` 在无恢复记录时永远不执行**(已复核)— 启动 effect 在 `records.length === 0` 时提前 return(App.tsx:365),读 `?file=` 的代码在 return 之后(:377)。即正常退出后的新窗口打开只剩欢迎页——这是最常见的情形,阶段 6 多窗口主路径实际是坏的(此前报告登记为已实现,属误报,一并更正)。
|
||
5. **UI 偏好启动竞态**(已复核结构)— `[ui]` effect 挂载即以初始值 `saveUiPref` 写库,初始化 effect 要先 `await loadRecent()` 才读库。桌面端偏好(主题/自动保存间隔/快捷键覆盖等)大概率被默认值覆盖后才读回,表现为设置不持久。
|
||
6. **全选 / 点击公式块等顶层原子节点必抛 RangeError**(已复核,含 PM 源码)— `editor.ts cursorFromSelection` 对 `$from.before($from.depth)`,depth=0(AllSelection、NodeSelection)时 PM 直接抛 `There is no position before the top-level node`,异常从 listener 同步回调冒出 dispatch,该次选区变更失败。
|
||
7. **查找命中不随编辑失效,替换会打在过期位置**(已复核设计)— EditorPane/SourceEditor 的命中表只在改查询词/选项/切标签时重算,文档变化从不失效。开着查找条改文档前部再按替换 → 在偏移后的错误文本上替换,**写坏正文**。
|
||
8. **自动保存不避让外部冲突弹窗**(已复核)— autosave 只判 `path && dirty`,ConflictModal 打开期间 30 秒一到先把本地版本写盘,外部修改被覆盖,弹窗「重新加载」选项失效一半。
|
||
9. **全文搜索跟随符号链接无限递归** — `search.rs` 用 `is_dir()`(跟随链接)无深度/总量上限;工作区里有指向父目录的 junction(Windows 常见)→ 无限递归栈溢出、进程崩溃;工作区开在用户主目录时同步全量扫描不可取消。
|
||
10. **所有 Rust 命令同步执行 + 整文件无上限读入** — 命令均为非 async 函数(Tauri 语义下跑主线程);`read_text_file`/`read_file_base64` 全量 `fs::read`、base64 再放大 4/3 经 IPC。误开数百 MB 文本 → 界面冻结数秒至 OOM;全文搜索同理冻结主线程。
|
||
|
||
## B. 逻辑错误——中危(节选)
|
||
|
||
- 替换被 App 与 EditorPane 两层各重查一遍全量索引且序号口径不一致,最终停留命中会错位一个(App.tsx onReplace + EditorPane.replaceCurrent 内部重查)。
|
||
- 关窗时未命名草稿的恢复写入是 fire-and-forget(`void saveRecovery` 后直接放行关窗),IPC 未落盘进程已退出,草稿可能丢。
|
||
- 另存为后旧恢复键(`untitled:<id>`/旧路径)残留,下次启动误报「未保存草稿」(重命名流程处理了、保存流程漏了)。
|
||
- 全局快捷键不感知弹窗打开:PromptModal 输入框里按 Ctrl+Shift+Q 会给背后编辑器插引用块;LinkModal 里 Ctrl+W 会关掉背后标签。
|
||
- 打印在源码模式下靠固定 60ms 等编辑器重建,大文档必赛跑,打印内容不可控。
|
||
- 临时文件名只有进程号成分(`.tmp-{pid}`),同进程并发保存同一文件(自动保存 × 手动保存)共用同一 tmp 互相踩。
|
||
- 删除到回收站失败时对文件静默退回**永久删除**(阶段 4 的设计决定,但用户无感知——网络盘/回收站禁用时以为可恢复、实际不可)。
|
||
- 编码:非 UTF-8 一律猜 GBK(Big5/Shift_JIS/UTF-16 无 BOM 必乱码);带 BOM 的 UTF-8 用 lossy 解码吞坏字节,保存后 <20> 被永久写入。工作区搜索对 GBK 文档用 lossy 解码,中文永远搜不到。
|
||
- assets 重名去重是先查后写 TOCTOU,两窗口同粘同名图可互相覆盖;循环兜底还会返回已存在路径被 `fs::write` 截断覆盖。
|
||
- 快捷键可录入「无修饰键」组合但分发层先拦截(无 ctrl/alt 直接 return),绑了永不触发且无提示;Ctrl+Shift+= 归一化与 zoomIn 的 ctrl+= 对不上。
|
||
- 源码模式切 UI 主题整组件重挂,光标回退到进入时的旧偏移且 CM 撤销史清空。
|
||
- 导出图片解析:扩展名/MIME 推导对带查询串、大写扩展名、%20 路径失效,内嵌缺图且静默无提示。
|
||
- 整词匹配在正则模式下静默失效(两处实现一致地跳过,用户无提示)。
|
||
|
||
## C. 性能问题(大文档为主)
|
||
|
||
1. **每次击键的 O(n) 叠加**(已复核):状态栏字数/字符数/行数在 render 里对全文现算;大纲 `extractOutline` 以整个 tab 对象为 useMemo 依赖、每击键全文逐行正则;菜单数组同样以 tab 为依赖每击键重建;Sidebar/TabBar/MenuBar 均未 memo,App 整树随每次 `updateContent` 重渲染。
|
||
2. **Milkdown listener 的 200ms 全量序列化**:打字期间每 200ms 对整篇文档序列化一次并附带全文档深比较(prevDoc.eq),626K 字符单次序列化 jsdom 口径 2.6s——序列化本身是架构必需(正文以 Markdown 字符串为状态源),但频率与深比较在大文档上是持续占用。
|
||
3. **选区回调每击键全文档扫描**:`cursorFromSelection` 每次选区变化跑一遍 `doc.descendants` 数文本块,另加 `domAtPos + closest('table')`。
|
||
4. **专注模式每击键全量重建装饰集**:当前块未变也不复用,数千个 `Decoration.node` 每击键重建 + PM 全文档装饰 diff;CSS 的 opacity transition 又让每批块持续做动画重算。
|
||
5. **查找逐字输入全量重建索引**(无防抖):1MB 文档每敲一个查询字符都重建双数组 + 整串 toLowerCase + 全表扫描;替换时再叠两遍(见 B 条)。
|
||
6. **图片粘贴走 `number[]` 过 IPC**:5MB 截图 = 500 万元素数组经 JSON 序列化传 Rust,内存/耗时放大一个量级,粘贴瞬间冻结可感。
|
||
7. **恢复写入每 6 秒写全文**:持续输入时每个脏标签每 6 秒把整篇正文经 IPC 写 SQLite(有内容比对挡无变化轮次);外部检测 4 秒轮询对多标签串行 await stat,轮次可交叠。
|
||
8. 搜索不区分大小写时逐行 `to_lowercase()` 全量分配(Rust 侧,小)。
|
||
|
||
## D. 样式问题
|
||
|
||
**高**
|
||
1. **`--panel` 变量从未定义却被用 3 处**(已复核)— 实际变量名是 `--bg-panel`。FindBar 背景、查找按钮、快捷键行按钮的背景全部失效变透明:查找条直接透出文档文字叠在一起。
|
||
2. **`.input` 基础样式根本不存在**(已复核)— 全仓只有一处布局规则(`.ws-search-box .input`)。FindBar、侧边栏搜索、阶段 4/5 弹窗的输入框全是浏览器原生外观,深色 UI 下白底黑字原生框——正是明确要求避免的样式。
|
||
3. **文档主题变量覆盖不对称,深色 UI 下不可读**(已复核)— notion 只设 `--doc-paper:#fbfbfa` 不设 `--doc-ink`(回落为 UI 的 `--text`),深色 UI(--text 近白)+ Notion 主题 = 近白字压近白纸;github 同理且代码块底色/标题边框写死浅色。dark-eye/paper 是成对设的、没问题。
|
||
|
||
**中**
|
||
4. 打印隐藏清单漏了 `.findbar` 与 Mermaid 的「预览/源码」按钮——查找条开着打印会印进 PDF。
|
||
5. 源码模式明暗只跟 UI 主题、不跟文档主题:浅色 UI + 深色护眼文档主题时源码是深纸面配深色字,低对比。
|
||
6. CSS 重复/冲突定义靠书写顺序兜底(`.findbar`/`.editor-scroll`/`.modal-note` 各两处,字号还不一致),维护雷区;全应用无滚动条与选区样式,深色下默认浅色粗滚动条割裂;Ctrl+= 缩放只作用渲染模式、源码模式字号写死 14px 不跟随;FindBar `min-width:380px` 无窄宽兜底;菜单下拉无右边缘防溢出(暂未触发)。
|
||
7. `themes.ts` 的 `darkPaper` 字段定义了但全仓无人消费,是误导性死代码。
|
||
|
||
## E. 待核实(有嫌疑、无运行证据,需真机或补测)
|
||
|
||
1. **Tab 键可能被 Milkdown 自带 indent 插件抢先**(优先级最高):`@milkdown/plugin-indent` 对 Tab 无条件插两空格,其 keymap 与自研表格 Tab/代码块 Tab 的合并顺序未验;现有测试都是直调函数、从未向组装好的编辑器派发真实 Tab 事件。若成立,表格跳格与代码块缩进在真机上是坏的。
|
||
2. CSP 为 null + 全部文件命令接受任意绝对路径:若某条原始 HTML 渲染路径可执行脚本,则等于任意文件读写删,需专项核实 Milkdown 对原始 HTML 的处理。
|
||
3. 导出时非法公式可能整篇失败:rehype-katex 未传 `throwOnError:false`,KaTeX 默认抛错。
|
||
4. Mermaid 导出 id 按内容 hash,同文档两段相同图表代码 id 重复,产物是否互相污染未验。
|
||
5. 图片落盘失败(onImageFile reject)时 upload 插件的占位节点是否残留未验;双进程同开一个 SQLite(双击图标二次启动)无 WAL/busy_timeout,写恢复记录时可能 SQLITE_BUSY 上抛(同进程多窗口共用 State 连接、无此问题)。
|
||
|
||
## F. 确认没问题的部分
|
||
|
||
- Markdown 双向往返保真与全量回归(61 例)覆盖扎实;原子写有临时文件 + sync_all + rename;CRLF 由前端 `toDiskContent` 写回时保持(Rust 层无换行参数的疑虑已排除);语法显隐插件的 composition 监听在 destroy 中成对移除;list_item/表格等 NodeView 无外部监听泄漏;恢复记录的磁盘比对划分(planRecovery)逻辑有单测且正确;快捷键注册表本身无默认冲突。
|
||
|
||
## G. 建议修复顺序
|
||
|
||
1. 先修 A1–A8(丢数据/失效类,改动都不大:saved 带快照、重载回写闸门、?file 位置、偏好读写顺序、depth=0 守卫、命中失效钩子、autosave 避让冲突)。
|
||
2. 样式 D1–D3(一处变量名、一份 .input 基础样式、两套主题补 --doc-ink,半小时级)。
|
||
3. 性能 C1(状态栏/大纲/菜单改「内容变化才重算」+ 关键组件 memo)、C4/C5(专注装饰复用、查找防抖与去重计算)。
|
||
4. E1 补一条真实派发 Tab 的集成测试定性,再决定是否调整插件顺序。
|