#![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 }
}
const STEAM_REGISTRY_KEY: &str = r"Software\Valve\Steam";
const STEAM_REGISTRY_VALUE: &str = "SteamExe";
const PROGRAM_FILES_VARS: [&str; 2] = ["ProgramFiles(x86)", "ProgramFiles"];
const STEAM_INSTALL_DIR: &str = "Steam";
const STEAM_EXE_NAME: &str = "steam.exe";
const STEAM_FALLBACK: &str = "steam";
fn steam_exe_candidates(roots: &[PathBuf]) -> Vec<PathBuf> {
roots
.iter()
.map(|root| root.join(STEAM_INSTALL_DIR).join(STEAM_EXE_NAME))
.collect()
}
fn first_existing(candidates: &[PathBuf]) -> Option<PathBuf> {
candidates
.iter()
.find(|candidate| candidate.is_file())
.cloned()
}
fn install_roots() -> Vec<PathBuf> {
if !cfg!(windows) {
return Vec::new();
}
PROGRAM_FILES_VARS
.iter()
.filter_map(std::env::var_os)
.map(PathBuf::from)
.collect()
}
#[cfg(windows)]
fn registry_steam_exe() -> Option<PathBuf> {
let key = windows_registry::CURRENT_USER
.open(STEAM_REGISTRY_KEY)
.ok()?;
let recorded = key.get_string(STEAM_REGISTRY_VALUE).ok()?;
(!recorded.trim().is_empty()).then(|| PathBuf::from(recorded))
}
#[cfg(not(windows))]
fn registry_steam_exe() -> Option<PathBuf> {
let _ = (STEAM_REGISTRY_KEY, STEAM_REGISTRY_VALUE);
None
}
fn resolve_steam_exe(registry: Option<PathBuf>, roots: &[PathBuf]) -> PathBuf {
registry
.filter(|path| path.is_file())
.or_else(|| first_existing(&steam_exe_candidates(roots)))
.unwrap_or_else(|| PathBuf::from(STEAM_FALLBACK))
}
pub fn steam_exe() -> PathBuf {
resolve_steam_exe(registry_steam_exe(), &install_roots())
}
#[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 steam_exe_candidates_names_steam_exe_under_each_root_in_order() {
let roots = vec![
PathBuf::from(r"C:\Program Files (x86)"),
PathBuf::from(r"C:\Program Files"),
];
assert_eq!(
steam_exe_candidates(&roots),
vec![
PathBuf::from(r"C:\Program Files (x86)")
.join("Steam")
.join("steam.exe"),
PathBuf::from(r"C:\Program Files")
.join("Steam")
.join("steam.exe"),
]
);
assert!(steam_exe_candidates(&[]).is_empty());
}
#[test]
fn first_existing_returns_the_earliest_candidate_present_on_disk() {
let dir = tempdir().unwrap();
let root = dir.path();
let absent = root.join("absent.exe");
let present = root.join("present.exe");
let also_present = root.join("also_present.exe");
std::fs::write(&present, b"MZ").unwrap();
std::fs::write(&also_present, b"MZ").unwrap();
assert_eq!(
first_existing(&[absent.clone(), present.clone(), also_present]),
Some(present)
);
assert_eq!(first_existing(&[absent]), None);
assert_eq!(first_existing(&[root.to_path_buf()]), None);
}
#[test]
fn resolve_steam_exe_prefers_the_registry_then_the_roots_then_the_fallback() {
let dir = tempdir().unwrap();
let root = dir.path();
let relocated = root.join("D_drive").join("Steam").join("steam.exe");
let standard = root.join("ProgramFilesX86");
let secondary = root.join("ProgramFiles");
std::fs::create_dir_all(relocated.parent().unwrap()).unwrap();
std::fs::create_dir_all(standard.join("Steam")).unwrap();
std::fs::create_dir_all(secondary.join("Steam")).unwrap();
std::fs::write(&relocated, b"MZ").unwrap();
std::fs::write(standard.join("Steam").join("steam.exe"), b"MZ").unwrap();
std::fs::write(secondary.join("Steam").join("steam.exe"), b"MZ").unwrap();
let roots = vec![standard.clone(), secondary.clone()];
assert_eq!(
resolve_steam_exe(Some(relocated.clone()), &roots),
relocated,
"a registry-recorded Steam must win over a standard root"
);
assert_eq!(
resolve_steam_exe(None, &roots),
standard.join("Steam").join("steam.exe"),
"with no registry value the first root wins"
);
assert_eq!(
resolve_steam_exe(Some(root.join("gone").join("steam.exe")), &roots),
standard.join("Steam").join("steam.exe"),
"a stale registry entry must fall through to the roots"
);
assert_eq!(
resolve_steam_exe(None, std::slice::from_ref(&secondary)),
secondary.join("Steam").join("steam.exe"),
"the second root resolves when it is the only one offered"
);
assert_eq!(
resolve_steam_exe(None, &[root.join("empty")]),
PathBuf::from("steam")
);
assert_eq!(resolve_steam_exe(None, &[]), PathBuf::from("steam"));
}
#[test]
fn resolution_is_inert_off_windows() {
if cfg!(windows) {
return;
}
assert!(
install_roots().is_empty(),
"no install roots may be proposed off Windows"
);
assert_eq!(
registry_steam_exe(),
None,
"there is no registry to read off Windows"
);
assert_eq!(
steam_exe(),
PathBuf::from("steam"),
"off Windows the bare program name is correct and must be preserved"
);
}
#[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
);
}
}