From feb5ae709f39661129fdb34fe61f950c5948b895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Wed, 17 Jun 2026 08:58:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=204=20=E4=B8=AA=20CRITICAL=20bug=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1: placement.rs 删除列偏移特殊处理 (col==6→5),place_bit 已自动跳过保留区 - C2: version.rs V5-H 纠错表 h_g1: 4→2 (总码字数 200→134) - C3: mode.rs Kanji 编码删除冗余 if/else 重复分支 - C4: galois.rs div() 返回 Option 替代 panic! --- core/src/ecc/galois.rs | 19 +++---- core/src/encoder/bitstream.rs | 4 +- core/src/encoder/mode.rs | 22 +++----- core/src/matrix/mask.rs | 4 +- core/src/matrix/placement.rs | 4 +- core/src/version.rs | 4 +- docs/code-review-report.md | 100 ++++++++++++++++++++++++++++++++++ 7 files changed, 124 insertions(+), 33 deletions(-) create mode 100644 docs/code-review-report.md diff --git a/core/src/ecc/galois.rs b/core/src/ecc/galois.rs index 439e9ba..33fa720 100644 --- a/core/src/ecc/galois.rs +++ b/core/src/ecc/galois.rs @@ -65,19 +65,19 @@ pub fn mul(a: u8, b: u8) -> u8 { exp_table()[log_a + log_b] } -/// GF(2⁸) 除法:a / b +/// GF(2⁸) 除法:a / b,b == 0 时返回 None #[inline] -pub fn div(a: u8, b: u8) -> u8 { +pub fn div(a: u8, b: u8) -> Option { if a == 0 { - return 0; + return Some(0); } if b == 0 { - panic!("GF(2⁸) 除以零"); + return None; } let log_a = log_table()[a as usize] as usize; let log_b = log_table()[b as usize] as usize; let diff = (log_a + 255 - log_b) % 255; - exp_table()[diff] + Some(exp_table()[diff]) } /// GF(2⁸) 幂运算:base^exp @@ -134,7 +134,7 @@ mod tests { #[test] fn test_div_inverse() { for a in 1..=255u8 { - let inv = div(1, a); + let inv = div(1, a).unwrap(); assert_eq!(mul(a, inv), 1, "逆元失败: {:02X} * {:02X} != 1", a, inv); } } @@ -143,7 +143,7 @@ mod tests { fn test_div_mul_consistency() { for a in 1..=255u8 { for b in (1..=255u8).step_by(17) { - let q = div(a, b); + let q = div(a, b).unwrap(); assert_eq!( mul(q, b), a, @@ -167,9 +167,8 @@ mod tests { } #[test] - #[should_panic(expected = "除以零")] - fn test_div_by_zero_panics() { - div(1, 0); + fn test_div_by_zero_returns_none() { + assert_eq!(div(1, 0), None); } #[test] diff --git a/core/src/encoder/bitstream.rs b/core/src/encoder/bitstream.rs index 4270796..4cad1f1 100644 --- a/core/src/encoder/bitstream.rs +++ b/core/src/encoder/bitstream.rs @@ -31,10 +31,10 @@ pub fn build_codewords(text: &str, version: Version, level: EcLevel) -> Vec // 2. 终止符(最多 4 bit 0) let total_capacity = get_data_capacity(version, level) as usize * 8; let terminator_len = 4usize.min(total_capacity.saturating_sub(bits.len())); - bits.extend(std::iter::repeat(false).take(terminator_len)); + bits.extend(std::iter::repeat_n(false, terminator_len)); // 3. 补零到 8-bit 边界 - while bits.len() % 8 != 0 { + while !bits.len().is_multiple_of(8) { bits.push(false); } diff --git a/core/src/encoder/mode.rs b/core/src/encoder/mode.rs index 5ae63c0..92ed5dd 100644 --- a/core/src/encoder/mode.rs +++ b/core/src/encoder/mode.rs @@ -158,22 +158,14 @@ fn unicode_to_shift_jis(c: char) -> Option { // 简化映射: 用 Unicode 码位偏移做近似 // 真实转换需要完整映射表,这里做合理近似 let base = code - 0x4E00; - let hi = 0x81 + (base / 0xBC) as u32; - let lo = 0x40 + (base % 0xBC) as u32; + let hi = 0x81 + (base / 0xBC); + let lo = 0x40 + (base % 0xBC); let sjis = ((hi << 8) | lo) as u16; - // 映射到 13-bit 码字 - let val = if sjis <= 0x9FFC { - let h = (sjis >> 8) as u16; - let l = (sjis & 0xFF) as u16; - if h >= 0x81 && h <= 0x9F { - (h - 0x81) * 0xBC + (l - 0x40) - } else { - (h - 0xC1) * 0xBC + (l - 0x40) - } - } else { - let h = (sjis >> 8) as u16; - let l = (sjis & 0xFF) as u16; - if h >= 0x81 && h <= 0x9F { + // 映射到 13-bit 码字(内层 if/else 已区分两个 Shift-JIS 区间) + let val = { + let h = (sjis >> 8); + let l = (sjis & 0xFF); + if (0x81..=0x9F).contains(&h) { (h - 0x81) * 0xBC + (l - 0x40) } else { (h - 0xC1) * 0xBC + (l - 0x40) diff --git a/core/src/matrix/mask.rs b/core/src/matrix/mask.rs index 547bfd2..eba6b70 100644 --- a/core/src/matrix/mask.rs +++ b/core/src/matrix/mask.rs @@ -11,8 +11,8 @@ pub const MASK_FNS: [MaskFn; 8] = [ |x, y| (x + y) % 3 == 0, |x, y| ((y / 2) + (x / 3)) % 2 == 0, |x, y| (x as u32 * y as u32) % 2 + (x as u32 * y as u32) % 3 == 0, - |x, y| ((x as u32 * y as u32) % 2 + (x as u32 * y as u32) % 3) % 2 == 0, - |x, y| ((x as u32 + y as u32) % 2 + (x as u32 * y as u32) % 3) % 2 == 0, + |x, y| ((x as u32 * y as u32) % 2 + (x as u32 * y as u32) % 3).is_multiple_of(2), + |x, y| ((x as u32 + y as u32) % 2 + (x as u32 * y as u32) % 3).is_multiple_of(2), ]; /// 应用掩码到矩阵的数据区域(跳过功能图案保留区域) diff --git a/core/src/matrix/placement.rs b/core/src/matrix/placement.rs index b7b4861..afc1b74 100644 --- a/core/src/matrix/placement.rs +++ b/core/src/matrix/placement.rs @@ -16,8 +16,8 @@ pub fn place_data(matrix: &mut Matrix, codewords: &[u8]) { let mut going_up = true; while col >= 0 && bit_idx < bits.len() { - // 跳过垂直时序线 (col=6) - let actual_col = if col == 6 { 5 } else { col as usize }; + // 列 6 是垂直时序图案,place_bit 会自动跳过保留区 + let actual_col = col as usize; if going_up { for row in (0..size).rev() { diff --git a/core/src/version.rs b/core/src/version.rs index 41d0376..3db4d3c 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -1,5 +1,5 @@ -use std::sync::OnceLock; use serde::Serialize; +use std::sync::OnceLock; /// 纠错级别 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] @@ -272,7 +272,7 @@ static VERSION_TABLE: [VersionRow; 40] = [ q_g2_data: 16, h_total: 134, h_ec: 22, - h_g1: 4, + h_g1: 2, h_g1_data: 11, h_g2: 2, h_g2_data: 12, diff --git a/docs/code-review-report.md b/docs/code-review-report.md new file mode 100644 index 0000000..62f21ba --- /dev/null +++ b/docs/code-review-report.md @@ -0,0 +1,100 @@ +# QRGen 全面代码审查报告 + +**日期**: 2026-06-17 +**审查方式**: 三重并行审查(算法正确性 + 安全性 + 架构/代码质量) +**测试状态**: 69 tests pass | clippy: 14 风格警告 + +--- + +## CRITICAL — 必须立即修复 + +### C1. placement.rs:20 — 数据排列列偏移 bug(所有版本) + +当 zigzag 扫描到 `col == 6`(时序图案列)时,代码改为 `col = 5` 并处理列 (5, 4)。下一轮 `col = 4` 处理列 (4, 3)。**列 4 被写入两次**,第一次写入的数据被覆盖丢失。 + +**修复**: 删除特殊处理。`place_bit` 已自动跳过保留区: + +```rust +// 错误: +let actual_col = if col == 6 { 5 } else { col as usize }; +// 正确: +let actual_col = col as usize; +``` + +### C2. version.rs:273 — V5-H 纠错表数据错误 + +`h_g1: 4` 应为 `h_g1: 2`。当前值导致总码字数不匹配 (200 ≠ 134),Version 5 + H 级生成的 QR 码数据错乱。 + +### C3. mode.rs:165-181 — Kanji Shift-JIS 编码 if/else 分支完全相同 + +`unicode_to_shift_jis` 中的 `if sjis <= 0x9FFC { ... } else { ... }` 两个分支代码完全一致。`else` 分支应使用 `h - 0xC1` 替代 `h - 0x81`。 + +### C4. galois.rs:75 — 库函数中 panic!() + +`div(a, b)` 中 `b == 0` 时 panic。库函数不应 panic,应返回 `Option` 或 `Result`。 + +--- + +## HIGH — 应尽快修复 + +### H1. patterns.rs — 定位图案分隔符未标记为保留区 + +ISO 18004 要求定位图案周围有 1 模块宽的白色分隔符。当前未 `reserve()`,数据可能写入该区域,导致扫码器识别失败。 + +### H2. gui/src-frontend/src/hooks/useQrEncode.ts — setTimeout 未在组件卸载时清理 + +React Strict Mode 下或快速切换模式时,stale callback 可能在 unmount 后触发 dispatch。 + +### H3. version.rs — 1352 行超 800 行限制 + +VERSION_TABLE 占 ~1100 行。建议拆出 `version/table.rs`。 + +### H4. 缺少 Kanji 编码单元测试 + +`encode_kanji` 和 `unicode_to_shift_jis` 零直接测试。 + +### H5. 历史记录未持久化 + +`AppState.history` 仅存内存,重启丢失。`tauri-plugin-store` 已注册但未接线。 + +### H6. 14 个 clippy 警告(12 个可 auto-fix) + +--- + +## MEDIUM + +| # | 文件 | 问题 | +|---|------|------| +| M1 | QrPreview.tsx:20 | `dangerouslySetInnerHTML` — 如 SVG 渲染器未来嵌入用户文本则有 XSS 风险 | +| M2 | ExportPanel.tsx / useQrEncode.ts | 3 处 `console.error` 在生产代码中 | +| M3 | App.tsx | 缺少 React Error Boundary | +| M4 | mode.rs:148 | Kanji 编码是"近似映射",非完整 Shift JIS 转换 | +| M5 | gui/lib.rs | 边距参数无上限验证 (0-255),极端值可导致 OOM | +| M6 | svg.rs | SVG 渲染用大量 `format!` 拼接,大版本效率低 | + +--- + +## LOW + +| # | 文件 | 问题 | +|---|------|------| +| L1 | render/png.rs:38 | `.expect()` 而非错误传播 | +| L2 | galois.rs | `OnceLock` 可替换为 `LazyLock` | +| L3 | mask.rs:188 | 不必要的 `matrix.clone()` | +| L4 | version.rs | `ec_info()` 每次都分配 Vec | +| L5 | App.tsx:20 | emoji 在标题栏 | + +--- + +## 已验证正确 + +- ✅ Galois GF(2⁸) 四则运算 + exp/log 表 +- ✅ Reed-Solomon 生成多项式 + 长除法 + 交错 +- ✅ 四种编码模式的比特计数(符合 ISO 18004) +- ✅ 比特流终止符 + 0xEC/0x11 填充 +- ✅ 版本容量表(除 V5-H 外全部数学验证通过) +- ✅ BCH(15,5) + BCH(18,6) 编码 +- ✅ 8 种掩码 + 四规则惩罚评分 +- ✅ 定位/对齐/时序图案位置 +- ✅ 格式信息 XOR 掩码 0x5412 +- ✅ 零 `unsafe` 块 | 零硬编码密钥 | 零命令注入