use core::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
#[cfg(feature = "archive")]
pub mod archive;
pub mod dwarf;
#[cfg(feature = "elf")]
pub mod elf;
#[cfg(feature = "macho")]
pub mod macho;
pub mod metrics;
pub mod native;
#[cfg(feature = "pe")]
pub mod pe;
pub mod symbols;
#[cfg(feature = "wasm")]
pub mod wasm;
pub mod x86;
pub const ARTIFACT_IR_SCHEMA_VERSION: &str = "artifact-ir-v1";
pub const ARTIFACT_FINGERPRINT_VERSION: &str = "artifact-fingerprint-v1";
pub mod base64_bytes {
use base64::Engine;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(bytes))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let encoded = String::deserialize(deserializer)?;
base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ArtifactFormat {
Wasm,
Elf,
MachO,
PeCoff,
Archive,
}
impl ArtifactFormat {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Wasm => "wasm",
Self::Elf => "elf",
Self::MachO => "macho",
Self::PeCoff => "pe-coff",
Self::Archive => "archive",
}
}
}
impl fmt::Display for ArtifactFormat {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.name())
}
}
impl Serialize for ArtifactFormat {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.name())
}
}
impl<'de> Deserialize<'de> for ArtifactFormat {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
match String::deserialize(deserializer)?.as_str() {
"wasm" => Ok(Self::Wasm),
"elf" => Ok(Self::Elf),
"macho" => Ok(Self::MachO),
"pe-coff" => Ok(Self::PeCoff),
"archive" => Ok(Self::Archive),
other => Err(serde::de::Error::unknown_variant(
other,
&["wasm", "elf", "macho", "pe-coff", "archive"],
)),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)] pub struct ArtifactCapabilities {
pub symbols: bool,
pub call_graph: bool,
pub source_mapping: bool,
pub debug_info_unreadable: bool,
pub normalized_duplicates: bool,
pub independent_data_segments: bool,
pub relocations: bool,
pub data_segments: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ArtifactFingerprint([u8; 16]);
impl ArtifactFingerprint {
#[must_use]
pub fn from_content(domain: &str, bytes: &[u8]) -> Self {
let mut hasher = blake3::Hasher::new();
hasher.update(ARTIFACT_FINGERPRINT_VERSION.as_bytes());
hasher.update(&(domain.len() as u64).to_le_bytes());
hasher.update(domain.as_bytes());
hasher.update(&(bytes.len() as u64).to_le_bytes());
hasher.update(bytes);
let mut fingerprint = [0_u8; 16];
fingerprint.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
Self(fingerprint)
}
#[must_use]
pub const fn as_bytes(self) -> [u8; 16] {
self.0
}
#[must_use]
pub fn to_hex(self) -> String {
self.to_string()
}
}
impl fmt::Display for ArtifactFingerprint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.0 {
write!(formatter, "{byte:02x}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactIr {
pub schema_version: String,
pub format: ArtifactFormat,
pub capabilities: ArtifactCapabilities,
pub fingerprint: ArtifactFingerprint,
pub observed_bytes: u64,
pub architecture: Option<String>,
pub skipped_architectures: Vec<String>,
pub sections: Vec<ArtifactSection>,
pub archive_members: Vec<ArtifactArchiveMember>,
pub imports: Vec<ArtifactImport>,
pub symbols: Vec<ArtifactSymbol>,
pub entry_points: Vec<ArtifactFingerprint>,
pub indirect_references: Vec<ArtifactFingerprint>,
pub calls: Vec<ArtifactCall>,
pub relocations: Vec<ArtifactRelocation>,
pub source_mappings: Vec<ArtifactSourceMapping>,
pub data_segments: Vec<ArtifactDataSegment>,
}
impl ArtifactIr {
#[must_use]
pub fn empty(format: ArtifactFormat, bytes: &[u8]) -> Self {
Self {
schema_version: ARTIFACT_IR_SCHEMA_VERSION.to_owned(),
format,
capabilities: ArtifactCapabilities::default(),
fingerprint: ArtifactFingerprint::from_content("artifact", bytes),
observed_bytes: bytes.len() as u64,
architecture: None,
skipped_architectures: Vec::new(),
sections: Vec::new(),
archive_members: Vec::new(),
imports: Vec::new(),
symbols: Vec::new(),
entry_points: Vec::new(),
indirect_references: Vec::new(),
calls: Vec::new(),
relocations: Vec::new(),
source_mappings: Vec::new(),
data_segments: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactArchiveMember {
pub name: String,
pub fingerprint: ArtifactFingerprint,
pub offset: u64,
pub size: u64,
pub format: Option<ArtifactFormat>,
pub thin: bool,
pub parse_error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactSection {
pub name: Option<String>,
pub offset: u64,
pub size: u64,
pub executable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactImport {
pub module: Option<String>,
pub name: Option<String>,
pub kind: ArtifactImportKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ArtifactImportKind {
Function,
Table,
Memory,
Global,
Tag,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactSymbol {
pub fingerprint: ArtifactFingerprint,
pub name: Option<String>,
pub exported: bool,
pub section: Option<u32>,
pub offset: u64,
pub size: u64,
pub size_inferred: bool,
#[serde(with = "base64_bytes")]
pub code: Vec<u8>,
pub normalized: Option<NormalizedInstructions>,
pub inline_stack: Vec<ArtifactInlineFrame>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactInlineFrame {
pub evidence_kind: ArtifactSourceLocationEvidenceKind,
pub source: String,
pub line: Option<u32>,
pub column: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactSourceLocationEvidenceKind {
Dwarf,
Pdb,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NormalizedInstructions {
pub version: String,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactCall {
pub caller: ArtifactFingerprint,
pub target: Option<ArtifactFingerprint>,
pub unresolved: Option<UnresolvedCall>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactRelocation {
pub section: Option<u32>,
pub offset: u64,
pub kind: String,
pub target: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactSourceMapping {
pub uri: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum UnresolvedCall {
IndirectTable,
ExternalImport,
NativeIndirect,
MissingRelocation,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactDataSegment {
pub fingerprint: ArtifactFingerprint,
pub section: Option<u32>,
pub offset: u64,
#[serde(with = "base64_bytes")]
pub bytes: Vec<u8>,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ArtifactError {
#[error("expected {expected} input")]
WrongFormat {
expected: ArtifactFormat,
},
#[error("malformed {format} input: {message}")]
Malformed {
format: ArtifactFormat,
message: String,
},
#[error("{format} is recognised but not supported")]
Unsupported {
format: ArtifactFormat,
},
}
pub trait ArtifactBackend: Send + Sync {
fn format(&self) -> ArtifactFormat;
fn detects(&self, bytes: &[u8]) -> bool;
fn parse(&self, bytes: &[u8]) -> Result<ArtifactIr, ArtifactError>;
fn capabilities(&self) -> ArtifactCapabilities;
}
#[must_use]
pub fn detect_format(bytes: &[u8]) -> Option<ArtifactFormat> {
if bytes.starts_with(b"\0asm") {
Some(ArtifactFormat::Wasm)
} else if bytes.starts_with(b"\x7fELF") {
Some(ArtifactFormat::Elf)
} else if bytes.starts_with(&[0xfe, 0xed, 0xfa, 0xce])
|| bytes.starts_with(&[0xfe, 0xed, 0xfa, 0xcf])
|| bytes.starts_with(&[0xce, 0xfa, 0xed, 0xfe])
|| bytes.starts_with(&[0xcf, 0xfa, 0xed, 0xfe])
|| bytes.starts_with(&[0xca, 0xfe, 0xba, 0xbe])
|| bytes.starts_with(&[0xca, 0xfe, 0xba, 0xbf])
{
Some(ArtifactFormat::MachO)
} else if bytes.starts_with(b"!<arch>\n") || bytes.starts_with(b"!<thin>\n") {
Some(ArtifactFormat::Archive)
} else if is_pe_coff(bytes) {
Some(ArtifactFormat::PeCoff)
} else {
None
}
}
fn is_pe_coff(bytes: &[u8]) -> bool {
if matches!(
bytes.get(..2),
Some([0x4c, 0x01] | [0x64, 0x86 | 0xaa] | [0xaa, 0x64])
) {
return true;
}
let Some(offset_bytes) = bytes.get(0x3c..0x40) else {
return false;
};
let offset = u32::from_le_bytes(offset_bytes.try_into().unwrap_or([0; 4]));
usize::try_from(offset)
.ok()
.and_then(|offset| bytes.get(offset..offset.saturating_add(4)))
== Some(b"PE\0\0".as_slice())
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn magic_detection_distinguishes_supported_planned_and_unknown_inputs() {
let mut pe = [0_u8; 68];
pe[..2].copy_from_slice(b"MZ");
pe[0x3c..0x40].copy_from_slice(&64_u32.to_le_bytes());
pe[64..68].copy_from_slice(b"PE\0\0");
assert_eq!(
detect_format(b"\0asm\x01\0\0\0"),
Some(ArtifactFormat::Wasm)
);
assert_eq!(detect_format(b"\x7fELF\x02"), Some(ArtifactFormat::Elf));
assert_eq!(
detect_format(b"\xcf\xfa\xed\xfe"),
Some(ArtifactFormat::MachO)
);
assert_eq!(
detect_format(b"\xca\xfe\xba\xbe"),
Some(ArtifactFormat::MachO)
);
assert_eq!(detect_format(&pe), Some(ArtifactFormat::PeCoff));
assert_eq!(detect_format(&[0x64, 0x86]), Some(ArtifactFormat::PeCoff));
assert_eq!(detect_format(b"MZ\x90\0"), None);
assert_eq!(detect_format(b"!<arch>\n"), Some(ArtifactFormat::Archive));
assert_eq!(detect_format(b"!<thin>\n"), Some(ArtifactFormat::Archive));
assert_eq!(detect_format(b"not an artifact"), None);
}
#[test]
fn artifact_identity_is_content_based_and_format_ir_starts_empty() {
assert_eq!(ARTIFACT_IR_SCHEMA_VERSION, "artifact-ir-v1");
let wasm = ArtifactIr::empty(ArtifactFormat::Wasm, b"\0asm\x01\0\0\0");
let same = ArtifactIr::empty(ArtifactFormat::Wasm, b"\0asm\x01\0\0\0");
let changed = ArtifactIr::empty(ArtifactFormat::Wasm, b"\0asm\x01\0\0\x01");
assert_eq!(wasm.schema_version, ARTIFACT_IR_SCHEMA_VERSION);
assert_eq!(wasm.observed_bytes, 8);
assert_eq!(wasm.fingerprint, same.fingerprint);
assert_ne!(wasm.fingerprint, changed.fingerprint);
assert!(wasm.symbols.is_empty());
}
#[test]
fn serde_uses_the_same_format_labels_as_every_other_surface() {
for format in [
ArtifactFormat::Wasm,
ArtifactFormat::Elf,
ArtifactFormat::MachO,
ArtifactFormat::PeCoff,
ArtifactFormat::Archive,
] {
let encoded = serde_json::to_string(&format).expect("format serializes");
assert_eq!(encoded, format!("\"{}\"", format.name()));
let decoded: ArtifactFormat = serde_json::from_str(&encoded).expect("format reads");
assert_eq!(decoded, format);
}
assert!(serde_json::from_str::<ArtifactFormat>("\"mach-o\"").is_err());
}
#[test]
fn artifact_payloads_are_base64_in_json_and_round_trip() {
let bytes = vec![0, 1, 2, 250, 255];
let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input");
artifact.data_segments.push(ArtifactDataSegment {
fingerprint: ArtifactFingerprint::from_content("data", &bytes),
section: Some(1),
offset: 0,
bytes: bytes.clone(),
});
let json = serde_json::to_string(&artifact).expect("artifact serializes");
assert!(json.contains("\"AAEC+v8=\""), "{json}");
assert_eq!(
serde_json::from_str::<ArtifactIr>(&json).expect("artifact reads"),
artifact
);
}
}