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