refactor(unwrap): 消除非测试代码中的裸 .unwrap(),改为带不变量说明的 .expect()
panic="abort" 下任何 panic 直接杀进程,与 unwrap 的「稍后处理」语义冲突。
本轮把所有可证不变量的裸 unwrap 改写为带说明的 expect,并修复 time.rs 中
u32→i32 转换在大 ms 值下的潜在溢出 panic(clamp 到 i32::MAX)。
- src/utils/time.rs: sleep_ms 的 ms.try_into().unwrap() 改为 clamp 到
i32::MAX,避免超大延时值溢出;window/set_timeout 的 expect 补充上下文说明
- src/utils/text.rs: 6 个静态正则的 unwrap 改为 expect,统一说明「编译期校验」
- src/api/{auth,comments/helpers}.rs: EMAIL_REGEX 同上
- src/api/markdown.rs: IMG_RE / TABLE_RE 同上
- src/api/rate_limit.rs: NonZeroU32::new(val.max(1)) 与 Quota::with_period
补充不变量说明
- src/api/image.rs: HeaderValue::from_str(&etag).unwrap() 改为 expect 说明
etag 仅含 ASCII hex;复用 etag_value 避免 clone
- src/middleware.rs: 静态 302 重定向响应的 unwrap 改为 expect
- src/components/post/post_content.rs、src/theme.rs: WASM 上下文 window().unwrap()
改为 expect 说明仅浏览器执行
保留的 unwrap/expect(合理不变量,已在注释中说明):
- DB_POOL LazyLock 闭包(已有详细注释 + validate_database_url 前置校验)
- csrf.rs 静态 forbidden 响应
- highlight.rs syntect 内置 Plain Text 语法
- bin/generate_highlight_css 构建工具
- comments/* 的 #[cfg(not(server))] unreachable!() WASM stub
This commit is contained in:
parent
d1013dfa08
commit
690e00e194
@ -36,7 +36,8 @@ fn validate_username(username: &str) -> Result<(), String> {
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
static EMAIL_REGEX: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
|
||||
regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()
|
||||
regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
||||
.expect("EMAIL_REGEX 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
|
||||
@ -93,7 +93,8 @@ pub fn validate_comment_name(name: &str) -> Result<(), String> {
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
static EMAIL_REGEX: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
|
||||
regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()
|
||||
regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
||||
.expect("EMAIL_REGEX 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
/// 校验评论作者邮箱格式。
|
||||
|
||||
@ -252,6 +252,10 @@ fn image_response(
|
||||
headers: &HeaderMap,
|
||||
) -> Response {
|
||||
let etag = etag_for(&data);
|
||||
// etag 形如 `"deadbeef..."`(引号 + hex),都是合法 token-char,from_str 不可能失败。
|
||||
// 用 expect 说明恒成立的不变量,避免裸 unwrap 触发 lint。
|
||||
let etag_value = HeaderValue::from_str(&etag)
|
||||
.expect("etag 仅含 ASCII hex 与双引号,必然是合法的 HeaderValue");
|
||||
|
||||
if let Some(if_none_match) = headers
|
||||
.get(header::IF_NONE_MATCH)
|
||||
@ -261,7 +265,7 @@ fn image_response(
|
||||
return (
|
||||
StatusCode::NOT_MODIFIED,
|
||||
[
|
||||
(header::ETAG, HeaderValue::from_str(&etag).unwrap()),
|
||||
(header::ETAG, etag_value.clone()),
|
||||
(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static(cache_control),
|
||||
@ -286,7 +290,7 @@ fn image_response(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static(cache_control),
|
||||
),
|
||||
(header::ETAG, HeaderValue::from_str(&etag).unwrap()),
|
||||
(header::ETAG, etag_value),
|
||||
(
|
||||
header::X_CONTENT_TYPE_OPTIONS,
|
||||
HeaderValue::from_static("nosniff"),
|
||||
|
||||
@ -423,7 +423,8 @@ where
|
||||
// 匹配 pulldown-cmark 产出的 <img src="..." alt="..." /> 或 <img src="..." alt="...">
|
||||
// pulldown-cmark 格式可控:src 在前,alt 在后,属性用双引号
|
||||
static IMG_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r#"<img\s+src="(/uploads/[^"]+)"(?:\s+alt="([^"]*)")?\s*/?>"#).unwrap()
|
||||
Regex::new(r#"<img\s+src="(/uploads/[^"]+)"(?:\s+alt="([^"]*)")?\s*/?>"#)
|
||||
.expect("IMG_RE 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
IMG_RE
|
||||
@ -476,8 +477,10 @@ fn wrap_tables(html: &str) -> String {
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static TABLE_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?s)<table(\s[^>]*)?>.*?</table>").unwrap());
|
||||
static TABLE_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(?s)<table(\s[^>]*)?>.*?</table>")
|
||||
.expect("TABLE_RE 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
TABLE_RE
|
||||
.replace_all(html, |caps: ®ex::Captures| {
|
||||
|
||||
@ -28,7 +28,8 @@ fn env_or(key: &str, default: u32) -> NonZeroU32 {
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(default);
|
||||
NonZeroU32::new(val.max(1)).unwrap()
|
||||
// val.max(1) 保证 ≥ 1,NonZeroU32::new 必然 Some;expect 说明该不变量。
|
||||
NonZeroU32::new(val.max(1)).expect("val.max(1) 保证非零,NonZeroU32::new 不可能失败")
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
@ -82,7 +83,7 @@ static CODE_EXEC_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::
|
||||
static CODE_EXEC_DAILY_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
|
||||
RateLimiter::keyed(
|
||||
Quota::with_period(Duration::from_secs(86_400))
|
||||
.unwrap()
|
||||
.expect("with_period 仅在 Duration 为 0 时返回 None;86_400s 必然 Some")
|
||||
.allow_burst(env_or("RATE_LIMIT_CODE_EXEC_DAILY", 50)),
|
||||
)
|
||||
});
|
||||
|
||||
@ -152,7 +152,8 @@ pub fn PostContent(content_html: String) -> Element {
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use_effect(move || {
|
||||
let window = web_sys::window().unwrap();
|
||||
let window =
|
||||
web_sys::window().expect("post_content use_effect 仅在 WASM 浏览器上下文执行:无 window");
|
||||
|
||||
// 调用 window.__initPostContent('.post-content'):函数不存在时静默跳过
|
||||
// (与旧 eval 中的 if 守卫语义一致)。
|
||||
|
||||
@ -222,7 +222,7 @@ pub(crate) async fn admin_guard(
|
||||
.status(StatusCode::FOUND)
|
||||
.header(header::LOCATION, "/login")
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
.expect("静态 302 重定向响应(合法 status + 固定 header + 空 body)必然构造成功")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -368,7 +368,8 @@ pub fn ThemeToggle() -> Element {
|
||||
let coords = evt.client_coordinates();
|
||||
let x = coords.x;
|
||||
let y = coords.y;
|
||||
let window = web_sys::window().unwrap();
|
||||
let window = web_sys::window()
|
||||
.expect("主题切换回调仅在 WASM 浏览器上下文执行:无 window");
|
||||
let key = "__startThemeTransition".into();
|
||||
if let Ok(fn_val) = js_sys::Reflect::get(&window, &key) {
|
||||
if !fn_val.is_undefined() && !fn_val.is_null() {
|
||||
|
||||
@ -5,27 +5,34 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// 匹配 fenced code block(```...```)的正则。
|
||||
static CODE_BLOCK_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"```[\s\S]*?```").unwrap());
|
||||
static CODE_BLOCK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"```[\s\S]*?```").expect("CODE_BLOCK_RE 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
/// 匹配行内代码(`...`)的正则。
|
||||
static INLINE_CODE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"`[^`]*`").unwrap());
|
||||
static INLINE_CODE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"`[^`]*`").expect("INLINE_CODE_RE 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
/// 匹配 Markdown 链接 `[text](url)` 的正则。
|
||||
static LINK_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\[([^\]]*)\]\([^)]*\)").unwrap());
|
||||
static LINK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"\[([^\]]*)\]\([^)]*\)").expect("LINK_RE 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
/// 匹配 Markdown 标题(# 到 ######)的正则。
|
||||
static HEADING_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"^#{1,6}\s*").unwrap());
|
||||
static HEADING_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"^#{1,6}\s*").expect("HEADING_RE 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
/// 匹配 Markdown 图片 `` 的正则。
|
||||
static IMAGE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"!\[([^\]]*)\]\([^)]*\)").unwrap());
|
||||
static IMAGE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"!\[([^\]]*)\]\([^)]*\)").expect("IMAGE_RE 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
/// 匹配任意空白字符的正则,用于把多个空白合并为单个空格。
|
||||
static WHITESPACE_RE: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"\s+").unwrap());
|
||||
static WHITESPACE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r"\s+").expect("WHITESPACE_RE 正则模式应在编译期通过校验")
|
||||
});
|
||||
|
||||
/// 去除 Markdown 标记,返回近似纯文本。
|
||||
///
|
||||
|
||||
@ -13,18 +13,23 @@ use chrono::DateTime;
|
||||
///
|
||||
/// WASM 端用 `js_sys::Promise` + `web_sys::Window::set_timeout_*` 构造,
|
||||
/// 避免 `js_sys::eval` 字符串求值。全项目统一的 sleep 入口。
|
||||
///
|
||||
/// `setTimeout` 的 delay 参数是 i32,超过 `i32::MAX` 会被浏览器立即触发;这里 clamp
|
||||
/// 到安全上限,既避免 `u32 -> i32` 转换溢出 panic,也贴合 setTimeout 的合法范围。
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn sleep_ms(ms: u32) {
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||
web_sys::window()
|
||||
.expect("no window")
|
||||
// ms 是 u32,setTimeout 接受 i32;clamp 到 i32::MAX(约 24.8 天)避免溢出。
|
||||
let delay = ms.min(i32::MAX as u32) as i32;
|
||||
let window = web_sys::window().expect("sleep_ms 必须在浏览器上下文中调用:无 window");
|
||||
window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
&resolve.unchecked_into(),
|
||||
ms.try_into().unwrap(),
|
||||
delay,
|
||||
)
|
||||
.expect("set_timeout failed");
|
||||
.expect("setTimeout with a number delay cannot fail per WebIDL");
|
||||
});
|
||||
let _ = JsFuture::from(promise).await;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user