//! Parsing `/proc//maps` and finding the FIFA 17 process. use std::fs; use std::io; #[derive(Debug, Clone)] pub struct Region { pub start: u64, pub end: u64, /// The raw four permission characters, e.g. `rwxp` or `r--s`. pub perms: String, /// File offset this mapping starts at, meaningless for anonymous regions. pub offset: u64, /// `None` for anonymous mappings. pub path: Option, } impl Region { pub fn size(&self) -> u64 { self.end - self.start } pub fn readable(&self) -> bool { self.perms.as_bytes().first() == Some(&b'r') } pub fn writable(&self) -> bool { self.perms.as_bytes().get(1) == Some(&b'w') } pub fn executable(&self) -> bool { self.perms.as_bytes().get(2) == Some(&b'x') } pub fn private(&self) -> bool { self.perms.as_bytes().get(3) == Some(&b'p') } pub fn anonymous(&self) -> bool { self.path.is_none() } /// Pseudo-files the kernel exposes. Reading `[vvar]` through /// `/proc/pid/mem` fails, and `[vsyscall]` is not interesting here. pub fn pseudo(&self) -> bool { matches!(self.path.as_deref(), Some(p) if p.starts_with('[')) } } pub fn read_maps(pid: i32) -> io::Result> { let text = fs::read_to_string(format!("/proc/{pid}/maps")).map_err(|e| { let hint = if fs::metadata(format!("/proc/{pid}")).is_err() { format!("no process with pid {pid}") } else { format!("pid {pid} exists but its maps are unreadable (different user?)") }; io::Error::new(e.kind(), format!("reading /proc/{pid}/maps: {hint}")) })?; Ok(text.lines().filter_map(parse_line).collect()) } /// The target's `comm`, so output can name what was actually inspected rather /// than assuming an explicit `--pid` pointed at the game. pub fn read_comm(pid: i32) -> String { fs::read_to_string(format!("/proc/{pid}/comm")) .map(|s| s.trim().to_string()) .unwrap_or_else(|_| "?".to_string()) } /// Pull the next whitespace-delimited field starting at `cursor`, advancing it. fn next_field<'a>(line: &'a str, cursor: &mut usize) -> Option<&'a str> { let bytes = line.as_bytes(); while *cursor < bytes.len() && bytes[*cursor].is_ascii_whitespace() { *cursor += 1; } let start = *cursor; while *cursor < bytes.len() && !bytes[*cursor].is_ascii_whitespace() { *cursor += 1; } if start == *cursor { None } else { Some(&line[start..*cursor]) } } fn parse_line(line: &str) -> Option { // Format: `start-end perms offset dev inode path` // // The path may contain spaces (`/mnt/games/FIFA 17/FIFA17.exe`) and may // carry a ` (deleted)` suffix, so we consume exactly five leading fields by // position and take the untouched remainder as the path. // // Doing this with `line.find(inode)` to locate the split point is a trap: // the inode of an anonymous mapping is "0", and `find("0")` happily matches // a zero digit inside the address range at the very start of the line. That // silently turns half the address into a path. Hence the explicit cursor. let mut cursor = 0usize; let range = next_field(line, &mut cursor)?; let perms = next_field(line, &mut cursor)?; let offset = next_field(line, &mut cursor)?; let _dev = next_field(line, &mut cursor)?; let _inode = next_field(line, &mut cursor)?; let (start, end) = range.split_once('-')?; let start = u64::from_str_radix(start, 16).ok()?; let end = u64::from_str_radix(end, 16).ok()?; let tail = line[cursor..].trim(); let path = if tail.is_empty() { None } else { Some(tail.to_string()) }; Some(Region { start, end, perms: perms.to_string(), offset: u64::from_str_radix(offset, 16).ok()?, path, }) } /// Find the FIFA 17 process. /// /// `comm` is the authority, NOT `cmdline`. Under Proton there are a dozen /// helper processes (bash, umu-run, srt-bwrap, pv-adverb, proton, umu.exe) /// whose command lines mention fifa17, and at least one of them /// (`umu.exe /mnt/games/FIFA 17/_fifa17.exe`) is a convincing decoy. Only the /// real game has `comm == "FIFA17.exe"`. Its `/proc//exe` points at /// wine64-preloader, which is expected and is not a reason to doubt the match. pub fn find_pid(comm_name: &str) -> io::Result { let mut hits = Vec::new(); for entry in fs::read_dir("/proc")? { let entry = entry?; let name = entry.file_name(); let Some(name) = name.to_str() else { continue }; let Ok(pid) = name.parse::() else { continue; }; if let Ok(comm) = fs::read_to_string(format!("/proc/{pid}/comm")) { if comm.trim() == comm_name { hits.push(pid); } } } match hits.len() { 0 => Err(io::Error::new( io::ErrorKind::NotFound, format!("no process with comm == {comm_name:?}; is the game running? pass --pid to override"), )), 1 => Ok(hits[0]), _ => Err(io::Error::new( io::ErrorKind::InvalidData, format!("{} processes have comm == {comm_name:?}: {hits:?}; pass --pid to disambiguate", hits.len()), )), } } pub fn human(bytes: u64) -> String { const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"]; let mut value = bytes as f64; let mut unit = 0; while value >= 1024.0 && unit < UNITS.len() - 1 { value /= 1024.0; unit += 1; } if unit == 0 { format!("{bytes} B") } else { format!("{value:.2} {}", UNITS[unit]) } }