Skip to content

[代码审查] 代码质量 + 死代码/冗余分析(40+ 项发现) #7

Description

@DefectingCat

背景

对全仓库(~44k LOC,184 个 Rust 文件)做的深度代码审查,覆盖两条主线:①代码质量(功能 bug / 安全 / 性能 / 一致性)与 ②死代码/冗余(整模块死代码、系统性重复、死分支)。

方法:编译器权威 dead-code 扫描(--force-warn dead_code)+ 4 路并行深度审查(API / MCP+Infra / 前端 / 跨模块去重),关键发现均已回源码核对。仅记录编译器无法发现或漏报的项;不包含 Dioxus #[server] 函数的 unreachable_pub 噪音(HTTP 调用,编译器看不到调用图)。

统计:严重 5 / 中等 12 / 死代码 12 / 系统性重复 6 / 轻微与文档漂移 ~20 / 重复常量 10+。下方 checkbox 可逐项勾选追踪。


一、🔴 严重(CRITICAL)— 功能 Bug / 安全 / 进程级风险

  • C1 · katex \pu 渲染从未生效(off-by-one)src/api/katex.rs:148
    expand_chem\pu 是 3 字节(\,p,u),{i+3(与 \ce 相同),但代码写 Some(i+4)。结果 \pu{9.8 m/s^2} 永远匹配不上,mhchem::pu() 从不触发,所有 \pu 物理单位公式渲染为红色废字。自 mhchem 初次提交(bcd1395)起存在。
    现有测试 mhchem_pu_units_renders空测试:它只断言 !contains("katex-error"),而 katex-rs 对未知命令走 color node(无 katex-error class),无论转译与否都通过。
    修复Some(i+3);测试改为断言转译产物(含 mathrm 等)。

  • C2 · katex expand_chem 破坏多字节 UTF-8src/api/katex.rs:172
    out.push(bytes[i] as char) 按单字节 Latin-1 转 char。任何含 \ce/\pu 且含非 ASCII(如中文 \text{浓度})的公式会被破坏成乱码。
    修复:用 char_indices / 对 gap 用 push_str

  • C3 · 代码执行器 SSE 流可无限挂起 → 跑全部并发槽 DoSsrc/infra/docker.rs:551-558
    run_in_container_streamlog_reader 先结束分支(输出超限时触发):注释写「短超时,避免无限等」,但 docker.wait_container().next().await 无超时、无 killwhile True: print() 式程序会永久挂起 → (a) RUNNER_SEMAPHORE 许可不释放(默认 4 并发 → 4 个即 DoS 全部代码执行器),(b) ContainerGuard 不 drop → 容器永久运行烧 CPU。该端点对非 admin 用户可达(仅限流)。
    修复:在该分支用 timeout(limits.timeout_secs, wait_container),超时则 kill_container

  • C4 · Dioxus 渲染纯度违规src/components/code_runner/runner.rs:68-71
    组件 body 里 source_prop_signal.set(source.clone()) —— 项目 dioxus-render-purity skill 明令禁止的「prop 镜像进 signal + 渲染期 set」反模式,会触发额外一次渲染。Dioxus 0.7 的 prop 本身是响应式的,下方同步 use_effect(L~207)可直接读 source prop。
    修复:删掉 source_prop_signal,让同步 effect 直接捕获 source

  • C5 · 图片缓存清理 panic 致进程崩溃src/tasks/image_cache_cleanup.rs:57
    now - max_ageIMAGE_DISK_CACHE_MAX_AGE_HOURS 设极大值时 SystemTime 下溢 panic。release panic="abort" → 整个进程崩溃;max_age_hours * SECS_PER_HOUR 也可能溢出。
    修复checked_sub + min(sane_cap)unwrap_or(SystemTime::UNIX_EPOCH)


二、🟠 中等(MODERATE)— 性能 / 一致性 / 数据正确性

  • M1 · MCP trash/delete 缺 author 归属校验src/mcp/tools/posts.rs:~437,~499
    trash_post/delete_postWHERE id = $1 …,无 author_id 过滤;而同文件 update_post/publish_post 都有 WHERE author_id = $2。任意 write-scope token 可删除/废弃他人文章。鉴权同文件内不一致。

  • M2 · 标签文章数 bug(api + mcp 双重镜像)src/api/posts/tags.rs:33 + src/mcp/tools/read.rs:~290
    COUNT(pt.post_id) 数了草稿/已删文章(posts JOIN 在 ON 子句里,count 没用 p.id)。应改 COUNT(p.id)。影响公开标签云(缓存 300s)和 MCP list_tags已是重复代码漂移的实例(见 R1)。

  • M3 · backup.rs 在 async fn 里阻塞 tokio workersrc/api/database/backup.rs:167-190,444-456
    std::process::Command + child.wait_with_output() 跑 pg_dump/psql(可能数十秒~分钟),违反 spawn_blocking 约定。L368 read_to_string 把整个备份文件读进内存只为验首行签名;L497-512 list_backups 对每个 .sql 文件全量读入只为取第三行 -- mode:
    修复:包 spawn_blocking(或 tokio::process/tokio::fs),读首行用 BufReader

  • M4 · 评论 markdown 渲染未走 spawn_blockingsrc/api/comments/create.rs:148
    render_comment_markdown(pulldown + syntect + KaTeX,CPU 密集)直接跑在 async runtime。posts/* 都包了 spawn_blocking。评论里的代码块是攻击者可控的 CPU 负载。

  • M5 · 回收站批量操作漏 SSR 失效src/api/posts/trash.rs:347-354,412-419
    batch_purge_posts/empty_trash 只调 bump_global_generation(),漏调 invalidate_ssr_all_public()(单条 purge/restore 都调了)。批删/清空回收站后 SSR 缓存的列表/归档页仍显示已删文章。bump_global_generation 自身注释也说"当前不会使 SSR 缓存失效"。

  • M6 · MCP publish_post 漏标签缓存失效src/mcp/tools/posts.rs:~405
    cache::invalidate_tag_posts_for(tags) → 标签页新发文后陈旧 ≤120s(web 端 update.rs:195 会失效)。

  • M7 · 分页标签缓存永不失效src/cache.rs:469-473
    invalidate_posts_by_tag 只清 PostsByTag 键,不清 PostsByTagPage{tag,page,per_page} → 分页标签列表陈旧 ≤120s。注:分页路径前端未用到(pages/tags.rs 总传 None,None),属半死 API。

  • M8 · 限流器无 key GC + XFF 可伪造src/api/rate_limit.rs
    (a) 7 个 keyed governor limiter(+ MCP 一个)从不调 retain_recent,公开 IP-keyed 桶在 IP 轮换攻击下内存无界增长;
    (b) TRUSTED_PROXY_COUNT≥1 时客户端可伪造 XFF 控制自己的限流 key(逃逸 + 加剧 a)。

  • M9 · export.rs 原始 DB 错误泄露到 HTTP 响应src/api/database/export.rs:138,154
    format!("查询失败:{e}") 把 tokio_postgres 错误(含 SQL/表名/内部细节)直接返回 body,违反全仓库 AppError 脱敏哲学(error.rs 测试明确锁定"不泄露 SQL")。

  • M10 · start_exec 与 stream 版 ~120 行重复src/api/code_runner/execute.rs:121-211 vs 244-340
    信号量→stage→clamp→run→match 几乎完全相同,唯一差异是调用 stream 版还是非 stream 版 runner。两条路径必然漂移。建议抽 spawn_exec_task(lang_key, req, stream_tx: Option<Sender>)

  • M11 · SQL 控制台全量物化结果集再客户端截断src/api/database/sql_console.rs:376-394
    读路径 client.query(stmt_sql) 全量物化后在客户端截断 500 行。SELECT * FROM big_table 会把整张表拉进进程内存,仅靠 statement_timeout 兜底。建议追加 LIMITquery_raw + take

  • M12 · admin 文章列表吞掉拉取错误src/pages/admin/posts.rs:172
    let _error = paginated.error; 把失败误报为"暂无文章"(兄弟 trash tab posts_trash.rs:62,135-142 正确处理了 error 分支)。


三、⚪ 死代码(Dead Code)

  • D1 · 整模块死代码:src/mcp/resources.rs(273 行全部死)
    完整的资源子系统(list_published_resources/read_post_resource/post_resource_template/ResourcePage/URI 助手/游标编解码/分页常量),但 ServerHandler 从未 override list_resources/read_resource,故无一处可达。模块自身 doc 声称"server.rs 把它们接到 ServerHandler",接线从未完成。被 src/main.rs:47#[allow(dead_code)] mod mcp; 模块级静默掩盖(会掩盖未来的 mcp 死代码)。
    内部还藏 bug:post_resource_template() 传字面量 "{POST_URI_PREFIX}{slug}"(缺 format!),测试 L290 把 bug 写进断言。
    建议:删除整个文件 + 收窄 main.rs 的 allow;或接进 ServerHandler 并修 bug。

  • D2 · src/models/user.rs:32-118 — 完整 User 结构(含 password_hash)+ From<User> for SessionUser + From<User> for PublicUser 生产环境从未使用(auth.rs:402 直接从 row 构造 SessionUser);仅自身测试构造。~65 行生产死代码。

  • D3 · src/api/posts/helpers.rs:57row_to_post_full(_client: &Client, row)_client 参数从未使用(两个调用点都传了活 client)。

  • D4 · src/api/mhchem.rs:959-969go_innerfield_str 字节级完全相同的函数(各被调 17/19 次)。删一留一。

  • D5 · src/api/mhchem_tables.rs:56-93tex 状态机表不可达(to_tex 只被 "ce"/"pu" 调用,无 go() 用 "tex")。~40 行死数据(机械移植残留)。

  • D6 · src/api/markdown.rs:10-13 + src/api/comments/markdown.rs:11-14clean_html/clean_comment_html 各是一行直通包装器,仅同文件调用,无外部 caller(comments/markdown 模块甚至未被 mod.rs re-export)。

  • D7 · src/api/sanitizer.rs:150-170is_safe_data_uri + allow_data_uri 配置字段生产死(两调用点硬编码 false)。

  • D8 · src/api/code_runner/mod.rs:47ExecStatus::RateLimited 服务端从不构造(限流在 insert 前拒绝;仅前端有 label)。serde 兼容保留,建议加注释。

  • D9 · src/api/database/mod.rs:6-7 — 过时规划注释,列出"后续 task 新增"的模块其实都已存在。

  • D10 · src/components/comments/item.rs:24,38let refresh_trigger = ctx.refresh_trigger; … let _ = refresh_trigger; 死绑定。

  • D11 · src/components/post/post_nav_links.rs:28,42 — 两个空 no-op onclick: move |_| {}

  • D12 · src/components/header.rs:40use_memo(|| "mobile-nav-menu".to_string()),对常量做 memo,用 &str 即可。


四、🔁 系统性代码重复(最大冗余来源)

冗余集中在两条轴:(A) api/mcp/tools/ 镜像(B) api/posts/ 内部自拷贝

  • R1 · MCP 工具镜像 api 业务逻辑(~700 行)mcp/tools/posts.rs 逐行移植 api/posts/{create,update,delete,trash}.rscomments.rs 镜像 api/comments/{list,update}(含相同的 WITH RECURSIVE ancestors CTE);media.rs 镜像 api/upload.rs(magic bytes、转码、CAS dedup SQL、日期目录命名);settings.rs 镜像 api/settings.rs + tasks/post_purge.rs(三份回收站设置读写)。已有 bug 因重复而扩散(M2 标签 count 在 api 与 mcp 各一份;post_purge.rs:45,55 硬编码 false/30 而另两份用 DEFAULT_* 常量)。

  • R2 · MCP 跨文件 helper 四重拷贝require_scope/internal()/ok_jsonposts.rs/comments.rs/media.rs/tags.rs 各一份完全相同;require_adminsettings.rs+runner.rs 两份。建议抽 mcp/tools/common.rs,删 ~10 份拷贝。

  • R3 · clean_tags/sync_tags/sync_asset_refspub(super) 被逐字拷进 MCPapi/posts/helpers.rs:139-252 vs mcp/tools/posts.rs:666-722,拷贝处注释自承「镜像 … 因 pub(super) 不可跨模块调用」。同文件 extract_asset_paths 已是 pub(crate) 并被共享——先例已存在。建议放宽为 pub(crate),删 MCP 拷贝(R1 的最大杠杆)。

  • R4 · render+metrics 块重复 6×spawn_blocking render → toc_html None-if-empty → auto_summary → PostStatus → cover → count_words → reading_timecreate.rs:66-84update.rs:45-63rebuild.rs:67-92 + 188-208mcp/posts.rs:68-93 + 178-203建议抽 helpers.rs::render_post_fields()

  • R5 · 标签获取 SQL 重复 9×SELECT t.name FROM tags JOIN post_tags … WHERE pt.post_id=$1update.rs:110delete.rs:47trash.rs:57,133,214,307(+ANY 变体 390)、mcp/posts.rs:257,449,513建议 fetch_post_tags() helper。

  • R6 · 缓存+SSR 失效「连祷」序列 6+×invalidate_post_metadata+invalidate_post_by_slug+invalidate_tag_posts_for+invalidate_ssr_route+invalidate_ssr_all_public+bump_global_generation,在 create/update/delete/trash + mcp 多处。建议 cache::invalidate_for_post_write(slugs, tags) 单一来源。

重复常量 / 工具函数(应集中)

  • EMAIL_REGEX 两份完全相同:auth.rs:38 + comments/helpers.rs:95
  • hash_token 两份逐字相同:auth/session.rs:22 + mcp/auth.rs:45
  • MAX_FILE_SIZE 两份:upload.rs:31 + mcp/tools/media.rs:24(均 5*1024*1024)。
  • MIGRATE_STARTUP_TIMEOUT_SECS 解析 3 份:main.rs:145db/pool.rs:157244(默认值 30 相同)。
  • LIKE 转义两套实现:list.rs:139 字符循环 vs search.rs:59 + mcp/read.rs:200 replace 链。
  • format_bytes 两份且分歧:assets.rs 1 位小数 vs system/mod.rs:129 2 位小数。
  • urlencode 两份:system/export.rs:131 + backup.rs:296(手写编码器,应进 utils)。
  • env-var 解析惯用法 var(k).ok().and_then(parse).unwrap_or(d) 散落 ~20 处;rate_limit.rs:26 已有 env_or 但私有未复用。
  • formatted_date 两份相同 body:models/post.rs:83(Post)+ 128(PostListItem)。

五、🟡 轻微(MINOR)— 文档漂移 / 风格(精选)

  • src/api/image.rs:746 get_image_dimensions整个文件只解析头部维度(有 24h 缓存,低优先)。
  • src/api/sanitizer.rs:369 unwrap_or_default():rewrite 出错静默返回空串(整篇文章 body 被抹掉,无日志)。
  • src/components/post/post_cover.rs:38-47 + pages/admin/write.rs:685-692 潜在 URL bug:含 ? 的资源 URL 被 split 后用 &w=… 拼回(缺 ?)。当前数据不触发。
  • src/pages/admin/system/db_status.rs:47-55 生产路径残留 console.log DEBUG 计时日志。
  • src/pages/admin/assets.rs:162-171 每次 render clone 最多 60 个 AssetDto;HashSet 每次切换 clone 整个 set(可用 write().insert)。
  • src/components/post_card.rs:65 每次 render post.tags.clone()
  • src/api/rate_limit.rs:70 注释「0.2 req/s」实际默认 1 req/s(doc 漂移)。
  • src/api/posts/search.rs:2-4 模块 doc 称 pg_trgm,实现是 ILIKE(doc 漂移)。
  • src/api/database/sql_console.rs:191-194 is_read_only doc 称覆盖 SELECT/EXPLAIN/SHOW/WITH,实际 match 只覆盖 Query+Explain → SHOW 落进写分支会误触发全缓存失效。
  • src/api/comments/check.rs:24 check_pending_status(ids) 未认证端点,ids 数组无长度上限。
  • src/pages/admin/comments.rs:~148 切换 filter 不重置路由 page,可能落到空页;?status= 仅挂载时读一次。
  • src/pages/admin/posts_trash.rs + comments.rs 乐观变更忽略 server Result,失败时 UI 与 DB 不一致。
  • src/pages/admin/asset_picker.rs:36-54 搜索每次按键触发 list_assets,无 debounce、无 stale 响应保护(assets.rs 有 debounce 可借鉴)。
  • src/components/skeletons/post_card_skeleton.rs:15 skeleton 样式陈旧(bg-white/rounded-lg/gray border)与现 PostCard(paper-entry/rounded-[2rem])不匹配 → 加载时视觉跳变。
  • src/components/admin_layout.rs:148-155 WriteEdit 路由回退到 AdminDashboardSkeleton 而非 WriteSkeleton,编辑页会闪错骨架屏。

六、✅ 整体健康项(无需改动)

  • panic="abort" 约定执行良好:12 处 panic! 全在 #[test] 或文档化的不可达 stub;非测试 expect() 均为带说明的真不变量;unwrap() 零违规。
  • AppError 脱敏分层除 export.rs(M9)外全部正确遵守;error.rs 源链日志 + 静态客户端消息。
  • DB 连接池防雪崩正确:pool-full Timeout 不重试,只对 Backend 错误退避;get_conn/get_conn_for_startup 分离合理。
  • 迁移系统(advisory lock + 单事务/迁移 + 回滚 + 数组/磁盘文件一致性编译测试)干净。
  • MCP crypto(AES-256-GCM,每次 OsRng nonce)安全;MCP router Origin/Host 白名单 + DNS-rebinding 防护 + dev-only 0.0.0.0 正确门控。
  • Docker 隔离测试固化(cap_drop ALL、no-new-privileges、pids_limit、memory_swap==memory、tmpfs exec 仅 /tmp)。
  • theme.rs 的 Theme/ResolvedTheme 模型堪称典范;forms.rs FormSelect 无障碍完备且翻转逻辑有测试。

七、建议优先级

立即修复(真实 bug / 稳定性):C1+C2(katex \pu + UTF-8,一行改动)→ C3(docker 挂起,DoS 级)→ C5(panic 崩溃)→ M1(越权)→ M2(公开数据错)→ M5(SSR 失效漏)。

重构(降重复防漂移):D1(删/接 resources.rs + 收窄 allow)→ R2(抽 MCP common)→ R3(放宽 helpers 可见性,R1 的最大杠杆)→ R4/R5/R6(抽 render/tags/失效三件套)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions