use crate::remediation_config::PooledTargetConfig;
use crate::stale_target_reap::is_safe_reap_base;
pub const RCH_CARGO_HOME_BASE_VAR: &str = "RCH_CH_BASE";
pub const RCH_CARGO_HOME_PREFIX: &str = "rch-cargo-home-";
pub fn remote_cargo_home_base_prelude() -> String {
format!(
"{var}=\"${{TMPDIR:-}}\"; \
[ -n \"${{{var}}}\" ] && [ -d \"${{{var}}}\" ] || {var}=/data/tmp; \
[ -d \"${{{var}}}\" ] || {var}=/tmp",
var = RCH_CARGO_HOME_BASE_VAR
)
}
pub fn remote_cargo_home_expr(suffix: &str) -> String {
format!(
"${{{var}}}/{prefix}{suffix}",
var = RCH_CARGO_HOME_BASE_VAR,
prefix = RCH_CARGO_HOME_PREFIX
)
}
pub const RCH_CARGO_CACHE_PREFIX: &str = "rch-cargo-cache-";
pub fn remote_cargo_cache_expr(worker: &str) -> String {
format!(
"${{{var}}}/{prefix}{worker}",
var = RCH_CARGO_HOME_BASE_VAR,
prefix = RCH_CARGO_CACHE_PREFIX
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GcRootSource {
PooledRemoteBase,
PooledStoreBase,
ConfiguredExtra,
CommandLine,
WorkerTempBase,
}
impl GcRootSource {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::PooledRemoteBase => "pooled_remote_base",
Self::PooledStoreBase => "pooled_store_base",
Self::ConfiguredExtra => "configured_extra",
Self::CommandLine => "command_line",
Self::WorkerTempBase => "worker_temp_base",
}
}
#[must_use]
pub const fn origin_hint(self) -> &'static str {
match self {
Self::PooledRemoteBase => "[remediation.pooled_target] remote_base",
Self::PooledStoreBase => "[remediation.pooled_target] store_base",
Self::ConfiguredExtra => "[remediation.pooled_target] gc_extra_roots",
Self::CommandLine => "--root",
Self::WorkerTempBase => "$TMPDIR → /data/tmp → /tmp (resolved on the worker)",
}
}
}
impl std::fmt::Display for GcRootSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub const WORKER_TEMP_BASE_PLACEHOLDER: &str = "<worker temp base>";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GcRoot {
pub path: String,
pub source: GcRootSource,
}
impl GcRoot {
#[must_use]
pub fn is_worker_resolved(&self) -> bool {
self.source == GcRootSource::WorkerTempBase
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GcRootRejection {
pub path: String,
pub source: GcRootSource,
pub reason: String,
}
impl std::fmt::Display for GcRootRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} root {:?} ({}) is unusable: {}",
self.source,
self.path,
self.source.origin_hint(),
self.reason
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GcRootSet {
pub roots: Vec<GcRoot>,
pub rejected: Vec<GcRootRejection>,
}
impl GcRootSet {
#[must_use]
pub fn scan_bases(&self) -> Vec<String> {
self.roots
.iter()
.filter(|r| !r.is_worker_resolved())
.map(|r| r.path.clone())
.collect()
}
#[must_use]
pub fn attribute<'a>(&'a self, path: &str) -> Option<&'a GcRoot> {
self.roots
.iter()
.filter(|root| !root.is_worker_resolved())
.filter(|root| path_is_under(path, &root.path))
.max_by_key(|root| root.path.len())
}
}
#[must_use]
pub fn path_is_under(path: &str, base: &str) -> bool {
let base = base.trim_end_matches('/');
if base.is_empty() {
return false;
}
if path == base {
return true;
}
path.strip_prefix(base)
.is_some_and(|rest| rest.starts_with('/'))
}
fn normalize(path: &str) -> String {
let trimmed = path.trim();
let stripped = trimmed.trim_end_matches('/');
if stripped.is_empty() {
trimmed.to_string()
} else {
stripped.to_string()
}
}
#[must_use]
pub fn derive_gc_roots(pooled: &PooledTargetConfig, cli_roots: &[String]) -> GcRootSet {
let mut roots: Vec<GcRoot> = Vec::new();
let mut rejected: Vec<GcRootRejection> = Vec::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut push = |raw: &str, source: GcRootSource| {
let path = normalize(raw);
if path.is_empty() {
rejected.push(GcRootRejection {
path: raw.to_string(),
source,
reason: "empty path".to_string(),
});
return;
}
if !is_safe_reap_base(&path) {
rejected.push(GcRootRejection {
path: raw.to_string(),
source,
reason: "not an absolute, `..`-free path of at least one segment built from \
[A-Za-z0-9/._-] (it is embedded in a remote shell command)"
.to_string(),
});
return;
}
if seen.insert(path.clone()) {
roots.push(GcRoot { path, source });
}
};
push(&pooled.remote_base, GcRootSource::PooledRemoteBase);
if let Some(store_base) = pooled.store_base.as_deref() {
push(store_base, GcRootSource::PooledStoreBase);
}
for extra in &pooled.gc_extra_roots {
push(extra, GcRootSource::ConfiguredExtra);
}
for cli in cli_roots {
push(cli, GcRootSource::CommandLine);
}
drop(push);
roots.push(GcRoot {
path: WORKER_TEMP_BASE_PLACEHOLDER.to_string(),
source: GcRootSource::WorkerTempBase,
});
GcRootSet { roots, rejected }
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> PooledTargetConfig {
PooledTargetConfig::default()
}
#[test]
fn default_root_set_is_remote_base_plus_worker_temp_base() {
let set = derive_gc_roots(&cfg(), &[]);
assert_eq!(
set.roots,
vec![
GcRoot {
path: "/data/projects".to_string(),
source: GcRootSource::PooledRemoteBase,
},
GcRoot {
path: WORKER_TEMP_BASE_PLACEHOLDER.to_string(),
source: GcRootSource::WorkerTempBase,
},
]
);
assert!(set.rejected.is_empty());
assert_eq!(set.scan_bases(), vec!["/data/projects".to_string()]);
}
#[test]
fn configured_extra_roots_and_cli_roots_are_scanned() {
let mut c = cfg();
c.store_base = Some("/bigdisk/rch-pools".to_string());
c.gc_extra_roots = vec!["/Volumes/EE1/rch".to_string(), "/srv/rch".to_string()];
let set = derive_gc_roots(&c, &["/private/tmp/ee-reality-1".to_string()]);
assert_eq!(
set.scan_bases(),
vec![
"/data/projects".to_string(),
"/bigdisk/rch-pools".to_string(),
"/Volumes/EE1/rch".to_string(),
"/srv/rch".to_string(),
"/private/tmp/ee-reality-1".to_string(),
]
);
assert!(set.rejected.is_empty(), "{:?}", set.rejected);
assert_eq!(
set.roots.last().map(|r| r.source),
Some(GcRootSource::WorkerTempBase),
"the worker temp base is always scanned last"
);
}
#[test]
fn duplicate_roots_collapse_and_keep_first_source() {
let mut c = cfg();
c.store_base = Some("/data/projects/".to_string());
c.gc_extra_roots = vec!["/data/projects".to_string()];
let set = derive_gc_roots(&c, &["/data/projects//".to_string()]);
assert_eq!(set.scan_bases(), vec!["/data/projects".to_string()]);
assert_eq!(set.roots[0].source, GcRootSource::PooledRemoteBase);
}
#[test]
fn unsafe_roots_are_rejected_not_sanitized() {
let c = cfg();
let bad = [
"/",
"relative/path",
"/tmp/../etc",
"/tmp/$(whoami)",
"/tmp/rch; rm -rf x",
"",
" ",
];
for candidate in bad {
let set = derive_gc_roots(&c, &[candidate.to_string()]);
assert_eq!(
set.scan_bases(),
vec!["/data/projects".to_string()],
"{candidate:?} must never become a scan base"
);
assert!(
set.rejected
.iter()
.any(|r| r.source == GcRootSource::CommandLine && r.path == candidate),
"{candidate:?} must be reported as rejected"
);
}
}
#[test]
fn a_rejected_root_does_not_drop_the_valid_ones() {
let set = derive_gc_roots(&cfg(), &["/".to_string(), "/srv/rch".to_string()]);
assert_eq!(
set.scan_bases(),
vec!["/data/projects".to_string(), "/srv/rch".to_string()]
);
assert_eq!(set.rejected.len(), 1);
assert!(set.rejected[0].to_string().contains("unusable"));
}
#[test]
fn attribution_is_longest_prefix_and_segment_aware() {
let mut c = cfg();
c.gc_extra_roots = vec![
"/data/projects/nested".to_string(),
"/data/projects-old".to_string(),
];
let set = derive_gc_roots(&c, &[]);
assert_eq!(
set.attribute("/data/projects/nested/x/.rch-target-w-pool-a")
.map(|r| r.path.as_str()),
Some("/data/projects/nested"),
);
assert_eq!(
set.attribute("/data/projects/x/.rch-target-w-pool-a")
.map(|r| r.path.as_str()),
Some("/data/projects"),
);
assert_eq!(
set.attribute("/data/projects-old/x")
.map(|r| r.path.as_str()),
Some("/data/projects-old"),
"a sibling root must not be swallowed by a prefix match",
);
assert_eq!(set.attribute("/data/tmp/rch-cargo-cache-hz2"), None);
}
#[test]
fn path_is_under_compares_whole_segments() {
assert!(path_is_under("/a/b", "/a/b"));
assert!(path_is_under("/a/b/c", "/a/b"));
assert!(path_is_under("/a/b/c", "/a/b/"));
assert!(!path_is_under("/a/bc", "/a/b"));
assert!(!path_is_under("/a", "/a/b"));
assert!(!path_is_under("/a/b", ""));
assert!(!path_is_under("/a/b", "/"));
}
}