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 #[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 #[cfg(feature = "md5")]
48 MD5(md5::Subcommand),
49 #[cfg(feature = "sha1")]
51 SHA1(sha1::Subcommand),
52 #[cfg(feature = "sha2-224")]
54 SHA2_224(sha2_224::Subcommand),
55 #[cfg(feature = "sha2-256")]
57 SHA2_256(sha2_256::Subcommand),
58 #[cfg(feature = "sha2-384")]
60 SHA2_384(sha2_384::Subcommand),
61 #[cfg(feature = "sha2-512")]
63 SHA2_512(sha2_512::Subcommand),
64}
65
66#[derive(Debug, clap::Args)]
67pub struct Args {
68 #[arg(value_name = "PATH")]
70 pub paths: Vec<PathBuf>,
71}
72
73fn 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
92pub(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 .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(tx);
151
152 printer.join().expect("The printer thread has panicked");
153
154 rc
155}
156
157fn 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}