Skip to main content

lean_ctx_patterns/
sysinfo.rs

1// SPDX-License-Identifier: Apache-2.0
2
3pub fn compress_ps(output: &str) -> Option<String> {
4    let lines: Vec<&str> = output.lines().collect();
5    if lines.len() < 2 {
6        return None;
7    }
8
9    let header = lines[0];
10    let procs: Vec<&str> = lines[1..]
11        .iter()
12        .filter(|l| !l.trim().is_empty())
13        .copied()
14        .collect();
15    let total = procs.len();
16
17    if total <= 10 {
18        return None;
19    }
20
21    let mut high_cpu: Vec<&str> = Vec::new();
22    let mut high_mem: Vec<&str> = Vec::new();
23
24    for &line in &procs {
25        let cols: Vec<&str> = line.split_whitespace().collect();
26        if cols.len() >= 4 {
27            let cpu: f64 = cols.get(2).and_then(|s| s.parse().ok()).unwrap_or(0.0);
28            let mem: f64 = cols.get(3).and_then(|s| s.parse().ok()).unwrap_or(0.0);
29            if cpu > 1.0 {
30                high_cpu.push(line);
31            }
32            if mem > 1.0 {
33                high_mem.push(line);
34            }
35        }
36    }
37
38    let mut out = format!("ps: {total} processes\n{header}\n");
39
40    if !high_cpu.is_empty() {
41        out.push_str(&format!("--- high CPU ({}) ---\n", high_cpu.len()));
42        for l in high_cpu.iter().take(15) {
43            out.push_str(l);
44            out.push('\n');
45        }
46    }
47    if !high_mem.is_empty() {
48        out.push_str(&format!("--- high MEM ({}) ---\n", high_mem.len()));
49        for l in high_mem.iter().take(15) {
50            out.push_str(l);
51            out.push('\n');
52        }
53    }
54
55    Some(out.trim_end().to_string())
56}
57
58/// df output is safety-critical (disk usage, root filesystem) and typically
59/// small (<30 lines). Verbatim passthrough prevents hiding critical info.
60pub fn compress_df(output: &str) -> Option<String> {
61    Some(output.to_string())
62}
63
64pub fn compress_du(output: &str) -> Option<String> {
65    let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
66
67    if lines.len() <= 10 {
68        return None;
69    }
70
71    let mut parsed: Vec<(u64, &str)> = lines
72        .iter()
73        .filter_map(|line| {
74            let parts: Vec<&str> = line.splitn(2, |c: char| c.is_whitespace()).collect();
75            if parts.len() == 2 {
76                let size = parse_size_field(parts[0]);
77                Some((size, parts[1].trim()))
78            } else {
79                None
80            }
81        })
82        .collect();
83
84    parsed.sort_by_key(|b| std::cmp::Reverse(b.0));
85
86    let top: Vec<String> = parsed
87        .iter()
88        .take(15)
89        .map(|(size, path)| format!("{}\t{path}", format_size(*size)))
90        .collect();
91
92    Some(format!(
93        "du: {} entries (top 15 by size)\n{}",
94        lines.len(),
95        top.join("\n")
96    ))
97}
98
99fn parse_size_field(s: &str) -> u64 {
100    let s = s.trim();
101    if let Ok(v) = s.parse::<u64>() {
102        return v;
103    }
104    let (num_part, suffix) = s.split_at(s.len().saturating_sub(1));
105    let base: f64 = num_part.parse().unwrap_or(0.0);
106    match suffix.to_uppercase().as_str() {
107        "K" => (base * 1024.0) as u64,
108        "M" => (base * 1024.0 * 1024.0) as u64,
109        "G" => (base * 1024.0 * 1024.0 * 1024.0) as u64,
110        _ => s.parse().unwrap_or(0),
111    }
112}
113
114fn format_size(bytes: u64) -> String {
115    if bytes >= 1_073_741_824 {
116        format!("{:.1}G", bytes as f64 / 1_073_741_824.0)
117    } else if bytes >= 1_048_576 {
118        format!("{:.1}M", bytes as f64 / 1_048_576.0)
119    } else if bytes >= 1024 {
120        format!("{:.0}K", bytes as f64 / 1024.0)
121    } else {
122        format!("{bytes}")
123    }
124}
125
126pub fn compress_ping(output: &str) -> Option<String> {
127    let lines: Vec<&str> = output.lines().collect();
128    if lines.len() < 3 {
129        return None;
130    }
131
132    let mut host = "";
133    let mut stats_line = "";
134    let mut rtt_line = "";
135
136    for line in &lines {
137        if line.starts_with("PING ") || line.starts_with("ping ") {
138            host = line;
139        }
140        if line.contains("packets transmitted") || line.contains("packet loss") {
141            stats_line = line;
142        }
143        if line.contains("rtt ") || line.contains("round-trip") {
144            rtt_line = line;
145        }
146    }
147
148    if stats_line.is_empty() {
149        return None;
150    }
151
152    let mut out = String::new();
153    if !host.is_empty() {
154        out.push_str(host);
155        out.push('\n');
156    }
157    out.push_str(stats_line);
158    if !rtt_line.is_empty() {
159        out.push('\n');
160        out.push_str(rtt_line);
161    }
162    Some(out)
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn ps_compresses_large_output() {
171        let mut lines = vec![
172            "USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND".to_string(),
173        ];
174        for i in 0..50 {
175            lines.push(format!(
176                "user     {:>5}  0.0  0.1  12345  1234 ?        S    10:00   0:00 process_{i}",
177                1000 + i
178            ));
179        }
180        lines.push(
181            "root      9999 95.0  8.5 999999 99999 ?        R    10:00   5:00 heavy_proc"
182                .to_string(),
183        );
184        let output = lines.join("\n");
185        let result = compress_ps(&output).expect("should compress");
186        assert!(result.contains("51 processes"));
187        assert!(result.contains("heavy_proc"));
188    }
189
190    #[test]
191    fn ps_skips_small_output() {
192        let output = "USER PID %CPU %MEM\nroot 1 0.0 0.1 init";
193        assert!(compress_ps(output).is_none());
194    }
195
196    #[test]
197    fn df_returns_verbatim() {
198        let mut lines =
199            vec!["Filesystem     1K-blocks    Used Available Use% Mounted on".to_string()];
200        for i in 0..20 {
201            let pct = if i < 5 { 90 } else { 10 };
202            lines.push(format!(
203                "/dev/sda{i}  1000000  500000  500000  {pct}% /mnt/disk{i}"
204            ));
205        }
206        let output = lines.join("\n");
207        let result = compress_df(&output).expect("should return Some");
208        assert_eq!(result, output, "df must pass through verbatim");
209    }
210
211    #[test]
212    fn du_compresses_large_listing() {
213        let mut lines = Vec::new();
214        for i in 0..30 {
215            lines.push(format!("{}\t./dir_{i}", (i + 1) * 1024));
216        }
217        let output = lines.join("\n");
218        let result = compress_du(&output).expect("should compress");
219        assert!(result.contains("30 entries"));
220        assert!(result.contains("top 15"));
221    }
222
223    #[test]
224    fn ping_extracts_summary() {
225        let output = "PING google.com (142.250.80.46): 56 data bytes\n64 bytes from 142.250.80.46: icmp_seq=0 ttl=116 time=12.3 ms\n64 bytes from 142.250.80.46: icmp_seq=1 ttl=116 time=11.8 ms\n64 bytes from 142.250.80.46: icmp_seq=2 ttl=116 time=12.1 ms\n\n--- google.com ping statistics ---\n3 packets transmitted, 3 packets received, 0.0% packet loss\nrtt min/avg/max/stddev = 11.8/12.1/12.3/0.2 ms";
226        let result = compress_ping(output).expect("should compress");
227        assert!(result.contains("3 packets transmitted"));
228        assert!(result.contains("rtt"));
229        assert!(!result.contains("icmp_seq=1"));
230    }
231}