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