zopen 1.0.1

Automatically open compressed files.
Documentation
/*
 * MIT License
 *
 * Copyright (c) 2017-2024 Frank Fischer <frank-fischer@shadow-soft.de>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

use std::ffi::OsStr;
use std::fs::File;
use std::io;
use std::path::Path;
use std::process::{Child, Command, Stdio};

/// Read handle that waits for command on close.
pub struct ToolRead(Option<Child>);

impl io::Read for ToolRead {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let child = self
            .0
            .as_mut()
            .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "external tool already closed".to_owned()))?;
        let n = child.stdout.as_mut().unwrap().read(buf)?;
        if n == 0 {
            let output = self.0.take().unwrap().wait_with_output()?;
            if !output.status.success() {
                let mut errmsg = String::from_utf8_lossy(&output.stderr).to_string();
                errmsg.truncate(errmsg.trim_end().len());
                return Err(io::Error::new(io::ErrorKind::Other, errmsg));
            }
        }
        Ok(n)
    }

    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
        let mut child = self
            .0
            .take()
            .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "external tool already closed".to_owned()))?;
        let n = child.stdout.as_mut().unwrap().read_to_end(buf)?;
        let output = child.wait_with_output()?;
        if !output.status.success() {
            let mut errmsg = String::from_utf8_lossy(&output.stderr).to_string();
            errmsg.truncate(errmsg.trim_end().len());
            return Err(io::Error::new(io::ErrorKind::Other, errmsg));
        }
        Ok(n)
    }
}

impl Drop for ToolRead {
    fn drop(&mut self) {
        if let Some(mut child) = self.0.take() {
            if let Err(_) | Ok(None) = child.try_wait() {
                let _ = child.kill(); // ignore the error
                child.wait().unwrap();
            }
        }
    }
}

impl ToolRead {
    /// Run a command and read from its standard output.
    ///
    /// The command's standard input will be closed.
    pub fn new<I, S>(cmd: &str, args: I) -> io::Result<ToolRead>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        Ok(ToolRead(Some(
            Command::new(cmd)
                .env_clear()
                .args(args)
                .stdin(Stdio::null())
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()?,
        )))
    }

    /// Run a command and read from its standard output.
    ///
    /// The command's standard input will read from the given file.
    pub fn new_with_file<I, S>(cmd: &str, args: I, path: impl AsRef<Path>) -> io::Result<ToolRead>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        Ok(ToolRead(Some(
            Command::new(cmd)
                .env_clear()
                .args(args)
                .stdin(File::open(path)?)
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()?,
        )))
    }
}

/// Write handle that waits for command on close.
pub struct ToolWrite(Child);

impl io::Write for ToolWrite {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.0.stdin.as_mut().unwrap().write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.0.stdin.as_mut().unwrap().flush()
    }
}

impl Drop for ToolWrite {
    fn drop(&mut self) {
        self.0.wait().unwrap();
    }
}

impl ToolWrite {
    /// Run a command and write to its standard input.
    ///
    /// The command's standard output will be closed.
    pub fn new<I, S>(cmd: &str, args: I) -> io::Result<ToolWrite>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        Ok(ToolWrite(
            Command::new(cmd)
                .env_clear()
                .args(args)
                .stdin(Stdio::piped())
                .stdout(Stdio::null())
                .spawn()?,
        ))
    }

    /// Run a command and write to its standard input.
    ///
    /// The command's standard output will written to the given file.
    pub fn new_with_file<I, S>(cmd: &str, args: I, path: impl AsRef<Path>) -> io::Result<ToolWrite>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        Ok(ToolWrite(
            Command::new(cmd)
                .env_clear()
                .args(args)
                .stdin(Stdio::piped())
                .stdout(File::create(path)?)
                .spawn()?,
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error;
    use std::io::{Read, Write};
    use tempfile::NamedTempFile;

    #[test]
    fn test_tool_read() -> Result<(), Box<dyn Error>> {
        let mut input = ToolRead::new("/usr/bin/echo", ["Hallo Welt"])?;
        let mut s = String::new();

        input.read_to_string(&mut s)?;
        assert_eq!(s, "Hallo Welt\n");

        Ok(())
    }

    #[test]
    fn test_tool_read_with_file() -> Result<(), Box<dyn Error>> {
        let mut f = NamedTempFile::new()?;
        writeln!(f, "Hello world")?;
        let mut input = ToolRead::new_with_file("/usr/bin/cat", &[] as &[&str], f.path().to_str().unwrap())?;
        let mut s = String::new();

        input.read_to_string(&mut s)?;
        assert_eq!(s, "Hello world\n");

        Ok(())
    }

    #[test]
    fn test_tool_write() -> Result<(), Box<dyn Error>> {
        let mut f = NamedTempFile::new()?;
        {
            let mut output = ToolWrite::new("/usr/bin/tee", [f.path().to_str().unwrap()])?;
            writeln!(output, "Hello world!")?;
        }
        let mut s = String::new();

        f.read_to_string(&mut s)?;
        assert_eq!(s, "Hello world!\n");

        Ok(())
    }

    #[test]
    fn test_tool_write_file() -> Result<(), Box<dyn Error>> {
        let mut f = NamedTempFile::new()?;
        {
            let mut output = ToolWrite::new_with_file("/usr/bin/tac", &[] as &[&str], f.path().to_str().unwrap())?;
            writeln!(output, "Welt")?;
            writeln!(output, "Hallo")?;
        }

        let mut s = String::new();
        f.read_to_string(&mut s)?;
        assert_eq!(s, "Hallo\nWelt\n");

        Ok(())
    }
}