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 = "Option::is_none")]
350 pub workdir: Option<String>,
351
352 pub cpus: u8,
354
355 pub mem: u32,
357
358 #[serde(default)]
361 pub image_size: u64,
362
363 #[serde(default)]
365 pub network: bool,
366
367 #[serde(default)]
369 pub gpu: bool,
370
371 pub host_platform: String,
375
376 pub created: String,
378
379 pub smolvm_version: String,
381
382 pub assets: AssetInventory,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct AssetInventory {
389 pub libraries: Vec<AssetEntry>,
391
392 pub agent_rootfs: AssetEntry,
394
395 pub layers: Vec<LayerEntry>,
397
398 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub storage_template: Option<AssetEntry>,
402
403 #[serde(default, skip_serializing_if = "Option::is_none")]
406 pub overlay_template: Option<AssetEntry>,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct AssetEntry {
412 pub path: String,
414
415 pub size: u64,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct LayerEntry {
422 pub digest: String,
424
425 pub path: String,
427
428 pub size: u64,
430}
431
432fn rfc3339_now() -> String {
434 let now = time::OffsetDateTime::now_utc();
435 now.format(&time::format_description::well_known::Rfc3339)
436 .expect("RFC 3339 formatting should never fail for a valid OffsetDateTime")
437}
438
439impl PackManifest {
440 pub fn new(image: String, digest: String, platform: String, host_platform: String) -> Self {
442 Self {
443 mode: PackMode::default(),
444 image,
445 digest,
446 platform,
447 entrypoint: Vec::new(),
448 cmd: Vec::new(),
449 env: Vec::new(),
450 workdir: None,
451 cpus: 1,
452 mem: 256,
453 image_size: 0,
454 network: false,
455 gpu: false,
456 host_platform,
457 created: rfc3339_now(),
458 smolvm_version: env!("CARGO_PKG_VERSION").to_string(),
459 assets: AssetInventory {
460 libraries: Vec::new(),
461 agent_rootfs: AssetEntry {
462 path: "agent-rootfs.tar".to_string(),
463 size: 0,
464 },
465 layers: Vec::new(),
466 storage_template: None,
467 overlay_template: None,
468 },
469 }
470 }
471
472 pub fn to_json(&self) -> Result<Vec<u8>> {
474 Ok(serde_json::to_vec_pretty(self)?)
475 }
476
477 pub fn from_json(data: &[u8]) -> Result<Self> {
479 Ok(serde_json::from_slice(data)?)
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[test]
488 fn test_footer_roundtrip() {
489 let footer = PackFooter {
490 stub_size: 512 * 1024,
491 assets_offset: 512 * 1024,
492 assets_size: 50 * 1024 * 1024,
493 manifest_offset: 512 * 1024 + 50 * 1024 * 1024,
494 manifest_size: 2048,
495 checksum: 0xDEADBEEF,
496 };
497
498 let bytes = footer.to_bytes();
499 assert_eq!(bytes.len(), FOOTER_SIZE);
500
501 let restored = PackFooter::from_bytes(&bytes).unwrap();
502 assert_eq!(restored.stub_size, footer.stub_size);
503 assert_eq!(restored.assets_offset, footer.assets_offset);
504 assert_eq!(restored.assets_size, footer.assets_size);
505 assert_eq!(restored.manifest_offset, footer.manifest_offset);
506 assert_eq!(restored.manifest_size, footer.manifest_size);
507 assert_eq!(restored.checksum, footer.checksum);
508 }
509
510 #[test]
511 fn test_footer_invalid_magic() {
512 let mut bytes = [0u8; FOOTER_SIZE];
513 bytes[0..8].copy_from_slice(b"BADMAGIC");
514
515 let result = PackFooter::from_bytes(&bytes);
516 assert!(matches!(result, Err(PackError::InvalidMagic)));
517 }
518
519 #[test]
520 fn test_footer_unsupported_version() {
521 let mut bytes = [0u8; FOOTER_SIZE];
522 bytes[0..8].copy_from_slice(MAGIC);
523 bytes[8..12].copy_from_slice(&99u32.to_le_bytes()); let result = PackFooter::from_bytes(&bytes);
526 assert!(matches!(result, Err(PackError::UnsupportedVersion(99))));
527 }
528
529 #[test]
530 fn test_manifest_roundtrip() {
531 let mut manifest = PackManifest::new(
532 "alpine:latest".to_string(),
533 "sha256:abc123".to_string(),
534 "linux/arm64".to_string(),
535 "darwin/arm64".to_string(),
536 );
537 manifest.cpus = 2;
538 manifest.mem = 1024;
539 manifest.entrypoint = vec!["/bin/sh".to_string()];
540 manifest.env = vec!["PATH=/usr/local/bin:/usr/bin:/bin".to_string()];
541 manifest.assets.libraries.push(AssetEntry {
542 path: "lib/libkrun.dylib".to_string(),
543 size: 4 * 1024 * 1024,
544 });
545
546 let json = manifest.to_json().unwrap();
547 let restored = PackManifest::from_json(&json).unwrap();
548
549 assert_eq!(restored.image, "alpine:latest");
550 assert_eq!(restored.digest, "sha256:abc123");
551 assert_eq!(restored.cpus, 2);
552 assert_eq!(restored.mem, 1024);
553 assert_eq!(restored.entrypoint, vec!["/bin/sh"]);
554 assert_eq!(restored.assets.libraries.len(), 1);
555 }
556
557 #[test]
558 fn test_manifest_json_format() {
559 let manifest = PackManifest::new(
560 "ubuntu:22.04".to_string(),
561 "sha256:def456".to_string(),
562 "linux/amd64".to_string(),
563 "linux/amd64".to_string(),
564 );
565
566 let json = String::from_utf8(manifest.to_json().unwrap()).unwrap();
567 assert!(json.contains("\"image\": \"ubuntu:22.04\""));
568 assert!(json.contains("\"platform\": \"linux/amd64\""));
569 assert!(json.contains("\"host_platform\": \"linux/amd64\""));
571 assert!(json.contains("\"smolvm_version\""));
572 assert!(json.contains("\"created\""));
573 }
574
575 #[test]
576 fn test_pack_mode_default_is_container() {
577 assert_eq!(PackMode::default(), PackMode::Container);
578 }
579
580 #[test]
581 fn test_pack_mode_vm_roundtrip() {
582 let mut manifest = PackManifest::new(
583 "vm://myvm".to_string(),
584 "none".to_string(),
585 "linux/arm64".to_string(),
586 "darwin/arm64".to_string(),
587 );
588 manifest.mode = PackMode::Vm;
589 manifest.assets.overlay_template = Some(AssetEntry {
590 path: "overlay.raw".to_string(),
591 size: 2 * 1024 * 1024 * 1024,
592 });
593
594 let json = manifest.to_json().unwrap();
595 let restored = PackManifest::from_json(&json).unwrap();
596 assert_eq!(restored.mode, PackMode::Vm);
597 assert!(restored.assets.overlay_template.is_some());
598 assert_eq!(
599 restored.assets.overlay_template.unwrap().path,
600 "overlay.raw"
601 );
602 }
603}