Skip to main content

knolo_core_rust/
lib.rs

1use std::collections::{HashMap, HashSet};
2use std::error::Error;
3use std::fmt::{Display, Formatter};
4
5#[derive(Debug, Clone)]
6pub enum KnoloError {
7    InvalidPack(String),
8}
9
10impl Display for KnoloError {
11    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
12        match self {
13            KnoloError::InvalidPack(msg) => write!(f, "invalid pack: {msg}"),
14        }
15    }
16}
17
18impl Error for KnoloError {}
19
20#[derive(Debug, Clone)]
21pub struct PackMeta {
22    pub version: u32,
23    pub stats: PackStats,
24}
25
26#[derive(Debug, Clone)]
27pub struct PackStats {
28    pub docs: usize,
29    pub blocks: usize,
30    pub terms: usize,
31    pub avg_block_len: Option<f64>,
32}
33
34#[derive(Debug, Clone)]
35pub struct Pack {
36    pub meta: PackMeta,
37    pub lexicon: HashMap<String, u32>,
38    pub postings: Vec<u32>,
39    pub blocks: Vec<String>,
40    pub headings: Vec<Option<String>>,
41    pub doc_ids: Vec<Option<String>>,
42    pub namespaces: Vec<Option<String>>,
43    pub block_token_lens: Vec<usize>,
44    pub metadata_json: String,
45    pub claims_json: Option<String>,
46}
47
48#[derive(Debug, Clone)]
49pub struct QueryOptions {
50    pub top_k: usize,
51    pub min_score: f64,
52    pub namespace: Option<Vec<String>>,
53    pub source: Option<Vec<String>>,
54}
55
56impl Default for QueryOptions {
57    fn default() -> Self {
58        Self {
59            top_k: 10,
60            min_score: 0.0,
61            namespace: None,
62            source: None,
63        }
64    }
65}
66
67#[derive(Debug, Clone, PartialEq)]
68pub struct Hit {
69    pub block_id: usize,
70    pub score: f64,
71    pub text: String,
72    pub source: Option<String>,
73    pub namespace: Option<String>,
74}
75
76pub fn mount_pack_from_bytes(bytes: &[u8]) -> Result<Pack, KnoloError> {
77    let mut cursor = 0usize;
78
79    let meta_len = read_u32(bytes, &mut cursor)? as usize;
80    let meta_json = read_slice(bytes, &mut cursor, meta_len)?;
81    let metadata_json = std::str::from_utf8(meta_json).map_err(|_| KnoloError::InvalidPack("meta utf8".into()))?.to_string();
82    let meta = parse_meta(&metadata_json)?;
83
84    let lex_len = read_u32(bytes, &mut cursor)? as usize;
85    let lex_json = read_slice(bytes, &mut cursor, lex_len)?;
86    let lexicon = parse_lexicon(std::str::from_utf8(lex_json).map_err(|_| KnoloError::InvalidPack("lexicon utf8".into()))?)?;
87
88    let post_count = read_u32(bytes, &mut cursor)? as usize;
89    let postings = read_u32_array(bytes, &mut cursor, post_count)?;
90
91    let blocks_len = read_u32(bytes, &mut cursor)? as usize;
92    let blocks_json = read_slice(bytes, &mut cursor, blocks_len)?;
93    let blocks_str = std::str::from_utf8(blocks_json).map_err(|_| KnoloError::InvalidPack("blocks utf8".into()))?;
94    let parsed_blocks = parse_blocks(blocks_str)?;
95
96    Ok(Pack {
97        meta,
98        lexicon,
99        postings,
100        blocks: parsed_blocks.texts,
101        headings: parsed_blocks.headings,
102        doc_ids: parsed_blocks.doc_ids,
103        namespaces: parsed_blocks.namespaces,
104        block_token_lens: parsed_blocks.lens,
105        metadata_json,
106        claims_json: None,
107    })
108}
109
110pub fn query(pack: &Pack, q: &str, opts: QueryOptions) -> Vec<Hit> {
111    if q.trim().is_empty() {
112        return vec![];
113    }
114    let tokens = tokenize(q);
115    if tokens.is_empty() {
116        return vec![];
117    }
118
119    let term_ids = tokens
120        .iter()
121        .filter_map(|t| pack.lexicon.get(t).copied())
122        .collect::<HashSet<_>>();
123    if term_ids.is_empty() {
124        return vec![];
125    }
126
127    let namespace_filter = normalize_filter(opts.namespace.as_ref());
128    let source_filter = normalize_filter(opts.source.as_ref());
129
130    let mut candidates: HashMap<usize, HashMap<u32, f64>> = HashMap::new();
131    let mut dfs: HashMap<u32, usize> = HashMap::new();
132    let uses_offset_block_ids = pack.meta.version >= 3;
133
134    let mut i = 0usize;
135    while i < pack.postings.len() {
136        let tid = pack.postings[i];
137        i += 1;
138        if tid == 0 {
139            continue;
140        }
141        let relevant = term_ids.contains(&tid);
142        let mut term_df = 0usize;
143
144        if i >= pack.postings.len() { break; }
145        let mut encoded_bid = pack.postings[i];
146        i += 1;
147
148        while encoded_bid != 0 && i < pack.postings.len() {
149            let bid = if uses_offset_block_ids {
150                encoded_bid.saturating_sub(1) as usize
151            } else {
152                encoded_bid as usize
153            };
154
155            let mut tf = 0usize;
156            while i < pack.postings.len() {
157                let pos = pack.postings[i];
158                i += 1;
159                if pos == 0 {
160                    break;
161                }
162                tf += 1;
163            }
164
165            term_df += 1;
166            if relevant && bid < pack.blocks.len() {
167                let entry = candidates.entry(bid).or_default();
168                *entry.entry(tid).or_insert(0.0) += tf as f64;
169            }
170
171            if i >= pack.postings.len() { break; }
172            encoded_bid = pack.postings[i];
173            i += 1;
174        }
175
176        if relevant {
177            dfs.insert(tid, term_df);
178        }
179    }
180
181    if !namespace_filter.is_empty() {
182        candidates.retain(|bid, _| {
183            pack.namespaces
184                .get(*bid)
185                .and_then(|n| n.clone())
186                .map(|n| namespace_filter.contains(&normalize(&n)))
187                .unwrap_or(false)
188        });
189    }
190
191    if !source_filter.is_empty() {
192        candidates.retain(|bid, _| {
193            pack.doc_ids
194                .get(*bid)
195                .and_then(|n| n.clone())
196                .map(|n| source_filter.contains(&normalize(&n)))
197                .unwrap_or(false)
198        });
199    }
200
201    let doc_count = pack.meta.stats.blocks.max(1) as f64;
202    let avg_len = pack
203        .meta
204        .stats
205        .avg_block_len
206        .unwrap_or_else(|| {
207            if pack.block_token_lens.is_empty() {
208                1.0
209            } else {
210                pack.block_token_lens.iter().sum::<usize>() as f64 / pack.block_token_lens.len() as f64
211            }
212        })
213        .max(1.0);
214
215    let mut scored = candidates
216        .into_iter()
217        .map(|(bid, tf_map)| {
218            let mut score = 0.0;
219            let len = *pack.block_token_lens.get(bid).unwrap_or(&1) as f64;
220            for (tid, tf) in tf_map {
221                let df = *dfs.get(&tid).unwrap_or(&0) as f64;
222                let idf = (1.0 + (doc_count - df + 0.5) / (df + 0.5)).ln();
223                let k1 = 1.5;
224                let b = 0.75;
225                let numer = tf * (k1 + 1.0);
226                let denom = tf + k1 * (1.0 - b + b * (len / avg_len));
227                score += idf * (numer / denom);
228            }
229            (bid, score)
230        })
231        .filter(|(_, score)| *score >= opts.min_score)
232        .collect::<Vec<_>>();
233
234    scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
235
236    scored
237        .into_iter()
238        .take(opts.top_k.max(1))
239        .map(|(bid, score)| Hit {
240            block_id: bid,
241            score,
242            text: pack.blocks.get(bid).cloned().unwrap_or_default(),
243            source: pack.doc_ids.get(bid).and_then(|s| s.clone()),
244            namespace: pack.namespaces.get(bid).and_then(|s| s.clone()),
245        })
246        .collect()
247}
248
249struct ParsedBlocks {
250    texts: Vec<String>,
251    headings: Vec<Option<String>>,
252    doc_ids: Vec<Option<String>>,
253    namespaces: Vec<Option<String>>,
254    lens: Vec<usize>,
255}
256
257fn parse_meta(json: &str) -> Result<PackMeta, KnoloError> {
258    Ok(PackMeta {
259        version: parse_u32_field(json, "version")?,
260        stats: PackStats {
261            docs: parse_u32_field(json, "docs")? as usize,
262            blocks: parse_u32_field(json, "blocks")? as usize,
263            terms: parse_u32_field(json, "terms")? as usize,
264            avg_block_len: parse_f64_field(json, "avgBlockLen"),
265        },
266    })
267}
268
269fn parse_lexicon(json: &str) -> Result<HashMap<String, u32>, KnoloError> {
270    let mut map = HashMap::new();
271    let s = compact(json);
272    let mut i = 0usize;
273    while let Some(start) = s[i..].find("[\"") {
274        let abs = i + start + 2;
275        let rest = &s[abs..];
276        let end = rest.find('"').ok_or_else(|| KnoloError::InvalidPack("lexicon key".into()))?;
277        let key = rest[..end].to_string();
278        let rest2 = &rest[end + 1..];
279        let comma = rest2.find(',').ok_or_else(|| KnoloError::InvalidPack("lexicon comma".into()))?;
280        let rest3 = &rest2[comma + 1..];
281        let mut n = String::new();
282        for ch in rest3.chars() {
283            if ch.is_ascii_digit() {
284                n.push(ch);
285            } else {
286                break;
287            }
288        }
289        if !n.is_empty() {
290            map.insert(key, n.parse::<u32>().map_err(|_| KnoloError::InvalidPack("lexicon tid".into()))?);
291        }
292        i = abs + end + 1;
293    }
294    Ok(map)
295}
296
297fn parse_blocks(json: &str) -> Result<ParsedBlocks, KnoloError> {
298    let s = compact(json);
299    if s.starts_with("[\"") {
300        let mut texts = Vec::new();
301        let mut i = 2usize;
302        while i < s.len() {
303            if let Some(end) = s[i..].find('"') {
304                let piece = &s[i..i + end];
305                texts.push(unescape(piece));
306                i += end + 1;
307                if let Some(next) = s[i..].find('"') {
308                    i += next + 1;
309                } else {
310                    break;
311                }
312            } else {
313                break;
314            }
315        }
316        let lens = texts.iter().map(|t| tokenize(t).len()).collect::<Vec<_>>();
317        return Ok(ParsedBlocks {
318            headings: vec![None; texts.len()],
319            doc_ids: vec![None; texts.len()],
320            namespaces: vec![None; texts.len()],
321            lens,
322            texts,
323        });
324    }
325
326    let objects = split_top_level_objects(&s)?;
327    let mut texts = Vec::new();
328    let mut headings = Vec::new();
329    let mut doc_ids = Vec::new();
330    let mut namespaces = Vec::new();
331    let mut lens = Vec::new();
332
333    for obj in objects {
334        let text = parse_string_or_null(&obj, "text").unwrap_or_default();
335        let len = parse_u32_field_optional(&obj, "len").map(|v| v as usize).unwrap_or_else(|| tokenize(&text).len());
336        texts.push(text);
337        headings.push(parse_string_or_null(&obj, "heading"));
338        doc_ids.push(parse_string_or_null(&obj, "docId"));
339        namespaces.push(parse_string_or_null(&obj, "namespace"));
340        lens.push(len);
341    }
342
343    Ok(ParsedBlocks { texts, headings, doc_ids, namespaces, lens })
344}
345
346fn split_top_level_objects(s: &str) -> Result<Vec<String>, KnoloError> {
347    let mut out = Vec::new();
348    let mut depth = 0i32;
349    let mut start = None;
350    let chars: Vec<char> = s.chars().collect();
351    for (i, ch) in chars.iter().enumerate() {
352        if *ch == '{' {
353            if depth == 0 {
354                start = Some(i);
355            }
356            depth += 1;
357        } else if *ch == '}' {
358            depth -= 1;
359            if depth == 0 {
360                if let Some(st) = start {
361                    out.push(chars[st..=i].iter().collect());
362                }
363                start = None;
364            }
365        }
366    }
367    if out.is_empty() {
368        return Err(KnoloError::InvalidPack("blocks objects".into()));
369    }
370    Ok(out)
371}
372
373fn parse_string_or_null(obj: &str, key: &str) -> Option<String> {
374    let needle = format!("\"{}\":", key);
375    let idx = obj.find(&needle)? + needle.len();
376    let tail = &obj[idx..];
377    if tail.starts_with("null") {
378        return None;
379    }
380    if !tail.starts_with('"') {
381        return None;
382    }
383    let rest = &tail[1..];
384    let end = rest.find('"')?;
385    Some(unescape(&rest[..end]))
386}
387
388fn parse_u32_field(json: &str, key: &str) -> Result<u32, KnoloError> {
389    parse_u32_field_optional(json, key).ok_or_else(|| KnoloError::InvalidPack(format!("missing {key}")))
390}
391
392fn parse_u32_field_optional(json: &str, key: &str) -> Option<u32> {
393    let needle = format!("\"{}\":", key);
394    let idx = json.find(&needle)? + needle.len();
395    let tail = &json[idx..];
396    let mut n = String::new();
397    for ch in tail.chars() {
398        if ch.is_ascii_digit() {
399            n.push(ch);
400        } else if !n.is_empty() {
401            break;
402        }
403    }
404    n.parse().ok()
405}
406
407fn parse_f64_field(json: &str, key: &str) -> Option<f64> {
408    let needle = format!("\"{}\":", key);
409    let idx = json.find(&needle)? + needle.len();
410    let tail = &json[idx..];
411    let mut n = String::new();
412    for ch in tail.chars() {
413        if ch.is_ascii_digit() || ch == '.' {
414            n.push(ch);
415        } else if !n.is_empty() {
416            break;
417        }
418    }
419    n.parse().ok()
420}
421
422fn normalize_filter(values: Option<&Vec<String>>) -> HashSet<String> {
423    values
424        .map(|arr| arr.iter().map(|s| normalize(s)).collect::<HashSet<_>>())
425        .unwrap_or_default()
426}
427
428fn normalize(s: &str) -> String {
429    s.to_lowercase().trim().to_string()
430}
431
432fn tokenize(text: &str) -> Vec<String> {
433    let mut out = Vec::new();
434    let mut cur = String::new();
435    for ch in text.chars() {
436        if ch.is_alphanumeric() {
437            cur.push(ch.to_ascii_lowercase());
438        } else if !cur.is_empty() {
439            out.push(std::mem::take(&mut cur));
440        }
441    }
442    if !cur.is_empty() {
443        out.push(cur);
444    }
445    out
446}
447
448fn compact(s: &str) -> String {
449    let mut out = String::with_capacity(s.len());
450    let mut in_string = false;
451    let mut escaped = false;
452
453    for ch in s.chars() {
454        if in_string {
455            out.push(ch);
456            if escaped {
457                escaped = false;
458            } else if ch == '\\' {
459                escaped = true;
460            } else if ch == '"' {
461                in_string = false;
462            }
463            continue;
464        }
465
466        if ch.is_whitespace() {
467            continue;
468        }
469
470        out.push(ch);
471        if ch == '"' {
472            in_string = true;
473        }
474    }
475
476    out
477}
478
479fn unescape(s: &str) -> String {
480    s.replace("\\\"", "\"")
481}
482
483fn read_u32(bytes: &[u8], cursor: &mut usize) -> Result<u32, KnoloError> {
484    let chunk = read_slice(bytes, cursor, 4)?;
485    Ok(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
486}
487
488fn read_u32_array(bytes: &[u8], cursor: &mut usize, len: usize) -> Result<Vec<u32>, KnoloError> {
489    let mut out = Vec::with_capacity(len);
490    for _ in 0..len {
491        out.push(read_u32(bytes, cursor)?);
492    }
493    Ok(out)
494}
495
496fn read_slice<'a>(bytes: &'a [u8], cursor: &mut usize, len: usize) -> Result<&'a [u8], KnoloError> {
497    let end = cursor.saturating_add(len);
498    if end > bytes.len() {
499        return Err(KnoloError::InvalidPack("unexpected end-of-buffer".into()));
500    }
501    let slice = &bytes[*cursor..end];
502    *cursor = end;
503    Ok(slice)
504}
505
506#[allow(dead_code)]
507struct VqfReader<'a> {
508    bytes: &'a [u8],
509    cursor: usize,
510}
511
512#[allow(dead_code)]
513impl<'a> VqfReader<'a> {
514    fn new(bytes: &'a [u8]) -> Self { Self { bytes, cursor: 0 } }
515    fn remaining(&self) -> usize { self.bytes.len().saturating_sub(self.cursor) }
516    fn byte(&mut self) -> Result<u8, KnoloError> {
517        let value = *self.bytes.get(self.cursor).ok_or_else(|| KnoloError::InvalidPack("truncated VQF body".into()))?;
518        self.cursor += 1;
519        Ok(value)
520    }
521    fn bytes(&mut self, length: usize) -> Result<&'a [u8], KnoloError> {
522        let end = self.cursor.checked_add(length).ok_or_else(|| KnoloError::InvalidPack("VQF length overflow".into()))?;
523        if end > self.bytes.len() { return Err(KnoloError::InvalidPack("truncated VQF body".into())); }
524        let value = &self.bytes[self.cursor..end];
525        self.cursor = end;
526        Ok(value)
527    }
528    fn uvarint(&mut self) -> Result<u64, KnoloError> {
529        let mut value = 0u64;
530        for index in 0..10 {
531            let byte = self.byte()?;
532            let payload = (byte & 0x7f) as u64;
533            if index == 9 && (byte & 0x7f) > 1 { return Err(KnoloError::InvalidPack("VQF varint overflow".into())); }
534            value |= payload << (index * 7);
535            if byte & 0x80 == 0 {
536                if index > 0 && payload == 0 { return Err(KnoloError::InvalidPack("noncanonical VQF varint".into())); }
537                return Ok(value);
538            }
539        }
540        Err(KnoloError::InvalidPack("VQF varint overflow".into()))
541    }
542    fn finish(&self) -> Result<(), KnoloError> {
543        if self.remaining() != 0 { return Err(KnoloError::InvalidPack("trailing VQF bytes".into())); }
544        Ok(())
545    }
546}
547
548#[allow(dead_code)]
549fn decode_vqf_digest_table(bytes: &[u8], max_entries: usize) -> Result<Vec<[u8; 32]>, KnoloError> {
550    let mut reader = VqfReader::new(bytes);
551    let count = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF digest count overflow".into()))?;
552    if count > max_entries || count.checked_mul(32).unwrap_or(usize::MAX) != reader.remaining() { return Err(KnoloError::InvalidPack("invalid VQF digest table length".into())); }
553    let mut values = Vec::with_capacity(count);
554    for _ in 0..count { values.push(reader.bytes(32)?.try_into().unwrap()); }
555    for pair in values.windows(2) { if pair[0] >= pair[1] { return Err(KnoloError::InvalidPack("VQF digest table is not strictly sorted".into())); } }
556    reader.finish()?;
557    Ok(values)
558}
559
560#[allow(dead_code)]
561fn decode_vqf_string_table(bytes: &[u8], max_entries: usize, max_bytes: usize) -> Result<Vec<String>, KnoloError> {
562    let mut reader = VqfReader::new(bytes);
563    let count = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF string count overflow".into()))?;
564    if count > max_entries { return Err(KnoloError::InvalidPack("VQF string table exceeds entry limit".into())); }
565    let mut values = Vec::with_capacity(count);
566    let mut previous: Option<Vec<u8>> = None;
567    for _ in 0..count {
568        let length = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF string length overflow".into()))?;
569        if length > max_bytes { return Err(KnoloError::InvalidPack("VQF string exceeds byte limit".into())); }
570        let raw = reader.bytes(length)?.to_vec();
571        if previous.as_ref().is_some_and(|item| item.as_slice() >= raw.as_slice()) { return Err(KnoloError::InvalidPack("VQF string table is not strictly sorted".into())); }
572        let value = String::from_utf8(raw.clone()).map_err(|_| KnoloError::InvalidPack("VQF string is not UTF-8".into()))?;
573        previous = Some(raw);
574        values.push(value);
575    }
576    reader.finish()?;
577    Ok(values)
578}
579
580#[allow(dead_code)]
581fn decode_vqf_blob_table(reader: &mut VqfReader<'_>, max_entries: usize, max_bytes: usize) -> Result<Vec<Vec<u8>>, KnoloError> {
582    let count = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF blob count overflow".into()))?;
583    if count > max_entries || count > reader.remaining() { return Err(KnoloError::InvalidPack("invalid VQF blob table count".into())); }
584    let mut blobs = Vec::with_capacity(count);
585    let mut total = 0usize;
586    for _ in 0..count {
587        let length = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF blob length overflow".into()))?;
588        total = total.checked_add(length).ok_or_else(|| KnoloError::InvalidPack("VQF blob bytes overflow".into()))?;
589        if total > max_bytes { return Err(KnoloError::InvalidPack("VQF blob table exceeds byte limit".into())); }
590        let blob = reader.bytes(length)?.to_vec();
591        if blobs.last().is_some_and(|previous: &Vec<u8>| previous.as_slice() >= blob.as_slice()) { return Err(KnoloError::InvalidPack("VQF blob table is not strictly sorted".into())); }
592        blobs.push(blob);
593    }
594    Ok(blobs)
595}
596
597#[allow(dead_code)]
598fn decode_vqf_object_bytes(reader: &mut VqfReader<'_>, blobs: &[Vec<u8>], source_spans: bool) -> Result<Vec<u8>, KnoloError> {
599    let mode = reader.byte()?;
600    let ordinal = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF object blob ordinal overflow".into()))?;
601    let source = blobs.get(ordinal).ok_or_else(|| KnoloError::InvalidPack("invalid VQF object blob ordinal".into()))?;
602    match mode {
603        0 => Ok(source.clone()),
604        1 if source_spans => {
605            let offset = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF source span offset overflow".into()))?;
606            let length = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF source span length overflow".into()))?;
607            let end = offset.checked_add(length).ok_or_else(|| KnoloError::InvalidPack("VQF source span overflow".into()))?;
608            if end > source.len() { return Err(KnoloError::InvalidPack("VQF source span exceeds blob".into())); }
609            Ok(source[offset..end].to_vec())
610        }
611        _ => Err(KnoloError::InvalidPack("invalid VQF object byte mode".into())),
612    }
613}
614
615#[allow(dead_code)]
616fn decode_vqf_cbor_field(reader: &mut VqfReader<'_>, max_bytes: usize) -> Result<CborValue, KnoloError> {
617    let length = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF CBOR field length overflow".into()))?;
618    if length > max_bytes { return Err(KnoloError::InvalidPack("VQF CBOR field exceeds byte limit".into())); }
619    let raw = reader.bytes(length)?;
620    let value = decode_cbor_exact(raw)?;
621    if encode_cbor(&value) != raw { return Err(KnoloError::InvalidPack("noncanonical VQF CBOR field".into())); }
622    Ok(value)
623}
624
625#[allow(dead_code)]
626fn verify_vqf_object_identity(id: &str, kind: &str, bytes: &[u8], meta: CborValue) -> Result<(), KnoloError> {
627    if !id.starts_with("sha256-") || id.len() != 71 { return Err(KnoloError::InvalidPack("invalid VQF object digest".into())); }
628    let body = CborValue::Map(vec![
629        ("bytes".into(), CborValue::Bytes(bytes.to_vec())),
630        ("kind".into(), CborValue::Text(kind.into())),
631        ("meta".into(), meta),
632    ]);
633    if digest_domain("object", &encode_cbor(&body)) != id { return Err(KnoloError::InvalidPack("VQF object identity mismatch".into())); }
634    Ok(())
635}
636
637#[allow(dead_code)]
638fn vqf_digest_string(raw: &[u8; 32]) -> String {
639    let mut value = String::from("sha256-");
640    for byte in raw { value.push_str(&format!("{byte:02x}")); }
641    value
642}
643
644#[allow(dead_code)]
645fn decode_vqf_string_value(reader: &mut VqfReader<'_>, strings: &[String]) -> Result<String, KnoloError> {
646    match reader.byte()? {
647        0 => {
648            let length = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF string length overflow".into()))?;
649            let raw = reader.bytes(length)?;
650            String::from_utf8(raw.to_vec()).map_err(|_| KnoloError::InvalidPack("VQF string is not UTF-8".into()))
651        }
652        1 => {
653            let ordinal = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF string ordinal overflow".into()))?;
654            strings.get(ordinal).cloned().ok_or_else(|| KnoloError::InvalidPack("invalid VQF string ordinal".into()))
655        }
656        _ => Err(KnoloError::InvalidPack("unknown VQF string tag".into())),
657    }
658}
659
660#[allow(dead_code)]
661fn decode_vqf_object_records(
662    reader: &mut VqfReader<'_>,
663    digests: &[[u8; 32]],
664    strings: &[String],
665    blobs: &[Vec<u8>],
666    source_spans: bool,
667    max_objects: usize,
668) -> Result<Vec<u8>, KnoloError> {
669    let count = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF object count overflow".into()))?;
670    if count > max_objects { return Err(KnoloError::InvalidPack("VQF object count exceeds limit".into())); }
671    let mut records = Vec::with_capacity(count);
672    for _ in 0..count {
673        let id_ordinal = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF digest ordinal overflow".into()))?;
674        let id = vqf_digest_string(digests.get(id_ordinal).ok_or_else(|| KnoloError::InvalidPack("invalid VQF digest ordinal".into()))?);
675        let kind = decode_vqf_string_value(reader, strings)?;
676        let bytes = decode_vqf_object_bytes(reader, blobs, source_spans)?;
677        let meta = decode_vqf_cbor_field(reader, 512 * 1024 * 1024)?;
678        let extra = decode_vqf_cbor_field(reader, 512 * 1024 * 1024)?;
679        if let CborValue::Map(entries) = &extra {
680            if entries.iter().any(|(key, _)| matches!(key.as_str(), "id" | "kind" | "bytes" | "meta")) { return Err(KnoloError::InvalidPack("VQF object extensions contain a reserved key".into())); }
681        } else { return Err(KnoloError::InvalidPack("invalid VQF object extensions".into())); }
682        verify_vqf_object_identity(&id, &kind, &bytes, meta.clone())?;
683        let mut record = match extra { CborValue::Map(entries) => entries, _ => unreachable!() };
684        record.push(("bytes".into(), CborValue::Bytes(bytes)));
685        record.push(("id".into(), CborValue::Text(id)));
686        record.push(("kind".into(), CborValue::Text(kind)));
687        record.push(("meta".into(), meta));
688        records.push(CborValue::Map(record));
689    }
690    reader.finish()?;
691    Ok(encode_cbor(&CborValue::Array(records)))
692}
693
694#[allow(dead_code)]
695fn decode_vqf_object_body(body: &[u8]) -> Result<Vec<u8>, KnoloError> {
696    let mut reader = VqfReader::new(body);
697    if reader.byte()? != 1 { return Err(KnoloError::InvalidPack("unsupported VQF object codec version".into())); }
698    let flags = reader.byte()?;
699    if flags & !1 != 0 { return Err(KnoloError::InvalidPack("unsupported VQF object codec flags".into())); }
700    let digest_len = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF digest table length overflow".into()))?;
701    let digest_table = decode_vqf_digest_table(reader.bytes(digest_len)?, 1_000_000)?;
702    let string_len = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF string table length overflow".into()))?;
703    let string_table = decode_vqf_string_table(reader.bytes(string_len)?, 1_000_000, 512 * 1024 * 1024)?;
704    let blobs = decode_vqf_blob_table(&mut reader, 1_000_000, 512 * 1024 * 1024)?;
705    decode_vqf_object_records(&mut reader, &digest_table, &string_table, &blobs, flags & 1 != 0, 1_000_000)
706}
707
708#[allow(dead_code)]
709fn decode_vqf_event_body(body: &[u8]) -> Result<Vec<u8>, KnoloError> {
710    let mut reader = VqfReader::new(body);
711    if reader.byte()? != 1 { return Err(KnoloError::InvalidPack("unsupported VQF event codec version".into())); }
712    if reader.byte()? != 0 { return Err(KnoloError::InvalidPack("unsupported VQF event codec flags".into())); }
713    let digest_len = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF digest table length overflow".into()))?;
714    let digests = decode_vqf_digest_table(reader.bytes(digest_len)?, 1_000_000)?;
715    let string_len = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF string table length overflow".into()))?;
716    let strings = decode_vqf_string_table(reader.bytes(string_len)?, 1_000_000, 512 * 1024 * 1024)?;
717    let count = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF event count overflow".into()))?;
718    if count > 1_000_000 || count > reader.remaining() { return Err(KnoloError::InvalidPack("invalid VQF event count".into())); }
719    let mut records = Vec::with_capacity(count);
720    for _ in 0..count {
721        let version = reader.uvarint()?;
722        let digest = |reader: &mut VqfReader<'_>| -> Result<String, KnoloError> {
723            let ordinal = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF digest ordinal overflow".into()))?;
724            Ok(vqf_digest_string(digests.get(ordinal).ok_or_else(|| KnoloError::InvalidPack("invalid VQF digest ordinal".into()))?))
725        };
726        let id = digest(&mut reader)?;
727        let transaction_id = digest(&mut reader)?;
728        let parent_count = usize::try_from(reader.uvarint()?).map_err(|_| KnoloError::InvalidPack("VQF parent count overflow".into()))?;
729        if parent_count > reader.remaining() { return Err(KnoloError::InvalidPack("truncated VQF event parents".into())); }
730        let mut parents = Vec::with_capacity(parent_count);
731        for _ in 0..parent_count { parents.push(digest(&mut reader)?); }
732        let actor = decode_vqf_string_value(&mut reader, &strings)?;
733        let actor_counter = reader.uvarint()?;
734        let kind = decode_vqf_string_value(&mut reader, &strings)?;
735        let target = digest(&mut reader)?;
736        let payload = digest(&mut reader)?;
737        let provenance = decode_vqf_cbor_field(&mut reader, 512 * 1024 * 1024)?;
738        let extra = decode_vqf_cbor_field(&mut reader, 512 * 1024 * 1024)?;
739        if version != 1 || actor.is_empty() || actor_counter < 1 { return Err(KnoloError::InvalidPack("invalid VQF event identity fields".into())); }
740        let provenance_map = match provenance { CborValue::Map(_) => provenance, _ => return Err(KnoloError::InvalidPack("invalid VQF event provenance".into())) };
741        let mut identity = vec![
742            ("actor".into(), CborValue::Text(actor.clone())),
743            ("actorCounter".into(), CborValue::UInt(actor_counter)),
744            ("kind".into(), CborValue::Text(kind.clone())),
745            ("parents".into(), CborValue::Array(parents.clone().into_iter().map(CborValue::Text).collect())),
746            ("payload".into(), CborValue::Text(payload.clone())),
747            ("provenance".into(), provenance_map.clone()),
748            ("target".into(), CborValue::Text(target.clone())),
749            ("transactionId".into(), CborValue::Text(transaction_id.clone())),
750            ("version".into(), CborValue::UInt(version)),
751        ];
752        if digest_domain("event", &encode_cbor(&CborValue::Map(identity.clone()))) != id { return Err(KnoloError::InvalidPack("VQF event identity mismatch".into())); }
753        let mut record = match extra { CborValue::Map(entries) => entries, _ => return Err(KnoloError::InvalidPack("invalid VQF event extensions".into())) };
754        if record.iter().any(|(key, _)| matches!(key.as_str(), "version" | "id" | "transactionId" | "parents" | "actor" | "actorCounter" | "kind" | "target" | "payload" | "provenance")) { return Err(KnoloError::InvalidPack("VQF event extensions contain a reserved key".into())); }
755        record.append(&mut identity);
756        record.push(("id".into(), CborValue::Text(id)));
757        records.push(CborValue::Map(record));
758    }
759    reader.finish()?;
760    Ok(encode_cbor(&CborValue::Array(records)))
761}
762
763// V5 read-only Knowledge Image support. The implementation intentionally has
764// no external dependencies so the format verifier can be used in offline and
765// embedded environments.
766
767pub const KNOWLEDGE_IMAGE_V5_MAGIC: &[u8; 8] = b"KNLOV5\0\0";
768const SUPERBLOCK_MAGIC: &[u8; 8] = b"KNLOSB1\0";
769const SEGMENT_MAGIC: &[u8; 4] = b"KSEG";
770const V5_HEADER_SIZE: usize = 16;
771const V5_SUPERBLOCK_SIZE: usize = 128;
772const V5_SEGMENT_HEADER_SIZE: usize = 48;
773const V5_DATA_START: usize = V5_HEADER_SIZE + V5_SUPERBLOCK_SIZE * 2;
774const V5_MAX_SEGMENT: u64 = 512 * 1024 * 1024;
775
776#[derive(Debug, Clone, PartialEq)]
777pub struct KnowledgeObjectV1 {
778    pub id: String,
779    pub kind: String,
780    pub bytes: Vec<u8>,
781    pub meta: CborValue,
782}
783
784#[derive(Debug, Clone, PartialEq)]
785pub struct KnowledgeEventV1 {
786    pub id: String,
787    pub transaction_id: String,
788    pub actor: String,
789    pub actor_counter: u64,
790    pub kind: String,
791    pub target: String,
792    pub payload: String,
793}
794
795#[derive(Debug, Clone, PartialEq)]
796pub struct KnowledgeCommitV1 {
797    pub state_root: String,
798    pub commit_digest: String,
799    pub parents: Vec<String>,
800    pub object_root: String,
801    pub event_root: String,
802    pub policy_root: String,
803    pub sequence: u64,
804    pub actor: String,
805}
806
807#[derive(Debug, Clone, PartialEq)]
808pub struct KnowledgeImageSegment {
809    pub kind: u8,
810    pub schema: u8,
811    pub flags: u16,
812    pub offset: usize,
813    pub length: usize,
814    pub payload_length: usize,
815    pub digest: String,
816}
817
818#[derive(Debug, Clone, PartialEq)]
819pub struct KnowledgeImage {
820    pub state_root: String,
821    pub commit_digest: String,
822    pub commit: KnowledgeCommitV1,
823    pub objects: Vec<KnowledgeObjectV1>,
824    pub events: Vec<KnowledgeEventV1>,
825    pub segments: Vec<KnowledgeImageSegment>,
826    pub active_superblock: char,
827}
828
829#[derive(Debug, Clone, PartialEq)]
830pub struct KnowledgeImageVerification {
831    pub valid: bool,
832    pub state_root: String,
833    pub commit_digest: String,
834    pub active_superblock: char,
835    pub segments: Vec<KnowledgeImageSegment>,
836}
837
838#[derive(Debug, Clone, PartialEq)]
839pub struct KnowledgeRuntimeDiagnosticsImageV1 {
840    pub state_root: String,
841    pub commit_digest: String,
842    pub sequence: u64,
843    pub object_count: usize,
844    pub event_count: usize,
845    pub segment_count: usize,
846    pub active_superblock: char,
847}
848
849#[derive(Debug, Clone, PartialEq)]
850pub struct KnowledgeRuntimeDiagnosticsV1 {
851    pub version: u64,
852    pub valid: bool,
853    pub image: KnowledgeRuntimeDiagnosticsImageV1,
854    pub diagnostics_root: String,
855}
856
857#[derive(Debug, Clone, PartialEq)]
858pub struct KnowledgeStudioCapabilitiesV1 {
859    pub inspect_image: bool,
860    pub verify_image: bool,
861    pub inspect_query_index: bool,
862    pub inspect_query_history: bool,
863    pub inspect_run: bool,
864    pub inspect_replay: bool,
865    pub mutate_image: bool,
866}
867
868#[derive(Debug, Clone, PartialEq)]
869pub struct KnowledgeStudioManagementV1 {
870    pub version: u64,
871    pub surface: String,
872    pub valid: bool,
873    pub read_only: bool,
874    pub diagnostics: KnowledgeRuntimeDiagnosticsV1,
875    pub capabilities: KnowledgeStudioCapabilitiesV1,
876    pub management_root: String,
877}
878
879#[derive(Debug, Clone, PartialEq)]
880pub struct MigrationMapping {
881    pub legacy_block_id: usize,
882    pub source_object: String,
883    pub chunk_object: String,
884}
885
886#[derive(Debug, Clone, PartialEq)]
887pub struct MigrationResult {
888    pub image: Vec<u8>,
889    pub receipt: Vec<u8>,
890    pub state_root: String,
891    pub mappings: Vec<MigrationMapping>,
892}
893
894#[derive(Debug, Clone, PartialEq)]
895pub enum CborValue {
896    Null,
897    Bool(bool),
898    UInt(u64),
899    NInt(i64),
900    Bytes(Vec<u8>),
901    Text(String),
902    Array(Vec<CborValue>),
903    Map(Vec<(String, CborValue)>),
904}
905
906pub fn mount_knowledge_image(bytes: &[u8]) -> Result<KnowledgeImage, KnoloError> {
907    let parsed = parse_v5_image(bytes)?;
908    Ok(parsed)
909}
910
911pub fn inspect_knowledge_image(bytes: &[u8]) -> Result<KnowledgeImageVerification, KnoloError> {
912    let image = mount_knowledge_image(bytes)?;
913    Ok(KnowledgeImageVerification {
914        valid: true,
915        state_root: image.state_root,
916        commit_digest: image.commit_digest,
917        active_superblock: image.active_superblock,
918        segments: image.segments,
919    })
920}
921
922/// Validates the VQF-1 envelope boundary before a codec-specific body reader.
923pub fn inspect_vqf_envelope(bytes: &[u8], expected_codec_kind: u8) -> Result<(u64, u64), KnoloError> {
924    const HEADER: usize = 56;
925    if bytes.len() < HEADER || &bytes[0..4] != b"VQF1" { return Err(KnoloError::InvalidPack("invalid VQF envelope".into())); }
926    if bytes[4] != 1 || bytes[5] != expected_codec_kind { return Err(KnoloError::InvalidPack("unsupported VQF envelope version or codec".into())); }
927    if bytes[6] != 0 || bytes[7] != 0 { return Err(KnoloError::InvalidPack("unsupported VQF envelope flags".into())); }
928    let logical = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
929    let body_len = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
930    if body_len != (bytes.len() - HEADER) as u64 { return Err(KnoloError::InvalidPack("invalid VQF envelope length".into())); }
931    let body = &bytes[HEADER..];
932    if digest_raw(&digest_domain("vqf-physical", body)) != bytes[24..56] { return Err(KnoloError::InvalidPack("VQF physical body digest mismatch".into())); }
933    Ok((logical, body_len))
934}
935
936pub fn verify_knowledge_image(bytes: &[u8]) -> Result<KnowledgeImageVerification, KnoloError> {
937    inspect_knowledge_image(bytes)
938}
939
940pub fn inspect_knowledge_runtime_v5(bytes: &[u8]) -> Result<KnowledgeRuntimeDiagnosticsV1, KnoloError> {
941    let image = mount_knowledge_image(bytes)?;
942    let diagnostics = KnowledgeRuntimeDiagnosticsV1 {
943        version: 1,
944        valid: true,
945        image: KnowledgeRuntimeDiagnosticsImageV1 {
946            state_root: image.state_root,
947            commit_digest: image.commit_digest,
948            sequence: image.commit.sequence,
949            object_count: image.objects.len(),
950            event_count: image.events.len(),
951            segment_count: image.segments.len(),
952            active_superblock: image.active_superblock,
953        },
954        diagnostics_root: String::new(),
955    };
956    let diagnostics_root = runtime_diagnostics_root_v5(&diagnostics);
957    Ok(KnowledgeRuntimeDiagnosticsV1 { diagnostics_root, ..diagnostics })
958}
959
960pub fn runtime_diagnostics_root_v5(diagnostics: &KnowledgeRuntimeDiagnosticsV1) -> String {
961    digest_domain("runtime-diagnostics", &encode_cbor(&runtime_diagnostics_body(diagnostics)))
962}
963
964pub fn inspect_knowledge_studio_management_v5(bytes: &[u8]) -> Result<KnowledgeStudioManagementV1, KnoloError> {
965    let diagnostics = inspect_knowledge_runtime_v5(bytes)?;
966    let management = KnowledgeStudioManagementV1 {
967        version: 1,
968        surface: "studio-management".into(),
969        valid: true,
970        read_only: true,
971        diagnostics,
972        capabilities: KnowledgeStudioCapabilitiesV1 {
973            inspect_image: true,
974            verify_image: true,
975            inspect_query_index: false,
976            inspect_query_history: false,
977            inspect_run: false,
978            inspect_replay: false,
979            mutate_image: false,
980        },
981        management_root: String::new(),
982    };
983    let management_root = studio_management_root_v5(&management);
984    Ok(KnowledgeStudioManagementV1 { management_root, ..management })
985}
986
987pub fn studio_management_root_v5(management: &KnowledgeStudioManagementV1) -> String {
988    digest_domain("studio-management", &encode_cbor(&CborValue::Map(vec![
989        ("capabilities".into(), studio_capabilities_value(&management.capabilities)),
990        ("diagnostics".into(), runtime_diagnostics_value(&management.diagnostics)),
991        ("readOnly".into(), CborValue::Bool(management.read_only)),
992        ("surface".into(), CborValue::Text(management.surface.clone())),
993        ("valid".into(), CborValue::Bool(management.valid)),
994        ("version".into(), CborValue::UInt(management.version)),
995    ])))
996}
997
998fn runtime_diagnostics_body(diagnostics: &KnowledgeRuntimeDiagnosticsV1) -> CborValue {
999    CborValue::Map(vec![
1000        ("image".into(), CborValue::Map(vec![
1001            ("activeSuperblock".into(), CborValue::Text(diagnostics.image.active_superblock.to_string())),
1002            ("commitDigest".into(), CborValue::Text(diagnostics.image.commit_digest.clone())),
1003            ("eventCount".into(), CborValue::UInt(diagnostics.image.event_count as u64)),
1004            ("objectCount".into(), CborValue::UInt(diagnostics.image.object_count as u64)),
1005            ("segmentCount".into(), CborValue::UInt(diagnostics.image.segment_count as u64)),
1006            ("sequence".into(), CborValue::UInt(diagnostics.image.sequence)),
1007            ("stateRoot".into(), CborValue::Text(diagnostics.image.state_root.clone())),
1008        ])),
1009        ("valid".into(), CborValue::Bool(diagnostics.valid)),
1010        ("version".into(), CborValue::UInt(diagnostics.version)),
1011    ])
1012}
1013
1014fn runtime_diagnostics_value(diagnostics: &KnowledgeRuntimeDiagnosticsV1) -> CborValue {
1015    let mut entries = match runtime_diagnostics_body(diagnostics) {
1016        CborValue::Map(entries) => entries,
1017        _ => Vec::new(),
1018    };
1019    entries.push(("diagnosticsRoot".into(), CborValue::Text(diagnostics.diagnostics_root.clone())));
1020    CborValue::Map(entries)
1021}
1022
1023fn studio_capabilities_value(capabilities: &KnowledgeStudioCapabilitiesV1) -> CborValue {
1024    CborValue::Map(vec![
1025        ("inspectImage".into(), CborValue::Bool(capabilities.inspect_image)),
1026        ("inspectQueryHistory".into(), CborValue::Bool(capabilities.inspect_query_history)),
1027        ("inspectQueryIndex".into(), CborValue::Bool(capabilities.inspect_query_index)),
1028        ("inspectReplay".into(), CborValue::Bool(capabilities.inspect_replay)),
1029        ("inspectRun".into(), CborValue::Bool(capabilities.inspect_run)),
1030        ("mutateImage".into(), CborValue::Bool(capabilities.mutate_image)),
1031        ("verifyImage".into(), CborValue::Bool(capabilities.verify_image)),
1032    ])
1033}
1034
1035pub fn state_root(image: &KnowledgeImage) -> &str {
1036    &image.state_root
1037}
1038
1039#[derive(Debug, Clone, PartialEq)]
1040pub struct KnowledgeQueryFilterV1 {
1041    pub field: String,
1042    pub value: CborValue,
1043}
1044
1045#[derive(Debug, Clone, PartialEq)]
1046pub struct KnowledgeQueryPlanV1 {
1047    pub kind: Option<String>,
1048    pub filters: Vec<KnowledgeQueryFilterV1>,
1049    pub search: Option<String>,
1050    pub limit: usize,
1051}
1052
1053#[derive(Debug, Clone, PartialEq)]
1054pub struct KnowledgeQueryHitV1 {
1055    pub object_id: String,
1056    pub kind: String,
1057}
1058
1059#[derive(Debug, Clone, PartialEq)]
1060pub struct KnowledgeQueryResultV1 {
1061    pub state_root: String,
1062    pub plan_root: String,
1063    pub hits: Vec<KnowledgeQueryHitV1>,
1064    pub result_root: String,
1065}
1066
1067#[derive(Debug, Clone, PartialEq)]
1068pub struct KnowledgePolicyRuleV1 {
1069    pub effect: String,
1070    pub action: String,
1071    pub principal: Option<String>,
1072    pub kind: Option<String>,
1073}
1074
1075#[derive(Debug, Clone, PartialEq)]
1076pub struct KnowledgePolicyV1 {
1077    pub default: String,
1078    pub rules: Vec<KnowledgePolicyRuleV1>,
1079}
1080
1081#[derive(Debug, Clone, PartialEq)]
1082pub struct KnowledgeAuthorizationResultV1 {
1083    pub state_root: String,
1084    pub plan_root: String,
1085    pub policy_root: String,
1086    pub authorization_root: String,
1087    pub principal: String,
1088    pub action: String,
1089    pub decision: String,
1090    pub allowed_object_ids: Vec<String>,
1091    pub denied_object_ids: Vec<String>,
1092}
1093
1094#[derive(Debug, Clone, PartialEq)]
1095pub struct KnowledgeDelegationV1 {
1096    pub version: u64,
1097    pub delegator: String,
1098    pub delegatee: String,
1099    pub action: String,
1100    pub issued_at: u64,
1101    pub expires_at: u64,
1102    pub algorithm: String,
1103    pub key_id: Option<String>,
1104    pub signature: Vec<u8>,
1105}
1106
1107#[derive(Debug, Clone, PartialEq)]
1108pub struct KnowledgeAuthorityEnvelopeV1 {
1109    pub version: u64,
1110    pub issuer: String,
1111    pub subject: String,
1112    pub authorization_root: String,
1113    pub keyring_root: Option<String>,
1114    pub issued_at: u64,
1115    pub expires_at: u64,
1116    pub algorithm: String,
1117    pub key_id: Option<String>,
1118    pub delegations: Vec<KnowledgeDelegationV1>,
1119    pub signature: Vec<u8>,
1120}
1121
1122#[derive(Debug, Clone, PartialEq)]
1123pub struct KnowledgeAuthorityVerificationV1 {
1124    pub envelope_root: String,
1125    pub issuer: String,
1126    pub subject: String,
1127    pub authorization_root: String,
1128    pub keyring_root: Option<String>,
1129    pub delegation_depth: usize,
1130}
1131
1132pub fn knowledge_policy_root_v5(policy: &KnowledgePolicyV1) -> Result<String, KnoloError> {
1133    if policy.default != "allow" && policy.default != "deny" { return Err(KnoloError::InvalidPack("invalid V5 policy default".into())); }
1134    let mut rules = policy.rules.clone();
1135    for rule in &rules {
1136        if !matches!(rule.effect.as_str(), "allow" | "deny") || !matches!(rule.action.as_str(), "query" | "read") { return Err(KnoloError::InvalidPack("invalid V5 policy rule".into())); }
1137        if rule.principal.as_deref() == Some("") || rule.kind.as_deref() == Some("") { return Err(KnoloError::InvalidPack("invalid V5 policy selector".into())); }
1138    }
1139    rules.sort_by(|left, right| policy_rule_key(left).cmp(&policy_rule_key(right)));
1140    let body = if rules.is_empty() {
1141        CborValue::Map(vec![("default".into(), CborValue::Text(policy.default.clone()))])
1142    } else {
1143        CborValue::Map(vec![
1144            ("default".into(), CborValue::Text(policy.default.clone())),
1145            ("rules".into(), CborValue::Array(rules.iter().map(policy_rule_value).collect())),
1146        ])
1147    };
1148    Ok(digest_domain("policy", &encode_cbor(&body)))
1149}
1150
1151pub fn evaluate_knowledge_query_policy_v5(
1152    image: &KnowledgeImage,
1153    query_result: &KnowledgeQueryResultV1,
1154    policy: &KnowledgePolicyV1,
1155    principal: &str,
1156    action: &str,
1157) -> Result<KnowledgeAuthorizationResultV1, KnoloError> {
1158    if principal.is_empty() || !matches!(action, "query" | "read") { return Err(KnoloError::InvalidPack("invalid V5 authorization input".into())); }
1159    if query_result.state_root != image.state_root { return Err(KnoloError::InvalidPack("V5 query state root mismatch".into())); }
1160    let policy_root = knowledge_policy_root_v5(policy)?;
1161    if policy_root != image.commit.policy_root { return Err(KnoloError::InvalidPack("V5 policy root mismatch".into())); }
1162    let mut allowed_object_ids = Vec::new();
1163    let mut denied_object_ids = Vec::new();
1164    for hit in &query_result.hits {
1165        if authorize_policy_hit(hit, policy, principal, action) { allowed_object_ids.push(hit.object_id.clone()); } else { denied_object_ids.push(hit.object_id.clone()); }
1166    }
1167    let decision = if denied_object_ids.is_empty() { "allow" } else if allowed_object_ids.is_empty() { "deny" } else { "partial" };
1168    let authorization_value = CborValue::Map(vec![
1169        ("action".into(), CborValue::Text(action.into())),
1170        ("allowedObjectIds".into(), CborValue::Array(allowed_object_ids.iter().map(|id| CborValue::Text(id.clone())).collect())),
1171        ("decision".into(), CborValue::Text(decision.into())),
1172        ("deniedObjectIds".into(), CborValue::Array(denied_object_ids.iter().map(|id| CborValue::Text(id.clone())).collect())),
1173        ("planRoot".into(), CborValue::Text(query_result.plan_root.clone())),
1174        ("policyRoot".into(), CborValue::Text(policy_root.clone())),
1175        ("principal".into(), CborValue::Text(principal.into())),
1176        ("stateRoot".into(), CborValue::Text(image.state_root.clone())),
1177    ]);
1178    let authorization_root = digest_domain("authorization", &encode_cbor(&authorization_value));
1179    Ok(KnowledgeAuthorizationResultV1 { state_root: image.state_root.clone(), plan_root: query_result.plan_root.clone(), policy_root, authorization_root, principal: principal.into(), action: action.into(), decision: decision.into(), allowed_object_ids, denied_object_ids })
1180}
1181
1182pub fn delegation_payload_v1(delegation: &KnowledgeDelegationV1) -> Vec<u8> {
1183    let mut entries = vec![
1184        ("action".into(), CborValue::Text(delegation.action.clone())),
1185        ("algorithm".into(), CborValue::Text(delegation.algorithm.clone())),
1186        ("delegatee".into(), CborValue::Text(delegation.delegatee.clone())),
1187        ("delegator".into(), CborValue::Text(delegation.delegator.clone())),
1188        ("expiresAt".into(), CborValue::UInt(delegation.expires_at)),
1189        ("issuedAt".into(), CborValue::UInt(delegation.issued_at)),
1190        ("version".into(), CborValue::UInt(delegation.version)),
1191    ];
1192    if let Some(key_id) = &delegation.key_id { entries.push(("keyId".into(), CborValue::Text(key_id.clone()))); }
1193    encode_cbor(&CborValue::Map(entries))
1194}
1195
1196pub fn authority_envelope_payload_v1(envelope: &KnowledgeAuthorityEnvelopeV1) -> Vec<u8> {
1197    let delegation_roots = envelope.delegations.iter().map(|delegation| digest_domain("delegation-payload", &delegation_payload_v1(delegation))).map(CborValue::Text).collect();
1198    let mut entries = vec![
1199        ("algorithm".into(), CborValue::Text(envelope.algorithm.clone())),
1200        ("authorizationRoot".into(), CborValue::Text(envelope.authorization_root.clone())),
1201        ("delegations".into(), CborValue::Array(delegation_roots)),
1202        ("expiresAt".into(), CborValue::UInt(envelope.expires_at)),
1203        ("issuedAt".into(), CborValue::UInt(envelope.issued_at)),
1204        ("issuer".into(), CborValue::Text(envelope.issuer.clone())),
1205        ("subject".into(), CborValue::Text(envelope.subject.clone())),
1206        ("version".into(), CborValue::UInt(envelope.version)),
1207    ];
1208    if let Some(keyring_root) = &envelope.keyring_root { entries.push(("keyringRoot".into(), CborValue::Text(keyring_root.clone()))); }
1209    if let Some(key_id) = &envelope.key_id { entries.push(("keyId".into(), CborValue::Text(key_id.clone()))); }
1210    encode_cbor(&CborValue::Map(entries))
1211}
1212
1213pub fn delegation_root_v1(delegation: &KnowledgeDelegationV1) -> String {
1214    digest_domain("delegation", &encode_cbor(&CborValue::Map(vec![
1215        ("payload".into(), CborValue::Bytes(delegation_payload_v1(delegation))),
1216        ("signature".into(), CborValue::Bytes(delegation.signature.clone())),
1217    ])))
1218}
1219
1220pub fn authority_envelope_root_v1(envelope: &KnowledgeAuthorityEnvelopeV1) -> String {
1221    digest_domain("authority-envelope", &encode_cbor(&CborValue::Map(vec![
1222        ("payload".into(), CborValue::Bytes(authority_envelope_payload_v1(envelope))),
1223        ("signature".into(), CborValue::Bytes(envelope.signature.clone())),
1224    ])))
1225}
1226
1227pub fn authority_session_root_v1(state_root: &str, plan_root: &str, result_root: &str, authorization_root: &str, envelope_root: &str, keyring_root: Option<&str>) -> String {
1228    let mut entries = vec![
1229        ("authorizationRoot".into(), CborValue::Text(authorization_root.into())),
1230        ("envelopeRoot".into(), CborValue::Text(envelope_root.into())),
1231        ("keyringRoot".into(), keyring_root.map_or(CborValue::Null, |value| CborValue::Text(value.into()))),
1232        ("planRoot".into(), CborValue::Text(plan_root.into())),
1233        ("resultRoot".into(), CborValue::Text(result_root.into())),
1234        ("stateRoot".into(), CborValue::Text(state_root.into())),
1235        ("version".into(), CborValue::UInt(1)),
1236    ];
1237    digest_domain("authority-session", &encode_cbor(&CborValue::Map(std::mem::take(&mut entries))))
1238}
1239
1240pub fn sync_request_payload_v1(request_id: &str, sender: &str, summary_root: &str, want_object_ids: &[String], want_event_ids: &[String], algorithm: &str, key_id: Option<&str>, keyring_root: Option<&str>, nonce: &[u8], issued_at: u64, expires_at: u64) -> Vec<u8> {
1241    encode_cbor(&CborValue::Map(vec![
1242        ("algorithm".into(), CborValue::Text(algorithm.into())),
1243        ("expiresAt".into(), CborValue::UInt(expires_at)),
1244        ("issuedAt".into(), CborValue::UInt(issued_at)),
1245        ("keyId".into(), key_id.map_or(CborValue::Null, |value| CborValue::Text(value.into()))),
1246        ("keyringRoot".into(), keyring_root.map_or(CborValue::Null, |value| CborValue::Text(value.into()))),
1247        ("kind".into(), CborValue::Text("sync-request".into())),
1248        ("nonce".into(), CborValue::Bytes(nonce.to_vec())),
1249        ("requestId".into(), CborValue::Text(request_id.into())),
1250        ("sender".into(), CborValue::Text(sender.into())),
1251        ("summary".into(), CborValue::Text(summary_root.into())),
1252        ("version".into(), CborValue::UInt(1)),
1253        ("wantEventIds".into(), CborValue::Array(want_event_ids.iter().map(|value| CborValue::Text(value.clone())).collect())),
1254        ("wantObjectIds".into(), CborValue::Array(want_object_ids.iter().map(|value| CborValue::Text(value.clone())).collect())),
1255    ]))
1256}
1257
1258pub fn sync_request_root_v1(request_id: &str, sender: &str, summary_root: &str, want_object_ids: &[String], want_event_ids: &[String], algorithm: &str, key_id: Option<&str>, keyring_root: Option<&str>, nonce: &[u8], issued_at: u64, expires_at: u64, signature: &[u8]) -> String {
1259    digest_domain("sync-request", &encode_cbor(&CborValue::Map(vec![
1260        ("payload".into(), CborValue::Bytes(sync_request_payload_v1(request_id, sender, summary_root, want_object_ids, want_event_ids, algorithm, key_id, keyring_root, nonce, issued_at, expires_at))),
1261        ("signature".into(), CborValue::Bytes(signature.to_vec())),
1262    ])))
1263}
1264
1265pub fn sync_response_payload_v1(request_root: &str, responder: &str, summary_root: &str, relation: &str, object_ids: &[String], event_ids: &[String], algorithm: &str, key_id: Option<&str>, keyring_root: Option<&str>, issued_at: u64, expires_at: u64) -> Vec<u8> {
1266    encode_cbor(&CborValue::Map(vec![
1267        ("algorithm".into(), CborValue::Text(algorithm.into())),
1268        ("eventIds".into(), CborValue::Array(event_ids.iter().map(|value| CborValue::Text(value.clone())).collect())),
1269        ("expiresAt".into(), CborValue::UInt(expires_at)),
1270        ("issuedAt".into(), CborValue::UInt(issued_at)),
1271        ("keyId".into(), key_id.map_or(CborValue::Null, |value| CborValue::Text(value.into()))),
1272        ("keyringRoot".into(), keyring_root.map_or(CborValue::Null, |value| CborValue::Text(value.into()))),
1273        ("kind".into(), CborValue::Text("sync-response".into())),
1274        ("objectIds".into(), CborValue::Array(object_ids.iter().map(|value| CborValue::Text(value.clone())).collect())),
1275        ("relation".into(), CborValue::Text(relation.into())),
1276        ("requestRoot".into(), CborValue::Text(request_root.into())),
1277        ("responder".into(), CborValue::Text(responder.into())),
1278        ("summary".into(), CborValue::Text(summary_root.into())),
1279        ("version".into(), CborValue::UInt(1)),
1280    ]))
1281}
1282
1283pub fn sync_response_root_v1(request_root: &str, responder: &str, summary_root: &str, relation: &str, object_ids: &[String], event_ids: &[String], algorithm: &str, key_id: Option<&str>, keyring_root: Option<&str>, issued_at: u64, expires_at: u64, signature: &[u8]) -> String {
1284    digest_domain("sync-response", &encode_cbor(&CborValue::Map(vec![
1285        ("payload".into(), CborValue::Bytes(sync_response_payload_v1(request_root, responder, summary_root, relation, object_ids, event_ids, algorithm, key_id, keyring_root, issued_at, expires_at))),
1286        ("signature".into(), CborValue::Bytes(signature.to_vec())),
1287    ])))
1288}
1289
1290pub fn sync_summary_root_v1(state_root: &str, commit_digest: &str, sequence: u64, parents: &[String], object_root: &str, event_root: &str, keyring_root: Option<&str>) -> String {
1291    digest_domain("sync-summary", &encode_cbor(&CborValue::Map(vec![
1292        ("commitDigest".into(), CborValue::Text(commit_digest.into())),
1293        ("eventRoot".into(), CborValue::Text(event_root.into())),
1294        ("keyringRoot".into(), keyring_root.map_or(CborValue::Null, |value| CborValue::Text(value.into()))),
1295        ("objectRoot".into(), CborValue::Text(object_root.into())),
1296        ("parents".into(), CborValue::Array(parents.iter().map(|value| CborValue::Text(value.clone())).collect())),
1297        ("sequence".into(), CborValue::UInt(sequence)),
1298        ("stateRoot".into(), CborValue::Text(state_root.into())),
1299        ("version".into(), CborValue::UInt(1)),
1300    ])))
1301}
1302
1303#[derive(Debug, Clone, PartialEq)]
1304pub struct KnowledgeAuthorityKeyV1 {
1305    pub version: u64,
1306    pub principal: String,
1307    pub key_id: String,
1308    pub algorithm: String,
1309    pub public_key: Vec<u8>,
1310    pub not_before: Option<u64>,
1311    pub not_after: Option<u64>,
1312    pub revoked_at: Option<u64>,
1313}
1314
1315#[derive(Debug, Clone, PartialEq)]
1316pub struct KnowledgeKeyRotationRecordV1 {
1317    pub version: u64,
1318    pub kind: String,
1319    pub issuer: String,
1320    pub issuer_key_id: String,
1321    pub principal: String,
1322    pub previous_key_id: Option<String>,
1323    pub key_id: String,
1324    pub algorithm: String,
1325    pub public_key: Vec<u8>,
1326    pub not_before: u64,
1327    pub not_after: Option<u64>,
1328    pub revoked_at: Option<u64>,
1329    pub issued_at: u64,
1330    pub expires_at: u64,
1331    pub signature: Vec<u8>,
1332}
1333
1334#[derive(Debug, Clone, PartialEq)]
1335pub struct KnowledgeAuthorityKeyringV1 {
1336    pub version: u64,
1337    pub sequence: u64,
1338    pub keys: Vec<KnowledgeAuthorityKeyV1>,
1339    pub rotations: Vec<KnowledgeKeyRotationRecordV1>,
1340}
1341
1342pub fn authority_key_payload_v1(key: &KnowledgeAuthorityKeyV1) -> Vec<u8> {
1343    encode_cbor(&authority_key_value(key))
1344}
1345
1346pub fn key_rotation_payload_v1(record: &KnowledgeKeyRotationRecordV1) -> Vec<u8> {
1347    encode_cbor(&key_rotation_value(record, false))
1348}
1349
1350pub fn key_rotation_root_v1(record: &KnowledgeKeyRotationRecordV1) -> String {
1351    digest_domain("key-rotation", &encode_cbor(&CborValue::Map(vec![
1352        ("payload".into(), CborValue::Bytes(key_rotation_payload_v1(record))),
1353        ("signature".into(), CborValue::Bytes(record.signature.clone())),
1354    ])))
1355}
1356
1357pub fn authority_keyring_root_v1(keyring: &KnowledgeAuthorityKeyringV1) -> String {
1358    let mut keys = keyring.keys.iter().collect::<Vec<_>>();
1359    keys.sort_by(|left, right| key_sort_key(left).cmp(&key_sort_key(right)));
1360    digest_domain("authority-keyring", &encode_cbor(&CborValue::Map(vec![
1361        ("keys".into(), CborValue::Array(keys.iter().map(|key| CborValue::Bytes(authority_key_payload_v1(key))).collect())),
1362        ("rotations".into(), CborValue::Array(keyring.rotations.iter().map(|record| CborValue::Text(key_rotation_root_v1(record))).collect())),
1363        ("sequence".into(), CborValue::UInt(keyring.sequence)),
1364        ("version".into(), CborValue::UInt(keyring.version)),
1365    ])))
1366}
1367
1368fn authority_key_value(key: &KnowledgeAuthorityKeyV1) -> CborValue {
1369    let mut entries = vec![
1370        ("algorithm".into(), CborValue::Text(key.algorithm.clone())),
1371        ("keyId".into(), CborValue::Text(key.key_id.clone())),
1372        ("principal".into(), CborValue::Text(key.principal.clone())),
1373        ("publicKey".into(), CborValue::Bytes(key.public_key.clone())),
1374        ("version".into(), CborValue::UInt(key.version)),
1375    ];
1376    if let Some(value) = key.not_after { entries.push(("notAfter".into(), CborValue::UInt(value))); }
1377    if let Some(value) = key.not_before { entries.push(("notBefore".into(), CborValue::UInt(value))); }
1378    if let Some(value) = key.revoked_at { entries.push(("revokedAt".into(), CborValue::UInt(value))); }
1379    CborValue::Map(entries)
1380}
1381
1382fn key_rotation_value(record: &KnowledgeKeyRotationRecordV1, include_signature: bool) -> CborValue {
1383    let mut entries = vec![
1384        ("algorithm".into(), CborValue::Text(record.algorithm.clone())),
1385        ("expiresAt".into(), CborValue::UInt(record.expires_at)),
1386        ("issuedAt".into(), CborValue::UInt(record.issued_at)),
1387        ("issuer".into(), CborValue::Text(record.issuer.clone())),
1388        ("issuerKeyId".into(), CborValue::Text(record.issuer_key_id.clone())),
1389        ("keyId".into(), CborValue::Text(record.key_id.clone())),
1390        ("kind".into(), CborValue::Text(record.kind.clone())),
1391        ("notBefore".into(), CborValue::UInt(record.not_before)),
1392        ("publicKey".into(), CborValue::Bytes(record.public_key.clone())),
1393        ("principal".into(), CborValue::Text(record.principal.clone())),
1394        ("version".into(), CborValue::UInt(record.version)),
1395    ];
1396    if include_signature { entries.push(("signature".into(), CborValue::Bytes(record.signature.clone()))); }
1397    if let Some(value) = record.not_after { entries.push(("notAfter".into(), CborValue::UInt(value))); }
1398    if let Some(value) = &record.previous_key_id { entries.push(("previousKeyId".into(), CborValue::Text(value.clone()))); }
1399    if let Some(value) = record.revoked_at { entries.push(("revokedAt".into(), CborValue::UInt(value))); }
1400    CborValue::Map(entries)
1401}
1402
1403fn key_sort_key(key: &KnowledgeAuthorityKeyV1) -> Vec<u8> {
1404    let mut out = key.principal.as_bytes().to_vec();
1405    out.push(0);
1406    out.extend_from_slice(key.key_id.as_bytes());
1407    out
1408}
1409
1410pub fn verify_knowledge_authority_envelope_v5<Resolve, Verify>(
1411    image: &KnowledgeImage,
1412    query_result: &KnowledgeQueryResultV1,
1413    policy: &KnowledgePolicyV1,
1414    authorization: &KnowledgeAuthorizationResultV1,
1415    envelope: &KnowledgeAuthorityEnvelopeV1,
1416    now: u64,
1417    resolve_key: Resolve,
1418    verify_signature: Verify,
1419) -> Result<KnowledgeAuthorityVerificationV1, KnoloError>
1420where
1421    Resolve: Fn(&str, &str, Option<&str>) -> Option<Vec<u8>>,
1422    Verify: Fn(&str, &[u8], &[u8], &[u8]) -> bool,
1423{
1424    verify_knowledge_authority_envelope_with_keyring_root_v5(image, query_result, policy, authorization, envelope, now, None, resolve_key, verify_signature)
1425}
1426
1427pub fn verify_knowledge_authority_envelope_with_keyring_root_v5<Resolve, Verify>(
1428    image: &KnowledgeImage,
1429    query_result: &KnowledgeQueryResultV1,
1430    policy: &KnowledgePolicyV1,
1431    authorization: &KnowledgeAuthorizationResultV1,
1432    envelope: &KnowledgeAuthorityEnvelopeV1,
1433    now: u64,
1434    expected_keyring_root: Option<&str>,
1435    resolve_key: Resolve,
1436    verify_signature: Verify,
1437) -> Result<KnowledgeAuthorityVerificationV1, KnoloError>
1438where
1439    Resolve: Fn(&str, &str, Option<&str>) -> Option<Vec<u8>>,
1440    Verify: Fn(&str, &[u8], &[u8], &[u8]) -> bool,
1441{
1442    let expected_authorization = evaluate_knowledge_query_policy_v5(image, query_result, policy, &authorization.principal, &authorization.action)?;
1443    if &expected_authorization != authorization { return Err(KnoloError::InvalidPack("V5 authorization result is not reproducible".into())); }
1444    if envelope.version != 1 || envelope.issuer.is_empty() || envelope.subject.is_empty() || envelope.algorithm.is_empty() || envelope.key_id.as_deref() == Some("") || envelope.issued_at > now || now >= envelope.expires_at { return Err(KnoloError::InvalidPack("invalid V5 authority envelope window".into())); }
1445    if envelope.keyring_root.is_some() && envelope.keyring_root.as_deref() != expected_keyring_root { return Err(KnoloError::InvalidPack("V5 authority keyring root mismatch".into())); }
1446    if authorization.state_root != image.state_root || envelope.authorization_root != authorization.authorization_root || envelope.subject != authorization.principal { return Err(KnoloError::InvalidPack("V5 authority binding mismatch".into())); }
1447    if envelope.delegations.len() > 8 { return Err(KnoloError::InvalidPack("V5 authority delegation depth exceeded".into())); }
1448    verify_authority_signature(&envelope.issuer, &envelope.algorithm, envelope.key_id.as_deref(), &authority_envelope_payload_v1(envelope), &envelope.signature, &resolve_key, &verify_signature)?;
1449    let mut previous = envelope.issuer.clone();
1450    for delegation in &envelope.delegations {
1451        if delegation.version != 1 || delegation.delegator.is_empty() || delegation.delegatee.is_empty() || delegation.algorithm.is_empty() || delegation.key_id.as_deref() == Some("") || delegation.action != authorization.action || delegation.issued_at > now || now >= delegation.expires_at || delegation.delegator != previous { return Err(KnoloError::InvalidPack("invalid V5 delegation chain".into())); }
1452        verify_authority_signature(&delegation.delegator, &delegation.algorithm, delegation.key_id.as_deref(), &delegation_payload_v1(delegation), &delegation.signature, &resolve_key, &verify_signature)?;
1453        previous = delegation.delegatee.clone();
1454    }
1455    if !envelope.delegations.is_empty() && previous != envelope.subject { return Err(KnoloError::InvalidPack("V5 delegation chain does not reach subject".into())); }
1456    Ok(KnowledgeAuthorityVerificationV1 { envelope_root: authority_envelope_root_v1(envelope), issuer: envelope.issuer.clone(), subject: envelope.subject.clone(), authorization_root: authorization.authorization_root.clone(), keyring_root: envelope.keyring_root.clone(), delegation_depth: envelope.delegations.len() })
1457}
1458
1459fn verify_authority_signature<Resolve, Verify>(principal: &str, algorithm: &str, key_id: Option<&str>, message: &[u8], signature: &[u8], resolve_key: &Resolve, verify_signature: &Verify) -> Result<(), KnoloError>
1460where
1461    Resolve: Fn(&str, &str, Option<&str>) -> Option<Vec<u8>>,
1462    Verify: Fn(&str, &[u8], &[u8], &[u8]) -> bool,
1463{
1464    let key = resolve_key(principal, algorithm, key_id).ok_or_else(|| KnoloError::InvalidPack("missing V5 authority key".into()))?;
1465    if !verify_signature(algorithm, &key, message, signature) { return Err(KnoloError::InvalidPack("V5 authority signature verification failed".into())); }
1466    Ok(())
1467}
1468
1469fn authorize_policy_hit(hit: &KnowledgeQueryHitV1, policy: &KnowledgePolicyV1, principal: &str, action: &str) -> bool {
1470    let matches = policy.rules.iter().filter(|rule| rule.action == action && rule.principal.as_deref().is_none_or(|value| value == principal) && rule.kind.as_deref().is_none_or(|value| value.to_lowercase() == hit.kind)).collect::<Vec<_>>();
1471    if matches.iter().any(|rule| rule.effect == "deny") { return false; }
1472    if matches.iter().any(|rule| rule.effect == "allow") { return true; }
1473    policy.default == "allow"
1474}
1475
1476fn policy_rule_value(rule: &KnowledgePolicyRuleV1) -> CborValue {
1477    let mut entries = vec![("action".into(), CborValue::Text(rule.action.clone())), ("effect".into(), CborValue::Text(rule.effect.clone()))];
1478    if let Some(kind) = &rule.kind { entries.push(("kind".into(), CborValue::Text(kind.to_lowercase()))); }
1479    if let Some(principal) = &rule.principal { entries.push(("principal".into(), CborValue::Text(principal.clone()))); }
1480    CborValue::Map(entries)
1481}
1482
1483fn policy_rule_key(rule: &KnowledgePolicyRuleV1) -> String { format!("{}\0{}\0{}\0{}", rule.effect, rule.action, rule.principal.as_deref().unwrap_or(""), rule.kind.as_deref().unwrap_or("").to_lowercase()) }
1484
1485pub fn parse_knowledge_query_v5(expression: &str) -> Result<KnowledgeQueryPlanV1, KnoloError> {
1486    let tokens = lex_knowledge_query(expression)?;
1487    let mut cursor = 0usize;
1488    expect_query_word(&tokens, &mut cursor, "FROM")?;
1489    let kind_token = query_word(&tokens, &mut cursor, "FROM requires an object kind or *")?;
1490    let kind = if kind_token == "*" { None } else { Some(normalize_query_text(&kind_token)) };
1491    if kind.as_deref() == Some("") { return Err(KnoloError::InvalidPack("empty V5 EQL object kind".into())); }
1492
1493    let mut filters = Vec::new();
1494    if query_is_word(tokens.get(cursor), "WHERE") {
1495        cursor += 1;
1496        loop {
1497            let field = query_word(&tokens, &mut cursor, "WHERE requires a field")?;
1498            let field = normalize_query_field(&field)?;
1499            if !matches!(tokens.get(cursor), Some(QueryToken::Equals)) {
1500                return Err(KnoloError::InvalidPack("V5 EQL WHERE only supports =".into()));
1501            }
1502            cursor += 1;
1503            let value = parse_query_literal(tokens.get(cursor))?;
1504            cursor += 1;
1505            filters.push(KnowledgeQueryFilterV1 { field, value });
1506            if query_is_word(tokens.get(cursor), "AND") {
1507                cursor += 1;
1508                continue;
1509            }
1510            break;
1511        }
1512    }
1513
1514    let mut search = None;
1515    if query_is_word(tokens.get(cursor), "SEARCH") {
1516        cursor += 1;
1517        let value = match tokens.get(cursor) {
1518            Some(QueryToken::String(value)) => normalize_query_text(value),
1519            _ => return Err(KnoloError::InvalidPack("V5 EQL SEARCH requires a quoted string".into())),
1520        };
1521        cursor += 1;
1522        if value.is_empty() { return Err(KnoloError::InvalidPack("empty V5 EQL SEARCH".into())); }
1523        search = Some(value);
1524    }
1525
1526    let mut limit = 100usize;
1527    if query_is_word(tokens.get(cursor), "LIMIT") {
1528        cursor += 1;
1529        let value = query_word(&tokens, &mut cursor, "LIMIT requires a positive integer")?;
1530        limit = value.parse::<usize>().map_err(|_| KnoloError::InvalidPack("invalid V5 EQL LIMIT".into()))?;
1531        if !(1..=1000).contains(&limit) { return Err(KnoloError::InvalidPack("V5 EQL LIMIT must be between 1 and 1000".into())); }
1532    }
1533    if cursor != tokens.len() { return Err(KnoloError::InvalidPack("unexpected V5 EQL token".into())); }
1534
1535    filters.sort_by(|left, right| left.field.cmp(&right.field).then_with(|| query_scalar_key(&left.value).cmp(&query_scalar_key(&right.value))));
1536    Ok(KnowledgeQueryPlanV1 { kind, filters, search, limit })
1537}
1538
1539pub fn query_knowledge_image_v5(image: &KnowledgeImage, expression: &str) -> Result<KnowledgeQueryResultV1, KnoloError> {
1540    let plan = parse_knowledge_query_v5(expression)?;
1541    query_knowledge_plan_v5(image, &plan)
1542}
1543
1544fn query_knowledge_plan_v5(image: &KnowledgeImage, plan: &KnowledgeQueryPlanV1) -> Result<KnowledgeQueryResultV1, KnoloError> {
1545    let plan_root = digest_domain("query-plan", &encode_cbor(&query_plan_value(plan)));
1546    let mut objects = image.objects.iter().filter(|object| query_matches_object(object, plan)).collect::<Vec<_>>();
1547    objects.sort_by(|left, right| left.id.cmp(&right.id));
1548    let hits = objects.into_iter().take(plan.limit).map(|object| KnowledgeQueryHitV1 { object_id: object.id.clone(), kind: object.kind.clone() }).collect::<Vec<_>>();
1549    let object_ids = CborValue::Array(hits.iter().map(|hit| CborValue::Text(hit.object_id.clone())).collect());
1550    let result_value = CborValue::Map(vec![
1551        ("objectIds".into(), object_ids),
1552        ("planRoot".into(), CborValue::Text(plan_root.clone())),
1553        ("stateRoot".into(), CborValue::Text(image.state_root.clone())),
1554    ]);
1555    let result_root = digest_domain("query-result", &encode_cbor(&result_value));
1556    Ok(KnowledgeQueryResultV1 { state_root: image.state_root.clone(), plan_root, hits, result_root })
1557}
1558
1559fn query_plan_value(plan: &KnowledgeQueryPlanV1) -> CborValue {
1560    CborValue::Map(vec![
1561        ("filters".into(), CborValue::Array(plan.filters.iter().map(|filter| CborValue::Map(vec![
1562            ("field".into(), CborValue::Text(filter.field.clone())),
1563            ("op".into(), CborValue::Text("=".into())),
1564            ("value".into(), filter.value.clone()),
1565        ])).collect())),
1566        ("kind".into(), plan.kind.clone().map(CborValue::Text).unwrap_or(CborValue::Null)),
1567        ("limit".into(), CborValue::UInt(plan.limit as u64)),
1568        ("search".into(), plan.search.clone().map(CborValue::Text).unwrap_or(CborValue::Null)),
1569        ("source".into(), CborValue::Text("knowledge-image-v5".into())),
1570        ("version".into(), CborValue::UInt(1)),
1571    ])
1572}
1573
1574fn query_matches_object(object: &KnowledgeObjectV1, plan: &KnowledgeQueryPlanV1) -> bool {
1575    if plan.kind.as_deref().is_some_and(|kind| kind != object.kind) { return false; }
1576    for filter in &plan.filters {
1577        let actual = if filter.field == "id" { Some(CborValue::Text(object.id.clone())) } else if filter.field == "kind" { Some(CborValue::Text(object.kind.clone())) } else { object.meta_map_value(&filter.field[5..]).cloned() };
1578        if !query_scalars_equal(actual.as_ref(), Some(&filter.value)) { return false; }
1579    }
1580    if let Some(search) = &plan.search {
1581        let text = normalize_query_text(&String::from_utf8_lossy(&object.bytes));
1582        if !search.split(' ').all(|term| text.contains(term)) { return false; }
1583    }
1584    true
1585}
1586
1587trait KnowledgeObjectMeta {
1588    fn meta_map_value(&self, key: &str) -> Option<&CborValue>;
1589}
1590
1591impl KnowledgeObjectMeta for KnowledgeObjectV1 {
1592    fn meta_map_value(&self, key: &str) -> Option<&CborValue> {
1593        match &self.meta { CborValue::Map(entries) => entries.iter().find(|(name, _)| name == key).map(|(_, value)| value), _ => None }
1594    }
1595}
1596
1597#[derive(Debug, Clone, PartialEq)]
1598enum QueryToken { Word(String), String(String), Equals }
1599
1600fn lex_knowledge_query(expression: &str) -> Result<Vec<QueryToken>, KnoloError> {
1601    let chars = expression.chars().collect::<Vec<_>>();
1602    let mut tokens = Vec::new();
1603    let mut cursor = 0usize;
1604    while cursor < chars.len() {
1605        while chars.get(cursor).is_some_and(|ch| ch.is_whitespace()) { cursor += 1; }
1606        if cursor >= chars.len() { break; }
1607        if chars[cursor] == '=' { tokens.push(QueryToken::Equals); cursor += 1; continue; }
1608        if chars[cursor] == '"' {
1609            cursor += 1;
1610            let mut value = String::new();
1611            let mut closed = false;
1612            while cursor < chars.len() {
1613                let ch = chars[cursor]; cursor += 1;
1614                if ch == '"' { closed = true; break; }
1615                if ch == '\\' {
1616                    let escaped = *chars.get(cursor).ok_or_else(|| KnoloError::InvalidPack("unterminated V5 EQL string".into()))?; cursor += 1;
1617                    if escaped != '"' && escaped != '\\' { return Err(KnoloError::InvalidPack("unsupported V5 EQL escape".into())); }
1618                    value.push(escaped);
1619                } else { value.push(ch); }
1620            }
1621            if !closed { return Err(KnoloError::InvalidPack("unterminated V5 EQL string".into())); }
1622            tokens.push(QueryToken::String(value));
1623            continue;
1624        }
1625        let start = cursor;
1626        while cursor < chars.len() && !chars[cursor].is_whitespace() && chars[cursor] != '=' { cursor += 1; }
1627        let value = chars[start..cursor].iter().collect::<String>();
1628        if value.is_empty() || !value.chars().all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.' | '*' | '-')) { return Err(KnoloError::InvalidPack("invalid V5 EQL token".into())); }
1629        tokens.push(QueryToken::Word(value));
1630    }
1631    Ok(tokens)
1632}
1633
1634fn expect_query_word(tokens: &[QueryToken], cursor: &mut usize, expected: &str) -> Result<(), KnoloError> {
1635    if !query_is_word(tokens.get(*cursor), expected) { return Err(KnoloError::InvalidPack(format!("V5 EQL query must start with {expected}"))); }
1636    *cursor += 1;
1637    Ok(())
1638}
1639
1640fn query_word(tokens: &[QueryToken], cursor: &mut usize, message: &str) -> Result<String, KnoloError> {
1641    match tokens.get(*cursor) { Some(QueryToken::Word(value)) => { *cursor += 1; Ok(value.clone()) }, _ => Err(KnoloError::InvalidPack(message.into())) }
1642}
1643
1644fn query_is_word(token: Option<&QueryToken>, expected: &str) -> bool { matches!(token, Some(QueryToken::Word(value)) if value.eq_ignore_ascii_case(expected)) }
1645
1646fn normalize_query_field(field: &str) -> Result<String, KnoloError> {
1647    let lower = field.to_ascii_lowercase();
1648    if lower == "id" || lower == "kind" { return Ok(lower); }
1649    if lower.strip_prefix("meta.").is_some_and(|key| !key.is_empty() && key.chars().all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-'))) { return Ok(lower); }
1650    Err(KnoloError::InvalidPack(format!("unsupported V5 EQL field: {field}")))
1651}
1652
1653fn parse_query_literal(token: Option<&QueryToken>) -> Result<CborValue, KnoloError> {
1654    match token {
1655        Some(QueryToken::String(value)) => Ok(CborValue::Text(normalize_query_text(value))),
1656        Some(QueryToken::Word(value)) if value == "true" => Ok(CborValue::Bool(true)),
1657        Some(QueryToken::Word(value)) if value == "false" => Ok(CborValue::Bool(false)),
1658        Some(QueryToken::Word(value)) if value == "null" => Ok(CborValue::Null),
1659        Some(QueryToken::Word(value)) if value.parse::<i64>().is_ok() => {
1660            let number = value.parse::<i64>().unwrap();
1661            if number >= 0 { Ok(CborValue::UInt(number as u64)) } else { Ok(CborValue::NInt(number)) }
1662        }
1663        _ => Err(KnoloError::InvalidPack("V5 EQL WHERE requires a scalar literal".into())),
1664    }
1665}
1666
1667fn normalize_query_text(value: &str) -> String { value.to_lowercase().split_whitespace().collect::<Vec<_>>().join(" ") }
1668fn query_scalars_equal(actual: Option<&CborValue>, expected: Option<&CborValue>) -> bool {
1669    match (actual, expected) {
1670        (Some(CborValue::Text(left)), Some(CborValue::Text(right))) => normalize_query_text(left) == *right,
1671        _ => actual == expected,
1672    }
1673}
1674fn query_scalar_key(value: &CborValue) -> String {
1675    match value {
1676        CborValue::Null => "null:".into(),
1677        CborValue::Bool(value) => format!("boolean:{value}"),
1678        CborValue::UInt(value) => format!("number:{value}"),
1679        CborValue::NInt(value) => format!("number:{value}"),
1680        CborValue::Text(value) => format!("string:{value}"),
1681        other => format!("{other:?}"),
1682    }
1683}
1684
1685pub fn migrate_v4_to_v5(bytes: &[u8]) -> Result<MigrationResult, KnoloError> {
1686    let pack = if bytes.starts_with(b"KNLOV4\0\0") {
1687        parse_v4_pack(bytes)?
1688    } else {
1689        mount_pack_from_bytes(bytes)?
1690    };
1691    build_migration_image(&pack, &digest_from_raw(&sha256(bytes)))
1692}
1693
1694fn parse_v4_pack(bytes: &[u8]) -> Result<Pack, KnoloError> {
1695    if bytes.len() < 192 || &bytes[0..8] != b"KNLOV4\0\0" { return Err(KnoloError::InvalidPack("invalid V4 pack".into())); }
1696    let header_length = u32::from_le_bytes(bytes[12..16].try_into().unwrap()) as usize;
1697    let manifest_length = u32::from_le_bytes(bytes[20..24].try_into().unwrap()) as usize;
1698    let directory_length = u32::from_le_bytes(bytes[24..28].try_into().unwrap()) as usize;
1699    if header_length > bytes.len() || 192usize.checked_add(manifest_length).and_then(|n| n.checked_add(directory_length)) != Some(header_length) { return Err(KnoloError::InvalidPack("invalid V4 directory bounds".into())); }
1700    let manifest = bytes.get(192..192 + manifest_length).ok_or_else(|| KnoloError::InvalidPack("V4 manifest exceeds file".into()))?;
1701    if read_ascii_digest(&bytes[32..112]) != digest_from_raw(&sha256(manifest)) || read_ascii_digest(&bytes[112..192]) != digest_from_raw(&sha256(&bytes[header_length..])) { return Err(KnoloError::InvalidPack("V4 pack or manifest digest mismatch".into())); }
1702    let directory = std::str::from_utf8(&bytes[192 + manifest_length..header_length]).map_err(|_| KnoloError::InvalidPack("V4 directory is not UTF-8".into()))?;
1703    let (metadata_offset, metadata_length, metadata_digest) = v4_section_bounds(directory, "metadata")?;
1704    let metadata_end = metadata_offset.checked_add(metadata_length).ok_or_else(|| KnoloError::InvalidPack("V4 metadata overflow".into()))?;
1705    if metadata_end > bytes.len() || metadata_digest != digest_from_raw(&sha256(&bytes[metadata_offset..metadata_end])) { return Err(KnoloError::InvalidPack("V4 metadata digest mismatch".into())); }
1706    let metadata_json = std::str::from_utf8(&bytes[metadata_offset..metadata_end]).map_err(|_| KnoloError::InvalidPack("V4 metadata is not UTF-8".into()))?.to_string();
1707    let meta = parse_meta(&metadata_json)?;
1708    let claims_json = match v4_section_bounds(directory, "claims") {
1709        Ok((offset, length, digest)) => {
1710            let end = offset.checked_add(length).ok_or_else(|| KnoloError::InvalidPack("V4 claims overflow".into()))?;
1711            if end > bytes.len() || digest != digest_from_raw(&sha256(&bytes[offset..end])) { return Err(KnoloError::InvalidPack("V4 claims digest mismatch".into())); }
1712            Some(std::str::from_utf8(&bytes[offset..end]).map_err(|_| KnoloError::InvalidPack("V4 claims are not UTF-8".into()))?.to_string())
1713        }
1714        Err(_) => None,
1715    };
1716    let (chunks_offset, chunks_length, chunks_digest) = v4_section_bounds(directory, "chunks")?;
1717    let chunks_end = chunks_offset.checked_add(chunks_length).ok_or_else(|| KnoloError::InvalidPack("V4 chunks overflow".into()))?;
1718    if chunks_end > bytes.len() { return Err(KnoloError::InvalidPack("V4 chunks exceed file".into())); }
1719    if chunks_digest != digest_from_raw(&sha256(&bytes[chunks_offset..chunks_end])) { return Err(KnoloError::InvalidPack("V4 chunks digest mismatch".into())); }
1720    let parsed = parse_blocks(std::str::from_utf8(&bytes[chunks_offset..chunks_end]).map_err(|_| KnoloError::InvalidPack("V4 chunks are not UTF-8".into()))?)?;
1721    let count = parsed.texts.len();
1722    Ok(Pack { meta: PackMeta { stats: PackStats { docs: count, blocks: count, ..meta.stats }, ..meta }, lexicon: HashMap::new(), postings: Vec::new(), blocks: parsed.texts, headings: parsed.headings, doc_ids: parsed.doc_ids, namespaces: parsed.namespaces, block_token_lens: parsed.lens, metadata_json, claims_json })
1723}
1724
1725fn v4_section_bounds(directory: &str, name: &str) -> Result<(usize, usize, String), KnoloError> {
1726    let marker = format!("\"name\":\"{name}\"");
1727    let start = directory.find(&marker).ok_or_else(|| KnoloError::InvalidPack(format!("missing V4 section: {name}")))?;
1728    let entry = &directory[start..];
1729    let offset = json_number_after(entry, "\"offset\":")? as usize;
1730    let length = json_number_after(entry, "\"length\":")? as usize;
1731    let digest = json_string_after(entry, "\"digest\":\"")?;
1732    Ok((offset, length, digest))
1733}
1734
1735fn json_number_after(input: &str, marker: &str) -> Result<u64, KnoloError> {
1736    let start = input.find(marker).ok_or_else(|| KnoloError::InvalidPack("missing V4 directory number".into()))? + marker.len();
1737    let digits = input[start..].chars().take_while(|ch| ch.is_ascii_digit()).collect::<String>();
1738    digits.parse::<u64>().map_err(|_| KnoloError::InvalidPack("invalid V4 directory number".into()))
1739}
1740
1741fn json_string_after(input: &str, marker: &str) -> Result<String, KnoloError> {
1742    let start = input.find(marker).ok_or_else(|| KnoloError::InvalidPack("missing V4 directory digest".into()))? + marker.len();
1743    let end = input[start..].find('"').ok_or_else(|| KnoloError::InvalidPack("invalid V4 directory digest".into()))?;
1744    Ok(input[start..start + end].to_string())
1745}
1746
1747fn json_field_raw(input: &str, key: &str) -> Option<String> {
1748    let marker = format!("\"{key}\":");
1749    let start = input.find(&marker)? + marker.len();
1750    let bytes = input.as_bytes();
1751    let first = *bytes.get(start)?;
1752    if first == b'{' || first == b'[' {
1753        let open = first;
1754        let close = if open == b'{' { b'}' } else { b']' };
1755        let mut depth = 0usize;
1756        let mut in_string = false;
1757        let mut escaped = false;
1758        for index in start..bytes.len() {
1759            let byte = bytes[index];
1760            if in_string {
1761                if escaped { escaped = false; } else if byte == b'\\' { escaped = true; } else if byte == b'"' { in_string = false; }
1762                continue;
1763            }
1764            if byte == b'"' { in_string = true; continue; }
1765            if byte == open { depth += 1; }
1766            if byte == close { depth -= 1; if depth == 0 { return Some(input[start..=index].to_string()); } }
1767        }
1768        None
1769    } else {
1770        let end = input[start..].find(',').map(|offset| start + offset).unwrap_or(input.len());
1771        Some(input[start..end].trim().to_string())
1772    }
1773}
1774
1775fn read_ascii_digest(bytes: &[u8]) -> String {
1776    let end = bytes.iter().position(|byte| *byte == 0).unwrap_or(bytes.len());
1777    String::from_utf8_lossy(&bytes[..end]).to_string()
1778}
1779
1780#[derive(Clone)]
1781struct MigrationObject { id: String, kind: String, bytes: Vec<u8>, meta: CborValue }
1782
1783fn build_migration_image(pack: &Pack, source_digest: &str) -> Result<MigrationResult, KnoloError> {
1784    let actor = "knolo-v4-migrator";
1785    let mut objects = Vec::new();
1786    let mut mappings = Vec::new();
1787    for (index, text) in pack.blocks.iter().enumerate() {
1788        let source_bytes = text.as_bytes().to_vec();
1789        let source_meta = CborValue::Map(vec![
1790            ("docId".into(), pack.doc_ids.get(index).and_then(|value| value.clone()).map(CborValue::Text).unwrap_or(CborValue::Null)),
1791            ("legacyBlockId".into(), CborValue::UInt(index as u64)),
1792            ("namespace".into(), pack.namespaces.get(index).and_then(|value| value.clone()).map(CborValue::Text).unwrap_or(CborValue::Null)),
1793        ]);
1794        let source_body = CborValue::Map(vec![("bytes".into(), CborValue::Bytes(source_bytes.clone())), ("kind".into(), CborValue::Text("source".into())), ("meta".into(), source_meta.clone())]);
1795        let source_id = digest_domain("object", &encode_cbor(&source_body));
1796        let chunk_meta = CborValue::Map(vec![
1797            ("docId".into(), pack.doc_ids.get(index).and_then(|value| value.clone()).map(CborValue::Text).unwrap_or(CborValue::Null)),
1798            ("heading".into(), pack.headings.get(index).and_then(|value| value.clone()).map(CborValue::Text).unwrap_or(CborValue::Null)),
1799            ("legacyBlockId".into(), CborValue::UInt(index as u64)),
1800            ("namespace".into(), pack.namespaces.get(index).and_then(|value| value.clone()).map(CborValue::Text).unwrap_or(CborValue::Null)),
1801            ("sourceObject".into(), CborValue::Text(source_id.clone())),
1802            ("span".into(), CborValue::Map(vec![("end".into(), CborValue::UInt(text.len() as u64)), ("start".into(), CborValue::UInt(0))])),
1803        ]);
1804        let chunk_body = CborValue::Map(vec![("bytes".into(), CborValue::Bytes(source_bytes.clone())), ("kind".into(), CborValue::Text("chunk".into())), ("meta".into(), chunk_meta.clone())]);
1805        let chunk_id = digest_domain("object", &encode_cbor(&chunk_body));
1806        objects.push(MigrationObject { id: source_id.clone(), kind: "source".into(), bytes: source_bytes.clone(), meta: source_meta });
1807        objects.push(MigrationObject { id: chunk_id.clone(), kind: "chunk".into(), bytes: source_bytes, meta: chunk_meta });
1808        mappings.push(MigrationMapping { legacy_block_id: index, source_object: source_id, chunk_object: chunk_id });
1809    }
1810    if let Some(claims_json) = &pack.claims_json {
1811        let bytes = claims_json.as_bytes().to_vec();
1812        let meta = CborValue::Map(vec![("encoding".into(), CborValue::Text("json-v4".into())), ("version".into(), CborValue::UInt(1))]);
1813        let body = CborValue::Map(vec![("bytes".into(), CborValue::Bytes(bytes.clone())), ("kind".into(), CborValue::Text("claims".into())), ("meta".into(), meta.clone())]);
1814        objects.push(MigrationObject { id: digest_domain("object", &encode_cbor(&body)), kind: "claims".into(), bytes, meta });
1815    }
1816    if let Some(agents_json) = json_field_raw(&pack.metadata_json, "agents") {
1817        let bytes = agents_json.as_bytes().to_vec();
1818        let meta = CborValue::Map(vec![("encoding".into(), CborValue::Text("json-v4".into())), ("version".into(), CborValue::UInt(1))]);
1819        let body = CborValue::Map(vec![("bytes".into(), CborValue::Bytes(bytes.clone())), ("kind".into(), CborValue::Text("agents".into())), ("meta".into(), meta.clone())]);
1820        objects.push(MigrationObject { id: digest_domain("object", &encode_cbor(&body)), kind: "agents".into(), bytes, meta });
1821    }
1822    let metadata_meta = CborValue::Map(vec![("sourceDigest".into(), CborValue::Text(source_digest.into())), ("sourceVersion".into(), CborValue::UInt(pack.meta.version as u64))]); let metadata_bytes = encode_cbor(&metadata_meta); let metadata_body = CborValue::Map(vec![("bytes".into(), CborValue::Bytes(metadata_bytes.clone())), ("kind".into(), CborValue::Text("metadata".into())), ("meta".into(), metadata_meta.clone())]);
1823    objects.push(MigrationObject { id: digest_domain("object", &encode_cbor(&metadata_body)), kind: "metadata".into(), bytes: metadata_bytes, meta: metadata_meta });
1824    objects.sort_by(|left, right| left.id.cmp(&right.id));
1825    let object_values = objects.iter().map(|object| CborValue::Map(vec![("bytes".into(), CborValue::Bytes(object.bytes.clone())), ("id".into(), CborValue::Text(object.id.clone())), ("kind".into(), CborValue::Text(object.kind.clone())), ("meta".into(), object.meta.clone())])).collect::<Vec<_>>();
1826    let object_payload = encode_cbor(&CborValue::Array(object_values));
1827    let object_segment_digest = digest_domain("segment", &object_payload);
1828    let object_ids = objects.iter().map(|object| CborValue::Text(object.id.clone())).collect::<Vec<_>>();
1829    let parents = CborValue::Array(Vec::new());
1830    let transaction_id = digest_domain("transaction", &encode_cbor(&CborValue::Map(vec![("actor".into(), CborValue::Text(actor.into())), ("objects".into(), CborValue::Array(object_ids.clone())), ("parents".into(), parents.clone())])));
1831    let mut events = Vec::new();
1832    for (index, object) in objects.iter().enumerate() {
1833        let event_kind = if object.kind == "chunk" { "document.put".to_string() } else { format!("{}.put", object.kind) };
1834        let event_body = CborValue::Map(vec![("actor".into(), CborValue::Text(actor.into())), ("actorCounter".into(), CborValue::UInt(index as u64 + 1)), ("kind".into(), CborValue::Text(event_kind.into())), ("parents".into(), if index == 0 { CborValue::Array(Vec::new()) } else { CborValue::Array(vec![CborValue::Text(transaction_id.clone())]) }), ("payload".into(), CborValue::Text(object.id.clone())), ("provenance".into(), CborValue::Map(vec![("objectId".into(), CborValue::Text(object.id.clone()))])), ("target".into(), CborValue::Text(object.id.clone())), ("transactionId".into(), CborValue::Text(transaction_id.clone())), ("version".into(), CborValue::UInt(1))]);
1835        let event_id = digest_domain("event", &encode_cbor(&event_body));
1836        events.push((event_id, event_body));
1837    }
1838    events.sort_by(|left, right| left.0.cmp(&right.0));
1839    let event_values = events.iter().map(|(id, body)| { let mut entries = match body { CborValue::Map(entries) => entries.clone(), _ => Vec::new() }; entries.push(("id".into(), CborValue::Text(id.clone()))); CborValue::Map(entries) }).collect::<Vec<_>>();
1840    let event_payload = encode_cbor(&CborValue::Array(event_values));
1841    let event_segment_digest = digest_domain("segment", &event_payload);
1842    let event_ids = events.iter().map(|(id, _)| CborValue::Text(id.clone())).collect::<Vec<_>>();
1843    let object_root = digest_domain("object-root", &encode_cbor(&CborValue::Array(object_ids.clone())));
1844    let event_root = digest_domain("event-root", &encode_cbor(&CborValue::Array(event_ids.clone())));
1845    let lexical_view = digest_domain("view", &encode_cbor(&CborValue::Map(vec![("kind".into(), CborValue::Text("lexical".into())), ("objectIds".into(), CborValue::Array(object_ids.clone()))])));
1846    let commit_value = CborValue::Map(vec![("actor".into(), CborValue::Text(actor.into())), ("eventRoot".into(), CborValue::Text(event_root.clone())), ("eventSegmentDigest".into(), CborValue::Text(event_segment_digest)), ("objectRoot".into(), CborValue::Text(object_root.clone())), ("objectSegmentDigest".into(), CborValue::Text(object_segment_digest)), ("parents".into(), parents), ("policyRoot".into(), CborValue::Text(digest_domain("policy", &encode_cbor(&CborValue::Map(vec![("default".into(), CborValue::Text("deny".into()))]))))), ("runtimeContract".into(), CborValue::Text(digest_domain("runtime", &encode_cbor(&CborValue::Map(vec![("codec".into(), CborValue::Text("cbor-v1".into())), ("format".into(), CborValue::UInt(5))]))))), ("schemaRoot".into(), CborValue::Text(digest_domain("schema", &encode_cbor(&CborValue::Map(vec![("version".into(), CborValue::UInt(1))]))))), ("sequence".into(), CborValue::UInt(1)), ("transactionRoot".into(), CborValue::Text(digest_domain("transaction-root", &encode_cbor(&CborValue::Map(vec![("eventIds".into(), CborValue::Array(event_ids)), ("objectIds".into(), CborValue::Array(object_ids)), ("transactionId".into(), CborValue::Text(transaction_id))]))))), ("version".into(), CborValue::UInt(1)), ("views".into(), CborValue::Map(vec![("lexical".into(), CborValue::Text(lexical_view))]))]);
1847    let commit_payload = encode_cbor(&commit_value); let commit_digest = digest_domain("commit", &commit_payload); let state = digest_domain("state", &digest_raw(&commit_digest));
1848    let object_segment = encode_v5_segment(1, &object_payload); let event_segment = encode_v5_segment(2, &event_payload); let commit_segment = encode_v5_segment(3, &commit_payload); let commit_offset = V5_DATA_START + object_segment.len() + event_segment.len();
1849    let mut image = vec![0u8; V5_DATA_START + object_segment.len() + event_segment.len() + commit_segment.len()]; image[0..8].copy_from_slice(KNOWLEDGE_IMAGE_V5_MAGIC); image[8..10].copy_from_slice(&5u16.to_le_bytes()); image[12..14].copy_from_slice(&(V5_SUPERBLOCK_SIZE as u16).to_le_bytes()); image[V5_DATA_START..V5_DATA_START + object_segment.len()].copy_from_slice(&object_segment); image[V5_DATA_START + object_segment.len()..commit_offset].copy_from_slice(&event_segment); image[commit_offset..].copy_from_slice(&commit_segment); let superblock = encode_v5_superblock(1, commit_offset, commit_segment.len(), &commit_digest, &state); image[V5_HEADER_SIZE..V5_HEADER_SIZE + V5_SUPERBLOCK_SIZE].copy_from_slice(&superblock);
1850    let receipt_body = CborValue::Map(vec![("kind".into(), CborValue::Text("v4-to-v5-migration".into())), ("objectMappings".into(), CborValue::Array(mappings.iter().map(|mapping| CborValue::Map(vec![("chunkObject".into(), CborValue::Text(mapping.chunk_object.clone())), ("legacyBlockId".into(), CborValue::UInt(mapping.legacy_block_id as u64)), ("sourceObject".into(), CborValue::Text(mapping.source_object.clone()))])).collect())), ("sourceDigest".into(), CborValue::Text(source_digest.into())), ("sourceVersion".into(), CborValue::UInt(pack.meta.version as u64)), ("stateRoot".into(), CborValue::Text(state.clone())), ("version".into(), CborValue::UInt(1))]); let receipt_digest = digest_domain("receipt", &encode_cbor(&receipt_body)); let mut receipt_entries = match receipt_body { CborValue::Map(entries) => entries, _ => Vec::new() }; receipt_entries.push(("receiptDigest".into(), CborValue::Text(receipt_digest))); let receipt = encode_cbor(&CborValue::Map(receipt_entries));
1851    Ok(MigrationResult { image, receipt, state_root: state, mappings })
1852}
1853
1854fn encode_v5_segment(kind: u8, payload: &[u8]) -> Vec<u8> { let mut out = vec![0u8; V5_SEGMENT_HEADER_SIZE + payload.len()]; out[0..4].copy_from_slice(SEGMENT_MAGIC); out[4] = kind; out[5] = 1; out[8..16].copy_from_slice(&(payload.len() as u64).to_le_bytes()); out[16..48].copy_from_slice(&digest_raw(&digest_domain("segment", payload))); out[V5_SEGMENT_HEADER_SIZE..].copy_from_slice(payload); out }
1855fn encode_v5_superblock(generation: u64, commit_offset: usize, commit_length: usize, commit_digest: &str, state: &str) -> Vec<u8> { let mut out = vec![0u8; V5_SUPERBLOCK_SIZE]; out[0..8].copy_from_slice(SUPERBLOCK_MAGIC); out[8..16].copy_from_slice(&generation.to_le_bytes()); out[16..24].copy_from_slice(&(commit_offset as u64).to_le_bytes()); out[24..32].copy_from_slice(&(commit_length as u64).to_le_bytes()); out[32..64].copy_from_slice(&digest_raw(commit_digest)); out[64..96].copy_from_slice(&digest_raw(state)); let digest = digest_raw(&digest_domain("superblock", &out[0..96])); out[96..128].copy_from_slice(&digest); out }
1856
1857fn parse_v5_image(bytes: &[u8]) -> Result<KnowledgeImage, KnoloError> {
1858    if bytes.len() < V5_DATA_START { return Err(KnoloError::InvalidPack("V5 image is truncated".into())); }
1859    if &bytes[0..8] != KNOWLEDGE_IMAGE_V5_MAGIC { return Err(KnoloError::InvalidPack("invalid V5 image magic".into())); }
1860    if le_u16(bytes, 8)? != 5 || le_u16(bytes, 12)? as usize != V5_SUPERBLOCK_SIZE { return Err(KnoloError::InvalidPack("unsupported V5 image header".into())); }
1861
1862    let a = read_v5_superblock(bytes, V5_HEADER_SIZE, 'A');
1863    let b = read_v5_superblock(bytes, V5_HEADER_SIZE + V5_SUPERBLOCK_SIZE, 'B');
1864    let mut candidates = [a, b].into_iter().flatten().collect::<Vec<_>>();
1865    if candidates.is_empty() { return Err(KnoloError::InvalidPack("no valid V5 superblock".into())); }
1866
1867    let mut segments = Vec::new();
1868    let mut offset = V5_DATA_START;
1869    let mut required_seen = [false; 3];
1870    while offset < bytes.len() {
1871        let segment = read_v5_segment(bytes, offset)?;
1872        if !matches!(segment.kind, 1..=3) && segment.kind < 128 { return Err(KnoloError::InvalidPack("unknown non-optional V5 segment".into())); }
1873        if segment.kind <= 3 && segment.flags != 0 && !(matches!(segment.kind, 1 | 2) && segment.flags == 1) {
1874            return Err(KnoloError::InvalidPack("unsupported required V5 segment flags".into()));
1875        }
1876        if segment.kind <= 3 && segment.schema != 1 { return Err(KnoloError::InvalidPack("unsupported required V5 segment schema".into())); }
1877        if (1..=3).contains(&segment.kind) {
1878            if required_seen[segment.kind as usize - 1] { return Err(KnoloError::InvalidPack("duplicate required V5 segment".into())); }
1879            required_seen[segment.kind as usize - 1] = true;
1880        }
1881        offset += segment.length;
1882        segments.push(segment);
1883    }
1884    if offset != bytes.len() || required_seen.iter().any(|seen| !seen) { return Err(KnoloError::InvalidPack("missing or misaligned V5 segment".into())); }
1885
1886    let commit_segment = segments.iter().find(|segment| segment.kind == 3).ok_or_else(|| KnoloError::InvalidPack("missing V5 commit segment".into()))?;
1887    let object_segment = segments.iter().find(|segment| segment.kind == 1).ok_or_else(|| KnoloError::InvalidPack("missing V5 object segment".into()))?;
1888    let event_segment = segments.iter().find(|segment| segment.kind == 2).ok_or_else(|| KnoloError::InvalidPack("missing V5 event segment".into()))?;
1889    let commit_payload = &bytes[commit_segment.offset + V5_SEGMENT_HEADER_SIZE..commit_segment.offset + commit_segment.length];
1890    let commit_value = decode_cbor_exact(commit_payload)?;
1891    if encode_cbor(&commit_value) != commit_payload { return Err(KnoloError::InvalidPack("non-canonical V5 commit CBOR".into())); }
1892    let mut commit = parse_commit(&commit_value)?;
1893    let commit_digest = digest_domain("commit", commit_payload);
1894    let state = digest_domain("state", &digest_raw(&commit_digest));
1895    candidates.sort_by(|left, right| right.generation.cmp(&left.generation));
1896    let active = candidates.into_iter().find(|candidate| candidate.commit_offset == commit_segment.offset && candidate.commit_length == commit_segment.length && candidate.commit_digest == commit_digest && candidate.state_root == state).ok_or_else(|| KnoloError::InvalidPack("no V5 superblock points to a valid commit".into()))?;
1897    commit.state_root = state.clone();
1898    commit.commit_digest = commit_digest.clone();
1899    if commit_field(&commit_value, "objectSegmentDigest")? != object_segment.digest || commit_field(&commit_value, "eventSegmentDigest")? != event_segment.digest { return Err(KnoloError::InvalidPack("V5 segment digest mismatch".into())); }
1900
1901    let object_payload = &bytes[object_segment.offset + V5_SEGMENT_HEADER_SIZE..object_segment.offset + object_segment.length];
1902    let objects = if object_segment.flags & 1 != 0 {
1903        let (_, _) = inspect_vqf_envelope(object_payload, 1)?;
1904        let body = &object_payload[56..];
1905        let logical = decode_vqf_object_body(body)?;
1906        if digest_domain("segment", &logical) != object_segment.digest { return Err(KnoloError::InvalidPack("VQF object logical segment digest mismatch".into())); }
1907        parse_objects(&logical)?
1908    } else {
1909        parse_objects(object_payload)?
1910    };
1911    let event_payload = &bytes[event_segment.offset + V5_SEGMENT_HEADER_SIZE..event_segment.offset + event_segment.length];
1912    let event_logical = if event_segment.flags & 1 != 0 {
1913        let (_, _) = inspect_vqf_envelope(event_payload, 2)?;
1914        let body = &event_payload[56..];
1915        let logical = decode_vqf_event_body(body)?;
1916        if digest_domain("segment", &logical) != event_segment.digest { return Err(KnoloError::InvalidPack("VQF event logical segment digest mismatch".into())); }
1917        logical
1918    } else { event_payload.to_vec() };
1919    let events = parse_events(&event_logical)?;
1920    let object_ids = CborValue::Array(objects.iter().map(|object| CborValue::Text(object.id.clone())).collect());
1921    let event_ids = CborValue::Array(events.iter().map(|event| CborValue::Text(event.id.clone())).collect());
1922    if digest_domain("object-root", &encode_cbor(&object_ids)) != commit.object_root || digest_domain("event-root", &encode_cbor(&event_ids)) != commit.event_root { return Err(KnoloError::InvalidPack("V5 object/event root mismatch".into())); }
1923
1924    Ok(KnowledgeImage { state_root: state, commit_digest, commit, objects, events, segments, active_superblock: active.slot })
1925}
1926
1927#[derive(Debug, Clone)]
1928struct V5Superblock { generation: u64, commit_offset: usize, commit_length: usize, commit_digest: String, state_root: String, slot: char }
1929
1930fn read_v5_superblock(bytes: &[u8], offset: usize, slot: char) -> Option<V5Superblock> {
1931    let raw = bytes.get(offset..offset + V5_SUPERBLOCK_SIZE)?;
1932    if raw.get(0..8)? != SUPERBLOCK_MAGIC { return None; }
1933    let generation = u64::from_le_bytes(raw.get(8..16)?.try_into().ok()?);
1934    let commit_offset = usize::try_from(u64::from_le_bytes(raw.get(16..24)?.try_into().ok()?)).ok()?;
1935    let commit_length = usize::try_from(u64::from_le_bytes(raw.get(24..32)?.try_into().ok()?)).ok()?;
1936    if commit_length < V5_SEGMENT_HEADER_SIZE || commit_offset.checked_add(commit_length)? > bytes.len() { return None; }
1937    let commit_digest = digest_from_raw(raw.get(32..64)?);
1938    let state_root = digest_from_raw(raw.get(64..96)?);
1939    if digest_from_raw(raw.get(96..128)?) != digest_domain("superblock", raw.get(0..96)?) { return None; }
1940    Some(V5Superblock { generation, commit_offset, commit_length, commit_digest, state_root, slot })
1941}
1942
1943fn read_v5_segment(bytes: &[u8], offset: usize) -> Result<KnowledgeImageSegment, KnoloError> {
1944    let header = read_slice(bytes, &mut offset.clone(), V5_SEGMENT_HEADER_SIZE)?;
1945    if header.get(0..4) != Some(SEGMENT_MAGIC) { return Err(KnoloError::InvalidPack("invalid V5 segment magic".into())); }
1946    let kind = header[4]; let schema = header[5]; let flags = u16::from_le_bytes([header[6], header[7]]);
1947    let payload_length = usize::try_from(u64::from_le_bytes(header[8..16].try_into().unwrap())).map_err(|_| KnoloError::InvalidPack("V5 segment length overflow".into()))?;
1948    if payload_length as u64 > V5_MAX_SEGMENT { return Err(KnoloError::InvalidPack("V5 segment exceeds safety limit".into())); }
1949    let length = V5_SEGMENT_HEADER_SIZE.checked_add(payload_length).ok_or_else(|| KnoloError::InvalidPack("V5 segment length overflow".into()))?;
1950    let payload = bytes.get(offset + V5_SEGMENT_HEADER_SIZE..offset + length).ok_or_else(|| KnoloError::InvalidPack("V5 segment exceeds file".into()))?;
1951    let digest = digest_from_raw(&header[16..48]);
1952    if flags == 0 && digest != digest_domain("segment", payload) { return Err(KnoloError::InvalidPack("V5 segment digest mismatch".into())); }
1953    Ok(KnowledgeImageSegment { kind, schema, flags, offset, length, payload_length, digest })
1954}
1955
1956fn parse_objects(payload: &[u8]) -> Result<Vec<KnowledgeObjectV1>, KnoloError> {
1957    let value = decode_cbor_exact(payload)?;
1958    if encode_cbor(&value) != payload { return Err(KnoloError::InvalidPack("non-canonical V5 object CBOR".into())); }
1959    let entries = match value { CborValue::Array(items) => items, _ => return Err(KnoloError::InvalidPack("invalid V5 object segment".into())) };
1960    entries.into_iter().map(|entry| {
1961        let map = as_map(&entry)?;
1962        let id = map_text(map, "id")?; let kind = map_text(map, "kind")?; let bytes = map_bytes(map, "bytes")?; let meta = map_value(map, "meta")?.clone();
1963        let body = CborValue::Map(vec![("bytes".into(), CborValue::Bytes(bytes.clone())), ("kind".into(), CborValue::Text(kind.clone())), ("meta".into(), meta.clone())]);
1964        if digest_domain("object", &encode_cbor(&body)) != id { return Err(KnoloError::InvalidPack("V5 object identity mismatch".into())); }
1965        Ok(KnowledgeObjectV1 { id, kind, bytes, meta })
1966    }).collect()
1967}
1968
1969fn parse_events(payload: &[u8]) -> Result<Vec<KnowledgeEventV1>, KnoloError> {
1970    let value = decode_cbor_exact(payload)?;
1971    if encode_cbor(&value) != payload { return Err(KnoloError::InvalidPack("non-canonical V5 event CBOR".into())); }
1972    let entries = match value { CborValue::Array(items) => items, _ => return Err(KnoloError::InvalidPack("invalid V5 event segment".into())) };
1973    entries.into_iter().map(|entry| {
1974        let map = as_map(&entry)?; let id = map_text(map, "id")?; let version = map_uint(map, "version")?; if version != 1 { return Err(KnoloError::InvalidPack("unsupported V5 event version".into())); }
1975        let transaction_id = map_text(map, "transactionId")?; let actor = map_text(map, "actor")?; let actor_counter = map_uint(map, "actorCounter")?; let kind = map_text(map, "kind")?; let target = map_text(map, "target")?; let payload_id = map_text(map, "payload")?;
1976        let body = CborValue::Map(vec![("actor".into(), CborValue::Text(actor.clone())), ("actorCounter".into(), CborValue::UInt(actor_counter)), ("kind".into(), CborValue::Text(kind.clone())), ("parents".into(), map_value(map, "parents")?.clone()), ("payload".into(), CborValue::Text(payload_id.clone())), ("provenance".into(), map_value(map, "provenance")?.clone()), ("target".into(), CborValue::Text(target.clone())), ("transactionId".into(), CborValue::Text(transaction_id.clone())), ("version".into(), CborValue::UInt(1))]);
1977        if digest_domain("event", &encode_cbor(&body)) != id { return Err(KnoloError::InvalidPack("V5 event identity mismatch".into())); }
1978        Ok(KnowledgeEventV1 { id, transaction_id, actor, actor_counter, kind, target, payload: payload_id })
1979    }).collect()
1980}
1981
1982fn parse_commit(value: &CborValue) -> Result<KnowledgeCommitV1, KnoloError> {
1983    let map = as_map(value)?; let version = map_uint(map, "version")?; if version != 1 { return Err(KnoloError::InvalidPack("unsupported V5 commit version".into())); }
1984    Ok(KnowledgeCommitV1 { state_root: String::new(), commit_digest: String::new(), parents: map_strings(map, "parents")?, object_root: map_text(map, "objectRoot")?, event_root: map_text(map, "eventRoot")?, policy_root: map_text(map, "policyRoot")?, sequence: map_uint(map, "sequence")?, actor: map_text(map, "actor")? })
1985}
1986
1987fn commit_field(value: &CborValue, field: &str) -> Result<String, KnoloError> { map_text(as_map(value)?, field) }
1988
1989fn as_map(value: &CborValue) -> Result<&[(String, CborValue)], KnoloError> { match value { CborValue::Map(entries) => Ok(entries), _ => Err(KnoloError::InvalidPack("expected V5 CBOR map".into())) } }
1990fn map_value<'a>(map: &'a [(String, CborValue)], key: &str) -> Result<&'a CborValue, KnoloError> { map.iter().find(|(name, _)| name == key).map(|(_, value)| value).ok_or_else(|| KnoloError::InvalidPack(format!("missing V5 field: {key}"))) }
1991fn map_text(map: &[(String, CborValue)], key: &str) -> Result<String, KnoloError> { match map_value(map, key)? { CborValue::Text(value) => Ok(value.clone()), _ => Err(KnoloError::InvalidPack(format!("V5 field is not text: {key}"))) } }
1992fn map_uint(map: &[(String, CborValue)], key: &str) -> Result<u64, KnoloError> { match map_value(map, key)? { CborValue::UInt(value) => Ok(*value), _ => Err(KnoloError::InvalidPack(format!("V5 field is not unsigned: {key}"))) } }
1993fn map_bytes(map: &[(String, CborValue)], key: &str) -> Result<Vec<u8>, KnoloError> { match map_value(map, key)? { CborValue::Bytes(value) => Ok(value.clone()), _ => Err(KnoloError::InvalidPack(format!("V5 field is not bytes: {key}"))) } }
1994fn map_strings(map: &[(String, CborValue)], key: &str) -> Result<Vec<String>, KnoloError> { match map_value(map, key)? { CborValue::Array(values) => values.iter().map(|value| match value { CborValue::Text(text) => Ok(text.clone()), _ => Err(KnoloError::InvalidPack("V5 array value is not text".into())) }).collect(), _ => Err(KnoloError::InvalidPack(format!("V5 field is not an array: {key}"))) } }
1995
1996fn decode_cbor_exact(bytes: &[u8]) -> Result<CborValue, KnoloError> { let mut cursor = 0; let value = decode_cbor(bytes, &mut cursor)?; if cursor != bytes.len() { return Err(KnoloError::InvalidPack("trailing V5 CBOR bytes".into())); } Ok(value) }
1997fn decode_cbor(bytes: &[u8], cursor: &mut usize) -> Result<CborValue, KnoloError> {
1998    let initial = *bytes.get(*cursor).ok_or_else(|| KnoloError::InvalidPack("truncated V5 CBOR".into()))?; *cursor += 1; let major = initial >> 5; let ai = initial & 31;
1999    let length = |cursor: &mut usize| read_cbor_length(bytes, cursor, ai);
2000    match major { 0 => Ok(CborValue::UInt(length(cursor)?)), 1 => { let value = length(cursor)?; if value > i64::MAX as u64 { return Err(KnoloError::InvalidPack("V5 negative integer overflow".into())); } Ok(CborValue::NInt(-1 - value as i64)) }, 2 => { let len = usize::try_from(length(cursor)?).map_err(|_| KnoloError::InvalidPack("V5 bytes length overflow".into()))?; Ok(CborValue::Bytes(read_slice(bytes, cursor, len)?.to_vec())) }, 3 => { let len = usize::try_from(length(cursor)?).map_err(|_| KnoloError::InvalidPack("V5 text length overflow".into()))?; let value = std::str::from_utf8(read_slice(bytes, cursor, len)?).map_err(|_| KnoloError::InvalidPack("V5 text is not UTF-8".into()))?; Ok(CborValue::Text(value.to_string())) }, 4 => { let len = usize::try_from(length(cursor)?).map_err(|_| KnoloError::InvalidPack("V5 array length overflow".into()))?; let mut values = Vec::with_capacity(len); for _ in 0..len { values.push(decode_cbor(bytes, cursor)?); } Ok(CborValue::Array(values)) }, 5 => { let len = usize::try_from(length(cursor)?).map_err(|_| KnoloError::InvalidPack("V5 map length overflow".into()))?; let mut values = Vec::with_capacity(len); for _ in 0..len { let key = match decode_cbor(bytes, cursor)? { CborValue::Text(value) => value, _ => return Err(KnoloError::InvalidPack("V5 map key is not text".into())) }; if values.iter().any(|(name, _): &(String, CborValue)| name == &key) { return Err(KnoloError::InvalidPack("duplicate V5 map key".into())); } values.push((key, decode_cbor(bytes, cursor)?)); } Ok(CborValue::Map(values)) }, 7 if ai == 20 => Ok(CborValue::Bool(false)), 7 if ai == 21 => Ok(CborValue::Bool(true)), 7 if ai == 22 => Ok(CborValue::Null), _ => Err(KnoloError::InvalidPack("unsupported V5 CBOR type".into())) }
2001}
2002fn read_cbor_length(bytes: &[u8], cursor: &mut usize, ai: u8) -> Result<u64, KnoloError> { match ai { 0..=23 => Ok(ai as u64), 24 => Ok(read_slice(bytes, cursor, 1)?[0] as u64), 25 => Ok(u16::from_be_bytes(read_slice(bytes, cursor, 2)?.try_into().unwrap()) as u64), 26 => Ok(u32::from_be_bytes(read_slice(bytes, cursor, 4)?.try_into().unwrap()) as u64), 27 => Ok(u64::from_be_bytes(read_slice(bytes, cursor, 8)?.try_into().unwrap())), _ => Err(KnoloError::InvalidPack("indefinite V5 CBOR is not allowed".into())) } }
2003
2004fn encode_cbor(value: &CborValue) -> Vec<u8> { let mut out = Vec::new(); encode_cbor_into(value, &mut out); out }
2005fn encode_cbor_into(value: &CborValue, out: &mut Vec<u8>) { match value { CborValue::Null => out.push(0xf6), CborValue::Bool(false) => out.push(0xf4), CborValue::Bool(true) => out.push(0xf5), CborValue::UInt(value) => encode_cbor_length(0, *value, out), CborValue::NInt(value) => encode_cbor_length(1, (-1 - *value) as u64, out), CborValue::Bytes(value) => { encode_cbor_length(2, value.len() as u64, out); out.extend_from_slice(value); }, CborValue::Text(value) => { encode_cbor_length(3, value.len() as u64, out); out.extend_from_slice(value.as_bytes()); }, CborValue::Array(values) => { encode_cbor_length(4, values.len() as u64, out); for value in values { encode_cbor_into(value, out); } }, CborValue::Map(values) => { let mut sorted = values.iter().collect::<Vec<_>>(); sorted.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes())); encode_cbor_length(5, sorted.len() as u64, out); for (key, value) in sorted { encode_cbor_into(&CborValue::Text(key.clone()), out); encode_cbor_into(value, out); } } } }
2006fn encode_cbor_length(major: u8, value: u64, out: &mut Vec<u8>) { if value < 24 { out.push((major << 5) | value as u8); } else if value <= u8::MAX as u64 { out.extend_from_slice(&[(major << 5) | 24, value as u8]); } else if value <= u16::MAX as u64 { out.push((major << 5) | 25); out.extend_from_slice(&(value as u16).to_be_bytes()); } else if value <= u32::MAX as u64 { out.push((major << 5) | 26); out.extend_from_slice(&(value as u32).to_be_bytes()); } else { out.push((major << 5) | 27); out.extend_from_slice(&value.to_be_bytes()); } }
2007
2008fn digest_domain(domain: &str, payload: &[u8]) -> String { let mut input = format!("knolo:{domain}:v1\0").into_bytes(); input.extend_from_slice(payload); digest_from_raw(&sha256(&input)) }
2009fn digest_from_raw(raw: &[u8]) -> String { format!("sha256-{}", raw.iter().map(|byte| format!("{byte:02x}")).collect::<String>()) }
2010fn digest_raw(digest: &str) -> Vec<u8> { let hex = digest.strip_prefix("sha256-").unwrap_or(""); (0..hex.len()).step_by(2).filter_map(|i| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok()).collect() }
2011
2012fn le_u16(bytes: &[u8], offset: usize) -> Result<u16, KnoloError> { Ok(u16::from_le_bytes(read_slice(bytes, &mut offset.clone(), 2)?.try_into().unwrap())) }
2013
2014// Minimal SHA-256 implementation for the dependency-free Rust kernel.
2015fn sha256(input: &[u8]) -> [u8; 32] {
2016    const K: [u32; 64] = [
2017        0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
2018        0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
2019        0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
2020        0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
2021        0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
2022        0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
2023        0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
2024        0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
2025    ];
2026    let mut data = input.to_vec(); let bit_len = (data.len() as u64) * 8; data.push(0x80); while data.len() % 64 != 56 { data.push(0); } data.extend_from_slice(&bit_len.to_be_bytes());
2027    let mut h: [u32; 8] = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];
2028    for chunk in data.chunks_exact(64) { let mut w = [0u32; 64]; for i in 0..16 { w[i] = u32::from_be_bytes(chunk[i*4..i*4+4].try_into().unwrap()); } for i in 16..64 { let s0 = w[i-15].rotate_right(7) ^ w[i-15].rotate_right(18) ^ (w[i-15] >> 3); let s1 = w[i-2].rotate_right(17) ^ w[i-2].rotate_right(19) ^ (w[i-2] >> 10); w[i] = w[i-16].wrapping_add(s0).wrapping_add(w[i-7]).wrapping_add(s1); } let mut a=h[0]; let mut b=h[1]; let mut c=h[2]; let mut d=h[3]; let mut e=h[4]; let mut f=h[5]; let mut g=h[6]; let mut hh=h[7]; for i in 0..64 { let s1=e.rotate_right(6)^e.rotate_right(11)^e.rotate_right(25); let ch=(e&f)^((!e)&g); let t1=hh.wrapping_add(s1).wrapping_add(ch).wrapping_add(K[i]).wrapping_add(w[i]); let s0=a.rotate_right(2)^a.rotate_right(13)^a.rotate_right(22); let maj=(a&b)^(a&c)^(b&c); let t2=s0.wrapping_add(maj); hh=g; g=f; f=e; e=d.wrapping_add(t1); d=c; c=b; b=a; a=t1.wrapping_add(t2); } h[0]=h[0].wrapping_add(a); h[1]=h[1].wrapping_add(b); h[2]=h[2].wrapping_add(c); h[3]=h[3].wrapping_add(d); h[4]=h[4].wrapping_add(e); h[5]=h[5].wrapping_add(f); h[6]=h[6].wrapping_add(g); h[7]=h[7].wrapping_add(hh); }
2029    let mut out = [0u8; 32]; for (i, value) in h.iter().enumerate() { out[i*4..i*4+4].copy_from_slice(&value.to_be_bytes()); } out
2030}