1use crate::guard_state::{
26 GitCleanAttestation, GitHashAlgorithm, GuardPolicyIdentity, GuardRootRecord, GuardRootState,
27 GUARD_SCHEMA_VERSION,
28};
29use lru::LruCache;
30use parking_lot::Mutex;
31use redb::ReadableTable;
32use std::num::NonZeroUsize;
33
34pub const DEFAULT_HOT_INDEX_MEMORY: usize = 64 * 1024 * 1024;
36
37const ESTIMATED_BYTES_PER_ENTRY: usize = 320;
41
42fn max_entries_for_budget(budget: usize) -> NonZeroUsize {
44 let n = budget / ESTIMATED_BYTES_PER_ENTRY;
45 NonZeroUsize::new(n.max(1)).unwrap_or(NonZeroUsize::MIN)
47}
48
49pub struct HotAttestationIndex {
54 cache: Mutex<LruCache<HotKey, GitCleanAttestation>>,
55 budget: usize,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61struct HotKey {
62 hash_algorithm: GitHashAlgorithm,
63 blob_oid: String,
64 policy_short_digest: String,
65}
66
67impl HotAttestationIndex {
68 pub fn new() -> Self {
70 Self::with_budget(DEFAULT_HOT_INDEX_MEMORY)
71 }
72
73 pub fn with_budget(budget: usize) -> Self {
75 let cap = max_entries_for_budget(budget);
76 Self {
77 cache: Mutex::new(LruCache::new(cap)),
78 budget,
79 }
80 }
81
82 pub fn get(
84 &self,
85 hash_algorithm: GitHashAlgorithm,
86 blob_oid: &str,
87 policy_short_digest: &str,
88 ) -> Option<GitCleanAttestation> {
89 let key = HotKey {
90 hash_algorithm,
91 blob_oid: blob_oid.to_string(),
92 policy_short_digest: policy_short_digest.to_string(),
93 };
94 self.cache.lock().get(&key).cloned()
95 }
96
97 pub fn insert(&self, attestation: GitCleanAttestation) {
101 let key = HotKey {
102 hash_algorithm: attestation.hash_algorithm,
103 blob_oid: attestation.blob_oid.clone(),
104 policy_short_digest: attestation
105 .policy_identity
106 .short_digest()
107 .unwrap_or_default(),
108 };
109 self.cache.lock().put(key, attestation);
110 }
111
112 pub fn remove(
114 &self,
115 hash_algorithm: GitHashAlgorithm,
116 blob_oid: &str,
117 policy_short_digest: &str,
118 ) -> Option<GitCleanAttestation> {
119 let key = HotKey {
120 hash_algorithm,
121 blob_oid: blob_oid.to_string(),
122 policy_short_digest: policy_short_digest.to_string(),
123 };
124 self.cache.lock().pop(&key)
125 }
126
127 pub fn invalidate_for_policy(&self, current: &GuardPolicyIdentity) -> usize {
130 let current_short = current.short_digest().unwrap_or_default();
131 let mut removed = 0;
132 let mut to_remove = Vec::new();
133 {
134 let cache = self.cache.lock();
135 for (key, value) in cache.iter() {
136 let key_digest = &key.policy_short_digest;
137 let value_digest = value.policy_identity.short_digest().unwrap_or_default();
138 if key_digest != ¤t_short || value_digest != current_short {
139 to_remove.push(key.clone());
140 }
141 }
142 }
143 let mut cache = self.cache.lock();
144 for key in to_remove {
145 if cache.pop(&key).is_some() {
146 removed += 1;
147 }
148 }
149 removed
150 }
151
152 pub fn len(&self) -> usize {
154 self.cache.lock().len()
155 }
156
157 pub fn is_empty(&self) -> bool {
159 self.cache.lock().is_empty()
160 }
161
162 pub fn budget(&self) -> usize {
164 self.budget
165 }
166
167 pub fn clear(&self) {
169 self.cache.lock().clear();
170 }
171}
172
173impl Default for HotAttestationIndex {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct StoreMeta {
184 pub schema_version: u32,
186 pub store_uuid: [u8; 16],
188 pub created_version: String,
190 pub last_successful_migration: u32,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
196pub enum GuardStoreError {
197 #[error("guard store schema version {found} is newer than supported {supported}; upgrade keyhog or run `keyhog guard rebuild <root>`")]
199 SchemaTooNew {
200 found: u32,
202 supported: u32,
204 },
205 #[error("guard store schema version {found} is no longer supported; run `keyhog guard rebuild <root>` to recreate state")]
208 SchemaObsolete {
209 found: u32,
211 },
212 #[error("guard store is corrupt: {detail}; run `keyhog guard rebuild <root>`")]
214 Corrupt {
215 detail: String,
217 },
218 #[error("guard store path is unsafe: {detail}")]
220 UnsafePath {
221 detail: String,
223 },
224 #[error("guard store I/O error: {0}")]
226 Io(String),
227 #[error("guard store was not closed cleanly; run `keyhog guard reconcile <root>`")]
230 UncleanShutdown,
231}
232
233pub fn check_schema_version(found: u32) -> Result<(), GuardStoreError> {
235 if found > GUARD_SCHEMA_VERSION {
236 return Err(GuardStoreError::SchemaTooNew {
237 found,
238 supported: GUARD_SCHEMA_VERSION,
239 });
240 }
241 if found < 1 {
242 return Err(GuardStoreError::SchemaObsolete { found });
243 }
244 Ok(())
246}
247
248#[derive(Debug, Default)]
253pub struct RootRegistry {
254 roots: std::collections::HashMap<Vec<u8>, GuardRootRecord>,
255}
256
257impl RootRegistry {
258 pub fn new() -> Self {
260 Self::default()
261 }
262
263 pub fn register(
265 &mut self,
266 canonical_path: Vec<u8>,
267 filesystem_identity: crate::guard_state::FilesystemIdentity,
268 mode: crate::guard_state::GuardRootMode,
269 ) -> GuardRootRecord {
270 let record = GuardRootRecord {
271 canonical_path: canonical_path.clone(),
272 filesystem_identity,
273 mode,
274 state: GuardRootState::Stopped,
275 terminal_sequence: 0,
276 accepted_event_sequence: 0,
277 completed_event_sequence: 0,
278 initial_reconciliation_time: None,
279 last_reconciliation_time: None,
280 backend_route_label: String::new(),
281 last_receipt: None,
282 };
283 self.roots.insert(canonical_path, record.clone());
284 record
285 }
286
287 pub fn insert_record(&mut self, record: GuardRootRecord) {
291 self.roots.insert(record.canonical_path.clone(), record);
292 }
293
294 pub fn get(&self, canonical_path: &[u8]) -> Option<&GuardRootRecord> {
296 self.roots.get(canonical_path)
297 }
298
299 pub fn get_mut(&mut self, canonical_path: &[u8]) -> Option<&mut GuardRootRecord> {
301 self.roots.get_mut(canonical_path)
302 }
303
304 pub fn remove(&mut self, canonical_path: &[u8]) -> Option<GuardRootRecord> {
306 self.roots.remove(canonical_path)
307 }
308
309 pub fn list(&self) -> Vec<&GuardRootRecord> {
311 self.roots.values().collect()
312 }
313
314 pub fn len(&self) -> usize {
316 self.roots.len()
317 }
318
319 pub fn is_empty(&self) -> bool {
321 self.roots.is_empty()
322 }
323
324 pub fn count_by_state(&self, state: GuardRootState) -> usize {
326 self.roots.values().filter(|r| r.state == state).count()
327 }
328}
329
330const META_TABLE: redb::TableDefinition<&str, &[u8]> = redb::TableDefinition::new("meta");
334
335const ROOTS_TABLE: redb::TableDefinition<&[u8], &[u8]> = redb::TableDefinition::new("roots");
337
338const ATTESTATIONS_TABLE: redb::TableDefinition<&[u8], &[u8]> =
341 redb::TableDefinition::new("git_clean_attestations");
342
343const ROOT_GAPS_TABLE: redb::TableDefinition<&[u8], &[u8]> =
346 redb::TableDefinition::new("root_gaps");
347
348const SERVICE_STATE_TABLE: redb::TableDefinition<&str, u8> =
351 redb::TableDefinition::new("service_state");
352
353pub struct DurableGuardStore {
359 db: redb::Database,
360 path: std::path::PathBuf,
361}
362
363impl DurableGuardStore {
364 pub fn open(path: &std::path::Path) -> Result<Self, GuardStoreError> {
370 if path.exists() {
373 let meta = std::fs::symlink_metadata(path)
374 .map_err(|e| GuardStoreError::Io(format!("stat guard store path: {e}")))?;
375 if meta.file_type().is_symlink() {
376 return Err(GuardStoreError::Io(
377 "guard store path is a symlink; refusing to open".to_string(),
378 ));
379 }
380 }
381 if let Some(parent) = path.parent() {
383 if !parent.exists() {
384 std::fs::create_dir_all(parent)
385 .map_err(|e| GuardStoreError::Io(format!("create guard store dir: {e}")))?;
386 #[cfg(unix)]
387 {
388 use std::os::unix::fs::PermissionsExt;
389 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
390 .map_err(|e| {
391 GuardStoreError::Io(format!("set guard store dir perms: {e}"))
392 })?;
393 }
394 }
395 }
396 let db = redb::Database::create(path)
397 .map_err(|e| GuardStoreError::Io(format!("open guard store: {e}")))?;
398 #[cfg(unix)]
400 {
401 use std::os::unix::fs::PermissionsExt;
402 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
403 .map_err(|e| GuardStoreError::Io(format!("set guard store perms: {e}")))?;
404 }
405 let store = Self {
406 db,
407 path: path.to_path_buf(),
408 };
409 store.ensure_schema()?;
410 Ok(store)
411 }
412
413 pub fn path(&self) -> &std::path::Path {
415 &self.path
416 }
417
418 fn ensure_schema(&self) -> Result<(), GuardStoreError> {
420 let txn = self
421 .db
422 .begin_write()
423 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
424 {
425 let mut meta = txn
426 .open_table(META_TABLE)
427 .map_err(|e| GuardStoreError::Io(format!("open meta table: {e}")))?;
428 let found_version: Option<u32> = meta
429 .get("schema_version")
430 .map_err(|e| GuardStoreError::Io(format!("read schema_version: {e}")))?
431 .map(|guard| {
432 let bytes: &[u8] = guard.value();
433 u32::from_le_bytes(bytes.try_into().unwrap_or([0, 0, 0, 0]))
434 });
435 match found_version {
436 Some(version) => {
437 check_schema_version(version)?;
438 }
439 None => {
440 let version_bytes = GUARD_SCHEMA_VERSION.to_le_bytes();
442 meta.insert("schema_version", version_bytes.as_slice())
443 .map_err(|e| GuardStoreError::Io(format!("write schema_version: {e}")))?;
444 }
445 }
446 }
447 {
449 let _ = txn
450 .open_table(ROOTS_TABLE)
451 .map_err(|e| GuardStoreError::Io(format!("create roots table: {e}")))?;
452 let _ = txn
453 .open_table(ATTESTATIONS_TABLE)
454 .map_err(|e| GuardStoreError::Io(format!("create attestations table: {e}")))?;
455 let _ = txn
456 .open_table(ROOT_GAPS_TABLE)
457 .map_err(|e| GuardStoreError::Io(format!("create root_gaps table: {e}")))?;
458 let _ = txn
459 .open_table(SERVICE_STATE_TABLE)
460 .map_err(|e| GuardStoreError::Io(format!("create service_state table: {e}")))?;
461 }
462 txn.commit()
463 .map_err(|e| GuardStoreError::Io(format!("commit schema: {e}")))?;
464 Ok(())
465 }
466
467 pub fn load_roots(&self) -> Result<RootRegistry, GuardStoreError> {
469 let txn = self
470 .db
471 .begin_read()
472 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
473 let table = txn
474 .open_table(ROOTS_TABLE)
475 .map_err(|e| GuardStoreError::Io(format!("open roots table: {e}")))?;
476 let mut registry = RootRegistry::new();
477 for entry in table
478 .range::<&[u8]>(..)
479 .map_err(|e| GuardStoreError::Io(format!("iterate roots: {e}")))?
480 {
481 let (key, value) =
482 entry.map_err(|e| GuardStoreError::Io(format!("read root entry: {e}")))?;
483 let record: GuardRootRecord =
484 serde_json::from_slice(value.value()).map_err(|e| GuardStoreError::Corrupt {
485 detail: format!("deserialize root record: {e}"),
486 })?;
487 registry.roots.insert(key.value().to_vec(), record);
488 }
489 Ok(registry)
490 }
491
492 pub fn save_root(&self, record: &GuardRootRecord) -> Result<(), GuardStoreError> {
494 let txn = self
495 .db
496 .begin_write()
497 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
498 {
499 let mut table = txn
500 .open_table(ROOTS_TABLE)
501 .map_err(|e| GuardStoreError::Io(format!("open roots table: {e}")))?;
502 let value = serde_json::to_vec(record)
503 .map_err(|e| GuardStoreError::Io(format!("serialize root record: {e}")))?;
504 table
505 .insert(record.canonical_path.as_slice(), value.as_slice())
506 .map_err(|e| GuardStoreError::Io(format!("insert root: {e}")))?;
507 }
508 txn.commit()
509 .map_err(|e| GuardStoreError::Io(format!("commit root: {e}")))?;
510 Ok(())
511 }
512
513 pub fn remove_root(&self, canonical_path: &[u8]) -> Result<(), GuardStoreError> {
515 let txn = self
516 .db
517 .begin_write()
518 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
519 {
520 let mut table = txn
521 .open_table(ROOTS_TABLE)
522 .map_err(|e| GuardStoreError::Io(format!("open roots table: {e}")))?;
523 table
524 .remove(canonical_path)
525 .map_err(|e| GuardStoreError::Io(format!("remove root: {e}")))?;
526 }
527 txn.commit()
528 .map_err(|e| GuardStoreError::Io(format!("commit remove root: {e}")))?;
529 Ok(())
530 }
531
532 pub fn load_attestations(&self) -> Result<Vec<GitCleanAttestation>, GuardStoreError> {
534 let txn = self
535 .db
536 .begin_read()
537 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
538 let table = txn
539 .open_table(ATTESTATIONS_TABLE)
540 .map_err(|e| GuardStoreError::Io(format!("open attestations table: {e}")))?;
541 let mut attestations = Vec::new();
542 for entry in table
543 .range::<&[u8]>(..)
544 .map_err(|e| GuardStoreError::Io(format!("iterate attestations: {e}")))?
545 {
546 let (_, value) =
547 entry.map_err(|e| GuardStoreError::Io(format!("read attestation entry: {e}")))?;
548 let att: GitCleanAttestation =
549 serde_json::from_slice(value.value()).map_err(|e| GuardStoreError::Corrupt {
550 detail: format!("deserialize attestation: {e}"),
551 })?;
552 attestations.push(att);
553 }
554 Ok(attestations)
555 }
556
557 pub fn save_attestation(&self, att: &GitCleanAttestation) -> Result<(), GuardStoreError> {
559 let key = attestation_key(att);
560 let txn = self
561 .db
562 .begin_write()
563 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
564 {
565 let mut table = txn
566 .open_table(ATTESTATIONS_TABLE)
567 .map_err(|e| GuardStoreError::Io(format!("open attestations table: {e}")))?;
568 let value = serde_json::to_vec(att)
569 .map_err(|e| GuardStoreError::Io(format!("serialize attestation: {e}")))?;
570 table
571 .insert(key.as_slice(), value.as_slice())
572 .map_err(|e| GuardStoreError::Io(format!("insert attestation: {e}")))?;
573 }
574 txn.commit()
575 .map_err(|e| GuardStoreError::Io(format!("commit attestation: {e}")))?;
576 Ok(())
577 }
578
579 pub fn remove_attestation(&self, att: &GitCleanAttestation) -> Result<(), GuardStoreError> {
581 let key = attestation_key(att);
582 let txn = self
583 .db
584 .begin_write()
585 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
586 {
587 let mut table = txn
588 .open_table(ATTESTATIONS_TABLE)
589 .map_err(|e| GuardStoreError::Io(format!("open attestations table: {e}")))?;
590 table
591 .remove(key.as_slice())
592 .map_err(|e| GuardStoreError::Io(format!("remove attestation: {e}")))?;
593 }
594 txn.commit()
595 .map_err(|e| GuardStoreError::Io(format!("commit remove attestation: {e}")))?;
596 Ok(())
597 }
598
599 pub fn clear_attestations_for_policy(
601 &self,
602 policy_short: &str,
603 ) -> Result<usize, GuardStoreError> {
604 let txn = self
605 .db
606 .begin_write()
607 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
608 let removed = {
609 let mut table = txn
610 .open_table(ATTESTATIONS_TABLE)
611 .map_err(|e| GuardStoreError::Io(format!("open attestations table: {e}")))?;
612 let prefix = policy_short.as_bytes();
613 let mut count = 0usize;
614 let keys_to_remove: Vec<Vec<u8>> = table
615 .range::<&[u8]>(..)
616 .map_err(|e| GuardStoreError::Io(format!("iterate attestations: {e}")))?
617 .filter_map(|entry| {
618 let (key, _) = entry.ok()?;
619 let k = key.value();
620 if k.ends_with(prefix) {
623 Some(k.to_vec())
624 } else {
625 None
626 }
627 })
628 .collect();
629 for key in keys_to_remove {
630 table
631 .remove(key.as_slice())
632 .map_err(|e| GuardStoreError::Io(format!("remove attestation: {e}")))?;
633 count += 1;
634 }
635 count
636 };
637 txn.commit()
638 .map_err(|e| GuardStoreError::Io(format!("commit clear attestations: {e}")))?;
639 Ok(removed)
640 }
641
642 pub fn save_root_gap(
645 &self,
646 canonical_path: &[u8],
647 blob_oid: &str,
648 description: &str,
649 ) -> Result<(), GuardStoreError> {
650 let mut key = Vec::with_capacity(canonical_path.len() + 1 + blob_oid.len());
651 key.extend_from_slice(canonical_path);
652 key.push(0);
653 key.extend_from_slice(blob_oid.as_bytes());
654 let txn = self
655 .db
656 .begin_write()
657 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
658 {
659 let mut table = txn
660 .open_table(ROOT_GAPS_TABLE)
661 .map_err(|e| GuardStoreError::Io(format!("open root_gaps table: {e}")))?;
662 table
663 .insert(key.as_slice(), description.as_bytes())
664 .map_err(|e| GuardStoreError::Io(format!("insert root gap: {e}")))?;
665 }
666 txn.commit()
667 .map_err(|e| GuardStoreError::Io(format!("commit root gap: {e}")))?;
668 Ok(())
669 }
670
671 pub fn load_root_gaps(
673 &self,
674 canonical_path: &[u8],
675 ) -> Result<Vec<(String, String)>, GuardStoreError> {
676 let txn = self
677 .db
678 .begin_read()
679 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
680 let table = txn
681 .open_table(ROOT_GAPS_TABLE)
682 .map_err(|e| GuardStoreError::Io(format!("open root_gaps table: {e}")))?;
683 let prefix = canonical_path;
684 let mut gaps = Vec::new();
685 for entry in table
686 .range::<&[u8]>(..)
687 .map_err(|e| GuardStoreError::Io(format!("iterate root_gaps: {e}")))?
688 {
689 let (key, value) =
690 entry.map_err(|e| GuardStoreError::Io(format!("read root gap entry: {e}")))?;
691 let k = key.value();
692 if !k.starts_with(prefix) {
693 continue;
694 }
695 if k.len() <= prefix.len() || k[prefix.len()] != 0 {
699 continue;
700 }
701 let rest = &k[prefix.len() + 1..];
703 let blob_oid = String::from_utf8_lossy(rest).to_string();
704 let desc = String::from_utf8_lossy(value.value()).to_string();
705 gaps.push((blob_oid, desc));
706 }
707 Ok(gaps)
708 }
709
710 pub fn clear_root_gaps(&self, canonical_path: &[u8]) -> Result<usize, GuardStoreError> {
712 let txn = self
713 .db
714 .begin_write()
715 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
716 let removed = {
717 let mut table = txn
718 .open_table(ROOT_GAPS_TABLE)
719 .map_err(|e| GuardStoreError::Io(format!("open root_gaps table: {e}")))?;
720 let prefix = canonical_path;
721 let keys_to_remove: Vec<Vec<u8>> = table
722 .range::<&[u8]>(..)
723 .map_err(|e| GuardStoreError::Io(format!("iterate root_gaps: {e}")))?
724 .filter_map(|entry| {
725 let (key, _) = entry.ok()?;
726 let k = key.value();
727 if k.starts_with(prefix) && k.len() > prefix.len() && k[prefix.len()] == 0 {
728 Some(k.to_vec())
729 } else {
730 None
731 }
732 })
733 .collect();
734 let count = keys_to_remove.len();
735 for key in keys_to_remove {
736 table
737 .remove(key.as_slice())
738 .map_err(|e| GuardStoreError::Io(format!("remove root gap: {e}")))?;
739 }
740 count
741 };
742 txn.commit()
743 .map_err(|e| GuardStoreError::Io(format!("commit clear root gaps: {e}")))?;
744 Ok(removed)
745 }
746
747 pub fn mark_unclean_shutdown(&self) -> Result<(), GuardStoreError> {
751 let txn = self
752 .db
753 .begin_write()
754 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
755 {
756 let mut table = txn
757 .open_table(SERVICE_STATE_TABLE)
758 .map_err(|e| GuardStoreError::Io(format!("open service_state table: {e}")))?;
759 table
760 .insert("clean_shutdown", 0u8)
761 .map_err(|e| GuardStoreError::Io(format!("write clean_shutdown: {e}")))?;
762 }
763 txn.commit()
764 .map_err(|e| GuardStoreError::Io(format!("commit service state: {e}")))?;
765 Ok(())
766 }
767
768 pub fn mark_clean_shutdown(&self) -> Result<(), GuardStoreError> {
771 let txn = self
772 .db
773 .begin_write()
774 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
775 {
776 let mut table = txn
777 .open_table(SERVICE_STATE_TABLE)
778 .map_err(|e| GuardStoreError::Io(format!("open service_state table: {e}")))?;
779 table
780 .insert("clean_shutdown", 1u8)
781 .map_err(|e| GuardStoreError::Io(format!("write clean_shutdown: {e}")))?;
782 }
783 txn.commit()
784 .map_err(|e| GuardStoreError::Io(format!("commit service state: {e}")))?;
785 Ok(())
786 }
787
788 pub fn was_clean_shutdown(&self) -> Result<bool, GuardStoreError> {
792 let txn = self
793 .db
794 .begin_read()
795 .map_err(|e| GuardStoreError::Io(format!("begin read: {e}")))?;
796 let table = txn
797 .open_table(SERVICE_STATE_TABLE)
798 .map_err(|e| GuardStoreError::Io(format!("open service_state table: {e}")))?;
799 let value = table
800 .get("clean_shutdown")
801 .map_err(|e| GuardStoreError::Io(format!("read clean_shutdown: {e}")))?;
802 Ok(value.map(|v| v.value() == 1u8).unwrap_or(false))
803 }
804
805 pub fn save_root_with_gaps(
809 &self,
810 record: &GuardRootRecord,
811 gaps: &[(String, String)],
812 ) -> Result<(), GuardStoreError> {
813 let txn = self
814 .db
815 .begin_write()
816 .map_err(|e| GuardStoreError::Io(format!("begin write: {e}")))?;
817 {
818 let mut roots = txn
819 .open_table(ROOTS_TABLE)
820 .map_err(|e| GuardStoreError::Io(format!("open roots table: {e}")))?;
821 let value = serde_json::to_vec(record)
822 .map_err(|e| GuardStoreError::Io(format!("serialize root record: {e}")))?;
823 roots
824 .insert(record.canonical_path.as_slice(), value.as_slice())
825 .map_err(|e| GuardStoreError::Io(format!("insert root: {e}")))?;
826
827 let mut gaps_table = txn
829 .open_table(ROOT_GAPS_TABLE)
830 .map_err(|e| GuardStoreError::Io(format!("open root_gaps table: {e}")))?;
831 let prefix = record.canonical_path.as_slice();
832 let keys_to_remove: Vec<Vec<u8>> = gaps_table
833 .range::<&[u8]>(..)
834 .map_err(|e| GuardStoreError::Io(format!("iterate root_gaps: {e}")))?
835 .filter_map(|entry| {
836 let (key, _) = entry.ok()?;
837 let k = key.value();
838 if k.starts_with(prefix) && k.len() > prefix.len() && k[prefix.len()] == 0 {
839 Some(k.to_vec())
840 } else {
841 None
842 }
843 })
844 .collect();
845 for key in keys_to_remove {
846 gaps_table
847 .remove(key.as_slice())
848 .map_err(|e| GuardStoreError::Io(format!("remove old root gap: {e}")))?;
849 }
850 for (blob_oid, desc) in gaps {
851 let mut key = Vec::with_capacity(prefix.len() + 1 + blob_oid.len());
852 key.extend_from_slice(prefix);
853 key.push(0);
854 key.extend_from_slice(blob_oid.as_bytes());
855 gaps_table
856 .insert(key.as_slice(), desc.as_bytes())
857 .map_err(|e| GuardStoreError::Io(format!("insert root gap: {e}")))?;
858 }
859 }
860 txn.commit()
861 .map_err(|e| GuardStoreError::Io(format!("commit root with gaps: {e}")))?;
862 Ok(())
863 }
864}
865
866fn attestation_key(att: &GitCleanAttestation) -> Vec<u8> {
869 let label = match att.hash_algorithm {
870 GitHashAlgorithm::Sha1 => "sha1",
871 GitHashAlgorithm::Sha256 => "sha256",
872 };
873 let mut key = Vec::with_capacity(label.len() + 1 + att.blob_oid.len() + 1 + 64);
874 key.extend_from_slice(label.as_bytes());
875 key.push(0);
876 key.extend_from_slice(att.blob_oid.as_bytes());
877 key.push(0);
878 key.extend_from_slice(att.policy_identity.detector_digest.as_bytes());
879 key
880}