summary refs log tree commit diff
path: root/src/filter.rs
blob: 3352b160a21abbfaa5e7622503ae679d76477bef (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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(())
    }
}