chksum_cli/
lib.rs

1#![forbid(unsafe_code)]
2
3#[cfg(feature = "color")]
4mod color;
5#[cfg(feature = "md5")]
6mod md5;
7#[cfg(feature = "sha1")]
8mod sha1;
9#[cfg(feature = "sha2-224")]
10mod sha2_224;
11#[cfg(feature = "sha2-256")]
12mod sha2_256;
13#[cfg(feature = "sha2-384")]
14mod sha2_384;
15#[cfg(feature = "sha2-512")]
16mod sha2_512;
17
18use std::fmt::{self, Display, Formatter};
19use std::io::{self, stderr, stdin, stdout, Write};
20use std::path::{Path, PathBuf};
21use std::sync::mpsc;
22use std::thread;
23
24use chksum::{chksum, Digest, Error, Hash};
25#[cfg(feature = "color")]
26use colored::Colorize;
27use exitcode::{IOERR as EXITCODE_IOERR, OK as EXITCODE_OK};
28use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
29
30#[cfg(feature = "color")]
31pub use crate::color::Color;
32
33#[derive(Clone, Debug)]
34enum Input {
35    Path(PathBuf),
36    Stdin,
37}
38
39impl Display for Input {
40    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::Path(path) => write!(f, "{}", path.display()),
43            Self::Stdin => write!(f, "<stdin>"),
44        }
45    }
46}
47
48impl<T> From<T> for Input
49where
50    T: AsRef<Path>,
51{
52    fn from(value: T) -> Self {
53        let value = value.as_ref();
54        Self::Path(value.to_path_buf())
55    }
56}
57
58#[derive(Debug, clap::Parser)]
59#[command(name = "chksum", version, about, long_about = None)]
60pub struct Command {
61    #[command(subcommand)]
62    pub subcommand: Subcommand,
63    /// Show colored output.
64    #[arg(value_enum, short, long, default_value_t = Color::Auto, global = true)]
65    #[cfg(feature = "color")]
66    pub color: Color,
67}
68
69#[derive(Debug, clap::Subcommand)]
70pub enum Subcommand {
71    /// Calculate MD5 digest.
72    #[cfg(feature = "md5")]
73    #[command(arg_required_else_help = true)]
74    MD5(md5::Subcommand),
75    /// Calculate SHA-1 digest.
76    #[cfg(feature = "sha1")]
77    #[command(arg_required_else_help = true)]
78    SHA1(sha1::Subcommand),
79    /// Calculate SHA-2 224 digest.
80    #[cfg(feature = "sha2-224")]
81    #[command(arg_required_else_help = true)]
82    SHA2_224(sha2_224::Subcommand),
83    /// Calculate SHA-2 256 digest.
84    #[cfg(feature = "sha2-256")]
85    #[command(arg_required_else_help = true)]
86    SHA2_256(sha2_256::Subcommand),
87    /// Calculate SHA-2 384 digest.
88    #[cfg(feature = "sha2-384")]
89    #[command(arg_required_else_help = true)]
90    SHA2_384(sha2_384::Subcommand),
91    /// Calculate SHA-2 512 digest.
92    #[cfg(feature = "sha2-512")]
93    #[command(arg_required_else_help = true)]
94    SHA2_512(sha2_512::Subcommand),
95}
96
97#[derive(Debug, clap::Args)]
98pub(crate) struct Args {
99    /// Path to file or directory.
100    #[arg(required = true, value_name = "PATH", conflicts_with = "stdin")]
101    pub paths: Vec<PathBuf>,
102}
103
104#[derive(Debug, clap::Args)]
105pub(crate) struct Options {
106    /// Calculate digest from stdin.
107    #[arg(short, long, default_value_t = false, conflicts_with = "paths")]
108    pub stdin: bool,
109}
110
111/// Prints result to stdout or stderr.
112fn print_result(
113    stdout: &mut impl Write,
114    stderr: &mut impl Write,
115    input: Input,
116    result: Result<impl Digest, Error>,
117) -> io::Result<()> {
118    match result {
119        Ok(digest) => writeln!(stdout, "{input}: {digest}"),
120        Err(error) => {
121            let error = error.to_string().to_lowercase();
122            let error = format!("{input}: {error}");
123            #[cfg(feature = "color")]
124            let error = error.red();
125            writeln!(stderr, "{error}")
126        },
127    }
128}
129
130/// Handles subcommand execution.
131pub(crate) fn subcommand<T>(args: &Args, options: &Options) -> i32
132where
133    T: Hash,
134    T::Digest: 'static + Send,
135{
136    let (tx, rx) = mpsc::sync_channel(1);
137
138    let printer = thread::spawn(move || {
139        let mut stdout = stdout().lock();
140        let mut stderr = stderr().lock();
141        while let Ok(pair) = rx.recv() {
142            let (input, result) = pair;
143            print_result(&mut stdout, &mut stderr, input, result).expect("Cannot print result");
144        }
145    });
146
147    let rc = if options.stdin {
148        let handle = stdin().lock();
149        let result = chksum::<T>(handle);
150        let rc = exitcode(&result);
151        let pair = (Input::Stdin, result);
152        tx.send(pair).expect("Cannot send result to printer thread");
153        rc
154    } else {
155        args.paths
156            .par_iter()
157            .map(|path| {
158                let result = chksum::<T>(path);
159                let rc = exitcode(&result);
160                let pair = (path.into(), result);
161                tx.send(pair).expect("Cannot send result to printer thread");
162                rc
163            })
164            // returns first occured error
165            .reduce(|| EXITCODE_OK, |acc, rc| if acc == EXITCODE_OK { rc } else { acc })
166    };
167
168    drop(tx); // must drop manually, otherwise rx.recv() never return an error
169
170    printer.join().expect("The printer thread has panicked");
171
172    rc
173}
174
175/// Turns result to exitcode.
176fn exitcode<T>(result: &Result<T, Error>) -> i32
177where
178    T: Digest,
179{
180    if result.is_ok() {
181        EXITCODE_OK
182    } else {
183        EXITCODE_IOERR
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use anyhow::Result;
190    use assert_fs::prelude::PathChild;
191    use assert_fs::TempDir;
192    use chksum::MD5;
193
194    use super::*;
195
196    #[test]
197    fn exitcode_ok() -> Result<()> {
198        let tmpdir = TempDir::new()?;
199
200        let result = chksum::<MD5>(tmpdir.path());
201        assert_eq!(exitcode(&result), EXITCODE_OK);
202
203        Ok(())
204    }
205
206    #[test]
207    fn exitcode_error() -> Result<()> {
208        let tmpdir = TempDir::new()?;
209        let child = tmpdir.child("child");
210
211        let result = chksum::<MD5>(child.path());
212        assert_eq!(exitcode(&result), EXITCODE_IOERR);
213
214        Ok(())
215    }
216}