@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/README.md +6 -0
- package/bin/knolo.mjs +639 -156
- package/package.json +2 -2
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/Cargo.toml +1 -1
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/lib.rs +294 -3
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.lock +2 -2
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.toml +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knolo/cli",
|
|
3
|
-
"version": "5.
|
|
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.
|
|
35
|
+
"@knolo/core": "^5.5.0"
|
|
36
36
|
},
|
|
37
37
|
"private": false
|
|
38
38
|
}
|
|
@@ -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
|
|
1628
|
-
let
|
|
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.
|
|
248
|
+
version = "5.5.0"
|
|
249
249
|
|
|
250
250
|
[[package]]
|
|
251
251
|
name = "knolo-icp-canister"
|
|
252
|
-
version = "5.
|
|
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.
|
|
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.
|
|
19
|
+
knolo-core-rust = { path = "../knolo-core-rust", version = "5.5.0" }
|
|
20
20
|
serde = { version = "1", features = ["derive"] }
|