fix(search): prevent crash on invalid regex input in search box

Closes #5609
This commit is contained in:
Slinetrac 2025-11-26 18:30:49 +08:00
parent cbd8c89097
commit a572708fd8
No known key found for this signature in database
2 changed files with 68 additions and 35 deletions

View File

@ -20,6 +20,7 @@
- 修复侧边栏可能的未能正确跳转
- 修复解锁测试部分地区图标编码不正确
- 修复 IP 检测切页后强制刷新,改为仅在必要时更新
- 修复在搜索框输入不完整正则直接崩溃
<details>
<summary><strong> ✨ 新增功能 </strong></summary>

View File

@ -50,6 +50,20 @@ export const BaseSearchBox = ({
onSearch,
}: SearchProps) => {
const { t } = useTranslation();
const escapeRegex = useCallback((value: string) => {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}, []);
const buildRegex = useCallback((pattern: string, flags = "") => {
try {
return new RegExp(pattern, flags);
} catch (e) {
console.warn("[BaseSearchBox] buildRegex error:", e);
return null;
}
}, []);
const inputRef = useRef<HTMLInputElement>(null);
const onSearchRef = useRef(onSearch);
const [matchCase, setMatchCase] = useState(defaultMatchCase);
@ -69,47 +83,63 @@ export const BaseSearchBox = ({
};
// Helper that verifies whether a pattern is a valid regular expression
const validateRegex = useCallback((pattern: string) => {
if (!pattern) return true;
try {
new RegExp(pattern);
return true;
} catch (e) {
console.warn("[BaseSearchBox] validateRegex error:", e);
return false;
}
}, []);
const validateRegex = useCallback(
(pattern: string, flags = "") => {
if (!pattern) return true;
return !!buildRegex(pattern, flags);
},
[buildRegex],
);
const createMatcher = useMemo(() => {
return (searchText: string) => {
if (useRegularExpression && searchText) {
const isValid = validateRegex(searchText);
if (!isValid) {
// Invalid regex should result in no match
return () => false;
}
if (!searchText) {
return () => true;
}
const flags = matchCase ? "" : "i";
if (useRegularExpression) {
const regex = buildRegex(searchText, flags);
if (!regex) return () => false;
return (content: string) => {
try {
return regex.test(content);
} catch (e) {
console.warn("[BaseSearchBox] regex match error:", e);
return false;
}
};
}
if (matchWholeWord) {
const regex = buildRegex(`\\b${escapeRegex(searchText)}\\b`, flags);
if (!regex) return () => false;
return (content: string) => {
try {
return regex.test(content);
} catch (e) {
console.warn("[BaseSearchBox] whole word match error:", e);
return false;
}
};
}
return (content: string) => {
if (!searchText) {
return true;
}
const item = !matchCase ? content.toLowerCase() : content;
const searchItem = !matchCase ? searchText.toLowerCase() : searchText;
if (useRegularExpression) {
return new RegExp(searchItem).test(item);
}
if (matchWholeWord) {
return new RegExp(`\\b${searchItem}\\b`).test(item);
}
return item.includes(searchItem);
const item = matchCase ? content : content.toLowerCase();
const target = matchCase ? searchText : searchText.toLowerCase();
return item.includes(target);
};
};
}, [matchCase, matchWholeWord, useRegularExpression, validateRegex]);
}, [
buildRegex,
escapeRegex,
matchCase,
matchWholeWord,
useRegularExpression,
]);
useEffect(() => {
onSearchRef.current = onSearch;
@ -130,10 +160,11 @@ export const BaseSearchBox = ({
const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target?.value ?? "";
setErrorMessage("");
const flags = matchCase ? "" : "i";
// Validate regex input eagerly
if (useRegularExpression && value) {
const isValid = validateRegex(value);
const isValid = validateRegex(value, flags);
if (!isValid) {
setErrorMessage(t("shared.validation.invalidRegex"));
}
@ -155,7 +186,8 @@ export const BaseSearchBox = ({
setErrorMessage("");
} else {
const value = inputRef.current?.value ?? "";
if (value && !validateRegex(value)) {
const flags = matchCase ? "" : "i";
if (value && !validateRegex(value, flags)) {
setErrorMessage(t("shared.validation.invalidRegex"));
}
}