use serde_json::{Map, Value};
use super::reasons::RefusalReason;
pub const COMPILER_FACTS_FORMAT: &str = "compiler-facts";
pub const COMPILER_FACTS_CONTRACT_VERSION: u64 = 1;
pub const COMPILER_FACTS_ARTIFACT_SCHEMA_VERSION: u64 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Profile {
pub target: String,
pub configuration: String,
pub platform: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IncompleteUnit {
pub unit: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Coverage {
Complete,
Incomplete {
units: Vec<IncompleteUnit>,
},
}
impl Coverage {
pub fn is_complete(&self) -> bool {
matches!(self, Coverage::Complete)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CompilationRef {
pub identity: Value,
pub fingerprint: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CandidateHeader {
pub contract_version: u64,
pub producer_name: String,
pub engine_revision: String,
pub profile: Profile,
pub dependency_fingerprint: String,
pub context_schema_version: u64,
pub context_fingerprint_header: Option<String>,
pub context_compilations: Option<Vec<CompilationRef>>,
pub occurrence_sites: Option<Vec<CompilationRef>>,
pub occurrences_capability_provided: bool,
pub source_head_sha: Option<String>,
pub completion_terminal: bool,
pub units_processed: Vec<String>,
pub units_missing: Vec<String>,
pub coverage: Coverage,
}
fn obj_str<'a>(obj: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
obj.get(key).and_then(Value::as_str)
}
fn obj_u64(obj: &Map<String, Value>, key: &str) -> Option<u64> {
obj.get(key).and_then(Value::as_u64)
}
fn obj_bool(obj: &Map<String, Value>, key: &str) -> Option<bool> {
obj.get(key).and_then(Value::as_bool)
}
fn obj_obj<'a>(obj: &'a Map<String, Value>, key: &str) -> Option<&'a Map<String, Value>> {
obj.get(key).and_then(Value::as_object)
}
fn str_array(obj: &Map<String, Value>, key: &str) -> Option<Vec<String>> {
obj.get(key)?
.as_array()?
.iter()
.map(|v| v.as_str().map(str::to_string))
.collect()
}
fn parse_coverage(obj: &Map<String, Value>) -> Result<Coverage, RefusalReason> {
let coverage_obj = obj_obj(obj, "coverage").ok_or(RefusalReason::MalformedEncoding)?;
match obj_str(coverage_obj, "state").ok_or(RefusalReason::MalformedEncoding)? {
"complete" => Ok(Coverage::Complete),
"incomplete" => {
let raw = coverage_obj
.get("incompleteUnits")
.and_then(Value::as_array)
.ok_or(RefusalReason::MalformedEncoding)?;
let mut units = Vec::with_capacity(raw.len());
for item in raw {
let item_obj = item.as_object().ok_or(RefusalReason::MalformedEncoding)?;
units.push(IncompleteUnit {
unit: obj_str(item_obj, "unit")
.ok_or(RefusalReason::MalformedEncoding)?
.to_string(),
reason: obj_str(item_obj, "reason")
.ok_or(RefusalReason::MalformedEncoding)?
.to_string(),
});
}
if units.is_empty() {
return Err(RefusalReason::MalformedEncoding);
}
Ok(Coverage::Incomplete { units })
}
_ => Err(RefusalReason::MalformedEncoding),
}
}
pub fn parse_header(bytes: &[u8]) -> Result<CandidateHeader, RefusalReason> {
let value: Value =
serde_json::from_slice(bytes).map_err(|_| RefusalReason::MalformedEncoding)?;
let obj = value.as_object().ok_or(RefusalReason::MalformedEncoding)?;
if obj_str(obj, "format") != Some(COMPILER_FACTS_FORMAT) {
return Err(RefusalReason::MalformedEncoding);
}
let contract_version =
obj_u64(obj, "contractVersion").ok_or(RefusalReason::MalformedEncoding)?;
obj_u64(obj, "artifactSchemaVersion").ok_or(RefusalReason::MalformedEncoding)?;
let producer = obj_obj(obj, "producer").ok_or(RefusalReason::MalformedEncoding)?;
let producer_name = obj_str(producer, "name")
.ok_or(RefusalReason::MalformedEncoding)?
.to_string();
let engine_revision = obj_str(producer, "engineRevision")
.ok_or(RefusalReason::MalformedEncoding)?
.to_string();
let profile_obj = obj_obj(obj, "profile").ok_or(RefusalReason::MalformedEncoding)?;
let profile = Profile {
target: obj_str(profile_obj, "target")
.ok_or(RefusalReason::MalformedEncoding)?
.to_string(),
configuration: obj_str(profile_obj, "configuration")
.ok_or(RefusalReason::MalformedEncoding)?
.to_string(),
platform: obj_str(profile_obj, "platform")
.ok_or(RefusalReason::MalformedEncoding)?
.to_string(),
};
let dependency_fingerprint = obj_str(obj, "dependencyFingerprint")
.ok_or(RefusalReason::MalformedEncoding)?
.to_string();
let context_obj = obj_obj(obj, "context").ok_or(RefusalReason::MalformedEncoding)?;
let context_schema_version =
obj_u64(context_obj, "schemaVersion").ok_or(RefusalReason::MalformedEncoding)?;
let context_fingerprint_header = obj_str(context_obj, "contextFingerprint").map(str::to_string);
let context_compilations = parse_compilation_refs(context_obj);
let occurrences_capability_provided = obj_obj(obj, "capabilities")
.and_then(|c| c.get("provided"))
.and_then(Value::as_array)
.is_some_and(|provided| provided.iter().any(|v| v.as_str() == Some("occurrences")));
let occurrence_sites = parse_occurrence_sites(obj);
let source_head_sha = obj_obj(obj, "sourceSnapshot")
.and_then(|s| obj_str(s, "headSha"))
.map(str::to_string);
let completion_obj = obj_obj(obj, "completion").ok_or(RefusalReason::MalformedEncoding)?;
let completion_terminal =
obj_bool(completion_obj, "terminal").ok_or(RefusalReason::MalformedEncoding)?;
let units_obj = obj_obj(obj, "units").ok_or(RefusalReason::MalformedEncoding)?;
let units_processed =
str_array(units_obj, "processed").ok_or(RefusalReason::MalformedEncoding)?;
let units_missing = str_array(units_obj, "missing").ok_or(RefusalReason::MalformedEncoding)?;
let coverage = parse_coverage(obj)?;
Ok(CandidateHeader {
contract_version,
producer_name,
engine_revision,
profile,
dependency_fingerprint,
context_schema_version,
context_fingerprint_header,
context_compilations,
occurrence_sites,
occurrences_capability_provided,
source_head_sha,
completion_terminal,
units_processed,
units_missing,
coverage,
})
}
fn parse_compilation_refs(context_obj: &Map<String, Value>) -> Option<Vec<CompilationRef>> {
let entries = obj_obj(context_obj, "envelope")?
.get("compilations")?
.as_array()?;
let mut result = Vec::with_capacity(entries.len());
for entry in entries {
let entry_obj = entry.as_object()?;
let identity = entry_obj.get("identity")?.clone();
let fingerprint = entry_obj
.get("fingerprint")
.and_then(Value::as_str)
.map(str::to_string);
result.push(CompilationRef {
identity,
fingerprint,
});
}
Some(result)
}
fn parse_occurrence_sites(obj: &Map<String, Value>) -> Option<Vec<CompilationRef>> {
let sites = obj_obj(obj, "occurrences")?.get("sites")?.as_array()?;
let mut result = Vec::with_capacity(sites.len());
for site in sites {
let compilation_obj = obj_obj(site.as_object()?, "compilation")?;
let identity = compilation_obj.get("identity")?.clone();
let fingerprint = compilation_obj
.get("fingerprint")
.and_then(Value::as_str)
.map(str::to_string);
result.push(CompilationRef {
identity,
fingerprint,
});
}
Some(result)
}