| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | | pub const PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; |
| 8 | |
|
| 9 | |
|
| 10 | |
|
| 11 | 5× | pub fn resolve_program(program: &str, path: &str) -> Option<String> { |
| 12 | | use std::os::unix::fs::PermissionsExt; |
| 13 | 5× | if program.is_empty() { |
| 14 | 1× | return None; |
| 15 | 4× | } |
| 16 | 4× | if program.contains('/') { |
| 17 | 1× | return Some(program.to_string()); |
| 18 | 3× | } |
| 19 | 3× | path.split(':') |
| 20 | 9× | .filter(|d| !d.is_empty()) |
| 21 | 6× | .map(|d| format!("{d}/{program}")) |
| 22 | 6× | .find(|p| std::fs::metadata(p).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)) |
| 23 | 5× | } |
| 24 | |
|
| 25 | |
|
| 26 | |
|
| 27 | |
|
| 28 | |
|
| 29 | 2× | pub fn job_env(image: Option<&str>, job: &[(String, String)]) -> std::collections::BTreeMap<String, String> { |
| 30 | 2× | let mut env = std::collections::BTreeMap::new(); |
| 31 | 2× | env.insert("PATH".to_string(), PATH.to_string()); |
| 32 | 2× | env.insert("HOME".to_string(), "/tmp".to_string()); |
| 33 | 3× | for line in image.unwrap_or("").lines() { |
| 34 | 3× | if let Some((k, v)) = line.split_once('=') { |
| 35 | 2× | if !k.is_empty() { |
| 36 | 2× | env.insert(k.to_string(), v.to_string()); |
| 37 | 2× | } |
| 38 | 1× | } |
| 39 | | } |
| 40 | 2× | for (k, v) in job { |
| 41 | 2× | env.insert(k.clone(), v.clone()); |
| 42 | 2× | } |
| 43 | 2× | env |
| 44 | 2× | } |
| 45 | |
|
| 46 | |
|
| 47 | |
|
| 48 | |
|
| 49 | 2× | pub fn dev_links(dev: &str) -> Result<(), String> { |
| 50 | 8× | for (name, target) in DEV_LINKS { |
| 51 | 8× | let link = format!("{dev}/{name}"); |
| 52 | 8× | if std::fs::symlink_metadata(&link).is_err() { |
| 53 | 4× | std::os::unix::fs::symlink(target, &link).map_err(|e| format!("link {link}: {e}"))?; |
| 54 | 4× | } |
| 55 | | } |
| 56 | 2× | Ok(()) |
| 57 | 2× | } |
| 58 | |
|
| 59 | | const DEV_LINKS: [(&str, &str); 4] = |
| 60 | | [("fd", "/proc/self/fd"), ("stdin", "/proc/self/fd/0"), ("stdout", "/proc/self/fd/1"), ("stderr", "/proc/self/fd/2")]; |
| 61 | |
|
| 62 | |
|
| 63 | 0× | pub fn loopback_up() -> Result<(), String> { |
| 64 | 0× | let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) }; |
| 65 | 0× | if fd < 0 { |
| 66 | 0× | return Err(format!("socket for lo: {}", std::io::Error::last_os_error())); |
| 67 | 0× | } |
| 68 | 0× | let mut req: libc::ifreq = unsafe { std::mem::zeroed() }; |
| 69 | 0× | for (i, b) in b"lo".iter().enumerate() { |
| 70 | 0× | req.ifr_name[i] = *b as libc::c_char; |
| 71 | 0× | } |
| 72 | 0× | let ok = unsafe { |
| 73 | | |
| 74 | 0× | libc::ioctl(fd, libc::SIOCGIFFLAGS as _, &mut req) == 0 && { |
| 75 | 0× | req.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short; |
| 76 | 0× | libc::ioctl(fd, libc::SIOCSIFFLAGS as _, &req) == 0 |
| 77 | | } |
| 78 | | }; |
| 79 | 0× | let err = std::io::Error::last_os_error(); |
| 80 | 0× | unsafe { libc::close(fd) }; |
| 81 | 0× | if !ok { |
| 82 | 0× | return Err(format!("bring up lo: {err}")); |
| 83 | 0× | } |
| 84 | 0× | Ok(()) |
| 85 | 0× | } |
| 86 | |
|
| 87 | | #[cfg(test)] |
| 88 | | mod tests { |
| 89 | | use super::{dev_links, job_env, resolve_program}; |
| 90 | |
|
| 91 | | #[test] |
| 92 | 1× | fn the_image_environment_sits_under_the_jobs() { |
| 93 | 1× | let image = "PATH=/opt/llvm-mos/bin:/usr/bin:/bin\nX16EMU=/opt/x16emu\nnot a line\n"; |
| 94 | 1× | let env = job_env(Some(image), &[("X16EMU".into(), "/job".into()), ("LANG".into(), "C".into())]); |
| 95 | 1× | assert_eq!(env["PATH"], "/opt/llvm-mos/bin:/usr/bin:/bin", "the image's PATH replaces the default"); |
| 96 | 1× | assert_eq!(env["X16EMU"], "/job", "the job's value wins"); |
| 97 | 1× | assert_eq!(env["HOME"], "/tmp"); |
| 98 | 1× | assert_eq!(env["LANG"], "C"); |
| 99 | 1× | assert_eq!(job_env(None, &[])["PATH"], super::PATH); |
| 100 | 1× | } |
| 101 | |
|
| 102 | | #[test] |
| 103 | 1× | fn dev_gets_the_usual_links_once() { |
| 104 | 1× | let dir = std::env::temp_dir().join(format!("flow-init-dev-{}", std::process::id())); |
| 105 | 1× | let _ = std::fs::remove_dir_all(&dir); |
| 106 | 1× | std::fs::create_dir_all(&dir).unwrap(); |
| 107 | 1× | let d = dir.to_str().unwrap(); |
| 108 | 1× | dev_links(d).unwrap(); |
| 109 | 1× | dev_links(d).unwrap(); |
| 110 | 1× | assert_eq!(std::fs::read_link(dir.join("fd")).unwrap(), std::path::Path::new("/proc/self/fd")); |
| 111 | 1× | assert_eq!(std::fs::read_link(dir.join("stdout")).unwrap(), std::path::Path::new("/proc/self/fd/1")); |
| 112 | | |
| 113 | 1× | assert!(dir.join("fd").join("0").exists() || !std::path::Path::new("/proc/self/fd/0").exists()); |
| 114 | 1× | std::fs::remove_dir_all(&dir).unwrap(); |
| 115 | 1× | } |
| 116 | |
|
| 117 | | use std::os::unix::fs::PermissionsExt; |
| 118 | |
|
| 119 | | #[test] |
| 120 | 1× | fn programs_are_found_in_the_jobs_path() { |
| 121 | 1× | let dir = std::env::temp_dir().join(format!("flow-init-test-{}", std::process::id())); |
| 122 | 1× | let (a, b) = (dir.join("a"), dir.join("b")); |
| 123 | 1× | std::fs::create_dir_all(&a).unwrap(); |
| 124 | 1× | std::fs::create_dir_all(&b).unwrap(); |
| 125 | 1× | std::fs::write(a.join("tool"), "not executable").unwrap(); |
| 126 | 1× | std::fs::write(b.join("tool"), "#!/bin/sh\n").unwrap(); |
| 127 | 1× | std::fs::set_permissions(b.join("tool"), std::fs::Permissions::from_mode(0o755)).unwrap(); |
| 128 | 1× | std::fs::create_dir_all(a.join("dir")).unwrap(); |
| 129 | 1× | let path = format!("{}::{}", a.display(), b.display()); |
| 130 | 1× | assert_eq!(resolve_program("tool", &path), Some(format!("{}/tool", b.display())), "skips a file that is not executable"); |
| 131 | 1× | assert_eq!(resolve_program("dir", &path), None, "and directories"); |
| 132 | 1× | assert_eq!(resolve_program("missing", &path), None); |
| 133 | 1× | assert_eq!(resolve_program("./x", &path).as_deref(), Some("./x"), "a path is used as given"); |
| 134 | 1× | assert_eq!(resolve_program("", &path), None); |
| 135 | 1× | std::fs::remove_dir_all(&dir).unwrap(); |
| 136 | 1× | } |
| 137 | | } |