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:
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user