summary refs log tree commit diff
path: root/src/filter.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/filter.rs')
-rw-r--r--src/filter.rs120
1 files changed, 120 insertions, 0 deletions
diff --git a/src/filter.rs b/src/filter.rs
new file mode 100644
index 0000000..3352b16
--- /dev/null
+++ b/src/filter.rs
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: EUPL-1.2
+// SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is>
+
+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<c_int> {
+    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<Vec<u8>, 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<io::Result<()>>,
+}
+
+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(())
+    }
+}