Skip to main content

hara_native/vm/
schema_links.rs

1//! HBC1 exact identified-schema links layered over canonical HBC0 programs.
2//!
3//! The first identified-schema artifact epoch is external-link-only. The
4//! structural program remains one unchanged HBC0 artifact; HBC1 authenticates
5//! that artifact together with a canonical vector of exact schema coordinates.
6//! Runtime installation must resolve those coordinates through an admitted
7//! catalog and must never fall back to an unpinned schema.
8
9use sha2::{Digest, Sha256};
10use std::collections::BTreeMap;
11
12use crate::vm::artifact::{decode_program, encode_program};
13use crate::vm::program::Program;
14
15const MAGIC: &[u8; 4] = b"HBC1";
16const DIGEST_BYTES: usize = 32;
17const HASH_PREFIX: &str = "sha256:";
18
19/// One immutable identified-schema coordinate used by an HBC1 artifact.
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub struct SchemaCoordinate {
22    pub id: String,
23    pub hash: String,
24}
25
26impl SchemaCoordinate {
27    pub fn new(
28        id: impl Into<String>,
29        hash: impl Into<String>,
30    ) -> Result<Self, String> {
31        let value = Self {
32            id: id.into(),
33            hash: hash.into(),
34        };
35        validate_coordinate(&value)?;
36        Ok(value)
37    }
38}
39
40/// A decoded HBC1 program plus every exact external schema dependency.
41#[derive(Debug, Clone)]
42pub struct LinkedProgram {
43    pub program: Program,
44    pub schema_links: Vec<SchemaCoordinate>,
45}
46
47/// Encodes an unchanged HBC0 program and exact linked schema coordinates as
48/// one checksummed HBC1 artifact.
49pub fn encode_linked_program(
50    program: &Program,
51    schema_links: &[SchemaCoordinate],
52) -> Result<Vec<u8>, String> {
53    let schema_links = canonical_links(schema_links)?;
54    let nested = encode_program(program)?;
55    let mut payload = Writer::default();
56    payload.bytes(&nested)?;
57    payload.len(schema_links.len())?;
58    for coordinate in &schema_links {
59        write_coordinate(&mut payload, coordinate)?;
60    }
61    encode_envelope(&payload.bytes)
62}
63
64/// Decodes and authenticates HBC1 without resolving any catalog alias or
65/// tooling-oriented fallback view.
66pub fn decode_linked_program(bytes: &[u8]) -> Result<LinkedProgram, String> {
67    let payload = decode_envelope(bytes)?;
68    let mut reader = Reader::new(payload);
69    let nested = reader.bytes()?;
70    let program = decode_program(nested)?;
71    let schema_links = reader.many(read_coordinate)?;
72    reader.finish()?;
73    let canonical = canonical_links(&schema_links)?;
74    if canonical != schema_links {
75        return Err("linked bytecode artifact has non-canonical schema link order".into());
76    }
77    Ok(LinkedProgram {
78        program,
79        schema_links,
80    })
81}
82
83fn canonical_links(schema_links: &[SchemaCoordinate]) -> Result<Vec<SchemaCoordinate>, String> {
84    let mut values = schema_links.to_vec();
85    values.sort();
86    let mut identities = BTreeMap::<String, String>::new();
87    for coordinate in &values {
88        validate_coordinate(coordinate)?;
89        let identity = coordinate.id.clone();
90        if let Some(existing) = identities.insert(identity, coordinate.hash.clone()) {
91            if existing == coordinate.hash {
92                return Err("linked bytecode artifact contains duplicate schema coordinate".into());
93            }
94            return Err("linked bytecode artifact contains conflicting schema identity".into());
95        }
96    }
97    Ok(values)
98}
99
100fn validate_coordinate(coordinate: &SchemaCoordinate) -> Result<(), String> {
101    validate_id(&coordinate.id)?;
102    hash_bytes(&coordinate.hash)?;
103    Ok(())
104}
105
106fn validate_id(id: &str) -> Result<(), String> {
107    let mut parts = id.split('/');
108    let namespace = parts.next().unwrap_or_default();
109    let name = parts.next().unwrap_or_default();
110    if namespace.is_empty()
111        || name.is_empty()
112        || parts.next().is_some()
113        || id.starts_with(':')
114        || id.chars().any(char::is_whitespace)
115    {
116        return Err("linked bytecode schema id must be a qualified keyword name".into());
117    }
118    Ok(())
119}
120
121fn hash_bytes(hash: &str) -> Result<[u8; DIGEST_BYTES], String> {
122    let Some(digest) = hash.strip_prefix(HASH_PREFIX) else {
123        return Err("linked bytecode schema hash must use sha256".into());
124    };
125    if digest.len() != DIGEST_BYTES * 2
126        || !digest
127            .bytes()
128            .all(|value| value.is_ascii_digit() || (b'a'..=b'f').contains(&value))
129    {
130        return Err("linked bytecode schema hash must be canonical lowercase hex".into());
131    }
132    let mut output = [0u8; DIGEST_BYTES];
133    for (index, byte) in output.iter_mut().enumerate() {
134        let offset = index * 2;
135        *byte = u8::from_str_radix(&digest[offset..offset + 2], 16)
136            .map_err(|_| "linked bytecode schema hash is invalid")?;
137    }
138    Ok(output)
139}
140
141fn display_hash(bytes: &[u8; DIGEST_BYTES]) -> String {
142    use std::fmt::Write;
143
144    let mut output = String::with_capacity(HASH_PREFIX.len() + DIGEST_BYTES * 2);
145    output.push_str(HASH_PREFIX);
146    for byte in bytes {
147        write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
148    }
149    output
150}
151
152fn write_coordinate(out: &mut Writer, coordinate: &SchemaCoordinate) -> Result<(), String> {
153    out.string(&coordinate.id)?;
154    out.raw(&hash_bytes(&coordinate.hash)?);
155    Ok(())
156}
157
158fn read_coordinate(reader: &mut Reader<'_>) -> Result<SchemaCoordinate, String> {
159    let id = reader.string()?;
160    let digest: [u8; DIGEST_BYTES] = reader
161        .take(DIGEST_BYTES)?
162        .try_into()
163        .expect("fixed digest length");
164    SchemaCoordinate::new(id, display_hash(&digest))
165}
166
167fn encode_envelope(payload: &[u8]) -> Result<Vec<u8>, String> {
168    let digest = Sha256::digest(payload);
169    let mut output = MAGIC.to_vec();
170    output.extend_from_slice(
171        &u32::try_from(payload.len())
172            .map_err(|_| "linked bytecode artifact is too large")?
173            .to_be_bytes(),
174    );
175    output.extend_from_slice(payload);
176    output.extend_from_slice(&digest);
177    Ok(output)
178}
179
180fn decode_envelope(bytes: &[u8]) -> Result<&[u8], String> {
181    if !bytes.starts_with(MAGIC) {
182        return Err("linked bytecode artifact has invalid magic".into());
183    }
184    if bytes.len() < 8 + DIGEST_BYTES {
185        return Err("linked bytecode artifact is truncated".into());
186    }
187    let payload_len = u32::from_be_bytes(bytes[4..8].try_into().unwrap()) as usize;
188    let payload_end = 8usize
189        .checked_add(payload_len)
190        .ok_or("linked bytecode artifact length overflow")?;
191    if payload_end.checked_add(DIGEST_BYTES) != Some(bytes.len()) {
192        return Err("linked bytecode artifact length mismatch".into());
193    }
194    let payload = &bytes[8..payload_end];
195    if Sha256::digest(payload)[..] != bytes[payload_end..] {
196        return Err("linked bytecode artifact checksum mismatch".into());
197    }
198    Ok(payload)
199}
200
201#[derive(Default)]
202struct Writer {
203    bytes: Vec<u8>,
204}
205
206impl Writer {
207    fn raw(&mut self, value: &[u8]) {
208        self.bytes.extend_from_slice(value);
209    }
210
211    fn u32(&mut self, value: u32) {
212        self.raw(&value.to_be_bytes());
213    }
214
215    fn len(&mut self, value: usize) -> Result<(), String> {
216        self.u32(u32::try_from(value).map_err(|_| "linked bytecode field is too large")?);
217        Ok(())
218    }
219
220    fn bytes(&mut self, value: &[u8]) -> Result<(), String> {
221        self.len(value.len())?;
222        self.raw(value);
223        Ok(())
224    }
225
226    fn string(&mut self, value: &str) -> Result<(), String> {
227        self.bytes(value.as_bytes())
228    }
229}
230
231struct Reader<'a> {
232    bytes: &'a [u8],
233    cursor: usize,
234}
235
236impl<'a> Reader<'a> {
237    fn new(bytes: &'a [u8]) -> Self {
238        Self { bytes, cursor: 0 }
239    }
240
241    fn take(&mut self, size: usize) -> Result<&'a [u8], String> {
242        let end = self
243            .cursor
244            .checked_add(size)
245            .ok_or("linked bytecode artifact length overflow")?;
246        if end > self.bytes.len() {
247            return Err("linked bytecode artifact is truncated".into());
248        }
249        let value = &self.bytes[self.cursor..end];
250        self.cursor = end;
251        Ok(value)
252    }
253
254    fn u32(&mut self) -> Result<u32, String> {
255        Ok(u32::from_be_bytes(self.take(4)?.try_into().unwrap()))
256    }
257
258    fn bytes(&mut self) -> Result<&'a [u8], String> {
259        let size = self.u32()? as usize;
260        self.take(size)
261    }
262
263    fn string(&mut self) -> Result<String, String> {
264        String::from_utf8(self.bytes()?.to_vec())
265            .map_err(|_| "linked bytecode artifact contains invalid UTF-8".into())
266    }
267
268    fn many<T>(
269        &mut self,
270        mut read: impl FnMut(&mut Reader<'a>) -> Result<T, String>,
271    ) -> Result<Vec<T>, String> {
272        let size = self.u32()? as usize;
273        let mut values = Vec::with_capacity(size.min(4096));
274        for _ in 0..size {
275            values.push(read(self)?);
276        }
277        Ok(values)
278    }
279
280    fn finish(&self) -> Result<(), String> {
281        if self.cursor == self.bytes.len() {
282            Ok(())
283        } else {
284            Err("linked bytecode artifact has trailing payload bytes".into())
285        }
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use crate::vm::compile_source;
293
294    fn coordinate(id: &str, digit: char) -> SchemaCoordinate {
295        SchemaCoordinate::new(
296            id,
297            format!("sha256:{}", digit.to_string().repeat(64)),
298        )
299        .unwrap()
300    }
301
302    #[test]
303    fn exact_schema_links_round_trip_canonically() {
304        let program = compile_source("(+ 19 23)").unwrap();
305        let account = coordinate("model/account", '2');
306        let identifier = coordinate("model/id", '1');
307        let encoded =
308            encode_linked_program(&program, &[identifier.clone(), account.clone()]).unwrap();
309        assert!(encoded.starts_with(b"HBC1"));
310        let decoded = decode_linked_program(&encoded).unwrap();
311        assert_eq!(decoded.schema_links, vec![account, identifier]);
312        assert_eq!(
313            encode_program(&decoded.program).unwrap(),
314            encode_program(&program).unwrap()
315        );
316        assert_eq!(
317            encode_linked_program(&decoded.program, &decoded.schema_links).unwrap(),
318            encoded
319        );
320    }
321
322    #[test]
323    fn duplicate_and_conflicting_schema_links_are_rejected() {
324        let program = compile_source("42").unwrap();
325        let first = coordinate("model/id", '1');
326        assert_eq!(
327            encode_linked_program(&program, &[first.clone(), first.clone()]).unwrap_err(),
328            "linked bytecode artifact contains duplicate schema coordinate"
329        );
330        let conflicting = coordinate("model/id", '2');
331        assert_eq!(
332            encode_linked_program(&program, &[first, conflicting]).unwrap_err(),
333            "linked bytecode artifact contains conflicting schema identity"
334        );
335    }
336
337    #[test]
338    fn malformed_coordinates_are_rejected_before_encoding() {
339        assert!(
340            SchemaCoordinate::new("unqualified", format!("sha256:{}", "1".repeat(64)))
341                .unwrap_err()
342                .contains("qualified keyword name")
343        );
344        assert!(SchemaCoordinate::new("model/id", "sha256:BAD")
345            .unwrap_err()
346            .contains("canonical lowercase hex"));
347    }
348
349    #[test]
350    fn corruption_is_rejected_before_nested_program_decode() {
351        let program = compile_source("42").unwrap();
352        let mut encoded =
353            encode_linked_program(&program, &[coordinate("model/id", '1')]).unwrap();
354        encoded[12] ^= 1;
355        assert_eq!(
356            decode_linked_program(&encoded).unwrap_err(),
357            "linked bytecode artifact checksum mismatch"
358        );
359    }
360
361    #[test]
362    fn hbc0_is_not_silently_treated_as_a_linked_artifact() {
363        let program = compile_source("42").unwrap();
364        let encoded = encode_program(&program).unwrap();
365        assert_eq!(
366            decode_linked_program(&encoded).unwrap_err(),
367            "linked bytecode artifact has invalid magic"
368        );
369    }
370}