Skip to main content

st/formatters/
digest.rs

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    /// Calculate a SHA256 hash of the tree structure for consistency verification
22    fn calculate_tree_hash(&self, nodes: &[FileNode]) -> String {
23        let mut hasher = Sha256::new();
24
25        // Hash each node's key properties in a deterministic way
26        for node in nodes {
27            // Hash: depth, name, type (dir/file), size, permissions
28            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        // Return first 16 chars of hex for brevity
42        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        // Calculate SHA256 hash of the tree structure
56        let tree_hash = self.calculate_tree_hash(nodes);
57
58        // First line: Hash and basic stats
59        write!(
60            writer,
61            "HASH: {} F:{} D:{} S:{:x}",
62            tree_hash, stats.total_files, stats.total_dirs, stats.total_size,
63        )?;
64
65        // File type summary (top 5) - super compact
66        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        // Add newline at the end
80        writeln!(writer)?;
81
82        Ok(())
83    }
84}