style: apply cargo fmt across the crate (A20)
The repo never enforced rustfmt, so formatting had drifted broadly. This is a single mechanical `cargo fmt` pass over the whole crate (no behavioral change; lib suite green, 493 passed). Going forward fmt should be enforced (planned CI fmt --check step). Part of the 0.6.1 hygiene pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+45
-12
@@ -9,11 +9,9 @@
|
||||
//! is factored into the pure [`poll_once`] so the wiring of resolve + match +
|
||||
//! debounce is unit-tested without any I/O.
|
||||
|
||||
use super::{
|
||||
builtin_denylist, match_processes, resolve, Debouncer, DetectedGame, ManualOverride,
|
||||
};
|
||||
use super::scan;
|
||||
use super::steam::SteamProbe;
|
||||
use super::{Debouncer, DetectedGame, ManualOverride, builtin_denylist, match_processes, resolve};
|
||||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -145,9 +143,14 @@ fn worker_loop(
|
||||
let steam_game = steam.detect();
|
||||
let processes = scan::running_executables();
|
||||
|
||||
if let Some(new_current) =
|
||||
poll_once(&mut debouncer, &override_, steam_game, &processes, &process_map, &denylist)
|
||||
{
|
||||
if let Some(new_current) = poll_once(
|
||||
&mut debouncer,
|
||||
&override_,
|
||||
steam_game,
|
||||
&processes,
|
||||
&process_map,
|
||||
&denylist,
|
||||
) {
|
||||
// A closed receiver means core shut down; stop quietly.
|
||||
if tx.send(new_current).is_err() {
|
||||
return;
|
||||
@@ -169,11 +172,18 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn game(id: &str, name: &str, source: GameSource) -> DetectedGame {
|
||||
DetectedGame { id: id.into(), name: Some(name.into()), source }
|
||||
DetectedGame {
|
||||
id: id.into(),
|
||||
name: Some(name.into()),
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
fn map(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
|
||||
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -185,17 +195,38 @@ mod tests {
|
||||
|
||||
// First poll: detected but not yet published (needs two hits).
|
||||
assert_eq!(
|
||||
poll_once(&mut d, &ManualOverride::Auto, Some(steam.clone()), &[], &empty, &deny),
|
||||
poll_once(
|
||||
&mut d,
|
||||
&ManualOverride::Auto,
|
||||
Some(steam.clone()),
|
||||
&[],
|
||||
&empty,
|
||||
&deny
|
||||
),
|
||||
None
|
||||
);
|
||||
// Second poll: published.
|
||||
assert_eq!(
|
||||
poll_once(&mut d, &ManualOverride::Auto, Some(steam.clone()), &[], &empty, &deny),
|
||||
poll_once(
|
||||
&mut d,
|
||||
&ManualOverride::Auto,
|
||||
Some(steam.clone()),
|
||||
&[],
|
||||
&empty,
|
||||
&deny
|
||||
),
|
||||
Some(Some(steam))
|
||||
);
|
||||
// Third identical poll: no change event.
|
||||
assert_eq!(
|
||||
poll_once(&mut d, &ManualOverride::Auto, Some(game("steam:730", "CS2", GameSource::Steam)), &[], &empty, &deny),
|
||||
poll_once(
|
||||
&mut d,
|
||||
&ManualOverride::Auto,
|
||||
Some(game("steam:730", "CS2", GameSource::Steam)),
|
||||
&[],
|
||||
&empty,
|
||||
&deny
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -209,7 +240,9 @@ mod tests {
|
||||
|
||||
poll_once(&mut d, &ManualOverride::Auto, None, &procs, &user, &deny);
|
||||
let change = poll_once(&mut d, &ManualOverride::Auto, None, &procs, &user, &deny);
|
||||
let published = change.expect("should publish on second hit").expect("a game");
|
||||
let published = change
|
||||
.expect("should publish on second hit")
|
||||
.expect("a game");
|
||||
assert_eq!(published.id, "exe:hl2_linux");
|
||||
assert_eq!(published.name.as_deref(), Some("Half-Life 2"));
|
||||
}
|
||||
|
||||
+48
-17
@@ -92,11 +92,20 @@ pub fn resolve(
|
||||
processes: &[DetectedGame],
|
||||
) -> Resolution {
|
||||
match override_ {
|
||||
ManualOverride::ForceNone => Resolution { game: None, immediate: true },
|
||||
ManualOverride::Force(g) => Resolution { game: Some(g.clone()), immediate: true },
|
||||
ManualOverride::ForceNone => Resolution {
|
||||
game: None,
|
||||
immediate: true,
|
||||
},
|
||||
ManualOverride::Force(g) => Resolution {
|
||||
game: Some(g.clone()),
|
||||
immediate: true,
|
||||
},
|
||||
ManualOverride::Auto => {
|
||||
let game = steam.or_else(|| processes.first().cloned());
|
||||
Resolution { game, immediate: false }
|
||||
Resolution {
|
||||
game,
|
||||
immediate: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,7 +201,11 @@ impl Debouncer {
|
||||
/// lowercase it. Keeps any extension (`minecraft.exe` stays distinct from a
|
||||
/// hypothetical `minecraft`), trims surrounding whitespace.
|
||||
pub fn normalize_exe(raw: &str) -> String {
|
||||
raw.rsplit(['/', '\\']).next().unwrap_or(raw).trim().to_lowercase()
|
||||
raw.rsplit(['/', '\\'])
|
||||
.next()
|
||||
.unwrap_or(raw)
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
}
|
||||
|
||||
/// Launcher/helper executables that must NEVER be reported as a game even if a
|
||||
@@ -245,8 +258,10 @@ pub fn match_processes(
|
||||
denylist: &BTreeSet<&str>,
|
||||
) -> Vec<DetectedGame> {
|
||||
// Normalize the user map once so lookups are basename/case-insensitive.
|
||||
let normalized_map: BTreeMap<String, &String> =
|
||||
user_map.iter().map(|(k, v)| (normalize_exe(k), v)).collect();
|
||||
let normalized_map: BTreeMap<String, &String> = user_map
|
||||
.iter()
|
||||
.map(|(k, v)| (normalize_exe(k), v))
|
||||
.collect();
|
||||
|
||||
let mut seen: BTreeSet<String> = BTreeSet::new();
|
||||
let mut out: Vec<DetectedGame> = Vec::new();
|
||||
@@ -288,8 +303,14 @@ mod tests {
|
||||
#[test]
|
||||
fn stable_ids_are_namespaced() {
|
||||
assert_eq!(DetectedGame::steam_id(730), "steam:730");
|
||||
assert_eq!(DetectedGame::exe_id("/usr/games/hl2_linux"), "exe:hl2_linux");
|
||||
assert_eq!(DetectedGame::exe_id("C:\\Games\\Minecraft.exe"), "exe:minecraft.exe");
|
||||
assert_eq!(
|
||||
DetectedGame::exe_id("/usr/games/hl2_linux"),
|
||||
"exe:hl2_linux"
|
||||
);
|
||||
assert_eq!(
|
||||
DetectedGame::exe_id("C:\\Games\\Minecraft.exe"),
|
||||
"exe:minecraft.exe"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -318,8 +339,16 @@ mod tests {
|
||||
#[test]
|
||||
fn resolve_falls_back_to_first_process_then_none() {
|
||||
let procs = vec![
|
||||
DetectedGame { id: "exe:a".into(), name: Some("A".into()), source: GameSource::Process },
|
||||
DetectedGame { id: "exe:b".into(), name: Some("B".into()), source: GameSource::Process },
|
||||
DetectedGame {
|
||||
id: "exe:a".into(),
|
||||
name: Some("A".into()),
|
||||
source: GameSource::Process,
|
||||
},
|
||||
DetectedGame {
|
||||
id: "exe:b".into(),
|
||||
name: Some("B".into()),
|
||||
source: GameSource::Process,
|
||||
},
|
||||
];
|
||||
let r = resolve(&ManualOverride::Auto, None, &procs);
|
||||
assert_eq!(r.game.as_ref().unwrap().id, "exe:a");
|
||||
@@ -426,7 +455,10 @@ mod tests {
|
||||
// --- process matching --------------------------------------------------
|
||||
|
||||
fn map(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
|
||||
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -461,14 +493,13 @@ mod tests {
|
||||
let user = map(&[("zed", "Zed"), ("alpha", "Alpha")]);
|
||||
let deny = builtin_denylist();
|
||||
// Same game twice (two processes) + reverse discovery order.
|
||||
let running = vec![
|
||||
"/b/zed".into(),
|
||||
"/a/alpha".into(),
|
||||
"/c/alpha".into(),
|
||||
];
|
||||
let running = vec!["/b/zed".into(), "/a/alpha".into(), "/c/alpha".into()];
|
||||
let got = match_processes(&running, &user, &deny);
|
||||
// Deduped to two, sorted by id (alpha before zed) regardless of scan order.
|
||||
assert_eq!(got.iter().map(|g| g.id.as_str()).collect::<Vec<_>>(), vec!["exe:alpha", "exe:zed"]);
|
||||
assert_eq!(
|
||||
got.iter().map(|g| g.id.as_str()).collect::<Vec<_>>(),
|
||||
vec!["exe:alpha", "exe:zed"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+12
-4
@@ -62,7 +62,7 @@ fn linux_proc_executables() -> Vec<String> {
|
||||
fn windows_toolhelp_executables() -> Vec<String> {
|
||||
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
|
||||
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
||||
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
|
||||
TH32CS_SNAPPROCESS,
|
||||
};
|
||||
|
||||
@@ -78,7 +78,11 @@ fn windows_toolhelp_executables() -> Vec<String> {
|
||||
let mut ok = unsafe { Process32FirstW(snapshot, &mut entry) };
|
||||
while ok != 0 {
|
||||
// szExeFile is a NUL-terminated UTF-16 array (the basename, e.g. game.exe).
|
||||
let end = entry.szExeFile.iter().position(|&c| c == 0).unwrap_or(entry.szExeFile.len());
|
||||
let end = entry
|
||||
.szExeFile
|
||||
.iter()
|
||||
.position(|&c| c == 0)
|
||||
.unwrap_or(entry.szExeFile.len());
|
||||
let name = String::from_utf16_lossy(&entry.szExeFile[..end]);
|
||||
if !name.is_empty() {
|
||||
out.push(name);
|
||||
@@ -101,7 +105,10 @@ mod tests {
|
||||
// The test runner itself is a process, so /proc enumeration must be
|
||||
// non-empty and include something that normalizes to our own exe basename.
|
||||
let exes = running_executables();
|
||||
assert!(!exes.is_empty(), "expected to see running processes via /proc");
|
||||
assert!(
|
||||
!exes.is_empty(),
|
||||
"expected to see running processes via /proc"
|
||||
);
|
||||
// Our own /proc/self/exe basename should appear among them.
|
||||
let me = std::fs::read_link("/proc/self/exe")
|
||||
.ok()
|
||||
@@ -109,7 +116,8 @@ mod tests {
|
||||
if let Some(me) = me {
|
||||
let me_norm = super::super::normalize_exe(&me);
|
||||
assert!(
|
||||
exes.iter().any(|e| super::super::normalize_exe(e) == me_norm),
|
||||
exes.iter()
|
||||
.any(|e| super::super::normalize_exe(e) == me_norm),
|
||||
"running list should include our own executable {me_norm:?}"
|
||||
);
|
||||
}
|
||||
|
||||
+57
-20
@@ -25,8 +25,7 @@ const MAX_STEAM_PATH_BYTES: u32 = 4 * 1024;
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
fn validate_reg_len(len: u32) -> Option<usize> {
|
||||
(len != 0 && len.is_multiple_of(2) && len <= MAX_STEAM_PATH_BYTES)
|
||||
.then_some(len as usize / 2)
|
||||
(len != 0 && len.is_multiple_of(2) && len <= MAX_STEAM_PATH_BYTES).then_some(len as usize / 2)
|
||||
}
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
@@ -48,7 +47,14 @@ fn decode_reg_sz(mut buf: Vec<u16>, returned_bytes: u32) -> Option<String> {
|
||||
pub fn parse_running_app_id(registry_vdf: &str) -> Option<u32> {
|
||||
let root = vdf::parse(registry_vdf).ok()?;
|
||||
let raw = root
|
||||
.get_path(&["Registry", "HKCU", "Software", "Valve", "Steam", "RunningAppID"])
|
||||
.get_path(&[
|
||||
"Registry",
|
||||
"HKCU",
|
||||
"Software",
|
||||
"Valve",
|
||||
"Steam",
|
||||
"RunningAppID",
|
||||
])
|
||||
.and_then(Value::as_str)?;
|
||||
let id: u32 = raw.trim().parse().ok()?;
|
||||
(id != 0).then_some(id)
|
||||
@@ -196,7 +202,13 @@ impl SteamProbe {
|
||||
return cached.name.clone();
|
||||
}
|
||||
let name = read_capped(&manifest).and_then(|c| parse_app_name(&c));
|
||||
self.manifests.insert(app_id, CachedManifest { mtime, name: name.clone() });
|
||||
self.manifests.insert(
|
||||
app_id,
|
||||
CachedManifest {
|
||||
mtime,
|
||||
name: name.clone(),
|
||||
},
|
||||
);
|
||||
name
|
||||
}
|
||||
|
||||
@@ -240,7 +252,11 @@ impl SteamProbe {
|
||||
paths.push(root.clone());
|
||||
}
|
||||
}
|
||||
self.libraries = CachedLibraries { source, mtime, paths: paths.clone() };
|
||||
self.libraries = CachedLibraries {
|
||||
source,
|
||||
mtime,
|
||||
paths: paths.clone(),
|
||||
};
|
||||
paths
|
||||
}
|
||||
}
|
||||
@@ -360,8 +376,8 @@ mod win {
|
||||
use std::path::PathBuf;
|
||||
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||
use windows_sys::Win32::System::Registry::{
|
||||
RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY, HKEY_CURRENT_USER, KEY_READ,
|
||||
REG_DWORD, REG_SZ,
|
||||
HKEY, HKEY_CURRENT_USER, KEY_READ, REG_DWORD, REG_SZ, RegCloseKey, RegOpenKeyExW,
|
||||
RegQueryValueExW,
|
||||
};
|
||||
|
||||
/// UTF-16, NUL-terminated, for a Win32 wide-string argument.
|
||||
@@ -374,9 +390,8 @@ mod win {
|
||||
let subkey = wide("Software\\Valve\\Steam");
|
||||
let mut hkey: HKEY = std::ptr::null_mut();
|
||||
// SAFETY: valid HKEY constant, NUL-terminated subkey, out-param for the handle.
|
||||
let rc = unsafe {
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, subkey.as_ptr(), 0, KEY_READ, &mut hkey)
|
||||
};
|
||||
let rc =
|
||||
unsafe { RegOpenKeyExW(HKEY_CURRENT_USER, subkey.as_ptr(), 0, KEY_READ, &mut hkey) };
|
||||
(rc == ERROR_SUCCESS).then_some(hkey)
|
||||
}
|
||||
|
||||
@@ -463,13 +478,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn registry_string_lengths_are_bounded_and_trimmed() {
|
||||
assert_eq!(validate_reg_len(5), None, "odd byte lengths are invalid UTF-16");
|
||||
assert_eq!(
|
||||
validate_reg_len(5),
|
||||
None,
|
||||
"odd byte lengths are invalid UTF-16"
|
||||
);
|
||||
assert_eq!(validate_reg_len(MAX_STEAM_PATH_BYTES + 2), None);
|
||||
assert_eq!(validate_reg_len(8), Some(4));
|
||||
|
||||
let raw = "C:\\Steam\0ignored".encode_utf16().collect::<Vec<_>>();
|
||||
let returned_bytes = ("C:\\Steam\0".encode_utf16().count() * 2) as u32;
|
||||
assert_eq!(decode_reg_sz(raw, returned_bytes).as_deref(), Some("C:\\Steam"));
|
||||
assert_eq!(
|
||||
decode_reg_sz(raw, returned_bytes).as_deref(),
|
||||
Some("C:\\Steam")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -495,10 +517,13 @@ mod tests {
|
||||
"contentstatsid" "12345"
|
||||
}"#;
|
||||
let got = parse_library_paths(current);
|
||||
assert_eq!(got, vec![
|
||||
PathBuf::from("/home/eric/.local/share/Steam"),
|
||||
PathBuf::from("/mnt/games/SteamLibrary"),
|
||||
]);
|
||||
assert_eq!(
|
||||
got,
|
||||
vec![
|
||||
PathBuf::from("/home/eric/.local/share/Steam"),
|
||||
PathBuf::from("/mnt/games/SteamLibrary"),
|
||||
]
|
||||
);
|
||||
|
||||
// Legacy shape: numeric keys map straight to path strings.
|
||||
let legacy = r#""LibraryFolders" {
|
||||
@@ -522,13 +547,25 @@ mod tests {
|
||||
let environ = b"PATH=/usr/bin\0SteamAppId=440\0HOME=/home/x\0SteamGameId=440\0";
|
||||
assert_eq!(parse_steam_app_id_from_environ(environ), Some(440));
|
||||
// Nonzero requirement: SteamAppId=0 (the launcher itself) is ignored.
|
||||
assert_eq!(parse_steam_app_id_from_environ(b"SteamAppId=0\0FOO=bar\0"), None);
|
||||
assert_eq!(
|
||||
parse_steam_app_id_from_environ(b"SteamAppId=0\0FOO=bar\0"),
|
||||
None
|
||||
);
|
||||
// Absent → None (a non-Steam process).
|
||||
assert_eq!(parse_steam_app_id_from_environ(b"PATH=/usr/bin\0HOME=/home/x\0"), None);
|
||||
assert_eq!(
|
||||
parse_steam_app_id_from_environ(b"PATH=/usr/bin\0HOME=/home/x\0"),
|
||||
None
|
||||
);
|
||||
// Not fooled by a different var that merely contains the substring.
|
||||
assert_eq!(parse_steam_app_id_from_environ(b"MY_SteamAppId=999\0"), None);
|
||||
assert_eq!(
|
||||
parse_steam_app_id_from_environ(b"MY_SteamAppId=999\0"),
|
||||
None
|
||||
);
|
||||
// Garbage value → None, no panic.
|
||||
assert_eq!(parse_steam_app_id_from_environ(b"SteamAppId=notanumber\0"), None);
|
||||
assert_eq!(
|
||||
parse_steam_app_id_from_environ(b"SteamAppId=notanumber\0"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+33
-7
@@ -234,10 +234,20 @@ mod tests {
|
||||
}
|
||||
"#;
|
||||
let root = parse(acf).unwrap();
|
||||
assert_eq!(root.get_path(&["AppState", "name"]).and_then(Value::as_str), Some("Counter-Strike 2"));
|
||||
assert_eq!(root.get_path(&["AppState", "appid"]).and_then(Value::as_str), Some("730"));
|
||||
assert_eq!(
|
||||
root.get_path(&["AppState", "name"]).and_then(Value::as_str),
|
||||
Some("Counter-Strike 2")
|
||||
);
|
||||
assert_eq!(
|
||||
root.get_path(&["AppState", "appid"])
|
||||
.and_then(Value::as_str),
|
||||
Some("730")
|
||||
);
|
||||
// Case-insensitive key lookup.
|
||||
assert_eq!(root.get_path(&["appstate", "NAME"]).and_then(Value::as_str), Some("Counter-Strike 2"));
|
||||
assert_eq!(
|
||||
root.get_path(&["appstate", "NAME"]).and_then(Value::as_str),
|
||||
Some("Counter-Strike 2")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -262,8 +272,14 @@ mod tests {
|
||||
"#;
|
||||
let root = parse(vdf).unwrap();
|
||||
let lf = root.get("libraryfolders").unwrap();
|
||||
assert_eq!(lf.get_path(&["0", "path"]).and_then(Value::as_str), Some(r"C:\Program Files (x86)\Steam"));
|
||||
assert_eq!(lf.get_path(&["1", "path"]).and_then(Value::as_str), Some("/home/eric/.local/share/Steam"));
|
||||
assert_eq!(
|
||||
lf.get_path(&["0", "path"]).and_then(Value::as_str),
|
||||
Some(r"C:\Program Files (x86)\Steam")
|
||||
);
|
||||
assert_eq!(
|
||||
lf.get_path(&["1", "path"]).and_then(Value::as_str),
|
||||
Some("/home/eric/.local/share/Steam")
|
||||
);
|
||||
// The library folder ids are iterable for discovery.
|
||||
let ids: Vec<&str> = lf.entries().iter().map(|(k, _)| k.as_str()).collect();
|
||||
assert_eq!(ids, vec!["0", "1"]);
|
||||
@@ -292,7 +308,14 @@ mod tests {
|
||||
"#;
|
||||
let root = parse(reg).unwrap();
|
||||
let appid = root
|
||||
.get_path(&["Registry", "HKCU", "Software", "Valve", "Steam", "RunningAppID"])
|
||||
.get_path(&[
|
||||
"Registry",
|
||||
"HKCU",
|
||||
"Software",
|
||||
"Valve",
|
||||
"Steam",
|
||||
"RunningAppID",
|
||||
])
|
||||
.and_then(Value::as_str);
|
||||
assert_eq!(appid, Some("570"));
|
||||
}
|
||||
@@ -301,7 +324,10 @@ mod tests {
|
||||
fn handles_comments_and_barewords() {
|
||||
let vdf = "// a comment\n\"root\"\n{\n\tbarekey barevalue // trailing\n}\n";
|
||||
let root = parse(vdf).unwrap();
|
||||
assert_eq!(root.get_path(&["root", "barekey"]).and_then(Value::as_str), Some("barevalue"));
|
||||
assert_eq!(
|
||||
root.get_path(&["root", "barekey"]).and_then(Value::as_str),
|
||||
Some("barevalue")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user