1mod check;
11mod commit;
12mod fetch;
13pub mod format;
14#[cfg(feature = "knit")]
15mod knit_repo;
16mod pack_2a;
17mod pack_2a_writer;
18mod pack_collection;
19#[cfg(feature = "knitpack")]
20mod pack_index;
21#[cfg(feature = "knitpack")]
22mod pack_knit;
23mod tree;
24#[cfg(feature = "weave")]
25mod weave_repo;
26
27pub use check::{check, CheckResult};
28pub use commit::CommitBuilder;
29pub use fetch::fetch;
30
31#[derive(Debug, Default, Clone, PartialEq, Eq)]
33pub struct ReconcileResult {
34 pub garbage_inventories: usize,
36 pub repacked: bool,
38}
39pub use format::{all_formats, find_format, RepositoryFormat};
40#[cfg(feature = "knit")]
41pub use knit_repo::KnitRepository;
42pub use pack_2a::{Pack2aRepository, RepositoryError, SharedTransport};
43#[cfg(feature = "knitpack")]
44pub use pack_knit::KnitPackRepository;
45pub use tree::RevisionTree;
46#[cfg(feature = "weave")]
47pub use weave_repo::WeaveRepository;
48
49use crate::inventory::Inventory;
50
51pub trait Repository: Send + Sync {
58 fn format(&self) -> &'static RepositoryFormat;
60
61 fn as_any(&self) -> &dyn std::any::Any;
67
68 fn try_fetch_from(
79 &mut self,
80 _source: &dyn Repository,
81 _revision_ids: &[Vec<u8>],
82 ) -> Result<bool, RepositoryError> {
83 Ok(false)
84 }
85
86 fn all_revision_ids(&self) -> Result<Vec<Vec<u8>>, RepositoryError>;
88
89 fn get_parent_map(
94 &self,
95 revision_ids: &[Vec<u8>],
96 ) -> Result<std::collections::HashMap<Vec<u8>, Vec<Vec<u8>>>, RepositoryError>;
97
98 fn has_revision(&self, revision_id: &[u8]) -> Result<bool, RepositoryError> {
102 Ok(self
103 .get_parent_map(std::slice::from_ref(&revision_id.to_vec()))?
104 .contains_key(revision_id))
105 }
106
107 fn get_revision(
109 &self,
110 revision_id: &[u8],
111 ) -> Result<crate::revision::Revision, RepositoryError>;
112
113 fn get_inventory(&self, revision_id: &[u8]) -> Result<Box<dyn Inventory>, RepositoryError>;
115
116 fn revision_tree(&self, revision_id: &[u8]) -> Result<RevisionTree, RepositoryError> {
120 if revision_id == crate::branch::NULL_REVISION {
121 let empty = crate::inventory::MutableInventory::new();
124 return Ok(RevisionTree::new(revision_id.to_vec(), Box::new(empty)));
125 }
126 let inventory = self.get_inventory(revision_id)?;
127 Ok(RevisionTree::new(revision_id.to_vec(), inventory))
128 }
129
130 fn get_file_text(&self, file_id: &[u8], revision: &[u8]) -> Result<Vec<u8>, RepositoryError>;
132
133 fn get_file_text_at_path(
138 &self,
139 path: &str,
140 revision: &[u8],
141 ) -> Result<Vec<u8>, RepositoryError> {
142 let tree = self.revision_tree(revision)?;
143 let file_id = tree
144 .path2id(path)
145 .ok_or_else(|| RepositoryError::NoSuchRevision(path.as_bytes().to_vec()))?;
146 self.get_file_text(file_id.as_bytes(), revision)
147 }
148
149 fn start_write_group(&mut self) -> Result<(), RepositoryError>;
152
153 fn add_revision(
156 &mut self,
157 revision: &crate::revision::Revision,
158 parents: &[Vec<u8>],
159 ) -> Result<(), RepositoryError>;
160
161 fn add_inventory_from_entries(
166 &mut self,
167 revision_id: &[u8],
168 parents: &[Vec<u8>],
169 root_id: &[u8],
170 entries: &[crate::inventory::Entry],
171 ) -> Result<Vec<u8>, RepositoryError>;
172
173 fn add_inventory_by_delta(
179 &mut self,
180 basis_revision_id: &[u8],
181 delta: &crate::inventory_delta::InventoryDelta,
182 new_revision_id: &[u8],
183 parents: &[Vec<u8>],
184 ) -> Result<Vec<u8>, RepositoryError>;
185
186 fn add_text(
189 &mut self,
190 file_id: &[u8],
191 revision: &[u8],
192 parents: &[(Vec<u8>, Vec<u8>)],
193 bytes: &[u8],
194 ) -> Result<(), RepositoryError>;
195
196 fn add_signature_text(
198 &mut self,
199 revision_id: &[u8],
200 signature: &[u8],
201 ) -> Result<(), RepositoryError>;
202
203 fn get_signature_text(&self, revision_id: &[u8]) -> Result<Option<Vec<u8>>, RepositoryError>;
205
206 fn commit_write_group(&mut self) -> Result<(), RepositoryError>;
208
209 fn pack(&mut self) -> Result<(), RepositoryError> {
214 Ok(())
215 }
216
217 fn autopack(&mut self) -> Result<bool, RepositoryError> {
222 Ok(false)
223 }
224
225 fn check(&self) -> Result<CheckResult, RepositoryError> {
229 check::check(self)
230 }
231
232 fn reconcile(&mut self) -> Result<ReconcileResult, RepositoryError> {
240 Ok(ReconcileResult::default())
241 }
242
243 fn add_fallback_repository(
251 &mut self,
252 _fallback: Box<dyn Repository>,
253 ) -> Result<(), RepositoryError> {
254 Err(RepositoryError::UnsupportedFormat(
255 "repository does not support fallbacks",
256 ))
257 }
258
259 #[cfg(feature = "gpg")]
269 fn verify_revision_signature(
270 &self,
271 revision_id: &[u8],
272 certs: &[sequoia_openpgp::Cert],
273 ) -> Result<crate::gpg::VerificationResult, RepositoryError> {
274 use crate::gpg::VerificationResult;
275 let Some(signature) = self.get_signature_text(revision_id)? else {
276 return Ok(VerificationResult::NotSigned);
277 };
278 let expected = testament_short_text_for_revision(self, revision_id)?;
279 let verification = crate::gpg::verify_clearsigned(&signature, certs);
280 if verification.plaintext.as_deref() != Some(expected.as_slice()) {
281 return Ok(VerificationResult::NotValid);
282 }
283 Ok(verification.result)
284 }
285
286 #[cfg(feature = "gpg")]
291 fn verify_revision_signature_bytes(
292 &self,
293 revision_id: &[u8],
294 keyring: &[Vec<u8>],
295 ) -> Result<crate::gpg::VerificationResult, RepositoryError> {
296 let certs = crate::gpg::parse_keyring(keyring)
297 .map_err(|e| RepositoryError::Corrupt(format!("keyring: {e}")))?;
298 self.verify_revision_signature(revision_id, &certs)
299 }
300}
301
302#[cfg(feature = "gpg")]
309fn testament_short_text_for_revision(
310 repo: &(impl Repository + ?Sized),
311 revision_id: &[u8],
312) -> Result<Vec<u8>, RepositoryError> {
313 use crate::testament::{EntryKind as TKind, Testament, TestamentEntry, TestamentFormat};
314
315 let revision = repo.get_revision(revision_id)?;
316 let tree = repo.revision_tree(revision_id)?;
317
318 let mut entries = Vec::new();
319 for (path, entry) in tree.iter_entries() {
320 if path.is_empty() || path == "." {
322 continue;
323 }
324 let (kind, content) = match entry.kind() {
325 crate::osutils::Kind::File => (
326 TKind::File,
327 entry.text_sha1().map(|s| s.to_vec()).unwrap_or_default(),
328 ),
329 crate::osutils::Kind::Directory => (TKind::Directory, Vec::new()),
330 crate::osutils::Kind::Symlink => (
331 TKind::Symlink,
332 entry
333 .symlink_target()
334 .map(|t| t.as_bytes().to_vec())
335 .unwrap_or_default(),
336 ),
337 crate::osutils::Kind::TreeReference => (TKind::TreeReference, Vec::new()),
338 };
339 entries.push(TestamentEntry {
340 path,
341 kind,
342 file_id: entry.file_id().as_bytes().to_vec(),
343 content,
344 revision: entry
345 .revision()
346 .map(|r| r.as_bytes().to_vec())
347 .unwrap_or_default(),
348 executable: entry.executable(),
349 });
350 }
351
352 let testament = Testament {
353 revision_id: revision_id.to_vec(),
354 committer: revision.committer.clone().unwrap_or_default(),
355 timestamp: revision.timestamp as i64,
356 timezone: revision.timezone.unwrap_or(0),
357 message: revision.message.clone(),
358 parent_ids: revision
359 .parent_ids
360 .iter()
361 .map(|p| p.as_bytes().to_vec())
362 .collect(),
363 revprops: revision
364 .properties
365 .iter()
366 .map(|(k, v)| (k.clone(), String::from_utf8_lossy(v).into_owned()))
367 .collect(),
368 entries,
369 };
370 testament
371 .as_short_text(TestamentFormat::V1)
372 .map_err(|e| RepositoryError::Corrupt(format!("testament: {e}")))
373}
374
375pub struct StackedRepository {
385 primary: Box<dyn Repository>,
386 fallbacks: Vec<Box<dyn Repository>>,
387}
388
389fn is_not_present(e: &RepositoryError) -> bool {
392 matches!(
393 e,
394 RepositoryError::NoSuchRevision(_) | RepositoryError::NoSuchFileText { .. }
395 )
396}
397
398impl StackedRepository {
399 pub fn new(primary: Box<dyn Repository>) -> Self {
401 StackedRepository {
402 primary,
403 fallbacks: Vec::new(),
404 }
405 }
406
407 fn first_present<T>(
413 &self,
414 mut f: impl FnMut(&dyn Repository) -> Result<T, RepositoryError>,
415 ) -> Result<T, RepositoryError> {
416 match f(self.primary.as_ref()) {
417 Err(e) if is_not_present(&e) => {
418 for fallback in &self.fallbacks {
419 match f(fallback.as_ref()) {
420 Err(e) if is_not_present(&e) => continue,
421 other => return other,
422 }
423 }
424 Err(e)
425 }
426 other => other,
427 }
428 }
429}
430
431impl Repository for StackedRepository {
432 fn format(&self) -> &'static RepositoryFormat {
433 self.primary.format()
434 }
435
436 fn as_any(&self) -> &dyn std::any::Any {
437 self
438 }
439
440 fn all_revision_ids(&self) -> Result<Vec<Vec<u8>>, RepositoryError> {
441 self.primary.all_revision_ids()
444 }
445
446 fn get_parent_map(
447 &self,
448 revision_ids: &[Vec<u8>],
449 ) -> Result<std::collections::HashMap<Vec<u8>, Vec<Vec<u8>>>, RepositoryError> {
450 let mut map = self.primary.get_parent_map(revision_ids)?;
451 let mut missing: Vec<Vec<u8>> = revision_ids
453 .iter()
454 .filter(|id| !map.contains_key(*id))
455 .cloned()
456 .collect();
457 for fallback in &self.fallbacks {
458 if missing.is_empty() {
459 break;
460 }
461 let found = fallback.get_parent_map(&missing)?;
462 missing.retain(|id| !found.contains_key(id));
463 map.extend(found);
464 }
465 Ok(map)
466 }
467
468 fn has_revision(&self, revision_id: &[u8]) -> Result<bool, RepositoryError> {
469 if self.primary.has_revision(revision_id)? {
470 return Ok(true);
471 }
472 for fallback in &self.fallbacks {
473 if fallback.has_revision(revision_id)? {
474 return Ok(true);
475 }
476 }
477 Ok(false)
478 }
479
480 fn get_revision(
481 &self,
482 revision_id: &[u8],
483 ) -> Result<crate::revision::Revision, RepositoryError> {
484 self.first_present(|r| r.get_revision(revision_id))
485 }
486
487 fn get_inventory(&self, revision_id: &[u8]) -> Result<Box<dyn Inventory>, RepositoryError> {
488 self.first_present(|r| r.get_inventory(revision_id))
489 }
490
491 fn get_file_text(&self, file_id: &[u8], revision: &[u8]) -> Result<Vec<u8>, RepositoryError> {
492 self.first_present(|r| r.get_file_text(file_id, revision))
493 }
494
495 fn start_write_group(&mut self) -> Result<(), RepositoryError> {
496 self.primary.start_write_group()
497 }
498
499 fn add_revision(
500 &mut self,
501 revision: &crate::revision::Revision,
502 parents: &[Vec<u8>],
503 ) -> Result<(), RepositoryError> {
504 self.primary.add_revision(revision, parents)
505 }
506
507 fn add_inventory_from_entries(
508 &mut self,
509 revision_id: &[u8],
510 parents: &[Vec<u8>],
511 root_id: &[u8],
512 entries: &[crate::inventory::Entry],
513 ) -> Result<Vec<u8>, RepositoryError> {
514 self.primary
515 .add_inventory_from_entries(revision_id, parents, root_id, entries)
516 }
517
518 fn add_inventory_by_delta(
519 &mut self,
520 basis_revision_id: &[u8],
521 delta: &crate::inventory_delta::InventoryDelta,
522 new_revision_id: &[u8],
523 parents: &[Vec<u8>],
524 ) -> Result<Vec<u8>, RepositoryError> {
525 self.primary
526 .add_inventory_by_delta(basis_revision_id, delta, new_revision_id, parents)
527 }
528
529 fn add_text(
530 &mut self,
531 file_id: &[u8],
532 revision: &[u8],
533 parents: &[(Vec<u8>, Vec<u8>)],
534 bytes: &[u8],
535 ) -> Result<(), RepositoryError> {
536 self.primary.add_text(file_id, revision, parents, bytes)
537 }
538
539 fn add_signature_text(
540 &mut self,
541 revision_id: &[u8],
542 signature: &[u8],
543 ) -> Result<(), RepositoryError> {
544 self.primary.add_signature_text(revision_id, signature)
545 }
546
547 fn get_signature_text(&self, revision_id: &[u8]) -> Result<Option<Vec<u8>>, RepositoryError> {
548 match self.primary.get_signature_text(revision_id)? {
549 Some(sig) => Ok(Some(sig)),
550 None => {
551 for fallback in &self.fallbacks {
552 if let Some(sig) = fallback.get_signature_text(revision_id)? {
553 return Ok(Some(sig));
554 }
555 }
556 Ok(None)
557 }
558 }
559 }
560
561 fn commit_write_group(&mut self) -> Result<(), RepositoryError> {
562 self.primary.commit_write_group()
563 }
564
565 fn add_fallback_repository(
566 &mut self,
567 fallback: Box<dyn Repository>,
568 ) -> Result<(), RepositoryError> {
569 self.fallbacks.push(fallback);
570 Ok(())
571 }
572}
573
574pub(crate) fn unkey_knit_parent_map(
579 raw: std::collections::HashMap<crate::knit::KnitKey, Vec<crate::knit::KnitKey>>,
580) -> std::collections::HashMap<Vec<u8>, Vec<Vec<u8>>> {
581 let mut out = std::collections::HashMap::with_capacity(raw.len());
582 for (key, parents) in raw {
583 if let Some(revid) = key.into_iter().next() {
584 let parent_ids = parents
585 .into_iter()
586 .filter_map(|p| p.into_iter().next())
587 .collect();
588 out.insert(revid, parent_ids);
589 }
590 }
591 out
592}
593
594impl dyn Repository + '_ {
595 pub fn get_commit_builder(
600 &mut self,
601 parents: Vec<Vec<u8>>,
602 new_revision_id: Vec<u8>,
603 committer: String,
604 timestamp: u64,
605 timezone: i32,
606 ) -> CommitBuilder<'_> {
607 CommitBuilder::new(
608 self,
609 parents,
610 new_revision_id,
611 committer,
612 timestamp,
613 timezone,
614 )
615 }
616}
617
618pub fn open(transport: SharedTransport) -> Result<Box<dyn Repository>, RepositoryError> {
622 let marker = transport.get_bytes("format")?;
623 let format =
624 find_format(&marker).ok_or_else(|| RepositoryError::UnknownFormat(marker.clone()))?;
625 (format.open)(transport)
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631 use crate::transport::LocalTransport;
632 use std::sync::Arc;
633
634 struct Scenario {
638 label: &'static str,
639 create: fn(SharedTransport) -> Box<dyn Repository>,
640 reopen: fn(SharedTransport) -> Box<dyn Repository>,
641 signs: bool,
642 }
643
644 #[cfg(feature = "knitpack")]
645 fn knitpack6() -> &'static RepositoryFormat {
646 find_format(b"Bazaar RepositoryFormatKnitPack6 (bzr 1.9)\n").unwrap()
647 }
648 #[cfg(feature = "knit")]
649 fn knit1() -> &'static RepositoryFormat {
650 find_format(b"Bazaar-NG Knit Repository Format 1").unwrap()
651 }
652 #[cfg(feature = "weave")]
653 fn weave6() -> &'static RepositoryFormat {
654 find_format(b"Bazaar-NG branch, format 6\n").unwrap()
655 }
656
657 fn scenarios() -> Vec<Scenario> {
660 vec![
661 Scenario {
662 label: "2a",
663 create: |t| Box::new(Pack2aRepository::create(t).unwrap()),
664 reopen: |t| Box::new(Pack2aRepository::open(t).unwrap()),
665 signs: true,
666 },
667 #[cfg(feature = "knitpack")]
668 Scenario {
669 label: "knit-pack",
670 create: |t| Box::new(KnitPackRepository::create(t, knitpack6()).unwrap()),
671 reopen: |t| Box::new(KnitPackRepository::open(t).unwrap()),
672 signs: true,
673 },
674 #[cfg(feature = "knit")]
675 Scenario {
676 label: "knit",
677 create: |t| Box::new(KnitRepository::create(t, knit1()).unwrap()),
678 reopen: |t| Box::new(KnitRepository::open(t).unwrap()),
679 signs: true,
680 },
681 #[cfg(feature = "weave")]
682 Scenario {
683 label: "weave",
684 create: |t| Box::new(WeaveRepository::create(t, weave6()).unwrap()),
685 reopen: |t| Box::new(WeaveRepository::open(t, weave6()).unwrap()),
686 signs: true,
687 },
688 ]
689 }
690
691 fn revision(id: &[u8], parents: Vec<&[u8]>, message: &str) -> crate::revision::Revision {
692 crate::revision::Revision::new(
693 crate::RevisionId::from(id),
694 parents.into_iter().map(crate::RevisionId::from).collect(),
695 Some("T <t@e>".to_string()),
696 message.to_string(),
697 std::collections::HashMap::new(),
698 None,
699 1577880000.0,
700 Some(0),
701 )
702 }
703
704 fn entries(rev: &[u8]) -> Vec<crate::inventory::Entry> {
705 use crate::FileId;
706 let root = crate::inventory::ROOT_ID;
707 vec![
708 crate::inventory::Entry::root(FileId::from(root), Some(crate::RevisionId::from(rev))),
709 crate::inventory::Entry::file(
710 FileId::from(&b"file-1"[..]),
711 "a.txt".into(),
712 FileId::from(root),
713 Some(crate::RevisionId::from(rev)),
714 Some(crate::weave::sha_strings(&[b"hello\n"])),
715 Some(6),
716 Some(false),
717 None,
718 ),
719 ]
720 }
721
722 #[test]
726 fn revision_text_inventory_signature_round_trip() {
727 for s in scenarios() {
728 let dir = tempfile::tempdir().unwrap();
729 let t: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
730 let mut repo = (s.create)(t.clone());
731
732 repo.start_write_group().unwrap();
733 repo.add_revision(&revision(b"rev-1", vec![], "first"), &[])
734 .unwrap();
735 repo.add_inventory_from_entries(
736 b"rev-1",
737 &[],
738 crate::inventory::ROOT_ID,
739 &entries(b"rev-1"),
740 )
741 .unwrap();
742 repo.add_text(b"file-1", b"rev-1", &[], b"hello\n").unwrap();
743 repo.add_text(
744 b"file-1",
745 b"rev-2",
746 &[(b"file-1".to_vec(), b"rev-1".to_vec())],
747 b"hello\ngoodbye\n",
748 )
749 .unwrap();
750 repo.add_revision(
751 &revision(b"rev-2", vec![b"rev-1"], "second"),
752 &[b"rev-1".to_vec()],
753 )
754 .unwrap();
755 if s.signs {
756 repo.add_signature_text(b"rev-1", b"-----SIG-----\nsigned\n")
757 .unwrap();
758 }
759 repo.commit_write_group().unwrap();
760
761 let repo = (s.reopen)(t);
762 let mut ids = repo.all_revision_ids().unwrap();
763 ids.sort();
764 assert_eq!(
765 ids,
766 vec![b"rev-1".to_vec(), b"rev-2".to_vec()],
767 "{}",
768 s.label
769 );
770
771 let pm = repo
774 .get_parent_map(&[b"rev-1".to_vec(), b"rev-2".to_vec(), b"nope".to_vec()])
775 .unwrap();
776 assert_eq!(pm.get(&b"rev-1".to_vec()), Some(&vec![]), "{}", s.label);
777 assert_eq!(
778 pm.get(&b"rev-2".to_vec()),
779 Some(&vec![b"rev-1".to_vec()]),
780 "{}",
781 s.label
782 );
783 assert!(!pm.contains_key(&b"nope".to_vec()), "{}", s.label);
784 assert!(repo.has_revision(b"rev-2").unwrap(), "{}", s.label);
785 assert!(!repo.has_revision(b"nope").unwrap(), "{}", s.label);
786 assert_eq!(
787 repo.get_revision(b"rev-1").unwrap().message,
788 "first",
789 "{}",
790 s.label
791 );
792 let got2 = repo.get_revision(b"rev-2").unwrap();
793 assert_eq!(got2.message, "second", "{}", s.label);
794 assert_eq!(
795 got2.parent_ids
796 .iter()
797 .map(|p| p.as_bytes().to_vec())
798 .collect::<Vec<_>>(),
799 vec![b"rev-1".to_vec()],
800 "{}",
801 s.label
802 );
803 assert_eq!(
804 repo.get_file_text(b"file-1", b"rev-1").unwrap(),
805 b"hello\n",
806 "{}",
807 s.label
808 );
809 assert_eq!(
810 repo.get_file_text(b"file-1", b"rev-2").unwrap(),
811 b"hello\ngoodbye\n",
812 "{}",
813 s.label
814 );
815 let inv = repo.get_inventory(b"rev-1").unwrap();
816 let paths: Vec<String> = inv.entries().unwrap().into_iter().map(|(p, _)| p).collect();
817 assert_eq!(paths, vec!["a.txt".to_string()], "{}", s.label);
818
819 let tree = repo.revision_tree(b"rev-1").unwrap();
822 assert_eq!(
823 tree.path2id("a.txt").map(|f| f.as_bytes().to_vec()),
824 Some(b"file-1".to_vec()),
825 "{}",
826 s.label
827 );
828 assert!(tree.path2id("nope").is_none(), "{}", s.label);
829 let tree_paths: Vec<String> = tree.iter_entries().into_iter().map(|(p, _)| p).collect();
830 assert_eq!(tree_paths, vec!["a.txt".to_string()], "{}", s.label);
831 assert_eq!(
832 repo.get_file_text_at_path("a.txt", b"rev-1").unwrap(),
833 b"hello\n",
834 "{}",
835 s.label
836 );
837
838 let expected = if s.signs {
840 Some(b"-----SIG-----\nsigned\n".to_vec())
841 } else {
842 None
843 };
844 assert_eq!(
845 repo.get_signature_text(b"rev-1").unwrap(),
846 expected,
847 "{}",
848 s.label
849 );
850 assert_eq!(
851 repo.get_signature_text(b"rev-2").unwrap(),
852 None,
853 "{}",
854 s.label
855 );
856 }
857 }
858
859 fn make_2a_with_rev(dir: &std::path::Path, rev: &[u8]) -> Box<dyn Repository> {
861 let t: SharedTransport = Arc::new(LocalTransport::new(dir));
862 let mut repo =
863 Box::new(Pack2aRepository::create(t.clone()).unwrap()) as Box<dyn Repository>;
864 repo.start_write_group().unwrap();
865 repo.add_revision(&revision(rev, vec![], "msg"), &[])
866 .unwrap();
867 repo.add_inventory_from_entries(rev, &[], crate::inventory::ROOT_ID, &entries(rev))
868 .unwrap();
869 repo.add_text(b"file-1", rev, &[], b"hello\n").unwrap();
870 repo.commit_write_group().unwrap();
871 Box::new(Pack2aRepository::open(t).unwrap())
872 }
873
874 #[test]
877 fn stacked_repository_reads_through_fallback() {
878 let base_dir = tempfile::tempdir().unwrap();
879 let base = make_2a_with_rev(base_dir.path(), b"rev-base");
880
881 let top_dir = tempfile::tempdir().unwrap();
883 let top_t: SharedTransport = Arc::new(LocalTransport::new(top_dir.path()));
884 let primary = Box::new(Pack2aRepository::create(top_t).unwrap()) as Box<dyn Repository>;
885
886 let mut stacked = StackedRepository::new(primary);
887 assert!(!stacked.has_revision(b"rev-base").unwrap());
888 stacked.add_fallback_repository(base).unwrap();
889
890 assert!(stacked.has_revision(b"rev-base").unwrap());
892 assert_eq!(stacked.get_revision(b"rev-base").unwrap().message, "msg");
893 assert_eq!(
894 stacked.get_file_text(b"file-1", b"rev-base").unwrap(),
895 b"hello\n"
896 );
897 let pm = stacked.get_parent_map(&[b"rev-base".to_vec()]).unwrap();
898 assert_eq!(pm.get(&b"rev-base".to_vec()), Some(&vec![]));
899
900 assert!(stacked.all_revision_ids().unwrap().is_empty());
902
903 assert!(matches!(
905 stacked.get_revision(b"rev-missing"),
906 Err(RepositoryError::NoSuchRevision(_))
907 ));
908 }
909
910 #[test]
912 fn plain_repository_rejects_fallback() {
913 let dir = tempfile::tempdir().unwrap();
914 let t: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
915 let mut repo = Pack2aRepository::create(t).unwrap();
916 let other_dir = tempfile::tempdir().unwrap();
917 let other = make_2a_with_rev(other_dir.path(), b"rev-x");
918 assert!(matches!(
919 repo.add_fallback_repository(other),
920 Err(RepositoryError::UnsupportedFormat(_))
921 ));
922 }
923
924 #[cfg(feature = "gpg")]
925 fn gen_signing_cert() -> (sequoia_openpgp::Cert, Vec<u8>) {
926 use sequoia_openpgp::cert::CertBuilder;
927 use sequoia_openpgp::serialize::Serialize;
928 let (cert, _) = CertBuilder::new().add_signing_subkey().generate().unwrap();
929 let mut tsk = Vec::new();
930 cert.as_tsk().serialize(&mut tsk).unwrap();
931 (cert, tsk)
932 }
933
934 #[cfg(feature = "gpg")]
937 #[test]
938 fn verify_revision_signature_valid_and_unsigned() {
939 use crate::gpg::VerificationResult;
940
941 let dir = tempfile::tempdir().unwrap();
942 let t: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
943 let repo = make_2a_with_rev_at(&t, b"rev-1");
944
945 assert_eq!(
947 repo.verify_revision_signature(b"rev-1", &[]).unwrap(),
948 VerificationResult::NotSigned
949 );
950
951 let (cert, tsk) = gen_signing_cert();
953 let testament = testament_short_text_for_revision(repo.as_ref(), b"rev-1").unwrap();
954 let signature = crate::gpg::clearsign(&testament, &tsk).unwrap();
955 let mut repo = repo;
956 repo.start_write_group().unwrap();
957 repo.add_signature_text(b"rev-1", &signature).unwrap();
958 repo.commit_write_group().unwrap();
959 let repo = Box::new(Pack2aRepository::open(t.clone()).unwrap()) as Box<dyn Repository>;
961
962 assert_eq!(
963 repo.verify_revision_signature(b"rev-1", std::slice::from_ref(&cert))
964 .unwrap(),
965 VerificationResult::Valid
966 );
967 }
968
969 #[cfg(feature = "gpg")]
972 #[test]
973 fn verify_revision_signature_wrong_testament_is_not_valid() {
974 use crate::gpg::VerificationResult;
975
976 let dir = tempfile::tempdir().unwrap();
977 let t: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
978 let mut repo = make_2a_with_rev_at(&t, b"rev-1");
979
980 let (cert, tsk) = gen_signing_cert();
981 let signature = crate::gpg::clearsign(b"not the testament\n", &tsk).unwrap();
983 repo.start_write_group().unwrap();
984 repo.add_signature_text(b"rev-1", &signature).unwrap();
985 repo.commit_write_group().unwrap();
986 let repo = Box::new(Pack2aRepository::open(t.clone()).unwrap()) as Box<dyn Repository>;
987
988 assert_eq!(
989 repo.verify_revision_signature(b"rev-1", std::slice::from_ref(&cert))
990 .unwrap(),
991 VerificationResult::NotValid
992 );
993 }
994
995 #[cfg(feature = "gpg")]
998 fn make_2a_with_rev_at(t: &SharedTransport, rev: &[u8]) -> Box<dyn Repository> {
999 let mut repo =
1000 Box::new(Pack2aRepository::create(t.clone()).unwrap()) as Box<dyn Repository>;
1001 repo.start_write_group().unwrap();
1002 repo.add_revision(&revision(rev, vec![], "msg"), &[])
1003 .unwrap();
1004 repo.add_inventory_from_entries(rev, &[], crate::inventory::ROOT_ID, &entries(rev))
1005 .unwrap();
1006 repo.add_text(b"file-1", rev, &[], b"hello\n").unwrap();
1007 repo.commit_write_group().unwrap();
1008 Box::new(Pack2aRepository::open(t.clone()).unwrap())
1009 }
1010}