Skip to main content

hexz_cli/cmd/vm/
commit.rs

1//! Commit overlay and optional memory into a new snapshot.
2//!
3//! This command merges changes from a writable overlay (created during VM execution)
4//! with a base snapshot to produce a new snapshot. It supports both "thick" snapshots
5//! (standalone, contains all data) and "thin" snapshots (references parent for
6//! unmodified blocks).
7
8use anyhow::Result;
9use hexz_common::constants::{META_ENTRY_SIZE, OVERLAY_BLOCK_SIZE};
10use hexz_core::File as HexzFile;
11use hexz_core::algo::compression::create_compressor_from_str;
12use hexz_core::api::file::SnapshotStream;
13use hexz_core::ops::snapshot_writer::SnapshotWriter;
14use hexz_core::store::local::FileBackend;
15use indicatif::ProgressBar;
16use std::collections::HashSet;
17use std::fs::File;
18use std::io::{Read, Seek, SeekFrom};
19use std::path::PathBuf;
20use std::sync::Arc;
21
22/// Executes the commit command to merge overlay changes into a new snapshot.
23#[allow(clippy::too_many_arguments)]
24pub fn run(
25    base_path: PathBuf,
26    overlay_path: PathBuf,
27    memory_path: Option<PathBuf>,
28    output_path: PathBuf,
29    algo: String,
30    block_size: u32,
31    keep_overlay: bool,
32    message: Option<String>,
33    thin: bool,
34) -> Result<()> {
35    println!(
36        "Committing changes from {:?} to {:?} (Thin: {})",
37        overlay_path, output_path, thin
38    );
39
40    let backend = Arc::new(FileBackend::new(&base_path)?);
41    let base_snap = HexzFile::open(backend, None)?;
42
43    let meta_path = overlay_path.with_extension("meta");
44    let mut modified_blocks = HashSet::new();
45
46    if meta_path.exists() {
47        let mut f = File::open(&meta_path)?;
48        let mut buf = [0u8; META_ENTRY_SIZE];
49        loop {
50            match f.read_exact(&mut buf) {
51                Ok(_) => {
52                    modified_blocks.insert(u64::from_le_bytes(buf));
53                }
54                Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
55                Err(e) => return Err(e.into()),
56            }
57        }
58    }
59
60    let mut overlay_file = File::open(&overlay_path)?;
61
62    let (write_compressor, compression_type) = create_compressor_from_str(&algo, None, None)?;
63
64    let mut writer = SnapshotWriter::builder(&output_path, write_compressor, compression_type)
65        .block_size(block_size)
66        .variable_blocks(false)
67        .build()?;
68
69    let base_primary_size = base_snap.size(SnapshotStream::Primary);
70    let overlay_len = overlay_file.metadata()?.len();
71    let final_primary_size = std::cmp::max(base_primary_size, overlay_len);
72
73    // --- Primary stream ---
74    writer.begin_stream(true, final_primary_size);
75
76    let pb = ProgressBar::new(final_primary_size);
77    let bs = block_size as u64;
78    let total_blocks = final_primary_size.div_ceil(bs);
79
80    for i in 0..total_blocks {
81        let block_start = i * bs;
82        let mut block_len = bs;
83        if block_start + block_len > final_primary_size {
84            block_len = final_primary_size - block_start;
85        }
86
87        let start_ov_blk = block_start / OVERLAY_BLOCK_SIZE;
88        let end_ov_blk = (block_start + block_len - 1) / OVERLAY_BLOCK_SIZE;
89        let is_modified =
90            (start_ov_blk..=end_ov_blk).any(|ov_blk| modified_blocks.contains(&ov_blk));
91
92        // Thin: unmodified block in base → parent ref
93        if thin && !is_modified && block_start < base_primary_size {
94            let hash = base_snap
95                .get_block_info(SnapshotStream::Primary, block_start)?
96                .map(|(_, info)| info.hash)
97                .unwrap_or([0u8; 32]);
98
99            writer.write_parent_ref(&hash, block_len as u32)?;
100            pb.inc(block_len);
101            continue;
102        }
103
104        // Build block data from base + overlay
105        let mut data = vec![0u8; block_len as usize];
106
107        if is_modified {
108            if block_start < base_primary_size {
109                let base_data =
110                    base_snap.read_at(SnapshotStream::Primary, block_start, block_len as usize)?;
111                data[..base_data.len()].copy_from_slice(&base_data);
112            }
113
114            for ov_blk in start_ov_blk..=end_ov_blk {
115                if modified_blocks.contains(&ov_blk) {
116                    let chunk_start = ov_blk * OVERLAY_BLOCK_SIZE;
117                    let chunk_end =
118                        std::cmp::min(chunk_start + OVERLAY_BLOCK_SIZE, final_primary_size);
119                    let chunk_len = (chunk_end - chunk_start) as usize;
120                    let rel_start = (chunk_start - block_start) as usize;
121
122                    overlay_file.seek(SeekFrom::Start(chunk_start))?;
123                    overlay_file.read_exact(&mut data[rel_start..rel_start + chunk_len])?;
124                }
125            }
126        } else if block_start < base_primary_size {
127            let base_data =
128                base_snap.read_at(SnapshotStream::Primary, block_start, block_len as usize)?;
129            data[..base_data.len()].copy_from_slice(&base_data);
130        }
131
132        writer.write_data_block(&data)?;
133        pb.inc(block_len);
134    }
135
136    writer.end_stream()?;
137
138    // --- Secondary stream ---
139    if let Some(mem_path) = memory_path {
140        println!("\nProcessing new memory state...");
141        let mut mem_file = File::open(&mem_path)?;
142        let mem_len = mem_file.metadata()?.len();
143
144        writer.begin_stream(false, mem_len);
145
146        let pb_mem = ProgressBar::new(mem_len);
147        let mut buf = vec![0u8; block_size as usize];
148
149        loop {
150            let mut pos = 0;
151            while pos < buf.len() {
152                match mem_file.read(&mut buf[pos..]) {
153                    Ok(0) => break,
154                    Ok(n) => pos += n,
155                    Err(e) => return Err(e.into()),
156                }
157            }
158            if pos == 0 {
159                break;
160            }
161
162            writer.write_data_block(&buf[..pos])?;
163            pb_mem.inc(pos as u64);
164        }
165
166        writer.end_stream()?;
167    }
168
169    // --- Finalize ---
170    let parent_path = if thin {
171        Some(
172            std::fs::canonicalize(&base_path)?
173                .to_string_lossy()
174                .to_string(),
175        )
176    } else {
177        None
178    };
179
180    let meta_bytes = message.as_ref().map(|m| m.as_bytes());
181    writer.finalize(parent_path, meta_bytes)?;
182
183    if !keep_overlay {
184        println!("Cleaning up overlay files...");
185        let _ = std::fs::remove_file(&overlay_path);
186        let _ = std::fs::remove_file(&meta_path);
187    }
188
189    println!("Commit complete: {:?}", output_path);
190    Ok(())
191}