11 KiB
11 KiB
代码审计报告(阶段 6 后全量只读审计)
审计日期:2026-11-13。方式:三路并行只读审计(编辑器内核 / App 状态与服务层 / Rust+样式)+ 主会话逐条复核关键项。未改任何代码。标注「已复核」的为主会话亲自对代码(及 ProseMirror 源码)验证过的结论;行号为审计时点位置。
A. 逻辑错误——高危(丢数据 / 崩溃 / 功能失效)
- 保存竞态会误标未保存内容并删恢复记录(已复核)—
stores/tabs.ts的savedreducer 写savedContent: t.content(分发时刻的当前正文),而非实际写盘的那份快照。App.tsx saveTab在await写盘期间用户继续输入 → 新输入被标「已保存」、标签变干净、恢复记录被删、后续自动保存跳过、关窗不提示。慢写(大文件)时窗口更宽。可真实丢字。 - 外部修改自动重载被旧编辑器卸载回写覆盖(已复核机制)—
reloadTabFromDisk先 dispatchloadContent再 bump editorEpoch,旧 EditorPane 卸载 cleanup 无条件用旧编辑器内容onMarkdown回写 → 刚载入的磁盘新内容被旧正文盖掉且标脏。自动重载实际上从未生效过,看到的仍是旧内容。 - 外部重载的检查—执行竞态 — 轮询判「不脏」后
await readTextFile期间用户开始输入,loadContent整表覆盖,数个击键被静默吞掉。与第 2 条独立。 - 多窗口
?file=在无恢复记录时永远不执行(已复核)— 启动 effect 在records.length === 0时提前 return(App.tsx:365),读?file=的代码在 return 之后(:377)。即正常退出后的新窗口打开只剩欢迎页——这是最常见的情形,阶段 6 多窗口主路径实际是坏的(此前报告登记为已实现,属误报,一并更正)。 - UI 偏好启动竞态(已复核结构)—
[ui]effect 挂载即以初始值saveUiPref写库,初始化 effect 要先await loadRecent()才读库。桌面端偏好(主题/自动保存间隔/快捷键覆盖等)大概率被默认值覆盖后才读回,表现为设置不持久。 - 全选 / 点击公式块等顶层原子节点必抛 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,该次选区变更失败。 - 查找命中不随编辑失效,替换会打在过期位置(已复核设计)— EditorPane/SourceEditor 的命中表只在改查询词/选项/切标签时重算,文档变化从不失效。开着查找条改文档前部再按替换 → 在偏移后的错误文本上替换,写坏正文。
- 自动保存不避让外部冲突弹窗(已复核)— autosave 只判
path && dirty,ConflictModal 打开期间 30 秒一到先把本地版本写盘,外部修改被覆盖,弹窗「重新加载」选项失效一半。 - 全文搜索跟随符号链接无限递归 —
search.rs用is_dir()(跟随链接)无深度/总量上限;工作区里有指向父目录的 junction(Windows 常见)→ 无限递归栈溢出、进程崩溃;工作区开在用户主目录时同步全量扫描不可取消。 - 所有 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. 性能问题(大文档为主)
- 每次击键的 O(n) 叠加(已复核):状态栏字数/字符数/行数在 render 里对全文现算;大纲
extractOutline以整个 tab 对象为 useMemo 依赖、每击键全文逐行正则;菜单数组同样以 tab 为依赖每击键重建;Sidebar/TabBar/MenuBar 均未 memo,App 整树随每次updateContent重渲染。 - Milkdown listener 的 200ms 全量序列化:打字期间每 200ms 对整篇文档序列化一次并附带全文档深比较(prevDoc.eq),626K 字符单次序列化 jsdom 口径 2.6s——序列化本身是架构必需(正文以 Markdown 字符串为状态源),但频率与深比较在大文档上是持续占用。
- 选区回调每击键全文档扫描:
cursorFromSelection每次选区变化跑一遍doc.descendants数文本块,另加domAtPos + closest('table')。 - 专注模式每击键全量重建装饰集:当前块未变也不复用,数千个
Decoration.node每击键重建 + PM 全文档装饰 diff;CSS 的 opacity transition 又让每批块持续做动画重算。 - 查找逐字输入全量重建索引(无防抖):1MB 文档每敲一个查询字符都重建双数组 + 整串 toLowerCase + 全表扫描;替换时再叠两遍(见 B 条)。
- 图片粘贴走
number[]过 IPC:5MB 截图 = 500 万元素数组经 JSON 序列化传 Rust,内存/耗时放大一个量级,粘贴瞬间冻结可感。 - 恢复写入每 6 秒写全文:持续输入时每个脏标签每 6 秒把整篇正文经 IPC 写 SQLite(有内容比对挡无变化轮次);外部检测 4 秒轮询对多标签串行 await stat,轮次可交叠。
- 搜索不区分大小写时逐行
to_lowercase()全量分配(Rust 侧,小)。
D. 样式问题
高
--panel变量从未定义却被用 3 处(已复核)— 实际变量名是--bg-panel。FindBar 背景、查找按钮、快捷键行按钮的背景全部失效变透明:查找条直接透出文档文字叠在一起。.input基础样式根本不存在(已复核)— 全仓只有一处布局规则(.ws-search-box .input)。FindBar、侧边栏搜索、阶段 4/5 弹窗的输入框全是浏览器原生外观,深色 UI 下白底黑字原生框——正是明确要求避免的样式。- 文档主题变量覆盖不对称,深色 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. 待核实(有嫌疑、无运行证据,需真机或补测)
- Tab 键可能被 Milkdown 自带 indent 插件抢先(优先级最高):
@milkdown/plugin-indent对 Tab 无条件插两空格,其 keymap 与自研表格 Tab/代码块 Tab 的合并顺序未验;现有测试都是直调函数、从未向组装好的编辑器派发真实 Tab 事件。若成立,表格跳格与代码块缩进在真机上是坏的。 - CSP 为 null + 全部文件命令接受任意绝对路径:若某条原始 HTML 渲染路径可执行脚本,则等于任意文件读写删,需专项核实 Milkdown 对原始 HTML 的处理。
- 导出时非法公式可能整篇失败:rehype-katex 未传
throwOnError:false,KaTeX 默认抛错。 - Mermaid 导出 id 按内容 hash,同文档两段相同图表代码 id 重复,产物是否互相污染未验。
- 图片落盘失败(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. 建议修复顺序
- 先修 A1–A8(丢数据/失效类,改动都不大:saved 带快照、重载回写闸门、?file 位置、偏好读写顺序、depth=0 守卫、命中失效钩子、autosave 避让冲突)。
- 样式 D1–D3(一处变量名、一份 .input 基础样式、两套主题补 --doc-ink,半小时级)。
- 性能 C1(状态栏/大纲/菜单改「内容变化才重算」+ 关键组件 memo)、C4/C5(专注装饰复用、查找防抖与去重计算)。
- E1 补一条真实派发 Tab 的集成测试定性,再决定是否调整插件顺序。