1use super::Formatter;
2use crate::scanner::{FileNode, TreeStats};
3use anyhow::Result;
4use sha2::{Digest, Sha256};
5use std::io::Write;
6use std::path::Path;
7
8pub struct DigestFormatter;
9
10impl Default for DigestFormatter {
11 fn default() -> Self {
12 Self::new()
13 }
14}
15
16impl DigestFormatter {
17 pub fn new() -> Self {
18 Self
19 }
20
21 fn calculate_tree_hash(&self, nodes: &[FileNode]) -> String {
23 let mut hasher = Sha256::new();
24
25 for node in nodes {
27 hasher.update(node.depth.to_le_bytes());
29 hasher.update(
30 node.path
31 .file_name()
32 .unwrap_or_default()
33 .to_string_lossy()
34 .as_bytes(),
35 );
36 hasher.update([if node.is_dir { 1 } else { 0 }]);
37 hasher.update(node.size.to_le_bytes());
38 hasher.update(node.permissions.to_le_bytes());
39 }
40
41 let result = hasher.finalize();
43 hex::encode(&result[..8])
44 }
45}
46
47impl Formatter for DigestFormatter {
48 fn format(
49 &self,
50 writer: &mut dyn Write,
51 nodes: &[FileNode],
52 stats: &TreeStats,
53 _root_path: &Path,
54 ) -> Result<()> {
55 let tree_hash = self.calculate_tree_hash(nodes);
57
58 write!(
60 writer,
61 "HASH: {} F:{} D:{} S:{:x}",
62 tree_hash, stats.total_files, stats.total_dirs, stats.total_size,
63 )?;
64
65 if !stats.file_types.is_empty() {
67 let mut types: Vec<_> = stats.file_types.iter().collect();
68 types.sort_by(|a, b| b.1.cmp(a.1));
69
70 let types_str: Vec<String> = types
71 .iter()
72 .take(5)
73 .map(|(ext, count)| format!("{}:{}", ext, count))
74 .collect();
75
76 write!(writer, " TYPES: {}", types_str.join(" "))?;
77 }
78
79 writeln!(writer)?;
81
82 Ok(())
83 }
84}