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}