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}