1use serde::{Deserialize, Serialize};
47
48use crate::{PackError, Result};
49
50pub const MAGIC: &[u8; 8] = b"SMOLPACK";
52
53pub const SECTION_MAGIC: &[u8; 8] = b"SMOLSECT";
55
56pub const LIBS_MAGIC: &[u8; 8] = b"SMOLLIBS";
58
59pub const FORMAT_VERSION: u32 = 1;
61
62pub const SIDECAR_EXTENSION: &str = ".smolmachine";
64
65pub const FOOTER_SIZE: usize = 64;
67
68pub const SECTION_HEADER_SIZE: usize = 32;
70
71pub const LIBS_FOOTER_SIZE: usize = 32;
73
74#[derive(Debug, Clone, Copy)]
94pub struct SectionHeader {
95 pub manifest_size: u32,
97 pub assets_size: u64,
99 pub checksum: u32,
101}
102
103impl SectionHeader {
104 pub fn to_bytes(&self) -> [u8; SECTION_HEADER_SIZE] {
106 let mut buf = [0u8; SECTION_HEADER_SIZE];
107
108 buf[0..8].copy_from_slice(SECTION_MAGIC);
110
111 buf[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes());
113
114 buf[12..16].copy_from_slice(&self.manifest_size.to_le_bytes());
116
117 buf[16..24].copy_from_slice(&self.assets_size.to_le_bytes());
119
120 buf[24..28].copy_from_slice(&self.checksum.to_le_bytes());
122
123 buf
126 }
127
128 pub fn from_bytes(buf: &[u8]) -> Result<Self> {
130 if buf.len() < SECTION_HEADER_SIZE {
131 return Err(PackError::InvalidMagic);
132 }
133
134 if &buf[0..8] != SECTION_MAGIC {
136 return Err(PackError::InvalidMagic);
137 }
138
139 let version = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
141 if version != FORMAT_VERSION {
142 return Err(PackError::UnsupportedVersion(version));
143 }
144
145 Ok(Self {
146 manifest_size: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
147 assets_size: u64::from_le_bytes([
148 buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
149 ]),
150 checksum: u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]),
151 })
152 }
153}
154
155#[derive(Debug, Clone, Copy)]
171pub struct LibsFooter {
172 pub libs_offset: u64,
174 pub libs_size: u64,
176}
177
178impl LibsFooter {
179 pub fn to_bytes(&self) -> [u8; LIBS_FOOTER_SIZE] {
181 let mut buf = [0u8; LIBS_FOOTER_SIZE];
182 buf[0..8].copy_from_slice(LIBS_MAGIC);
183 buf[8..12].copy_from_slice(&1u32.to_le_bytes()); buf[12..20].copy_from_slice(&self.libs_offset.to_le_bytes());
185 buf[20..28].copy_from_slice(&self.libs_size.to_le_bytes());
186 buf
188 }
189
190 pub fn from_bytes(buf: &[u8; LIBS_FOOTER_SIZE]) -> Result<Self> {
192 if &buf[0..8] != LIBS_MAGIC {
193 return Err(PackError::InvalidMagic);
194 }
195 let version = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
196 if version != 1 {
197 return Err(PackError::UnsupportedVersion(version));
198 }
199 Ok(Self {
200 libs_offset: u64::from_le_bytes([
201 buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19],
202 ]),
203 libs_size: u64::from_le_bytes([
204 buf[20], buf[21], buf[22], buf[23], buf[24], buf[25], buf[26], buf[27],
205 ]),
206 })
207 }
208}
209
210#[derive(Debug, Clone, Copy)]
226pub struct PackFooter {
227 pub stub_size: u64,
229 pub assets_offset: u64,
231 pub assets_size: u64,
233 pub manifest_offset: u64,
235 pub manifest_size: u64,
237 pub checksum: u32,
239}
240
241impl PackFooter {
242 pub fn to_bytes(&self) -> [u8; FOOTER_SIZE] {
244 let mut buf = [0u8; FOOTER_SIZE];
245
246 buf[0..8].copy_from_slice(MAGIC);
248
249 buf[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes());
251
252 buf[12..20].copy_from_slice(&self.stub_size.to_le_bytes());
254
255 buf[20..28].copy_from_slice(&self.assets_offset.to_le_bytes());
257 buf[28..36].copy_from_slice(&self.assets_size.to_le_bytes());
258
259 buf[36..44].copy_from_slice(&self.manifest_offset.to_le_bytes());
261 buf[44..52].copy_from_slice(&self.manifest_size.to_le_bytes());
262
263 buf[52..56].copy_from_slice(&self.checksum.to_le_bytes());
265
266 buf
269 }
270
271 pub fn from_bytes(buf: &[u8; FOOTER_SIZE]) -> Result<Self> {
273 if &buf[0..8] != MAGIC {
275 return Err(PackError::InvalidMagic);
276 }
277
278 let version = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
280 if version != FORMAT_VERSION {
281 return Err(PackError::UnsupportedVersion(version));
282 }
283
284 Ok(Self {
285 stub_size: u64::from_le_bytes([
286 buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19],
287 ]),
288 assets_offset: u64::from_le_bytes([
289 buf[20], buf[21], buf[22], buf[23], buf[24], buf[25], buf[26], buf[27],
290 ]),
291 assets_size: u64::from_le_bytes([
292 buf[28], buf[29], buf[30], buf[31], buf[32], buf[33], buf[34], buf[35],
293 ]),
294 manifest_offset: u64::from_le_bytes([
295 buf[36], buf[37], buf[38], buf[39], buf[40], buf[41], buf[42], buf[43],
296 ]),
297 manifest_size: u64::from_le_bytes([
298 buf[44], buf[45], buf[46], buf[47], buf[48], buf[49], buf[50], buf[51],
299 ]),
300 checksum: u32::from_le_bytes([buf[52], buf[53], buf[54], buf[55]]),
301 })
302 }
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
311#[serde(rename_all = "lowercase")]
312pub enum PackMode {
313 #[default]
315 Container,
316 Vm,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct PackManifest {
323 #[serde(default)]
325 pub mode: PackMode,
326
327 pub image: String,
329
330 pub digest: String,
332
333 pub platform: String,
335
336 #[serde(default, skip_serializing_if = "Vec::is_empty")]
338 pub entrypoint: Vec<String>,
339
340 #[serde(default, skip_serializing_if = "Vec::is_empty")]
342 pub cmd: Vec<String>,
343
344 #[serde(default, skip_serializing_if = "Vec::is_empty")]
346 pub env: Vec<String>,
347
348 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
353 pub secret_refs: std::collections::BTreeMap<String, smolvm_protocol::SecretRef>,
354
355 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub workdir: Option<String>,
358
359 pub cpus: u8,
361
362 pub mem: u32,
364
365 #[serde(default)]
368 pub image_size: u64,
369
370 #[serde(default)]
372 pub network: bool,
373
374 #[serde(default)]
376 pub gpu: bool,
377
378 #[serde(default)]
383 pub cuda: bool,
384
385 pub host_platform: String,
389
390 pub created: String,
392
393 pub smolvm_version: String,
395
396 pub assets: AssetInventory,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct AssetInventory {
403 pub libraries: Vec<AssetEntry>,
405
406 pub agent_rootfs: AssetEntry,
408
409 pub layers: Vec<LayerEntry>,
411
412 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub storage_template: Option<AssetEntry>,
416
417 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub overlay_template: Option<AssetEntry>,
421
422 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub overlay_logical_size: Option<u64>,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct AssetEntry {
434 pub path: String,
436
437 pub size: u64,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct LayerEntry {
444 pub digest: String,
446
447 pub path: String,
449
450 pub size: u64,
452}
453
454fn rfc3339_now() -> String {
456 let now = time::OffsetDateTime::now_utc();
457 now.format(&time::format_description::well_known::Rfc3339)
458 .expect("RFC 3339 formatting should never fail for a valid OffsetDateTime")
459}
460
461impl PackManifest {
462 pub fn new(image: String, digest: String, platform: String, host_platform: String) -> Self {
464 Self {
465 mode: PackMode::default(),
466 image,
467 digest,
468 platform,
469 entrypoint: Vec::new(),
470 cmd: Vec::new(),
471 env: Vec::new(),
472 secret_refs: std::collections::BTreeMap::new(),
473 workdir: None,
474 cpus: 1,
475 mem: 256,
476 image_size: 0,
477 network: false,
478 gpu: false,
479 cuda: false,
480 host_platform,
481 created: rfc3339_now(),
482 smolvm_version: env!("CARGO_PKG_VERSION").to_string(),
483 assets: AssetInventory {
484 libraries: Vec::new(),
485 agent_rootfs: AssetEntry {
486 path: "agent-rootfs.tar".to_string(),
487 size: 0,
488 },
489 layers: Vec::new(),
490 storage_template: None,
491 overlay_template: None,
492 overlay_logical_size: None,
493 },
494 }
495 }
496
497 pub fn to_json(&self) -> Result<Vec<u8>> {
499 Ok(serde_json::to_vec_pretty(self)?)
500 }
501
502 pub fn from_json(data: &[u8]) -> Result<Self> {
504 Ok(serde_json::from_slice(data)?)
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn test_manifest_secret_refs_roundtrip() {
514 let empty = PackManifest::new(
518 "alpine".to_string(),
519 "sha256:abc".to_string(),
520 "linux/arm64".to_string(),
521 "linux/arm64".to_string(),
522 );
523 let json = String::from_utf8(empty.to_json().unwrap()).unwrap();
524 assert!(
525 !json.contains("secret_refs"),
526 "empty secret_refs must not be serialized"
527 );
528
529 let mut m = empty;
530 m.secret_refs.insert(
531 "DB_PASSWORD".to_string(),
532 smolvm_protocol::SecretRef {
533 from_env: Some("PGPASSWORD".to_string()),
534 from_file: None,
535 },
536 );
537 let restored = PackManifest::from_json(&m.to_json().unwrap()).unwrap();
538 assert_eq!(restored.secret_refs.len(), 1);
539 assert_eq!(
540 restored.secret_refs["DB_PASSWORD"].from_env.as_deref(),
541 Some("PGPASSWORD")
542 );
543 }
544
545 #[test]
546 fn test_footer_roundtrip() {
547 let footer = PackFooter {
548 stub_size: 512 * 1024,
549 assets_offset: 512 * 1024,
550 assets_size: 50 * 1024 * 1024,
551 manifest_offset: 512 * 1024 + 50 * 1024 * 1024,
552 manifest_size: 2048,
553 checksum: 0xDEADBEEF,
554 };
555
556 let bytes = footer.to_bytes();
557 assert_eq!(bytes.len(), FOOTER_SIZE);
558
559 let restored = PackFooter::from_bytes(&bytes).unwrap();
560 assert_eq!(restored.stub_size, footer.stub_size);
561 assert_eq!(restored.assets_offset, footer.assets_offset);
562 assert_eq!(restored.assets_size, footer.assets_size);
563 assert_eq!(restored.manifest_offset, footer.manifest_offset);
564 assert_eq!(restored.manifest_size, footer.manifest_size);
565 assert_eq!(restored.checksum, footer.checksum);
566 }
567
568 #[test]
569 fn test_footer_invalid_magic() {
570 let mut bytes = [0u8; FOOTER_SIZE];
571 bytes[0..8].copy_from_slice(b"BADMAGIC");
572
573 let result = PackFooter::from_bytes(&bytes);
574 assert!(matches!(result, Err(PackError::InvalidMagic)));
575 }
576
577 #[test]
578 fn test_footer_unsupported_version() {
579 let mut bytes = [0u8; FOOTER_SIZE];
580 bytes[0..8].copy_from_slice(MAGIC);
581 bytes[8..12].copy_from_slice(&99u32.to_le_bytes()); let result = PackFooter::from_bytes(&bytes);
584 assert!(matches!(result, Err(PackError::UnsupportedVersion(99))));
585 }
586
587 #[test]
588 fn test_manifest_roundtrip() {
589 let mut manifest = PackManifest::new(
590 "alpine:latest".to_string(),
591 "sha256:abc123".to_string(),
592 "linux/arm64".to_string(),
593 "darwin/arm64".to_string(),
594 );
595 manifest.cpus = 2;
596 manifest.mem = 1024;
597 manifest.entrypoint = vec!["/bin/sh".to_string()];
598 manifest.env = vec!["PATH=/usr/local/bin:/usr/bin:/bin".to_string()];
599 manifest.assets.libraries.push(AssetEntry {
600 path: "lib/libkrun.dylib".to_string(),
601 size: 4 * 1024 * 1024,
602 });
603
604 let json = manifest.to_json().unwrap();
605 let restored = PackManifest::from_json(&json).unwrap();
606
607 assert_eq!(restored.image, "alpine:latest");
608 assert_eq!(restored.digest, "sha256:abc123");
609 assert_eq!(restored.cpus, 2);
610 assert_eq!(restored.mem, 1024);
611 assert_eq!(restored.entrypoint, vec!["/bin/sh"]);
612 assert_eq!(restored.assets.libraries.len(), 1);
613 }
614
615 #[test]
616 fn test_manifest_json_format() {
617 let manifest = PackManifest::new(
618 "ubuntu:22.04".to_string(),
619 "sha256:def456".to_string(),
620 "linux/amd64".to_string(),
621 "linux/amd64".to_string(),
622 );
623
624 let json = String::from_utf8(manifest.to_json().unwrap()).unwrap();
625 assert!(json.contains("\"image\": \"ubuntu:22.04\""));
626 assert!(json.contains("\"platform\": \"linux/amd64\""));
627 assert!(json.contains("\"host_platform\": \"linux/amd64\""));
629 assert!(json.contains("\"smolvm_version\""));
630 assert!(json.contains("\"created\""));
631 }
632
633 #[test]
634 fn test_pack_mode_default_is_container() {
635 assert_eq!(PackMode::default(), PackMode::Container);
636 }
637
638 #[test]
639 fn test_pack_mode_vm_roundtrip() {
640 let mut manifest = PackManifest::new(
641 "vm://myvm".to_string(),
642 "none".to_string(),
643 "linux/arm64".to_string(),
644 "darwin/arm64".to_string(),
645 );
646 manifest.mode = PackMode::Vm;
647 manifest.assets.overlay_template = Some(AssetEntry {
648 path: "overlay.raw".to_string(),
649 size: 2 * 1024 * 1024 * 1024,
650 });
651
652 let json = manifest.to_json().unwrap();
653 let restored = PackManifest::from_json(&json).unwrap();
654 assert_eq!(restored.mode, PackMode::Vm);
655 assert!(restored.assets.overlay_template.is_some());
656 assert_eq!(
657 restored.assets.overlay_template.unwrap().path,
658 "overlay.raw"
659 );
660 }
661}