rightkit_child_process/
login_shell.rs1use std::time::Duration;
16
17#[cfg(unix)]
18use crate::{run, RunOptions};
19
20#[cfg(unix)]
21const PATH_BEGIN: &str = "__RIGHTKIT_PATH_BEGIN__";
22#[cfg(unix)]
23const PATH_END: &str = "__RIGHTKIT_PATH_END__";
24#[cfg(unix)]
25const ENV_BEGIN: &str = "__RIGHTKIT_ENV_BEGIN__";
26#[cfg(unix)]
27const ENV_END: &str = "__RIGHTKIT_ENV_END__";
28#[cfg(unix)]
30const OUTPUT_CAP: usize = 256 * 1024;
31
32pub fn path(timeout: Duration) -> Option<String> {
34 #[cfg(unix)]
35 {
36 let script =
37 format!("printf '%s' '{PATH_BEGIN}'; printf '%s' \"$PATH\"; printf '%s' '{PATH_END}'");
38 let output = run(shell(), ["-lic", script.as_str()], &capped(timeout)).ok()?;
39 if output.timed_out || output.stdout.truncated {
40 return None;
41 }
42 let text = String::from_utf8_lossy(&output.stdout.bytes);
43 let (_, rest) = text.split_once(PATH_BEGIN)?;
44 let (value, _) = rest.split_once(PATH_END)?;
45 (!value.is_empty()).then(|| value.to_string())
46 }
47 #[cfg(not(unix))]
48 {
49 let _ = timeout;
50 None
51 }
52}
53
54pub fn env_values(names: &[String], timeout: Duration) -> Vec<(String, String)> {
59 #[cfg(unix)]
60 {
61 let wanted: Vec<&str> = names
62 .iter()
63 .map(String::as_str)
64 .filter(|name| is_identifier(name))
65 .collect();
66 if wanted.is_empty() {
67 return Vec::new();
68 }
69 let mut script = format!("printf '%s' '{ENV_BEGIN}'");
70 for name in &wanted {
71 script.push_str(&format!("; printf '%s\\0' \"${{{name}-}}\""));
73 }
74 script.push_str(&format!("; printf '%s' '{ENV_END}'"));
75 let Ok(output) = run(shell(), ["-lic", script.as_str()], &capped(timeout)) else {
76 return Vec::new();
77 };
78 if output.timed_out || output.stdout.truncated {
79 return Vec::new();
80 }
81 let text = String::from_utf8_lossy(&output.stdout.bytes);
82 let Some((_, rest)) = text.split_once(ENV_BEGIN) else {
83 return Vec::new();
84 };
85 let Some((body, _)) = rest.split_once(ENV_END) else {
86 return Vec::new();
87 };
88 body.split('\0')
89 .zip(wanted)
90 .filter(|(value, _)| !value.is_empty())
91 .map(|(value, name)| (name.to_string(), value.to_string()))
92 .collect()
93 }
94 #[cfg(not(unix))]
95 {
96 let _ = (names, timeout);
97 Vec::new()
98 }
99}
100
101#[cfg(unix)]
102fn capped(timeout: Duration) -> RunOptions {
103 RunOptions::new(timeout)
104 .stdout_cap(OUTPUT_CAP)
105 .stderr_cap(0)
106 .no_window(true)
107}
108
109#[cfg(unix)]
112fn shell() -> std::path::PathBuf {
113 use std::path::PathBuf;
114 let from_env = std::env::var_os("SHELL")
115 .map(PathBuf::from)
116 .filter(|shell| {
117 shell.is_absolute()
118 && shell.is_file()
119 && shell
120 .to_str()
121 .is_some_and(|text| !text.chars().any(char::is_control))
122 });
123 from_env.unwrap_or_else(|| {
124 ["/bin/zsh", "/bin/sh"]
125 .iter()
126 .map(PathBuf::from)
127 .find(|candidate| candidate.is_file())
128 .unwrap_or_else(|| PathBuf::from("/bin/sh"))
129 })
130}
131
132#[cfg(unix)]
133fn is_identifier(name: &str) -> bool {
134 let mut chars = name.chars();
135 chars
136 .next()
137 .is_some_and(|first| first.is_ascii_alphabetic() || first == '_')
138 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
139}
140
141#[cfg(all(test, unix))]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn only_plain_identifiers_reach_the_script() {
147 assert!(is_identifier("PATH"));
148 assert!(is_identifier("_X1"));
149 assert!(!is_identifier("1X"));
150 assert!(!is_identifier("A;rm"));
151 assert!(!is_identifier("A B"));
152 assert!(!is_identifier(""));
153 }
154
155 #[test]
156 fn a_nonexistent_shell_falls_back_to_a_real_one() {
157 let shell = shell();
158 assert!(shell.is_absolute());
159 assert!(shell.is_file());
160 }
161
162 #[test]
163 fn env_values_skips_invalid_names_and_returns_nothing_for_none_valid() {
164 assert!(env_values(&["not valid".to_string()], Duration::from_secs(1)).is_empty());
165 }
166}