use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
pub fn discover_owned_chromium_tmp_side_channels(
profile: Option<&Path>,
chrome_pid: Option<u32>,
not_before: SystemTime,
) -> Vec<PathBuf> {
let mut out = Vec::new();
let tmp = std::env::temp_dir();
let Ok(entries) = std::fs::read_dir(&tmp) else {
return out;
};
let pid_s = chrome_pid.map(|p| p.to_string());
let profile_s = profile.map(|p| p.display().to_string());
for ent in entries.flatten() {
let name = ent.file_name();
let name = name.to_string_lossy();
let is_chromium_tmp = name.starts_with("org.chromium.Chromium.")
|| name.starts_with(".org.chromium.Chromium.")
|| name.starts_with(".com.google.Chrome.")
|| name.starts_with("com.google.Chrome.");
let is_cli_marker = name.starts_with("browser-automation-cli-chrome-");
if !is_chromium_tmp && !is_cli_marker {
continue;
}
let path = ent.path();
if !owned_by_current_user(&path) {
continue;
}
if !created_or_modified_after(&path, not_before) {
continue;
}
if is_cli_marker {
out.push(path);
continue;
}
if let Some(ref pid) = pid_s {
if path_references(&path, pid) {
out.push(path);
continue;
}
}
if let Some(ref prof) = profile_s {
if path_references(&path, prof) {
out.push(path);
continue;
}
}
if age_since(not_before) < Duration::from_secs(5) {
if let Ok(meta) = path.metadata() {
if meta.len() <= 4096 {
out.push(path);
}
}
}
}
out
}
pub fn list_cli_chrome_marker_dirs() -> Vec<PathBuf> {
let mut out = Vec::new();
let tmp = std::env::temp_dir();
let Ok(entries) = std::fs::read_dir(tmp) else {
return out;
};
for ent in entries.flatten() {
let name = ent.file_name();
let name = name.to_string_lossy();
if name.starts_with("browser-automation-cli-chrome-") {
out.push(ent.path());
}
}
out
}
pub fn scavenge_owned_chromium_tmp_orphans(
profile: Option<&Path>,
chrome_pid: Option<u32>,
not_before: SystemTime,
) -> Vec<PathBuf> {
let candidates = discover_owned_chromium_tmp_side_channels(profile, chrome_pid, not_before);
let mut wiped = Vec::new();
for path in candidates {
if path_has_live_process(&path) {
continue;
}
let name = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let is_cli_marker = name.starts_with("browser-automation-cli-chrome-");
let is_singletonish = is_singleton_only_or_small(&path);
if is_cli_marker || is_singletonish {
wipe_path(&path);
wiped.push(path);
}
}
wiped
}
fn is_singleton_only_or_small(path: &Path) -> bool {
if !path.is_dir() {
if let Ok(meta) = path.metadata() {
return meta.len() <= 4096;
}
return false;
}
let Ok(entries) = std::fs::read_dir(path) else {
return false;
};
let mut count = 0usize;
let mut only_singleton = true;
for ent in entries.flatten() {
count += 1;
if count > 8 {
return false;
}
let n = ent.file_name();
let n = n.to_string_lossy();
if !(n.starts_with("Singleton")
|| n == "DevToolsActivePort"
|| n.starts_with(".org.chromium")
|| n.ends_with(".lock"))
{
only_singleton = false;
}
}
only_singleton || count == 0
}
fn path_has_live_process(path: &Path) -> bool {
let needle = path.display().to_string();
#[cfg(unix)]
{
let Ok(proc) = std::fs::read_dir("/proc") else {
return false;
};
for ent in proc.flatten() {
let name = ent.file_name();
let name = name.to_string_lossy();
if !name.chars().all(|c| c.is_ascii_digit()) {
continue;
}
let cmdline = ent.path().join("cmdline");
if let Ok(bytes) = std::fs::read(cmdline) {
let s = String::from_utf8_lossy(&bytes);
if s.contains(&needle) {
return true;
}
}
}
false
}
#[cfg(not(unix))]
{
let _ = needle;
false
}
}
fn wipe_path(path: &Path) {
if !path.exists() {
return;
}
if path.is_dir() {
let _ = std::fs::remove_dir_all(path);
} else {
let _ = std::fs::remove_file(path);
}
}
fn owned_by_current_user(path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let Ok(meta) = path.metadata() else {
return false;
};
meta.uid() == unsafe { libc::getuid() }
}
#[cfg(not(unix))]
{
let _ = path;
true
}
}
fn created_or_modified_after(path: &Path, not_before: SystemTime) -> bool {
let Ok(meta) = path.metadata() else {
return false;
};
let modified = meta.modified().ok();
let created = meta.created().ok();
let skew = Duration::from_secs(2);
let threshold = not_before.checked_sub(skew).unwrap_or(not_before);
if let Some(m) = modified {
if m >= threshold {
return true;
}
}
if let Some(c) = created {
if c >= threshold {
return true;
}
}
false
}
fn path_references(path: &Path, needle: &str) -> bool {
if path.display().to_string().contains(needle) {
return true;
}
if let Ok(target) = std::fs::read_link(path) {
if target.to_string_lossy().contains(needle) {
return true;
}
}
if let Ok(meta) = path.metadata() {
if meta.is_file() && meta.len() <= 4096 {
if let Ok(bytes) = std::fs::read(path) {
if String::from_utf8_lossy(&bytes).contains(needle) {
return true;
}
}
}
}
false
}
fn age_since(t: SystemTime) -> Duration {
SystemTime::now().duration_since(t).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::time::SystemTime;
#[test]
fn marker_scan_finds_and_filters() {
let tmp = std::env::temp_dir();
let dir = tmp.join(format!(
"browser-automation-cli-chrome-test-{}",
uuid::Uuid::new_v4()
));
let _ = fs::create_dir_all(&dir);
let found = list_cli_chrome_marker_dirs();
assert!(
found.iter().any(|p| p == &dir),
"expected marker dir in list"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn discover_respects_not_before() {
let future = SystemTime::now() + Duration::from_secs(3600);
let found = discover_owned_chromium_tmp_side_channels(None, None, future);
assert!(
found.is_empty(),
"future not_before must yield no side channels"
);
}
}