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        HalcOrigin::LegacyHir => "legacy-hir",
100    }
101}
102
103pub fn inspect_halc_artifact(bytes: &[u8]) -> Result<HalcArtifactInspection, String> {
104    let module = decode_halc(bytes)?;
105    if bytes.len() < HEADER_BYTES {
106        return Err("truncated artifact".into());
107    }
108
109    let mut schema_definitions: Vec<String> = module.schemas.definitions.keys().cloned().collect();
110    schema_definitions.sort();
111    let mut schema_functions: Vec<String> = module.schemas.functions.keys().cloned().collect();
112    schema_functions.sort();
113
114    Ok(HalcArtifactInspection {
115        origin: module.origin,
116        format_version: read_u16(bytes, VERSION_OFFSET),
117        flags: read_u16(bytes, FLAGS_OFFSET),
118        payload_length: read_u32(bytes, PAYLOAD_LENGTH_OFFSET),
119        payload_checksum: hex(&bytes[CHECKSUM_OFFSET..HEADER_BYTES]),
120        namespace: module.namespace,
121        resource: module.resource,
122        source_hash: hex(&module.source_hash),
123        form_count: module.forms.len(),
124        schema_definitions,
125        schema_functions,
126    })
127}
128
129fn string(value: impl Into<String>) -> HalcTraceValue {
130    HalcTraceValue::String(value.into())
131}
132
133fn integer(value: usize) -> HalcTraceValue {
134    HalcTraceValue::Integer(value as u64)
135}
136
137fn event(
138    id: u64,
139    stage: &'static str,
140    status: HalcTraceStatus,
141    evidence: HalcTraceEvidence,
142    error: Option<String>,
143) -> HalcTraceEvent {
144    HalcTraceEvent {
145        id,
146        sequence: id,
147        stage,
148        status,
149        evidence,
150        error,
151    }
152}
153
154fn successful_trace(id: String, inspection: HalcArtifactInspection) -> HalcArtifactTrace {
155    let mut module_identity = HalcTraceEvidence::new();
156    module_identity.insert("module/namespace".into(), string(&inspection.namespace));
157    module_identity.insert("module/resource".into(), string(&inspection.resource));
158    module_identity.insert("source/hash".into(), string(&inspection.source_hash));
159    module_identity.insert("form/count".into(), integer(inspection.form_count));
160
161    let mut schema_index = HalcTraceEvidence::new();
162    schema_index.insert(
163        "schema/definitions".into(),
164        HalcTraceValue::Strings(inspection.schema_definitions.clone()),
165    );
166    schema_index.insert(
167        "schema/functions".into(),
168        HalcTraceValue::Strings(inspection.schema_functions.clone()),
169    );
170
171    let mut envelope = HalcTraceEvidence::new();
172    envelope.insert(
173        "artifact/origin".into(),
174        string(origin_name(inspection.origin)),
175    );
176    envelope.insert(
177        "format/version".into(),
178        HalcTraceValue::Integer(inspection.format_version as u64),
179    );
180    envelope.insert(
181        "format/flags".into(),
182        HalcTraceValue::Integer(inspection.flags as u64),
183    );
184    envelope.insert(
185        "payload/bytes".into(),
186        HalcTraceValue::Integer(inspection.payload_length as u64),
187    );
188    envelope.insert(
189        "payload/checksum".into(),
190        string(&inspection.payload_checksum),
191    );
192
193    let mut validation = HalcTraceEvidence::new();
194    validation.insert("artifact/valid".into(), HalcTraceValue::Boolean(true));
195    validation.insert(
196        "payload/checksum".into(),
197        string(&inspection.payload_checksum),
198    );
199
200    let mut decoded = HalcTraceEvidence::new();
201    decoded.insert("decode/parity".into(), HalcTraceValue::Boolean(true));
202    decoded.insert("form/count".into(), integer(inspection.form_count));
203
204    let events = vec![
205        event(
206            1,
207            "module/identity",
208            HalcTraceStatus::Ok,
209            module_identity.clone(),
210            None,
211        ),
212        event(
213            2,
214            "schema/index",
215            HalcTraceStatus::Ok,
216            schema_index.clone(),
217            None,
218        ),
219        event(
220            3,
221            "envelope/build",
222            HalcTraceStatus::Ok,
223            envelope.clone(),
224            None,
225        ),
226        event(
227            4,
228            "artifact/validate",
229            HalcTraceStatus::Ok,
230            validation,
231            None,
232        ),
233        event(5, "artifact/decode", HalcTraceStatus::Ok, decoded, None),
234    ];
235
236    let mut result = module_identity;
237    result.extend(schema_index);
238    result.extend(envelope);
239    result.insert("decode/parity".into(), HalcTraceValue::Boolean(true));
240
241    HalcArtifactTrace {
242        schema: HALC_TRACE_SCHEMA,
243        id,
244        status: HalcTraceStatus::Ok,
245        events,
246        result: Some(result),
247        error: None,
248    }
249}
250
251fn failed_trace(id: String, bytes: &[u8], error: String) -> HalcArtifactTrace {
252    let mut evidence = HalcTraceEvidence::new();
253    evidence.insert("artifact/bytes".into(), integer(bytes.len()));
254    let failure = event(
255        1,
256        "artifact/validate",
257        HalcTraceStatus::Error,
258        evidence,
259        Some(error.clone()),
260    );
261    HalcArtifactTrace {
262        schema: HALC_TRACE_SCHEMA,
263        id,
264        status: HalcTraceStatus::Error,
265        events: vec![failure],
266        result: None,
267        error: Some(error),
268    }
269}
270
271pub fn trace_halc_artifact(id: impl Into<String>, bytes: &[u8]) -> HalcArtifactTrace {
272    let id = id.into();
273    let id = if id.is_empty() {
274        "halc-artifact".to_owned()
275    } else {
276        id
277    };
278    match inspect_halc_artifact(bytes) {
279        Ok(inspection) => successful_trace(id, inspection),
280        Err(error) => failed_trace(id, bytes, error),
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::kernel::{halc::encode_halc_module, parse_forms};
288
289    fn artifact() -> Vec<u8> {
290        let source = "(ns demo.schema) \
291                      (def Customer [:map [:id :int]]) \
292                      (defn ^{:schema #'-/Customer} customer-id [customer] customer)";
293        encode_halc_module(
294            "demo.schema",
295            "demo/schema.hal",
296            source,
297            parse_forms(source).unwrap(),
298        )
299        .unwrap()
300    }
301
302    #[test]
303    fn inspects_and_traces_the_production_halc_artifact() {
304        let artifact = artifact();
305        let inspection = inspect_halc_artifact(&artifact).unwrap();
306        assert_eq!(inspection.origin, HalcOrigin::Halc);
307        assert_eq!(inspection.format_version, 1);
308        assert_eq!(inspection.flags, 1);
309        assert_eq!(inspection.namespace, "demo.schema");
310        assert_eq!(inspection.resource, "demo/schema.hal");
311        assert_eq!(inspection.form_count, 3);
312        assert_eq!(
313            inspection.schema_definitions,
314            vec!["demo.schema/Customer".to_owned()]
315        );
316        assert_eq!(
317            inspection.schema_functions,
318            vec!["demo.schema/customer-id".to_owned()]
319        );
320
321        let trace = trace_halc_artifact("trace-1", &artifact);
322        assert_eq!(trace.schema, HALC_TRACE_SCHEMA);
323        assert_eq!(trace.status, HalcTraceStatus::Ok);
324        assert_eq!(
325            trace
326                .events
327                .iter()
328                .map(|event| event.stage)
329                .collect::<Vec<_>>(),
330            vec![
331                "module/identity",
332                "schema/index",
333                "envelope/build",
334                "artifact/validate",
335                "artifact/decode",
336            ]
337        );
338        assert_eq!(
339            trace.result.as_ref().unwrap().get("module/namespace"),
340            Some(&HalcTraceValue::String("demo.schema".to_owned()))
341        );
342        assert_eq!(
343            trace.result.as_ref().unwrap().get("decode/parity"),
344            Some(&HalcTraceValue::Boolean(true))
345        );
346    }
347
348    #[test]
349    fn classifies_legacy_hir_without_weakening_validation() {
350        let mut artifact = artifact();
351        artifact[..MAGIC_BYTES].copy_from_slice(b"HIR\0");
352        let inspection = inspect_halc_artifact(&artifact).unwrap();
353        assert_eq!(inspection.origin, HalcOrigin::LegacyHir);
354        assert_eq!(inspection.format_version, 1);
355        assert_eq!(inspection.flags, 1);
356    }
357
358    #[test]
359    fn invalid_artifacts_become_precise_validation_failure_traces() {
360        let trace = trace_halc_artifact("bad", b"NOPE");
361        assert_eq!(trace.status, HalcTraceStatus::Error);
362        assert_eq!(trace.events.len(), 1);
363        assert_eq!(trace.events[0].stage, "artifact/validate");
364        assert_eq!(trace.events[0].status, HalcTraceStatus::Error);
365        assert_eq!(trace.events[0].error.as_deref(), Some("bad magic"));
366        assert_eq!(trace.error.as_deref(), Some("bad magic"));
367        assert!(trace.result.is_none());
368    }
369}