rustc.rs (11324B)
1 extern crate alloc; 2 use super::{ 3 Error, ExitCode, 4 io::{self, Write as _}, 5 }; 6 #[cfg(target_os = "openbsd")] 7 use super::{Permissions, Promises, env}; 8 use alloc::string::FromUtf8Error; 9 #[cfg(target_os = "openbsd")] 10 use std::{ffi::OsStr, fs, io::ErrorKind}; 11 use std::{ 12 io::Read as _, 13 path::{Path, PathBuf}, 14 process::{Command, Stdio}, 15 }; 16 /// Error when executing `rustc -Whelp`. 17 pub(crate) enum E { 18 /// I/O error. 19 Io(Error), 20 /// Error when there is no `$PATH` variable. 21 #[cfg(target_os = "openbsd")] 22 NoPathVariable, 23 /// Error when `"rustc"` is unable to be located in `$PATH`. 24 #[cfg(target_os = "openbsd")] 25 NoRustcInPath, 26 /// `rustc -Whelp` didn't return a status code, and nothing was written to `stderr`. 27 NoStatusNoErr, 28 /// `rustc -Whelp` didn't return a status code, and invalid UTF-8 was written to `stderr`. 29 NoStatusInvalidUtf8(FromUtf8Error), 30 /// `rustc -Whelp` didn't return a status code, and the contained `String` was written to `stderr`. 31 NoStatusErr(String), 32 /// `rustc -Whelp` returned an error code, but nothing was written to `stderr`. 33 ErrStatusNoErr(i32), 34 /// `rustc -Whelp` returned an error code, but `stderr` contained invalid UTF-8. 35 ErrStatusInvalidUtf8(i32, FromUtf8Error), 36 /// `rustc -Whelp` returned an error code, and the contained `String` was written to `stderr`. 37 ErrStatus(i32, String), 38 /// `rustc -Whelp` returned a success code, but `stderr` contained invalid UTF-8. 39 SuccessErrInvalidUtf8(FromUtf8Error), 40 /// `rustc -Whelp` returned a success code, but the contained `String` was written to `stderr`. 41 SuccessErr(String), 42 } 43 impl E { 44 /// Writes `self` into `stderr`. 45 pub(crate) fn into_exit_code(self) -> ExitCode { 46 let mut stderr = io::stderr().lock(); 47 match self { 48 Self::Io(err) => writeln!(stderr, "I/O issue: {err}."), 49 #[cfg(target_os = "openbsd")] 50 Self::NoPathVariable => writeln!( 51 stderr, 52 "No PATH variable." 53 ), 54 #[cfg(target_os = "openbsd")] 55 Self::NoRustcInPath => writeln!( 56 stderr, 57 "rustc could not be found based on the PATH variable." 58 ), 59 Self::NoStatusNoErr => writeln!( 60 stderr, 61 "rustc -Whelp didn't return a status code but didn't write anything to stderr." 62 ), 63 Self::NoStatusInvalidUtf8(err) => writeln!( 64 stderr, 65 "rustc -Whelp didn't return a status code, but stderr contained invalid UTF-8: {err}." 66 ), 67 Self::NoStatusErr(err) => writeln!( 68 stderr, 69 "rustc -Whelp didn't return a status code, and the following was written to stderr: {err}." 70 ), 71 Self::ErrStatusNoErr(code) => writeln!( 72 stderr, 73 "rustc -Whelp returned status {code}, but didn't write anything to stderr." 74 ), 75 Self::ErrStatusInvalidUtf8(code, err) => writeln!( 76 stderr, 77 "rustc -Whelp returned status {code}, but stderr contained invalid UTF-8: {err}." 78 ), 79 Self::ErrStatus(code, err) => writeln!( 80 stderr, 81 "rustc -Whelp returned status {code}, and the following was written to stderr: {err}." 82 ), 83 Self::SuccessErrInvalidUtf8(err) => writeln!( 84 stderr, 85 "rustc -Whelp returned a successful status code, but stderr contained invalid UTF-8: {err}." 86 ), 87 Self::SuccessErr(err) => writeln!( 88 stderr, 89 "rustc -Whelp returned a successful status code, but the following was written to stderr: {err}." 90 ), 91 }.map_or(ExitCode::FAILURE, |()| ExitCode::FAILURE) 92 } 93 } 94 /// `"rustc"`. 95 const RUSTC: &str = "rustc"; 96 /// Returns [`RUSTC`] as a `PathBuf`. 97 #[expect(clippy::unnecessary_wraps, reason = "unify with OpenBSD")] 98 #[cfg(not(target_os = "openbsd"))] 99 fn priv_sep<Never>() -> Result<PathBuf, Never> { 100 Ok(RUSTC.into()) 101 } 102 /// `unveil(2)`s the file system for read-only permissions. 103 /// Traverses `$PATH` to find `"rustc"`; when found, removes read permissions on the file system before 104 /// `unveil(2)`ing `"rustc"` with execute permissions. Last, `pledge(2)`s `c"exec proc stdio unveil"`. 105 #[expect(unsafe_code, reason = "comment justifies correctness")] 106 #[expect(clippy::option_if_let_else, reason = "false positive")] 107 #[cfg(target_os = "openbsd")] 108 fn priv_sep() -> Result<PathBuf, E> { 109 Permissions::unveil_raw(c"/", c"r") 110 .map_err(|e| E::Io(e.into())) 111 .and_then(|()| { 112 env::var_os("PATH").map_or(Err(E::NoPathVariable), |path| { 113 path.as_encoded_bytes() 114 .split(|b| *b == b':') 115 .try_fold((), |(), dir| { 116 // SAFETY: 117 // `dir` is obtained directly from `path.as_encoded_bytes` with at most a single 118 // `b':'` removed ensuring any valid UTF-8 that existed before still does. 119 let dir_os = unsafe { OsStr::from_encoded_bytes_unchecked(dir) }; 120 fs::read_dir(dir_os).map_or_else( 121 |e| { 122 if matches!(e.kind(), ErrorKind::NotADirectory) { 123 let val = PathBuf::from(dir_os); 124 match val.file_name() { 125 None => Ok(()), 126 Some(file) => { 127 if file == RUSTC { 128 Err(val) 129 } else { 130 Ok(()) 131 } 132 } 133 } 134 } else { 135 Ok(()) 136 } 137 }, 138 |mut ents| { 139 ents.try_fold((), |(), ent_res| { 140 ent_res.map_or(Ok(()), |ent| { 141 if ent.file_name() == RUSTC { 142 Err(PathBuf::from(dir_os).join(RUSTC)) 143 } else { 144 Ok(()) 145 } 146 }) 147 }) 148 }, 149 ) 150 }) 151 .map_or_else( 152 |rustc| { 153 Permissions::unveil_raw(c"/", c"") 154 .and_then(|()| { 155 Permissions::unveil_raw(&rustc, c"x").and_then(|()| { 156 Promises::pledge_raw(c"exec proc stdio unveil") 157 .map(|()| rustc) 158 }) 159 }) 160 .map_err(|e| E::Io(e.into())) 161 }, 162 |()| Err(E::NoRustcInPath), 163 ) 164 }) 165 }) 166 } 167 /// No-op. 168 #[expect(clippy::unnecessary_wraps, reason = "unify with OpenBSD")] 169 #[cfg(not(target_os = "openbsd"))] 170 const fn priv_sep_final<Never>(_: &Path) -> Result<(), Never> { 171 Ok(()) 172 } 173 /// Removes execute permissions on `path` before `pledge(2)`ing `c"stdio"`. 174 #[cfg(target_os = "openbsd")] 175 fn priv_sep_final(path: &Path) -> Result<(), E> { 176 Permissions::unveil_raw(path, c"") 177 .and_then(|()| Promises::pledge_raw(c"stdio")) 178 .map_err(|e| E::Io(e.into())) 179 } 180 /// No-op. 181 #[expect(clippy::unnecessary_wraps, reason = "unify with OpenBSD")] 182 #[cfg(not(target_os = "openbsd"))] 183 const fn priv_sep_stdin<Never>() -> Result<(), Never> { 184 Ok(()) 185 } 186 /// `pledge(2)`s `c"stdio"`. 187 #[cfg(target_os = "openbsd")] 188 fn priv_sep_stdin() -> Result<(), E> { 189 Promises::pledge_raw(c"stdio").map_err(|e| E::Io(e.into())) 190 } 191 /// Gets output of `rustc -Whelp`. 192 pub(crate) fn execute(read_stdin: bool) -> Result<Vec<u8>, E> { 193 if read_stdin { 194 priv_sep_stdin().and_then(|()| { 195 #[cfg(target_pointer_width = "16")] 196 let cap = 0x2000; 197 #[cfg(not(target_pointer_width = "16"))] 198 let cap = 0x10000; 199 let mut output = Vec::with_capacity(cap); 200 io::stdin() 201 .lock() 202 .read_to_end(&mut output) 203 .map_err(E::Io) 204 .map(|_| output) 205 }) 206 } else { 207 priv_sep().and_then(|path| { 208 Command::new(&path) 209 .arg("-Whelp") 210 .stderr(Stdio::piped()) 211 .stdin(Stdio::null()) 212 .stdout(Stdio::piped()) 213 .output() 214 .map_err(E::Io) 215 .and_then(|output| { 216 priv_sep_final(&path).and_then(|()| match output.status.code() { 217 None => { 218 if output.stderr.is_empty() { 219 Err(E::NoStatusNoErr) 220 } else { 221 String::from_utf8(output.stderr) 222 .map_err(E::NoStatusInvalidUtf8) 223 .and_then(|err| Err(E::NoStatusErr(err))) 224 } 225 } 226 Some(code) => { 227 if code == 0i32 { 228 if output.stderr.is_empty() { 229 Ok(output.stdout) 230 } else { 231 String::from_utf8(output.stderr) 232 .map_err(E::SuccessErrInvalidUtf8) 233 .and_then(|err| Err(E::SuccessErr(err))) 234 } 235 } else if output.stderr.is_empty() { 236 Err(E::ErrStatusNoErr(code)) 237 } else { 238 String::from_utf8(output.stderr) 239 .map_err(|err| E::ErrStatusInvalidUtf8(code, err)) 240 .and_then(|err| Err(E::ErrStatus(code, err))) 241 } 242 } 243 }) 244 }) 245 }) 246 } 247 } 248 #[cfg(test)] 249 mod tests { 250 #[cfg(not(target_os = "openbsd"))] 251 use core::convert::Infallible; 252 #[cfg(target_os = "openbsd")] 253 use std::ffi::OsString; 254 #[test] 255 #[cfg(not(target_os = "openbsd"))] 256 fn priv_sep() { 257 assert_eq!(super::priv_sep::<Infallible>(), Ok("rustc".into())); 258 } 259 #[ignore = "interferes with testing. should run separately to avoid issues."] 260 #[test] 261 #[cfg(target_os = "openbsd")] 262 fn priv_sep() { 263 assert!(super::priv_sep().is_ok_and( 264 |path| path.is_absolute() && path.file_name() == Some(&OsString::from("rustc")) 265 )); 266 } 267 }