1use crate::rgh::file::{
8 DirectoryHashPlan, EntryStatus, ErrorHandlingProfile,
9 ManifestEntry, ManifestOutcome, ManifestSummary, ManifestWriter,
10 ProgressConfig, ProgressEmitter, Walker,
11};
12use crate::rgh::multihash::MultihashEncoder;
13use crate::rgh::output::{
14 serialize_records, DigestOutputFormat, DigestRecord,
15 DigestSource, OutputError, OutputFormatProfile,
16 SerializationResult,
17};
18use crate::rgh::weak;
19use argon2::{
20 password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
21 Argon2,
22};
23use ascon_hash::AsconHash;
24use balloon_hash::{
25 password_hash::{
26 rand_core::OsRng as BalOsRng, SaltString as BalSaltString,
27 },
28 Algorithm as BalAlgorithm, Balloon, Params as BalParams,
29};
30use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine};
31use blake2::Digest;
32use chrono::{DateTime, Utc};
33use digest::DynDigest;
34use pbkdf2::{
35 password_hash::{Ident as PbIdent, SaltString as PbSaltString},
36 Pbkdf2,
37};
38use scrypt::{
39 password_hash::SaltString as ScSaltString, Params as ScParams,
40 Scrypt,
41};
42use serde_json::to_writer_pretty;
43use skein::{consts::U32, Skein1024, Skein256, Skein512};
44use std::fs::{self, File};
45use std::io::IsTerminal;
46use std::path::{Path, PathBuf};
47use std::{collections::HashMap, io};
48
49#[cfg(all(
50 feature = "asm-accel",
51 not(feature = "portable-only"),
52 any(target_arch = "x86", target_arch = "x86_64")
53))]
54const ASM_ACCEL_DIGESTS: &[&str] = &[
55 "MD5",
56 "SHA1",
57 "SHA224",
58 "SHA256",
59 "SHA384",
60 "SHA512",
61 "WHIRLPOOL",
62];
63
64#[cfg(all(
65 feature = "asm-accel",
66 not(feature = "portable-only"),
67 target_arch = "aarch64"
68))]
69const ASM_ACCEL_DIGESTS: &[&str] =
70 &["SHA1", "SHA224", "SHA256", "SHA384", "SHA512"];
71
72#[cfg(any(
73 not(feature = "asm-accel"),
74 feature = "portable-only",
75 all(
76 feature = "asm-accel",
77 not(any(
78 target_arch = "x86",
79 target_arch = "x86_64",
80 target_arch = "aarch64"
81 ))
82 )
83))]
84const ASM_ACCEL_DIGESTS: &[&str] = &[];
85
86pub fn asm_accelerated_digests() -> &'static [&'static str] {
87 ASM_ACCEL_DIGESTS
88}
89
90pub fn algorithm_uses_asm(algorithm: &str) -> bool {
91 let needle = algorithm.to_uppercase();
92 ASM_ACCEL_DIGESTS
93 .iter()
94 .any(|candidate| candidate.eq_ignore_ascii_case(&needle))
95}
96
97#[derive(Clone, Debug)]
98pub struct WeakAlgorithmWarning {
99 pub severity_icon: &'static str,
100 pub headline: String,
101 pub body: String,
102 pub references: &'static [&'static str],
103}
104
105impl From<weak::WarningMessage> for WeakAlgorithmWarning {
106 fn from(value: weak::WarningMessage) -> Self {
107 Self {
108 severity_icon: value.severity_icon,
109 headline: value.headline,
110 body: value.body,
111 references: value.references,
112 }
113 }
114}
115
116pub fn weak_algorithm_warning(
117 algorithm: &str,
118) -> Option<WeakAlgorithmWarning> {
119 weak::warning_for(algorithm).map(WeakAlgorithmWarning::from)
120}
121
122pub(crate) fn assemble_output(
123 hash_only: bool,
124 mut tokens: Vec<String>,
125 original: Option<&str>,
126) -> String {
127 if !hash_only {
128 if let Some(value) = original {
129 tokens.push(value.to_string());
130 }
131 }
132 tokens.join(" ")
133}
134
135#[derive(Clone, Debug)]
136pub struct FileDigestOptions {
137 pub algorithm: String,
138 pub plan: DirectoryHashPlan,
139 pub format: DigestOutputFormat,
140 pub hash_only: bool,
141 pub progress: ProgressConfig,
142 pub manifest_path: Option<PathBuf>,
143 pub error_profile: ErrorHandlingProfile,
144}
145
146impl FileDigestOptions {
147 fn algorithm_uppercase(&self) -> String {
148 self.algorithm.to_uppercase()
149 }
150}
151
152pub struct FileDigestResult {
153 pub summary: ManifestSummary,
154 pub lines: Vec<String>,
155 pub warnings: Vec<String>,
156 pub exit_code: i32,
157 pub should_write_manifest: bool,
158 pub fatal_error: Option<String>,
159}
160
161#[derive(Clone, Debug, PartialEq, Eq)]
162pub enum CompareMode {
163 Manifest,
164 Text,
165}
166
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub enum CompareDiffKind {
169 Changed,
170 MissingLeft,
171 MissingRight,
172}
173
174#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct CompareDifference {
176 pub identifier: String,
177 pub kind: CompareDiffKind,
178 pub expected: Option<String>,
179 pub actual: Option<String>,
180}
181
182#[derive(Clone, Debug)]
183pub struct CompareSummary {
184 pub mode: CompareMode,
185 pub differences: Vec<CompareDifference>,
186 pub exit_code: i32,
187 pub incomplete: bool,
188 pub left_failures: u64,
189 pub right_failures: u64,
190 pub left_entries: usize,
191 pub right_entries: usize,
192}
193
194#[derive(Clone, Debug)]
195pub struct Argon2Config {
196 pub mem_cost: u32,
197 pub time_cost: u32,
198 pub parallelism: u32,
199}
200impl Default for Argon2Config {
201 fn default() -> Self {
202 Self {
203 mem_cost: 65536,
204 time_cost: 3,
205 parallelism: 4,
206 }
207 }
208}
209
210#[derive(Clone, Debug)]
211pub struct ScryptConfig {
212 pub log_n: u8,
213 pub r: u32,
214 pub p: u32,
215}
216impl Default for ScryptConfig {
217 fn default() -> Self {
218 Self {
219 log_n: 15,
220 r: 8,
221 p: 1,
222 }
223 }
224}
225
226#[derive(Clone, Debug)]
227pub struct BcryptConfig {
228 pub cost: u32,
229}
230impl Default for BcryptConfig {
231 fn default() -> Self {
232 Self { cost: 12 }
233 }
234}
235
236#[derive(Clone, Debug)]
237pub struct Pbkdf2Config {
238 pub rounds: u32,
239 pub output_length: usize,
240}
241impl Default for Pbkdf2Config {
242 fn default() -> Self {
243 Self {
244 rounds: 100_000,
245 output_length: 32,
246 }
247 }
248}
249
250#[derive(Clone, Debug)]
251pub struct BalloonConfig {
252 pub time_cost: u32,
253 pub memory_cost: u32,
254 pub parallelism: u32,
255}
256impl Default for BalloonConfig {
257 fn default() -> Self {
258 Self {
259 time_cost: 3,
260 memory_cost: 65536,
261 parallelism: 4,
262 }
263 }
264}
265
266macro_rules! impl_password_hash_fn {
267 ($name:ident, $impl_fn:ident, $cfg:ty, $salt:expr) => {
268 pub fn $name(password: &str, config: &$cfg, hash_only: bool) {
269 let salt = $salt;
270 let hash = match Self::$impl_fn(password, config, &salt) {
271 Ok(h) => h,
272 Err(e) => {
273 println!("Error hashing password: {}", e);
274 return;
275 }
276 };
277 let output = assemble_output(
278 hash_only,
279 vec![hash],
280 Some(password),
281 );
282 println!("{}", output);
283 }
284 };
285}
286
287macro_rules! impl_hash_function {
288 ($name:ident, $hasher:expr) => {
289 pub fn $name(password: &str, hash_only: bool) {
290 let result = $hasher(password.as_bytes());
291 let output = assemble_output(
292 hash_only,
293 vec![hex::encode(result)],
294 Some(password),
295 );
296 println!("{}", output);
297 }
298 };
299}
300
301pub struct PHash {}
302impl PHash {
303 pub fn derive_argon2_output(
304 password: &str,
305 cfg: &Argon2Config,
306 hash_only: bool,
307 ) -> Result<String, String> {
308 let salt = SaltString::generate(&mut OsRng);
309 Self::hash_argon2_impl(password, cfg, &salt)
310 .map(|hash| {
311 assemble_output(hash_only, vec![hash], Some(password))
312 })
313 .map_err(|err| err.to_string())
314 }
315
316 impl_hash_function!(hash_ascon, AsconHash::digest);
317
318 impl_password_hash_fn!(
319 hash_argon2,
320 hash_argon2_impl,
321 Argon2Config,
322 SaltString::generate(&mut OsRng)
323 );
324 pub(crate) fn hash_argon2_impl(
325 password: &str,
326 cfg: &Argon2Config,
327 salt: &SaltString,
328 ) -> Result<String, argon2::password_hash::Error> {
329 let argon2 = Argon2::new(
330 argon2::Algorithm::Argon2id,
331 argon2::Version::V0x13,
332 argon2::Params::new(
333 cfg.mem_cost,
334 cfg.time_cost,
335 cfg.parallelism,
336 None,
337 )
338 .unwrap(),
339 );
340 Ok(argon2
341 .hash_password(password.as_bytes(), salt)?
342 .to_string())
343 }
344
345 impl_password_hash_fn!(
346 hash_balloon,
347 hash_balloon_impl,
348 BalloonConfig,
349 BalSaltString::generate(&mut BalOsRng)
350 );
351 pub fn derive_balloon_output(
352 password: &str,
353 cfg: &BalloonConfig,
354 hash_only: bool,
355 ) -> Result<String, String> {
356 let salt = BalSaltString::generate(&mut BalOsRng);
357 Self::hash_balloon_impl(password, cfg, &salt)
358 .map(|hash| {
359 assemble_output(hash_only, vec![hash], Some(password))
360 })
361 .map_err(|err| err.to_string())
362 }
363 pub(crate) fn hash_balloon_impl(
364 password: &str,
365 cfg: &BalloonConfig,
366 salt: &BalSaltString,
367 ) -> Result<String, balloon_hash::password_hash::Error> {
368 let balloon = Balloon::<sha2::Sha256>::new(
369 BalAlgorithm::Balloon,
370 BalParams::new(
371 cfg.time_cost,
372 cfg.memory_cost,
373 cfg.parallelism,
374 )
375 .unwrap(),
376 None,
377 );
378 Ok(balloon
379 .hash_password(password.as_bytes(), salt)?
380 .to_string())
381 }
382
383 impl_password_hash_fn!(
384 hash_scrypt,
385 hash_scrypt_impl,
386 ScryptConfig,
387 ScSaltString::generate(&mut OsRng)
388 );
389 pub fn derive_scrypt_output(
390 password: &str,
391 cfg: &ScryptConfig,
392 hash_only: bool,
393 ) -> Result<String, String> {
394 let salt = ScSaltString::generate(&mut OsRng);
395 Self::hash_scrypt_impl(password, cfg, &salt)
396 .map(|hash| {
397 assemble_output(hash_only, vec![hash], Some(password))
398 })
399 .map_err(|err| err.to_string())
400 }
401 pub(crate) fn hash_scrypt_impl(
402 password: &str,
403 cfg: &ScryptConfig,
404 salt: &ScSaltString,
405 ) -> Result<String, scrypt::password_hash::Error> {
406 let params = ScParams::new(cfg.log_n, cfg.r, cfg.p).unwrap();
407 Ok(Scrypt
408 .hash_password_customized(
409 password.as_bytes(),
410 None,
411 None,
412 params,
413 salt.as_salt(),
414 )?
415 .to_string())
416 }
417
418 pub fn hash_bcrypt(
419 password: &str,
420 cfg: &BcryptConfig,
421 hash_only: bool,
422 ) {
423 match Self::derive_bcrypt_output(password, cfg, hash_only) {
424 Ok(output) => {
425 println!("{}", output);
426 }
427 Err(err) => {
428 eprintln!("Error: {}", err);
429 std::process::exit(1);
430 }
431 }
432 }
433
434 pub fn derive_bcrypt_output(
435 password: &str,
436 cfg: &BcryptConfig,
437 hash_only: bool,
438 ) -> Result<String, String> {
439 let salt = SaltString::generate(&mut OsRng);
440 Self::hash_bcrypt_hex(password, cfg, &salt)
441 .map(|hex| {
442 assemble_output(hash_only, vec![hex], Some(password))
443 })
444 .map_err(|err| err.to_string())
445 }
446
447 pub fn hash_sha_crypt(password: &str, hash_only: bool) {
448 match Self::derive_sha_crypt_output(password, hash_only) {
449 Ok(output) => println!("{}", output),
450 Err(err) => {
451 eprintln!("Error: {}", err);
452 std::process::exit(1);
453 }
454 }
455 }
456
457 pub fn derive_sha_crypt_output(
458 password: &str,
459 hash_only: bool,
460 ) -> Result<String, String> {
461 let params = sha_crypt::Sha512Params::new(10_000)
462 .map_err(|err| format!("{:?}", err))?;
463 let hash = sha_crypt::sha512_simple(password, ¶ms)
464 .map_err(|err| format!("{:?}", err))?;
465 Ok(assemble_output(hash_only, vec![hash], Some(password)))
466 }
467
468 pub fn hash_pbkdf2(
469 password: &str,
470 pb_scheme: &str,
471 cfg: &Pbkdf2Config,
472 hash_only: bool,
473 ) {
474 match Self::derive_pbkdf2_output(
475 password, pb_scheme, cfg, hash_only,
476 ) {
477 Ok(output) => println!("{}", output),
478 Err(err) => {
479 eprintln!("Error: {}", err);
480 std::process::exit(1);
481 }
482 }
483 }
484
485 pub fn derive_pbkdf2_output(
486 password: &str,
487 pb_scheme: &str,
488 cfg: &Pbkdf2Config,
489 hash_only: bool,
490 ) -> Result<String, String> {
491 let schemes = HashMap::from([
492 ("pbkdf2sha256", "pbkdf2-sha256"),
493 ("pbkdf2sha512", "pbkdf2-sha512"),
494 ]);
495 let alg =
496 PbIdent::new(schemes.get(pb_scheme).unwrap_or(&"NONE"))
497 .map_err(|err| err.to_string())?;
498 let salt = PbSaltString::generate(&mut OsRng);
499 let params = pbkdf2::Params {
500 output_length: cfg.output_length,
501 rounds: cfg.rounds,
502 };
503 let hash = Pbkdf2::hash_password_customized(
504 &Pbkdf2,
505 password.as_bytes(),
506 Some(alg),
507 None,
508 params,
509 salt.as_salt(),
510 )
511 .map_err(|err| err.to_string())?;
512 Ok(assemble_output(
513 hash_only,
514 vec![hash.to_string()],
515 Some(password),
516 ))
517 }
518
519 pub(crate) fn hash_pbkdf2_with_salt(
520 password: &str,
521 pb_scheme: &str,
522 cfg: &Pbkdf2Config,
523 salt_b64: &str,
524 ) -> Result<String, String> {
525 let schemes = HashMap::from([
526 ("pbkdf2sha256", "pbkdf2-sha256"),
527 ("pbkdf2sha512", "pbkdf2-sha512"),
528 ]);
529 let alg =
530 PbIdent::new(schemes.get(pb_scheme).unwrap_or(&"NONE"))
531 .map_err(|err| err.to_string())?;
532 let salt_bytes = STANDARD_NO_PAD
533 .decode(salt_b64)
534 .map_err(|err| err.to_string())?;
535 let salt = PbSaltString::b64_encode(&salt_bytes)
536 .map_err(|err| err.to_string())?;
537 let params = pbkdf2::Params {
538 output_length: cfg.output_length,
539 rounds: cfg.rounds,
540 };
541 let hash = Pbkdf2::hash_password_customized(
542 &Pbkdf2,
543 password.as_bytes(),
544 Some(alg),
545 None,
546 params,
547 salt.as_salt(),
548 )
549 .map_err(|err| err.to_string())?;
550 Ok(hash.to_string())
551 }
552
553 pub(crate) fn hash_bcrypt_hex(
554 password: &str,
555 cfg: &BcryptConfig,
556 salt: &SaltString,
557 ) -> Result<String, bcrypt_pbkdf::Error> {
558 let mut out = [0; 64];
559 bcrypt_pbkdf::bcrypt_pbkdf(
560 password.as_bytes(),
561 salt.as_bytes(),
562 cfg.cost,
563 &mut out,
564 )?;
565 Ok(hex::encode(out))
566 }
567
568 pub(crate) fn hash_bcrypt_with_salt(
569 password: &str,
570 cfg: &BcryptConfig,
571 salt_b64: &str,
572 ) -> Result<String, String> {
573 let salt_bytes = STANDARD_NO_PAD
574 .decode(salt_b64)
575 .map_err(|err| err.to_string())?;
576 let salt = SaltString::b64_encode(&salt_bytes)
577 .map_err(|err| err.to_string())?;
578 Self::hash_bcrypt_hex(password, cfg, &salt)
579 .map_err(|err| err.to_string())
580 }
581
582 pub(crate) fn hash_argon2_with_salt(
583 password: &str,
584 cfg: &Argon2Config,
585 salt_b64: &str,
586 ) -> Result<String, String> {
587 let salt_bytes = STANDARD_NO_PAD
588 .decode(salt_b64)
589 .map_err(|err| err.to_string())?;
590 let salt = SaltString::b64_encode(&salt_bytes)
591 .map_err(|err| err.to_string())?;
592 Self::hash_argon2_impl(password, cfg, &salt)
593 .map_err(|err| err.to_string())
594 }
595
596 pub(crate) fn hash_balloon_with_salt(
597 password: &str,
598 cfg: &BalloonConfig,
599 salt_b64: &str,
600 ) -> Result<String, String> {
601 let salt_bytes = STANDARD_NO_PAD
602 .decode(salt_b64)
603 .map_err(|err| err.to_string())?;
604 let salt = BalSaltString::b64_encode(&salt_bytes)
605 .map_err(|err| err.to_string())?;
606 Self::hash_balloon_impl(password, cfg, &salt)
607 .map_err(|err| err.to_string())
608 }
609
610 pub(crate) fn hash_scrypt_with_salt(
611 password: &str,
612 cfg: &ScryptConfig,
613 salt_b64: &str,
614 ) -> Result<String, String> {
615 let salt_bytes = STANDARD_NO_PAD
616 .decode(salt_b64)
617 .map_err(|err| err.to_string())?;
618 let salt = ScSaltString::b64_encode(&salt_bytes)
619 .map_err(|err| err.to_string())?;
620 Self::hash_scrypt_impl(password, cfg, &salt)
621 .map_err(|err| err.to_string())
622 }
623}
624
625macro_rules! create_hasher {
626 ($alg:expr, $($pat:expr => $hasher:expr),+ $(,)?) => {
627 match $alg {
628 $($pat => Box::new($hasher),)+
629 _ => panic!("Unknown algorithm"),
630 }
631 };
632}
633
634#[derive(Clone)]
635pub struct RHash {
636 digest: Box<dyn DynDigest>,
637}
638impl RHash {
639 pub fn new(alg: &str) -> Self {
640 Self {
641 digest: create_hasher!(alg,
642 "BELTHASH" => belt_hash::BeltHash::new(),
643 "BLAKE2B" => blake2::Blake2b512::new(),
644 "BLAKE2S" => blake2::Blake2s256::new(),
645 "BLAKE3" => blake3::Hasher::new(),
646 "FSB160" => fsb::Fsb160::new(),
647 "FSB224" => fsb::Fsb224::new(),
648 "FSB256" => fsb::Fsb256::new(),
649 "FSB384" => fsb::Fsb384::new(),
650 "FSB512" => fsb::Fsb512::new(),
651 "GOST94" => gost94::Gost94Test::new(),
652 "GOST94UA" => gost94::Gost94UA::new(),
653 "GROESTL" => groestl::Groestl256::new(),
654 "JH224" => jh::Jh224::new(),
655 "JH256" => jh::Jh256::new(),
656 "JH384" => jh::Jh384::new(),
657 "JH512" => jh::Jh512::new(),
658 "MD2" => md2::Md2::new(),
659 "MD5" => md5::Md5::new(),
660 "MD4" => md4::Md4::new(),
661 "RIPEMD160" => ripemd::Ripemd160::new(),
662 "RIPEMD320" => ripemd::Ripemd320::new(),
663 "SHA1" => sha1::Sha1::new(),
664 "SHA224" => sha2::Sha224::new(),
665 "SHA256" => sha2::Sha256::new(),
666 "SHA384" => sha2::Sha384::new(),
667 "SHA512" => sha2::Sha512::new(),
668 "SHA3_224" => sha3::Sha3_224::new(),
669 "SHA3_256" => sha3::Sha3_256::new(),
670 "SHA3_384" => sha3::Sha3_384::new(),
671 "SHA3_512" => sha3::Sha3_512::new(),
672 "SHABAL192" => shabal::Shabal192::new(),
673 "SHABAL224" => shabal::Shabal224::new(),
674 "SHABAL256" => shabal::Shabal256::new(),
675 "SHABAL384" => shabal::Shabal384::new(),
676 "SHABAL512" => shabal::Shabal512::new(),
677 "SKEIN256" => Skein256::<U32>::new(),
678 "SKEIN512" => Skein512::<U32>::new(),
679 "SKEIN1024" => Skein1024::<U32>::new(),
680 "SM3" => sm3::Sm3::new(),
681 "STREEBOG256" => streebog::Streebog256::new(),
682 "STREEBOG512" => streebog::Streebog512::new(),
683 "TIGER" => tiger::Tiger::new(),
684 "WHIRLPOOL" => whirlpool::Whirlpool::new(),
685 ),
686 }
687 }
688
689 pub fn process_string(&mut self, data: &[u8]) -> Vec<u8> {
690 self.digest.update(data);
691 self.digest.finalize_reset().to_vec()
692 }
693
694 pub fn read_file(
695 &mut self,
696 path: &str,
697 ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
698 let data = std::fs::read(path)?;
699 self.digest.update(&data);
700 Ok(self.digest.finalize_reset().to_vec())
701 }
702}
703
704pub fn digest_bytes_to_record(
705 algorithm: &str,
706 data: &[u8],
707 label: Option<&str>,
708 source: DigestSource,
709) -> Result<DigestRecord, String> {
710 let mut engine = RHash::new(&algorithm.to_uppercase());
711 let digest = engine.process_string(data);
712 let path = label.map(|value| value.to_string());
713 Ok(DigestRecord::from_digest(path, algorithm, &digest, source))
714}
715
716pub fn serialize_digest_output(
717 records: &[DigestRecord],
718 format: DigestOutputFormat,
719 hash_only: bool,
720) -> Result<SerializationResult, OutputError> {
721 let profile = OutputFormatProfile::new(format);
722 serialize_records(records, &profile, hash_only)
723}
724
725pub fn digest_with_options(
726 options: &FileDigestOptions,
727) -> Result<
728 (ManifestOutcome, SerializationResult),
729 Box<dyn std::error::Error>,
730> {
731 let is_tty = io::stderr().is_terminal();
732 let mut emitter =
733 if options.progress.should_emit(options.hash_only, is_tty) {
734 Some(ProgressEmitter::new(options.progress))
735 } else {
736 None
737 };
738
739 let (outcome, records) = {
740 let emitter_ref = emitter.as_mut();
741 digest_with_options_internal(options, emitter_ref)?
742 };
743
744 if let Some(emitter) = emitter.as_mut() {
745 emitter.emit_final();
746 }
747
748 let serialization = serialize_digest_output(
749 &records,
750 options.format,
751 options.hash_only,
752 )
753 .map_err(|err| -> Box<dyn std::error::Error> { Box::new(err) })?;
754
755 if outcome.should_write_manifest {
756 if let Some(manifest_path) = &options.manifest_path {
757 write_manifest(manifest_path, &outcome.summary)?;
758 }
759 }
760
761 Ok((outcome, serialization))
762}
763
764pub fn digest_with_options_collect(
765 options: &FileDigestOptions,
766) -> Result<FileDigestResult, Box<dyn std::error::Error>> {
767 let (outcome, serialization) = digest_with_options(options)?;
768 let ManifestOutcome {
769 summary,
770 exit_code,
771 should_write_manifest,
772 fatal_error,
773 } = outcome;
774 Ok(FileDigestResult {
775 summary,
776 lines: serialization.lines,
777 warnings: serialization.warnings,
778 exit_code,
779 should_write_manifest,
780 fatal_error,
781 })
782}
783
784fn digest_with_options_internal(
785 options: &FileDigestOptions,
786 mut emitter: Option<&mut ProgressEmitter>,
787) -> Result<
788 (ManifestOutcome, Vec<DigestRecord>),
789 Box<dyn std::error::Error>,
790> {
791 let algorithm_upper = options.algorithm_uppercase();
792 let walker = Walker::new(options.plan.clone());
793 let entries = walker.walk()?;
794 let mut writer = ManifestWriter::new(
795 options.plan.clone(),
796 options.error_profile.clone(),
797 );
798 let mut records = Vec::new();
799
800 for entry in entries {
801 let path = entry.path.clone();
802 let display_path = path.to_string_lossy();
803 let metadata = match fs::metadata(&path) {
804 Ok(meta) => meta,
805 Err(err) => {
806 let status = match err.kind() {
807 io::ErrorKind::PermissionDenied => {
808 EntryStatus::Skipped
809 }
810 _ => EntryStatus::Error,
811 };
812 let message = format!(
813 "failed to read metadata for {}: {}",
814 display_path, err
815 );
816 let should_continue = writer.record_failure(
817 path,
818 &options.algorithm,
819 message.clone(),
820 status,
821 );
822 eprintln!("{}", message);
823 if !should_continue {
824 return Ok((writer.finalize(), records));
825 }
826 continue;
827 }
828 };
829 let size = metadata.len();
830 let modified =
831 metadata.modified().ok().map(DateTime::<Utc>::from);
832
833 let mut engine = RHash::new(&algorithm_upper);
834 let digest_bytes = match engine
835 .read_file(display_path.as_ref())
836 {
837 Ok(bytes) => bytes,
838 Err(err) => {
839 let status = entry_status_from_error(err.as_ref());
840 let message = format!(
841 "failed to hash {}: {}",
842 display_path, err
843 );
844 let should_continue = writer.record_failure(
845 path,
846 &options.algorithm,
847 message.clone(),
848 status,
849 );
850 eprintln!("{}", message);
851 if !should_continue {
852 return Ok((writer.finalize(), records));
853 }
854 continue;
855 }
856 };
857 let record = DigestRecord::from_digest(
858 Some(display_path.to_string()),
859 &options.algorithm,
860 &digest_bytes,
861 DigestSource::File,
862 );
863 if let Some(emitter) = emitter.as_mut() {
864 emitter.record(size);
865 emitter.maybe_emit();
866 }
867 let mut manifest_digest = record.digest_hex.clone();
868 if options.format == DigestOutputFormat::Multihash {
869 let algorithm = options.algorithm.to_ascii_lowercase();
870 match MultihashEncoder::encode(&algorithm, &digest_bytes)
871 {
872 Ok(token) => manifest_digest = token,
873 Err(err) => eprintln!(
874 "warning: failed to encode multihash for manifest entry {}: {}",
875 display_path,
876 err
877 ),
878 }
879 }
880 records.push(record);
881 writer.record_success(
882 path,
883 &options.algorithm,
884 manifest_digest,
885 size,
886 modified,
887 );
888 }
889
890 Ok((writer.finalize(), records))
891}
892
893fn write_manifest(
894 path: &PathBuf,
895 summary: &ManifestSummary,
896) -> Result<(), Box<dyn std::error::Error>> {
897 if let Some(parent) = path.parent() {
898 if !parent.as_os_str().is_empty() {
899 fs::create_dir_all(parent)?;
900 }
901 }
902 let file = File::create(path)?;
903 to_writer_pretty(file, summary)?;
904 Ok(())
905}
906
907enum CompareInput {
908 Manifest(ManifestSummary),
909 Lines(Vec<String>),
910}
911
912pub fn compare_file_hashes(
913 baseline: &str,
914 candidate: &str,
915) -> Result<CompareSummary, Box<dyn std::error::Error>> {
916 let baseline_path = Path::new(baseline);
917 let candidate_path = Path::new(candidate);
918 let baseline_input = load_compare_input(baseline_path)?;
919 let candidate_input = load_compare_input(candidate_path)?;
920 match (baseline_input, candidate_input) {
921 (
922 CompareInput::Manifest(left),
923 CompareInput::Manifest(right),
924 ) => Ok(compare_manifests(left, right)),
925 (CompareInput::Lines(left), CompareInput::Lines(right)) => {
926 Ok(compare_line_lists(left, right))
927 }
928 (CompareInput::Manifest(_), CompareInput::Lines(_))
929 | (CompareInput::Lines(_), CompareInput::Manifest(_)) => {
930 Err(io::Error::new(
931 io::ErrorKind::InvalidInput,
932 "Cannot compare manifest JSON with plain digest list",
933 )
934 .into())
935 }
936 }
937}
938
939fn load_compare_input(
940 path: &Path,
941) -> Result<CompareInput, Box<dyn std::error::Error>> {
942 let contents = fs::read_to_string(path)?;
943 match serde_json::from_str::<ManifestSummary>(&contents) {
944 Ok(summary) => Ok(CompareInput::Manifest(summary)),
945 Err(err) => {
946 if contents.trim_start().starts_with('{') {
947 return Err(Box::new(err));
948 }
949 let lines = contents
950 .lines()
951 .map(|line| line.trim_end_matches(['\r', '\n']))
952 .map(|line| line.to_string())
953 .collect();
954 Ok(CompareInput::Lines(lines))
955 }
956 }
957}
958
959fn compare_manifests(
960 left: ManifestSummary,
961 right: ManifestSummary,
962) -> CompareSummary {
963 let mut differences = Vec::new();
964 let mut incomplete = false;
965 let mut right_map: HashMap<PathBuf, &ManifestEntry> = right
966 .entries
967 .iter()
968 .map(|entry| (entry.path.clone(), entry))
969 .collect();
970 for entry in &left.entries {
971 if entry.status != EntryStatus::Hashed
972 || entry.digest.is_none()
973 {
974 incomplete = true;
975 }
976 match right_map.remove(&entry.path) {
977 Some(other) => {
978 if other.status != EntryStatus::Hashed
979 || other.digest.is_none()
980 {
981 incomplete = true;
982 }
983 match (entry.digest.as_ref(), other.digest.as_ref()) {
984 (Some(expected), Some(actual)) => {
985 if expected != actual {
986 let identifier =
987 entry.path.display().to_string();
988 differences.push(CompareDifference {
989 identifier,
990 kind: CompareDiffKind::Changed,
991 expected: Some(expected.clone()),
992 actual: Some(actual.clone()),
993 });
994 }
995 }
996 (Some(expected), None) => {
997 let identifier =
998 entry.path.display().to_string();
999 differences.push(CompareDifference {
1000 identifier,
1001 kind: CompareDiffKind::Changed,
1002 expected: Some(expected.clone()),
1003 actual: None,
1004 });
1005 incomplete = true;
1006 }
1007 (None, Some(actual)) => {
1008 let identifier =
1009 entry.path.display().to_string();
1010 differences.push(CompareDifference {
1011 identifier,
1012 kind: CompareDiffKind::Changed,
1013 expected: None,
1014 actual: Some(actual.clone()),
1015 });
1016 incomplete = true;
1017 }
1018 (None, None) => {
1019 incomplete = true;
1020 }
1021 }
1022 }
1023 None => {
1024 let identifier = entry.path.display().to_string();
1025 differences.push(CompareDifference {
1026 identifier,
1027 kind: CompareDiffKind::MissingRight,
1028 expected: entry.digest.clone(),
1029 actual: None,
1030 });
1031 }
1032 }
1033 }
1034
1035 for entry in right_map.values() {
1036 let identifier = entry.path.display().to_string();
1037 differences.push(CompareDifference {
1038 identifier,
1039 kind: CompareDiffKind::MissingLeft,
1040 expected: None,
1041 actual: entry.digest.clone(),
1042 });
1043 if entry.status != EntryStatus::Hashed
1044 || entry.digest.is_none()
1045 {
1046 incomplete = true;
1047 }
1048 }
1049
1050 if left.failure_count > 0 || right.failure_count > 0 {
1051 incomplete = true;
1052 }
1053
1054 let mut exit_code = 0;
1055 let has_mismatch = differences.iter().any(|diff| {
1056 matches!(
1057 diff.kind,
1058 CompareDiffKind::Changed
1059 | CompareDiffKind::MissingLeft
1060 | CompareDiffKind::MissingRight
1061 )
1062 });
1063 if has_mismatch {
1064 exit_code = 1;
1065 } else if incomplete {
1066 exit_code = 2;
1067 }
1068
1069 differences.sort_by(|a, b| a.identifier.cmp(&b.identifier));
1070
1071 CompareSummary {
1072 mode: CompareMode::Manifest,
1073 differences,
1074 exit_code,
1075 incomplete,
1076 left_failures: left.failure_count,
1077 right_failures: right.failure_count,
1078 left_entries: left.entries.len(),
1079 right_entries: right.entries.len(),
1080 }
1081}
1082
1083fn compare_line_lists(
1084 left: Vec<String>,
1085 right: Vec<String>,
1086) -> CompareSummary {
1087 let mut differences = Vec::new();
1088 let max_len = left.len().max(right.len());
1089 for idx in 0..max_len {
1090 let left_line = left.get(idx);
1091 let right_line = right.get(idx);
1092 match (left_line, right_line) {
1093 (Some(expected), Some(actual)) => {
1094 if expected != actual {
1095 differences.push(CompareDifference {
1096 identifier: format!("line {}", idx + 1),
1097 kind: CompareDiffKind::Changed,
1098 expected: Some(expected.clone()),
1099 actual: Some(actual.clone()),
1100 });
1101 }
1102 }
1103 (Some(expected), None) => {
1104 differences.push(CompareDifference {
1105 identifier: format!("line {}", idx + 1),
1106 kind: CompareDiffKind::MissingRight,
1107 expected: Some(expected.clone()),
1108 actual: None,
1109 });
1110 }
1111 (None, Some(actual)) => {
1112 differences.push(CompareDifference {
1113 identifier: format!("line {}", idx + 1),
1114 kind: CompareDiffKind::MissingLeft,
1115 expected: None,
1116 actual: Some(actual.clone()),
1117 });
1118 }
1119 (None, None) => {}
1120 }
1121 }
1122
1123 let exit_code = if differences.is_empty() { 0 } else { 1 };
1124
1125 CompareSummary {
1126 mode: CompareMode::Text,
1127 differences,
1128 exit_code,
1129 incomplete: false,
1130 left_failures: 0,
1131 right_failures: 0,
1132 left_entries: left.len(),
1133 right_entries: right.len(),
1134 }
1135}
1136
1137fn entry_status_from_error(
1138 err: &(dyn std::error::Error + 'static),
1139) -> EntryStatus {
1140 if let Some(io_err) = err.downcast_ref::<io::Error>() {
1141 return match io_err.kind() {
1142 io::ErrorKind::PermissionDenied => EntryStatus::Skipped,
1143 _ => EntryStatus::Error,
1144 };
1145 }
1146 EntryStatus::Error
1147}