Skip to main content

hara_native/kernel/
halc_trace.rs

1use super::halc::{decode_halc, HalcOrigin};
2use std::collections::BTreeMap;
3
4pub const HALC_TRACE_SCHEMA: &str = "hal.halc-trace/0-alpha";
5
6const MAGIC_BYTES: usize = 4;
7const VERSION_OFFSET: usize = MAGIC_BYTES;
8const FLAGS_OFFSET: usize = VERSION_OFFSET + 2;
9const PAYLOAD_LENGTH_OFFSET: usize = FLAGS_OFFSET + 2;
10const CHECKSUM_OFFSET: usize = PAYLOAD_LENGTH_OFFSET + 4;
11const CHECKSUM_BYTES: usize = 32;
12const HEADER_BYTES: usize = CHECKSUM_OFFSET + CHECKSUM_BYTES;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum HalcTraceStatus {
16    Ok,
17    Error,
18}
19
20impl HalcTraceStatus {
21    pub fn as_keyword(self) -> &'static str {
22        match self {
23            Self::Ok => "ok",
24            Self::Error => "error",
25        }
26    }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum HalcTraceValue {
31    String(String),
32    Integer(u64),
33    Boolean(bool),
34    Strings(Vec<String>),
35}
36
37pub type HalcTraceEvidence = BTreeMap<String, HalcTraceValue>;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct HalcTraceEvent {
41    pub id: u64,
42    pub sequence: u64,
43    pub stage: &'static str,
44    pub status: HalcTraceStatus,
45    pub evidence: HalcTraceEvidence,
46    pub error: Option<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct HalcArtifactTrace {
51    pub schema: &'static str,
52    pub id: String,
53    pub status: HalcTraceStatus,
54    pub events: Vec<HalcTraceEvent>,
55    pub result: Option<HalcTraceEvidence>,
56    pub error: Option<String>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct HalcArtifactInspection {
61    pub origin: HalcOrigin,
62    pub format_version: u16,
63    pub flags: u16,
64    pub payload_length: u32,
65    pub payload_checksum: String,
66    pub namespace: String,
67    pub resource: String,
68    pub source_hash: String,
69    pub form_count: usize,
70    pub schema_definitions: Vec<String>,
71    pub schema_functions: Vec<String>,
72}
73
74fn read_u16(bytes: &[u8], offset: usize) -> u16 {
75    u16::from_be_bytes([bytes[offset], bytes[offset + 1]])
76}
77
78fn read_u32(bytes: &[u8], offset: usize) -> u32 {
79    u32::from_be_bytes([
80        bytes[offset],
81        bytes[offset + 1],
82        bytes[offset + 2],
83        bytes[offset + 3],
84    ])
85}
86
87fn hex(bytes: &[u8]) -> String {
88    let mut output = String::with_capacity(bytes.len() * 2);
89    for byte in bytes {
90        use std::fmt::Write;
91        let _ = write!(&mut output, "{byte:02x}");
92    }
93    output
94}
95
96fn origin_name(origin: HalcOrigin) -> &'static str {
97    match origin {
98        HalcOrigin::Halc => "halc",
99    }
100}
101
102pub fn inspect_halc_artifact(bytes: &[u8]) -> Result<HalcArtifactInspection, String> {
103    let module = decode_halc(bytes)?;
104    if bytes.len() < HEADER_BYTES {
105        return Err("truncated artifact".into());
106    }
107
108    let mut schema_definitions: Vec<String> = module.schemas.definitions.keys().cloned().collect();
109    schema_definitions.sort();
110    let mut schema_functions: Vec<String> = module.schemas.functions.keys().cloned().collect();
111    schema_functions.sort();
112
113    Ok(HalcArtifactInspection {
114        origin: module.origin,
115        format_version: read_u16(bytes, VERSION_OFFSET),
116        flags: read_u16(bytes, FLAGS_OFFSET),
117        payload_length: read_u32(bytes, PAYLOAD_LENGTH_OFFSET),
118        payload_checksum: hex(&bytes[CHECKSUM_OFFSET..HEADER_BYTES]),
119        namespace: module.namespace,
120        resource: module.resource,
121        source_hash: hex(&module.source_hash),
122        form_count: module.forms.len(),
123        schema_definitions,
124        schema_functions,
125    })
126}
127
128fn string(value: impl Into<String>) -> HalcTraceValue {
129    HalcTraceValue::String(value.into())
130}
131
132fn integer(value: usize) -> HalcTraceValue {
133    HalcTraceValue::Integer(value as u64)
134}
135
136fn event(
137    id: u64,
138    stage: &'static str,
139    status: HalcTraceStatus,
140    evidence: HalcTraceEvidence,
141    error: Option<String>,
142) -> HalcTraceEvent {
143    HalcTraceEvent {
144        id,
145        sequence: id,
146        stage,
147        status,
148        evidence,
149        error,
150    }
151}
152
153fn successful_trace(id: String, inspection: HalcArtifactInspection) -> HalcArtifactTrace {
154    let mut module_identity = HalcTraceEvidence::new();
155    module_identity.insert("module/namespace".into(), string(&inspection.namespace));
156    module_identity.insert("module/resource".into(), string(&inspection.resource));
157    module_identity.insert("source/hash".into(), string(&inspection.source_hash));
158    module_identity.insert("form/count".into(), integer(inspection.form_count));
159
160    let mut schema_index = HalcTraceEvidence::new();
161    schema_index.insert(
162        "schema/definitions".into(),
163        HalcTraceValue::Strings(inspection.schema_definitions.clone()),
164    );
165    schema_index.insert(
166        "schema/functions".into(),
167        HalcTraceValue::Strings(inspection.schema_functions.clone()),
168    );
169
170    let mut envelope = HalcTraceEvidence::new();
171    envelope.insert(
172        "artifact/origin".into(),
173        string(origin_name(inspection.origin)),
174    );
175    envelope.insert(
176        "format/version".into(),
177        HalcTraceValue::Integer(inspection.format_version as u64),
178    );
179    envelope.insert(
180        "format/flags".into(),
181        HalcTraceValue::Integer(inspection.flags as u64),
182    );
183    envelope.insert(
184        "payload/bytes".into(),
185        HalcTraceValue::Integer(inspection.payload_length as u64),
186    );
187    envelope.insert(
188        "payload/checksum".into(),
189        string(&inspection.payload_checksum),
190    );
191
192    let mut validation = HalcTraceEvidence::new();
193    validation.insert("artifact/valid".into(), HalcTraceValue::Boolean(true));
194    validation.insert(
195        "payload/checksum".into(),
196        string(&inspection.payload_checksum),
197    );
198
199    let mut decoded = HalcTraceEvidence::new();
200    decoded.insert("decode/parity".into(), HalcTraceValue::Boolean(true));
201    decoded.insert("form/count".into(), integer(inspection.form_count));
202
203    let events = vec![
204        event(
205            1,
206            "module/identity",
207            HalcTraceStatus::Ok,
208            module_identity.clone(),
209            None,
210        ),
211        event(
212            2,
213            "schema/index",
214            HalcTraceStatus::Ok,
215            schema_index.clone(),
216            None,
217        ),
218        event(
219            3,
220            "envelope/build",
221            HalcTraceStatus::Ok,
222            envelope.clone(),
223            None,
224        ),
225        event(
226            4,
227            "artifact/validate",
228            HalcTraceStatus::Ok,
229            validation,
230            None,
231        ),
232        event(5, "artifact/decode", HalcTraceStatus::Ok, decoded, None),
233    ];
234
235    let mut result = module_identity;
236    result.extend(schema_index);
237    result.extend(envelope);
238    result.insert("decode/parity".into(), HalcTraceValue::Boolean(true));
239
240    HalcArtifactTrace {
241        schema: HALC_TRACE_SCHEMA,
242        id,
243        status: HalcTraceStatus::Ok,
244        events,
245        result: Some(result),
246        error: None,
247    }
248}
249
250fn failed_trace(id: String, bytes: &[u8], error: String) -> HalcArtifactTrace {
251    let mut evidence = HalcTraceEvidence::new();
252    evidence.insert("artifact/bytes".into(), integer(bytes.len()));
253    let failure = event(
254        1,
255        "artifact/validate",
256        HalcTraceStatus::Error,
257        evidence,
258        Some(error.clone()),
259    );
260    HalcArtifactTrace {
261        schema: HALC_TRACE_SCHEMA,
262        id,
263        status: HalcTraceStatus::Error,
264        events: vec![failure],
265        result: None,
266        error: Some(error),
267    }
268}
269
270pub fn trace_halc_artifact(id: impl Into<String>, bytes: &[u8]) -> HalcArtifactTrace {
271    let id = id.into();
272    let id = if id.is_empty() {
273        "halc-artifact".to_owned()
274    } else {
275        id
276    };
277    match inspect_halc_artifact(bytes) {
278        Ok(inspection) => successful_trace(id, inspection),
279        Err(error) => failed_trace(id, bytes, error),
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::kernel::{halc::encode_halc_module, parse_forms};
287
288    fn artifact() -> Vec<u8> {
289        let source = "(ns demo.schema) \
290                      (def Customer [:map [:id :int]]) \
291                      (defn ^{:schema #'-/Customer} customer-id [customer] customer)";
292        encode_halc_module(
293            "demo.schema",
294            "demo/schema.hal",
295            source,
296            parse_forms(source).unwrap(),
297        )
298        .unwrap()
299    }
300
301    #[test]
302    fn inspects_and_traces_the_production_halc_artifact() {
303        let artifact = artifact();
304        let inspection = inspect_halc_artifact(&artifact).unwrap();
305        assert_eq!(inspection.origin, HalcOrigin::Halc);
306        assert_eq!(inspection.format_version, 1);
307        assert_eq!(inspection.flags, 1);
308        assert_eq!(inspection.namespace, "demo.schema");
309        assert_eq!(inspection.resource, "demo/schema.hal");
310        assert_eq!(inspection.form_count, 3);
311        assert_eq!(
312            inspection.schema_definitions,
313            vec!["demo.schema/Customer".to_owned()]
314        );
315        assert_eq!(
316            inspection.schema_functions,
317            vec!["demo.schema/customer-id".to_owned()]
318        );
319
320        let trace = trace_halc_artifact("trace-1", &artifact);
321        assert_eq!(trace.schema, HALC_TRACE_SCHEMA);
322        assert_eq!(trace.status, HalcTraceStatus::Ok);
323        assert_eq!(
324            trace
325                .events
326                .iter()
327                .map(|event| event.stage)
328                .collect::<Vec<_>>(),
329            vec![
330                "module/identity",
331                "schema/index",
332                "envelope/build",
333                "artifact/validate",
334                "artifact/decode",
335            ]
336        );
337        assert_eq!(
338            trace.result.as_ref().unwrap().get("module/namespace"),
339            Some(&HalcTraceValue::String("demo.schema".to_owned()))
340        );
341        assert_eq!(
342            trace.result.as_ref().unwrap().get("decode/parity"),
343            Some(&HalcTraceValue::Boolean(true))
344        );
345    }
346
347    #[test]
348    fn rejects_legacy_hir_artifacts() {
349        let mut artifact = artifact();
350        artifact[..MAGIC_BYTES].copy_from_slice(b"HIR\0");
351        assert_eq!(inspect_halc_artifact(&artifact).unwrap_err(), "bad magic");
352    }
353
354    #[test]
355    fn invalid_artifacts_become_precise_validation_failure_traces() {
356        let trace = trace_halc_artifact("bad", b"NOPE");
357        assert_eq!(trace.status, HalcTraceStatus::Error);
358        assert_eq!(trace.events.len(), 1);
359        assert_eq!(trace.events[0].stage, "artifact/validate");
360        assert_eq!(trace.events[0].status, HalcTraceStatus::Error);
361        assert_eq!(trace.events[0].error.as_deref(), Some("bad magic"));
362        assert_eq!(trace.error.as_deref(), Some("bad magic"));
363        assert!(trace.result.is_none());
364    }
365}