use super::build_script_directives::BuildScriptDirectives;
use crate::core::hash::{bytes_to_hex, Sha256};
use anyhow::Result;
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuildScriptPurity {
pub is_pure: bool,
pub out_dir_hash: String,
pub reason: Option<String>,
}
pub fn extract_out_dir_files_from_dep_info(
staging_dir: &Path,
out_dir: &Path,
clean_crate: &str,
ws_root: Option<&Path>,
) -> Option<HashSet<String>> {
let entries = fs::read_dir(staging_dir).ok()?;
let d_files: Vec<PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| p.is_file() && p.extension().is_some_and(|ext| ext == "d"))
.collect();
if d_files.is_empty() {
return None;
}
let cleaned_out = super::unit_inputs::clean_path(out_dir);
let cleaned_staging = super::unit_inputs::clean_path(staging_dir);
let prefixes = [
cleaned_out,
cleaned_staging
.join("debug")
.join("build")
.join(clean_crate)
.join("out"),
cleaned_staging
.join("release")
.join("build")
.join(clean_crate)
.join("out"),
cleaned_staging.join("out"),
PathBuf::from(format!("/mitts/out/{clean_crate}")),
];
let mut referenced = HashSet::new();
for d_path in d_files {
let parsed = super::dep_info::parse_dep_info_full(&d_path, ws_root).ok()?;
for file_path in parsed.files {
let cleaned = super::unit_inputs::clean_path(&file_path);
for prefix in &prefixes {
if let Ok(rel) = cleaned.strip_prefix(prefix) {
if !rel.as_os_str().is_empty() {
referenced.insert(rel.to_string_lossy().to_string());
break;
}
}
}
}
}
Some(referenced)
}
pub fn assess_build_script_purity(
pkg_dir: &Path,
ws_root: Option<&Path>,
staging_out_dir: &Path,
target_dir: Option<&Path>,
cas_root: Option<&Path>,
directives: &BuildScriptDirectives,
) -> Result<BuildScriptPurity> {
assess_build_script_purity_with_dep_info(
pkg_dir,
ws_root,
staging_out_dir,
target_dir,
cas_root,
directives,
None,
)
}
pub fn assess_build_script_purity_with_dep_info(
pkg_dir: &Path,
ws_root: Option<&Path>,
staging_out_dir: &Path,
target_dir: Option<&Path>,
cas_root: Option<&Path>,
directives: &BuildScriptDirectives,
referenced_out_files: Option<&HashSet<String>>,
) -> Result<BuildScriptPurity> {
let mut forbidden_needles: Vec<Vec<u8>> = Vec::new();
let mut add_needle = |p: &Path| {
let s = p.to_string_lossy();
if s.len() >= 3 {
forbidden_needles.push(s.as_bytes().to_vec());
}
};
if let Some(ws) = ws_root {
add_needle(ws);
if pkg_dir.starts_with(ws) {
add_needle(pkg_dir);
}
} else {
add_needle(pkg_dir);
}
add_needle(staging_out_dir);
if let Some(td) = target_dir {
add_needle(td);
}
let mut home_needle_opt: Option<Vec<u8>> = None;
let mut allowed_home_prefixes: Vec<Vec<u8>> = Vec::new();
if let Some(cas) = cas_root {
add_needle(&cas.join("tmp"));
let cache_root = cas.parent().unwrap_or(cas);
let root_bytes = cache_root.to_string_lossy().as_bytes().to_vec();
if !root_bytes.is_empty() {
allowed_home_prefixes.push(root_bytes);
}
let cas_bytes = cas.to_string_lossy().as_bytes().to_vec();
if !cas_bytes.is_empty() {
allowed_home_prefixes.push(cas_bytes);
}
}
if let Ok(home) = std::env::var("HOME") {
if home.len() >= 3 {
home_needle_opt = Some(home.as_bytes().to_vec());
if let Ok(mitts_cache) = std::env::var("MITTS_CACHE_DIR") {
if !mitts_cache.is_empty() {
allowed_home_prefixes.push(mitts_cache.into_bytes());
}
} else {
allowed_home_prefixes.push(format!("{}/.cache/mitts", home).into_bytes());
}
if let Ok(cargo_home) = std::env::var("CARGO_HOME") {
if !cargo_home.is_empty() {
allowed_home_prefixes.push(format!("{}/registry", cargo_home).into_bytes());
allowed_home_prefixes.push(format!("{}/git", cargo_home).into_bytes());
}
} else {
allowed_home_prefixes.push(format!("{}/.cargo/registry", home).into_bytes());
allowed_home_prefixes.push(format!("{}/.cargo/git", home).into_bytes());
}
if let Ok(rustup_home) = std::env::var("RUSTUP_HOME") {
if !rustup_home.is_empty() {
allowed_home_prefixes.push(rustup_home.into_bytes());
}
} else {
allowed_home_prefixes.push(format!("{}/.rustup", home).into_bytes());
}
}
}
let check_bytes = |bytes: &[u8]| -> Option<String> {
for needle in &forbidden_needles {
if bytes.windows(needle.len()).any(|w| w == needle.as_slice()) {
return Some(String::from_utf8_lossy(needle).to_string());
}
}
if let Some(ref home_needle) = home_needle_opt {
let h_len = home_needle.len();
let mut offset = 0;
while offset + h_len <= bytes.len() {
if &bytes[offset..offset + h_len] == home_needle.as_slice() {
let mut is_allowed = false;
for allowed in &allowed_home_prefixes {
if bytes[offset..].starts_with(allowed.as_slice()) {
is_allowed = true;
offset += allowed.len();
break;
}
}
if !is_allowed {
return Some(String::from_utf8_lossy(home_needle).to_string());
}
} else {
offset += 1;
}
}
}
None
};
for (label, list) in [
("rustc-cfg", &directives.cfgs),
("rustc-flag", &directives.flags),
("rustc-link-lib", &directives.link_libs),
("rustc-link-arg", &directives.link_args),
] {
for item in list {
if let Some(leak) = check_bytes(item.as_bytes()) {
return Ok(BuildScriptPurity {
is_pure: false,
out_dir_hash: String::new(),
reason: Some(format!("{label} contains absolute path needle '{leak}'")),
});
}
}
}
for s in &directives.link_searches {
let path_str = if let Some((_kind, path)) = s.split_once('=') {
path
} else {
s.as_str()
};
let p = Path::new(path_str);
let is_out_dir = p == staging_out_dir
|| p.starts_with(staging_out_dir)
|| directives
.out_dir
.as_deref()
.is_some_and(|d| p == d || p.starts_with(d))
|| cas_root.is_some_and(|cas| p.starts_with(cas.join("build_scripts")))
|| (p.ends_with("out") && p.to_string_lossy().contains("build_scripts"));
if is_out_dir {
continue;
}
if let Some(leak) = check_bytes(s.as_bytes()) {
return Ok(BuildScriptPurity {
is_pure: false,
out_dir_hash: String::new(),
reason: Some(format!(
"rustc-link-search contains absolute path needle '{}'",
leak
)),
});
}
}
for (k, v) in &directives.envs {
let v_scrubbed = {
let mut s = v.clone();
let staging_str = staging_out_dir.to_string_lossy();
if s.contains(staging_str.as_ref()) {
s = s.replace(staging_str.as_ref(), "<OUT_DIR>");
}
if let Some(ref d) = directives.out_dir {
let d_str = d.to_string_lossy();
if s.contains(d_str.as_ref()) {
s = s.replace(d_str.as_ref(), "<OUT_DIR>");
}
}
s
};
if let Some(leak) = check_bytes(k.as_bytes()).or_else(|| check_bytes(v_scrubbed.as_bytes()))
{
return Ok(BuildScriptPurity {
is_pure: false,
out_dir_hash: String::new(),
reason: Some(format!(
"rustc-env contains absolute path needle '{}'",
leak
)),
});
}
}
let mut out_dir_entries: Vec<(String, String, String)> = Vec::new();
let mut leak_detected: Option<String> = None;
if staging_out_dir.is_dir() {
let mut stack: Vec<PathBuf> = vec![staging_out_dir.to_path_buf()];
while let Some(dir) = stack.pop() {
let read_res = fs::read_dir(&dir);
if let Ok(entries) = read_res {
let mut dir_empty = true;
for entry in entries.flatten() {
dir_empty = false;
let path = entry.path();
let file_type = entry.file_type();
let is_symlink = file_type.as_ref().map(|t| t.is_symlink()).unwrap_or(false);
let rel = path
.strip_prefix(staging_out_dir)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
let should_check_leak = match referenced_out_files {
Some(set) => set.contains(&rel),
None => true,
};
if is_symlink {
let target = fs::read_link(&path).unwrap_or_default();
let target_str = target.to_string_lossy().to_string();
if should_check_leak {
if let Some(leak) = check_bytes(target_str.as_bytes()) {
leak_detected = Some(format!(
"Symlink target '{}' at '{}' contains absolute path needle '{}'",
target_str, rel, leak
));
} else if path.is_file() {
let target_bytes = fs::read(&path).unwrap_or_default();
if let Some(leak) = check_bytes(&target_bytes) {
leak_detected = Some(format!(
"File '{}' in OUT_DIR contains absolute path needle '{}'",
rel, leak
));
}
}
}
let target_hash = Sha256::digest(target_str.as_bytes());
out_dir_entries.push((rel, "symlink".to_string(), target_hash));
} else if path.is_dir() {
stack.push(path);
} else if path.is_file() {
if rel.ends_with(".d") && !should_check_leak {
continue;
}
let content_bytes = fs::read(&path).unwrap_or_default();
if should_check_leak {
if let Some(leak) = check_bytes(&content_bytes) {
leak_detected = Some(format!(
"File '{}' in OUT_DIR contains absolute path needle '{}'",
rel, leak
));
}
}
let content_hash = Sha256::digest(&content_bytes);
out_dir_entries.push((rel, "file".to_string(), content_hash));
}
}
if dir_empty && dir != *staging_out_dir {
let rel = dir
.strip_prefix(staging_out_dir)
.unwrap_or(&dir)
.to_string_lossy()
.to_string();
out_dir_entries.push((rel, "dir".to_string(), String::new()));
}
}
}
}
out_dir_entries.sort_by(|a, b| a.0.cmp(&b.0));
let mut out_hasher = Sha256::new();
out_hasher.update(b"out_dir_entries_v1:\n");
for (rel, kind, h) in &out_dir_entries {
for chunk in [
b"path:" as &[u8],
rel.as_bytes(),
b":kind:",
kind.as_bytes(),
b":hash:",
h.as_bytes(),
b"\n",
] {
out_hasher.update(chunk);
}
}
let out_dir_hash = bytes_to_hex(&out_hasher.finalize());
Ok(BuildScriptPurity {
is_pure: leak_detected.is_none(),
out_dir_hash,
reason: leak_detected,
})
}