rustgenhash/rgh/
hash.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Project: rustgenhash
3// File: hash.rs
4// Author: Volker Schwaberow <volker@schwaberow.de>
5// Copyright (c) 2022 Volker Schwaberow
6
7use 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, &params)
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}