Skip to main content

coreutils_rs/hash/
core.rs

1use std::fs::File;
2use std::io::{self, BufRead, BufReader, Read, Write};
3use std::path::Path;
4
5use blake2::Blake2b512;
6use md5::Md5;
7use sha2::{Digest, Sha256};
8
9/// Supported hash algorithms.
10#[derive(Debug, Clone, Copy)]
11pub enum HashAlgorithm {
12    Sha256,
13    Md5,
14    Blake2b,
15}
16
17impl HashAlgorithm {
18    pub fn name(self) -> &'static str {
19        match self {
20            HashAlgorithm::Sha256 => "SHA256",
21            HashAlgorithm::Md5 => "MD5",
22            HashAlgorithm::Blake2b => "BLAKE2b",
23        }
24    }
25}
26
27/// Compute hash of data from a reader, returning hex string.
28pub fn hash_reader<R: Read>(algo: HashAlgorithm, mut reader: R) -> io::Result<String> {
29    let mut buf = vec![0u8; 256 * 1024]; // 256KB buffer like GNU
30
31    match algo {
32        HashAlgorithm::Sha256 => {
33            let mut hasher = Sha256::new();
34            loop {
35                let n = reader.read(&mut buf)?;
36                if n == 0 {
37                    break;
38                }
39                hasher.update(&buf[..n]);
40            }
41            Ok(hex_encode(&hasher.finalize()))
42        }
43        HashAlgorithm::Md5 => {
44            let mut hasher = Md5::new();
45            loop {
46                let n = reader.read(&mut buf)?;
47                if n == 0 {
48                    break;
49                }
50                hasher.update(&buf[..n]);
51            }
52            Ok(hex_encode(&hasher.finalize()))
53        }
54        HashAlgorithm::Blake2b => {
55            let mut hasher = Blake2b512::new();
56            loop {
57                let n = reader.read(&mut buf)?;
58                if n == 0 {
59                    break;
60                }
61                hasher.update(&buf[..n]);
62            }
63            Ok(hex_encode(&hasher.finalize()))
64        }
65    }
66}
67
68/// Hash a file by path. Returns the hex digest.
69pub fn hash_file(algo: HashAlgorithm, path: &Path) -> io::Result<String> {
70    let file = File::open(path)?;
71    let reader = BufReader::with_capacity(256 * 1024, file);
72    hash_reader(algo, reader)
73}
74
75/// Hash stdin. Returns the hex digest.
76pub fn hash_stdin(algo: HashAlgorithm) -> io::Result<String> {
77    hash_reader(algo, io::stdin().lock())
78}
79
80/// Print hash result in GNU format: "hash  filename\n"
81pub fn print_hash(
82    out: &mut impl Write,
83    hash: &str,
84    filename: &str,
85    binary: bool,
86) -> io::Result<()> {
87    let mode_char = if binary { '*' } else { ' ' };
88    writeln!(out, "{} {}{}", hash, mode_char, filename)
89}
90
91/// Print hash result in BSD tag format: "ALGO (filename) = hash\n"
92pub fn print_hash_tag(
93    out: &mut impl Write,
94    algo: HashAlgorithm,
95    hash: &str,
96    filename: &str,
97) -> io::Result<()> {
98    writeln!(out, "{} ({}) = {}", algo.name(), filename, hash)
99}
100
101/// Options for check mode.
102pub struct CheckOptions {
103    pub quiet: bool,
104    pub status_only: bool,
105    pub strict: bool,
106    pub warn: bool,
107}
108
109/// Verify checksums from a check file.
110/// Each line should be "hash  filename" or "hash *filename".
111/// Returns (ok_count, fail_count, error_count).
112pub fn check_file<R: BufRead>(
113    algo: HashAlgorithm,
114    reader: R,
115    opts: &CheckOptions,
116    out: &mut impl Write,
117    err_out: &mut impl Write,
118) -> io::Result<(usize, usize, usize)> {
119    let CheckOptions {
120        quiet,
121        status_only,
122        strict,
123        warn,
124    } = *opts;
125    let mut ok_count = 0;
126    let mut fail_count = 0;
127    let mut format_errors = 0;
128    let mut line_num = 0;
129
130    for line_result in reader.lines() {
131        line_num += 1;
132        let line = line_result?;
133        let line = line.trim_end();
134
135        if line.is_empty() {
136            continue;
137        }
138
139        // Parse "hash  filename" or "hash *filename"
140        let (expected_hash, filename) = match parse_check_line(line) {
141            Some(v) => v,
142            None => {
143                format_errors += 1;
144                if warn {
145                    writeln!(
146                        err_out,
147                        "line {}: improperly formatted checksum line",
148                        line_num
149                    )?;
150                }
151                continue;
152            }
153        };
154
155        // Compute actual hash
156        let actual = match hash_file(algo, Path::new(filename)) {
157            Ok(h) => h,
158            Err(e) => {
159                fail_count += 1;
160                if !status_only {
161                    writeln!(err_out, "{}: FAILED open or read: {}", filename, e)?;
162                }
163                continue;
164            }
165        };
166
167        if actual == expected_hash {
168            ok_count += 1;
169            if !quiet && !status_only {
170                writeln!(out, "{}: OK", filename)?;
171            }
172        } else {
173            fail_count += 1;
174            if !status_only {
175                writeln!(out, "{}: FAILED", filename)?;
176            }
177        }
178    }
179
180    if strict && format_errors > 0 {
181        fail_count += format_errors;
182    }
183
184    Ok((ok_count, fail_count, format_errors))
185}
186
187/// Parse a checksum line: "hash  filename" or "hash *filename"
188pub(crate) fn parse_check_line(line: &str) -> Option<(&str, &str)> {
189    // Find the two-space separator
190    if let Some(idx) = line.find("  ") {
191        let hash = &line[..idx];
192        let rest = &line[idx + 2..];
193        return Some((hash, rest));
194    }
195    // Try "hash *filename" (binary mode marker)
196    if let Some(idx) = line.find(" *") {
197        let hash = &line[..idx];
198        let rest = &line[idx + 2..];
199        return Some((hash, rest));
200    }
201    None
202}
203
204/// Convert bytes to lowercase hex string.
205pub(crate) fn hex_encode(bytes: &[u8]) -> String {
206    let mut s = String::with_capacity(bytes.len() * 2);
207    for &b in bytes {
208        s.push_str(&format!("{:02x}", b));
209    }
210    s
211}