decode LC_DYLD_INFO_ONLY and wire export trie through DylibFile
- SHA
336a3daa7d66e95640c9538126007d878ef7515f- Parents
-
015579b - Tree
20ce1a3
336a3da
336a3daa7d66e95640c9538126007d878ef7515f015579b
20ce1a3| Status | File | + | - |
|---|---|---|---|
| M |
src/args.rs
|
6 | 0 |
| M |
src/dump.rs
|
93 | 2 |
| M |
src/lib.rs
|
3 | 0 |
| M |
src/macho/dylib.rs
|
30 | 13 |
| M |
src/macho/reader.rs
|
100 | 0 |
| M |
src/main.rs
|
10 | 0 |
| A |
tests/dylib_integration.rs
|
103 | 0 |
src/args.rsmodified@@ -68,6 +68,12 @@ pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> { | ||
| 68 | 68 | .ok_or_else(|| ArgsError::MissingValue("--dump-archive".into()))?, |
| 69 | 69 | )); |
| 70 | 70 | } |
| 71 | + "--dump-dylib" => { | |
| 72 | + opts.dump_dylib = Some(PathBuf::from( | |
| 73 | + it.next() | |
| 74 | + .ok_or_else(|| ArgsError::MissingValue("--dump-dylib".into()))?, | |
| 75 | + )); | |
| 76 | + } | |
| 71 | 77 | s if s.starts_with('-') => { |
| 72 | 78 | return Err(ArgsError::UnknownFlag(s.to_string())); |
| 73 | 79 | } |
src/dump.rsmodified@@ -9,10 +9,12 @@ use std::path::Path; | ||
| 9 | 9 | |
| 10 | 10 | use crate::archive::{Archive, Flavor, SpecialMember}; |
| 11 | 11 | use crate::input::ObjectFile; |
| 12 | +use crate::macho::dylib::{DylibFile, DylibLoadKind}; | |
| 13 | +use crate::macho::exports::ExportKind; | |
| 12 | 14 | use crate::macho::constants::*; |
| 13 | 15 | use crate::macho::reader::{ |
| 14 | - BuildVersionCmd, DylibCmd, DysymtabCmd, LinkEditDataCmd, LoadCommand, MachHeader64, | |
| 15 | - RpathCmd, Section64Header, Segment64, SymtabCmd, | |
| 16 | + BuildVersionCmd, DyldInfoCmd, DylibCmd, DysymtabCmd, LinkEditDataCmd, LoadCommand, | |
| 17 | + MachHeader64, RpathCmd, Section64Header, Segment64, SymtabCmd, | |
| 16 | 18 | }; |
| 17 | 19 | use crate::reloc::{parse_raw_relocs, parse_relocs, Referent, Reloc, RelocKind}; |
| 18 | 20 | use crate::section::InputSection; |
@@ -64,6 +66,75 @@ pub fn dump_archive_file(path: &Path) -> io::Result<()> { | ||
| 64 | 66 | Ok(()) |
| 65 | 67 | } |
| 66 | 68 | |
| 69 | +pub fn dump_dylib_file(path: &Path) -> io::Result<()> { | |
| 70 | + let bytes = std::fs::read(path)?; | |
| 71 | + let dy = DylibFile::parse(path, &bytes) | |
| 72 | + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; | |
| 73 | + let out = io::stdout(); | |
| 74 | + let mut h = out.lock(); | |
| 75 | + writeln!(h, "{}:", path.display())?; | |
| 76 | + writeln!( | |
| 77 | + h, | |
| 78 | + "dylib: install_name={:?} current={} compat={}", | |
| 79 | + dy.install_name, | |
| 80 | + version_str(dy.current_version), | |
| 81 | + version_str(dy.compatibility_version) | |
| 82 | + )?; | |
| 83 | + writeln!(h, "Dependencies ({}):", dy.dependencies.len())?; | |
| 84 | + for d in &dy.dependencies { | |
| 85 | + let kind = match d.kind { | |
| 86 | + DylibLoadKind::Normal => "normal", | |
| 87 | + DylibLoadKind::Weak => "weak", | |
| 88 | + DylibLoadKind::Reexport => "reexport", | |
| 89 | + DylibLoadKind::Upward => "upward", | |
| 90 | + }; | |
| 91 | + writeln!( | |
| 92 | + h, | |
| 93 | + " [{}] {kind:<8} {} current={} compat={}", | |
| 94 | + d.ordinal, | |
| 95 | + d.install_name, | |
| 96 | + version_str(d.current_version), | |
| 97 | + version_str(d.compatibility_version) | |
| 98 | + )?; | |
| 99 | + } | |
| 100 | + if !dy.rpaths.is_empty() { | |
| 101 | + writeln!(h, "Rpaths ({}):", dy.rpaths.len())?; | |
| 102 | + for (i, p) in dy.rpaths.iter().enumerate() { | |
| 103 | + writeln!(h, " [{i}] {p}")?; | |
| 104 | + } | |
| 105 | + } | |
| 106 | + let entries = dy | |
| 107 | + .exports | |
| 108 | + .entries() | |
| 109 | + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; | |
| 110 | + writeln!(h, "Exports ({}):", entries.len())?; | |
| 111 | + for (i, e) in entries.iter().enumerate().take(32) { | |
| 112 | + let rhs = match &e.kind { | |
| 113 | + ExportKind::Regular { address } => format!("addr=0x{address:x}"), | |
| 114 | + ExportKind::ThreadLocal { address } => format!("tlv addr=0x{address:x}"), | |
| 115 | + ExportKind::Absolute { address } => format!("abs=0x{address:x}"), | |
| 116 | + ExportKind::Reexport { | |
| 117 | + ordinal, | |
| 118 | + imported_name, | |
| 119 | + } => { | |
| 120 | + if imported_name.is_empty() { | |
| 121 | + format!("reexport from dylib#{ordinal}") | |
| 122 | + } else { | |
| 123 | + format!("reexport from dylib#{ordinal} as {imported_name}") | |
| 124 | + } | |
| 125 | + } | |
| 126 | + ExportKind::StubAndResolver { stub, resolver } => { | |
| 127 | + format!("stub=0x{stub:x} resolver=0x{resolver:x}") | |
| 128 | + } | |
| 129 | + }; | |
| 130 | + writeln!(h, " [{i}] {:<40} {rhs}", e.name)?; | |
| 131 | + } | |
| 132 | + if entries.len() > 32 { | |
| 133 | + writeln!(h, " ... ({} more)", entries.len() - 32)?; | |
| 134 | + } | |
| 135 | + Ok(()) | |
| 136 | +} | |
| 137 | + | |
| 67 | 138 | pub fn dump_file(path: &Path) -> io::Result<()> { |
| 68 | 139 | let bytes = std::fs::read(path)?; |
| 69 | 140 | let obj = ObjectFile::parse(path, &bytes) |
@@ -108,6 +179,9 @@ fn write_command(w: &mut impl Write, idx: usize, cmd: &LoadCommand) -> io::Resul | ||
| 108 | 179 | LoadCommand::LinkerOptimizationHint(l) => write_linkedit_data(w, l, "LOH"), |
| 109 | 180 | LoadCommand::Dylib(d) => write_dylib(w, d), |
| 110 | 181 | LoadCommand::Rpath(r) => write_rpath(w, r), |
| 182 | + LoadCommand::DyldInfoOnly(d) => write_dyld_info(w, d), | |
| 183 | + LoadCommand::DyldExportsTrie(l) => write_linkedit_data(w, l, "EXPORTS_TRIE"), | |
| 184 | + LoadCommand::DyldChainedFixups(l) => write_linkedit_data(w, l, "CHAINED_FIXUPS"), | |
| 111 | 185 | LoadCommand::Raw { cmd, data, .. } => { |
| 112 | 186 | writeln!( |
| 113 | 187 | w, |
@@ -134,6 +208,23 @@ fn write_rpath(w: &mut impl Write, r: &RpathCmd) -> io::Result<()> { | ||
| 134 | 208 | writeln!(w, " path={:?}", r.path) |
| 135 | 209 | } |
| 136 | 210 | |
| 211 | +fn write_dyld_info(w: &mut impl Write, d: &DyldInfoCmd) -> io::Result<()> { | |
| 212 | + writeln!( | |
| 213 | + w, | |
| 214 | + " rebase=@{}..+{} bind=@{}..+{} weak_bind=@{}..+{} lazy_bind=@{}..+{} export=@{}..+{}", | |
| 215 | + d.rebase_off, | |
| 216 | + d.rebase_size, | |
| 217 | + d.bind_off, | |
| 218 | + d.bind_size, | |
| 219 | + d.weak_bind_off, | |
| 220 | + d.weak_bind_size, | |
| 221 | + d.lazy_bind_off, | |
| 222 | + d.lazy_bind_size, | |
| 223 | + d.export_off, | |
| 224 | + d.export_size | |
| 225 | + ) | |
| 226 | +} | |
| 227 | + | |
| 137 | 228 | fn write_segment64(w: &mut impl Write, s: &Segment64) -> io::Result<()> { |
| 138 | 229 | writeln!( |
| 139 | 230 | w, |
src/lib.rsmodified@@ -38,6 +38,8 @@ pub struct LinkOptions { | ||
| 38 | 38 | pub dump: Option<PathBuf>, |
| 39 | 39 | /// When set, afs-ld dumps the named static archive's structure. |
| 40 | 40 | pub dump_archive: Option<PathBuf>, |
| 41 | + /// When set, afs-ld dumps the named MH_DYLIB's load commands + exports. | |
| 42 | + pub dump_dylib: Option<PathBuf>, | |
| 41 | 43 | } |
| 42 | 44 | |
| 43 | 45 | impl Default for LinkOptions { |
@@ -50,6 +52,7 @@ impl Default for LinkOptions { | ||
| 50 | 52 | kind: OutputKind::Executable, |
| 51 | 53 | dump: None, |
| 52 | 54 | dump_archive: None, |
| 55 | + dump_dylib: None, | |
| 53 | 56 | } |
| 54 | 57 | } |
| 55 | 58 | } |
src/macho/dylib.rsmodified@@ -127,29 +127,46 @@ impl DylibFile { | ||
| 127 | 127 | } |
| 128 | 128 | } |
| 129 | 129 | |
| 130 | -/// The export trie lives in `__LINKEDIT` pointed at either by | |
| 131 | -/// `LC_DYLD_INFO_ONLY.export_off / export_size` (classic) or by | |
| 132 | -/// `LC_DYLD_EXPORTS_TRIE` (chained-fixups era). We accept both; the latter | |
| 133 | -/// shares the `linkedit_data_command` wire shape. | |
| 134 | -/// | |
| 135 | -/// Dylibs built with older toolchains may have no export trie at all (the | |
| 136 | -/// symbol table was the only source of exports). Return an empty trie so | |
| 137 | -/// downstream code doesn't crash. | |
| 130 | +/// Locate the export-trie bytes in either `LC_DYLD_INFO_ONLY.export_*` or | |
| 131 | +/// `LC_DYLD_EXPORTS_TRIE` (chained-fixups era). Dylibs built by older | |
| 132 | +/// toolchains may have no export trie; in that case return an empty trie. | |
| 138 | 133 | fn locate_exports_trie( |
| 139 | 134 | commands: &[LoadCommand], |
| 140 | 135 | file_bytes: &[u8], |
| 141 | 136 | ) -> Result<ExportTrie, ReadError> { |
| 142 | - // Sprint 5 intentionally surfaces only the raw trie bytes; the walker | |
| 143 | - // arrives in the next commit. Placeholder for now: return the empty trie. | |
| 144 | 137 | for cmd in commands { |
| 145 | - if let LoadCommand::LinkerOptimizationHint(_) = cmd { | |
| 146 | - // LC_LOH is not the trie — just here to keep the walk explicit. | |
| 138 | + match cmd { | |
| 139 | + LoadCommand::DyldInfoOnly(d) if d.export_size != 0 => { | |
| 140 | + return trie_slice(file_bytes, d.export_off, d.export_size); | |
| 141 | + } | |
| 142 | + LoadCommand::DyldExportsTrie(l) if l.datasize != 0 => { | |
| 143 | + return trie_slice(file_bytes, l.dataoff, l.datasize); | |
| 144 | + } | |
| 145 | + _ => {} | |
| 147 | 146 | } |
| 148 | 147 | } |
| 149 | - let _ = file_bytes; | |
| 150 | 148 | Ok(ExportTrie::empty()) |
| 151 | 149 | } |
| 152 | 150 | |
| 151 | +fn trie_slice(file_bytes: &[u8], off: u32, size: u32) -> Result<ExportTrie, ReadError> { | |
| 152 | + let start = off as usize; | |
| 153 | + let end = start | |
| 154 | + .checked_add(size as usize) | |
| 155 | + .ok_or(ReadError::Truncated { | |
| 156 | + need: usize::MAX, | |
| 157 | + have: file_bytes.len(), | |
| 158 | + context: "export trie (offset + size overflows)", | |
| 159 | + })?; | |
| 160 | + if end > file_bytes.len() { | |
| 161 | + return Err(ReadError::Truncated { | |
| 162 | + need: end, | |
| 163 | + have: file_bytes.len(), | |
| 164 | + context: "export trie", | |
| 165 | + }); | |
| 166 | + } | |
| 167 | + Ok(ExportTrie::from_bytes(&file_bytes[start..end])) | |
| 168 | +} | |
| 169 | + | |
| 153 | 170 | /// Look up the 1-based ordinal of a dependency by its install name. Used by |
| 154 | 171 | /// Sprint 14's symbol-table writer when encoding each undefined symbol's |
| 155 | 172 | /// two-level-namespace library ordinal into its `n_desc` high byte. |
src/macho/reader.rsmodified@@ -134,6 +134,14 @@ pub enum LoadCommand { | ||
| 134 | 134 | Dylib(DylibCmd), |
| 135 | 135 | /// `LC_RPATH` — one runtime-search path per entry. |
| 136 | 136 | Rpath(RpathCmd), |
| 137 | + /// `LC_DYLD_INFO_ONLY` — classic locator for rebase/bind/lazy/weak/export | |
| 138 | + /// streams in `__LINKEDIT`. | |
| 139 | + DyldInfoOnly(DyldInfoCmd), | |
| 140 | + /// `LC_DYLD_EXPORTS_TRIE` — the modern chained-fixups alternative that | |
| 141 | + /// holds only the export trie (paired with `LC_DYLD_CHAINED_FIXUPS`). | |
| 142 | + DyldExportsTrie(LinkEditDataCmd), | |
| 143 | + /// `LC_DYLD_CHAINED_FIXUPS` — pointer to the chained-fixups blob. | |
| 144 | + DyldChainedFixups(LinkEditDataCmd), | |
| 137 | 145 | /// A load command whose payload we haven't decoded yet. Preserves bytes |
| 138 | 146 | /// verbatim for byte-level round-trip. |
| 139 | 147 | Raw { cmd: u32, cmdsize: u32, data: Vec<u8> }, |
@@ -149,6 +157,9 @@ impl LoadCommand { | ||
| 149 | 157 | LoadCommand::LinkerOptimizationHint(_) => LC_LINKER_OPTIMIZATION_HINT, |
| 150 | 158 | LoadCommand::Dylib(d) => d.cmd, |
| 151 | 159 | LoadCommand::Rpath(_) => LC_RPATH, |
| 160 | + LoadCommand::DyldInfoOnly(_) => LC_DYLD_INFO_ONLY, | |
| 161 | + LoadCommand::DyldExportsTrie(_) => LC_DYLD_EXPORTS_TRIE, | |
| 162 | + LoadCommand::DyldChainedFixups(_) => LC_DYLD_CHAINED_FIXUPS, | |
| 152 | 163 | LoadCommand::Raw { cmd, .. } => *cmd, |
| 153 | 164 | } |
| 154 | 165 | } |
@@ -162,6 +173,9 @@ impl LoadCommand { | ||
| 162 | 173 | LoadCommand::LinkerOptimizationHint(_) => LinkEditDataCmd::WIRE_SIZE, |
| 163 | 174 | LoadCommand::Dylib(d) => d.wire_size(), |
| 164 | 175 | LoadCommand::Rpath(r) => r.wire_size(), |
| 176 | + LoadCommand::DyldInfoOnly(_) => DyldInfoCmd::WIRE_SIZE, | |
| 177 | + LoadCommand::DyldExportsTrie(_) => LinkEditDataCmd::WIRE_SIZE, | |
| 178 | + LoadCommand::DyldChainedFixups(_) => LinkEditDataCmd::WIRE_SIZE, | |
| 165 | 179 | LoadCommand::Raw { cmdsize, .. } => *cmdsize, |
| 166 | 180 | } |
| 167 | 181 | } |
@@ -431,6 +445,17 @@ fn decode_command(cmd: u32, cmdsize: u32, payload: &[u8]) -> Result<LoadCommand, | ||
| 431 | 445 | | LC_REEXPORT_DYLIB |
| 432 | 446 | | LC_LOAD_UPWARD_DYLIB => Ok(LoadCommand::Dylib(DylibCmd::parse(cmd, cmdsize, payload)?)), |
| 433 | 447 | LC_RPATH => Ok(LoadCommand::Rpath(RpathCmd::parse(cmdsize, payload)?)), |
| 448 | + LC_DYLD_INFO_ONLY => Ok(LoadCommand::DyldInfoOnly(DyldInfoCmd::parse(cmdsize, payload)?)), | |
| 449 | + LC_DYLD_EXPORTS_TRIE => Ok(LoadCommand::DyldExportsTrie(LinkEditDataCmd::parse( | |
| 450 | + LC_DYLD_EXPORTS_TRIE, | |
| 451 | + cmdsize, | |
| 452 | + payload, | |
| 453 | + )?)), | |
| 454 | + LC_DYLD_CHAINED_FIXUPS => Ok(LoadCommand::DyldChainedFixups(LinkEditDataCmd::parse( | |
| 455 | + LC_DYLD_CHAINED_FIXUPS, | |
| 456 | + cmdsize, | |
| 457 | + payload, | |
| 458 | + )?)), | |
| 434 | 459 | _ => Ok(LoadCommand::Raw { |
| 435 | 460 | cmd, |
| 436 | 461 | cmdsize, |
@@ -452,6 +477,9 @@ pub fn write_commands(cmds: &[LoadCommand], out: &mut Vec<u8>) { | ||
| 452 | 477 | LoadCommand::LinkerOptimizationHint(l) => l.write(LC_LINKER_OPTIMIZATION_HINT, out), |
| 453 | 478 | LoadCommand::Dylib(d) => d.write(out), |
| 454 | 479 | LoadCommand::Rpath(r) => r.write(out), |
| 480 | + LoadCommand::DyldInfoOnly(d) => d.write(out), | |
| 481 | + LoadCommand::DyldExportsTrie(l) => l.write(LC_DYLD_EXPORTS_TRIE, out), | |
| 482 | + LoadCommand::DyldChainedFixups(l) => l.write(LC_DYLD_CHAINED_FIXUPS, out), | |
| 455 | 483 | LoadCommand::Raw { cmd, cmdsize, data } => { |
| 456 | 484 | out.extend_from_slice(&cmd.to_le_bytes()); |
| 457 | 485 | out.extend_from_slice(&cmdsize.to_le_bytes()); |
@@ -865,6 +893,78 @@ fn pad8(n: usize) -> usize { | ||
| 865 | 893 | (n + 7) & !7 |
| 866 | 894 | } |
| 867 | 895 | |
| 896 | +// --------------------------------------------------------------------------- | |
| 897 | +// LC_DYLD_INFO_ONLY — classic locator for rebase/bind/lazy/weak/export. | |
| 898 | +// --------------------------------------------------------------------------- | |
| 899 | + | |
| 900 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] | |
| 901 | +pub struct DyldInfoCmd { | |
| 902 | + pub rebase_off: u32, | |
| 903 | + pub rebase_size: u32, | |
| 904 | + pub bind_off: u32, | |
| 905 | + pub bind_size: u32, | |
| 906 | + pub weak_bind_off: u32, | |
| 907 | + pub weak_bind_size: u32, | |
| 908 | + pub lazy_bind_off: u32, | |
| 909 | + pub lazy_bind_size: u32, | |
| 910 | + pub export_off: u32, | |
| 911 | + pub export_size: u32, | |
| 912 | +} | |
| 913 | + | |
| 914 | +impl DyldInfoCmd { | |
| 915 | + pub const WIRE_SIZE: u32 = 8 + 40; | |
| 916 | + | |
| 917 | + pub fn parse(cmdsize: u32, payload: &[u8]) -> Result<Self, ReadError> { | |
| 918 | + if cmdsize != Self::WIRE_SIZE { | |
| 919 | + return Err(ReadError::BadCmdsize { | |
| 920 | + cmd: LC_DYLD_INFO_ONLY, | |
| 921 | + cmdsize, | |
| 922 | + at_offset: 0, | |
| 923 | + reason: "LC_DYLD_INFO_ONLY cmdsize must be 48", | |
| 924 | + }); | |
| 925 | + } | |
| 926 | + if payload.len() < 40 { | |
| 927 | + return Err(ReadError::Truncated { | |
| 928 | + need: 40, | |
| 929 | + have: payload.len(), | |
| 930 | + context: "dyld_info_command", | |
| 931 | + }); | |
| 932 | + } | |
| 933 | + let g = |i: usize| u32_le(&payload[i * 4..(i + 1) * 4]); | |
| 934 | + Ok(DyldInfoCmd { | |
| 935 | + rebase_off: g(0), | |
| 936 | + rebase_size: g(1), | |
| 937 | + bind_off: g(2), | |
| 938 | + bind_size: g(3), | |
| 939 | + weak_bind_off: g(4), | |
| 940 | + weak_bind_size: g(5), | |
| 941 | + lazy_bind_off: g(6), | |
| 942 | + lazy_bind_size: g(7), | |
| 943 | + export_off: g(8), | |
| 944 | + export_size: g(9), | |
| 945 | + }) | |
| 946 | + } | |
| 947 | + | |
| 948 | + pub fn write(&self, out: &mut Vec<u8>) { | |
| 949 | + out.extend_from_slice(&LC_DYLD_INFO_ONLY.to_le_bytes()); | |
| 950 | + out.extend_from_slice(&Self::WIRE_SIZE.to_le_bytes()); | |
| 951 | + for v in [ | |
| 952 | + self.rebase_off, | |
| 953 | + self.rebase_size, | |
| 954 | + self.bind_off, | |
| 955 | + self.bind_size, | |
| 956 | + self.weak_bind_off, | |
| 957 | + self.weak_bind_size, | |
| 958 | + self.lazy_bind_off, | |
| 959 | + self.lazy_bind_size, | |
| 960 | + self.export_off, | |
| 961 | + self.export_size, | |
| 962 | + ] { | |
| 963 | + out.extend_from_slice(&v.to_le_bytes()); | |
| 964 | + } | |
| 965 | + } | |
| 966 | +} | |
| 967 | + | |
| 868 | 968 | // --------------------------------------------------------------------------- |
| 869 | 969 | // linkedit_data_command — shared wire format for LC_LINKER_OPTIMIZATION_HINT, |
| 870 | 970 | // LC_FUNCTION_STARTS, LC_DATA_IN_CODE, LC_CODE_SIGNATURE, LC_DYLD_EXPORTS_TRIE, |
src/main.rsmodified@@ -33,6 +33,16 @@ fn main() -> ExitCode { | ||
| 33 | 33 | }; |
| 34 | 34 | } |
| 35 | 35 | |
| 36 | + if let Some(path) = &opts.dump_dylib { | |
| 37 | + return match dump::dump_dylib_file(path) { | |
| 38 | + Ok(()) => ExitCode::SUCCESS, | |
| 39 | + Err(e) => { | |
| 40 | + diag::error(&format!("{}: {}", path.display(), e)); | |
| 41 | + ExitCode::from(1) | |
| 42 | + } | |
| 43 | + }; | |
| 44 | + } | |
| 45 | + | |
| 36 | 46 | match Linker::run(&opts) { |
| 37 | 47 | Ok(()) => ExitCode::SUCCESS, |
| 38 | 48 | Err(LinkError::NoInputs) => { |
tests/dylib_integration.rsadded@@ -0,0 +1,103 @@ | ||
| 1 | +//! Sprint 5 real-world gate: build a tiny dylib with `clang`, parse it via | |
| 2 | +//! `DylibFile`, and confirm: | |
| 3 | +//! | |
| 4 | +//! - `install_name` is what `-install_name` requested; | |
| 5 | +//! - the exported symbol surfaces in the trie; | |
| 6 | +//! - at least one `LC_LOAD_DYLIB` dependency is present (every clang-linked | |
| 7 | +//! dylib picks up libSystem). | |
| 8 | +//! | |
| 9 | +//! Skipped if `xcrun clang` isn't available or fails for any reason. | |
| 10 | + | |
| 11 | +use std::path::PathBuf; | |
| 12 | +use std::process::Command; | |
| 13 | + | |
| 14 | +use afs_ld::macho::dylib::DylibFile; | |
| 15 | +use afs_ld::macho::exports::ExportKind; | |
| 16 | + | |
| 17 | +fn build_test_dylib(src: &str, out: &PathBuf) -> Result<(), String> { | |
| 18 | + let mut child = Command::new("xcrun") | |
| 19 | + .args([ | |
| 20 | + "--sdk", | |
| 21 | + "macosx", | |
| 22 | + "clang", | |
| 23 | + "-x", | |
| 24 | + "c", | |
| 25 | + "-arch", | |
| 26 | + "arm64", | |
| 27 | + "-shared", | |
| 28 | + "-o", | |
| 29 | + ]) | |
| 30 | + .arg(out) | |
| 31 | + .arg("-install_name") | |
| 32 | + .arg("@rpath/libafsldtest.dylib") | |
| 33 | + .arg("-") | |
| 34 | + .stdin(std::process::Stdio::piped()) | |
| 35 | + .stdout(std::process::Stdio::piped()) | |
| 36 | + .stderr(std::process::Stdio::piped()) | |
| 37 | + .spawn() | |
| 38 | + .map_err(|e| format!("spawn: {e}"))?; | |
| 39 | + use std::io::Write; | |
| 40 | + child | |
| 41 | + .stdin | |
| 42 | + .as_mut() | |
| 43 | + .unwrap() | |
| 44 | + .write_all(src.as_bytes()) | |
| 45 | + .map_err(|e| format!("write: {e}"))?; | |
| 46 | + let out = child.wait_with_output().map_err(|e| format!("wait: {e}"))?; | |
| 47 | + if !out.status.success() { | |
| 48 | + return Err(format!( | |
| 49 | + "clang failed: {}", | |
| 50 | + String::from_utf8_lossy(&out.stderr) | |
| 51 | + )); | |
| 52 | + } | |
| 53 | + Ok(()) | |
| 54 | +} | |
| 55 | + | |
| 56 | +#[test] | |
| 57 | +fn small_clang_dylib_parses_and_exports_function() { | |
| 58 | + let which = Command::new("xcrun").arg("-f").arg("clang").output(); | |
| 59 | + if !matches!(which, Ok(o) if o.status.success()) { | |
| 60 | + eprintln!("skipping: xcrun clang unavailable"); | |
| 61 | + return; | |
| 62 | + } | |
| 63 | + | |
| 64 | + use std::sync::atomic::{AtomicUsize, Ordering}; | |
| 65 | + static SEQ: AtomicUsize = AtomicUsize::new(0); | |
| 66 | + let out_path = std::env::temp_dir().join(format!( | |
| 67 | + "afs-ld-dylib-{}-{}.dylib", | |
| 68 | + std::process::id(), | |
| 69 | + SEQ.fetch_add(1, Ordering::Relaxed) | |
| 70 | + )); | |
| 71 | + | |
| 72 | + let src = r#" | |
| 73 | + int afsld_answer(int x) { return x + 42; } | |
| 74 | + "#; | |
| 75 | + if let Err(e) = build_test_dylib(src, &out_path) { | |
| 76 | + eprintln!("skipping: clang could not build test dylib: {e}"); | |
| 77 | + return; | |
| 78 | + } | |
| 79 | + | |
| 80 | + let bytes = std::fs::read(&out_path).expect("read test dylib"); | |
| 81 | + let dy = DylibFile::parse(&out_path, &bytes).expect("parse test dylib"); | |
| 82 | + | |
| 83 | + assert_eq!(dy.install_name, "@rpath/libafsldtest.dylib"); | |
| 84 | + | |
| 85 | + // clang-linked dylibs pull libSystem in as a Normal dependency. | |
| 86 | + assert!( | |
| 87 | + dy.dependencies | |
| 88 | + .iter() | |
| 89 | + .any(|d| d.install_name.contains("libSystem")), | |
| 90 | + "expected a libSystem dependency; got {:?}", | |
| 91 | + dy.dependencies | |
| 92 | + ); | |
| 93 | + | |
| 94 | + let entries = dy.exports.entries().expect("decode exports"); | |
| 95 | + let name = "_afsld_answer"; | |
| 96 | + let found = entries.iter().find(|e| e.name == name).unwrap_or_else(|| { | |
| 97 | + let exported: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); | |
| 98 | + panic!("expected {name} in exports; got {exported:?}") | |
| 99 | + }); | |
| 100 | + assert!(matches!(found.kind, ExportKind::Regular { .. })); | |
| 101 | + | |
| 102 | + let _ = std::fs::remove_file(&out_path); | |
| 103 | +} | |