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
+53 -15
View File
@@ -2,9 +2,19 @@ import { useCallback, useRef, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Store } from '@tauri-apps/plugin-store';
import { useQrState } from '../store/qrContext';
import type { HistoryEntry } from '../types';
import type { HistoryEntry, ModeType } from '../types';
const HISTORY_KEY = 'qr-history';
const STORE_FILE = 'history.json';
/** 缓存的 Store 实例,避免每次编码都重新加载 */
let storeCache: Promise<Store> | null = null;
function getStore(): Promise<Store> {
if (!storeCache) {
storeCache = Store.load(STORE_FILE);
}
return storeCache;
}
interface QrResponse {
svg: string;
@@ -13,6 +23,36 @@ interface QrResponse {
mask: number;
}
/** 对 WiFi 密码进行脱敏处理 */
function sanitizeContent(mode: ModeType, content: string): string {
if (mode === 'wifi') {
return content.replace(/P:[^;]*/, 'P:***');
}
return content;
}
/**
* 持久化整个历史列表到 store
* 作为内存状态的唯一持久化出口
*/
export async function persistHistory(history: HistoryEntry[]): Promise<void> {
try {
const store = await getStore();
await store.set(HISTORY_KEY, history);
await store.save();
} catch { /* store 不可用时静默忽略 */ }
}
/** 从 store 加载历史记录(应用启动时调用) */
export async function loadHistory(): Promise<HistoryEntry[]> {
try {
const store = await getStore();
return (await store.get<HistoryEntry[]>(HISTORY_KEY)) || [];
} catch {
return [];
}
}
export function useQrEncode() {
const { state, dispatch } = useQrState();
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -46,28 +86,26 @@ export function useQrEncode() {
// 保存到历史(内存 + 持久化)
const entryId = Date.now().toString();
const currentMode = modeRef.current;
const entry: HistoryEntry = {
id: entryId,
mode: modeRef.current,
content: text,
mode: currentMode,
content: sanitizeContent(currentMode, text),
timestamp: Date.now(),
formData: { ...state.formData },
};
dispatch({ type: 'ADD_HISTORY', payload: entry });
// 持久化到 tauri-plugin-store
try {
const store = await Store.load('history.json');
const current = await store.get<HistoryEntry[]>(HISTORY_KEY) || [];
const updated = [entry, ...current].slice(0, 50);
await store.set(HISTORY_KEY, updated);
await store.save();
} catch { /* store 不可用时静默忽略 */ }
} catch (e) {
// 编码失败已在 dispatch SET_PREVIEW(null) 中处理,无需额外日志
// 从内存状态持久化(避免 store 读写竞态)
// 注意: dispatch ADD_HISTORY 是异步的,这里手动计算最新列表
// 确保持久化的数据与内存一致
persistHistory([entry, ...state.history].slice(0, 50));
} catch {
// 编码失败时清空预览
dispatch({ type: 'SET_PREVIEW', payload: null });
}
}, 200);
}, [state.config.level, state.config.margin, dispatch]);
}, [state.config.level, state.config.margin, state.formData, state.history, dispatch]);
return { encode };
return { encode, persistHistory };
}