fix(console): unverbatim 用 to_str 而非 to_string_lossy(不损坏非 UTF-8 路径)
to_string_lossy 会把非法字节替换成 U+FFFD 再重建路径 —— 那是损坏路径, 比不净化更糟:原本只是 node 报 EISDIR,改完变成指向一个不存在的目录。 改为 to_str():非 UTF-8 → None → 原样返回(Windows 上是含未配对代理项的路径, Unix 上是含非法字节的文件名),绝不悄悄改写。 新增守卫 unverbatim_对非_utf8_的_verbatim_路径必须原样返回不得损坏(#[cfg(unix)]): 构造必须同时含 \\?\ 前缀与非法字节才有区分力 —— 不带前缀时新旧实现都走 「原样返回」那一步,测不出差异。 注入故障(退回 to_string_lossy)实测该守卫如期变红;还原后 sha256 逐字节一致。 pnpm test:rust 18/18(+1),clippy 零告警。
This commit is contained in:
1 parent
56f64e1cc1
commit
2437b0e863
2 files changed
+51
-15
No files matched your search
+12
-5
@@ -464,6 +464,9 @@ Windows 侧只有打包(`console-windows.yml`)。于是「本地全绿 + CI
|
||||
- 新增 `unverbatim()`:剥 `\\?\UNC\` → `\\`、剥 `\\?\` → 无前缀,其余原样返回;
|
||||
**纯字符串处理,任何平台都可安全调用** —— 因此可以用 Windows 形态的字符串在
|
||||
macOS / Linux 上直接守住 Windows 分支(铁律 4:跨平台差异不必真上 Windows 才能验)。
|
||||
实现用 `to_str()` 而**不是** `to_string_lossy()`:后者会把非法字节替换成 U+FFFD
|
||||
再重建路径,那是**损坏路径**(把「node 报 EISDIR」换成「指向不存在的路径」,
|
||||
更难排查)。非 UTF-8 一律原样返回。
|
||||
- 新增 `sidecar_command()`:**唯一**把路径交给 node 的地方,内部统一净化。
|
||||
纪律只在这里守一次,单测直接断言它产出的 `program` / `args`(守「最终下发文本」,
|
||||
不是「源码里出现过某个常量」)。
|
||||
@@ -474,12 +477,16 @@ Windows 侧只有打包(`console-windows.yml`)。于是「本地全绿 + CI
|
||||
与上述同源风险。
|
||||
|
||||
**回归**:
|
||||
- 新增 3 条守卫 —— `unverbatim_去掉各类_windows_前缀`(盘符 / UNC / 普通 / POSIX / **幂等**)、
|
||||
- 新增 4 条守卫 —— `unverbatim_去掉各类_windows_前缀`(盘符 / UNC / 普通 / POSIX / **幂等**)、
|
||||
`unverbatim_对_resource_dir_拼接后的路径同样生效`(前缀在目录上、拼完仍在最前)、
|
||||
**`交给_node_的路径必须去掉_verbatim_前缀`**(断言最终 `program` 与 `args` 文本)。
|
||||
- **注入故障实测**:把 `sidecar_command` 里的 `unverbatim` 去掉 → 第三条如期变红,
|
||||
报错正文即 `program 仍带 verbatim 前缀(示例:\\?\C:\…\node.exe)`;还原后 **sha256 逐字节一致**,复跑全绿。
|
||||
- `pnpm test:rust` **17/17**(14 → +3)。
|
||||
**`交给_node_的路径必须去掉_verbatim_前缀`**(断言最终 `program` 与 `args` 文本)、
|
||||
`unverbatim_对非_utf8_的_verbatim_路径必须原样返回不得损坏`(`#[cfg(unix)]`,见上)。
|
||||
- **注入故障实测(两轮)**:
|
||||
① 把 `sidecar_command` 里的 `unverbatim` 去掉 → 第三条如期变红,
|
||||
报错正文即 `program 仍带 verbatim 前缀(示例:\\?\C:\…\node.exe)`;
|
||||
② 把 `unverbatim` 的实现退回 `to_string_lossy()` → 第四条如期变红。
|
||||
两轮还原后均 **sha256 逐字节一致**,复跑全绿。
|
||||
- `pnpm test:rust` **18/18**(14 → +4)。
|
||||
|
||||
> ⚠️ **本项属控制台上壳变更,不是被控端变更**,因此**不触发** `pnpm release patch`。
|
||||
> 但要让用户真正拿到修复,**必须重新构建并分发控制台安装包**。
|
||||
|
||||
@@ -75,17 +75,23 @@ use tokio::sync::oneshot;
|
||||
///
|
||||
/// 纯字符串处理,**任何平台都可安全调用**(非 Windows 上一般原样返回),
|
||||
/// 因此可以用 Windows 形态的字符串在 macOS / Linux 上直接验证 Windows 分支。
|
||||
///
|
||||
/// ## 为什么用 `to_str()` 而不是 `to_string_lossy()`
|
||||
/// 后者会把**非法字节替换成 U+FFFD** 再用替换后的串重建路径 —— 那是**损坏路径**,
|
||||
/// 比不净化更糟:原本只是 node 会报 EISDIR,改完会变成「指向一个不存在的目录」,
|
||||
/// 更难排查。故非 UTF-8 一律**原样返回**(Windows 上是含未配对代理项的路径;
|
||||
/// Unix 上是含非法字节的文件名 —— 两者都不该被我们的「净化」悄悄改写)。
|
||||
pub(crate) fn unverbatim(path: PathBuf) -> PathBuf {
|
||||
let s = path.to_string_lossy();
|
||||
if let Some(rest) = s.strip_prefix(r"\\?\UNC\") {
|
||||
// \\?\UNC\server\share\x → \\server\share\x
|
||||
return PathBuf::from(format!(r"\\{rest}"));
|
||||
}
|
||||
if let Some(rest) = s.strip_prefix(r"\\?\") {
|
||||
// \\?\C:\x → C:\x
|
||||
return PathBuf::from(rest);
|
||||
}
|
||||
path
|
||||
let fixed: Option<PathBuf> = path.to_str().and_then(|s| {
|
||||
let rest = s.strip_prefix(r"\\?\")?;
|
||||
Some(match rest.strip_prefix("UNC\\") {
|
||||
// \\?\UNC\server\share\x → \\server\share\x(UNC 必须还原成双反斜杠)
|
||||
Some(unc) => PathBuf::from(format!(r"\\{unc}")),
|
||||
// \\?\C:\x → C:\x
|
||||
None => PathBuf::from(rest),
|
||||
})
|
||||
});
|
||||
fixed.unwrap_or(path)
|
||||
}
|
||||
|
||||
/// 构造「把 sidecar 拉起来」的那条命令 —— **唯一**把路径交给 node 的地方。
|
||||
@@ -450,6 +456,29 @@ mod tests {
|
||||
assert_eq!(unverbatim(once.clone()), once);
|
||||
}
|
||||
|
||||
/// 反例守卫:**非 UTF-8 路径必须原样返回,不得被 lossy 重建损坏**。
|
||||
///
|
||||
/// ⚠️ 构造必须**同时**含 `\\?\` 前缀与非法字节,否则测不出差异 ——
|
||||
/// 不带前缀时新旧实现都会落到「原样返回」那一步(`to_string_lossy` 也安全),
|
||||
/// 只有在「剥前缀 → `PathBuf::from(rest)`」这一步才会把 U+FFFD 写进路径。
|
||||
/// 若把实现退回 `to_string_lossy()`,本用例立刻变红:那是把「node 报 EISDIR」
|
||||
/// 换成「指向一个不存在的路径」,更难排查 —— 宁可原样返回。
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn unverbatim_对非_utf8_的_verbatim_路径必须原样返回不得损坏() {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
// 0xff / 0xfe 永不出现在合法 UTF-8 序列里;前缀形态按 Windows 写法给全
|
||||
let raw = OsStr::from_bytes(b"\\\\?\\C:\\na-\xff\xfe-weird.mjs");
|
||||
let p = PathBuf::from(raw);
|
||||
assert!(p.to_str().is_none(), "构造出来的路径应确实是非 UTF-8");
|
||||
assert_eq!(
|
||||
unverbatim(p.clone()),
|
||||
p,
|
||||
"非 UTF-8 路径被改写了(很可能退回了 to_string_lossy)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unverbatim_对_resource_dir_拼接后的路径同样生效() {
|
||||
// 真实形态就是「resource_dir().join("sidecar.mjs")」——前缀在目录上,拼完仍在最前
|
||||
|
||||
Reference in new issue
Block a user