Skip to content

Commit 6fb5d58

Browse files
committed
fix(note): 修复词汇表锚点 CJK 多字节字符切片 panic
word_boundary_contains 命中后前进量取 pos+1,CJK 多字节术语(如'告'占 3 字节)时 start 落入字符中间,下轮 lower[start..] 切片 panic,笔记预览任务崩溃。改用 is_char_boundary 前移到下一合法边界,补 4 个回归单测。
1 parent fa56ea5 commit 6fb5d58

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,12 @@ fn first_occurrence_ms(kept: &[crate::types::SessionSegment], term: &str) -> Opt
194194
/// 前后均非 ASCII 字母数字。
195195
///
196196
/// @ai-context: 只检查单字节(UTF-8 多字节字符的高位字节恒非 ASCII 字母数字,
197-
/// 汉字/标点天然通过);find 偏移恒为合法字符边界,无切片 panic。
197+
/// 汉字/标点天然通过);find 偏移恒为合法字符边界。
198198
/// @ai-context: 内部自行大小写折叠(调用方不须预 lower——TD-B 审查后健壮化)。
199+
/// @ai-context: 缺陷修复(2026-08-21):命中后前进量不能是 pos+1——术语自身为
200+
/// CJK 多字节字符(如"告"占 3 字节)时 pos+1 落在字符中间,下轮
201+
/// lower[start..] 切片 panic(预览任务崩溃)。改为前移到下一个
202+
/// 合法字符边界(is_char_boundary 逐字节扫描,最多到串尾=恒合法)。
199203
fn word_boundary_contains(text: &str, term: &str) -> bool {
200204
if term.is_empty() {
201205
return true;
@@ -212,7 +216,12 @@ fn word_boundary_contains(text: &str, term: &str) -> bool {
212216
if before_ok && after_ok {
213217
return true;
214218
}
219+
// 前移到下一个字符边界:pos+1 可能位于 CJK 字符内部(多字节),
220+
// 直接 +1 会在下轮切片 panic;逐字节前移保证 start 恒为合法边界
215221
start = pos + 1;
222+
while start < bytes.len() && !lower.is_char_boundary(start) {
223+
start += 1;
224+
}
216225
if start >= bytes.len() {
217226
break;
218227
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,36 @@ fn word_boundary_empty_term_and_edge_positions() {
448448
assert!(word_boundary_contains("起步AI", "ai"));
449449
}
450450

451+
#[test]
452+
fn word_boundary_cjk_term_after_ascii_no_panic() {
453+
// Arrange/Act/Assert:术语自身为 CJK 多字节字符("告" 占 3 字节)且前邻
454+
// ASCII 字母数字——命中被边界拒绝后须前移到下一字符边界;旧实现 start=
455+
// pos+1 落在多字节中间,下轮 lower[start..] 切片 panic(预览任务崩溃:
456+
// "start byte index 103 ... inside '告' (bytes 102..105)")。
457+
assert!(!word_boundary_contains("A告B", "告"), "前后 ASCII 字母数字 → 边界拒绝");
458+
assert!(
459+
word_boundary_contains("A告 告", "告"),
460+
"第二个命中(空格边界 + 串尾)仍可匹配——前移不丢后续候选"
461+
);
462+
}
463+
464+
#[test]
465+
fn word_boundary_cjk_term_at_high_byte_offset_no_panic() {
466+
// Arrange/Act/Assert:复现线上偏移——'告' 位于字节 102..105,前邻 ASCII,
467+
// 旧实现 start=103 落在字符中间 panic;修复后应返回 false 而非崩溃
468+
let text = "x".repeat(102) + "告B";
469+
assert!(!word_boundary_contains(&text, "告"));
470+
}
471+
472+
#[test]
473+
fn first_occurrence_cjk_term_after_ascii_no_panic() {
474+
// Arrange:kept 段中 CJK 术语前邻 ASCII 字母数字(边界拒绝路径)
475+
let kept = vec![seg(1, 1_000, 2_000, "本段讲 A告B 的定义")];
476+
477+
// Act/Assert:不 panic,边界拒绝 → 无锚点
478+
assert_eq!(super::first_occurrence_ms(&kept, "告"), None);
479+
}
480+
451481
#[test]
452482
fn first_occurrence_anchor_respects_word_boundary() {
453483
// Arrange:段文本含 "AI 提效"(应命中)与 "AIR 质量"(不应命中)

0 commit comments

Comments
 (0)