Skip to main content

heft_format/
pack.rs

1use std::fs::File;
2use std::io::{Read, Write};
3use std::path::Path;
4use std::time::SystemTime;
5
6use serde::{Deserialize, Serialize};
7
8use crate::display::{format_count, format_size};
9use crate::error::{HeftError, Result};
10use crate::tree::TreeNode;
11
12/// Magic bytes identifying a .heft file, plus format version.
13const MAGIC: &[u8; 5] = b"HEFT\x02";
14
15/// Metadata stored in the .heft file header (readable without decompression).
16#[derive(Debug, Serialize, Deserialize)]
17pub struct PackMeta {
18    pub root: String,
19    pub timestamp: u64,
20    pub heft_version: String,
21    pub file_count: u64,
22    pub dir_count: u64,
23    pub size_actual: u64,
24}
25
26/// Pack a pre-built tree into a portable `.heft` file.
27pub fn pack_tree(
28    tree: &TreeNode,
29    root_path: &str,
30    output: &Path,
31) -> Result<()> {
32    let (file_count, dir_count) = count_nodes(tree);
33    let timestamp = SystemTime::now()
34        .duration_since(SystemTime::UNIX_EPOCH)
35        .map(|d| d.as_secs())
36        .unwrap_or(0);
37
38    let meta = PackMeta {
39        root: root_path.to_string(),
40        timestamp,
41        heft_version: env!("CARGO_PKG_VERSION").to_string(),
42        file_count,
43        dir_count,
44        size_actual: tree.size_actual,
45    };
46
47    let mut file = File::create(output)?;
48
49    // Write magic.
50    file.write_all(MAGIC)?;
51
52    // Write metadata length + metadata JSON.
53    let meta_json = serde_json::to_vec(&meta)?;
54    file.write_all(&(meta_json.len() as u32).to_le_bytes())?;
55    file.write_all(&meta_json)?;
56
57    // Write zstd-compressed tree JSON.
58    let mut encoder = zstd::Encoder::new(&mut file, 3)?;
59    serde_json::to_writer(&mut encoder, tree)?;
60    encoder.finish()?;
61
62    let file_size = file.metadata().map(|m| m.len()).unwrap_or(0);
63    println!(
64        "Packed {} ({} files, {} dirs, {}) → {} ({})",
65        root_path,
66        format_count(file_count),
67        format_count(dir_count),
68        format_size(tree.size_actual),
69        output.display(),
70        format_size(file_size),
71    );
72
73    Ok(())
74}
75
76/// Load a `.heft` file: decompress and deserialize the full tree.
77pub fn load_tree(input: &Path) -> Result<(PackMeta, TreeNode)> {
78    let mut file = File::open(input)?;
79
80    // Validate magic.
81    let mut magic = [0u8; 5];
82    file.read_exact(&mut magic)?;
83    if &magic != MAGIC {
84        return Err(HeftError::Other(format!(
85            "not a .heft file (bad magic): {}",
86            input.display()
87        )));
88    }
89
90    // Read metadata.
91    let mut len_buf = [0u8; 4];
92    file.read_exact(&mut len_buf)?;
93    let meta_len = u32::from_le_bytes(len_buf) as usize;
94    let mut meta_buf = vec![0u8; meta_len];
95    file.read_exact(&mut meta_buf)?;
96    let meta: PackMeta = serde_json::from_slice(&meta_buf)?;
97
98    // Decompress and deserialize tree.
99    let decoder = zstd::Decoder::new(&mut file)?;
100    let tree: TreeNode = serde_json::from_reader(decoder)?;
101
102    Ok((meta, tree))
103}
104
105/// Read only the metadata header from a `.heft` file (no decompression).
106pub fn read_info(input: &Path) -> Result<PackMeta> {
107    let mut file = File::open(input)?;
108
109    let mut magic = [0u8; 5];
110    file.read_exact(&mut magic)?;
111    if &magic != MAGIC {
112        return Err(HeftError::Other(format!(
113            "not a .heft file (bad magic): {}",
114            input.display()
115        )));
116    }
117
118    let mut len_buf = [0u8; 4];
119    file.read_exact(&mut len_buf)?;
120    let meta_len = u32::from_le_bytes(len_buf) as usize;
121    let mut meta_buf = vec![0u8; meta_len];
122    file.read_exact(&mut meta_buf)?;
123
124    Ok(serde_json::from_slice(&meta_buf)?)
125}
126
127// ── Helpers ─────────────────────────────────────────────────────────────
128
129/// Count files and directories in a tree (directories include the root).
130fn count_nodes(node: &TreeNode) -> (u64, u64) {
131    let mut files = 0u64;
132    let mut dirs = 1u64; // count this node as a directory
133    if let Some(children) = &node.children {
134        for child in children {
135            if child.children.is_some() {
136                let (f, d) = count_nodes(child);
137                files += f;
138                dirs += d;
139            } else {
140                files += 1;
141            }
142        }
143    }
144    (files, dirs)
145}