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_disk_size = base_snap.size(SnapshotStream::Disk);
70    let overlay_len = overlay_file.metadata()?.len();
71    let final_disk_size = std::cmp::max(base_disk_size, overlay_len);
72
73    // --- Disk stream ---
74    writer.begin_stream(true, final_disk_size);
75
76    let pb = ProgressBar::new(final_disk_size);
77    let bs = block_size as u64;
78    let total_blocks = final_disk_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_disk_size {
84            block_len = final_disk_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_disk_size {
94            writer.write_parent_ref(block_len as u32)?;
95            pb.inc(block_len);
96            continue;
97        }
98
99        // Build block data from base + overlay
100        let mut data = vec![0u8; block_len as usize];
101
102        if is_modified {
103            if block_start < base_disk_size {
104                let base_data =
105                    base_snap.read_at(SnapshotStream::Disk, block_start, block_len as usize)?;
106                data[..base_data.len()].copy_from_slice(&base_data);
107            }
108
109            for ov_blk in start_ov_blk..=end_ov_blk {
110                if modified_blocks.contains(&ov_blk) {
111                    let chunk_start = ov_blk * OVERLAY_BLOCK_SIZE;
112                    let chunk_end =
113                        std::cmp::min(chunk_start + OVERLAY_BLOCK_SIZE, final_disk_size);
114                    let chunk_len = (chunk_end - chunk_start) as usize;
115                    let rel_start = (chunk_start - block_start) as usize;
116
117                    overlay_file.seek(SeekFrom::Start(chunk_start))?;
118                    overlay_file.read_exact(&mut data[rel_start..rel_start + chunk_len])?;
119                }
120            }
121        } else if block_start < base_disk_size {
122            let base_data =
123                base_snap.read_at(SnapshotStream::Disk, block_start, block_len as usize)?;
124            data[..base_data.len()].copy_from_slice(&base_data);
125        }
126
127        writer.write_data_block(&data)?;
128        pb.inc(block_len);
129    }
130
131    writer.end_stream()?;
132
133    // --- Memory stream ---
134    if let Some(mem_path) = memory_path {
135        println!("\nProcessing new memory state...");
136        let mut mem_file = File::open(&mem_path)?;
137        let mem_len = mem_file.metadata()?.len();
138
139        writer.begin_stream(false, mem_len);
140
141        let pb_mem = ProgressBar::new(mem_len);
142        let mut buf = vec![0u8; block_size as usize];
143
144        loop {
145            let mut pos = 0;
146            while pos < buf.len() {
147                match mem_file.read(&mut buf[pos..]) {
148                    Ok(0) => break,
149                    Ok(n) => pos += n,
150                    Err(e) => return Err(e.into()),
151                }
152            }
153            if pos == 0 {
154                break;
155            }
156
157            writer.write_data_block(&buf[..pos])?;
158            pb_mem.inc(pos as u64);
159        }
160
161        writer.end_stream()?;
162    }
163
164    // --- Finalize ---
165    let parent_path = if thin {
166        Some(
167            std::fs::canonicalize(&base_path)?
168                .to_string_lossy()
169                .to_string(),
170        )
171    } else {
172        None
173    };
174
175    let meta_bytes = message.as_ref().map(|m| m.as_bytes());
176    writer.finalize(parent_path, meta_bytes)?;
177
178    if !keep_overlay {
179        println!("Cleaning up overlay files...");
180        let _ = std::fs::remove_file(&overlay_path);
181        let _ = std::fs::remove_file(&meta_path);
182    }
183
184    println!("Commit complete: {:?}", output_path);
185    Ok(())
186}