1use std::fs::{self, File};
8use std::io::{Read, Seek, SeekFrom, Write};
9use std::path::{Path, PathBuf};
10
11use crate::assets::{crc32_file_range, AssetCollector};
12use crate::format::{PackFooter, PackManifest, FOOTER_SIZE, SIDECAR_EXTENSION};
13use crate::Result;
14
15const MAX_MANIFEST_SIZE: u64 = 16 * 1024 * 1024;
18
19pub struct Packer {
21 stub_path: Option<std::path::PathBuf>,
22 manifest: PackManifest,
23 asset_collector: Option<AssetCollector>,
24}
25
26#[cfg(target_os = "macos")]
28enum PackedEmbeddedError {
29 NotMachO(Box<Packer>),
31 PackError(crate::PackError),
33}
34
35impl Packer {
36 pub fn new(manifest: PackManifest) -> Self {
38 Self {
39 stub_path: None,
40 manifest,
41 asset_collector: None,
42 }
43 }
44
45 pub fn with_stub(mut self, stub_path: impl AsRef<Path>) -> Self {
47 self.stub_path = Some(stub_path.as_ref().to_path_buf());
48 self
49 }
50
51 pub fn with_assets(mut self, collector: AssetCollector) -> Self {
53 self.manifest.assets = collector.inventory().clone();
55 self.asset_collector = Some(collector);
56 self
57 }
58
59 pub fn with_asset_collector(mut self, collector: AssetCollector) -> Self {
64 self.asset_collector = Some(collector);
65 self
66 }
67
68 pub fn manifest_mut(&mut self) -> &mut PackManifest {
70 &mut self.manifest
71 }
72
73 pub fn pack(self, output: impl AsRef<Path>) -> Result<PackedInfo> {
82 let output = output.as_ref();
83 let temp_dir = tempfile::tempdir()?;
84
85 let stub_path = self
87 .stub_path
88 .as_ref()
89 .ok_or_else(|| crate::PackError::AssetNotFound("stub executable".to_string()))?;
90
91 let stub_data = fs::read(stub_path)?;
93 let stub_size = stub_data.len() as u64;
94 fs::write(output, &stub_data)?;
95
96 #[cfg(unix)]
98 {
99 use std::os::unix::fs::PermissionsExt;
100 let mut perms = fs::metadata(output)?.permissions();
101 perms.set_mode(0o755);
102 fs::set_permissions(output, perms)?;
103 }
104
105 let sidecar_path = sidecar_path_for(output);
107 let mut sidecar_file = File::create(&sidecar_path)?;
108
109 let assets_temp = temp_dir.path().join("assets.tar.zst");
111 let assets_size = if let Some(collector) = &self.asset_collector {
112 collector.compress(&assets_temp, true)?
113 } else {
114 let empty_file = File::create(&assets_temp)?;
115 let encoder = zstd::stream::Encoder::new(empty_file, 1)?;
116 let tar_builder = tar::Builder::new(encoder);
117 let encoder = tar_builder.into_inner()?;
118 encoder.finish()?;
119 fs::metadata(&assets_temp)?.len()
120 };
121
122 let mut assets_file = File::open(&assets_temp)?;
123 std::io::copy(&mut assets_file, &mut sidecar_file)?;
124
125 let manifest_offset = assets_size;
127 let manifest_json = self.manifest.to_json()?;
128 let manifest_size = manifest_json.len() as u64;
129 sidecar_file.write_all(&manifest_json)?;
130
131 sidecar_file.flush()?;
133 drop(sidecar_file);
134 let checksum_size = assets_size + manifest_size;
135 let checksum = crc32_file_range(&sidecar_path, 0, checksum_size)?;
136
137 let footer = PackFooter {
139 stub_size: 0, assets_offset: 0, assets_size,
142 manifest_offset,
143 manifest_size,
144 checksum,
145 };
146
147 let mut sidecar_file = fs::OpenOptions::new().append(true).open(&sidecar_path)?;
148 sidecar_file.write_all(&footer.to_bytes())?;
149
150 let sidecar_total = assets_size + manifest_size + FOOTER_SIZE as u64;
151 let total_size = stub_size + sidecar_total;
152
153 Ok(PackedInfo {
154 stub_size,
155 assets_size,
156 manifest_size,
157 total_size,
158 checksum,
159 sidecar_path: Some(sidecar_path),
160 })
161 }
162
163 pub fn pack_embedded(self, output: impl AsRef<Path>) -> Result<PackedInfo> {
171 #[cfg(target_os = "macos")]
172 {
173 match self.try_pack_embedded_macho(output.as_ref()) {
175 Ok(info) => Ok(info),
176 Err(PackedEmbeddedError::NotMachO(packer)) => {
177 packer.pack_embedded_append(output)
179 }
180 Err(PackedEmbeddedError::PackError(e)) => Err(e),
181 }
182 }
183 #[cfg(not(target_os = "macos"))]
184 {
185 self.pack_embedded_append(output)
186 }
187 }
188
189 #[cfg(target_os = "macos")]
194 fn try_pack_embedded_macho(
195 self,
196 output: &Path,
197 ) -> std::result::Result<PackedInfo, PackedEmbeddedError> {
198 use crate::macho::MachoFile;
199
200 let stub_path = match self.stub_path.as_ref() {
202 Some(p) => p,
203 None => {
204 return Err(PackedEmbeddedError::PackError(
205 crate::PackError::AssetNotFound("stub executable".to_string()),
206 ))
207 }
208 };
209
210 let stub_data = match fs::read(stub_path) {
212 Ok(data) => data,
213 Err(e) => return Err(PackedEmbeddedError::PackError(e.into())),
214 };
215
216 let macho = match MachoFile::parse(&stub_data) {
218 Ok(m) => m,
219 Err(_) => return Err(PackedEmbeddedError::NotMachO(Box::new(self))), };
221
222 if macho.find_section("__SMOLVM", "__smolvm").is_none() {
224 return Err(PackedEmbeddedError::NotMachO(Box::new(self))); }
226
227 match self.pack_embedded_macho_inner(output, stub_data, macho) {
229 Ok(info) => Ok(info),
230 Err(e) => Err(PackedEmbeddedError::PackError(e)),
231 }
232 }
233
234 #[cfg(target_os = "macos")]
239 fn pack_embedded_macho_inner(
240 self,
241 output: &Path,
242 stub_data: Vec<u8>,
243 mut macho: crate::macho::MachoFile,
244 ) -> Result<PackedInfo> {
245 use crate::format::{SectionHeader, SECTION_HEADER_SIZE};
246
247 let stub_size = stub_data.len() as u64;
248 let temp_dir = tempfile::tempdir()?;
249
250 let assets_temp = temp_dir.path().join("assets.tar.zst");
252 let assets_size = if let Some(collector) = &self.asset_collector {
253 collector.compress(&assets_temp, false)? } else {
255 let empty_file = File::create(&assets_temp)?;
256 let encoder = zstd::stream::Encoder::new(empty_file, 1)?;
257 let tar_builder = tar::Builder::new(encoder);
258 let encoder = tar_builder.into_inner()?;
259 encoder.finish()?;
260 fs::metadata(&assets_temp)?.len()
261 };
262
263 let manifest_json = self.manifest.to_json()?;
265 let manifest_size = manifest_json.len() as u32;
266
267 let mut hasher = crc32fast::Hasher::new();
269 hasher.update(&manifest_json);
270 let assets_data = fs::read(&assets_temp)?;
271 hasher.update(&assets_data);
272 let checksum = hasher.finalize();
273
274 let header = SectionHeader {
276 manifest_size,
277 assets_size,
278 checksum,
279 };
280
281 let mut section_data =
282 Vec::with_capacity(SECTION_HEADER_SIZE + manifest_json.len() + assets_data.len());
283 section_data.extend_from_slice(&header.to_bytes());
284 section_data.extend_from_slice(&manifest_json);
285 section_data.extend_from_slice(&assets_data);
286
287 macho
289 .write_section("__SMOLVM", "__smolvm", §ion_data)
290 .map_err(|e| crate::PackError::Signing(format!("failed to write section: {}", e)))?;
291
292 macho
294 .sign_adhoc()
295 .map_err(|e| crate::PackError::Signing(format!("failed to sign: {}", e)))?;
296
297 let output_data = macho.write();
299 let total_size = output_data.len() as u64;
300 fs::write(output, &output_data)?;
301
302 #[cfg(unix)]
304 {
305 use std::os::unix::fs::PermissionsExt;
306 let mut perms = fs::metadata(output)?.permissions();
307 perms.set_mode(0o755);
308 fs::set_permissions(output, perms)?;
309 }
310
311 Ok(PackedInfo {
312 stub_size,
313 assets_size,
314 manifest_size: manifest_size as u64,
315 total_size,
316 checksum,
317 sidecar_path: None,
318 })
319 }
320
321 fn pack_embedded_append(self, output: impl AsRef<Path>) -> Result<PackedInfo> {
326 let output = output.as_ref();
327 let temp_dir = tempfile::tempdir()?;
328
329 let stub_path = self
331 .stub_path
332 .as_ref()
333 .ok_or_else(|| crate::PackError::AssetNotFound("stub executable".to_string()))?;
334
335 let stub_data = fs::read(stub_path)?;
337 let stub_size = stub_data.len() as u64;
338 let mut output_file = File::create(output)?;
339 output_file.write_all(&stub_data)?;
340
341 let assets_temp = temp_dir.path().join("assets.tar.zst");
343 let assets_size = if let Some(collector) = &self.asset_collector {
344 collector.compress(&assets_temp, false)? } else {
346 let empty_file = File::create(&assets_temp)?;
347 let encoder = zstd::stream::Encoder::new(empty_file, 1)?;
348 let tar_builder = tar::Builder::new(encoder);
349 let encoder = tar_builder.into_inner()?;
350 encoder.finish()?;
351 fs::metadata(&assets_temp)?.len()
352 };
353
354 let assets_offset = stub_size; let mut assets_file = File::open(&assets_temp)?;
356 std::io::copy(&mut assets_file, &mut output_file)?;
357
358 let manifest_offset = stub_size + assets_size;
360 let manifest_json = self.manifest.to_json()?;
361 let manifest_size = manifest_json.len() as u64;
362 output_file.write_all(&manifest_json)?;
363
364 output_file.flush()?;
366 drop(output_file);
367 let checksum_size = assets_size + manifest_size;
368 let checksum = crc32_file_range(output, assets_offset, checksum_size)?;
369
370 let footer = PackFooter {
372 stub_size,
373 assets_offset, assets_size,
375 manifest_offset,
376 manifest_size,
377 checksum,
378 };
379
380 let mut output_file = fs::OpenOptions::new().append(true).open(output)?;
381 output_file.write_all(&footer.to_bytes())?;
382
383 #[cfg(unix)]
385 {
386 use std::os::unix::fs::PermissionsExt;
387 let mut perms = fs::metadata(output)?.permissions();
388 perms.set_mode(0o755);
389 fs::set_permissions(output, perms)?;
390 }
391
392 let total_size = stub_size + assets_size + manifest_size + FOOTER_SIZE as u64;
393
394 Ok(PackedInfo {
395 stub_size,
396 assets_size,
397 manifest_size,
398 total_size,
399 checksum,
400 sidecar_path: None, })
402 }
403}
404
405pub fn embed_libs_in_binary(binary_path: &Path, staging_dir: &Path) -> Result<()> {
410 use crate::format::LibsFooter;
411
412 let lib_dir = staging_dir.join("lib");
413 if !lib_dir.exists() {
414 return Ok(()); }
416
417 let has_libs = fs::read_dir(&lib_dir)?
419 .filter_map(|e| e.ok())
420 .any(|e| e.path().is_file());
421 if !has_libs {
422 return Ok(());
423 }
424
425 let temp_dir = tempfile::tempdir()?;
426 let libs_temp = temp_dir.path().join("libs.tar.zst");
427
428 let output_file = File::create(&libs_temp)?;
430 let encoder = zstd::stream::Encoder::new(output_file, crate::assets::ZSTD_LEVEL)
431 .map_err(|e| crate::PackError::Compression(e.to_string()))?;
432 let mut tar_builder = tar::Builder::new(encoder);
433 tar_builder
434 .append_dir_all("lib", &lib_dir)
435 .map_err(|e| crate::PackError::Tar(e.to_string()))?;
436 let encoder = tar_builder
437 .into_inner()
438 .map_err(|e| crate::PackError::Tar(e.to_string()))?;
439 encoder
440 .finish()
441 .map_err(|e| crate::PackError::Compression(e.to_string()))?;
442
443 let libs_size = fs::metadata(&libs_temp)?.len();
444 let binary_size = fs::metadata(binary_path)?.len();
445
446 let mut output_file = fs::OpenOptions::new().append(true).open(binary_path)?;
448 let mut libs_file = File::open(&libs_temp)?;
449 std::io::copy(&mut libs_file, &mut output_file)?;
450
451 let libs_footer = LibsFooter {
452 libs_offset: binary_size,
453 libs_size,
454 };
455 output_file.write_all(&libs_footer.to_bytes())?;
456
457 Ok(())
458}
459
460pub fn sidecar_path_for(binary_path: impl AsRef<Path>) -> PathBuf {
462 let mut path = binary_path.as_ref().to_path_buf();
463 let filename = path
464 .file_name()
465 .map(|s| s.to_string_lossy().to_string())
466 .unwrap_or_default();
467 path.set_file_name(format!("{}{}", filename, SIDECAR_EXTENSION));
468 path
469}
470
471#[derive(Debug, Clone)]
473pub struct PackedInfo {
474 pub stub_size: u64,
476 pub assets_size: u64,
478 pub manifest_size: u64,
480 pub total_size: u64,
482 pub checksum: u32,
484 pub sidecar_path: Option<PathBuf>,
486}
487
488pub fn read_footer_from_sidecar(sidecar_path: impl AsRef<Path>) -> Result<PackFooter> {
493 let mut file = File::open(sidecar_path.as_ref())?;
494 let file_size = file.metadata()?.len();
495
496 if file_size < FOOTER_SIZE as u64 {
497 return Err(crate::PackError::Io(std::io::Error::new(
498 std::io::ErrorKind::InvalidData,
499 "sidecar file too small to contain footer",
500 )));
501 }
502
503 file.seek(SeekFrom::End(-(FOOTER_SIZE as i64)))?;
504 let mut footer_bytes = [0u8; FOOTER_SIZE];
505 file.read_exact(&mut footer_bytes)?;
506
507 let footer = PackFooter::from_bytes(&footer_bytes)?;
508
509 validate_footer_bounds(&footer, file_size)?;
511
512 Ok(footer)
513}
514
515pub fn read_manifest_from_sidecar(sidecar_path: impl AsRef<Path>) -> Result<PackManifest> {
517 let footer = read_footer_from_sidecar(sidecar_path.as_ref())?;
518
519 let mut file = File::open(sidecar_path.as_ref())?;
520 file.seek(SeekFrom::Start(footer.manifest_offset))?;
521
522 let mut manifest_bytes = vec![0u8; footer.manifest_size as usize];
523 file.read_exact(&mut manifest_bytes)?;
524
525 PackManifest::from_json(&manifest_bytes)
526}
527
528pub fn read_footer(path: impl AsRef<Path>) -> Result<PackFooter> {
530 let sidecar = sidecar_path_for(path.as_ref());
532 if sidecar.exists() {
533 return read_footer_from_sidecar(&sidecar);
534 }
535
536 let mut file = File::open(path.as_ref())?;
538 let file_size = file.metadata()?.len();
539
540 if file_size < FOOTER_SIZE as u64 {
541 return Err(crate::PackError::Io(std::io::Error::new(
542 std::io::ErrorKind::InvalidData,
543 "file too small to contain footer",
544 )));
545 }
546
547 file.seek(SeekFrom::End(-(FOOTER_SIZE as i64)))?;
548 let mut footer_bytes = [0u8; FOOTER_SIZE];
549 file.read_exact(&mut footer_bytes)?;
550
551 PackFooter::from_bytes(&footer_bytes)
552}
553
554pub fn read_manifest(path: impl AsRef<Path>) -> Result<PackManifest> {
556 let sidecar = sidecar_path_for(path.as_ref());
558 if sidecar.exists() {
559 return read_manifest_from_sidecar(&sidecar);
560 }
561
562 let footer = read_footer(path.as_ref())?;
564
565 let mut file = File::open(path.as_ref())?;
566 file.seek(SeekFrom::Start(footer.manifest_offset))?;
567
568 let mut manifest_bytes = vec![0u8; footer.manifest_size as usize];
569 file.read_exact(&mut manifest_bytes)?;
570
571 PackManifest::from_json(&manifest_bytes)
572}
573
574pub fn is_sidecar_mode(footer: &PackFooter) -> bool {
576 footer.assets_offset == 0
577}
578
579pub fn verify_checksum(path: impl AsRef<Path>) -> Result<bool> {
581 let footer = read_footer(path.as_ref())?;
582
583 if is_sidecar_mode(&footer) {
584 let sidecar = sidecar_path_for(path.as_ref());
586 if !sidecar.exists() {
587 return Ok(false);
588 }
589 let checksum_size = footer.assets_size + footer.manifest_size;
590 let actual = crc32_file_range(&sidecar, 0, checksum_size)?;
591 Ok(actual == footer.checksum)
592 } else {
593 let checksum_size = footer.assets_size + footer.manifest_size;
595 let actual = crc32_file_range(path.as_ref(), footer.assets_offset, checksum_size)?;
596 Ok(actual == footer.checksum)
597 }
598}
599
600pub fn verify_sidecar_checksum(
605 sidecar_path: impl AsRef<Path>,
606 footer: &PackFooter,
607) -> Result<bool> {
608 let checksum_size = footer
609 .assets_size
610 .checked_add(footer.manifest_size)
611 .ok_or_else(|| {
612 crate::PackError::Io(std::io::Error::new(
613 std::io::ErrorKind::InvalidData,
614 "assets_size + manifest_size overflow",
615 ))
616 })?;
617 let actual = crc32_file_range(sidecar_path.as_ref(), 0, checksum_size)?;
618 Ok(actual == footer.checksum)
619}
620
621fn validate_footer_bounds(footer: &PackFooter, file_size: u64) -> Result<()> {
623 if footer.manifest_size > MAX_MANIFEST_SIZE {
625 return Err(crate::PackError::Io(std::io::Error::new(
626 std::io::ErrorKind::InvalidData,
627 format!(
628 "manifest size ({} bytes) exceeds maximum ({} bytes)",
629 footer.manifest_size, MAX_MANIFEST_SIZE
630 ),
631 )));
632 }
633
634 let content_end = footer
636 .assets_size
637 .checked_add(footer.manifest_size)
638 .and_then(|s| s.checked_add(FOOTER_SIZE as u64));
639
640 match content_end {
641 Some(end) if end <= file_size => {}
642 _ => {
643 return Err(crate::PackError::Io(std::io::Error::new(
644 std::io::ErrorKind::InvalidData,
645 "footer sizes exceed file size (corrupt or truncated sidecar)",
646 )));
647 }
648 }
649
650 if footer.assets_offset == 0 && footer.manifest_offset != footer.assets_size {
657 return Err(crate::PackError::Io(std::io::Error::new(
658 std::io::ErrorKind::InvalidData,
659 format!(
660 "sidecar manifest_offset ({}) does not match assets_size ({}): corrupt footer",
661 footer.manifest_offset, footer.assets_size
662 ),
663 )));
664 }
665
666 let manifest_end = footer
668 .manifest_offset
669 .checked_add(footer.manifest_size)
670 .and_then(|end| end.checked_add(FOOTER_SIZE as u64));
671
672 match manifest_end {
673 Some(end) if end <= file_size => {}
674 _ => {
675 return Err(crate::PackError::Io(std::io::Error::new(
676 std::io::ErrorKind::InvalidData,
677 "manifest offset/size exceeds file bounds",
678 )));
679 }
680 }
681
682 Ok(())
683}
684
685pub fn extract_assets(packed_path: impl AsRef<Path>, output_dir: impl AsRef<Path>) -> Result<()> {
687 let footer = read_footer(packed_path.as_ref())?;
688
689 if is_sidecar_mode(&footer) {
690 let sidecar = sidecar_path_for(packed_path.as_ref());
692 crate::assets::decompress_assets_from_file(&sidecar, output_dir.as_ref())?;
693 } else {
694 let mut file = File::open(packed_path.as_ref())?;
696 file.seek(SeekFrom::Start(footer.assets_offset))?;
697
698 let mut compressed = vec![0u8; footer.assets_size as usize];
700 file.read_exact(&mut compressed)?;
701
702 crate::assets::decompress_assets(&compressed, output_dir.as_ref())?;
704 }
705
706 Ok(())
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use std::io::Write;
713
714 #[test]
715 fn test_pack_and_read() {
716 let temp_dir = tempfile::tempdir().unwrap();
717
718 let stub_path = temp_dir.path().join("stub");
720 let mut stub_file = File::create(&stub_path).unwrap();
721 stub_file.write_all(b"#!/bin/sh\necho stub").unwrap();
722
723 let manifest = PackManifest::new(
725 "alpine:latest".to_string(),
726 "sha256:abc123".to_string(),
727 "linux/arm64".to_string(),
728 "darwin/arm64".to_string(),
729 );
730
731 let output_path = temp_dir.path().join("packed");
733 let packer = Packer::new(manifest).with_stub(&stub_path);
734 let info = packer.pack(&output_path).unwrap();
735
736 assert!(info.stub_size > 0);
737 assert!(info.total_size > info.stub_size);
738
739 let sidecar = sidecar_path_for(&output_path);
741 assert!(sidecar.exists());
742
743 let footer = read_footer(&output_path).unwrap();
745 assert_eq!(footer.stub_size, 0); assert_eq!(footer.assets_offset, 0); let manifest = read_manifest(&output_path).unwrap();
749 assert_eq!(manifest.image, "alpine:latest");
750
751 assert!(verify_checksum(&output_path).unwrap());
753 }
754
755 #[test]
756 fn test_pack_with_assets() {
757 let temp_dir = tempfile::tempdir().unwrap();
758
759 let stub_path = temp_dir.path().join("stub");
761 fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
762
763 let staging = temp_dir.path().join("staging");
765 let mut collector = AssetCollector::new(staging).unwrap();
766
767 collector
769 .add_layer("sha256:abc123def456", b"layer content")
770 .unwrap();
771
772 let manifest = PackManifest::new(
774 "test:latest".to_string(),
775 "sha256:test".to_string(),
776 "linux/arm64".to_string(),
777 "darwin/arm64".to_string(),
778 );
779
780 let output_path = temp_dir.path().join("packed");
782 let packer = Packer::new(manifest)
783 .with_stub(&stub_path)
784 .with_assets(collector);
785 packer.pack(&output_path).unwrap();
786
787 let manifest = read_manifest(&output_path).unwrap();
789 assert_eq!(manifest.assets.layers.len(), 1);
790 assert_eq!(manifest.assets.layers[0].digest, "sha256:abc123def456");
791
792 let extract_dir = temp_dir.path().join("extracted");
794 extract_assets(&output_path, &extract_dir).unwrap();
795
796 let layer_file = extract_dir.join("layers/abc123def456.tar");
797 assert!(layer_file.exists());
798 assert_eq!(fs::read_to_string(&layer_file).unwrap(), "layer content");
799 }
800
801 #[test]
802 fn test_pack_embedded() {
803 let temp_dir = tempfile::tempdir().unwrap();
804
805 let stub_path = temp_dir.path().join("stub");
807 fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
808
809 let manifest = PackManifest::new(
811 "alpine:latest".to_string(),
812 "sha256:abc123".to_string(),
813 "linux/arm64".to_string(),
814 "darwin/arm64".to_string(),
815 );
816
817 let output_path = temp_dir.path().join("packed-single");
819 let packer = Packer::new(manifest).with_stub(&stub_path);
820 let info = packer.pack_embedded(&output_path).unwrap();
821
822 assert!(info.sidecar_path.is_none());
824 assert!(info.stub_size > 0);
825 assert!(info.total_size > info.stub_size);
826
827 let sidecar = sidecar_path_for(&output_path);
829 assert!(!sidecar.exists());
830
831 let output_size = fs::metadata(&output_path).unwrap().len();
833 assert_eq!(output_size, info.total_size);
834
835 let footer = read_footer(&output_path).unwrap();
837 assert!(footer.assets_offset > 0); assert_eq!(footer.stub_size, info.stub_size);
839
840 let manifest = read_manifest(&output_path).unwrap();
841 assert_eq!(manifest.image, "alpine:latest");
842
843 assert!(verify_checksum(&output_path).unwrap());
845 }
846
847 #[test]
848 fn test_pack_embedded_with_assets() {
849 let temp_dir = tempfile::tempdir().unwrap();
850
851 let stub_path = temp_dir.path().join("stub");
853 fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
854
855 let staging = temp_dir.path().join("staging");
857 let mut collector = AssetCollector::new(staging).unwrap();
858
859 collector
863 .add_layer("sha256:abcdef123456", b"embedded layer content")
864 .unwrap();
865
866 let manifest = PackManifest::new(
868 "test:embedded".to_string(),
869 "sha256:test".to_string(),
870 "linux/arm64".to_string(),
871 "darwin/arm64".to_string(),
872 );
873
874 let output_path = temp_dir.path().join("packed-embedded");
876 let packer = Packer::new(manifest)
877 .with_stub(&stub_path)
878 .with_assets(collector);
879 packer.pack_embedded(&output_path).unwrap();
880
881 let manifest = read_manifest(&output_path).unwrap();
883 assert_eq!(manifest.assets.layers.len(), 1);
884 assert_eq!(manifest.assets.layers[0].digest, "sha256:abcdef123456");
885
886 let footer = read_footer(&output_path).unwrap();
888 assert!(footer.assets_offset > 0);
889 assert!(!is_sidecar_mode(&footer));
890
891 let extract_dir = temp_dir.path().join("extracted");
893 extract_assets(&output_path, &extract_dir).unwrap();
894
895 let layer_file = extract_dir.join("layers/abcdef123456.tar"); assert!(layer_file.exists());
897 assert_eq!(
898 fs::read_to_string(&layer_file).unwrap(),
899 "embedded layer content"
900 );
901 }
902
903 #[test]
904 fn test_sidecar_checksum_verification() {
905 let temp_dir = tempfile::tempdir().unwrap();
906
907 let stub_path = temp_dir.path().join("stub");
909 fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
910
911 let manifest = PackManifest::new(
912 "alpine:latest".to_string(),
913 "sha256:abc123".to_string(),
914 "linux/arm64".to_string(),
915 "darwin/arm64".to_string(),
916 );
917
918 let output_path = temp_dir.path().join("packed");
920 let packer = Packer::new(manifest).with_stub(&stub_path);
921 packer.pack(&output_path).unwrap();
922
923 let sidecar = sidecar_path_for(&output_path);
924 let footer = read_footer_from_sidecar(&sidecar).unwrap();
925
926 assert!(verify_sidecar_checksum(&sidecar, &footer).unwrap());
928
929 let mut data = fs::read(&sidecar).unwrap();
931 if !data.is_empty() {
932 data[0] ^= 0xFF;
933 }
934 fs::write(&sidecar, &data).unwrap();
935
936 assert!(!verify_sidecar_checksum(&sidecar, &footer).unwrap());
938 }
939
940 #[test]
941 fn test_footer_bounds_reject_oversized_manifest() {
942 let temp_dir = tempfile::tempdir().unwrap();
943 let sidecar = temp_dir.path().join("bad.smolmachine");
944
945 let footer = PackFooter {
947 stub_size: 0,
948 assets_offset: 0,
949 assets_size: 100,
950 manifest_offset: 100,
951 manifest_size: 32 * 1024 * 1024, checksum: 0,
953 };
954
955 let footer_bytes = footer.to_bytes();
957 let mut data = vec![0u8; 200]; data.extend_from_slice(&footer_bytes);
959 fs::write(&sidecar, &data).unwrap();
960
961 let result = read_footer_from_sidecar(&sidecar);
962 assert!(result.is_err());
963 let err_msg = result.unwrap_err().to_string();
964 assert!(err_msg.contains("exceeds maximum"), "got: {}", err_msg);
965 }
966
967 #[test]
968 fn test_footer_bounds_reject_manifest_offset_mismatch() {
969 let temp_dir = tempfile::tempdir().unwrap();
970 let sidecar = temp_dir.path().join("mismatch.smolmachine");
971
972 let footer = PackFooter {
975 stub_size: 0,
976 assets_offset: 0, assets_size: 100,
978 manifest_offset: 50, manifest_size: 50,
980 checksum: 0,
981 };
982
983 let footer_bytes = footer.to_bytes();
984 let mut data = vec![0u8; 150];
986 data.extend_from_slice(&footer_bytes);
987 fs::write(&sidecar, &data).unwrap();
988
989 let result = read_footer_from_sidecar(&sidecar);
990 assert!(result.is_err());
991 let err_msg = result.unwrap_err().to_string();
992 assert!(
993 err_msg.contains("manifest_offset") && err_msg.contains("does not match"),
994 "got: {}",
995 err_msg
996 );
997 }
998
999 #[test]
1000 fn test_sidecar_vs_embedded_mode_detection() {
1001 let temp_dir = tempfile::tempdir().unwrap();
1002
1003 let stub_path = temp_dir.path().join("stub");
1005 fs::write(&stub_path, b"#!/bin/sh\necho stub").unwrap();
1006
1007 let manifest = PackManifest::new(
1008 "test:latest".to_string(),
1009 "sha256:test".to_string(),
1010 "linux/arm64".to_string(),
1011 "darwin/arm64".to_string(),
1012 );
1013
1014 let sidecar_output = temp_dir.path().join("sidecar-mode");
1016 let packer = Packer::new(manifest.clone()).with_stub(&stub_path);
1017 packer.pack(&sidecar_output).unwrap();
1018
1019 let embedded_output = temp_dir.path().join("embedded-mode");
1021 let packer = Packer::new(manifest).with_stub(&stub_path);
1022 packer.pack_embedded(&embedded_output).unwrap();
1023
1024 let sidecar_footer = read_footer(&sidecar_output).unwrap();
1026 let embedded_footer = read_footer(&embedded_output).unwrap();
1027
1028 assert!(is_sidecar_mode(&sidecar_footer));
1029 assert!(!is_sidecar_mode(&embedded_footer));
1030
1031 assert!(verify_checksum(&sidecar_output).unwrap());
1033 assert!(verify_checksum(&embedded_output).unwrap());
1034 }
1035}