#![cfg(target_os = "windows")]
use std::io::{self, Write};
use std::path::Path;
use flate2::write::GzEncoder;
use flate2::Compression;
use sha2::{Digest, Sha256};
use tar::Builder;
use zlayer_agent::windows::wclayer::{self, LayerChain};
#[derive(Debug, Clone)]
pub struct CapturedLayer {
pub bytes: Vec<u8>,
pub digest: String,
pub size: u64,
pub diff_id: String,
}
pub fn capture_diff_blob(
layer_path: &Path,
parent_chain: &LayerChain,
export_dir: &Path,
) -> io::Result<CapturedLayer> {
if std::fs::metadata(export_dir).is_ok() {
std::fs::remove_dir_all(export_dir)?;
}
std::fs::create_dir_all(export_dir)?;
wclayer::export_layer(layer_path, export_dir, parent_chain, "{}")
.map_err(|e| io::Error::other(format!("HcsExportLayer: {e}")))?;
let tar_bytes = tar_export_folder(export_dir)?;
let diff_id = format!("sha256:{}", hex::encode(Sha256::digest(&tar_bytes)));
let compressed = gzip_bytes(&tar_bytes)?;
let digest = format!("sha256:{}", hex::encode(Sha256::digest(&compressed)));
let size = compressed.len() as u64;
if let Err(e) = std::fs::remove_dir_all(export_dir) {
tracing::warn!(
export_dir = %export_dir.display(),
error = %e,
"failed to remove HCS export folder after capture"
);
}
Ok(CapturedLayer {
bytes: compressed,
digest,
size,
diff_id,
})
}
fn tar_export_folder(folder: &Path) -> io::Result<Vec<u8>> {
let mut builder = Builder::new(Vec::new());
append_dir_contents(&mut builder, folder, Path::new(""))?;
builder.finish()?;
builder
.into_inner()
.map_err(|e| io::Error::other(format!("tar finalize: {e}")))
}
fn append_dir_contents<W: Write>(
builder: &mut Builder<W>,
dir: &Path,
tar_rel: &Path,
) -> io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let name = entry.file_name();
let entry_tar_path = tar_rel.join(&name);
let meta = entry.metadata()?;
if meta.is_dir() {
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Directory);
header.set_size(0);
header.set_mode(0o755);
header.set_mtime(0);
header.set_path(format!(
"{}/",
entry_tar_path.to_string_lossy().replace('\\', "/")
))?;
header.set_cksum();
builder.append(&header, std::io::empty())?;
append_dir_contents(builder, &path, &entry_tar_path)?;
} else {
let data = std::fs::read(&path)?;
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o644);
header.set_mtime(0);
header.set_path(entry_tar_path.to_string_lossy().replace('\\', "/"))?;
header.set_cksum();
builder.append(&header, data.as_slice())?;
}
}
Ok(())
}
fn gzip_bytes(input: &[u8]) -> io::Result<Vec<u8>> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(input)?;
encoder.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gzip_bytes_roundtrips_via_flate2() {
let original = b"hello from the hcs builder test";
let compressed = gzip_bytes(original).expect("gzip encode");
assert_eq!(&compressed[..2], &[0x1f, 0x8b]);
let mut decoder = flate2::read::GzDecoder::new(&compressed[..]);
let mut back = Vec::new();
std::io::Read::read_to_end(&mut decoder, &mut back).expect("gzip decode");
assert_eq!(back, original);
}
#[test]
fn tar_export_folder_includes_nested_structure() {
let tmp = tempfile::tempdir().expect("tmpdir");
std::fs::create_dir_all(tmp.path().join("Files")).unwrap();
std::fs::create_dir_all(tmp.path().join("Hives")).unwrap();
std::fs::write(tmp.path().join("Files").join("alpha.txt"), b"hello").unwrap();
std::fs::write(tmp.path().join("Hives").join("REGISTRY"), b"hive").unwrap();
std::fs::write(tmp.path().join("tombstones.txt"), b"").unwrap();
let bytes = tar_export_folder(tmp.path()).expect("tar build");
assert!(!bytes.is_empty());
let mut archive = tar::Archive::new(bytes.as_slice());
let mut paths: Vec<String> = Vec::new();
for entry in archive.entries().expect("entries") {
let entry = entry.unwrap();
paths.push(entry.path().unwrap().to_string_lossy().into_owned());
}
assert!(paths.iter().any(|p| p.contains("Files/alpha.txt")));
assert!(paths.iter().any(|p| p.contains("Hives/REGISTRY")));
assert!(paths.iter().any(|p| p.contains("tombstones.txt")));
}
#[test]
fn compressed_and_uncompressed_digests_differ() {
let uncompressed = vec![0u8; 1024];
let compressed = gzip_bytes(&uncompressed).unwrap();
let diff_id = format!("sha256:{}", hex::encode(Sha256::digest(&uncompressed)));
let digest = format!("sha256:{}", hex::encode(Sha256::digest(&compressed)));
assert_ne!(diff_id, digest);
}
#[test]
fn gzip_bytes_handles_empty_input() {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(b"").unwrap();
let gz = encoder.finish().unwrap();
assert!(gz.len() >= 2);
}
}