Skip to main content

chksum_cli/
lib.rs

1#![allow(clippy::multiple_crate_versions)]
2#![forbid(unsafe_code)]
3
4#[cfg(feature = "color")]
5mod color;
6#[cfg(feature = "md5")]
7pub mod md5;
8#[cfg(feature = "sha1")]
9mod sha1;
10#[cfg(feature = "sha2-224")]
11mod sha2_224;
12#[cfg(feature = "sha2-256")]
13mod sha2_256;
14#[cfg(feature = "sha2-384")]
15mod sha2_384;
16#[cfg(feature = "sha2-512")]
17mod sha2_512;
18
19use std::io::{self, IsTerminal, Write, stderr, stdin, stdout};
20use std::path::PathBuf;
21use std::sync::mpsc;
22use std::thread;
23
24use chksum::{Digest, Error, Hash, chksum};
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(Debug, clap::Parser)]
34#[command(name = "chksum", version, about, long_about = None)]
35pub struct Command {
36    #[command(subcommand)]
37    pub subcommand: Subcommand,
38    /// Show colored output.
39    #[arg(value_enum, short, long, default_value_t = Color::Auto, global = true)]
40    #[cfg(feature = "color")]
41    pub color: Color,
42}
43
44#[derive(Debug, clap::Subcommand)]
45pub enum Subcommand {
46    /// Calculate MD5 digest.
47    #[cfg(feature = "md5")]
48    MD5(md5::Subcommand),
49    /// Calculate SHA-1 digest.
50    #[cfg(feature = "sha1")]
51    SHA1(sha1::Subcommand),
52    /// Calculate SHA-2 224 digest.
53    #[cfg(feature = "sha2-224")]
54    SHA2_224(sha2_224::Subcommand),
55    /// Calculate SHA-2 256 digest.
56    #[cfg(feature = "sha2-256")]
57    SHA2_256(sha2_256::Subcommand),
58    /// Calculate SHA-2 384 digest.
59    #[cfg(feature = "sha2-384")]
60    SHA2_384(sha2_384::Subcommand),
61    /// Calculate SHA-2 512 digest.
62    #[cfg(feature = "sha2-512")]
63    SHA2_512(sha2_512::Subcommand),
64}
65
66#[derive(Debug, clap::Args)]
67pub struct Args {
68    /// Path to file or directory.
69    #[arg(value_name = "PATH")]
70    pub paths: Vec<PathBuf>,
71}
72
73/// Prints result to stdout or stderr.
74fn print_result(
75    stdout: &mut impl Write,
76    stderr: &mut impl Write,
77    input: String,
78    result: Result<impl Digest, Error>,
79) -> io::Result<()> {
80    match result {
81        Ok(digest) => writeln!(stdout, "{input}: {digest}"),
82        Err(error) => {
83            let error = error.to_string().to_lowercase();
84            let error = format!("{input}: {error}");
85            #[cfg(feature = "color")]
86            let error = error.red();
87            writeln!(stderr, "{error}")
88        },
89    }
90}
91
92/// Handles subcommand execution.
93pub(crate) fn subcommand<T>(args: &Args) -> i32
94where
95    T: Hash,
96    T::Digest: 'static + Send,
97{
98    let (tx, rx) = mpsc::sync_channel(1);
99
100    let printer = thread::spawn(move || {
101        let mut stdout = stdout().lock();
102        let mut stderr = stderr().lock();
103        while let Ok((input, result)) = rx.recv() {
104            print_result(&mut stdout, &mut stderr, input, result).expect("Cannot print result");
105        }
106    });
107
108    let mut rc = EXITCODE_OK;
109
110    let stdin_handle = stdin();
111    let stdin_thread = if !stdin_handle.is_terminal() {
112        let tx = tx.clone();
113        Some(thread::spawn(move || {
114            let result = chksum::<T>(stdin_handle.lock());
115            let rc = to_exit_code(&result);
116            tx.send(("<stdin>".to_string(), result))
117                .expect("Cannot send result to printer thread");
118            rc
119        }))
120    } else {
121        None
122    };
123
124    if !args.paths.is_empty() {
125        let path_rc = args
126            .paths
127            .par_iter()
128            .map(|path| {
129                let result = chksum::<T>(path);
130                let path_rc = to_exit_code(&result);
131                tx.send((path.display().to_string(), result)).expect("Cannot send result to printer thread");
132                path_rc
133            })
134            // propagates any error exit code; OK (0) is the identity element
135            .reduce(|| EXITCODE_OK, |acc, rc| if acc == EXITCODE_OK { rc } else { acc });
136
137        if rc == EXITCODE_OK {
138            rc = path_rc;
139        }
140    }
141
142    if let Some(handle) = stdin_thread {
143        let stdin_rc = handle.join().expect("The stdin thread has panicked");
144        if rc == EXITCODE_OK {
145            rc = stdin_rc;
146        }
147    }
148
149    // drop sender so the receiver loop terminates once all results are consumed
150    drop(tx);
151
152    printer.join().expect("The printer thread has panicked");
153
154    rc
155}
156
157/// Maps a chksum result to a process exit code.
158fn to_exit_code<T>(result: &Result<T, Error>) -> i32
159where
160    T: Digest,
161{
162    if result.is_ok() { EXITCODE_OK } else { EXITCODE_IOERR }
163}
164
165#[cfg(test)]
166mod tests {
167    use anyhow::Result;
168    use assert_fs::TempDir;
169    use assert_fs::prelude::PathChild;
170    use chksum::MD5;
171
172    use super::*;
173
174    #[test]
175    fn exitcode_ok() -> Result<()> {
176        let tmpdir = TempDir::new()?;
177
178        let result = chksum::<MD5>(tmpdir.path());
179        assert_eq!(to_exit_code(&result), EXITCODE_OK);
180
181        Ok(())
182    }
183
184    #[test]
185    fn exitcode_error() -> Result<()> {
186        let tmpdir = TempDir::new()?;
187        let child = tmpdir.child("child");
188
189        let result = chksum::<MD5>(child.path());
190        assert_eq!(to_exit_code(&result), EXITCODE_IOERR);
191
192        Ok(())
193    }
194}