fix: 全面代码审查修复 — 安全/类型/持久化/代码质量 (28项)

🔴 CRITICAL (1):
- tauri.conf.json: CSP 从 null 改为最小权限策略

🟠 HIGH (6):
- 新建 capabilities/default.json: Tauri v2 权限约束(store/dialog/clipboard)
- cli: 路径遍历防护 — 拒绝含 ParentDir 组件的输出路径
- HistoryList: 删除/清空同步持久化到 store,历史点击用 formData 回填
- ExportPanel: 移除 console.warn,getCurrentText any→QrState
- useQrEncode: WiFi 密码在历史中脱敏显示(P:***),Store 实例缓存

🟡 MEDIUM (10):
- mode.rs: Kanji fallback 从 UTF-8 字节改为 13-bit 零值占位(段内模式一致)
- mode.rs: Shift JIS 第二字节跳过 0x7F 空洞,修正行内索引
- mode.rs: encode_numeric/alphanumeric 添加 debug_assert! 前置条件
- mask.rs: best_matrix.unwrap()→expect() 附错误信息
- version.rs: ec_info 仅返回 count>0 的 BlockInfo,EcInfo/BlockInfo 加 Debug+Clone
- types/index.ts: HistoryEntry.mode string→ModeType,新增 formData 字段
- qrContext.tsx: 使用缓存 Store 加载历史

