#![deny(missing_docs)]
use std::path::{Path, PathBuf};
const SUSPECT_PREFIXES: [&str; 4] = ["/tmp", "/var/tmp", "/dev/shm", "/run/user"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Visibility {
Reachable,
Unreachable {
steam_pid: u32,
},
Unknown {
suspect: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Ok,
Warn,
Refuse,
}
impl Visibility {
pub fn severity(&self) -> Severity {
match self {
Self::Reachable | Self::Unknown { suspect: false } => Severity::Ok,
Self::Unknown { suspect: true } => Severity::Warn,
Self::Unreachable { .. } => Severity::Refuse,
}
}
}
pub fn suspect_prefix(path: &Path) -> bool {
SUSPECT_PREFIXES
.iter()
.any(|prefix| starts_with_components(path, Path::new(prefix)))
}
fn starts_with_components(path: &Path, prefix: &Path) -> bool {
let mut components = path.components();
for expected in prefix.components() {
match components.next() {
Some(actual) if actual == expected => {}
_ => return false,
}
}
true
}
fn visible_in(namespace_root: &Path, path: &Path) -> bool {
let relative = path.strip_prefix("/").unwrap_or(path);
namespace_root.join(relative).exists()
}
pub fn steam_client_pid() -> Option<u32> {
let home = std::env::var_os("HOME")?;
let pid_file = PathBuf::from(home).join(".steam").join("steam.pid");
let pid: u32 = std::fs::read_to_string(pid_file)
.ok()?
.trim()
.parse()
.ok()?;
let comm = std::fs::read_to_string(format!("/proc/{pid}/comm")).ok()?;
(comm.trim() == "steam").then_some(pid)
}
pub fn visibility(path: &Path) -> Visibility {
let Some(steam_pid) = steam_client_pid() else {
return Visibility::Unknown {
suspect: suspect_prefix(path),
};
};
let namespace_root = PathBuf::from(format!("/proc/{steam_pid}/root"));
if !namespace_root.is_dir() {
return Visibility::Unknown {
suspect: suspect_prefix(path),
};
}
if visible_in(&namespace_root, path) {
return Visibility::Reachable;
}
Visibility::Unreachable { steam_pid }
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn suspect_prefix_matches_whole_components_only() {
assert!(suspect_prefix(Path::new(
"/tmp/t/d/valheim/steam-wrapper.sh"
)));
assert!(suspect_prefix(Path::new("/var/tmp/x")));
assert!(suspect_prefix(Path::new("/dev/shm/x")));
assert!(suspect_prefix(Path::new("/run/user/1000/x")));
assert!(!suspect_prefix(Path::new("/home/u/.local/share/vmm")));
assert!(!suspect_prefix(Path::new("/mnt/games/vmm")));
assert!(!suspect_prefix(Path::new("/tmpfoo/x")));
assert!(!suspect_prefix(Path::new("/var/tmpfoo/x")));
assert!(suspect_prefix(Path::new("/tmp")));
assert!(!suspect_prefix(Path::new("relative/path")));
}
#[test]
fn visible_in_resolves_an_absolute_path_under_the_namespace_root() {
let namespace = tempdir().unwrap();
let root = namespace.path();
std::fs::create_dir_all(root.join("home/u/.local/share/vmm/valheim")).unwrap();
std::fs::write(
root.join("home/u/.local/share/vmm/valheim/steam-wrapper.sh"),
b"#!/bin/sh\n",
)
.unwrap();
assert!(visible_in(
root,
Path::new("/home/u/.local/share/vmm/valheim/steam-wrapper.sh")
));
assert!(!visible_in(
root,
Path::new("/tmp/t/d/valheim/steam-wrapper.sh")
));
assert!(visible_in(root, Path::new("/home/u/.local/share/vmm")));
}
#[test]
fn visible_in_does_not_fall_through_to_the_host_filesystem() {
let namespace = tempdir().unwrap();
let real = tempdir().unwrap();
let outside = real.path().join("wrapper.sh");
std::fs::write(&outside, b"#!/bin/sh\n").unwrap();
assert!(outside.exists(), "the file must exist on the host");
assert!(
!visible_in(namespace.path(), &outside),
"a host path must not be reported visible in a namespace that lacks it"
);
}
#[test]
fn visibility_returns_a_verdict_without_panicking() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home".to_string());
let verdict = visibility(Path::new(&home));
assert!(
!matches!(verdict, Visibility::Unreachable { .. }),
"a home path should never be proven unreachable, got: {verdict:?}"
);
}
#[test]
fn only_a_proven_absence_refuses() {
assert_eq!(Visibility::Reachable.severity(), Severity::Ok);
assert_eq!(
Visibility::Unknown { suspect: false }.severity(),
Severity::Ok
);
assert_eq!(
Visibility::Unknown { suspect: true }.severity(),
Severity::Warn
);
assert_eq!(
Visibility::Unreachable { steam_pid: 1234 }.severity(),
Severity::Refuse
);
}
}