1use std::collections::HashMap;
36use std::io::{self, Read, Seek, SeekFrom, Write};
37use std::path::Path;
38
39pub const MAGIC: &[u8; 8] = b"SEMAEXEC";
41
42pub const TRAILER_SIZE: usize = 16;
44
45pub const FORMAT_VERSION: u16 = 1;
47
48#[derive(Debug, Clone)]
54#[allow(dead_code)]
55pub struct Archive {
56 pub format_version: u16,
57 pub flags: u16,
58 pub metadata: HashMap<String, Vec<u8>>,
59 pub files: HashMap<String, Vec<u8>>,
60}
61
62impl Default for Archive {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl Archive {
69 #[allow(dead_code)]
71 pub fn new() -> Self {
72 Self {
73 format_version: FORMAT_VERSION,
74 flags: 0,
75 metadata: HashMap::new(),
76 files: HashMap::new(),
77 }
78 }
79}
80
81pub fn has_embedded_archive(path: &Path) -> io::Result<bool> {
88 let mut file = std::fs::File::open(path)?;
89 let file_len = file.metadata()?.len();
90 if file_len < TRAILER_SIZE as u64 {
91 return Ok(false);
92 }
93 file.seek(SeekFrom::End(-(TRAILER_SIZE as i64)))?;
94 let mut trailer = [0u8; TRAILER_SIZE];
95 file.read_exact(&mut trailer)?;
96 let magic = &trailer[8..16];
97 Ok(magic == MAGIC)
98}
99
100#[allow(dead_code)]
107pub fn extract_archive(path: &Path) -> io::Result<Archive> {
108 let data = std::fs::read(path)?;
109 let len = data.len();
110
111 if len < TRAILER_SIZE {
112 return Err(io::Error::new(
113 io::ErrorKind::InvalidData,
114 "file too small to contain archive trailer",
115 ));
116 }
117
118 let trailer = &data[len - TRAILER_SIZE..];
120 let magic = &trailer[8..16];
121 if magic != MAGIC {
122 return Err(io::Error::new(
123 io::ErrorKind::InvalidData,
124 "SEMAEXEC magic not found in trailer",
125 ));
126 }
127
128 let archive_size = u64::from_le_bytes(trailer[0..8].try_into().unwrap()) as usize;
129 let archive_start = len - TRAILER_SIZE - archive_size;
130
131 if archive_start > len - TRAILER_SIZE {
132 return Err(io::Error::new(
133 io::ErrorKind::InvalidData,
134 "archive size exceeds file size",
135 ));
136 }
137
138 let archive_bytes = &data[archive_start..archive_start + archive_size];
139 deserialize_archive(archive_bytes)
140}
141
142fn read_u16(data: &[u8], pos: &mut usize) -> io::Result<u16> {
148 if *pos + 2 > data.len() {
149 return Err(io::Error::new(
150 io::ErrorKind::UnexpectedEof,
151 "unexpected end of archive (u16)",
152 ));
153 }
154 let val = u16::from_le_bytes(data[*pos..*pos + 2].try_into().unwrap());
155 *pos += 2;
156 Ok(val)
157}
158
159fn read_u32(data: &[u8], pos: &mut usize) -> io::Result<u32> {
161 if *pos + 4 > data.len() {
162 return Err(io::Error::new(
163 io::ErrorKind::UnexpectedEof,
164 "unexpected end of archive (u32)",
165 ));
166 }
167 let val = u32::from_le_bytes(data[*pos..*pos + 4].try_into().unwrap());
168 *pos += 4;
169 Ok(val)
170}
171
172fn read_u64(data: &[u8], pos: &mut usize) -> io::Result<u64> {
174 if *pos + 8 > data.len() {
175 return Err(io::Error::new(
176 io::ErrorKind::UnexpectedEof,
177 "unexpected end of archive (u64)",
178 ));
179 }
180 let val = u64::from_le_bytes(data[*pos..*pos + 8].try_into().unwrap());
181 *pos += 8;
182 Ok(val)
183}
184
185fn read_bytes<'a>(data: &'a [u8], pos: &mut usize, n: usize) -> io::Result<&'a [u8]> {
187 if *pos + n > data.len() {
188 return Err(io::Error::new(
189 io::ErrorKind::UnexpectedEof,
190 "unexpected end of archive (bytes)",
191 ));
192 }
193 let slice = &data[*pos..*pos + n];
194 *pos += n;
195 Ok(slice)
196}
197
198fn deserialize_archive(data: &[u8]) -> io::Result<Archive> {
200 let mut pos = 0;
201
202 let format_version = read_u16(data, &mut pos)?;
204 if format_version != FORMAT_VERSION {
205 return Err(io::Error::new(
206 io::ErrorKind::InvalidData,
207 format!(
208 "unsupported archive format version {format_version}, expected {FORMAT_VERSION}"
209 ),
210 ));
211 }
212
213 let flags = read_u16(data, &mut pos)?;
214 let stored_checksum = read_u32(data, &mut pos)?;
215 let checksum_start = pos;
217
218 let computed_checksum = crc32fast::hash(&data[checksum_start..]);
220 if stored_checksum != computed_checksum {
221 return Err(io::Error::new(
222 io::ErrorKind::InvalidData,
223 format!(
224 "archive checksum mismatch: stored {stored_checksum:#010x}, computed {computed_checksum:#010x}"
225 ),
226 ));
227 }
228
229 let metadata_count = read_u32(data, &mut pos)? as usize;
231 let remaining = data.len().saturating_sub(pos);
235 let mut metadata = HashMap::with_capacity(metadata_count.min(remaining / 8));
236 for _ in 0..metadata_count {
237 let key_len = read_u16(data, &mut pos)? as usize;
238 let key_bytes = read_bytes(data, &mut pos, key_len)?;
239 let key = String::from_utf8(key_bytes.to_vec()).map_err(|e| {
240 io::Error::new(
241 io::ErrorKind::InvalidData,
242 format!("metadata key is not valid UTF-8: {e}"),
243 )
244 })?;
245 let val_len = read_u32(data, &mut pos)? as usize;
246 let val = read_bytes(data, &mut pos, val_len)?.to_vec();
247 metadata.insert(key, val);
248 }
249
250 let entry_count = read_u32(data, &mut pos)? as usize;
252
253 struct TocEntry {
254 path: String,
255 offset: u64,
256 size: u64,
257 }
258
259 let remaining = data.len().saturating_sub(pos);
261 let mut toc = Vec::with_capacity(entry_count.min(remaining / 20));
262 for _ in 0..entry_count {
263 let path_len = read_u32(data, &mut pos)? as usize;
264 let path_bytes = read_bytes(data, &mut pos, path_len)?;
265 let path = String::from_utf8(path_bytes.to_vec()).map_err(|e| {
266 io::Error::new(
267 io::ErrorKind::InvalidData,
268 format!("file path is not valid UTF-8: {e}"),
269 )
270 })?;
271 let offset = read_u64(data, &mut pos)?;
272 let size = read_u64(data, &mut pos)?;
273 toc.push(TocEntry { path, offset, size });
274 }
275
276 let file_data_start = pos;
278 let mut files = HashMap::with_capacity(toc.len());
279 for entry in &toc {
280 let start = file_data_start + entry.offset as usize;
281 let end = start + entry.size as usize;
282 if end > data.len() {
283 return Err(io::Error::new(
284 io::ErrorKind::InvalidData,
285 format!(
286 "file entry '{}' extends beyond archive (offset={}, size={}, data_len={})",
287 entry.path,
288 entry.offset,
289 entry.size,
290 data.len()
291 ),
292 ));
293 }
294 files.insert(entry.path.clone(), data[start..end].to_vec());
295 }
296
297 Ok(Archive {
298 format_version,
299 flags,
300 metadata,
301 files,
302 })
303}
304
305pub fn deserialize_archive_from_bytes(data: &[u8]) -> io::Result<Archive> {
308 deserialize_archive(data)
309}
310
311pub fn serialize_archive(
321 metadata: &HashMap<String, Vec<u8>>,
322 files: &HashMap<String, Vec<u8>>,
323) -> Vec<u8> {
324 let mut buf: Vec<u8> = Vec::new();
325
326 buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); let mut meta_keys: Vec<&String> = metadata.keys().collect();
334 meta_keys.sort();
335
336 buf.extend_from_slice(&(meta_keys.len() as u32).to_le_bytes()); for key in &meta_keys {
338 let key_bytes = key.as_bytes();
339 buf.extend_from_slice(&(key_bytes.len() as u16).to_le_bytes());
340 buf.extend_from_slice(key_bytes);
341 let val = &metadata[*key];
342 buf.extend_from_slice(&(val.len() as u32).to_le_bytes());
343 buf.extend_from_slice(val);
344 }
345
346 let mut file_keys: Vec<&String> = files.keys().collect();
349 file_keys.sort();
350
351 struct FileEntry<'a> {
353 path: &'a str,
354 data: &'a [u8],
355 offset: u64,
356 }
357
358 let mut entries = Vec::with_capacity(file_keys.len());
359 let mut current_offset: u64 = 0;
360 for key in &file_keys {
361 let data = &files[*key];
362 entries.push(FileEntry {
363 path: key.as_str(),
364 data,
365 offset: current_offset,
366 });
367 current_offset += data.len() as u64;
368 }
369
370 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
372
373 for entry in &entries {
375 let path_bytes = entry.path.as_bytes();
376 buf.extend_from_slice(&(path_bytes.len() as u32).to_le_bytes());
377 buf.extend_from_slice(path_bytes);
378 buf.extend_from_slice(&entry.offset.to_le_bytes());
379 buf.extend_from_slice(&(entry.data.len() as u64).to_le_bytes());
380 }
381
382 for entry in &entries {
384 buf.extend_from_slice(entry.data);
385 }
386
387 let checksum = crc32fast::hash(&buf[8..]); buf[4..8].copy_from_slice(&checksum.to_le_bytes());
390
391 buf
392}
393
394#[allow(dead_code)]
401pub fn write_bundled_executable(
402 runtime_path: &Path,
403 output_path: &Path,
404 archive_bytes: &[u8],
405) -> io::Result<()> {
406 let runtime = std::fs::read(runtime_path)?;
408
409 let mut out = std::fs::File::create(output_path)?;
411 out.write_all(&runtime)?;
412 out.write_all(archive_bytes)?;
413
414 let archive_size = archive_bytes.len() as u64;
416 out.write_all(&archive_size.to_le_bytes())?;
417 out.write_all(MAGIC)?;
418
419 out.flush()?;
420 drop(out);
421
422 #[cfg(unix)]
424 {
425 use std::os::unix::fs::PermissionsExt;
426 let perms = std::fs::Permissions::from_mode(0o755);
427 std::fs::set_permissions(output_path, perms)?;
428 }
429
430 Ok(())
431}
432
433pub fn write_bundled_executable_from_bytes(
437 runtime: &[u8],
438 output_path: &Path,
439 archive_bytes: &[u8],
440) -> io::Result<()> {
441 let mut out = std::fs::File::create(output_path)?;
442 out.write_all(runtime)?;
443 out.write_all(archive_bytes)?;
444
445 let archive_size = archive_bytes.len() as u64;
447 out.write_all(&archive_size.to_le_bytes())?;
448 out.write_all(MAGIC)?;
449
450 out.flush()?;
451 drop(out);
452
453 #[cfg(unix)]
454 {
455 use std::os::unix::fs::PermissionsExt;
456 let perms = std::fs::Permissions::from_mode(0o755);
457 std::fs::set_permissions(output_path, perms)?;
458 }
459
460 Ok(())
461}
462
463#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn test_archive_roundtrip() {
473 let mut metadata = HashMap::new();
474 metadata.insert("entry".to_string(), b"main.semac".to_vec());
475 metadata.insert("version".to_string(), b"1".to_vec());
476
477 let mut files = HashMap::new();
478 files.insert("main.semac".to_string(), vec![0xDE, 0xAD, 0xBE, 0xEF]);
479 files.insert("lib/utils.sema".to_string(), b"(define x 42)".to_vec());
480
481 let bytes = serialize_archive(&metadata, &files);
482 let archive = deserialize_archive(&bytes).expect("deserialize should succeed");
483
484 assert_eq!(archive.format_version, FORMAT_VERSION);
485 assert_eq!(archive.flags, 0);
486 assert_eq!(archive.metadata.len(), 2);
487 assert_eq!(archive.metadata.get("entry").unwrap(), b"main.semac");
488 assert_eq!(archive.metadata.get("version").unwrap(), b"1");
489 assert_eq!(archive.files.len(), 2);
490 assert_eq!(
491 archive.files.get("main.semac").unwrap(),
492 &vec![0xDE, 0xAD, 0xBE, 0xEF]
493 );
494 assert_eq!(
495 archive.files.get("lib/utils.sema").unwrap(),
496 b"(define x 42)"
497 );
498 }
499
500 #[test]
501 fn test_archive_empty() {
502 let metadata = HashMap::new();
503 let files = HashMap::new();
504
505 let bytes = serialize_archive(&metadata, &files);
506 let archive = deserialize_archive(&bytes).expect("deserialize should succeed");
507
508 assert_eq!(archive.format_version, FORMAT_VERSION);
509 assert_eq!(archive.flags, 0);
510 assert!(archive.metadata.is_empty());
511 assert!(archive.files.is_empty());
512 }
513
514 #[test]
515 fn test_crc32_known_value() {
516 let empty = crc32fast::hash(b"");
519 assert_eq!(empty, 0x0000_0000);
520
521 let check = crc32fast::hash(b"123456789");
523 assert_eq!(check, 0xCBF4_3926);
524 }
525
526 #[test]
527 fn test_checksum_validation() {
528 let metadata = HashMap::new();
529 let files = HashMap::new();
530 let mut bytes = serialize_archive(&metadata, &files);
531
532 bytes[4] ^= 0xFF;
534
535 let result = deserialize_archive(&bytes);
536 assert!(result.is_err());
537 let err = result.unwrap_err();
538 assert!(
539 err.to_string().contains("checksum mismatch"),
540 "error should mention checksum: {err}"
541 );
542 }
543
544 #[test]
545 fn test_deserialize_archive_from_bytes_public() {
546 let metadata = HashMap::new();
547 let mut files = HashMap::new();
548 files.insert("test.txt".to_string(), b"hello".to_vec());
549
550 let bytes = serialize_archive(&metadata, &files);
551 let archive =
552 deserialize_archive_from_bytes(&bytes).expect("public deserialize should succeed");
553
554 assert_eq!(archive.files.len(), 1);
555 assert_eq!(archive.files.get("test.txt").unwrap(), b"hello");
556 }
557
558 fn craft_archive_with_counts(metadata_count: u32, entry_count: u32) -> Vec<u8> {
561 let mut buf: Vec<u8> = Vec::new();
562 buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
564 buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&metadata_count.to_le_bytes());
569 buf.extend_from_slice(&entry_count.to_le_bytes());
573 let checksum = crc32fast::hash(&buf[8..]);
577 buf[4..8].copy_from_slice(&checksum.to_le_bytes());
578 buf
579 }
580
581 #[test]
582 fn test_huge_metadata_count_does_not_oom() {
583 let data = craft_archive_with_counts(u32::MAX, 0);
586 let result = deserialize_archive(&data);
587 assert!(result.is_err(), "should fail, not OOM");
588 }
589
590 #[test]
591 fn test_huge_entry_count_does_not_oom() {
592 let data = craft_archive_with_counts(0, u32::MAX);
594 let result = deserialize_archive(&data);
595 assert!(result.is_err(), "should fail, not OOM");
596 }
597
598 #[test]
599 fn test_write_and_detect_bundled() {
600 use std::io::Write;
601
602 let dir = std::env::temp_dir().join("sema_archive_test");
603 let _ = std::fs::create_dir_all(&dir);
604
605 let runtime_path = dir.join("fake_runtime");
606 let output_path = dir.join("bundled_output");
607
608 {
610 let mut f = std::fs::File::create(&runtime_path).unwrap();
611 f.write_all(b"FAKE_RUNTIME_BINARY").unwrap();
612 }
613
614 let mut metadata = HashMap::new();
616 metadata.insert("entry".to_string(), b"main.semac".to_vec());
617 let mut files = HashMap::new();
618 files.insert("main.semac".to_string(), vec![1, 2, 3, 4]);
619
620 let archive_bytes = serialize_archive(&metadata, &files);
621
622 write_bundled_executable(&runtime_path, &output_path, &archive_bytes).unwrap();
624
625 assert!(has_embedded_archive(&output_path).unwrap());
627
628 assert!(!has_embedded_archive(&runtime_path).unwrap());
630
631 let extracted = extract_archive(&output_path).unwrap();
633 assert_eq!(extracted.metadata.get("entry").unwrap(), b"main.semac");
634 assert_eq!(
635 extracted.files.get("main.semac").unwrap(),
636 &vec![1, 2, 3, 4]
637 );
638
639 let _ = std::fs::remove_dir_all(&dir);
641 }
642
643 #[test]
644 fn test_write_bundled_from_bytes_roundtrip() {
645 let dir = std::env::temp_dir().join("sema_archive_from_bytes_test");
646 let _ = std::fs::create_dir_all(&dir);
647 let output_path = dir.join("bundled_from_bytes");
648
649 let runtime = b"FAKE_RUNTIME_BINARY";
650 let mut metadata = HashMap::new();
651 metadata.insert("entry".to_string(), b"main.semac".to_vec());
652 let mut files = HashMap::new();
653 files.insert("main.semac".to_string(), vec![1, 2, 3, 4]);
654 let archive_bytes = serialize_archive(&metadata, &files);
655
656 write_bundled_executable_from_bytes(runtime, &output_path, &archive_bytes).unwrap();
657
658 assert!(has_embedded_archive(&output_path).unwrap());
659 let extracted = extract_archive(&output_path).unwrap();
660 assert_eq!(extracted.metadata.get("entry").unwrap(), b"main.semac");
661 assert_eq!(
662 extracted.files.get("main.semac").unwrap(),
663 &vec![1, 2, 3, 4]
664 );
665
666 let _ = std::fs::remove_dir_all(&dir);
667 }
668}