🟢 LOW (11):
- cargo fmt 全部文件
- svg.rs: String::new()→with_capacity() 预分配
- patterns.rs: encode_format_info 拆分为两行提高可读性
- png.rs: 提取 fill_module() 辅助函数降低嵌套
- ErrorBoundary: 添加 componentDidCatch 错误日志入口
- QrPreview: dangerouslySetInnerHTML→<img>+data URL(安全),loading 状态指示
- galois.rs/version.rs: 5 处 #[allow(clippy::indexing_slicing)]+安全文档
- 新建 utils/qrText.ts: 集中管理 6 种模式的文本构造,消除 ExportPanel/mode 间重复

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-17 14:10:13 +08:00
parent 3f1b9901b5
commit 1e9c94eff9
26 changed files with 413 additions and 154 deletions
+89 -18
View File
@@ -44,18 +44,45 @@ fn test_dump_format_info() {
// 格式信息位置 (按标准顺序 bit14→bit0)
let coords = [
(0, 8), (1, 8), (2, 8), (3, 8), (4, 8), (5, 8), (7, 8), (8, 8),
(8, 7), (8, 5), (8, 4), (8, 3), (8, 2), (8, 1), (8, 0),
(0, 8),
(1, 8),
(2, 8),
(3, 8),
(4, 8),
(5, 8),
(7, 8),
(8, 8),
(8, 7),
(8, 5),
(8, 4),
(8, 3),
(8, 2),
(8, 1),
(8, 0),
];
let mut fmt_bits = 0u16;
for (i, &(x, y)) in coords.iter().enumerate() {
let bit = if m[y as usize][x as usize] { 1u16 } else { 0u16 };
let bit = if m[y as usize][x as usize] {
1u16
} else {
0u16
};
fmt_bits = (fmt_bits << 1) | bit;
print!("{} ", if m[y as usize][x as usize] { '█' } else { '_' });
print!(
"{} ",
if m[y as usize][x as usize] {
'█'
} else {
'_'
}
);
}
println!();
println!("读取的格式信息 (原始, 含 XOR mask 0x5412): 0x{:04X}", fmt_bits);
println!(
"读取的格式信息 (原始, 含 XOR mask 0x5412): 0x{:04X}",
fmt_bits
);
// 去掉 XOR mask
let unmasked = fmt_bits ^ 0x5412;
@@ -63,7 +90,10 @@ fn test_dump_format_info() {
let ec_bits = (unmasked >> 13) & 0x03;
let mask_bits = (unmasked >> 10) & 0x07;
let bch = unmasked & 0x3FF;
println!("EC bits: {:02b} 掩码 bits: {:03b} BCH: 0x{:03X}", ec_bits, mask_bits, bch);
println!(
"EC bits: {:02b} 掩码 bits: {:03b} BCH: 0x{:03X}",
ec_bits, mask_bits, bch
);
// 期望值
let expected = {
@@ -71,7 +101,14 @@ fn test_dump_format_info() {
encode_format_info(qr.level.indicator_bits(), qr.mask)
};
println!("期望的格式信息: 0x{:04X}", expected);
println!("匹配: {}", if fmt_bits == expected { "" } else { "❌ 不匹配!" });
println!(
"匹配: {}",
if fmt_bits == expected {
""
} else {
"❌ 不匹配!"
}
);
assert_eq!(fmt_bits, expected, "格式信息不匹配!");
}
#[test]
@@ -84,11 +121,30 @@ fn test_finder_patterns_present() {
let s = size as u8;
let finders: [(u8, u8); 3] = [(0, 0), (s - 7, 0), (0, s - 7)];
for (fx, fy) in finders {
assert!(m[fy as usize][fx as usize], "定位({},{}): 左上角应为暗", fx, fy);
assert!(m[fy as usize][(fx + 6) as usize], "定位({},{}): 右上角应为暗", fx, fy);
assert!(m[(fy + 6) as usize][fx as usize], "定位({},{}): 左角应为暗", fx, fy);
assert!(
m[fy as usize][fx as usize],
"定位({},{}): 左角应为暗",
fx, fy
);
assert!(
m[fy as usize][(fx + 6) as usize],
"定位({},{}): 右上角应为暗",
fx,
fy
);
assert!(
m[(fy + 6) as usize][fx as usize],
"定位({},{}): 左下角应为暗",
fx,
fy
);
// 内部 3×3 是暗色
assert!(m[(fy + 2) as usize][(fx + 2) as usize], "定位({},{}): 中心3×3应为暗", fx, fy);
assert!(
m[(fy + 2) as usize][(fx + 2) as usize],
"定位({},{}): 中心3×3应为暗",
fx,
fy
);
}
}
@@ -128,8 +184,10 @@ fn test_format_info_written() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let m = qr.modules();
// 格式信息在定位图案旁,检查几个位置不是全亮
assert!(m[8][0] || m[8][1] || m[8][2] || !m[8][0],
"格式信息应已写入");
assert!(
m[8][0] || m[8][1] || m[8][2] || !m[8][0],
"格式信息应已写入"
);
}
#[test]
@@ -140,8 +198,10 @@ fn test_svg_valid_structure() {
assert!(svg.starts_with("<svg"), "SVG 应以 <svg 开头");
assert!(svg.contains("rect"), "SVG 应包含 rect 元素");
assert!(svg.contains("fill=\"black\""), "SVG 暗模块应是黑色");
assert!(svg.ends_with("</svg>\n") || svg.ends_with("</svg>"),
"SVG 应以 </svg> 结尾");
assert!(
svg.ends_with("</svg>\n") || svg.ends_with("</svg>"),
"SVG 应以 </svg> 结尾"
);
}
#[test]
@@ -163,7 +223,10 @@ fn test_qr_structure_dump() {
let size = qr.size() as usize;
let m = qr.modules();
println!("\n=== QR Matrix {}x{} v{} mask{} ===", size, size, qr.version.0, qr.mask);
println!(
"\n=== QR Matrix {}x{} v{} mask{} ===",
size, size, qr.version.0, qr.mask
);
for y in 0..size {
for x in 0..size {
print!("{}", if m[y][x] { "##" } else { " " });
@@ -174,9 +237,17 @@ fn test_qr_structure_dump() {
// 统计
let dark: usize = m.iter().flatten().filter(|&&x| x).count();
let total = size * size;
println!("\n暗/总: {}/{} = {:.1}%", dark, total, dark as f64 / total as f64 * 100.0);
println!(
"\n暗/总: {}/{} = {:.1}%",
dark,
total,
dark as f64 / total as f64 * 100.0
);
println!("尺寸: {}×{}", size, size);
println!("版本: {} 掩码: {} 纠错: {:?}", qr.version.0, qr.mask, qr.level);
println!(
"版本: {} 掩码: {} 纠错: {:?}",
qr.version.0, qr.mask, qr.level
);
}
#[test]