@knolo/cli 5.2.3 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knolo/cli",
3
- "version": "5.2.3",
3
+ "version": "5.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "knolo": "bin/knolo.mjs"
@@ -32,7 +32,7 @@
32
32
  "test:hub": "node --test test/hub.integration.test.mjs"
33
33
  },
34
34
  "dependencies": {
35
- "@knolo/core": "^5.1.0"
35
+ "@knolo/core": "^5.5.0"
36
36
  },
37
37
  "private": false
38
38
  }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "knolo-core-rust"
3
- version = "5.1.0"
3
+ version = "5.5.0"
4
4
  edition = "2021"
5
5
  license = "Apache-2.0"
6
6
  description = "Rust runtime support for reading and querying .knolo packs"
@@ -503,6 +503,263 @@ fn read_slice<'a>(bytes: &'a [u8], cursor: &mut usize, len: usize) -> Result<&'a
503
503
  Ok(slice)
504
504
  }
505
505
 
506
+ #[allow(dead_code)]
507
+ struct VqfReader<'a> {
508
+ bytes: &'a [u8],
509
+ cursor: usize,
510
+ }
511
+
512
+ #[allow(dead_code)]
513
+ impl<'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)]
549
+ fn 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)]
561
+ fn 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)]
581
+ fn 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)]
598
+ fn 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)]
616
+ fn 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)]
626
+ fn 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)]
638
+ fn 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)]
645
+ fn 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)]
661
+ fn 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)]
695
+ fn 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)]
709
+ fn 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
+
506
763
  // V5 read-only Knowledge Image support. The implementation intentionally has
507
764
  // no external dependencies so the format verifier can be used in offline and
508
765
  // embedded environments.
@@ -662,6 +919,20 @@ pub fn inspect_knowledge_image(bytes: &[u8]) -> Result<KnowledgeImageVerificatio
662
919
  })
663
920
  }
664
921
 
922
+ /// Validates the VQF-1 envelope boundary before a codec-specific body reader.
923
+ pub 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
+
665
936
  pub fn verify_knowledge_image(bytes: &[u8]) -> Result<KnowledgeImageVerification, KnoloError> {
666
937
  inspect_knowledge_image(bytes)
667
938
  }
@@ -1599,6 +1870,9 @@ fn parse_v5_image(bytes: &[u8]) -> Result<KnowledgeImage, KnoloError> {
1599
1870
  while offset < bytes.len() {
1600
1871
  let segment = read_v5_segment(bytes, offset)?;
1601
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
+ }
1602
1876
  if segment.kind <= 3 && segment.schema != 1 { return Err(KnoloError::InvalidPack("unsupported required V5 segment schema".into())); }
1603
1877
  if (1..=3).contains(&segment.kind) {
1604
1878
  if required_seen[segment.kind as usize - 1] { return Err(KnoloError::InvalidPack("duplicate required V5 segment".into())); }
@@ -1624,8 +1898,25 @@ fn parse_v5_image(bytes: &[u8]) -> Result<KnowledgeImage, KnoloError> {
1624
1898
  commit.commit_digest = commit_digest.clone();
1625
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())); }
1626
1900
 
1627
- let objects = parse_objects(&bytes[object_segment.offset + V5_SEGMENT_HEADER_SIZE..object_segment.offset + object_segment.length])?;
1628
- let events = parse_events(&bytes[event_segment.offset + V5_SEGMENT_HEADER_SIZE..event_segment.offset + event_segment.length])?;
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)?;
1629
1920
  let object_ids = CborValue::Array(objects.iter().map(|object| CborValue::Text(object.id.clone())).collect());
1630
1921
  let event_ids = CborValue::Array(events.iter().map(|event| CborValue::Text(event.id.clone())).collect());
1631
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())); }
@@ -1658,7 +1949,7 @@ fn read_v5_segment(bytes: &[u8], offset: usize) -> Result<KnowledgeImageSegment,
1658
1949
  let length = V5_SEGMENT_HEADER_SIZE.checked_add(payload_length).ok_or_else(|| KnoloError::InvalidPack("V5 segment length overflow".into()))?;
1659
1950
  let payload = bytes.get(offset + V5_SEGMENT_HEADER_SIZE..offset + length).ok_or_else(|| KnoloError::InvalidPack("V5 segment exceeds file".into()))?;
1660
1951
  let digest = digest_from_raw(&header[16..48]);
1661
- if digest != digest_domain("segment", payload) { return Err(KnoloError::InvalidPack("V5 segment digest mismatch".into())); }
1952
+ if flags == 0 && digest != digest_domain("segment", payload) { return Err(KnoloError::InvalidPack("V5 segment digest mismatch".into())); }
1662
1953
  Ok(KnowledgeImageSegment { kind, schema, flags, offset, length, payload_length, digest })
1663
1954
  }
1664
1955
 
@@ -245,11 +245,11 @@ dependencies = [
245
245
 
246
246
  [[package]]
247
247
  name = "knolo-core-rust"
248
- version = "5.1.0"
248
+ version = "5.5.0"
249
249
 
250
250
  [[package]]
251
251
  name = "knolo-icp-canister"
252
- version = "5.1.0"
252
+ version = "5.5.0"
253
253
  dependencies = [
254
254
  "candid",
255
255
  "ic-cdk",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "knolo-icp-canister"
3
- version = "5.1.0"
3
+ version = "5.5.0"
4
4
  edition = "2021"
5
5
  license = "Apache-2.0"
6
6
  description = "ICP canister adapter for Knolo retrieval over Candid"
@@ -16,5 +16,5 @@ path = "src/lib.rs"
16
16
  candid = "0.10"
17
17
  ic-cdk = "0.17"
18
18
  ic-cdk-macros = "0.17"
19
- knolo-core-rust = { path = "../knolo-core-rust", version = "5.1.0" }
19
+ knolo-core-rust = { path = "../knolo-core-rust", version = "5.5.0" }
20
20
  serde = { version = "1", features = ["derive"] }