use serde::{Deserialize, Serialize};
use crate::{PackError, Result};
pub const MAGIC: &[u8; 8] = b"SMOLPACK";
pub const SECTION_MAGIC: &[u8; 8] = b"SMOLSECT";
pub const LIBS_MAGIC: &[u8; 8] = b"SMOLLIBS";
pub const FORMAT_VERSION: u32 = 1;
pub const SIDECAR_EXTENSION: &str = ".smolmachine";
pub const FOOTER_SIZE: usize = 64;
pub const SECTION_HEADER_SIZE: usize = 32;
pub const LIBS_FOOTER_SIZE: usize = 32;
#[derive(Debug, Clone, Copy)]
pub struct SectionHeader {
pub manifest_size: u32,
pub assets_size: u64,
pub checksum: u32,
}
impl SectionHeader {
pub fn to_bytes(&self) -> [u8; SECTION_HEADER_SIZE] {
let mut buf = [0u8; SECTION_HEADER_SIZE];
buf[0..8].copy_from_slice(SECTION_MAGIC);
buf[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes());
buf[12..16].copy_from_slice(&self.manifest_size.to_le_bytes());
buf[16..24].copy_from_slice(&self.assets_size.to_le_bytes());
buf[24..28].copy_from_slice(&self.checksum.to_le_bytes());
buf
}
pub fn from_bytes(buf: &[u8]) -> Result<Self> {
if buf.len() < SECTION_HEADER_SIZE {
return Err(PackError::InvalidMagic);
}
if &buf[0..8] != SECTION_MAGIC {
return Err(PackError::InvalidMagic);
}
let version = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
if version != FORMAT_VERSION {
return Err(PackError::UnsupportedVersion(version));
}
Ok(Self {
manifest_size: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
assets_size: u64::from_le_bytes([
buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
]),
checksum: u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]),
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct LibsFooter {
pub libs_offset: u64,
pub libs_size: u64,
}
impl LibsFooter {
pub fn to_bytes(&self) -> [u8; LIBS_FOOTER_SIZE] {
let mut buf = [0u8; LIBS_FOOTER_SIZE];
buf[0..8].copy_from_slice(LIBS_MAGIC);
buf[8..12].copy_from_slice(&1u32.to_le_bytes()); buf[12..20].copy_from_slice(&self.libs_offset.to_le_bytes());
buf[20..28].copy_from_slice(&self.libs_size.to_le_bytes());
buf
}
pub fn from_bytes(buf: &[u8; LIBS_FOOTER_SIZE]) -> Result<Self> {
if &buf[0..8] != LIBS_MAGIC {
return Err(PackError::InvalidMagic);
}
let version = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
if version != 1 {
return Err(PackError::UnsupportedVersion(version));
}
Ok(Self {
libs_offset: u64::from_le_bytes([
buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19],
]),
libs_size: u64::from_le_bytes([
buf[20], buf[21], buf[22], buf[23], buf[24], buf[25], buf[26], buf[27],
]),
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct PackFooter {
pub stub_size: u64,
pub assets_offset: u64,
pub assets_size: u64,
pub manifest_offset: u64,
pub manifest_size: u64,
pub checksum: u32,
}
impl PackFooter {
pub fn to_bytes(&self) -> [u8; FOOTER_SIZE] {
let mut buf = [0u8; FOOTER_SIZE];
buf[0..8].copy_from_slice(MAGIC);
buf[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes());
buf[12..20].copy_from_slice(&self.stub_size.to_le_bytes());
buf[20..28].copy_from_slice(&self.assets_offset.to_le_bytes());
buf[28..36].copy_from_slice(&self.assets_size.to_le_bytes());
buf[36..44].copy_from_slice(&self.manifest_offset.to_le_bytes());
buf[44..52].copy_from_slice(&self.manifest_size.to_le_bytes());
buf[52..56].copy_from_slice(&self.checksum.to_le_bytes());
buf
}
pub fn from_bytes(buf: &[u8; FOOTER_SIZE]) -> Result<Self> {
if &buf[0..8] != MAGIC {
return Err(PackError::InvalidMagic);
}
let version = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
if version != FORMAT_VERSION {
return Err(PackError::UnsupportedVersion(version));
}
Ok(Self {
stub_size: u64::from_le_bytes([
buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19],
]),
assets_offset: u64::from_le_bytes([
buf[20], buf[21], buf[22], buf[23], buf[24], buf[25], buf[26], buf[27],
]),
assets_size: u64::from_le_bytes([
buf[28], buf[29], buf[30], buf[31], buf[32], buf[33], buf[34], buf[35],
]),
manifest_offset: u64::from_le_bytes([
buf[36], buf[37], buf[38], buf[39], buf[40], buf[41], buf[42], buf[43],
]),
manifest_size: u64::from_le_bytes([
buf[44], buf[45], buf[46], buf[47], buf[48], buf[49], buf[50], buf[51],
]),
checksum: u32::from_le_bytes([buf[52], buf[53], buf[54], buf[55]]),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum PackMode {
#[default]
Container,
Vm,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackManifest {
#[serde(default)]
pub mode: PackMode,
pub image: String,
pub digest: String,
pub platform: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entrypoint: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cmd: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub env: Vec<String>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub secret_refs: std::collections::BTreeMap<String, smolvm_protocol::SecretRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workdir: Option<String>,
pub cpus: u8,
pub mem: u32,
#[serde(default)]
pub image_size: u64,
#[serde(default)]
pub network: bool,
#[serde(default)]
pub gpu: bool,
pub host_platform: String,
pub created: String,
pub smolvm_version: String,
pub assets: AssetInventory,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetInventory {
pub libraries: Vec<AssetEntry>,
pub agent_rootfs: AssetEntry,
pub layers: Vec<LayerEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage_template: Option<AssetEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overlay_template: Option<AssetEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overlay_logical_size: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetEntry {
pub path: String,
pub size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayerEntry {
pub digest: String,
pub path: String,
pub size: u64,
}
fn rfc3339_now() -> String {
let now = time::OffsetDateTime::now_utc();
now.format(&time::format_description::well_known::Rfc3339)
.expect("RFC 3339 formatting should never fail for a valid OffsetDateTime")
}
impl PackManifest {
pub fn new(image: String, digest: String, platform: String, host_platform: String) -> Self {
Self {
mode: PackMode::default(),
image,
digest,
platform,
entrypoint: Vec::new(),
cmd: Vec::new(),
env: Vec::new(),
secret_refs: std::collections::BTreeMap::new(),
workdir: None,
cpus: 1,
mem: 256,
image_size: 0,
network: false,
gpu: false,
host_platform,
created: rfc3339_now(),
smolvm_version: env!("CARGO_PKG_VERSION").to_string(),
assets: AssetInventory {
libraries: Vec::new(),
agent_rootfs: AssetEntry {
path: "agent-rootfs.tar".to_string(),
size: 0,
},
layers: Vec::new(),
storage_template: None,
overlay_template: None,
overlay_logical_size: None,
},
}
}
pub fn to_json(&self) -> Result<Vec<u8>> {
Ok(serde_json::to_vec_pretty(self)?)
}
pub fn from_json(data: &[u8]) -> Result<Self> {
Ok(serde_json::from_slice(data)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_secret_refs_roundtrip() {
let empty = PackManifest::new(
"alpine".to_string(),
"sha256:abc".to_string(),
"linux/arm64".to_string(),
"linux/arm64".to_string(),
);
let json = String::from_utf8(empty.to_json().unwrap()).unwrap();
assert!(
!json.contains("secret_refs"),
"empty secret_refs must not be serialized"
);
let mut m = empty;
m.secret_refs.insert(
"API_TOKEN".to_string(),
smolvm_protocol::SecretRef {
from_store: Some("prod-token".to_string()),
from_env: None,
from_file: None,
},
);
let restored = PackManifest::from_json(&m.to_json().unwrap()).unwrap();
assert_eq!(restored.secret_refs.len(), 1);
assert_eq!(
restored.secret_refs["API_TOKEN"].from_store.as_deref(),
Some("prod-token")
);
}
#[test]
fn test_footer_roundtrip() {
let footer = PackFooter {
stub_size: 512 * 1024,
assets_offset: 512 * 1024,
assets_size: 50 * 1024 * 1024,
manifest_offset: 512 * 1024 + 50 * 1024 * 1024,
manifest_size: 2048,
checksum: 0xDEADBEEF,
};
let bytes = footer.to_bytes();
assert_eq!(bytes.len(), FOOTER_SIZE);
let restored = PackFooter::from_bytes(&bytes).unwrap();
assert_eq!(restored.stub_size, footer.stub_size);
assert_eq!(restored.assets_offset, footer.assets_offset);
assert_eq!(restored.assets_size, footer.assets_size);
assert_eq!(restored.manifest_offset, footer.manifest_offset);
assert_eq!(restored.manifest_size, footer.manifest_size);
assert_eq!(restored.checksum, footer.checksum);
}
#[test]
fn test_footer_invalid_magic() {
let mut bytes = [0u8; FOOTER_SIZE];
bytes[0..8].copy_from_slice(b"BADMAGIC");
let result = PackFooter::from_bytes(&bytes);
assert!(matches!(result, Err(PackError::InvalidMagic)));
}
#[test]
fn test_footer_unsupported_version() {
let mut bytes = [0u8; FOOTER_SIZE];
bytes[0..8].copy_from_slice(MAGIC);
bytes[8..12].copy_from_slice(&99u32.to_le_bytes());
let result = PackFooter::from_bytes(&bytes);
assert!(matches!(result, Err(PackError::UnsupportedVersion(99))));
}
#[test]
fn test_manifest_roundtrip() {
let mut manifest = PackManifest::new(
"alpine:latest".to_string(),
"sha256:abc123".to_string(),
"linux/arm64".to_string(),
"darwin/arm64".to_string(),
);
manifest.cpus = 2;
manifest.mem = 1024;
manifest.entrypoint = vec!["/bin/sh".to_string()];
manifest.env = vec!["PATH=/usr/local/bin:/usr/bin:/bin".to_string()];
manifest.assets.libraries.push(AssetEntry {
path: "lib/libkrun.dylib".to_string(),
size: 4 * 1024 * 1024,
});
let json = manifest.to_json().unwrap();
let restored = PackManifest::from_json(&json).unwrap();
assert_eq!(restored.image, "alpine:latest");
assert_eq!(restored.digest, "sha256:abc123");
assert_eq!(restored.cpus, 2);
assert_eq!(restored.mem, 1024);
assert_eq!(restored.entrypoint, vec!["/bin/sh"]);
assert_eq!(restored.assets.libraries.len(), 1);
}
#[test]
fn test_manifest_json_format() {
let manifest = PackManifest::new(
"ubuntu:22.04".to_string(),
"sha256:def456".to_string(),
"linux/amd64".to_string(),
"linux/amd64".to_string(),
);
let json = String::from_utf8(manifest.to_json().unwrap()).unwrap();
assert!(json.contains("\"image\": \"ubuntu:22.04\""));
assert!(json.contains("\"platform\": \"linux/amd64\""));
assert!(json.contains("\"host_platform\": \"linux/amd64\""));
assert!(json.contains("\"smolvm_version\""));
assert!(json.contains("\"created\""));
}
#[test]
fn test_pack_mode_default_is_container() {
assert_eq!(PackMode::default(), PackMode::Container);
}
#[test]
fn test_pack_mode_vm_roundtrip() {
let mut manifest = PackManifest::new(
"vm://myvm".to_string(),
"none".to_string(),
"linux/arm64".to_string(),
"darwin/arm64".to_string(),
);
manifest.mode = PackMode::Vm;
manifest.assets.overlay_template = Some(AssetEntry {
path: "overlay.raw".to_string(),
size: 2 * 1024 * 1024 * 1024,
});
let json = manifest.to_json().unwrap();
let restored = PackManifest::from_json(&json).unwrap();
assert_eq!(restored.mode, PackMode::Vm);
assert!(restored.assets.overlay_template.is_some());
assert_eq!(
restored.assets.overlay_template.unwrap().path,
"overlay.raw"
);
}
}