Skip to content

Commit aece95f

Browse files
committed
fix(v0.15): 审查即修(b64 解码前尺寸防线/拖拽监听清理/列布局守卫/死代码清理)
1 parent 7164041 commit aece95f

4 files changed

Lines changed: 40 additions & 30 deletions

File tree

‎app/src-tauri/src/commands_note_images.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ pub async fn import_note_image_b64(
120120
if state.db.get_note(note_id).map_err(|e| e.to_string())?.is_none() {
121121
return Err("笔记不存在".to_string());
122122
}
123+
// 解码前尺寸防线:base64 膨胀率 4/3(+填充余量)——纯文本长度超上限直接拒绝,
124+
// 防恶意/畸形超大输入先完整解码再检查的内存峰值(解码缓冲区翻倍)
125+
if image_b64.len() as u64 > IMAGE_MAX_BYTES / 3 * 4 + 64 {
126+
return Err(format!("图片数据超过大小上限({} 字节)", IMAGE_MAX_BYTES));
127+
}
123128
let bytes = BASE64
124129
.decode(image_b64.trim())
125130
.map_err(|_| "图片数据解码失败(非法 base64)".to_string())?;

‎app/src/components/ColumnBar.tsx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@ interface Props {
88
icon: string;
99
title: string;
1010
onClick: () => void;
11-
/** 附加徽标(如收件箱计数)——窄条上不常显,仅 title 提示 */
12-
badge?: string;
1311
}
1412

15-
export default function ColumnBar({ icon, title, onClick, badge }: Props) {
13+
export default function ColumnBar({ icon, title, onClick }: Props) {
1614
return (
1715
<div
1816
data-testid="column-bar"
1917
onClick={onClick}
20-
title={`${title}${badge ? `(${badge})` : ""}`}
18+
title={title}
2119
style={{
2220
width: 26,
2321
flexShrink: 0,

‎app/src/components/ColumnResizer.tsx‎

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/**
22
* ColumnResizer — 列拖拽手柄(v0.15 全站自适应)。
33
*
4-
* @ai-context: pointer 事件捕获(setPointerCapture → window move/up)——拖出窗口
5-
* 或丢失 mouseup 仍能结束;方向由调用方决定(side="right" 拖右边
6-
* 增宽/左边收窄;side="left" 反向——拖左手柄收窄列本身)。纯展示,
7-
* 增量化经 onResize 上抛(useColumnLayout.resizeBy 夹取 min/max)。
4+
* @ai-context: pointer 事件捕获(setPointerCapture → 事件全量路由到元素本身,
5+
* 拖出窗口仍持续;元素卸载即自动停止——无 window 监听残留,审查即修)。
6+
* 方向由调用方决定(side="right" 拖右边增宽/左边收窄;side="left"
7+
* 反向——拖左手柄收窄列本身)。纯展示,增量化经 onResize 上抛
8+
* (useColumnLayout.resizeBy 夹取 min/max)。
89
*/
9-
import { useState } from "react";
10+
import { useRef, useState } from "react";
1011

1112
interface Props {
1213
/** 手柄所在边的对齐方向(决定拖拽增量的符号) */
@@ -18,28 +19,35 @@ interface Props {
1819

1920
export default function ColumnResizer({ side = "right", onResize, onReset }: Props) {
2021
const [hover, setHover] = useState(false);
22+
const [dragging, setDragging] = useState(false);
23+
const startXRef = useRef(0);
2124

2225
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
2326
e.preventDefault();
24-
const startX = e.clientX;
25-
const el = e.currentTarget;
26-
try { el.setPointerCapture(e.pointerId); } catch { /* 旧 WebView 无捕获——move 仍走 window */ }
27-
const move = (ev: PointerEvent) => {
28-
const dx = ev.clientX - startX;
29-
onResize(side === "right" ? dx : -dx);
30-
};
31-
const up = () => {
32-
window.removeEventListener("pointermove", move);
33-
window.removeEventListener("pointerup", up);
34-
};
35-
window.addEventListener("pointermove", move);
36-
window.addEventListener("pointerup", up);
27+
startXRef.current = e.clientX;
28+
try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* 旧 WebView 无捕获——move 仍走元素事件 */ }
29+
setDragging(true);
30+
};
31+
32+
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
33+
if (!dragging) return;
34+
const dx = e.clientX - startXRef.current;
35+
onResize(side === "right" ? dx : -dx);
36+
};
37+
38+
const endDrag = (e: React.PointerEvent<HTMLDivElement>) => {
39+
if (!dragging) return;
40+
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch { /* 未捕获则忽略 */ }
41+
setDragging(false);
3742
};
3843

3944
return (
4045
<div
4146
data-testid="column-resizer"
4247
onPointerDown={onPointerDown}
48+
onPointerMove={onPointerMove}
49+
onPointerUp={endDrag}
50+
onPointerCancel={endDrag}
4351
onPointerEnter={() => setHover(true)}
4452
onPointerLeave={() => setHover(false)}
4553
onDoubleClick={() => onReset?.()}
@@ -48,10 +56,11 @@ export default function ColumnResizer({ side = "right", onResize, onReset }: Pro
4856
width: 5,
4957
flexShrink: 0,
5058
cursor: "col-resize",
51-
background: hover ? "#0d9488" : "transparent",
59+
background: hover || dragging ? "#0d9488" : "transparent",
5260
borderLeft: "1px solid #e5e7eb",
5361
transition: "background 0.15s",
5462
userSelect: "none",
63+
touchAction: "none",
5564
}}
5665
/>
5766
);

‎app/src/hooks/useColumnLayout.ts‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,19 @@ export interface ColumnLayout {
2626
folded: boolean;
2727
/** 用户手动折叠态(自动折叠兜底下的用户意图保留) */
2828
manuallyFolded: boolean;
29-
setWidth: (w: number) => void;
3029
/** 拖拽手柄增量化(负数=收窄——列方向感知由调用方处理) */
3130
resizeBy: (delta: number) => void;
3231
resetWidth: () => void;
3332
setManualFolded: (v: boolean) => void;
34-
toggleFold: () => void;
3533
/** 立即展开(同时清自动/手动折叠态——窄窗下点击窄条仍可展开,resize 后按阈值重折) */
3634
expand: () => void;
3735
}
3836

3937
const clamp = (w: number, min: number, max: number) => Math.max(min, Math.min(max, Math.round(w)));
4038

4139
function readNumber(key: string, fallback: number): number {
40+
// window 守卫(GroupSidebar 同款模式)——非浏览器环境(SSR/未来测试宿主)零崩溃
41+
if (typeof window === "undefined") return fallback;
4242
try {
4343
const v = window.localStorage.getItem(`layout:col-width:${key}`);
4444
return v != null ? Number(v) : fallback;
@@ -48,6 +48,7 @@ function readNumber(key: string, fallback: number): number {
4848
}
4949

5050
function readBool(key: string, fallback: boolean): boolean {
51+
if (typeof window === "undefined") return fallback;
5152
try {
5253
const v = window.localStorage.getItem(`layout:col-fold:${key}`);
5354
return v != null ? v === "1" : fallback;
@@ -57,6 +58,7 @@ function readBool(key: string, fallback: boolean): boolean {
5758
}
5859

5960
function writeLayout(key: string, value: string) {
61+
if (typeof window === "undefined") return;
6062
try {
6163
window.localStorage.setItem(key, value);
6264
} catch {
@@ -85,12 +87,10 @@ export function useColumnLayout(key: string, opts: Options): ColumnLayout {
8587
useEffect(() => { writeLayout(`layout:col-width:${key}`, String(width)); }, [key, width]);
8688
useEffect(() => { writeLayout(`layout:col-fold:${key}`, manualFolded ? "1" : "0"); }, [key, manualFolded]);
8789

88-
const setWidth = useCallback((w: number) => setWidthState(clamp(w, opts.min, opts.max)), [opts.min, opts.max]);
8990
const resizeBy = useCallback((delta: number) => {
9091
setWidthState((cur) => clamp(cur + delta, opts.min, opts.max));
9192
}, [opts.min, opts.max]);
9293
const resetWidth = useCallback(() => setWidthState(opts.default), [opts.default]);
93-
const toggleFold = useCallback(() => setManualFolded((v) => !v), []);
9494
const expand = useCallback(() => {
9595
setAutoFolded(false);
9696
setManualFolded(false);
@@ -100,11 +100,9 @@ export function useColumnLayout(key: string, opts: Options): ColumnLayout {
100100
width,
101101
folded: autoFolded || manualFolded,
102102
manuallyFolded: manualFolded,
103-
setWidth,
104103
resizeBy,
105104
resetWidth,
106105
setManualFolded,
107-
toggleFold,
108106
expand,
109-
}), [width, autoFolded, manualFolded, setWidth, resizeBy, resetWidth, toggleFold, expand]);
107+
}), [width, autoFolded, manualFolded, resizeBy, resetWidth, expand]);
110108
}

0 commit comments

Comments
 (0)