Skip to main content

git_policy_cli/
index_lock_holder_parsers.rs

1//! What:
2//!  Parsers of the holder-scan output that is not `/proc`:
3//!  `lsof -F pcDi` records and the
4//!       Restart Manager script lines,
5//!  both matched to a lock by device and inode.
6//! Why:
7//!  The parsers are pure text functions,
8//!  so their controls run on every host,
9//!  whichever scan
10//!      produced the text (`src/index-lock/index-lock-holders-darwin.ts`,
11//!  `-win32.ts`).
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! parseLsofFields(output); parseRestartManagerOutput(output);
16//! ```
17
18/// The holder record type the parsers produce.
19use super::index_lock_holders::Holder;
20
21/// What:
22///  One `lsof -F pcDi` process record:
23///  its PID,
24///  command and the device and inode of every
25///       listed file (each field present when parsable).
26/// Why:
27///   `LsofProcess`.
28///
29/// In TS you'd write (pseudocode):
30/// ```ts
31/// type LsofProcess = { pid: number; command?: string; files: { device?: bigint; inode?: bigint }[] };
32/// ```
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct LsofProcess {
35    /// Process ID as `Number` reads it;
36    ///  `None` when it is not a number.
37    pub pid: Option<i64>,
38    /// Command name.
39    pub command: Option<String>,
40    /// Device and inode of each listed file.
41    pub files: Vec<(Option<u128>, Option<u128>)>,
42}
43
44/// What:
45///  An `lsof` number as `BigInt` reads it:
46///  decimal,
47///  or `0x`,
48///  `0o` or `0b` prefixed.
49/// Why:
50///   Device numbers come `0x`-prefixed,
51///  inodes decimal.
52///
53/// In TS you'd write (pseudocode):
54/// ```ts
55/// BigInt(text)
56/// ```
57pub fn parse_lsof_number(text: &str) -> Option<u128> {
58    let trimmed: &str = super::js_text::trim_javascript(text);
59    if trimmed.is_empty() {
60        return Some(0);
61    }
62    let (digits, radix) = match trimmed.get(..2) {
63        Some("0x" | "0X") => (&trimmed[2..], 16),
64        Some("0o" | "0O") => (&trimmed[2..], 8),
65        Some("0b" | "0B") => (&trimmed[2..], 2),
66        _ => (trimmed, 10),
67    };
68    if digits.is_empty() || digits.starts_with(['+', '-']) {
69        return None;
70    }
71    return u128::from_str_radix(digits, radix).ok();
72}
73
74/// What:
75///  The first field with this identifier among lines,
76///  parsed as a number.
77/// Why:
78///   `numberField`.
79///
80/// In TS you'd write (pseudocode):
81/// ```ts
82/// numberField({ lines, identifier })
83/// ```
84fn number_field(lines: &[&str], identifier: char) -> Option<u128> {
85    for line in lines {
86        if let Some(rest) = line.strip_prefix(identifier) {
87            return parse_lsof_number(rest);
88        }
89    }
90    return None;
91}
92
93/// What:
94///  Lines grouped under each line starting with `identifier`;
95///  earlier lines are dropped.
96/// Why:
97///   `groupsStartingAt`.
98///
99/// In TS you'd write (pseudocode):
100/// ```ts
101/// groupsStartingAt({ lines, identifier })
102/// ```
103fn groups<'a>(lines: &[&'a str], identifier: char) -> Vec<Vec<&'a str>> {
104    let mut grouped: Vec<Vec<&'a str>> = Vec::new();
105    for line in lines {
106        if line.starts_with(identifier) {
107            grouped.push(vec![*line]);
108        } else if let Some(last) = grouped.last_mut() {
109            last.push(*line);
110        }
111    }
112    return grouped;
113}
114
115/// What:
116///  Parse `lsof -F pcDi` output into process records.
117/// Why:
118///   `parseLsofFields`.
119///
120/// In TS you'd write (pseudocode):
121/// ```ts
122/// parseLsofFields('p42\ncgit\nf3\nD0x1000011\ni77\n')
123/// ```
124pub fn parse_lsof_fields(output: &str) -> Vec<LsofProcess> {
125    let lines: Vec<&str> = output.split('\n').collect();
126    let mut processes: Vec<LsofProcess> = Vec::new();
127    for group in groups(lines.as_slice(), 'p') {
128        let mut command: Option<String> = None;
129        for line in &group {
130            if let Some(rest) = line.strip_prefix('c') {
131                command = Some(String::from(rest));
132                break;
133            }
134        }
135        let mut files: Vec<(Option<u128>, Option<u128>)> = Vec::new();
136        for file in groups(group.as_slice(), 'f') {
137            files.push((number_field(file.as_slice(), 'D'), number_field(file.as_slice(), 'i')));
138        }
139        processes.push(LsofProcess {
140            pid: super::recovery_evidence::decimal_attempt(&group[0][1..]),
141            command,
142            files,
143        });
144    }
145    return processes;
146}
147
148/// What:
149///  Processes whose listed file matches the lock's device and inode.
150/// Why:
151///   `matchingHolders`.
152///
153/// In TS you'd write (pseudocode):
154/// ```ts
155/// matchingHolders({ processes, device, inode })
156/// ```
157pub fn matching_holders(processes: &[LsofProcess], device: u64, inode: u64) -> Vec<Holder> {
158    let mut holders: Vec<Holder> = Vec::new();
159    for process in processes {
160        let Some(pid) = process.pid else {
161            continue;
162        };
163        let wanted: (Option<u128>, Option<u128>) = (Some(u128::from(device)), Some(u128::from(inode)));
164        if process.files.contains(&wanted) {
165            holders.push(Holder {
166                pid,
167                command: process.command.clone(),
168            });
169        }
170    }
171    return holders;
172}
173
174/// What:
175///  Parse the Restart Manager script's `<pid>\t<application name>` lines.
176/// Why:
177///   `parseRestartManagerOutput`.
178///
179/// In TS you'd write (pseudocode):
180/// ```ts
181/// parseRestartManagerOutput('4242\tgit.exe\n')
182/// ```
183pub fn parse_restart_manager_output(output: &str) -> Vec<Holder> {
184    let mut holders: Vec<Holder> = Vec::new();
185    for raw in output.split('\n') {
186        let line: &str = super::js_text::trim_javascript(raw);
187        if line.is_empty() {
188            continue;
189        }
190        let (pid_text, command) = match line.split_once('\t') {
191            Some((pid, name)) => (pid, name),
192            None => (line, ""),
193        };
194        let Some(pid) = super::recovery_evidence::decimal_attempt(super::js_text::trim_javascript(pid_text)) else {
195            continue;
196        };
197        holders.push(Holder {
198            pid,
199            command: if command.is_empty() { None } else { Some(String::from(command)) },
200        });
201    }
202    return holders;
203}