Skip to main content

codehelion_artifact/
lib.rs

1//! Format-neutral artifact parsing boundary and intermediate representation.
2//!
3//! This crate owns no source-analysis dependency. Format backends turn bytes
4//! into [`ArtifactIr`]; common metrics can then operate on that IR without
5//! knowing whether it came from WebAssembly, ELF, or a later format.
6//!
7//! Each format backend sits behind a feature of the same name, so a caller
8//! that reads one format does not build the parsers for the others. The
9//! `archive` feature turns on the four backends it delegates members to.
10
11use 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
32/// Version of the artifact IR document.
33pub const ARTIFACT_IR_SCHEMA_VERSION: &str = "artifact-ir-v1";
34
35/// Version of the fingerprint recipe for parsed artifact entities.
36pub const ARTIFACT_FINGERPRINT_VERSION: &str = "artifact-fingerprint-v1";
37
38/// JSON uses base64 strings for opaque artifact payloads rather than one
39/// number per byte, while binary serde formats retain their usual byte form.
40/// Serde adapter for base64-encoded artifact byte payloads.
41pub mod base64_bytes {
42    use base64::Engine;
43    use serde::{Deserialize, Deserializer, Serializer};
44
45    /// Serialize bytes as one base64 string.
46    ///
47    /// # Errors
48    ///
49    /// Returns the serializer's error when it cannot write the string.
50    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    /// Deserialize one base64 string to its bytes.
58    ///
59    /// # Errors
60    ///
61    /// Returns the deserializer's error for a non-string or invalid base64 payload.
62    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/// A binary container format that codehelion recognises.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
75pub enum ArtifactFormat {
76    /// A WebAssembly core module or component.
77    Wasm,
78    /// An ELF executable, shared library, or relocatable object.
79    Elf,
80    /// A Mach-O executable, dynamic library, or relocatable object.
81    MachO,
82    /// A PE image or COFF relocatable object.
83    PeCoff,
84    /// A static archive.
85    Archive,
86}
87
88impl ArtifactFormat {
89    /// Stable format label used in reports and fingerprint inputs.
90    #[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/// Information a format backend could establish without guessing.
137#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
138#[allow(clippy::struct_excessive_bools)] // Independent facts a backend may establish.
139pub struct ArtifactCapabilities {
140    /// Whether symbol or function boundaries are available.
141    pub symbols: bool,
142    /// Whether direct call edges are available.
143    pub call_graph: bool,
144    /// Whether source locations or mappings are available.
145    pub source_mapping: bool,
146    /// Whether debug information was present but could not be decoded safely.
147    pub debug_info_unreadable: bool,
148    /// Whether this artifact's instruction architecture has a normalizer.
149    pub normalized_duplicates: bool,
150    /// Whether the parser established independently sized data regions.
151    pub independent_data_segments: bool,
152    /// Whether relocations are available.
153    pub relocations: bool,
154    /// Whether data segments can be independently inspected.
155    pub data_segments: bool,
156}
157
158/// The stable content fingerprint of an artifact or entity inside one.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
160pub struct ArtifactFingerprint([u8; 16]);
161
162impl ArtifactFingerprint {
163    /// Hash `bytes` under a domain that keeps artifact identities apart from
164    /// source-audit fingerprints.
165    #[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    /// Raw fingerprint bytes for persistence.
179    #[must_use]
180    pub const fn as_bytes(self) -> [u8; 16] {
181        self.0
182    }
183
184    /// Lowercase hexadecimal representation used in reports.
185    #[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/// One parsed artifact, independent of the container that supplied it.
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct ArtifactIr {
203    /// Version of this document's shape.
204    pub schema_version: String,
205    /// Container format that supplied this IR.
206    pub format: ArtifactFormat,
207    /// Facts that the parser could establish for this individual input.
208    pub capabilities: ArtifactCapabilities,
209    /// Stable identity of the complete byte input.
210    pub fingerprint: ArtifactFingerprint,
211    /// Input length measured directly from the byte stream.
212    pub observed_bytes: u64,
213    /// Architecture selected from the parsed input, when the format exposes one.
214    ///
215    /// A universal Mach-O records its explicitly selected slice here. Ordinary
216    /// single-architecture inputs also retain their parser-observed architecture.
217    pub architecture: Option<String>,
218    /// Architectures deliberately not parsed from a universal container.
219    ///
220    /// This is display evidence, not a stable identity or a claim that the
221    /// skipped slices are semantically equivalent to the selected one.
222    pub skipped_architectures: Vec<String>,
223    /// Parsed sections, when the format exposes them.
224    pub sections: Vec<ArtifactSection>,
225    /// Object members when this artifact is an archive.
226    ///
227    /// Ordinary containers leave this empty. Archive members retain their own
228    /// content identity and parser outcome even though their parsed facts are
229    /// also flattened into this IR for the format-neutral metrics layer.
230    pub archive_members: Vec<ArtifactArchiveMember>,
231    /// Declared imports, when the format exposes them.
232    pub imports: Vec<ArtifactImport>,
233    /// Parsed functions or symbols.
234    pub symbols: Vec<ArtifactSymbol>,
235    /// Parser-established entry points in the local symbol identity space.
236    pub entry_points: Vec<ArtifactFingerprint>,
237    /// Functions retained by an indirect-dispatch table or equivalent parser
238    /// evidence. These are roots for conservative reachability, not IDs.
239    pub indirect_references: Vec<ArtifactFingerprint>,
240    /// Direct and unresolved call relations.
241    pub calls: Vec<ArtifactCall>,
242    /// Relocation anchors, when a format preserves them.
243    pub relocations: Vec<ArtifactRelocation>,
244    /// Source-map references the artifact itself declares.
245    pub source_mappings: Vec<ArtifactSourceMapping>,
246    /// Independent data regions that can participate in duplicate detection.
247    pub data_segments: Vec<ArtifactDataSegment>,
248}
249
250impl ArtifactIr {
251    /// Start an IR for `bytes`; backends add only facts they actually parsed.
252    #[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/// One object member observed inside a static archive.
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278pub struct ArtifactArchiveMember {
279    /// Archive-provided member name, preserved only as display evidence.
280    pub name: String,
281    /// Content-derived identity of this member's bytes.
282    pub fingerprint: ArtifactFingerprint,
283    /// Byte offset of the member data in the archive, never an identity input.
284    pub offset: u64,
285    /// Member byte length observed in the archive.
286    pub size: u64,
287    /// Container format recognised inside this member, when any.
288    pub format: Option<ArtifactFormat>,
289    /// Whether this member is thin and therefore has no local bytes to parse.
290    pub thin: bool,
291    /// Parser failure or deliberate non-support for this individual member.
292    ///
293    /// This retains a partial archive result rather than hiding unsupported or
294    /// malformed member bytes behind a successful outer container parse.
295    pub parse_error: Option<String>,
296}
297
298/// One named or numbered region of an artifact.
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300pub struct ArtifactSection {
301    /// A format-provided name, when one exists.
302    pub name: Option<String>,
303    /// Offset in the input byte stream.
304    pub offset: u64,
305    /// Length in bytes.
306    pub size: u64,
307    /// Whether executable code resides in this section.
308    pub executable: bool,
309}
310
311/// A dependency the artifact declares without requiring it to be loaded.
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313pub struct ArtifactImport {
314    /// Importing module or library namespace, when the format uses one.
315    pub module: Option<String>,
316    /// Imported item name, when supplied by the format.
317    pub name: Option<String>,
318    /// Declared kind of the imported item.
319    pub kind: ArtifactImportKind,
320}
321
322/// Kind of an [`ArtifactImport`].
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(rename_all = "kebab-case")]
325pub enum ArtifactImportKind {
326    /// Callable import.
327    Function,
328    /// Table import.
329    Table,
330    /// Linear-memory import.
331    Memory,
332    /// Global-value import.
333    Global,
334    /// Exception tag import.
335    Tag,
336    /// A format-specific kind that this IR version does not classify further.
337    Other,
338}
339
340/// A function or symbol extracted from an artifact.
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct ArtifactSymbol {
343    /// Stable identity built from semantic name, normalized body, and section.
344    pub fingerprint: ArtifactFingerprint,
345    /// Demangled name when a format supplied one.
346    pub name: Option<String>,
347    /// Whether the container declares this symbol as externally reachable.
348    pub exported: bool,
349    /// Section index is display-only; it is never used as an identity.
350    pub section: Option<u32>,
351    /// Start offset in the artifact.
352    pub offset: u64,
353    /// Observed or conservatively inferred byte size.
354    pub size: u64,
355    /// Whether `size` was inferred rather than provided by the format.
356    pub size_inferred: bool,
357    /// Exact code bytes, when a boundary could be established.
358    #[serde(with = "base64_bytes")]
359    pub code: Vec<u8>,
360    /// Versioned normalized instruction stream, when decoding is supported.
361    pub normalized: Option<NormalizedInstructions>,
362    /// Inline source locations, when debug information established them.
363    pub inline_stack: Vec<ArtifactInlineFrame>,
364}
365
366/// One source frame associated with an inlined artifact symbol.
367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368pub struct ArtifactInlineFrame {
369    /// Debug metadata family that established this location.
370    pub evidence_kind: ArtifactSourceLocationEvidenceKind,
371    /// Source file or source-map URL supplied by debug metadata.
372    pub source: String,
373    /// One-based source line, when supplied.
374    pub line: Option<u32>,
375    /// One-based source column, when supplied.
376    pub column: Option<u32>,
377}
378
379/// Debug metadata family that established one artifact source location.
380///
381/// This is evidence provenance, not an artifact or source identity. The
382/// correlation layer preserves it so a PDB-derived location is never reported
383/// as DWARF evidence.
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
385#[serde(rename_all = "snake_case")]
386pub enum ArtifactSourceLocationEvidenceKind {
387    /// DWARF debug metadata established the source location.
388    Dwarf,
389    /// PDB debug metadata established the source location.
390    Pdb,
391}
392
393/// A versioned representation used for normalized duplicate detection.
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395pub struct NormalizedInstructions {
396    /// Version of the format-specific normalization recipe.
397    pub version: String,
398    /// Normalized instruction bytes or tokens.
399    pub bytes: Vec<u8>,
400}
401
402/// One relation from a caller to a direct target or an unresolved dispatch.
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct ArtifactCall {
405    /// Caller symbol fingerprint.
406    pub caller: ArtifactFingerprint,
407    /// A direct target, when the format makes one provable.
408    pub target: Option<ArtifactFingerprint>,
409    /// Why no exact target is asserted.
410    pub unresolved: Option<UnresolvedCall>,
411}
412
413/// A relocation anchor retained as parsed evidence rather than a stable ID.
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct ArtifactRelocation {
416    /// Section index used only to locate this parser observation.
417    pub section: Option<u32>,
418    /// Offset in the artifact byte stream.
419    pub offset: u64,
420    /// Parser-provided relocation kind label.
421    pub kind: String,
422    /// Display target, when the format makes one available.
423    pub target: Option<String>,
424}
425
426/// A source-map reference declared by an artifact.
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428pub struct ArtifactSourceMapping {
429    /// URL or path declared by the artifact, without fetching it.
430    pub uri: String,
431}
432
433/// A conservative reason a call edge has no direct target.
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(rename_all = "kebab-case")]
436pub enum UnresolvedCall {
437    /// A WebAssembly indirect call whose possible targets require table flow.
438    IndirectTable,
439    /// A direct call whose target is imported rather than defined in this
440    /// artifact, so there is no local symbol fingerprint to reference.
441    ExternalImport,
442    /// A native indirect call through a register or memory location.
443    NativeIndirect,
444    /// A relocation or symbol target was unavailable.
445    MissingRelocation,
446}
447
448/// A data region eligible for exact duplicate analysis.
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
450pub struct ArtifactDataSegment {
451    /// Stable identity of the segment's bytes.
452    pub fingerprint: ArtifactFingerprint,
453    /// Source section, when known.
454    pub section: Option<u32>,
455    /// Offset in the artifact.
456    pub offset: u64,
457    /// Bytes as observed; later storage may deduplicate the payload.
458    #[serde(with = "base64_bytes")]
459    pub bytes: Vec<u8>,
460}
461
462/// A parser failure that preserves the source artifact and never executes it.
463#[derive(Debug, Error, PartialEq, Eq)]
464pub enum ArtifactError {
465    /// The bytes do not belong to this backend's format.
466    #[error("expected {expected} input")]
467    WrongFormat {
468        /// Format the backend handles.
469        expected: ArtifactFormat,
470    },
471    /// The bytes were recognised but could not be safely parsed.
472    #[error("malformed {format} input: {message}")]
473    Malformed {
474        /// Recognised binary format.
475        format: ArtifactFormat,
476        /// Parser-provided error explanation.
477        message: String,
478    },
479    /// The backend is a recognised future format with no parser yet.
480    #[error("{format} is recognised but not supported")]
481    Unsupported {
482        /// Recognised format without a backend.
483        format: ArtifactFormat,
484    },
485}
486
487/// Format-specific parser isolated behind a common Artifact IR boundary.
488pub trait ArtifactBackend: Send + Sync {
489    /// Format this backend accepts.
490    fn format(&self) -> ArtifactFormat;
491
492    /// Whether `bytes` begin with this format's magic number.
493    fn detects(&self, bytes: &[u8]) -> bool;
494
495    /// Parse bytes without executing the artifact.
496    ///
497    /// # Errors
498    ///
499    /// Returns [`ArtifactError::WrongFormat`] for input with another magic and
500    /// [`ArtifactError::Malformed`] for a recognised but invalid input.
501    fn parse(&self, bytes: &[u8]) -> Result<ArtifactIr, ArtifactError>;
502
503    /// Facts this backend can potentially provide for a well-formed input.
504    fn capabilities(&self) -> ArtifactCapabilities;
505}
506
507/// Recognise supported and planned artifact formats from their magic bytes.
508#[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
531/// Whether the input starts as a supported PE image or COFF object.
532///
533/// `MZ` alone also names historical DOS executables, so treating it as PE/COFF
534/// would promise a backend for bytes the parser cannot read. COFF has no DOS
535/// header; its file-header machine value is the only inexpensive dispatch fact.
536fn 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}