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 #[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 #[cfg(feature = "md5")]
73 #[command(arg_required_else_help = true)]
74 MD5(md5::Subcommand),
75 #[cfg(feature = "sha1")]
77 #[command(arg_required_else_help = true)]
78 SHA1(sha1::Subcommand),
79 #[cfg(feature = "sha2-224")]
81 #[command(arg_required_else_help = true)]
82 SHA2_224(sha2_224::Subcommand),
83 #[cfg(feature = "sha2-256")]
85 #[command(arg_required_else_help = true)]
86 SHA2_256(sha2_256::Subcommand),
87 #[cfg(feature = "sha2-384")]
89 #[command(arg_required_else_help = true)]
90 SHA2_384(sha2_384::Subcommand),
91 #[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 #[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 #[arg(short, long, default_value_t = false, conflicts_with = "paths")]
108 pub stdin: bool,
109}
110
111fn 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
130pub(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 .reduce(|| EXITCODE_OK, |acc, rc| if acc == EXITCODE_OK { rc } else { acc })
166 };
167
168 drop(tx); printer.join().expect("The printer thread has panicked");
171
172 rc
173}
174
175fn 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}