Rust · 3739 bytes Raw Blame History
1 use std::fs;
2 use std::path::{Path, PathBuf};
3 use std::process::Command;
4
5 use afs_ld::layout::Layout;
6 use afs_ld::macho::constants::{LC_ID_DYLIB, MH_DYLIB, MH_EXECUTE};
7 use afs_ld::macho::reader::{parse_commands, parse_header, LoadCommand};
8 use afs_ld::macho::writer::write;
9 use afs_ld::{LinkOptions, OutputKind};
10
11 fn have_tool(name: &str) -> bool {
12 Command::new(name)
13 .arg("-h")
14 .output()
15 .map(|_| true)
16 .unwrap_or(false)
17 }
18
19 fn scratch(name: &str) -> PathBuf {
20 std::env::temp_dir().join(format!("afs-ld-writer-{}-{name}", std::process::id()))
21 }
22
23 fn write_temp(name: &str, bytes: &[u8]) -> PathBuf {
24 let path = scratch(name);
25 fs::write(&path, bytes).expect("write temp mach-o");
26 path
27 }
28
29 fn run_otool_lv(path: &Path) -> Result<String, String> {
30 let out = Command::new("otool")
31 .arg("-lV")
32 .arg(path)
33 .output()
34 .map_err(|e| format!("spawn otool -lV: {e}"))?;
35 if !out.status.success() {
36 return Err(format!(
37 "otool -lV failed: {}",
38 String::from_utf8_lossy(&out.stderr)
39 ));
40 }
41 Ok(String::from_utf8_lossy(&out.stdout).into_owned())
42 }
43
44 fn run_file(path: &Path) -> Result<String, String> {
45 let out = Command::new("file")
46 .arg(path)
47 .output()
48 .map_err(|e| format!("spawn file: {e}"))?;
49 if !out.status.success() {
50 return Err(format!(
51 "file failed: {}",
52 String::from_utf8_lossy(&out.stderr)
53 ));
54 }
55 Ok(String::from_utf8_lossy(&out.stdout).into_owned())
56 }
57
58 #[test]
59 fn empty_executable_writer_emits_parseable_macho() {
60 let layout = Layout::empty(OutputKind::Executable, 0);
61 let opts = LinkOptions::default();
62 let mut bytes = Vec::new();
63 write(&layout, OutputKind::Executable, &opts, &mut bytes).expect("write executable");
64
65 let hdr = parse_header(&bytes).expect("header parses");
66 assert_eq!(hdr.filetype, MH_EXECUTE);
67 let cmds = parse_commands(&hdr, &bytes).expect("commands parse");
68 assert!(
69 cmds.iter()
70 .any(|cmd| matches!(cmd, LoadCommand::Segment64(seg) if seg.segname_str() == "__TEXT"))
71 );
72
73 let path = write_temp("empty-exec", &bytes);
74 if have_tool("otool") {
75 let dump = run_otool_lv(&path).expect("otool -lV");
76 assert!(dump.contains("LC_MAIN"));
77 assert!(dump.contains("segname __TEXT"));
78 }
79 if have_tool("file") {
80 let desc = run_file(&path).expect("file");
81 assert!(desc.contains("Mach-O 64-bit executable arm64"));
82 }
83 let _ = fs::remove_file(path);
84 }
85
86 #[test]
87 fn empty_dylib_writer_emits_parseable_macho() {
88 let layout = Layout::empty(OutputKind::Dylib, 0);
89 let mut opts = LinkOptions {
90 kind: OutputKind::Dylib,
91 ..LinkOptions::default()
92 };
93 opts.output = Some(PathBuf::from("libempty.dylib"));
94
95 let mut bytes = Vec::new();
96 write(&layout, OutputKind::Dylib, &opts, &mut bytes).expect("write dylib");
97
98 let hdr = parse_header(&bytes).expect("header parses");
99 assert_eq!(hdr.filetype, MH_DYLIB);
100 let cmds = parse_commands(&hdr, &bytes).expect("commands parse");
101 assert!(cmds.iter().any(|cmd| {
102 matches!(cmd, LoadCommand::Dylib(d) if d.cmd == LC_ID_DYLIB && d.name == "@rpath/libempty.dylib")
103 }));
104
105 let path = write_temp("empty-dylib.dylib", &bytes);
106 if have_tool("otool") {
107 let dump = run_otool_lv(&path).expect("otool -lV");
108 assert!(dump.contains("LC_ID_DYLIB"));
109 assert!(dump.contains("@rpath/libempty.dylib"));
110 }
111 if have_tool("file") {
112 let desc = run_file(&path).expect("file");
113 assert!(desc.contains("dynamically linked shared library arm64"));
114 }
115 let _ = fs::remove_file(path);
116 }
117