use std::path::{Path, PathBuf};
const SENSITIVE_WORKSPACE_PREFIXES: &[&str] = &[
"/proc", "/sys", "/dev", "/etc", "/boot", "/root", "/bin", "/sbin", "/usr",
];
fn is_sensitive_workspace_path(path: &Path) -> bool {
SENSITIVE_WORKSPACE_PREFIXES.iter().any(|prefix| {
let p = Path::new(prefix);
path == p || path.starts_with(p)
})
}
pub(super) fn resolve_workspace_allowed_roots(
roots_opt: Option<Vec<String>>,
_run_root: &Path,
) -> Result<Vec<PathBuf>, String> {
let cwd = std::env::current_dir().map_err(|e| format!("无法获取当前工作目录: {}", e))?;
let Some(roots_in) = roots_opt.filter(|v| !v.is_empty()) else {
return Ok(vec![]);
};
let mut out: Vec<PathBuf> = Vec::new();
for s in roots_in {
let s = s.trim();
if s.is_empty() {
continue;
}
let p = Path::new(s);
let joined = if p.is_absolute() {
p.to_path_buf()
} else {
cwd.join(p)
};
let canon = joined
.canonicalize()
.map_err(|e| format!("workspace_allowed_roots 项 {:?} 无法解析为目录: {}", s, e))?;
if !canon.is_dir() {
return Err(format!(
"workspace_allowed_roots 项 {} 不是目录",
canon.display()
));
}
out.push(canon);
}
if out.is_empty() {
return Err(
"workspace_allowed_roots 配置为空:请省略该项(允许任意路径)或至少填写一个有效路径"
.to_string(),
);
}
out.sort();
out.dedup();
Ok(out)
}
fn join_pool_path(raw: &str) -> Result<PathBuf, String> {
let cwd = std::env::current_dir().map_err(|e| format!("无法获取当前工作目录: {}", e))?;
let p = Path::new(raw);
Ok(if p.is_absolute() {
p.to_path_buf()
} else {
cwd.join(p)
})
}
fn canonicalize_web_workspace_pool_dir(joined: PathBuf, raw: &str) -> Result<PathBuf, String> {
if is_sensitive_workspace_path(&joined) {
return Err(format!(
"web_workspace_pool {} 命中敏感系统路径前缀,已拒绝",
joined.display()
));
}
if !joined.exists() {
std::fs::create_dir_all(&joined)
.map_err(|e| format!("web_workspace_pool {:?} 无法创建: {}", raw, e))?;
}
let canon = joined
.canonicalize()
.map_err(|e| format!("web_workspace_pool {:?} 无法解析为目录: {}", raw, e))?;
if !canon.is_dir() {
return Err(format!("web_workspace_pool {} 不是目录", canon.display()));
}
if is_sensitive_workspace_path(&canon) {
return Err(format!(
"web_workspace_pool {} 命中敏感系统路径前缀,已拒绝",
canon.display()
));
}
Ok(canon)
}
pub(super) fn resolve_web_workspace_pool(
pool_opt: Option<String>,
allowed_roots: &[PathBuf],
) -> Result<Option<PathBuf>, String> {
let Some(raw) = pool_opt.filter(|s| !s.trim().is_empty()) else {
return Ok(None);
};
let raw = raw.trim();
if allowed_roots.is_empty() {
return Err(
"配置 web_workspace_pool 时必须同时设置非空的 workspace_allowed_roots(或 CM_WORKSPACE_ALLOWED_ROOTS)"
.to_string(),
);
}
let canon = canonicalize_web_workspace_pool_dir(join_pool_path(raw)?, raw)?;
if !is_within_allowed_roots(&canon, allowed_roots) {
let roots_display = allowed_roots
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"web_workspace_pool {} 不在 workspace_allowed_roots 允许范围内({roots_display})",
canon.display()
));
}
Ok(Some(canon))
}
pub fn is_within_allowed_roots(candidate: &Path, allowed_roots: &[PathBuf]) -> bool {
if allowed_roots.is_empty() {
return true;
}
allowed_roots
.iter()
.any(|root| candidate == root.as_path() || candidate.starts_with(root))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn pool_requires_allowed_roots() {
let err = resolve_web_workspace_pool(Some("/tmp/unused".into()), &[])
.expect_err("must require roots");
assert!(err.contains("workspace_allowed_roots"), "{err}");
}
#[test]
fn pool_rejects_sensitive_prefix() {
let root = tempfile::tempdir().expect("tempdir");
let allowed = vec![root.path().to_path_buf()];
let err = resolve_web_workspace_pool(Some("/etc".into()), &allowed).expect_err("sensitive");
assert!(err.contains("敏感"), "{err}");
}
#[test]
fn pool_creates_and_accepts_under_allowed() {
let root = tempfile::tempdir().expect("tempdir");
let pool = root.path().join("pool");
let allowed = vec![root.path().canonicalize().expect("canon root")];
let got = resolve_web_workspace_pool(Some(pool.display().to_string()), &allowed)
.expect("ok")
.expect("some");
assert!(got.is_dir());
assert!(got.starts_with(&allowed[0]));
assert!(fs::metadata(&got).expect("meta").is_dir());
}
}