1use std::fs::{self, File, OpenOptions};
80use std::io::{self, Read, Seek, SeekFrom, Write};
81use std::os::unix::fs::PermissionsExt;
82use std::path::{Path, PathBuf};
83
84pub const AOT_MAGIC: &[u8; 8] = b"ZSHRSAOT";
86pub const AOT_VERSION_V1: u32 = 1;
88pub const AOT_VERSION_V2: u32 = 2;
90pub const TRAILER_LEN: u64 = 32;
92#[derive(Debug, Clone)]
94pub struct EmbeddedFile {
95 pub name: String,
97 pub source: String,
99}
100
101#[derive(Debug, Clone)]
104pub struct EmbeddedFiles(pub Vec<EmbeddedFile>);
105
106fn encode_payload_v2(files: &[EmbeddedFile]) -> Vec<u8> {
107 let mut out = Vec::with_capacity(
108 64 + files
109 .iter()
110 .map(|f| f.name.len() + f.source.len() + 8)
111 .sum::<usize>(),
112 );
113 let count = u32::try_from(files.len()).expect("file count fits in u32");
114 out.extend_from_slice(&count.to_le_bytes());
115 for f in files {
116 let name_len = u32::try_from(f.name.len()).expect("name length fits in u32");
117 let src_len = u32::try_from(f.source.len()).expect("source length fits in u32");
118 out.extend_from_slice(&name_len.to_le_bytes());
119 out.extend_from_slice(f.name.as_bytes());
120 out.extend_from_slice(&src_len.to_le_bytes());
121 out.extend_from_slice(f.source.as_bytes());
122 }
123 out
124}
125
126fn decode_payload_v2(bytes: &[u8]) -> Option<EmbeddedFiles> {
127 let mut pos = 0usize;
128 if bytes.len() < 4 {
129 return None;
130 }
131 let count = u32::from_le_bytes(bytes[pos..pos + 4].try_into().ok()?) as usize;
132 pos += 4;
133 let mut out = Vec::with_capacity(count);
134 for _ in 0..count {
135 if pos + 4 > bytes.len() {
136 return None;
137 }
138 let name_len = u32::from_le_bytes(bytes[pos..pos + 4].try_into().ok()?) as usize;
139 pos += 4;
140 if pos + name_len > bytes.len() {
141 return None;
142 }
143 let name = std::str::from_utf8(&bytes[pos..pos + name_len])
144 .ok()?
145 .to_string();
146 pos += name_len;
147 if pos + 4 > bytes.len() {
148 return None;
149 }
150 let src_len = u32::from_le_bytes(bytes[pos..pos + 4].try_into().ok()?) as usize;
151 pos += 4;
152 if pos + src_len > bytes.len() {
153 return None;
154 }
155 let source = std::str::from_utf8(&bytes[pos..pos + src_len])
156 .ok()?
157 .to_string();
158 pos += src_len;
159 out.push(EmbeddedFile { name, source });
160 }
161 Some(EmbeddedFiles(out))
162}
163
164fn decode_payload_v1(bytes: &[u8]) -> Option<EmbeddedFiles> {
168 if bytes.len() < 4 {
169 return None;
170 }
171 let name_len = u32::from_le_bytes(bytes[0..4].try_into().ok()?) as usize;
172 if 4 + name_len > bytes.len() {
173 return None;
174 }
175 let name = std::str::from_utf8(&bytes[4..4 + name_len])
176 .ok()?
177 .to_string();
178 let source = std::str::from_utf8(&bytes[4 + name_len..])
179 .ok()?
180 .to_string();
181 Some(EmbeddedFiles(vec![EmbeddedFile { name, source }]))
182}
183
184fn build_trailer(compressed_len: u64, uncompressed_len: u64, version: u32) -> [u8; 32] {
185 let mut trailer = [0u8; 32];
186 trailer[0..8].copy_from_slice(&compressed_len.to_le_bytes());
187 trailer[8..16].copy_from_slice(&uncompressed_len.to_le_bytes());
188 trailer[16..20].copy_from_slice(&version.to_le_bytes());
189 trailer[24..32].copy_from_slice(AOT_MAGIC);
191 trailer
192}
193
194pub fn append_embedded_files(out_path: &Path, files: &[EmbeddedFile]) -> io::Result<()> {
199 let payload = encode_payload_v2(files);
200 let compressed = zstd::stream::encode_all(&payload[..], 3)?;
201 let mut f = OpenOptions::new().append(true).open(out_path)?;
202 f.write_all(&compressed)?;
203 let trailer = build_trailer(
204 compressed.len() as u64,
205 payload.len() as u64,
206 AOT_VERSION_V2,
207 );
208 f.write_all(&trailer)?;
209 f.sync_all()?;
210 Ok(())
211}
212
213pub fn try_load_embedded(exe: &Path) -> Option<EmbeddedFiles> {
218 let mut f = File::open(exe).ok()?;
219 let size = f.metadata().ok()?.len();
220 if size < TRAILER_LEN {
221 return None;
222 }
223 f.seek(SeekFrom::End(-(TRAILER_LEN as i64))).ok()?;
224 let mut trailer = [0u8; TRAILER_LEN as usize];
225 f.read_exact(&mut trailer).ok()?;
226 if &trailer[24..32] != AOT_MAGIC {
227 return None;
228 }
229 let compressed_len = u64::from_le_bytes(trailer[0..8].try_into().ok()?);
230 let uncompressed_len = u64::from_le_bytes(trailer[8..16].try_into().ok()?);
231 let version = u32::from_le_bytes(trailer[16..20].try_into().ok()?);
232 if compressed_len == 0 || compressed_len > size - TRAILER_LEN {
233 return None;
234 }
235 let payload_start = size - TRAILER_LEN - compressed_len;
236 f.seek(SeekFrom::Start(payload_start)).ok()?;
237 let mut compressed = vec![0u8; compressed_len as usize];
238 f.read_exact(&mut compressed).ok()?;
239 let payload = zstd::stream::decode_all(&compressed[..]).ok()?;
240 if payload.len() != uncompressed_len as usize {
241 return None;
242 }
243 match version {
244 AOT_VERSION_V1 => decode_payload_v1(&payload),
245 AOT_VERSION_V2 => decode_payload_v2(&payload),
246 _ => None,
247 }
248}
249
250#[cfg(unix)]
251fn set_executable(path: &Path) {
252 if let Ok(meta) = fs::metadata(path) {
253 let mut p = meta.permissions();
254 p.set_mode(p.mode() | 0o111);
255 let _ = fs::set_permissions(path, p);
256 }
257}
258
259#[cfg(not(unix))]
260fn set_executable(_path: &Path) {}
261
262fn copy_exe_without_trailer(src: &Path, dst: &Path) -> io::Result<()> {
266 let mut sf = File::open(src)?;
267 let size = sf.metadata()?.len();
268 let keep = if size >= TRAILER_LEN {
269 sf.seek(SeekFrom::End(-(TRAILER_LEN as i64)))?;
270 let mut trailer = [0u8; TRAILER_LEN as usize];
271 if sf.read_exact(&mut trailer).is_ok() && &trailer[24..32] == AOT_MAGIC {
272 let compressed_len = u64::from_le_bytes(trailer[0..8].try_into().unwrap());
273 if compressed_len > 0 && compressed_len <= size - TRAILER_LEN {
274 size - TRAILER_LEN - compressed_len
275 } else {
276 size
277 }
278 } else {
279 size
280 }
281 } else {
282 size
283 };
284 sf.seek(SeekFrom::Start(0))?;
285 let _ = fs::remove_file(dst);
286 let mut df = File::create(dst)?;
287 let mut remaining = keep;
288 let mut buf = vec![0u8; 64 * 1024];
289 while remaining > 0 {
290 let n = std::cmp::min(remaining as usize, buf.len());
291 sf.read_exact(&mut buf[..n])?;
292 df.write_all(&buf[..n])?;
293 remaining -= n as u64;
294 }
295 df.sync_all()?;
296 Ok(())
297}
298
299pub fn build(script_paths: &[PathBuf], out_path: &Path) -> Result<PathBuf, String> {
308 if script_paths.is_empty() {
309 return Err("zbuild: at least one --in PATH required".to_string());
310 }
311 let mut files: Vec<EmbeddedFile> = Vec::with_capacity(script_paths.len());
312 for p in script_paths {
313 let source = fs::read_to_string(p)
314 .map_err(|e| format!("zbuild: cannot read {}: {}", p.display(), e))?;
315 let name = p
316 .file_name()
317 .and_then(|s| s.to_str())
318 .unwrap_or("script.zsh")
319 .to_string();
320 files.push(EmbeddedFile { name, source });
321 }
322 let exe = std::env::current_exe()
323 .map_err(|e| format!("zbuild: locating current executable: {}", e))?;
324 copy_exe_without_trailer(&exe, out_path).map_err(|e| {
325 format!(
326 "zbuild: copy {} -> {}: {}",
327 exe.display(),
328 out_path.display(),
329 e
330 )
331 })?;
332 append_embedded_files(out_path, &files).map_err(|e| format!("zbuild: write trailer: {}", e))?;
333 set_executable(out_path);
334 Ok(out_path.to_path_buf())
335}
336
337#[cfg(feature = "aot-hook")]
367#[no_mangle]
368pub extern "C" fn fusevm_aot_register_builtins(vm: *mut fusevm::VM) {
369 let vm = unsafe { &mut *vm };
371 let exec: &'static mut crate::vm_helper::ShellExecutor =
372 Box::leak(Box::new(crate::vm_helper::ShellExecutor::new()));
373 let args: Vec<String> = std::env::args().skip(1).collect();
375 exec.set_pparams(args);
376 let last = exec.last_status();
377 crate::fusevm_bridge::register_builtins(vm);
378 vm.last_status = last;
379 std::mem::forget(crate::fusevm_bridge::ExecutorContext::enter(exec));
382}
383
384fn compile_source_to_chunk(source: &str) -> Result<fusevm::Chunk, String> {
386 let program = crate::vm_helper::parse_isolated(source);
387 let chunk = crate::compile_zsh::ZshCompiler::new().compile(&program);
388 if chunk.ops.is_empty() {
389 return Err("zbuild --native: script compiled to an empty chunk".to_string());
390 }
391 Ok(chunk)
392}
393
394fn runtime_staticlib() -> Result<PathBuf, String> {
398 if let Ok(p) = std::env::var("ZSHRS_AOT_RUNTIME_LIB") {
399 return Ok(PathBuf::from(p));
400 }
401 let exe = std::env::current_exe().map_err(|e| e.to_string())?;
402 if let Some(dir) = exe.parent() {
403 let cand = dir.join("libzsh.a");
404 if cand.exists() {
405 return Ok(cand);
406 }
407 }
408 Err("could not locate libzsh.a (set ZSHRS_AOT_RUNTIME_LIB)".to_string())
409}
410
411pub fn build_native(script_paths: &[PathBuf], out_path: &Path) -> Result<PathBuf, String> {
415 if script_paths.is_empty() {
416 return Err("zbuild --native: at least one --in PATH required".to_string());
417 }
418 let mut source = String::new();
419 for p in script_paths {
420 let s = fs::read_to_string(p)
421 .map_err(|e| format!("zbuild --native: cannot read {}: {}", p.display(), e))?;
422 source.push_str(&s);
423 if !source.ends_with('\n') {
424 source.push('\n');
425 }
426 }
427 let chunk = compile_source_to_chunk(&source)?;
428
429 let runtime_lib = runtime_staticlib()?;
430 if !runtime_lib.exists() {
431 return Err(format!(
432 "zbuild --native: runtime staticlib not found at {}",
433 runtime_lib.display()
434 ));
435 }
436
437 let obj = out_path.with_extension("o");
438 fusevm::aot::compile_object(&chunk, &obj).map_err(|e| format!("zbuild --native: {}", e))?;
439
440 let stub = out_path.with_extension("aot_main.c");
441 fs::write(
442 &stub,
443 b"extern long fusevm_aot_run_embedded(void);\nint main(void){return (int)fusevm_aot_run_embedded();}\n" as &[u8],
444 )
445 .map_err(|e| format!("zbuild --native: write entry stub: {}", e))?;
446
447 let mut cmd = std::process::Command::new("cc");
448 cmd.arg(&stub).arg(&obj).arg(&runtime_lib);
449 cmd.arg("-lncurses");
451 if !cfg!(target_os = "macos") {
452 cmd.arg("-lm");
459 }
460 if !cfg!(any(target_os = "linux", target_os = "android")) {
461 cmd.arg("-liconv");
465 }
466 if cfg!(target_os = "macos") {
467 for fw in [
471 "CoreFoundation",
472 "CoreServices",
473 "Security",
474 "SystemConfiguration",
475 ] {
476 cmd.arg("-framework").arg(fw);
477 }
478 }
479 cmd.arg("-o").arg(out_path);
480 let status = crate::fusevm_bridge::foreground_status(&mut cmd)
487 .map_err(|e| format!("zbuild --native: invoking cc: {}", e))?;
488 let _ = fs::remove_file(&stub);
489 let _ = fs::remove_file(&obj);
490 if !status.success() {
491 return Err(format!(
492 "zbuild --native: link failed (cc exit {:?})",
493 status.code()
494 ));
495 }
496 set_executable(out_path);
497 Ok(out_path.to_path_buf())
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 use fusevm::aot::{lowering_for, Lowering};
519
520 fn lowering_of(src: &str) -> (usize, Lowering) {
522 let chunk = compile_source_to_chunk(src).expect("compiles");
523 (chunk.ops.len(), lowering_for(&chunk))
524 }
525
526 #[test]
527 fn native_lowering_covers_the_whole_script_not_one_op() {
528 for (label, src) in [
533 ("simplest", "print hello\n"),
534 (
535 "arith loop",
536 "integer i sum\nsum=0\nfor (( i = 1; i <= 10; i++ )); do (( sum += i )); done\nprint $sum\n",
537 ),
538 (
539 "function",
540 "f() { local n=$1; print $(( n * 2 )) }\nf 21\n",
541 ),
542 (
543 "arrays",
544 "a=(one two three)\nfor x in $a; do print \"item:$x\"; done\nprint ${#a} ${a[2]}\n",
545 ),
546 ] {
547 let (ops, lowering) = lowering_of(src);
548 assert!(ops > 10, "{label}: chunk is only {ops} ops");
549 match lowering {
550 Lowering::Threaded => {}
553 Lowering::Native { covered, .. } => assert!(
556 covered * 2 >= ops,
557 "{label}: native plan covers {covered} of {ops} ops — \
558 the rest is interpreted, which a threaded lowering would not be"
559 ),
560 }
561 }
562 }
563
564 #[test]
565 fn emitted_code_grows_with_the_program() {
566 let small = compile_source_to_chunk("print hello\n").expect("compiles");
574 let mut big_src = String::new();
575 for i in 0..60 {
576 big_src.push_str(&format!("print line{i}\n"));
577 }
578 let big = compile_source_to_chunk(&big_src).expect("compiles");
579 assert!(
580 big.ops.len() > small.ops.len() * 4,
581 "test setup: {} vs {} ops",
582 big.ops.len(),
583 small.ops.len()
584 );
585
586 let dir = tempfile::tempdir().expect("tempdir");
587 let code_len = |chunk: &fusevm::Chunk, name: &str| -> i64 {
588 let path = dir.path().join(name);
589 fusevm::aot::compile_object(chunk, &path).expect("emits an object");
590 let obj = fs::metadata(&path).expect("object exists").len() as i64;
591 let blob = bincode::serialize(chunk).expect("chunk serializes").len() as i64;
592 obj - blob
593 };
594 let small_code = code_len(&small, "small.o");
595 let big_code = code_len(&big, "big.o");
596 assert!(
597 small_code > 0 && big_code > small_code * 3,
598 "code size did not follow the program: {small_code} vs {big_code} bytes \
599 (a stub driver is constant-size whatever the script)"
600 );
601 }
602
603 #[test]
604 fn zsh_builtins_are_not_arity_honest() {
605 let chunk = compile_source_to_chunk("print hello\n").expect("compiles");
612 assert!(
613 !chunk.builtin_argc_is_arity,
614 "the zsh compiler declared builtin argc as a stack arity; every \
615 handler in fusevm_bridge.rs must pop exactly argc before this holds"
616 );
617 }
618
619 fn mkfile(name: &str, src: &str) -> EmbeddedFile {
620 EmbeddedFile {
621 name: name.into(),
622 source: src.into(),
623 }
624 }
625
626 #[test]
627 fn build_trailer_layout_matches_spec() {
628 let t = build_trailer(0x11_22_33_44, 0xAA_BB_CC_DD, AOT_VERSION_V2);
629 assert_eq!(t.len(), TRAILER_LEN as usize);
630 assert_eq!(&t[0..8], &0x11_22_33_44u64.to_le_bytes());
632 assert_eq!(&t[8..16], &0xAA_BB_CC_DDu64.to_le_bytes());
634 assert_eq!(&t[16..20], &AOT_VERSION_V2.to_le_bytes());
636 assert_eq!(&t[20..24], &[0u8; 4]);
638 assert_eq!(&t[24..32], AOT_MAGIC);
640 }
641
642 #[test]
643 fn payload_v2_roundtrip_single_file() {
644 let files = vec![mkfile("hello.zsh", "echo hi\n")];
645 let encoded = encode_payload_v2(&files);
646 let decoded = decode_payload_v2(&encoded).expect("decode v2");
647 assert_eq!(decoded.0.len(), 1);
648 assert_eq!(decoded.0[0].name, "hello.zsh");
649 assert_eq!(decoded.0[0].source, "echo hi\n");
650 }
651
652 #[test]
653 fn payload_v2_roundtrip_multiple_files_preserves_order() {
654 let files = vec![
655 mkfile("a.zsh", "echo a\n"),
656 mkfile("b.zsh", "echo b\n"),
657 mkfile("c.zsh", "echo c\n"),
658 ];
659 let encoded = encode_payload_v2(&files);
660 let decoded = decode_payload_v2(&encoded).expect("decode v2");
661 assert_eq!(decoded.0.len(), 3);
662 assert_eq!(decoded.0[0].name, "a.zsh");
663 assert_eq!(decoded.0[1].name, "b.zsh");
664 assert_eq!(decoded.0[2].name, "c.zsh");
665 assert_eq!(decoded.0[0].source, "echo a\n");
666 assert_eq!(decoded.0[1].source, "echo b\n");
667 assert_eq!(decoded.0[2].source, "echo c\n");
668 }
669
670 #[test]
671 fn payload_v2_roundtrip_zero_files() {
672 let files: Vec<EmbeddedFile> = vec![];
673 let encoded = encode_payload_v2(&files);
674 assert_eq!(encoded.as_slice(), &0u32.to_le_bytes());
676 let decoded = decode_payload_v2(&encoded).expect("decode zero-file v2");
677 assert!(decoded.0.is_empty());
678 }
679
680 #[test]
681 fn payload_v2_handles_empty_source() {
682 let files = vec![mkfile("empty.zsh", "")];
683 let encoded = encode_payload_v2(&files);
684 let decoded = decode_payload_v2(&encoded).expect("decode empty source");
685 assert_eq!(decoded.0[0].source, "");
686 assert_eq!(decoded.0[0].name, "empty.zsh");
687 }
688
689 #[test]
690 fn payload_v2_preserves_utf8_in_names_and_sources() {
691 let files = vec![
692 mkfile("名前.zsh", "echo こんにちは\n"),
693 mkfile("emoji-🚀.zsh", "echo $'\\xf0\\x9f\\x9a\\x80'\n"),
694 ];
695 let encoded = encode_payload_v2(&files);
696 let decoded = decode_payload_v2(&encoded).expect("decode utf8");
697 assert_eq!(decoded.0[0].name, "名前.zsh");
698 assert_eq!(decoded.0[0].source, "echo こんにちは\n");
699 assert_eq!(decoded.0[1].name, "emoji-🚀.zsh");
700 }
701
702 #[test]
703 fn payload_v2_rejects_truncated_input() {
704 let files = vec![mkfile("x.zsh", "echo x\n")];
705 let mut encoded = encode_payload_v2(&files);
706 encoded.pop();
708 assert!(decode_payload_v2(&encoded).is_none());
709 }
710
711 #[test]
712 fn payload_v2_rejects_empty_buffer() {
713 assert!(decode_payload_v2(&[]).is_none());
714 }
715
716 #[test]
717 fn payload_v2_rejects_lying_count_header() {
718 let mut buf = Vec::new();
720 buf.extend_from_slice(&5u32.to_le_bytes());
721 assert!(decode_payload_v2(&buf).is_none());
722 }
723
724 #[test]
725 fn payload_v1_decodes_legacy_single_script() {
726 let name = "old.zsh";
727 let source = "echo legacy\n";
728 let mut buf = Vec::new();
729 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
730 buf.extend_from_slice(name.as_bytes());
731 buf.extend_from_slice(source.as_bytes());
732 let decoded = decode_payload_v1(&buf).expect("decode v1");
733 assert_eq!(decoded.0.len(), 1);
734 assert_eq!(decoded.0[0].name, name);
735 assert_eq!(decoded.0[0].source, source);
736 }
737
738 #[test]
739 fn payload_v1_rejects_short_buffer() {
740 assert!(decode_payload_v1(&[]).is_none());
741 assert!(decode_payload_v1(&[1, 2, 3]).is_none());
742 }
743
744 #[test]
745 fn payload_v1_rejects_name_len_larger_than_buffer() {
746 let mut buf = Vec::new();
747 buf.extend_from_slice(&999u32.to_le_bytes()); buf.extend_from_slice(b"abc"); assert!(decode_payload_v1(&buf).is_none());
750 }
751
752 #[test]
753 fn payload_v1_handles_empty_source_after_name() {
754 let mut buf = Vec::new();
755 let name = "x";
756 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
757 buf.extend_from_slice(name.as_bytes());
758 let decoded = decode_payload_v1(&buf).expect("decode v1 empty source");
760 assert_eq!(decoded.0[0].source, "");
761 }
762
763 #[test]
764 fn append_and_load_roundtrip_single_file() {
765 let tmp = tempfile::NamedTempFile::new().expect("temp file");
766 std::fs::write(tmp.path(), b"FAKE-ELF-PREFIX").expect("write prefix");
768 let files = vec![mkfile("greet.zsh", "echo hello\n")];
769 append_embedded_files(tmp.path(), &files).expect("append");
770 let loaded = try_load_embedded(tmp.path()).expect("load back");
771 assert_eq!(loaded.0.len(), 1);
772 assert_eq!(loaded.0[0].name, "greet.zsh");
773 assert_eq!(loaded.0[0].source, "echo hello\n");
774 }
775
776 #[test]
777 fn append_and_load_roundtrip_multiple_files() {
778 let tmp = tempfile::NamedTempFile::new().expect("temp file");
779 std::fs::write(tmp.path(), b"PREFIX").expect("write prefix");
780 let files = vec![
781 mkfile("first.zsh", "first() { :; }\n"),
782 mkfile("second.zsh", "first\nsecond() { :; }\n"),
783 mkfile("third.zsh", "echo done\n"),
784 ];
785 append_embedded_files(tmp.path(), &files).expect("append");
786 let loaded = try_load_embedded(tmp.path()).expect("load back");
787 assert_eq!(loaded.0.len(), 3);
788 assert_eq!(loaded.0[0].name, "first.zsh");
789 assert_eq!(loaded.0[1].name, "second.zsh");
790 assert_eq!(loaded.0[2].name, "third.zsh");
791 }
792
793 #[test]
794 fn try_load_embedded_returns_none_without_magic() {
795 let tmp = tempfile::NamedTempFile::new().expect("temp file");
796 std::fs::write(tmp.path(), vec![0u8; 64]).expect("write");
798 assert!(try_load_embedded(tmp.path()).is_none());
799 }
800
801 #[test]
802 fn try_load_embedded_returns_none_for_small_file() {
803 let tmp = tempfile::NamedTempFile::new().expect("temp file");
804 std::fs::write(tmp.path(), b"tiny").expect("write");
806 assert!(try_load_embedded(tmp.path()).is_none());
807 }
808
809 #[test]
810 fn try_load_embedded_returns_none_for_missing_path() {
811 let path = std::path::PathBuf::from("/this/path/does/not/exist/zshrs-aot");
812 assert!(try_load_embedded(&path).is_none());
813 }
814
815 #[test]
816 fn try_load_embedded_returns_none_for_zero_compressed_len() {
817 let tmp = tempfile::NamedTempFile::new().expect("temp file");
819 let mut data = vec![0u8; 100];
820 let trailer = build_trailer(0, 0, AOT_VERSION_V2);
821 data.extend_from_slice(&trailer);
822 std::fs::write(tmp.path(), &data).expect("write");
823 assert!(try_load_embedded(tmp.path()).is_none());
824 }
825
826 #[test]
827 fn try_load_embedded_rejects_unknown_version() {
828 let tmp = tempfile::NamedTempFile::new().expect("temp file");
830 let payload = encode_payload_v2(&[mkfile("x.zsh", "y\n")]);
831 let compressed = zstd::stream::encode_all(&payload[..], 3).expect("zstd");
832 let mut data = vec![0u8; 32]; data.extend_from_slice(&compressed);
834 let trailer = build_trailer(compressed.len() as u64, payload.len() as u64, 99);
835 data.extend_from_slice(&trailer);
836 std::fs::write(tmp.path(), &data).expect("write");
837 assert!(try_load_embedded(tmp.path()).is_none());
838 }
839
840 #[test]
841 fn try_load_embedded_rejects_corrupt_uncompressed_len() {
842 let tmp = tempfile::NamedTempFile::new().expect("temp file");
844 let payload = encode_payload_v2(&[mkfile("x.zsh", "y\n")]);
845 let compressed = zstd::stream::encode_all(&payload[..], 3).expect("zstd");
846 let mut data = vec![0u8; 32];
847 data.extend_from_slice(&compressed);
848 let trailer = build_trailer(
850 compressed.len() as u64,
851 payload.len() as u64 + 1,
852 AOT_VERSION_V2,
853 );
854 data.extend_from_slice(&trailer);
855 std::fs::write(tmp.path(), &data).expect("write");
856 assert!(try_load_embedded(tmp.path()).is_none());
857 }
858
859 #[test]
860 fn build_rejects_empty_input_list() {
861 let out = std::path::PathBuf::from("/tmp/zshrs-aot-empty-out");
862 let res = build(&[], &out);
863 assert!(res.is_err());
864 let err = res.unwrap_err();
865 assert!(err.contains("at least one"), "got: {err}");
866 }
867}