1use std::collections::BTreeMap;
8
9use crate::elf::ElfBackend;
10use crate::macho::MachOBackend;
11use crate::pe::PeCoffBackend;
12use crate::wasm::WasmBackend;
13use crate::{
14 ArtifactArchiveMember, ArtifactBackend, ArtifactCapabilities, ArtifactError,
15 ArtifactFingerprint, ArtifactFormat, ArtifactIr, detect_format,
16};
17use object::read::archive::ArchiveFile;
18
19#[derive(Debug, Default, Clone, Copy)]
21pub struct ArchiveBackend;
22
23impl ArtifactBackend for ArchiveBackend {
24 fn format(&self) -> ArtifactFormat {
25 ArtifactFormat::Archive
26 }
27
28 fn detects(&self, bytes: &[u8]) -> bool {
29 bytes.starts_with(b"!<arch>\n") || bytes.starts_with(b"!<thin>\n")
30 }
31
32 fn parse(&self, bytes: &[u8]) -> Result<ArtifactIr, ArtifactError> {
33 let archive = ArchiveFile::parse(bytes).map_err(|error| malformed(error.to_string()))?;
34 let mut ir = ArtifactIr::empty(ArtifactFormat::Archive, bytes);
35 for member in archive.members() {
36 let member = match member {
37 Ok(member) => member,
38 Err(error) => {
39 ir.archive_members.push(incomplete_member(
40 "<truncated archive member>".to_owned(),
41 u64::try_from(bytes.len()).unwrap_or(u64::MAX),
42 0,
43 &error.to_string(),
44 ));
45 break;
46 }
47 };
48 let name = String::from_utf8_lossy(member.name()).into_owned();
49 let (offset, size) = member.file_range();
50 let thin = member.is_thin();
51 let data = match member.data(bytes) {
52 Ok(data) => data,
53 Err(error) => {
54 ir.archive_members.push(incomplete_member(
55 name,
56 offset,
57 size,
58 &error.to_string(),
59 ));
60 continue;
61 }
62 };
63 let fingerprint = if thin {
64 ArtifactFingerprint::from_content("thin-archive-member", name.as_bytes())
68 } else {
69 ArtifactFingerprint::from_content("archive-member", data)
70 };
71 let format = detect_format(data);
72 let mut provenance = ArtifactArchiveMember {
73 name,
74 fingerprint,
75 offset,
76 size,
77 format,
78 thin,
79 parse_error: None,
80 };
81 if thin {
82 provenance.parse_error = Some(
83 "thin archive member has no local bytes; external paths are never followed"
84 .to_owned(),
85 );
86 } else if let Some(format) = format {
87 match parse_member(format, data) {
88 Ok(member_ir) => merge_member(&mut ir, member_ir, &provenance),
89 Err(error) => provenance.parse_error = Some(error.to_string()),
90 }
91 } else {
92 provenance.parse_error = Some("member format is not supported".to_owned());
93 }
94 ir.archive_members.push(provenance);
95 }
96 ir.capabilities = ArtifactCapabilities {
97 symbols: !ir.symbols.is_empty(),
98 call_graph: !ir.calls.is_empty(),
99 source_mapping: !ir.source_mappings.is_empty(),
100 debug_info_unreadable: ir.capabilities.debug_info_unreadable,
101 normalized_duplicates: !ir.symbols.is_empty()
102 && ir.symbols.iter().all(|symbol| symbol.normalized.is_some()),
103 independent_data_segments: false,
104 relocations: !ir.relocations.is_empty(),
105 data_segments: !ir.data_segments.is_empty(),
106 };
107 Ok(ir)
108 }
109
110 fn capabilities(&self) -> ArtifactCapabilities {
111 ArtifactCapabilities {
112 symbols: true,
113 call_graph: true,
114 source_mapping: true,
115 debug_info_unreadable: false,
116 normalized_duplicates: false,
117 independent_data_segments: false,
118 relocations: true,
119 data_segments: true,
120 }
121 }
122}
123
124fn incomplete_member(name: String, offset: u64, size: u64, error: &str) -> ArtifactArchiveMember {
126 let identity = format!("{name}:{offset}:{size}");
127 ArtifactArchiveMember {
128 name,
129 fingerprint: ArtifactFingerprint::from_content(
130 "archive-incomplete-member",
131 identity.as_bytes(),
132 ),
133 offset,
134 size,
135 format: None,
136 thin: false,
137 parse_error: Some(format!("truncated or unreadable archive member: {error}")),
138 }
139}
140
141fn parse_member(format: ArtifactFormat, bytes: &[u8]) -> Result<ArtifactIr, ArtifactError> {
142 match format {
143 ArtifactFormat::Wasm => WasmBackend.parse(bytes),
144 ArtifactFormat::Elf => ElfBackend.parse(bytes),
145 ArtifactFormat::MachO => MachOBackend.parse(bytes),
146 ArtifactFormat::PeCoff => PeCoffBackend.parse(bytes),
147 ArtifactFormat::Archive => Err(ArtifactError::Unsupported {
148 format: ArtifactFormat::Archive,
149 }),
150 }
151}
152
153fn merge_member(archive: &mut ArtifactIr, member: ArtifactIr, provenance: &ArtifactArchiveMember) {
155 archive.capabilities.debug_info_unreadable |= member.capabilities.debug_info_unreadable;
156 let prefix = format!("{}:", provenance.name);
157 archive
158 .sections
159 .extend(member.sections.into_iter().map(|mut section| {
160 section.name = Some(format!("{prefix}{}", section.name.unwrap_or_default()));
161 section.offset = provenance.offset.saturating_add(section.offset);
162 section
163 }));
164 archive.imports.extend(member.imports);
165 let mut fingerprints = BTreeMap::new();
166 for mut symbol in member.symbols {
167 let original = symbol.fingerprint;
168 let fingerprint =
169 archive_member_fingerprint("archive-symbol", provenance.fingerprint, original);
170 fingerprints.insert(original, fingerprint);
171 symbol.fingerprint = fingerprint;
172 symbol.section = None;
173 symbol.offset = provenance.offset.saturating_add(symbol.offset);
174 archive.symbols.push(symbol);
175 }
176 archive.entry_points.extend(
177 member
178 .entry_points
179 .into_iter()
180 .filter_map(|value| fingerprints.get(&value).copied()),
181 );
182 archive.indirect_references.extend(
183 member
184 .indirect_references
185 .into_iter()
186 .filter_map(|value| fingerprints.get(&value).copied()),
187 );
188 archive
189 .calls
190 .extend(member.calls.into_iter().filter_map(|mut call| {
191 let caller = fingerprints.get(&call.caller).copied()?;
192 call.caller = caller;
193 call.target = call
194 .target
195 .and_then(|target| fingerprints.get(&target).copied());
196 Some(call)
197 }));
198 archive
199 .relocations
200 .extend(member.relocations.into_iter().map(|mut relocation| {
201 relocation.section = None;
202 relocation.offset = provenance.offset.saturating_add(relocation.offset);
203 relocation
204 }));
205 archive.source_mappings.extend(member.source_mappings);
206 archive
207 .source_mappings
208 .sort_by(|left, right| left.uri.cmp(&right.uri));
209 archive.source_mappings.dedup();
210 archive
211 .data_segments
212 .extend(member.data_segments.into_iter().map(|mut data| {
213 data.fingerprint = archive_member_fingerprint(
214 "archive-data",
215 provenance.fingerprint,
216 data.fingerprint,
217 );
218 data.section = None;
219 data.offset = provenance.offset.saturating_add(data.offset);
220 data
221 }));
222 archive.entry_points.sort();
223 archive.entry_points.dedup();
224 archive.indirect_references.sort();
225 archive.indirect_references.dedup();
226}
227
228fn archive_member_fingerprint(
229 domain: &str,
230 member: ArtifactFingerprint,
231 child: ArtifactFingerprint,
232) -> ArtifactFingerprint {
233 let mut bytes = Vec::with_capacity(32);
234 bytes.extend(member.as_bytes());
235 bytes.extend(child.as_bytes());
236 ArtifactFingerprint::from_content(domain, &bytes)
237}
238
239const fn malformed(message: String) -> ArtifactError {
240 ArtifactError::Malformed {
241 format: ArtifactFormat::Archive,
242 message,
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 #![allow(clippy::expect_used, clippy::unwrap_used)]
249
250 use super::*;
251 use object::write::{Object as WriteObject, StandardSection, Symbol, SymbolSection};
252 use object::{Architecture, BinaryFormat, Endianness, SymbolFlags, SymbolKind, SymbolScope};
253 use proptest::prelude::*;
254
255 fn coff_member(name: &[u8]) -> Vec<u8> {
256 let mut object =
257 WriteObject::new(BinaryFormat::Coff, Architecture::X86_64, Endianness::Little);
258 let text = object.section_id(StandardSection::Text);
259 let offset = object.append_section_data(text, &[0x90, 0xc3], 1);
260 object.add_symbol(Symbol {
261 name: name.to_vec(),
262 value: offset,
263 size: 2,
264 kind: SymbolKind::Text,
265 scope: SymbolScope::Dynamic,
266 weak: false,
267 section: SymbolSection::Section(text),
268 flags: SymbolFlags::None,
269 });
270 object.write().expect("write COFF member")
271 }
272
273 fn archive_member(name: &str, bytes: &[u8]) -> Vec<u8> {
274 let mut member = Vec::new();
275 let name = format!("{name}/");
276 member.extend(format!("{name:<16}").as_bytes());
277 member.extend(b"0 0 0 100644 ");
278 member.extend(format!("{:<10}", bytes.len()).as_bytes());
279 member.extend(b"`\n");
280 member.extend(bytes);
281 if bytes.len() % 2 != 0 {
282 member.push(b'\n');
283 }
284 member
285 }
286
287 fn archive_fixture() -> Vec<u8> {
288 let first = coff_member(b"left");
289 let second = coff_member(b"right");
290 let mut archive = b"!<arch>\n".to_vec();
291 archive.extend(archive_member("left.obj", &first));
292 archive.extend(archive_member("right.obj", &second));
293 archive
294 }
295
296 fn thin_archive_fixture() -> Vec<u8> {
297 let mut archive = b"!<thin>\n".to_vec();
298 archive.extend(archive_member("left.obj", b""));
299 archive.extend(archive_member("right.obj", b""));
300 archive
301 }
302
303 #[test]
304 fn delegates_local_coff_members_without_executing_them() {
305 let ir = ArchiveBackend
306 .parse(&archive_fixture())
307 .expect("parse archive fixture");
308 assert_eq!(ir.format, ArtifactFormat::Archive);
309 assert_eq!(ir.archive_members.len(), 2, "{ir:#?}");
310 assert!(
311 ir.archive_members
312 .iter()
313 .all(|member| member.parse_error.is_none())
314 );
315 assert_eq!(ir.symbols.len(), 2, "{ir:#?}");
316 assert_ne!(ir.symbols[0].fingerprint, ir.symbols[1].fingerprint);
317 assert!(ir.capabilities.symbols);
318 }
319
320 #[test]
321 fn a_truncated_later_member_keeps_the_earlier_members_available() {
322 let mut bytes = archive_fixture();
323 bytes.truncate(bytes.len().saturating_sub(1));
324
325 let ir = ArchiveBackend
326 .parse(&bytes)
327 .expect("a readable archive prefix remains useful");
328 assert!(
329 ir.archive_members
330 .iter()
331 .any(|member| member.name == "left.obj" && member.parse_error.is_none()),
332 "the complete leading member remains available: {:#?}",
333 ir.archive_members
334 );
335 assert!(
336 ir.archive_members.iter().any(|member| {
337 member
338 .parse_error
339 .as_deref()
340 .is_some_and(|error| error.contains("truncated or unreadable"))
341 }),
342 "the unreadable tail is accounted for: {:#?}",
343 ir.archive_members
344 );
345 }
346
347 #[test]
348 fn thin_members_keep_distinct_path_based_fingerprints() {
349 let ir = ArchiveBackend
350 .parse(&thin_archive_fixture())
351 .expect("parse thin archive manifest");
352 assert_eq!(ir.archive_members.len(), 2, "{ir:#?}");
353 assert!(ir.archive_members.iter().all(|member| member.thin));
354 assert_ne!(
355 ir.archive_members[0].fingerprint,
356 ir.archive_members[1].fingerprint
357 );
358 }
359
360 #[test]
361 fn other_bytes_do_not_claim_the_backend() {
362 assert!(!ArchiveBackend.detects(b"not an archive"));
363 assert!(matches!(
364 ArchiveBackend.parse(b"not an archive"),
365 Err(ArtifactError::Malformed { .. })
366 ));
367 }
368
369 proptest::proptest! {
370 #[test]
371 fn arbitrary_bytes_never_panic(bytes in proptest::collection::vec(any::<u8>(), 0..4096)) {
372 let _ = ArchiveBackend.parse(&bytes);
373 }
374 }
375}