use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use crate::assets::{crc32_file_range, AssetCollector};
use crate::format::{PackFooter, PackManifest, FOOTER_SIZE, SIDECAR_EXTENSION};
use crate::Result;
const MAX_MANIFEST_SIZE: u64 = 16 * 1024 * 1024;
pub struct Packer {
stub_path: Option<std::path::PathBuf>,
manifest: PackManifest,
asset_collector: Option<AssetCollector>,
}
#[cfg(target_os = "macos")]
enum PackedEmbeddedError {
NotMachO(Box<Packer>),
PackError(crate::PackError),
}
impl Packer {
pub fn new(manifest: PackManifest) -> Self {
Self {
stub_path: None,
manifest,
asset_collector: None,
}
}
pub fn with_stub(mut self, stub_path: impl AsRef<Path>) -> Self {
self.stub_path = Some(stub_path.as_ref().to_path_buf());
self
}
pub fn with_assets(mut self, collector: AssetCollector) -> Self {
self.manifest.assets = collector.inventory().clone();
self.asset_collector = Some(collector);
self
}
pub fn with_asset_collector(mut self, collector: AssetCollector) -> Self {
self.asset_collector = Some(collector);
self
}
pub fn manifest_mut(&mut self) -> &mut PackManifest {
&mut self.manifest
}
pub fn pack(self, output: impl AsRef<Path>) -> Result<PackedInfo> {
let output = output.as_ref();
let temp_dir = tempfile::tempdir()?;
let stub_path = self
.stub_path
.as_ref()
.ok_or_else(|| crate::PackError::AssetNotFound("stub executable".to_string()))?;
let stub_data = fs::read(stub_path)?;
let stub_size = stub_data.len() as u64;
fs::write(output, &stub_data)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(output)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(output, perms)?;
}
let sidecar_path = sidecar_path_for(output);
let mut sidecar_file = File::create(&sidecar_path)?;
let assets_temp = temp_dir.path().join("assets.tar.zst");
let assets_size = if let Some(collector) = &self.asset_collector {
collector.compress(&assets_temp, true)?
} else {
let empty_file = File::create(&assets_temp)?;
let encoder = zstd::stream::Encoder::new(empty_file, 1)?;
let tar_builder = tar::Builder::new(encoder);
let encoder = tar_builder.into_inner()?;
encoder.finish()?;
fs::metadata(&assets_temp)?.len()
};
let mut assets_file = File::open(&assets_temp)?;
std::io::copy(&mut assets_file, &mut sidecar_file)?;
let manifest_offset = assets_size;
let manifest_json = self.manifest.to_json()?;
let manifest_size = manifest_json.len() as u64;
sidecar_file.write_all(&manifest_json)?;
sidecar_file.flush()?;
drop(sidecar_file);
let checksum_size = assets_size + manifest_size;
let checksum = crc32_file_range(&sidecar_path, 0, checksum_size)?;
let footer = PackFooter {
stub_size: 0, assets_offset: 0, assets_size,
manifest_offset,
manifest_size,
checksum,
};
let mut sidecar_file = fs::OpenOptions::new().append(true).open(&sidecar_path)?;
sidecar_file.write_all(&footer.to_bytes())?;
let sidecar_total = assets_size + manifest_size + FOOTER_SIZE as u64;
let total_size = stub_size + sidecar_total;
Ok(PackedInfo {
stub_size,
assets_size,
manifest_size,
total_size,
checksum,
sidecar_path: Some(sidecar_path),
})
}
pub fn pack_embedded(self, output: impl AsRef<Path>) -> Result<PackedInfo> {
#[cfg(target_os = "macos")]
{
match self.try_pack_embedded_macho(output.as_ref()) {
Ok(info) => Ok(info),
Err(PackedEmbeddedError::NotMachO(packer)) => {
packer.pack_embedded_append(output)
}
Err(PackedEmbeddedError::PackError(e)) => Err(e),
}
}
#[cfg(not(target_os = "macos"))]
{
self.pack_embedded_append(output)
}
}
#[cfg(target_os = "macos")]
fn try_pack_embedded_macho(
self,
output: &Path,
) -> std::result::Result<PackedInfo, PackedEmbeddedError> {
use crate::macho::MachoFile;
let stub_path = match self.stub_path.as_ref() {
Some(p) => p,
None => {
return Err(PackedEmbeddedError::PackError(
crate::PackError::AssetNotFound("stub executable".to_string()),
))
}
};
let stub_data = match fs::read(stub_path) {
Ok(data) => data,
Err(e) => return Err(PackedEmbeddedError::PackError(e.into())),
};
let macho = match MachoFile::parse(&stub_data) {
Ok(m) => m,
Err(_) => return Err(PackedEmbeddedError::NotMachO(Box::new(self))), };
if macho.find_section("__SMOLVM", "__smolvm").is_none() {
return Err(PackedEmbeddedError::NotMachO(Box::new(self))); }
match self.pack_embedded_macho_inner(output, stub_data, macho) {
Ok(info) => Ok(info),
Err(e) => Err(PackedEmbeddedError::PackError(e)),
}
}
#[cfg(target_os = "macos")]
fn pack_embedded_macho_inner(
self,
output: &Path,
stub_data: Vec<u8>,
mut macho: crate::macho::MachoFile,
) -> Result<PackedInfo> {
use crate::format::{SectionHeader, SECTION_HEADER_SIZE};
let stub_size = stub_data.len() as u64;
let temp_dir = tempfile::tempdir()?;
let assets_temp = temp_dir.path().join("assets.tar.zst");
let assets_size = if let Some(collector) = &self.asset_collector {
collector.compress(&assets_temp, false)? } else {
let empty_file = File::create(&assets_temp)?;
let encoder = zstd::stream::Encoder::new(empty_file, 1)?;
let tar_builder = tar::Builder::new(encoder);
let encoder = tar_builder.into_inner()?;
encoder.finish()?;
fs::metadata(&assets_temp)?.len()
};
let manifest_json = self.manifest.to_json()?;
let manifest_size = manifest_json.len() as u32;
let mut hasher = crc32fast::Hasher::new();
hasher.update(&manifest_json);
let assets_data = fs::read(&assets_temp)?;
hasher.update(&assets_data);
let checksum = hasher.finalize();
let header = SectionHeader {
manifest_size,
assets_size,
checksum,
};
let mut section_data =
Vec::with_capacity(SECTION_HEADER_SIZE + manifest_json.len() + assets_data.len());
section_data.extend_from_slice(&header.to_bytes());
section_data.extend_from_slice(&manifest_json);
section_data.extend_from_slice(&assets_data);
macho
.write_section("__SMOLVM", "__smolvm", §ion_data)
.map_err(|e| crate::PackError::Signing(format!("failed to write section: {}", e)))?;
macho
.sign_adhoc()
.map_err(|e| crate::PackError::Signing(format!("failed to sign: {}", e)))?;
let output_data = macho.write();
let total_size = output_data.len() as u64;
fs::write(output, &output_data)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(output)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(output, perms)?;
}
Ok(PackedInfo {
stub_size,
assets_size,
manifest_size: manifest_size as u64,
total_size,
checksum,
sidecar_path: None,
})
}
fn pack_embedded_append(self, output: impl AsRef<Path>) -> Result<PackedInfo> {
let output = output.as_ref();
let temp_dir = tempfile::tempdir()?;
let stub_path = self
.stub_path
.as_ref()
.ok_or_else(|| crate::PackError::AssetNotFound("stub executable".to_string()))?;
let stub_data = fs::read(stub_path)?;
let stub_size = stub_data.len() as u64;
let mut output_file = File::create(output)?;
output_file.write_all(&stub_data)?;
let assets_temp = temp_dir.path().join("assets.tar.zst");
let assets_size = if let Some(collector) = &self.asset_collector {
collector.compress(&assets_temp, false)? } else {
let empty_file = File::create(&assets_temp)?;
let encoder = zstd::stream::Encoder::new(empty_file, 1)?;
let tar_builder = tar::Builder::new(encoder);
let encoder = tar_builder.into_inner()?;
encoder.finish()?;
fs::metadata(&assets_temp)?.len()
};
let assets_offset = stub_size; let mut assets_file = File::open(&assets_temp)?;
std::io::copy(&mut assets_file, &mut output_file)?;
let manifest_offset = stub_size + assets_size;
let manifest_json = self.manifest.to_json()?;
let manifest_size = manifest_json.len() as u64;
output_file.write_all(&manifest_json)?;
output_file.flush()?;
drop(output_file);
let checksum_size = assets_size + manifest_size;
let checksum = crc32_file_range(output, assets_offset, checksum_size)?;
let footer = PackFooter {
stub_size,
assets_offset, assets_size,
manifest_offset,
manifest_size,
checksum,
};
let mut output_file = fs::OpenOptions::new().append(true).open(output)?;
output_file.write_all(&footer.to_bytes())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(output)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(output, perms)?;
}
let total_size = stub_size + assets_size + manifest_size + FOOTER_SIZE as u64;
Ok(PackedInfo {
stub_size,
assets_size,
manifest_size,
total_size,
checksum,
sidecar_path: None, })
}
}
pub fn embed_libs_in_binary(binary_path: &Path, staging_dir: &Path) -> Result<()> {
use crate::format::LibsFooter;
let lib_dir = staging_dir.join("lib");
if !lib_dir.exists() {
return Ok(()); }
let has_libs = fs::read_dir(&lib_dir)?
.filter_map(|e| e.ok())
.any(|e| e.path().is_file());
if !has_libs {
return Ok(());
}
let temp_dir = tempfile::tempdir()?;
let libs_temp = temp_dir.path().join("libs.tar.zst");
let output_file = File::create(&libs_temp)?;
let encoder = zstd::stream::Encoder::new(output_file, crate::assets::ZSTD_LEVEL)
.map_err(|e| crate::PackError::Compression(e.to_string()))?;
let mut tar_builder = tar::Builder::new(encoder);
tar_builder
.append_dir_all("lib", &lib_dir)
.map_err(|e| crate::PackError::Tar(e.to_string()))?;
let encoder = tar_builder
.into_inner()
.map_err(|e| crate::PackError::Tar(e.to_string()))?;
encoder
.finish()
.map_err(|e| crate::PackError::Compression(e.to_string()))?;
let libs_size = fs::metadata(&libs_temp)?.len();
let binary_size = fs::metadata(binary_path)?.len();
let mut output_file = fs::OpenOptions::new().append(true).open(binary_path)?;
let mut libs_file = File::open(&libs_temp)?;
std::io::copy(&mut libs_file, &mut output_file)?;
let libs_footer = LibsFooter {
libs_offset: binary_size,
libs_size,
};
output_file.write_all(&libs_footer.to_bytes())?;
Ok(())
}
pub fn sidecar_path_for(binary_path: impl AsRef<Path>) -> PathBuf {
let mut path = binary_path.as_ref().to_path_buf();
let filename = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
path.set_file_name(format!("{}{}", filename, SIDECAR_EXTENSION));
path
}
#[derive(Debug, Clone)]
pub struct PackedInfo {
pub stub_size: u64,
pub assets_size: u64,
pub manifest_size: u64,
pub total_size: u64,
pub checksum: u32,
pub sidecar_path: Option<PathBuf>,
}
pub fn read_footer_from_sidecar(sidecar_path: impl AsRef<Path>) -> Result<PackFooter> {
let mut file = File::open(sidecar_path.as_ref())?;
let file_size = file.metadata()?.len();
if file_size < FOOTER_SIZE as u64 {
return Err(crate::PackError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"sidecar file too small to contain footer",
)));
}
file.seek(SeekFrom::End(-(FOOTER_SIZE as i64)))?;
let mut footer_bytes = [0u8; FOOTER_SIZE];
file.read_exact(&mut footer_bytes)?;
let footer = PackFooter::from_bytes(&footer_bytes)?;
validate_footer_bounds(&footer, file_size)?;
Ok(footer)
}
pub fn read_manifest_from_sidecar(sidecar_path: impl AsRef<Path>) -> Result<PackManifest> {
let footer = read_footer_from_sidecar(sidecar_path.as_ref())?;
let mut file = File::open(sidecar_path.as_ref())?;
file.seek(SeekFrom::Start(footer.manifest_offset))?;
let mut manifest_bytes = vec![0u8; footer.manifest_size as usize];
file.read_exact(&mut manifest_bytes)?;
PackManifest::from_json(&manifest_bytes)
}
pub fn read_footer(path: impl AsRef<Path>) -> Result<PackFooter> {
let sidecar = sidecar_path_for(path.as_ref());
if sidecar.exists() {
return read_footer_from_sidecar(&sidecar);
}
let mut file = File::open(path.as_ref())?;
let file_size = file.metadata()?.len();
if file_size < FOOTER_SIZE as u64 {
return Err(crate::PackError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"file too small to contain footer",
)));
}
file.seek(SeekFrom::End(-(FOOTER_SIZE as i64)))?;
let mut footer_bytes = [0u8; FOOTER_SIZE];
file.read_exact(&mut footer_bytes)?;
PackFooter::from_bytes(&footer_bytes)
}
pub fn read_manifest(path: impl AsRef<Path>) -> Result<PackManifest> {
let sidecar = sidecar_path_for(path.as_ref());
if sidecar.exists() {
return read_manifest_from_sidecar(&sidecar);
}
let footer = read_footer(path.as_ref())?;
let mut file = File::open(path.as_ref())?;
file.seek(SeekFrom::Start(footer.manifest_offset))?;
let mut manifest_bytes = vec![0u8; footer.manifest_size as usize];
file.read_exact(&mut manifest_bytes)?;
PackManifest::from_json(&manifest_bytes)
}
pub fn is_sidecar_mode(footer: &PackFooter) -> bool {
footer.assets_offset == 0
}
pub fn verify_checksum(path: impl AsRef<Path>) -> Result<bool> {
let footer = read_footer(path.as_ref())?;
if is_sidecar_mode(&footer) {
let sidecar = sidecar_path_for(path.as_ref());
if !sidecar.exists() {
return Ok(false);
}
let checksum_size = footer.assets_size + footer.manifest_size;
let actual = crc32_file_range(&sidecar, 0, checksum_size)?;
Ok(actual == footer.checksum)
} else {
let checksum_size = footer.assets_size + footer.manifest_size;
let actual = crc32_file_range(path.as_ref(), footer.assets_offset, checksum_size)?;
Ok(actual == footer.checksum)
}
}
pub fn verify_sidecar_checksum(
sidecar_path: impl AsRef<Path>,
footer: &PackFooter,
) -> Result<bool> {
let checksum_size = footer
.assets_size
.checked_add(footer.manifest_size)
.ok_or_else(|| {
crate::PackError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"assets_size + manifest_size overflow",
))
})?;
let actual = crc32_file_range(sidecar_path.as_ref(), 0, checksum_size)?;
Ok(actual == footer.checksum)
}
fn validate_footer_bounds(footer: &PackFooter, file_size: u64) -> Result<()> {
if footer.manifest_size > MAX_MANIFEST_SIZE {
return Err(crate::PackError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"manifest size ({} bytes) exceeds maximum ({} bytes)",
footer.manifest_size, MAX_MANIFEST_SIZE
),
)));
}
let content_end = footer
.assets_size
.checked_add(footer.manifest_size)
.and_then(|s| s.checked_add(FOOTER_SIZE as u64));
match content_end {
Some(end) if end <= file_size => {}
_ => {
return Err(crate::PackError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"footer sizes exceed file size (corrupt or truncated sidecar)",
)));
}
}
if footer.assets_offset == 0 && footer.manifest_offset != footer.assets_size {
return Err(crate::PackError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"sidecar manifest_offset ({}) does not match assets_size ({}): corrupt footer",
footer.manifest_offset, footer.assets_size
),
)));
}
let manifest_end = footer
.manifest_offset
.checked_add(footer.manifest_size)
.and_then(|end| end.checked_add(FOOTER_SIZE as u64));
match manifest_end {
Some(end) if end <= file_size => {}
_ => {
return Err(crate::PackError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"manifest offset/size exceeds file bounds",
)));
}
}
Ok(())
}
pub fn extract_assets(packed_path: impl AsRef<Path>, output_dir: impl AsRef<Path>) -> Result<()> {
let footer = read_footer(packed_path.as_ref())?;
if is_sidecar_mode(&footer) {
let sidecar = sidecar_path_for(packed_path.as_ref());
crate::assets::decompress_assets_from_file(&sidecar, output_dir.as_ref())?;
} else {
let mut file = File::open(packed_path.as_ref())?;
file.seek(SeekFrom::Start(footer.assets_offset))?;
let mut compressed = vec![0u8; footer.assets_size as usize];
file.read_exact(&mut compressed)?;
crate::assets::decompress_assets(&compressed, output_dir.as_ref())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_pack_and_read() {
let temp_dir = tempfile::tempdir().unwrap();
let stub_path = temp_dir.path().join("stub");
let mut stub_file = File::create(&stub_path).unwrap();
stub_file.write_all(b"#!/bin/sh\necho stub").unwrap();
let manifest = PackManifest::new(
"alpine:latest".to_string(),
"sha256:abc123".to_string(),
"linux/arm64".to_string(),
"darwin/arm64".to_string(),
);
let output_path = temp_dir.path().join("packed");
let packer = Packer::new(manifest).with_stub(&stub_path);
let info = packer.pack(&output_path).unwrap();
assert!(info.stub_size > 0);
assert!(info.total_size > info.stub_size);
let sidecar = sidecar_path_for(&output_path);
assert!(sidecar.exists());
let footer = read_footer(&output_path).unwrap();
assert_eq!(footer.stub_size, 0); assert_eq!(footer.assets_offset, 0);
let manifest = read_manifest(&output_path).unwrap();
assert_eq!(manifest.image, "alpine:latest");
assert!(verify_checksum(&output_path).unwrap());
}
#[test]
fn test_pack_with_assets() {
let temp_dir = tempfile::tempdir().unwrap();
let stub_path = temp_dir.path().join("stub");
fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
let staging = temp_dir.path().join("staging");
let mut collector = AssetCollector::new(staging).unwrap();
collector
.add_layer("sha256:abc123def456", b"layer content")
.unwrap();
let manifest = PackManifest::new(
"test:latest".to_string(),
"sha256:test".to_string(),
"linux/arm64".to_string(),
"darwin/arm64".to_string(),
);
let output_path = temp_dir.path().join("packed");
let packer = Packer::new(manifest)
.with_stub(&stub_path)
.with_assets(collector);
packer.pack(&output_path).unwrap();
let manifest = read_manifest(&output_path).unwrap();
assert_eq!(manifest.assets.layers.len(), 1);
assert_eq!(manifest.assets.layers[0].digest, "sha256:abc123def456");
let extract_dir = temp_dir.path().join("extracted");
extract_assets(&output_path, &extract_dir).unwrap();
let layer_file = extract_dir.join("layers/abc123def456.tar");
assert!(layer_file.exists());
assert_eq!(fs::read_to_string(&layer_file).unwrap(), "layer content");
}
#[test]
fn test_pack_embedded() {
let temp_dir = tempfile::tempdir().unwrap();
let stub_path = temp_dir.path().join("stub");
fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
let manifest = PackManifest::new(
"alpine:latest".to_string(),
"sha256:abc123".to_string(),
"linux/arm64".to_string(),
"darwin/arm64".to_string(),
);
let output_path = temp_dir.path().join("packed-single");
let packer = Packer::new(manifest).with_stub(&stub_path);
let info = packer.pack_embedded(&output_path).unwrap();
assert!(info.sidecar_path.is_none());
assert!(info.stub_size > 0);
assert!(info.total_size > info.stub_size);
let sidecar = sidecar_path_for(&output_path);
assert!(!sidecar.exists());
let output_size = fs::metadata(&output_path).unwrap().len();
assert_eq!(output_size, info.total_size);
let footer = read_footer(&output_path).unwrap();
assert!(footer.assets_offset > 0); assert_eq!(footer.stub_size, info.stub_size);
let manifest = read_manifest(&output_path).unwrap();
assert_eq!(manifest.image, "alpine:latest");
assert!(verify_checksum(&output_path).unwrap());
}
#[test]
fn test_pack_embedded_with_assets() {
let temp_dir = tempfile::tempdir().unwrap();
let stub_path = temp_dir.path().join("stub");
fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
let staging = temp_dir.path().join("staging");
let mut collector = AssetCollector::new(staging).unwrap();
collector
.add_layer("sha256:embedded123456", b"embedded layer content")
.unwrap();
let manifest = PackManifest::new(
"test:embedded".to_string(),
"sha256:test".to_string(),
"linux/arm64".to_string(),
"darwin/arm64".to_string(),
);
let output_path = temp_dir.path().join("packed-embedded");
let packer = Packer::new(manifest)
.with_stub(&stub_path)
.with_assets(collector);
packer.pack_embedded(&output_path).unwrap();
let manifest = read_manifest(&output_path).unwrap();
assert_eq!(manifest.assets.layers.len(), 1);
assert_eq!(manifest.assets.layers[0].digest, "sha256:embedded123456");
let footer = read_footer(&output_path).unwrap();
assert!(footer.assets_offset > 0);
assert!(!is_sidecar_mode(&footer));
let extract_dir = temp_dir.path().join("extracted");
extract_assets(&output_path, &extract_dir).unwrap();
let layer_file = extract_dir.join("layers/embedded1234.tar"); assert!(layer_file.exists());
assert_eq!(
fs::read_to_string(&layer_file).unwrap(),
"embedded layer content"
);
}
#[test]
fn test_sidecar_checksum_verification() {
let temp_dir = tempfile::tempdir().unwrap();
let stub_path = temp_dir.path().join("stub");
fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
let manifest = PackManifest::new(
"alpine:latest".to_string(),
"sha256:abc123".to_string(),
"linux/arm64".to_string(),
"darwin/arm64".to_string(),
);
let output_path = temp_dir.path().join("packed");
let packer = Packer::new(manifest).with_stub(&stub_path);
packer.pack(&output_path).unwrap();
let sidecar = sidecar_path_for(&output_path);
let footer = read_footer_from_sidecar(&sidecar).unwrap();
assert!(verify_sidecar_checksum(&sidecar, &footer).unwrap());
let mut data = fs::read(&sidecar).unwrap();
if !data.is_empty() {
data[0] ^= 0xFF;
}
fs::write(&sidecar, &data).unwrap();
assert!(!verify_sidecar_checksum(&sidecar, &footer).unwrap());
}
#[test]
fn test_footer_bounds_reject_oversized_manifest() {
let temp_dir = tempfile::tempdir().unwrap();
let sidecar = temp_dir.path().join("bad.smolmachine");
let footer = PackFooter {
stub_size: 0,
assets_offset: 0,
assets_size: 100,
manifest_offset: 100,
manifest_size: 32 * 1024 * 1024, checksum: 0,
};
let footer_bytes = footer.to_bytes();
let mut data = vec![0u8; 200]; data.extend_from_slice(&footer_bytes);
fs::write(&sidecar, &data).unwrap();
let result = read_footer_from_sidecar(&sidecar);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("exceeds maximum"), "got: {}", err_msg);
}
#[test]
fn test_footer_bounds_reject_manifest_offset_mismatch() {
let temp_dir = tempfile::tempdir().unwrap();
let sidecar = temp_dir.path().join("mismatch.smolmachine");
let footer = PackFooter {
stub_size: 0,
assets_offset: 0, assets_size: 100,
manifest_offset: 50, manifest_size: 50,
checksum: 0,
};
let footer_bytes = footer.to_bytes();
let mut data = vec![0u8; 150];
data.extend_from_slice(&footer_bytes);
fs::write(&sidecar, &data).unwrap();
let result = read_footer_from_sidecar(&sidecar);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("manifest_offset") && err_msg.contains("does not match"),
"got: {}",
err_msg
);
}
#[test]
fn test_sidecar_vs_embedded_mode_detection() {
let temp_dir = tempfile::tempdir().unwrap();
let stub_path = temp_dir.path().join("stub");
fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
let manifest = PackManifest::new(
"test:latest".to_string(),
"sha256:test".to_string(),
"linux/arm64".to_string(),
"darwin/arm64".to_string(),
);
let sidecar_output = temp_dir.path().join("sidecar-mode");
let packer = Packer::new(manifest.clone()).with_stub(&stub_path);
packer.pack(&sidecar_output).unwrap();
let embedded_output = temp_dir.path().join("embedded-mode");
let packer = Packer::new(manifest).with_stub(&stub_path);
packer.pack_embedded(&embedded_output).unwrap();
let sidecar_footer = read_footer(&sidecar_output).unwrap();
let embedded_footer = read_footer(&embedded_output).unwrap();
assert!(is_sidecar_mode(&sidecar_footer));
assert!(!is_sidecar_mode(&embedded_footer));
assert!(verify_checksum(&sidecar_output).unwrap());
assert!(verify_checksum(&embedded_output).unwrap());
}
}