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