main.rs (9478B)
1 //! # `git_index` 2 //! 3 //! Consult [`README.md`](https://git.philomathiclife.com/git_index/file/README.md.html). 4 extern crate alloc; 5 /// Module that parsed passed options into the application. 6 mod args; 7 use alloc::collections::BTreeMap; 8 use args::{AbsDirPath, ArgsErr}; 9 #[cfg(not(target_os = "openbsd"))] 10 use core::convert::Infallible; 11 use core::{ 12 error, 13 ffi::CStr, 14 fmt::{self, Display, Formatter}, 15 str::{self, Utf8Error}, 16 }; 17 use jiff::{Error as TimeErr, Timestamp, fmt::temporal::DateTimeParser}; 18 #[cfg(target_os = "openbsd")] 19 use priv_sep::{Errno, Permissions, Promise, Promises}; 20 use std::{ 21 ffi::OsString, 22 fs, 23 io::{self, Error, Write as _}, 24 path::PathBuf, 25 process::{Command, Stdio}, 26 }; 27 /// Module for reading the options passed to the application. 28 /// Error returned from the program. 29 enum E { 30 /// Variant for errors due to incorrect arguments being passed. 31 Args(ArgsErr), 32 /// Variant for errors due to calls to `pledge`. 33 #[cfg(target_os = "openbsd")] 34 Pledge(Errno), 35 /// Unify with non-OpenBSD. 36 #[cfg(not(target_os = "openbsd"))] 37 Pledge(Infallible), 38 /// Variant for errors due to calls to `unveil`. 39 #[cfg(target_os = "openbsd")] 40 Unveil(Errno), 41 /// Unify with non-OpenBSD. 42 #[cfg(not(target_os = "openbsd"))] 43 Unveil(Infallible), 44 /// Variant for IO errors. 45 Io(Error), 46 /// Variant when a git repo directory is not valid UTF-8. 47 NonUtf8Path(OsString), 48 /// Variant when git errors. 49 GitErr(Vec<u8>), 50 /// Variant when git output is not valid UTF-8. 51 Git(Utf8Error), 52 /// Variant when git output is not a valid ISO 8601 timestamp. 53 Time(TimeErr), 54 } 55 impl fmt::Debug for E { 56 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 57 <Self as Display>::fmt(self, f) 58 } 59 } 60 impl Display for E { 61 #[expect( 62 clippy::use_debug, 63 reason = "some contained errors don't implement Display" 64 )] 65 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 66 match *self { 67 Self::Args(ref e) => e.fmt(f), 68 #[cfg(target_os = "openbsd")] 69 Self::Pledge(err) => write!(f, "pledge(2) failed with {err}"), 70 #[cfg(not(target_os = "openbsd"))] 71 Self::Pledge(e) => match e {}, 72 #[cfg(target_os = "openbsd")] 73 Self::Unveil(err) => write!(f, "unveil(2) failed with {err}"), 74 #[cfg(not(target_os = "openbsd"))] 75 Self::Unveil(e) => match e {}, 76 Self::Io(ref e) => e.fmt(f), 77 Self::NonUtf8Path(ref e) => write!(f, "{} is not valid UTF-8", e.display()), 78 Self::GitErr(ref e) => match str::from_utf8(e.as_slice()) { 79 Ok(err) => write!(f, "git errored with: {err}"), 80 Err(err) => write!(f, "git errored with an invalid UTF-8 error: {err}"), 81 }, 82 Self::Git(ref e) => write!( 83 f, 84 "git succeeded, but its output was not valid UTF-8: {e:?}" 85 ), 86 Self::Time(ref e) => write!( 87 f, 88 "git succeeded, but its output is not a valid ISO 8601 timestamp: {e}" 89 ), 90 } 91 } 92 } 93 impl error::Error for E {} 94 impl From<ArgsErr> for E { 95 fn from(value: ArgsErr) -> Self { 96 Self::Args(value) 97 } 98 } 99 impl From<Error> for E { 100 fn from(value: Error) -> Self { 101 Self::Io(value) 102 } 103 } 104 impl From<Utf8Error> for E { 105 fn from(value: Utf8Error) -> Self { 106 Self::Git(value) 107 } 108 } 109 impl From<OsString> for E { 110 fn from(value: OsString) -> Self { 111 Self::NonUtf8Path(value) 112 } 113 } 114 impl From<TimeErr> for E { 115 fn from(value: TimeErr) -> Self { 116 Self::Time(value) 117 } 118 } 119 /// Calls `pledge` with only the sys calls necessary for a minimal application 120 /// to run. Specifically, the `Promise`s `Exec`, `Proc`, `Rpath`, `Stdio`, and `Unveil` 121 /// are passed. 122 #[cfg(target_os = "openbsd")] 123 fn pledge_init() -> Result<Promises, Errno> { 124 let promises = Promises::new([ 125 Promise::Exec, 126 Promise::Proc, 127 Promise::Rpath, 128 Promise::Stdio, 129 Promise::Unveil, 130 ]); 131 match promises.pledge() { 132 Ok(()) => Ok(promises), 133 Err(e) => Err(e), 134 } 135 } 136 /// No-op that returns `Ok`. 137 #[expect( 138 clippy::unnecessary_wraps, 139 reason = "consistent API as openbsd feature" 140 )] 141 #[cfg(not(target_os = "openbsd"))] 142 const fn pledge_init() -> Result<(), Infallible> { 143 Ok(()) 144 } 145 /// Removes `Promise::Unveil`. 146 #[cfg(target_os = "openbsd")] 147 fn pledge_away_unveil(promises: &mut Promises) -> Result<(), Errno> { 148 promises.remove_then_pledge(Promise::Unveil) 149 } 150 /// No-op that returns `Ok`. 151 #[expect( 152 clippy::unnecessary_wraps, 153 reason = "consistent API as openbsd feature" 154 )] 155 #[cfg(not(target_os = "openbsd"))] 156 const fn pledge_away_unveil(_: &mut ()) -> Result<(), Infallible> { 157 Ok(()) 158 } 159 /// Removes all `Promise`s except `Stdio`. 160 #[cfg(target_os = "openbsd")] 161 fn pledge_away_all_but_stdio(promises: &mut Promises) -> Result<(), Errno> { 162 promises.retain_then_pledge([Promise::Stdio]) 163 } 164 /// No-op that returns `Ok`. 165 #[expect( 166 clippy::unnecessary_wraps, 167 reason = "consistent API as openbsd feature" 168 )] 169 #[cfg(not(target_os = "openbsd"))] 170 const fn pledge_away_all_but_stdio(_: &mut ()) -> Result<(), Infallible> { 171 Ok(()) 172 } 173 /// Calls `unveil_none` on `/`. 174 #[cfg(target_os = "openbsd")] 175 fn veil_all() -> Result<(), Errno> { 176 Permissions::NONE.unveil(c"/") 177 } 178 /// No-op that returns `Ok`. 179 #[expect( 180 clippy::unnecessary_wraps, 181 reason = "consistent API as openbsd feature" 182 )] 183 #[cfg(not(target_os = "openbsd"))] 184 const fn veil_all() -> Result<(), Infallible> { 185 Ok(()) 186 } 187 /// Calls `unveil`_on `GIT_CSTR` with `Permissions::EXECUTE`. 188 #[cfg(target_os = "openbsd")] 189 fn unveil_git() -> Result<(), Errno> { 190 Permissions::EXECUTE.unveil(GIT_CSTR) 191 } 192 /// No-op that returns `Ok`. 193 #[expect( 194 clippy::unnecessary_wraps, 195 reason = "consistent API as openbsd feature" 196 )] 197 #[cfg(not(target_os = "openbsd"))] 198 const fn unveil_git() -> Result<(), Infallible> { 199 Ok(()) 200 } 201 /// Calls `unveil`_on `path` with `Permissions::READ`. 202 #[cfg(target_os = "openbsd")] 203 fn unveil_read(path: &AbsDirPath) -> Result<(), Errno> { 204 Permissions::READ.unveil(path.as_cstr()) 205 } 206 /// No-op that returns `Ok`. 207 #[expect( 208 clippy::unnecessary_wraps, 209 reason = "consistent API as openbsd feature" 210 )] 211 #[cfg(not(target_os = "openbsd"))] 212 const fn unveil_read(_: &AbsDirPath) -> Result<(), Infallible> { 213 Ok(()) 214 } 215 /// For each entry in `dir`, `git` is forked and invoked with `-C <dir><entry_in_dir> log -1 --date=iso-strict --pretty=format:"%cd"`. 216 fn get_git_data(map: &mut BTreeMap<Timestamp, Vec<String>>, dir: PathBuf) -> Result<(), E> { 217 let parser = DateTimeParser::new(); 218 for entry in fs::read_dir(dir)? { 219 let repo = entry?; 220 if repo.file_type()?.is_dir() { 221 let path = repo.path().into_os_string().into_string()?; 222 let output = Command::new(GIT) 223 .stderr(Stdio::piped()) 224 .stdin(Stdio::null()) 225 .stdout(Stdio::piped()) 226 .args([ 227 "-C", 228 path.as_str(), 229 "log", 230 "-1", 231 "--date=iso-strict", 232 "--pretty=format:%cd", 233 ]) 234 .output()?; 235 if output.status.success() { 236 let value = str::from_utf8(output.stdout.as_slice())?; 237 parser.parse_timestamp(value).map(|time| { 238 map.entry(time) 239 .or_insert_with(|| Vec::with_capacity(1)) 240 .push(path); 241 })?; 242 } else { 243 return Err(E::GitErr(output.stderr)); 244 } 245 } 246 } 247 Ok(()) 248 } 249 /// Writes each `String` in each `Vec` to `stdout` with spaces in between in descending order 250 /// of `map`. 251 fn write_results(map: BTreeMap<Timestamp, Vec<String>>) -> Result<(), Error> { 252 let mut stdout = io::stdout().lock(); 253 map.into_values().rev().try_fold((), |(), paths| { 254 paths 255 .into_iter() 256 .try_fold((), |(), path| write!(&mut stdout, "{path} ")) 257 }) 258 } 259 /// The absolute path to `git`. 260 const GIT_CSTR: &CStr = c"/usr/local/bin/git"; 261 /// The absolute path to `git`. 262 const GIT: &str = match GIT_CSTR.to_str() { 263 Ok(val) => val, 264 Err(_) => panic!("/usr/local/bin/git is not a valid str"), 265 }; 266 fn main() -> Result<(), E> { 267 pledge_init().map_err(E::Pledge).and_then(|mut promises| { 268 veil_all().map_err(E::Unveil).and_then(|()| { 269 args::from_env_args().map_err(E::Args).and_then(|git_dir| { 270 unveil_read(&git_dir).map_err(E::Unveil).and_then(|()| { 271 unveil_git().map_err(E::Unveil).and_then(|()| { 272 pledge_away_unveil(&mut promises) 273 .map_err(E::Pledge) 274 .and_then(|()| { 275 let mut map = BTreeMap::new(); 276 get_git_data(&mut map, git_dir.into_path_buf()).and_then(|()| { 277 pledge_away_all_but_stdio(&mut promises) 278 .map_err(E::Pledge) 279 .and_then(|()| write_results(map).map_err(E::Io)) 280 }) 281 }) 282 }) 283 }) 284 }) 285 }) 286 }) 287 }