//! Steam store provider: installed-title scan (local `libraryfolders.vdf` + app manifests, no //! API key) and Steam-CDN / local-`librarycache` artwork. Split out of the `library` facade (plan §W5). use super::art::fetch_image; use super::*; /// Reads the **local** Steam install — no Steam Web API key, no network. Installed titles come /// from `steamapps/appmanifest_.acf`; extra library folders from /// `steamapps/libraryfolders.vdf`; the user's own non-Steam shortcuts ("Add a Non-Steam Game to My /// Library") from each account's binary `userdata//config/shortcuts.vdf`; artwork from the /// public Steam CDN by appid, or the user's per-account `grid/` overrides (all a shortcut ever has). pub struct SteamProvider; impl LibraryProvider for SteamProvider { fn store(&self) -> &'static str { "steam" } fn list(&self) -> Vec { let mut by_appid: std::collections::BTreeMap = Default::default(); for steamapps in steam_library_dirs() { for (appid, name) in scan_manifests(&steamapps) { by_appid.entry(appid).or_insert(name); // first library wins; dedups shared appids } } let mut games: Vec = by_appid .into_iter() .filter(|(appid, name)| !is_steam_tool(*appid, name)) .map(|(appid, title)| GameEntry { provider: None, id: format!("steam:{appid}"), store: "steam".into(), title, art: steam_art(appid), launch: Some(LaunchSpec { kind: "steam_appid".into(), value: appid.to_string(), }), }) .collect(); // Non-Steam shortcuts have no `appmanifest` — [`scan_manifests`] can't see them, so the // user's own custom entries are gathered separately from `shortcuts.vdf`. games.extend(steam_shortcuts()); games } } /// The Steam CDN poster/hero/logo/header for an appid — relative proxy paths the *client* resolves /// against the host it just talked to (so they work the same whichever interface/port the client /// reached the host on), backed by [`steam_art_bytes`] on the way out. Not every appid has a /// 600×900 capsule, but `header.jpg` is effectively universal — the client falls back to it. fn steam_art(appid: u32) -> Artwork { let url = |kind: &str| Some(format!("/api/v1/library/art/steam:{appid}/{kind}")); Artwork { portrait: url("portrait"), hero: url("hero"), logo: url("logo"), header: url("header"), } } /// Resolve one Steam cover-art kind to bytes: the host's own local Steam cache first (exact — it's /// literally what the user's Steam client already shows for this title), then the user's per-account /// `grid/` overrides (the *only* art a non-Steam shortcut ever has), then the legacy flat CDN URL. /// `None` when none has it (the client then falls through to its next art candidate). Blocking /// (disk + network) — call off the async runtime. pub fn steam_art_bytes(appid: u32, kind: ArtKind) -> Option<(Vec, String)> { if let Some(local) = steam_local_art_bytes(appid, kind).or_else(|| steam_grid_art_bytes(appid, kind)) { return Some(local); } // A non-Steam shortcut's appid has the high bit set (see [`shortcut_appid`]) and is never a real // store appid, so the CDN would only 404 — skip the wasted request and fall through cleanly. if appid & 0x8000_0000 != 0 { return None; } let url = format!( "https://cdn.cloudflare.steamstatic.com/steam/apps/{appid}/{}", kind.cdn_filename() ); fetch_image(&url) } /// Cap on a local librarycache file we'll read into memory — generous for a Steam-quality JPEG/PNG /// (these run well under 2 MiB in practice) while bounding a pathological file. const LOCAL_ART_MAX_BYTES: u64 = 8 * 1024 * 1024; /// `appcache/librarycache///` across every Steam root, for whichever /// `` subdirectory actually has this kind's file (Steam reuses one hash dir per asset /// version, so there's normally exactly one candidate per kind). fn steam_local_art_bytes(appid: u32, kind: ArtKind) -> Option<(Vec, String)> { steam_roots() .into_iter() .find_map(|root| find_local_art_file(&root, appid, kind)) .and_then(|path| { let bytes = std::fs::read(&path).ok()?; let ctype = if path.extension().is_some_and(|e| e == "png") { "image/png" } else { "image/jpeg" }; Some((bytes, ctype.to_string())) }) } /// Find this kind's cached file under one Steam root's `appcache/librarycache///`, /// trying each hash subdirectory (normally just one) and each candidate filename in priority /// order. Pure path lookup — no env/HOME dependency — so it's unit-testable against a plain /// directory fixture. fn find_local_art_file(root: &Path, appid: u32, kind: ArtKind) -> Option { let cache_dir = root .join("appcache") .join("librarycache") .join(appid.to_string()); let hash_dirs = std::fs::read_dir(&cache_dir).ok()?; for hash_dir in hash_dirs.flatten() { for name in kind.local_filenames() { let path = hash_dir.path().join(name); let Ok(meta) = std::fs::metadata(&path) else { continue; }; if meta.len() > 0 && meta.len() <= LOCAL_ART_MAX_BYTES { return Some(path); } } } None } /// Artwork a user set in Steam itself for a **non-Steam shortcut** (or a `grid/` override for a real /// title), stored per-account under `userdata//config/grid/`, keyed by the same 32-bit appid the /// shortcut carries. Tried before the CDN — a shortcut has no CDN art at all, so this is where its /// poster lives. fn steam_grid_art_bytes(appid: u32, kind: ArtKind) -> Option<(Vec, String)> { for root in steam_roots() { let Ok(users) = std::fs::read_dir(root.join("userdata")) else { continue; }; for user in users.flatten() { let grid = user.path().join("config").join("grid"); if let Some(path) = find_grid_art_file(&grid, appid, kind) { if let Ok(bytes) = std::fs::read(&path) { let ctype = if path.extension().is_some_and(|e| e == "png") { "image/png" } else { "image/jpeg" }; return Some((bytes, ctype.to_string())); } } } } None } /// The `grid/` filenames Steam stores this art kind under for appid ``, tried in order (PNG then /// JPG — Steam accepts either). Pure path logic, so it's unit-testable against a directory fixture. fn find_grid_art_file(grid: &Path, appid: u32, kind: ArtKind) -> Option { for name in grid_filenames(kind, appid) { let path = grid.join(&name); if let Ok(meta) = std::fs::metadata(&path) { if meta.len() > 0 && meta.len() <= LOCAL_ART_MAX_BYTES { return Some(path); } } } None } /// The `userdata//config/grid/` basenames Steam names each art kind under for appid ``: /// portrait `p`, hero `_hero`, logo `_logo`, and the wide capsule `` — each as `.png` /// then `.jpg`. fn grid_filenames(kind: ArtKind, appid: u32) -> Vec { let both = |base: String| vec![format!("{base}.png"), format!("{base}.jpg")]; match kind { ArtKind::Portrait => both(format!("{appid}p")), ArtKind::Hero => both(format!("{appid}_hero")), ArtKind::Logo => both(format!("{appid}_logo")), ArtKind::Header => both(format!("{appid}")), } } /// Candidate Steam roots (classic, Flatpak, Deck) that actually exist, canonicalized + deduped. #[cfg(not(target_os = "windows"))] fn steam_roots() -> Vec { let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else { return Vec::new(); }; let candidates = [ home.join(".local/share/Steam"), home.join(".steam/steam"), home.join(".steam/root"), home.join(".var/app/com.valvesoftware.Steam/.local/share/Steam"), // Flatpak Steam ]; steam_roots_existing(candidates) } /// Windows Steam roots: the default install dirs under Program Files. Games installed on other /// drives are still found via each root's `libraryfolders.vdf` (see [`steam_library_dirs`]). A /// non-default Steam install dir (registry `Valve\Steam\InstallPath`) isn't covered yet. #[cfg(target_os = "windows")] fn steam_roots() -> Vec { let mut candidates = Vec::new(); for var in ["ProgramFiles(x86)", "ProgramFiles", "ProgramW6432"] { if let Some(pf) = std::env::var_os(var) { candidates.push(PathBuf::from(pf).join("Steam")); } } steam_roots_existing(candidates) } /// Keep only the candidate roots that exist (have a `steamapps` dir), canonicalized + deduped. fn steam_roots_existing(candidates: impl IntoIterator) -> Vec { let mut seen = HashSet::new(); let mut roots = Vec::new(); for c in candidates { if let Ok(canon) = c.canonicalize() { if canon.join("steamapps").is_dir() && seen.insert(canon.clone()) { roots.push(canon); } } } roots } /// Every `steamapps` dir holding installed titles: each root's own, plus the extra library /// folders listed in `libraryfolders.vdf` (Steam lets you install games on other drives). fn steam_library_dirs() -> Vec { let mut seen = HashSet::new(); let mut dirs = Vec::new(); let mut push = |steamapps: PathBuf, dirs: &mut Vec| { if let Ok(canon) = steamapps.canonicalize() { if canon.is_dir() && seen.insert(canon.clone()) { dirs.push(canon); } } }; for root in steam_roots() { let steamapps = root.join("steamapps"); if let Ok(text) = std::fs::read_to_string(steamapps.join("libraryfolders.vdf")) { for path in vdf_paths(&text) { push(PathBuf::from(path).join("steamapps"), &mut dirs); } } push(steamapps, &mut dirs); } dirs } /// Pull every `"path" ""` value out of a `libraryfolders.vdf`. We don't need a full VDF /// parser for the two flat fields we read. On Windows the values are backslash-escaped /// (`D:\\SteamLibrary`), so unescape `\\` → `\`; Linux paths need no unescaping. fn vdf_paths(text: &str) -> Vec { text.lines() .filter_map(|l| vdf_value(l.trim(), "path")) .map(|p| { #[cfg(target_os = "windows")] { p.replace("\\\\", "\\") } #[cfg(not(target_os = "windows"))] { p.to_string() } }) .collect() } /// `"" ""` on a single line → ``. Used for both VDF and ACF flat fields. fn vdf_value<'a>(line: &'a str, key: &str) -> Option<&'a str> { let rest = line.strip_prefix(&format!("\"{key}\""))?; let after = &rest[rest.find('"')? + 1..]; Some(&after[..after.find('"')?]) } /// Scan a `steamapps` dir for `appmanifest_*.acf` files → (appid, name) of installed titles. fn scan_manifests(steamapps: &Path) -> Vec<(u32, String)> { let Ok(rd) = std::fs::read_dir(steamapps) else { return Vec::new(); }; let mut out = Vec::new(); for entry in rd.flatten() { let fname = entry.file_name(); let fname = fname.to_string_lossy(); if !(fname.starts_with("appmanifest_") && fname.ends_with(".acf")) { continue; } if let Ok(text) = std::fs::read_to_string(entry.path()) { let appid = text.lines().find_map(|l| vdf_value(l.trim(), "appid")); let name = text.lines().find_map(|l| vdf_value(l.trim(), "name")); if let (Some(Ok(appid)), Some(name)) = (appid.map(str::parse::), name) { out.push((appid, name.to_string())); } } } out } /// Steam installs runtimes/redistributables as "apps" too — keep them out of a *game* library. fn is_steam_tool(appid: u32, name: &str) -> bool { // Steamworks Common Redistributables; Steam Linux Runtime 1.0/2.0/3.0 (Sniper/Soldier). const TOOL_IDS: &[u32] = &[228980, 1070560, 1391110, 1628350, 1493710]; if TOOL_IDS.contains(&appid) { return true; } let n = name.to_ascii_lowercase(); n.contains("proton") || n.starts_with("steam linux runtime") || n.contains("steamworks common") || n.contains("steamvr") } /// One non-Steam shortcut ("Add a Non-Steam Game to My Library"), as read from `shortcuts.vdf`. struct Shortcut { /// The 32-bit shortcut appid Steam assigns it (high bit set — see [`shortcut_appid`]). Keys the /// entry id and its `grid/` artwork; the 64-bit launch id derives from it ([`shortcut_gameid`]). appid: u32, /// Display name (`AppName`). name: String, /// Whether Steam has this shortcut hidden from the library (`IsHidden`) — we honor that. hidden: bool, } /// Every non-Steam shortcut across all Steam accounts on this host, as launchable [`GameEntry`]s. /// These carry no `appmanifest`, so [`scan_manifests`] never sees them — this is the only path that /// surfaces a user's custom Steam entries. Best-effort: an unreadable/absent `shortcuts.vdf` /// contributes nothing; hidden shortcuts and duplicate appids are dropped. fn steam_shortcuts() -> Vec { let mut seen = HashSet::new(); let mut out = Vec::new(); for path in shortcuts_files() { let Ok(bytes) = std::fs::read(&path) else { continue; }; for sc in parse_shortcuts(&bytes) { if seen.insert(sc.appid) { if let Some(entry) = shortcut_entry(sc) { out.push(entry); } } } } out } /// Map one parsed [`Shortcut`] to a library entry, or `None` if Steam has it hidden. Launch reuses /// the `steam_appid` recipe (`steam steam://rungameid/`) — the value is the 64-bit shortcut /// game id, not the 32-bit appid, because the plain appid won't launch a non-Steam shortcut. fn shortcut_entry(sc: Shortcut) -> Option { if sc.hidden { return None; } Some(GameEntry { provider: None, id: format!("steam:{}", sc.appid), store: "steam".into(), title: sc.name, art: steam_art(sc.appid), launch: Some(LaunchSpec { kind: "steam_appid".into(), value: shortcut_gameid(sc.appid).to_string(), }), }) } /// Every `userdata//config/shortcuts.vdf` under each Steam root — one file per Steam account /// that has signed in on this host. fn shortcuts_files() -> Vec { let mut files = Vec::new(); for root in steam_roots() { let Ok(users) = std::fs::read_dir(root.join("userdata")) else { continue; }; for user in users.flatten() { let path = user.path().join("config").join("shortcuts.vdf"); if path.is_file() { files.push(path); } } } files } /// The 64-bit game id `steam://rungameid/` needs to launch a non-Steam shortcut: high dword = the /// 32-bit shortcut appid, low dword = the shortcut marker `0x0200_0000`. (Handing `rungameid` the /// bare 32-bit appid does not launch a shortcut — it must be this composed id.) fn shortcut_gameid(appid: u32) -> u64 { ((appid as u64) << 32) | 0x0200_0000 } /// The 32-bit appid Steam derives for a shortcut from its target+name — `crc32(exe + name)` with the /// high bit set. Only used when `shortcuts.vdf` omits the stored `appid` (very old Steam); modern /// Steam writes it, and we prefer the stored value. fn shortcut_appid(exe: &str, name: &str) -> u32 { crc32(format!("{exe}{name}").as_bytes()) | 0x8000_0000 } /// Standard reflected (IEEE) CRC-32 — a few short strings' worth per scan, so a table-free bitwise /// loop is plenty. Matches what Steam uses to hash a shortcut's `exe + name`. fn crc32(data: &[u8]) -> u32 { let mut crc: u32 = 0xFFFF_FFFF; for &byte in data { crc ^= byte as u32; for _ in 0..8 { let mask = (crc & 1).wrapping_neg(); crc = (crc >> 1) ^ (0xEDB8_8320 & mask); } } !crc } /// Parse a **binary** `shortcuts.vdf` into its shortcuts. The format is Steam's binary KeyValues: a /// 1-byte type tag (`0x00` nested map, `0x01` string, `0x02` int32), a NUL-terminated key, then a /// type-specific payload; `0x08` closes the current map. The whole file is one `shortcuts` map whose /// children (keyed `"0"`, `"1"`, …) are the individual shortcuts. Lenient and panic-free: a /// truncated file or an unrecognized tag stops the walk and returns whatever parsed so far. fn parse_shortcuts(buf: &[u8]) -> Vec { let mut out = Vec::new(); let mut pos = 0usize; // Enter the top-level map (`<0x00> "shortcuts" `); tolerate any key name. if buf.first() != Some(&0x00) { return out; } pos += 1; if read_cstr(buf, &mut pos).is_none() { return out; } // Each child is a map describing one shortcut, until the map-closing `0x08`. while let Some(&tag) = buf.get(pos) { pos += 1; if tag != 0x00 { break; // `0x08` (end of shortcuts) or anything unexpected } if read_cstr(buf, &mut pos).is_none() { break; // the index key ("0", "1", …) } match parse_one_shortcut(buf, &mut pos) { Some(sc) => out.push(sc), None => break, } } out } /// Parse one shortcut's fields (positioned just after its index key) up to the map-closing `0x08`, /// pulling the ones we surface. `None` on a truncated/garbled entry. fn parse_one_shortcut(buf: &[u8], pos: &mut usize) -> Option { let mut appid: Option = None; let mut name = String::new(); let mut exe = String::new(); let mut hidden = false; loop { let tag = *buf.get(*pos)?; *pos += 1; if tag == 0x08 { break; // end of this shortcut } let key = read_cstr(buf, pos)?.to_ascii_lowercase(); match tag { 0x00 => skip_map(buf, pos)?, // nested map (e.g. `tags`) — not needed 0x01 => { let val = read_cstr(buf, pos)?; match key.as_str() { "appname" => name = val, "exe" => exe = val, _ => {} } } 0x02 => { let val = read_i32(buf, pos)?; match key.as_str() { "appid" => appid = Some(val as u32), "ishidden" => hidden = val != 0, _ => {} } } 0x07 => *pos += 8, // uint64 — skip _ => return None, // unknown tag: payload size unknown, can't continue safely } } if name.trim().is_empty() { return None; // nothing worth showing } // Prefer the stored appid; fall back to Steam's derivation when it's absent (0 / missing). let appid = appid .filter(|a| *a != 0) .unwrap_or_else(|| shortcut_appid(&exe, &name)); Some(Shortcut { appid, name, hidden, }) } /// Skip a nested map's contents (positioned just after its key) up to and including its `0x08`. fn skip_map(buf: &[u8], pos: &mut usize) -> Option<()> { loop { let tag = *buf.get(*pos)?; *pos += 1; if tag == 0x08 { return Some(()); } read_cstr(buf, pos)?; // key match tag { 0x00 => skip_map(buf, pos)?, 0x01 => { read_cstr(buf, pos)?; } 0x02 => *pos += 4, 0x07 => *pos += 8, _ => return None, } } } /// Read a NUL-terminated UTF-8 string (lossy) starting at `pos`, advancing past the terminator. /// `None` if the buffer ends before a NUL. fn read_cstr(buf: &[u8], pos: &mut usize) -> Option { let start = *pos; let end = buf.get(start..)?.iter().position(|&b| b == 0)? + start; let s = String::from_utf8_lossy(&buf[start..end]).into_owned(); *pos = end + 1; Some(s) } /// Read a little-endian int32 at `pos`, advancing 4 bytes. `None` if fewer than 4 bytes remain. fn read_i32(buf: &[u8], pos: &mut usize) -> Option { let bytes: [u8; 4] = buf.get(*pos..*pos + 4)?.try_into().ok()?; *pos += 4; Some(i32::from_le_bytes(bytes)) } #[cfg(test)] mod tests { use super::*; #[test] fn vdf_value_extracts_quoted_field() { assert_eq!( vdf_value("\"path\"\t\t\"/mnt/games/SteamLibrary\"", "path"), Some("/mnt/games/SteamLibrary") ); assert_eq!(vdf_value("\"appid\"\t\t\"570\"", "appid"), Some("570")); assert_eq!(vdf_value("\"name\"\t\t\"Dota 2\"", "name"), Some("Dota 2")); assert_eq!(vdf_value("\"installdir\"\t\t\"x\"", "appid"), None); } #[test] fn vdf_paths_pulls_all_library_folders() { let vdf = r#" "libraryfolders" { "0" { "path" "/home/u/.local/share/Steam" "apps" { "570" "123" } } "1" { "path" "/mnt/ssd/SteamLibrary" } } "#; assert_eq!( vdf_paths(vdf), vec![ "/home/u/.local/share/Steam".to_string(), "/mnt/ssd/SteamLibrary".to_string() ] ); } #[test] fn tools_are_filtered_but_games_kept() { assert!(is_steam_tool(228980, "Steamworks Common Redistributables")); assert!(is_steam_tool(1493710, "Proton Experimental")); assert!(is_steam_tool(0, "Steam Linux Runtime 3.0 (sniper)")); assert!(!is_steam_tool(570, "Dota 2")); assert!(!is_steam_tool(1245620, "ELDEN RING")); } #[test] fn steam_art_points_at_the_host_art_proxy() { let art = steam_art(570); assert_eq!( art.portrait.as_deref(), Some("/api/v1/library/art/steam:570/portrait") ); assert_eq!( art.header.as_deref(), Some("/api/v1/library/art/steam:570/header") ); } #[test] fn find_local_art_file_matches_the_hashed_librarycache_layout() { let dir = tempfile::tempdir().unwrap(); let cache = dir .path() .join("appcache/librarycache/3527290/480bd879ac737921bfa2529a6fea15961267ad21"); std::fs::create_dir_all(&cache).unwrap(); std::fs::write(cache.join("library_600x900.jpg"), b"not really a jpeg").unwrap(); let found = find_local_art_file(dir.path(), 3527290, ArtKind::Portrait).unwrap(); assert_eq!(found, cache.join("library_600x900.jpg")); // A kind with no cached file, and an appid with no cache dir at all, both miss cleanly. assert_eq!( find_local_art_file(dir.path(), 3527290, ArtKind::Hero), None ); assert_eq!( find_local_art_file(dir.path(), 570, ArtKind::Portrait), None ); } #[test] fn find_local_art_file_prefers_the_2x_portrait() { let dir = tempfile::tempdir().unwrap(); let cache = dir.path().join("appcache/librarycache/570/somehash"); std::fs::create_dir_all(&cache).unwrap(); std::fs::write(cache.join("library_600x900.jpg"), b"1x").unwrap(); std::fs::write(cache.join("library_600x900_2x.jpg"), b"2x").unwrap(); let found = find_local_art_file(dir.path(), 570, ArtKind::Portrait).unwrap(); assert_eq!(found, cache.join("library_600x900_2x.jpg")); } // --- Non-Steam shortcuts (custom Steam entries) --- /// Build one binary-VDF field for a test `shortcuts.vdf`. fn field_str(key: &str, val: &str) -> Vec { let mut v = vec![0x01u8]; v.extend_from_slice(key.as_bytes()); v.push(0); v.extend_from_slice(val.as_bytes()); v.push(0); v } fn field_i32(key: &str, val: i32) -> Vec { let mut v = vec![0x02u8]; v.extend_from_slice(key.as_bytes()); v.push(0); v.extend_from_slice(&val.to_le_bytes()); v } fn map_open(key: &str) -> Vec { let mut v = vec![0x00u8]; v.extend_from_slice(key.as_bytes()); v.push(0); v } #[test] fn parse_shortcuts_reads_entries_honors_hidden_and_key_case() { let mut buf = Vec::new(); buf.extend(map_open("shortcuts")); // Entry 0: a normal shortcut, with a nested `tags` map to exercise skip_map, and mixed-case // keys (Steam has shipped both `AppName` and `appname`). appid stored as a negative i32. buf.extend(map_open("0")); buf.extend(field_i32("appid", -1838178284)); // == 2456789012 as u32 buf.extend(field_str("AppName", "My Emulator")); buf.extend(field_str("Exe", "\"/usr/bin/foo\"")); buf.extend(field_i32("IsHidden", 0)); buf.extend(map_open("tags")); buf.extend(field_str("0", "emulator")); buf.push(0x08); // end tags buf.push(0x08); // end entry 0 // Entry 1: hidden, lowercase key variant. buf.extend(map_open("1")); buf.extend(field_str("appname", "Hidden Game")); buf.extend(field_i32("ishidden", 1)); buf.push(0x08); // end entry 1 buf.push(0x08); // end shortcuts let scs = parse_shortcuts(&buf); assert_eq!(scs.len(), 2); assert_eq!(scs[0].appid, 2_456_789_012); assert_eq!(scs[0].name, "My Emulator"); assert!(!scs[0].hidden); assert_eq!(scs[1].name, "Hidden Game"); assert!(scs[1].hidden); // A hidden shortcut is dropped from the surfaced library; a visible one launches via its // 64-bit game id (not the bare appid). assert!(shortcut_entry(scs.into_iter().nth(1).unwrap()).is_none()); } #[test] fn parse_shortcuts_is_lenient() { assert!(parse_shortcuts(b"").is_empty()); // not even a top-level map assert!(parse_shortcuts(b"{not binary vdf}").is_empty()); // A truncated entry (buffer ends mid-int) yields what parsed cleanly before it — here, none. let mut buf = Vec::new(); buf.extend(map_open("shortcuts")); buf.extend(map_open("0")); buf.extend_from_slice(b"\x02appid\x00\x01\x02"); // 2 of 4 int bytes, then EOF assert!(parse_shortcuts(&buf).is_empty()); } #[test] fn shortcut_entry_launches_via_rungameid() { let sc = Shortcut { appid: 2_456_789_012, name: "My Emulator".into(), hidden: false, }; let entry = shortcut_entry(sc).unwrap(); assert_eq!(entry.id, "steam:2456789012"); assert_eq!(entry.store, "steam"); let launch = entry.launch.unwrap(); assert_eq!(launch.kind, "steam_appid"); // Value is the 64-bit game id — digits only, so it passes the shared appid guard. assert_eq!(launch.value, shortcut_gameid(2_456_789_012).to_string()); assert!(launch.value.bytes().all(|b| b.is_ascii_digit())); } #[test] fn shortcut_gameid_composes_appid_and_marker() { let id = shortcut_gameid(0x8000_0000); assert_eq!(id >> 32, 0x8000_0000); // high dword is the appid assert_eq!(id & 0xFFFF_FFFF, 0x0200_0000); // low dword is the shortcut marker } #[test] fn crc32_matches_the_known_check_value_and_derives_a_high_bit_appid() { assert_eq!(crc32(b"123456789"), 0xCBF4_3926); // IEEE CRC-32 check value // A derived shortcut appid always has the high bit set (so it never collides with a real // store appid, and its CDN art fetch is skipped). assert_ne!( shortcut_appid("\"/usr/bin/foo\"", "My Emulator") & 0x8000_0000, 0 ); } #[test] fn grid_filenames_follow_steams_naming() { assert_eq!( grid_filenames(ArtKind::Portrait, 42), vec!["42p.png", "42p.jpg"] ); assert_eq!( grid_filenames(ArtKind::Hero, 42), vec!["42_hero.png", "42_hero.jpg"] ); assert_eq!( grid_filenames(ArtKind::Logo, 42), vec!["42_logo.png", "42_logo.jpg"] ); assert_eq!( grid_filenames(ArtKind::Header, 42), vec!["42.png", "42.jpg"] ); } #[test] fn find_grid_art_file_matches_the_userdata_grid_layout() { let grid = tempfile::tempdir().unwrap(); std::fs::write(grid.path().join("2456789012p.jpg"), b"poster").unwrap(); let found = find_grid_art_file(grid.path(), 2_456_789_012, ArtKind::Portrait).unwrap(); assert_eq!(found, grid.path().join("2456789012p.jpg")); // Missing kinds and a zero-byte file both miss cleanly. assert_eq!( find_grid_art_file(grid.path(), 2_456_789_012, ArtKind::Hero), None ); std::fs::write(grid.path().join("42p.png"), b"").unwrap(); assert_eq!(find_grid_art_file(grid.path(), 42, ArtKind::Portrait), None); } }