Skip to main content

peek_proc_reader/
environ.rs

1// /proc/PID/environ. Logic moved from peek-core::proc::env.
2//
3// This crate is responsible for low-level /proc parsing and intentionally
4// avoids depending on peek-core types. Callers can turn the raw key/value
5// pairs into their own domain structs.
6
7use crate::error::{io_to_error, Result};
8use std::path::PathBuf;
9
10/// Raw environment entry as parsed from `/proc/<pid>/environ`.
11#[derive(Debug, Clone)]
12pub struct EnvironEntry {
13    pub key: String,
14    pub value: String,
15}
16
17/// Parse raw bytes from `/proc/<pid>/environ` into key/value pairs.
18#[cfg(target_os = "linux")]
19fn parse_environ_bytes(raw: &[u8]) -> Vec<EnvironEntry> {
20    let mut vars = Vec::new();
21
22    for entry in raw.split(|&b| b == 0) {
23        if entry.is_empty() {
24            continue;
25        }
26        let s = String::from_utf8_lossy(entry);
27        if let Some(eq) = s.find('=') {
28            let key = s[..eq].to_string();
29            let value = s[eq + 1..].to_string();
30            vars.push(EnvironEntry { key, value });
31        }
32    }
33
34    vars.sort_by(|a, b| a.key.cmp(&b.key));
35    vars
36}
37
38/// Read and parse `/proc/<pid>/environ` into raw key/value pairs.
39#[cfg(target_os = "linux")]
40pub fn read_environ(pid: i32) -> Result<Vec<EnvironEntry>> {
41    let path = PathBuf::from(format!("/proc/{}/environ", pid));
42    let raw = std::fs::read(&path).map_err(|e| io_to_error(path, e, pid))?;
43    Ok(parse_environ_bytes(&raw))
44}
45
46/// On non-Linux platforms we don't have /proc; return an empty set.
47#[cfg(not(target_os = "linux"))]
48pub fn read_environ(_pid: i32) -> Result<Vec<EnvironEntry>> {
49    Ok(Vec::new())
50}
51
52#[cfg(test)]
53mod tests {
54    use super::{parse_environ_bytes, EnvironEntry};
55    use proptest::prelude::*;
56
57    #[test]
58    fn environ_entry_debug_clone_works() {
59        let e = EnvironEntry {
60            key: "KEY".to_string(),
61            value: "VALUE".to_string(),
62        };
63        let cloned = e.clone();
64        assert_eq!(cloned.key, "KEY");
65        assert_eq!(cloned.value, "VALUE");
66        let debug = format!("{:?}", e);
67        assert!(debug.contains("KEY"));
68    }
69
70    proptest! {
71        #[test]
72        fn parse_environ_bytes_round_trips_basic_pairs(
73            kvs in prop::collection::vec(
74                // Keys and values without '=' or NUL
75                (r"[A-Z_][A-Z0-9_]{0,4}", r"[a-zA-Z0-9_]{0,8}"),
76                0..16
77            )
78        ) {
79            // Build raw environ-style buffer
80            let mut raw = Vec::new();
81            for (k, v) in &kvs {
82                if k.is_empty() {
83                    continue;
84                }
85                raw.extend_from_slice(k.as_bytes());
86                raw.push(b'=');
87                raw.extend_from_slice(v.as_bytes());
88                raw.push(0);
89            }
90
91            let parsed = parse_environ_bytes(&raw);
92
93            // All parsed keys must have existed in the original set.
94            let original_keys: std::collections::HashSet<&str> =
95                kvs.iter().map(|(k, _)| k.as_str()).collect();
96            for EnvironEntry { key, .. } in &parsed {
97                assert!(original_keys.contains(key.as_str()));
98            }
99        }
100    }
101}