hermit_record_format/
script.rs1use std::ffi::OsStr;
10use std::ffi::OsString;
11use std::fs;
12use std::io::Read;
13use std::os::unix::ffi::OsStrExt;
14use std::path::Path;
15use std::path::PathBuf;
16
17const BINPRM_BUF_SIZE: usize = 256;
19
20#[derive(Debug, Eq, PartialEq)]
22pub struct Shebang {
23 program: PathBuf,
24 args: Vec<OsString>,
25}
26
27impl Shebang {
28 pub fn from_buf(buf: &[u8]) -> Option<Self> {
30 if !buf.starts_with(b"#!") {
31 return None;
32 }
33
34 let mut i = 2;
35 while i < buf.len() {
36 if buf[i] == b' ' || buf[i] == b'\t' {
37 i += 1;
38 } else {
39 break;
40 }
41 }
42
43 let mut j = i;
44 while j < buf.len() {
45 if b" \t\r\n".contains(&buf[j]) {
46 break;
47 } else {
48 j += 1;
49 }
50 }
51
52 let program = PathBuf::from(OsStr::from_bytes(&buf[i..j]));
53
54 i = j;
55 while j < buf.len() {
56 if b"\n".contains(&buf[j]) {
57 break;
58 } else {
59 j += 1;
60 }
61 }
62
63 let args = String::from_utf8_lossy(&buf[i..j])
64 .split_ascii_whitespace()
65 .map(OsString::from)
66 .collect::<Vec<_>>();
67
68 Some(Shebang { program, args })
69 }
70
71 pub fn interpreter_from_buf(buf: &[u8]) -> Option<PathBuf> {
76 Self::from_buf(buf).map(|shebang| shebang.program)
77 }
78
79 pub fn new<P: AsRef<Path>>(path: P) -> Option<Self> {
81 let mut buf = Vec::new();
82 let nb = fs::File::open(path)
83 .and_then(|f| {
84 let mut handle = f.take(BINPRM_BUF_SIZE as u64);
85 handle.read_to_end(&mut buf)
86 })
87 .ok()?;
88 let shebang = Self::from_buf(&buf[..nb])?;
91 let metadata = fs::metadata(&shebang.program).ok()?;
92 if metadata.is_file() {
93 Some(shebang)
94 } else {
95 None
96 }
97 }
98
99 pub fn interpreter(&self) -> &Path {
101 &self.program
102 }
103
104 pub fn args(&self) -> impl Iterator<Item = &OsStr> {
106 self.args.iter().map(OsStr::new)
107 }
108
109 pub fn into_parts(self) -> (PathBuf, Vec<OsString>) {
111 (self.program, self.args)
112 }
113}
114
115#[derive(Debug, Eq, PartialEq)]
119pub enum KernelScript {
120 NotScript,
122 Declined,
130 Interpreter(PathBuf),
132}
133
134impl KernelScript {
135 pub fn from_buf(buf: &[u8]) -> Self {
141 let mut header = [0_u8; BINPRM_BUF_SIZE];
144 let len = buf.len().min(BINPRM_BUF_SIZE);
145 header[..len].copy_from_slice(&buf[..len]);
146 if !header.starts_with(b"#!") {
147 return Self::NotScript;
148 }
149 let last = BINPRM_BUF_SIZE - 1;
150 let newline = header
152 .iter()
153 .take_while(|byte| **byte != 0)
154 .position(|byte| *byte == b'\n');
155 let line_end = match newline {
156 Some(newline) => newline,
157 None => {
158 let Some(start) = (2..=last).find(|&index| !matches!(header[index], b' ' | b'\t'))
161 else {
162 return Self::Declined;
163 };
164 if !header[start..=last]
165 .iter()
166 .any(|byte| matches!(byte, b' ' | b'\t' | 0))
167 {
168 return Self::Declined;
169 }
170 last
172 }
173 };
174 let line = &header[2..line_end];
175 let start = line
176 .iter()
177 .position(|byte| !matches!(byte, b' ' | b'\t'))
178 .unwrap_or(line.len());
179 let name = &line[start..];
180 let name = &name[..name
181 .iter()
182 .position(|byte| matches!(byte, b' ' | b'\t' | 0))
183 .unwrap_or(name.len())];
184 if name.is_empty() {
185 Self::Declined
186 } else {
187 Self::Interpreter(PathBuf::from(OsStr::from_bytes(name)))
188 }
189 }
190}
191
192#[cfg(test)]
193mod test {
194 use super::*;
195
196 #[test]
197 fn shebang_invalid_shebang() {
198 assert_eq!(Shebang::from_buf(b"! /bin/bash"), None);
199 }
200
201 #[test]
202 fn shebang_empty_interpreter() {
203 for input in [b"#!".as_slice(), b"#!\n", b"#! \t\n"] {
204 assert_eq!(
205 Shebang::from_buf(input).map(|shebang| shebang.program),
206 Some(PathBuf::new()),
207 "input: {input:?}"
208 );
209 }
210 }
211
212 #[test]
213 fn shebang_bin_bash() {
214 assert_eq!(
215 Shebang::from_buf(b"#! /bin/bash").map(|s| s.program),
216 Some(PathBuf::from("/bin/bash"))
217 );
218 assert_eq!(
219 Shebang::from_buf(b"#! \t/bin/bash").map(|s| s.program),
220 Some(PathBuf::from("/bin/bash"))
221 );
222 assert_eq!(
223 Shebang::from_buf(b"#!/bin/bash\nfoobar").map(|s| s.program),
224 Some(PathBuf::from("/bin/bash"))
225 );
226 assert_eq!(
227 Shebang::from_buf(b"#!/bin/bash").map(|s| s.program),
228 Some(PathBuf::from("/bin/bash"))
229 );
230 }
231
232 #[test]
233 fn shebang_env_python() {
234 assert_eq!(
235 Shebang::from_buf(b"#! /usr/bin/env python").map(|s| s.program),
236 Some(PathBuf::from("/usr/bin/env"))
237 );
238 }
239
240 #[test]
241 fn shebang_struct_parse() {
242 assert_eq!(
243 Shebang::from_buf(b"#! /usr/bin/env python3"),
244 Some(Shebang {
245 program: PathBuf::from("/usr/bin/env"),
246 args: vec![OsString::from("python3")]
247 })
248 );
249 assert_eq!(
250 Shebang::from_buf(b"#!/bin/bash\nfoobar"),
251 Some(Shebang {
252 program: PathBuf::from("/bin/bash"),
253 args: Vec::new(),
254 })
255 );
256 }
257
258 #[test]
259 fn shebang_struct_env_python_get_interpreter() {
260 assert_eq!(
261 Shebang::from_buf(b"#! /usr/bin/env python3"),
262 Some(Shebang {
263 program: PathBuf::from("/usr/bin/env"),
264 args: vec![OsString::from("python3")],
265 })
266 );
267
268 assert_eq!(
269 Shebang::from_buf(b"#! /usr/bin/env python3 -c "),
270 Some(Shebang {
271 program: PathBuf::from("/usr/bin/env"),
272 args: vec![OsString::from("python3"), OsString::from("-c")],
273 })
274 );
275
276 assert_eq!(
277 Shebang::from_buf(b"#! /usr/bin/env python3 -c \nfoobar"),
278 Some(Shebang {
279 program: PathBuf::from("/usr/bin/env"),
280 args: vec![OsString::from("python3"), OsString::from("-c")],
281 })
282 );
283 }
284
285 fn kernel_interpreter(path: &[u8]) -> KernelScript {
286 KernelScript::Interpreter(PathBuf::from(OsStr::from_bytes(path)))
287 }
288
289 #[test]
290 fn kernel_script_reads_the_interpreter_as_load_script_does() {
291 let cases: [(&[u8], KernelScript); 7] = [
292 (b"\x7fELF\x02\x01\x01", KernelScript::NotScript),
293 (b"! /bin/bash", KernelScript::NotScript),
294 (b"#!/tmp/interp\r\n", kernel_interpreter(b"/tmp/interp\r")),
296 (
297 b"#! \t/usr/bin/env python3 -c\nbody",
298 kernel_interpreter(b"/usr/bin/env"),
299 ),
300 (b"#!/bin/sh\0junk\n", kernel_interpreter(b"/bin/sh")),
302 (b"#!/bin/bash", kernel_interpreter(b"/bin/bash")),
304 (b"#!relative arg\n", kernel_interpreter(b"relative")),
305 ];
306 for (input, expected) in cases {
307 assert_eq!(KernelScript::from_buf(input), expected, "input: {input:?}");
308 }
309 }
310
311 #[test]
312 fn kernel_script_declines_a_missing_or_truncated_interpreter() {
313 for input in [b"#!".as_slice(), b"#!\n", b"#! \t\n", b"#!\0/bin/sh\n"] {
314 assert_eq!(
315 KernelScript::from_buf(input),
316 KernelScript::Declined,
317 "input: {input:?}"
318 );
319 }
320 let mut long = b"#!/".to_vec();
323 long.resize(BINPRM_BUF_SIZE, b'a');
324 assert_eq!(KernelScript::from_buf(&long), KernelScript::Declined);
325 long.extend_from_slice(b" arg\n");
326 assert_eq!(KernelScript::from_buf(&long), KernelScript::Declined);
327 long.truncate(BINPRM_BUF_SIZE);
329 long[BINPRM_BUF_SIZE - 1] = b' ';
330 let expected = long[2..BINPRM_BUF_SIZE - 1].to_vec();
331 assert_eq!(KernelScript::from_buf(&long), kernel_interpreter(&expected));
332 }
333}