// SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021 Alyssa Ross use std::ffi::{CStr, OsString}; use std::fmt::{Display, Formatter}; use std::io::{self, ErrorKind, Write}; use std::os::raw::{c_char, c_int}; use std::path::Path; use std::process::{Child, ChildStdout, Command, Stdio}; use std::thread::{spawn, JoinHandle}; use git2::{Repository, Tree}; use libc::{SIGPIPE, WEXITSTATUS, WIFEXITED, WIFSIGNALED, WTERMSIG}; extern "C" { fn sigdescr_np(sig: c_int) -> *const c_char; } unsafe fn wait(pid: u32) -> io::Result { let mut wstatus: c_int = 0; if libc::waitpid(pid as i32, &mut wstatus, 0) == -1 { return Err(io::Error::last_os_error()); } Ok(wstatus) } fn filter_output(argv: &[OsString]) -> Child { Command::new(argv.get(0).unwrap()) .args(&argv[1..]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("spawn") } fn tree_path_content(repo: &Repository, tree: &Tree, path: &Path) -> Result, git2::Error> { let blob = tree.get_path(path)?.to_object(&repo)?.peel_to_blob()?; Ok(blob.content().to_vec()) } #[derive(Debug)] pub enum FilterError { Write(io::Error), Exit(c_int), Signal(c_int), } impl Display for FilterError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { use FilterError::*; match self { Write(e) => write!(f, "writing to filter stdin: {}", e), Exit(code) => write!(f, "filter command failed with code {}", code), Signal(signo) => { let sigdescr = unsafe { CStr::from_ptr(sigdescr_np(*signo)) }.to_string_lossy(); write!(f, "filter command killed by signal: {}", sigdescr) } } } } impl std::error::Error for FilterError {} pub struct Filter { pid: u32, writer: JoinHandle>, } impl Filter { pub fn new( repo: &Repository, tree: &Tree, path: &Path, args: &[OsString], ) -> (Self, ChildStdout) { let blob = tree_path_content(repo, tree, path).unwrap(); let child = filter_output(args); let pid = child.id(); // Destructure here to make sure there are no // remaining references to the Child structs after // this. This makes sure nothing else will wait for // our filter processes, and we don't have to worry // about PID reuse. let Child { stdin, stdout, .. } = child; let mut stdin = stdin.unwrap(); let stdout = stdout.unwrap(); let writer = spawn(move || stdin.write_all(&blob)); (Self { pid, writer }, stdout) } pub fn wait(self) -> Result<(), FilterError> { if let Err(e) = self.writer.join().unwrap() { if e.kind() != ErrorKind::BrokenPipe { return Err(FilterError::Write(e)); } } let wstatus = unsafe { wait(self.pid).unwrap() }; if WIFEXITED(wstatus) { let status = WEXITSTATUS(wstatus); if status != 0 { return Err(FilterError::Exit(status)); } } else if WIFSIGNALED(wstatus) { let signal = WTERMSIG(wstatus); if signal != SIGPIPE { return Err(FilterError::Signal(signal)); } } else { unreachable!() } Ok(()) } }