use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use super::super::normalize_for_policy;
const SYSTEM_ROOT_ENV_VARS: &[&str] = &[
"SystemRoot",
"windir",
"ProgramFiles",
"ProgramFiles(x86)",
"ProgramW6432",
"ProgramData",
"AGENT_TOOLSDIRECTORY",
"RUNNER_TOOL_CACHE",
];
pub(crate) fn system_read_roots() -> Vec<PathBuf> {
static ROOTS: std::sync::OnceLock<Vec<PathBuf>> = std::sync::OnceLock::new();
ROOTS.get_or_init(compute_system_read_roots).clone()
}
fn compute_system_read_roots() -> Vec<PathBuf> {
let path_entries = std::env::var_os("PATH")
.map(|value| std::env::split_paths(&value).collect::<Vec<_>>())
.unwrap_or_default();
let system_entries = SYSTEM_ROOT_ENV_VARS
.iter()
.filter_map(std::env::var_os)
.map(PathBuf::from);
let mut seen = BTreeSet::new();
let mut roots = Vec::new();
for entry in path_entries.into_iter().chain(system_entries) {
if entry.as_os_str().is_empty() {
continue;
}
let normalized = normalize_for_policy(&entry);
if !normalized.is_absolute() || !normalized.is_dir() {
continue;
}
let key = normalized.to_string_lossy().to_ascii_lowercase();
if seen.insert(key) {
roots.push(normalized);
}
}
roots
}
pub(crate) fn hosts_an_executable(dir: &Path) -> bool {
static CACHE: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<PathBuf, bool>>> =
std::sync::OnceLock::new();
let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
if let Ok(map) = cache.lock() {
if let Some(known) = map.get(dir) {
return *known;
}
}
let answer = read_hosts_an_executable(dir);
if let Ok(mut map) = cache.lock() {
map.insert(dir.to_path_buf(), answer);
}
answer
}
fn read_hosts_an_executable(dir: &Path) -> bool {
let extensions = executable_extensions();
let Ok(entries) = std::fs::read_dir(dir) else {
return false;
};
for entry in entries.flatten() {
if !entry
.file_type()
.map(|kind| kind.is_file())
.unwrap_or(false)
{
continue;
}
let name = entry.file_name();
let Some(extension) = Path::new(&name).extension() else {
continue;
};
let extension = format!(".{}", extension.to_string_lossy().to_ascii_uppercase());
if extensions.iter().any(|candidate| *candidate == extension) {
return true;
}
}
false
}
fn executable_extensions() -> Vec<String> {
parse_executable_extensions(&std::env::var("PATHEXT").unwrap_or_default())
}
fn parse_executable_extensions(raw: &str) -> Vec<String> {
let parsed: Vec<String> = raw
.split(';')
.map(|entry| entry.trim().to_ascii_uppercase())
.filter(|entry| entry.starts_with('.') && entry.len() > 1)
.collect();
if parsed.is_empty() {
return [".COM", ".EXE", ".BAT", ".CMD"]
.iter()
.map(|entry| (*entry).to_string())
.collect();
}
parsed
}
pub(crate) fn cached_tree_entry_count(dir: &Path, ceiling: usize) -> Option<usize> {
type Cache = std::sync::Mutex<std::collections::HashMap<(PathBuf, usize), Option<usize>>>;
static CACHE: std::sync::OnceLock<Cache> = std::sync::OnceLock::new();
let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
let key = (dir.to_path_buf(), ceiling);
if let Ok(map) = cache.lock() {
if let Some(known) = map.get(&key) {
return *known;
}
}
let answer = tree_entry_count_within(dir, ceiling);
if let Ok(mut map) = cache.lock() {
map.insert(key, answer);
}
answer
}
pub(crate) fn tree_entry_count_within(dir: &Path, ceiling: usize) -> Option<usize> {
let mut count = 0usize;
let mut pending = vec![dir.to_path_buf()];
while let Some(current) = pending.pop() {
let Ok(entries) = std::fs::read_dir(¤t) else {
continue;
};
for entry in entries.flatten() {
count += 1;
if count > ceiling {
return None;
}
if entry
.file_type()
.map(|kind| kind.is_dir() && !kind.is_symlink())
.unwrap_or(false)
{
pending.push(entry.path());
}
}
}
Some(count)
}
pub(crate) fn broad_system_root(path: &Path) -> bool {
let depth = path
.components()
.filter(|component| {
matches!(
component,
std::path::Component::Normal(_) | std::path::Component::ParentDir
)
})
.count();
if depth == 0 {
return true;
}
let normalized = path.to_string_lossy().to_ascii_lowercase();
let normalized = normalized.trim_end_matches(['\\', '/']).to_string();
SYSTEM_ROOT_ENV_VARS
.iter()
.filter_map(std::env::var_os)
.map(|value| {
normalize_for_policy(Path::new(&value))
.to_string_lossy()
.to_ascii_lowercase()
.trim_end_matches(['\\', '/'])
.to_string()
})
.chain(crate::user_dirs::home_dir().map(|home| {
normalize_for_policy(&home)
.to_string_lossy()
.to_ascii_lowercase()
.trim_end_matches(['\\', '/'])
.to_string()
}))
.any(|root| !root.is_empty() && root == normalized)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_volume_root_is_broad() {
assert!(broad_system_root(Path::new("C:\\")));
assert!(broad_system_root(Path::new("\\")));
}
#[test]
fn a_leaf_under_a_system_prefix_is_not_broad() {
assert!(!broad_system_root(Path::new("C:\\Program Files\\nodejs")));
}
#[test]
fn a_directory_holding_an_executable_is_grant_worthy() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("tool.exe"), b"").expect("write tool");
assert!(hosts_an_executable(dir.path()));
}
#[test]
fn a_directory_holding_no_executable_is_not_grant_worthy() {
let dir = tempfile::tempdir().expect("temp dir");
for name in ["libfoo.lib", "foo.o", "foo.h", "output"] {
std::fs::write(dir.path().join(name), b"").expect("write artifact");
}
assert!(!hosts_an_executable(dir.path()));
}
#[test]
fn an_executable_in_a_subdirectory_does_not_make_the_parent_grant_worthy() {
let dir = tempfile::tempdir().expect("temp dir");
let nested = dir.path().join("nested");
std::fs::create_dir(&nested).expect("create nested");
std::fs::write(nested.join("tool.exe"), b"").expect("write tool");
assert!(!hosts_an_executable(dir.path()));
}
#[test]
fn a_missing_directory_is_not_grant_worthy() {
let dir = tempfile::tempdir().expect("temp dir");
let absent = dir.path().join("does-not-exist");
assert!(!hosts_an_executable(&absent));
}
#[test]
fn an_absent_or_malformed_pathext_falls_back_to_the_windows_default_set() {
for raw in ["", " ", ";;;", "bogus"] {
let extensions = parse_executable_extensions(raw);
assert!(
extensions.iter().any(|entry| entry == ".EXE"),
"PATHEXT {raw:?} produced {extensions:?}, which cannot match an .exe"
);
}
}
#[test]
fn pathext_is_honoured_and_normalized_when_the_host_sets_one() {
let extensions = parse_executable_extensions(".com;.Exe; .Ps1 ;notanext");
assert_eq!(extensions, vec![".COM", ".EXE", ".PS1"]);
}
#[test]
fn a_tree_under_the_ceiling_is_counted_exactly() {
let dir = tempfile::tempdir().expect("temp dir");
let nested = dir.path().join("nested");
std::fs::create_dir(&nested).expect("create nested");
std::fs::write(dir.path().join("a.txt"), b"").expect("write a");
std::fs::write(nested.join("b.txt"), b"").expect("write b");
assert_eq!(tree_entry_count_within(dir.path(), 64), Some(3));
}
#[test]
fn a_tree_over_the_ceiling_is_abandoned_rather_than_counted() {
let dir = tempfile::tempdir().expect("temp dir");
for index in 0..32 {
std::fs::write(dir.path().join(format!("{index}.txt")), b"").expect("write");
}
assert_eq!(tree_entry_count_within(dir.path(), 8), None);
}
#[test]
fn an_unreadable_or_missing_tree_counts_as_empty_rather_than_enormous() {
let dir = tempfile::tempdir().expect("temp dir");
let absent = dir.path().join("does-not-exist");
assert_eq!(tree_entry_count_within(&absent, 64), Some(0));
}
#[test]
fn system_read_roots_are_absolute_directories_without_duplicates() {
let roots = system_read_roots();
let mut seen = BTreeSet::new();
for root in &roots {
assert!(root.is_absolute(), "non-absolute read root {root:?}");
assert!(root.is_dir(), "non-directory read root {root:?}");
assert!(
seen.insert(root.to_string_lossy().to_ascii_lowercase()),
"duplicate read root {root:?}"
);
}
}
}