1use core::fmt;
12
13use serde::{Deserialize, Deserializer, Serialize, Serializer};
14use thiserror::Error;
15
16#[cfg(feature = "archive")]
17pub mod archive;
18pub mod dwarf;
19#[cfg(feature = "elf")]
20pub mod elf;
21#[cfg(feature = "macho")]
22pub mod macho;
23pub mod metrics;
24pub mod native;
25#[cfg(feature = "pe")]
26pub mod pe;
27pub mod symbols;
28#[cfg(feature = "wasm")]
29pub mod wasm;
30pub mod x86;
31
32pub const ARTIFACT_IR_SCHEMA_VERSION: &str = "artifact-ir-v1";
34
35pub const ARTIFACT_FINGERPRINT_VERSION: &str = "artifact-fingerprint-v1";
37
38pub mod base64_bytes {
42 use base64::Engine;
43 use serde::{Deserialize, Deserializer, Serializer};
44
45 pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
51 where
52 S: Serializer,
53 {
54 serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(bytes))
55 }
56
57 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
63 where
64 D: Deserializer<'de>,
65 {
66 let encoded = String::deserialize(deserializer)?;
67 base64::engine::general_purpose::STANDARD
68 .decode(encoded)
69 .map_err(serde::de::Error::custom)
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
75pub enum ArtifactFormat {
76 Wasm,
78 Elf,
80 MachO,
82 PeCoff,
84 Archive,
86}
87
88impl ArtifactFormat {
89 #[must_use]
91 pub const fn name(self) -> &'static str {
92 match self {
93 Self::Wasm => "wasm",
94 Self::Elf => "elf",
95 Self::MachO => "macho",
96 Self::PeCoff => "pe-coff",
97 Self::Archive => "archive",
98 }
99 }
100}
101
102impl fmt::Display for ArtifactFormat {
103 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104 formatter.write_str(self.name())
105 }
106}
107
108impl Serialize for ArtifactFormat {
109 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
110 where
111 S: Serializer,
112 {
113 serializer.serialize_str(self.name())
114 }
115}
116
117impl<'de> Deserialize<'de> for ArtifactFormat {
118 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
119 where
120 D: Deserializer<'de>,
121 {
122 match String::deserialize(deserializer)?.as_str() {
123 "wasm" => Ok(Self::Wasm),
124 "elf" => Ok(Self::Elf),
125 "macho" => Ok(Self::MachO),
126 "pe-coff" => Ok(Self::PeCoff),
127 "archive" => Ok(Self::Archive),
128 other => Err(serde::de::Error::unknown_variant(
129 other,
130 &["wasm", "elf", "macho", "pe-coff", "archive"],
131 )),
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
138#[allow(clippy::struct_excessive_bools)] pub struct ArtifactCapabilities {
140 pub symbols: bool,
142 pub call_graph: bool,
144 pub source_mapping: bool,
146 pub debug_info_unreadable: bool,
148 pub normalized_duplicates: bool,
150 pub independent_data_segments: bool,
152 pub relocations: bool,
154 pub data_segments: bool,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
160pub struct ArtifactFingerprint([u8; 16]);
161
162impl ArtifactFingerprint {
163 #[must_use]
166 pub fn from_content(domain: &str, bytes: &[u8]) -> Self {
167 let mut hasher = blake3::Hasher::new();
168 hasher.update(ARTIFACT_FINGERPRINT_VERSION.as_bytes());
169 hasher.update(&(domain.len() as u64).to_le_bytes());
170 hasher.update(domain.as_bytes());
171 hasher.update(&(bytes.len() as u64).to_le_bytes());
172 hasher.update(bytes);
173 let mut fingerprint = [0_u8; 16];
174 fingerprint.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
175 Self(fingerprint)
176 }
177
178 #[must_use]
180 pub const fn as_bytes(self) -> [u8; 16] {
181 self.0
182 }
183
184 #[must_use]
186 pub fn to_hex(self) -> String {
187 self.to_string()
188 }
189}
190
191impl fmt::Display for ArtifactFingerprint {
192 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
193 for byte in self.0 {
194 write!(formatter, "{byte:02x}")?;
195 }
196 Ok(())
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct ArtifactIr {
203 pub schema_version: String,
205 pub format: ArtifactFormat,
207 pub capabilities: ArtifactCapabilities,
209 pub fingerprint: ArtifactFingerprint,
211 pub observed_bytes: u64,
213 pub architecture: Option<String>,
218 pub skipped_architectures: Vec<String>,
223 pub sections: Vec<ArtifactSection>,
225 pub archive_members: Vec<ArtifactArchiveMember>,
231 pub imports: Vec<ArtifactImport>,
233 pub symbols: Vec<ArtifactSymbol>,
235 pub entry_points: Vec<ArtifactFingerprint>,
237 pub indirect_references: Vec<ArtifactFingerprint>,
240 pub calls: Vec<ArtifactCall>,
242 pub relocations: Vec<ArtifactRelocation>,
244 pub source_mappings: Vec<ArtifactSourceMapping>,
246 pub data_segments: Vec<ArtifactDataSegment>,
248}
249
250impl ArtifactIr {
251 #[must_use]
253 pub fn empty(format: ArtifactFormat, bytes: &[u8]) -> Self {
254 Self {
255 schema_version: ARTIFACT_IR_SCHEMA_VERSION.to_owned(),
256 format,
257 capabilities: ArtifactCapabilities::default(),
258 fingerprint: ArtifactFingerprint::from_content("artifact", bytes),
259 observed_bytes: bytes.len() as u64,
260 architecture: None,
261 skipped_architectures: Vec::new(),
262 sections: Vec::new(),
263 archive_members: Vec::new(),
264 imports: Vec::new(),
265 symbols: Vec::new(),
266 entry_points: Vec::new(),
267 indirect_references: Vec::new(),
268 calls: Vec::new(),
269 relocations: Vec::new(),
270 source_mappings: Vec::new(),
271 data_segments: Vec::new(),
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278pub struct ArtifactArchiveMember {
279 pub name: String,
281 pub fingerprint: ArtifactFingerprint,
283 pub offset: u64,
285 pub size: u64,
287 pub format: Option<ArtifactFormat>,
289 pub thin: bool,
291 pub parse_error: Option<String>,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300pub struct ArtifactSection {
301 pub name: Option<String>,
303 pub offset: u64,
305 pub size: u64,
307 pub executable: bool,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313pub struct ArtifactImport {
314 pub module: Option<String>,
316 pub name: Option<String>,
318 pub kind: ArtifactImportKind,
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(rename_all = "kebab-case")]
325pub enum ArtifactImportKind {
326 Function,
328 Table,
330 Memory,
332 Global,
334 Tag,
336 Other,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct ArtifactSymbol {
343 pub fingerprint: ArtifactFingerprint,
345 pub name: Option<String>,
347 pub exported: bool,
349 pub section: Option<u32>,
351 pub offset: u64,
353 pub size: u64,
355 pub size_inferred: bool,
357 #[serde(with = "base64_bytes")]
359 pub code: Vec<u8>,
360 pub normalized: Option<NormalizedInstructions>,
362 pub inline_stack: Vec<ArtifactInlineFrame>,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368pub struct ArtifactInlineFrame {
369 pub evidence_kind: ArtifactSourceLocationEvidenceKind,
371 pub source: String,
373 pub line: Option<u32>,
375 pub column: Option<u32>,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
385#[serde(rename_all = "snake_case")]
386pub enum ArtifactSourceLocationEvidenceKind {
387 Dwarf,
389 Pdb,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395pub struct NormalizedInstructions {
396 pub version: String,
398 pub bytes: Vec<u8>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct ArtifactCall {
405 pub caller: ArtifactFingerprint,
407 pub target: Option<ArtifactFingerprint>,
409 pub unresolved: Option<UnresolvedCall>,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct ArtifactRelocation {
416 pub section: Option<u32>,
418 pub offset: u64,
420 pub kind: String,
422 pub target: Option<String>,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428pub struct ArtifactSourceMapping {
429 pub uri: String,
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(rename_all = "kebab-case")]
436pub enum UnresolvedCall {
437 IndirectTable,
439 ExternalImport,
442 NativeIndirect,
444 MissingRelocation,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
450pub struct ArtifactDataSegment {
451 pub fingerprint: ArtifactFingerprint,
453 pub section: Option<u32>,
455 pub offset: u64,
457 #[serde(with = "base64_bytes")]
459 pub bytes: Vec<u8>,
460}
461
462#[derive(Debug, Error, PartialEq, Eq)]
464pub enum ArtifactError {
465 #[error("expected {expected} input")]
467 WrongFormat {
468 expected: ArtifactFormat,
470 },
471 #[error("malformed {format} input: {message}")]
473 Malformed {
474 format: ArtifactFormat,
476 message: String,
478 },
479 #[error("{format} is recognised but not supported")]
481 Unsupported {
482 format: ArtifactFormat,
484 },
485}
486
487pub trait ArtifactBackend: Send + Sync {
489 fn format(&self) -> ArtifactFormat;
491
492 fn detects(&self, bytes: &[u8]) -> bool;
494
495 fn parse(&self, bytes: &[u8]) -> Result<ArtifactIr, ArtifactError>;
502
503 fn capabilities(&self) -> ArtifactCapabilities;
505}
506
507#[must_use]
509pub fn detect_format(bytes: &[u8]) -> Option<ArtifactFormat> {
510 if bytes.starts_with(b"\0asm") {
511 Some(ArtifactFormat::Wasm)
512 } else if bytes.starts_with(b"\x7fELF") {
513 Some(ArtifactFormat::Elf)
514 } else if bytes.starts_with(&[0xfe, 0xed, 0xfa, 0xce])
515 || bytes.starts_with(&[0xfe, 0xed, 0xfa, 0xcf])
516 || bytes.starts_with(&[0xce, 0xfa, 0xed, 0xfe])
517 || bytes.starts_with(&[0xcf, 0xfa, 0xed, 0xfe])
518 || bytes.starts_with(&[0xca, 0xfe, 0xba, 0xbe])
519 || bytes.starts_with(&[0xca, 0xfe, 0xba, 0xbf])
520 {
521 Some(ArtifactFormat::MachO)
522 } else if bytes.starts_with(b"!<arch>\n") || bytes.starts_with(b"!<thin>\n") {
523 Some(ArtifactFormat::Archive)
524 } else if is_pe_coff(bytes) {
525 Some(ArtifactFormat::PeCoff)
526 } else {
527 None
528 }
529}
530
531fn is_pe_coff(bytes: &[u8]) -> bool {
537 if matches!(
538 bytes.get(..2),
539 Some([0x4c, 0x01] | [0x64, 0x86 | 0xaa] | [0xaa, 0x64])
540 ) {
541 return true;
542 }
543 let Some(offset_bytes) = bytes.get(0x3c..0x40) else {
544 return false;
545 };
546 let offset = u32::from_le_bytes(offset_bytes.try_into().unwrap_or([0; 4]));
547 usize::try_from(offset)
548 .ok()
549 .and_then(|offset| bytes.get(offset..offset.saturating_add(4)))
550 == Some(b"PE\0\0".as_slice())
551}
552
553#[cfg(test)]
554#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn magic_detection_distinguishes_supported_planned_and_unknown_inputs() {
560 let mut pe = [0_u8; 68];
561 pe[..2].copy_from_slice(b"MZ");
562 pe[0x3c..0x40].copy_from_slice(&64_u32.to_le_bytes());
563 pe[64..68].copy_from_slice(b"PE\0\0");
564 assert_eq!(
565 detect_format(b"\0asm\x01\0\0\0"),
566 Some(ArtifactFormat::Wasm)
567 );
568 assert_eq!(detect_format(b"\x7fELF\x02"), Some(ArtifactFormat::Elf));
569 assert_eq!(
570 detect_format(b"\xcf\xfa\xed\xfe"),
571 Some(ArtifactFormat::MachO)
572 );
573 assert_eq!(
574 detect_format(b"\xca\xfe\xba\xbe"),
575 Some(ArtifactFormat::MachO)
576 );
577 assert_eq!(detect_format(&pe), Some(ArtifactFormat::PeCoff));
578 assert_eq!(detect_format(&[0x64, 0x86]), Some(ArtifactFormat::PeCoff));
579 assert_eq!(detect_format(b"MZ\x90\0"), None);
580 assert_eq!(detect_format(b"!<arch>\n"), Some(ArtifactFormat::Archive));
581 assert_eq!(detect_format(b"!<thin>\n"), Some(ArtifactFormat::Archive));
582 assert_eq!(detect_format(b"not an artifact"), None);
583 }
584
585 #[test]
586 fn artifact_identity_is_content_based_and_format_ir_starts_empty() {
587 assert_eq!(ARTIFACT_IR_SCHEMA_VERSION, "artifact-ir-v1");
588 let wasm = ArtifactIr::empty(ArtifactFormat::Wasm, b"\0asm\x01\0\0\0");
589 let same = ArtifactIr::empty(ArtifactFormat::Wasm, b"\0asm\x01\0\0\0");
590 let changed = ArtifactIr::empty(ArtifactFormat::Wasm, b"\0asm\x01\0\0\x01");
591 assert_eq!(wasm.schema_version, ARTIFACT_IR_SCHEMA_VERSION);
592 assert_eq!(wasm.observed_bytes, 8);
593 assert_eq!(wasm.fingerprint, same.fingerprint);
594 assert_ne!(wasm.fingerprint, changed.fingerprint);
595 assert!(wasm.symbols.is_empty());
596 }
597
598 #[test]
599 fn serde_uses_the_same_format_labels_as_every_other_surface() {
600 for format in [
601 ArtifactFormat::Wasm,
602 ArtifactFormat::Elf,
603 ArtifactFormat::MachO,
604 ArtifactFormat::PeCoff,
605 ArtifactFormat::Archive,
606 ] {
607 let encoded = serde_json::to_string(&format).expect("format serializes");
608 assert_eq!(encoded, format!("\"{}\"", format.name()));
609 let decoded: ArtifactFormat = serde_json::from_str(&encoded).expect("format reads");
610 assert_eq!(decoded, format);
611 }
612 assert!(serde_json::from_str::<ArtifactFormat>("\"mach-o\"").is_err());
613 }
614
615 #[test]
616 fn artifact_payloads_are_base64_in_json_and_round_trip() {
617 let bytes = vec![0, 1, 2, 250, 255];
618 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input");
619 artifact.data_segments.push(ArtifactDataSegment {
620 fingerprint: ArtifactFingerprint::from_content("data", &bytes),
621 section: Some(1),
622 offset: 0,
623 bytes: bytes.clone(),
624 });
625 let json = serde_json::to_string(&artifact).expect("artifact serializes");
626 assert!(json.contains("\"AAEC+v8=\""), "{json}");
627 assert_eq!(
628 serde_json::from_str::<ArtifactIr>(&json).expect("artifact reads"),
629 artifact
630 );
631 }
632}