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}