Files
new-markdown/docs/11-code-audit.md

76 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 代码审计报告(阶段 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 的集成测试定性,再决定是否调整插件顺序。