use std::path::{Path, PathBuf};
pub fn destination(tool: &str, root: &Path, candidate: &Path) -> Result<PathBuf, String> {
let (output, _) = crate::core::io_boundary::jail_and_check_export_path(tool, candidate, root)?;
let canonical_root = crate::core::pathutil::canonicalize_secure(root)
.map_err(|_| "project root is unavailable".to_string())?;
let inside = output
.parent()
.and_then(|parent| crate::core::pathutil::canonicalize_secure(parent).ok())
.is_some_and(|parent| parent.starts_with(&canonical_root));
if !inside {
return Err("a bundle file must stay inside the project root".to_string());
}
crate::core::pathjail::enforce_writable(&output)?;
Ok(output)
}
pub fn write(tool: &str, root: &Path, candidate: &Path, bytes: &[u8]) -> Result<PathBuf, String> {
let output = destination(tool, root, candidate)?;
crate::core::pathjail_write::write_requested_below(root, candidate, &output, bytes)
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::symlink;
#[test]
fn write_refuses_a_symlinked_bundle_file() {
let _iso = crate::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("proj");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("Cargo.toml"), "[package]\nname=\"p\"\n").unwrap();
let outside = tmp.path().join("outside.xml");
symlink(&outside, root.join("bundle.xml")).unwrap();
let err = write("ctx_pack", &root, &root.join("bundle.xml"), b"<x/>").unwrap_err();
assert!(err.contains("symlink"), "{err}");
assert!(!outside.exists());
let victim = tmp.path().join("victim.xml");
std::fs::write(&victim, "keep").unwrap();
symlink(&victim, root.join("bundle2.xml")).unwrap();
assert!(write("ctx_pack", &root, &root.join("bundle2.xml"), b"<x/>").is_err());
assert_eq!(std::fs::read_to_string(&victim).unwrap(), "keep");
let path = write("ctx_pack", &root, &root.join("ok.xml"), b"<x/>").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"<x/>");
}
}