1use crate::{
2 activity::{LatestReviewActivity, ReviewActivity},
3 id::{self, LockedId, PassphraseFn},
4 util::{self, git::is_unrecoverable},
5 Error, ProofStore, Result, Warning,
6};
7use crev_common::{
8 self, sanitize_name_for_fs, sanitize_url_for_fs,
9 serde::{as_base64, from_base64},
10};
11use crev_data::{
12 id::UnlockedId,
13 proof::{self, trust::TrustLevel, OverrideItem},
14 Id, PublicId, RegistrySource, Url,
15};
16use default::default;
17use directories::ProjectDirs;
18use log::{debug, error, info, warn};
19use resiter::{FilterMap, Map};
20use serde::{Deserialize, Serialize};
21use std::{
22 collections::HashSet,
23 ffi::OsString,
24 fs,
25 io::{BufRead, BufReader, Write},
26 path::{Path, PathBuf},
27 str::FromStr,
28 sync::{Arc, Mutex},
29};
30
31const CURRENT_USER_CONFIG_SERIALIZATION_VERSION: i64 = -1;
32
33fn generete_salt() -> Vec<u8> {
35 crev_common::rand::random_vec(32)
36}
37
38fn backfill_salt() -> Vec<u8> {
46 crev_common::blake2b256sum(b"BACKFILLED_SUM").to_vec()
47}
48
49fn is_none_or_empty(s: &Option<String>) -> bool {
50 if let Some(s) = s {
51 s.is_empty()
52 } else {
53 true
54 }
55}
56
57#[derive(Serialize, Deserialize, Debug, Clone)]
58pub struct UserConfig {
59 pub version: i64,
60 #[serde(rename = "current-id")]
61 pub current_id: Option<Id>,
62 #[serde(
63 rename = "host-salt",
64 serialize_with = "as_base64",
65 deserialize_with = "from_base64",
66 default = "backfill_salt"
67 )]
68 host_salt: Vec<u8>,
69
70 #[serde(
71 rename = "open-cmd",
72 skip_serializing_if = "is_none_or_empty",
73 default = "Option::default"
74 )]
75 pub open_cmd: Option<String>,
76}
77
78impl Default for UserConfig {
79 fn default() -> Self {
80 Self {
81 version: CURRENT_USER_CONFIG_SERIALIZATION_VERSION,
82 current_id: None,
83 host_salt: generete_salt(),
84 open_cmd: None,
85 }
86 }
87}
88
89impl UserConfig {
90 pub fn get_current_userid(&self) -> Result<&Id> {
91 self.get_current_userid_opt().ok_or(Error::CurrentIDNotSet)
92 }
93
94 #[must_use]
95 pub fn get_current_userid_opt(&self) -> Option<&Id> {
96 self.current_id.as_ref()
97 }
98}
99
100pub struct Local {
104 config_path: PathBuf,
105 data_path: PathBuf,
106 cache_path: PathBuf,
107 cur_url: Mutex<Option<Url>>,
108 user_config: Mutex<Option<UserConfig>>,
109}
110
111impl Local {
112 #[allow(clippy::new_ret_no_self)]
114 fn new() -> Result<Self> {
115 let proj_dir = match std::env::var_os("CARGO_CREV_ROOT_DIR_OVERRIDE") {
116 None => ProjectDirs::from("", "", "crev"),
117 Some(path) => ProjectDirs::from_path(path.into()),
118 }
119 .ok_or(Error::NoHomeDirectory)?;
120 let config_path = proj_dir.config_dir().into();
121 let data_path = proj_dir.data_dir().into();
122 let cache_path = proj_dir.cache_dir().into();
123 Ok(Self {
124 config_path,
125 data_path,
126 cache_path,
127 cur_url: Mutex::new(None),
128 user_config: Mutex::new(None),
129 })
130 }
131
132 pub fn load_db(&self) -> Result<crev_wot::ProofDB> {
134 let mut db = crev_wot::ProofDB::new();
135 for local_id in self.get_current_user_public_ids()? {
136 db.record_trusted_url_from_own_id(&local_id);
137 }
138 db.import_from_iter(
139 self.all_local_proofs()
140 .map(move |p| (p, crev_wot::FetchSource::LocalUser)),
141 );
142 db.import_from_iter(proofs_iter_for_remotes_checkouts(
143 self.cache_remotes_path(),
144 )?);
145 Ok(db)
146 }
147
148 pub fn config_root(&self) -> &Path {
150 &self.config_path
151 }
152
153 pub fn data_root(&self) -> &Path {
155 &self.data_path
156 }
157
158 pub fn cache_root(&self) -> &Path {
160 &self.cache_path
161 }
162
163 pub fn auto_open() -> Result<Self> {
165 let repo = Self::new()?;
166 fs::create_dir_all(repo.cache_remotes_path())?;
167 if !repo.config_path.exists() || !repo.user_config_path().exists() {
168 return Err(Error::UserConfigNotInitialized);
169 }
170 fs::create_dir_all(&repo.data_path)?;
171
172 let old_proofs = repo.config_path.join("proofs");
174 let new_proofs = repo.data_path.join("proofs");
175 if !new_proofs.exists() && old_proofs.exists() {
176 fs::rename(old_proofs, new_proofs)?;
177 }
178
179 *repo.user_config.lock().unwrap() = Some(repo.load_user_config()?);
180 Ok(repo)
181 }
182
183 pub fn auto_create() -> Result<Self> {
185 let repo = Self::new()?;
186 fs::create_dir_all(&repo.config_path)?;
187 fs::create_dir_all(&repo.data_path)?;
188 fs::create_dir_all(repo.cache_remotes_path())?;
189
190 let config_path = repo.user_config_path();
191 if config_path.exists() {
192 return Err(Error::UserConfigAlreadyExists);
193 }
194 let config: UserConfig = default();
195 repo.store_user_config(&config)?;
196 *repo.user_config.lock().unwrap() = Some(config);
197 Ok(repo)
198 }
199
200 pub fn auto_create_or_open() -> Result<Self> {
202 let repo = Self::new()?;
203 let config_path = repo.user_config_path();
204 if config_path.exists() {
205 Self::auto_open()
206 } else {
207 Self::auto_create()
208 }
209 }
210
211 pub fn read_current_id(&self) -> Result<crev_data::Id> {
213 Ok(self.load_user_config()?.get_current_userid()?.clone())
214 }
215
216 pub fn read_current_id_opt(&self) -> Result<Option<crev_data::Id>> {
218 Ok(self.load_user_config()?.get_current_userid_opt().cloned())
219 }
220
221 pub fn get_for_id_from_str_opt(&self, id_str: Option<&str>) -> Result<Option<Id>> {
226 id_str
227 .map(|s| crev_data::id::Id::crevid_from_str(s).map_err(Error::from))
228 .or_else(|| self.read_current_id_opt().transpose())
229 .transpose()
230 }
231
232 pub fn get_for_id_from_str(&self, id_str: Option<&str>) -> Result<Id> {
233 self.get_for_id_from_str_opt(id_str)?
234 .ok_or(Error::IDNotSpecifiedAndCurrentIDNotSet)
235 }
236
237 pub fn save_current_id(&self, id: &Id) -> Result<()> {
239 let path = self.id_path(id);
240 if !path.exists() {
241 return Err(Error::IDFileNotFound);
242 }
243
244 *self.cur_url.lock().unwrap() = None;
245
246 let mut config = self.load_user_config()?;
247 config.current_id = Some(id.clone());
248 if config.host_salt == backfill_salt() {
251 config.host_salt = generete_salt();
252 }
253 self.store_user_config(&config)?;
254
255 Ok(())
256 }
257
258 pub fn user_dir_path(&self) -> PathBuf {
260 self.config_path.clone()
261 }
262
263 pub fn user_ids_path(&self) -> PathBuf {
265 self.user_dir_path().join("ids")
266 }
267
268 pub fn user_ids_path_opt(&self) -> Option<PathBuf> {
270 let path = self.user_dir_path().join("ids");
271
272 path.exists().then_some(path)
273 }
274
275 pub fn user_proofs_path(&self) -> PathBuf {
279 self.data_path.join("proofs")
280 }
281
282 pub fn user_proofs_path_opt(&self) -> Option<PathBuf> {
284 let path = self.user_proofs_path();
285
286 path.exists().then_some(path)
287 }
288
289 fn id_path(&self, id: &Id) -> PathBuf {
291 match id {
292 Id::Crev { id } => self
293 .user_ids_path()
294 .join(format!("{}.yaml", crev_common::base64_encode(id))),
295 }
296 }
297
298 pub fn get_current_user_public_ids(&self) -> Result<Vec<PublicId>> {
300 let mut ids = vec![];
301 if let Some(ids_path) = self.user_ids_path_opt() {
302 for dir_entry in std::fs::read_dir(ids_path)? {
303 let path = dir_entry?.path();
304 if path.extension().map_or(false, |ext| ext == "yaml") {
305 let locked_id = LockedId::read_from_yaml_file(&path)?;
306 ids.push(locked_id.to_public_id());
307 }
308 }
309 }
310
311 Ok(ids)
312 }
313
314 fn user_config_path(&self) -> PathBuf {
316 self.user_dir_path().join("config.yaml")
317 }
318
319 pub fn cache_remotes_path(&self) -> PathBuf {
321 self.cache_path.join("remotes")
322 }
323
324 fn cache_activity_path(&self) -> PathBuf {
326 self.cache_path.join("activity")
327 }
328
329 fn sanitized_crate_path(
331 &self,
332 source: RegistrySource<'_>,
333 name: &str,
334 version: &crev_data::Version,
335 ) -> PathBuf {
336 let dir_name = format!("{name}_{version}_{source}");
337 self.cache_path
338 .join("src")
339 .join(sanitize_name_for_fs(&dir_name))
340 }
341
342 pub fn sanitized_crate_copy(
344 &self,
345 source: RegistrySource<'_>,
346 name: &str,
347 version: &crev_data::Version,
348 src_dir: &Path,
349 ) -> Result<PathBuf> {
350 let dest_dir = self.sanitized_crate_path(source, name, version);
351 let mut changes = Vec::new();
352 let _ = std::fs::create_dir_all(&dest_dir);
353 util::copy_dir_sanitized(src_dir, &dest_dir, &mut changes)
354 .map_err(Error::CrateSourceSanitizationError)?;
355 if !changes.is_empty() {
356 let msg = format!("Some files were renamed by cargo-crev to prevent accidental code execution or hiding of code:\n\n{}", changes.join("\n"));
357 std::fs::write(dest_dir.join("README-CREV.txt"), msg)?;
358 }
359 Ok(dest_dir)
360 }
361
362 fn cache_review_activity_path(
364 &self,
365 source: RegistrySource<'_>,
366 name: &str,
367 version: &crev_data::Version,
368 ) -> PathBuf {
369 self.cache_activity_path()
370 .join("review")
371 .join(sanitize_name_for_fs(source))
372 .join(sanitize_name_for_fs(name))
373 .join(sanitize_name_for_fs(&version.to_string()))
374 .with_extension("yaml")
375 }
376
377 fn cache_latest_review_activity_path(&self) -> PathBuf {
378 self.cache_activity_path().join("latest_review.yaml")
379 }
380
381 pub fn latest_review_activity(&self) -> Option<LatestReviewActivity> {
383 let latest_path = self.cache_latest_review_activity_path();
384 crev_common::read_from_yaml_file(&latest_path).ok()?
385 }
386
387 pub fn record_review_activity(
389 &self,
390 source: RegistrySource<'_>,
391 name: &str,
392 version: &crev_data::Version,
393 activity: &ReviewActivity,
394 ) -> Result<()> {
395 let path = self.cache_review_activity_path(source, name, version);
396
397 crev_common::save_to_yaml_file(&path, activity)
398 .map_err(|e| Error::ReviewActivity(Box::new(e)))?;
399
400 let latest_path = self.cache_latest_review_activity_path();
401 crev_common::save_to_yaml_file(&latest_path, &LatestReviewActivity {
402 source: source.to_string(),
403 name: name.to_string(),
404 version: version.clone(),
405 diff_base: activity.diff_base.clone(),
406 }).map_err(|e| Error::ReviewActivity(Box::new(e)))?;
407
408 Ok(())
409 }
410
411 pub fn read_review_activity(
413 &self,
414 source: RegistrySource<'_>,
415 name: &str,
416 version: &crev_data::Version,
417 ) -> Result<Option<ReviewActivity>> {
418 let path = self.cache_review_activity_path(source, name, version);
419
420 if path.exists() {
421 Ok(Some(
422 crev_common::read_from_yaml_file(&path)
423 .map_err(|e| Error::ReviewActivity(Box::new(e)))?,
424 ))
425 } else {
426 Ok(None)
427 }
428 }
429
430 pub fn load_user_config(&self) -> Result<UserConfig> {
432 let path = self.user_config_path();
433
434 let config_str = std::fs::read_to_string(&path)
435 .map_err(|e| Error::UserConfigLoadError(Box::new((path, e))))?;
436
437 serde_yaml::from_str(&config_str).map_err(Error::UserConfigParse)
438 }
439
440 pub fn store_user_config(&self, config: &UserConfig) -> Result<()> {
442 let path = self.user_config_path();
443
444 let config_str = serde_yaml::to_string(&config)?;
445
446 util::store_str_to_file(&path, &config_str)?;
447
448 *self.user_config.lock().unwrap() = Some(config.clone());
449 Ok(())
450 }
451
452 pub fn get_current_userid(&self) -> Result<Id> {
454 self.get_current_userid_opt()?.ok_or(Error::CurrentIDNotSet)
455 }
456
457 pub fn get_current_userid_opt(&self) -> Result<Option<Id>> {
459 let config = self.load_user_config()?;
460 Ok(config.current_id)
461 }
462
463 pub fn read_locked_id(&self, id: &Id) -> Result<LockedId> {
465 let path = self.id_path(id);
466 LockedId::read_from_yaml_file(&path)
467 }
468
469 pub fn read_current_locked_id_opt(&self) -> Result<Option<LockedId>> {
471 self.get_current_userid_opt()?
472 .map(|current_id| self.read_locked_id(¤t_id))
473 .transpose()
474 }
475
476 pub fn read_current_locked_id(&self) -> Result<LockedId> {
478 self.read_current_locked_id_opt()?
479 .ok_or(Error::CurrentIDNotSet)
480 }
481
482 pub fn read_current_unlocked_id_opt(
484 &self,
485 passphrase_callback: PassphraseFn<'_>,
486 ) -> Result<Option<UnlockedId>> {
487 self.get_current_userid_opt()?
488 .map(|current_id| self.read_unlocked_id(¤t_id, passphrase_callback))
489 .transpose()
490 }
491
492 pub fn read_current_unlocked_id(
494 &self,
495 passphrase_callback: PassphraseFn<'_>,
496 ) -> Result<UnlockedId> {
497 self.read_current_unlocked_id_opt(passphrase_callback)?
498 .ok_or(Error::CurrentIDNotSet)
499 }
500
501 pub fn read_unlocked_id(
505 &self,
506 id: &Id,
507 passphrase_callback: PassphraseFn<'_>,
508 ) -> Result<UnlockedId> {
509 let locked = self.read_locked_id(id)?;
510 let mut i = 0;
511 loop {
512 let passphrase = if locked.has_no_passphrase() {
513 String::new()
514 } else {
515 passphrase_callback()?
516 };
517 match locked.to_unlocked(&passphrase) {
518 Ok(o) => return Ok(o),
519 Err(e) => {
520 error!("Error: {}", e);
521 if i == 5 {
522 return Err(e);
523 }
524 }
525 }
526 i += 1;
527 }
528 }
529
530 pub fn change_locked_id_url(
534 &self,
535 id: &mut id::LockedId,
536 git_https_url: &str,
537 use_https_push: bool,
538 warnings: &mut Vec<Warning>,
539 ) -> Result<()> {
540 self.ensure_proofs_root_exists()?;
541
542 let old_proof_dir = self.local_proofs_repo_path_for_id(&id.to_public_id().id);
543 let new_url = Url::new_git(git_https_url.to_owned());
544 let new_proof_dir = self.get_proofs_dir_path_for_url(&new_url)?;
545 if old_proof_dir.exists() {
546 if !new_proof_dir.exists() {
547 fs::rename(&old_proof_dir, &new_proof_dir)?;
548 } else {
549 warn!(
550 "Abandoning old temporary repo in {}",
551 old_proof_dir.display()
552 );
553 }
554 }
555
556 self.clone_proof_dir_from_git(git_https_url, use_https_push, warnings)?;
557
558 id.url = Some(new_url);
559 self.save_locked_id(id)?;
560
561 let _ = self.proof_dir_commit("Setting up new CrevID URL");
563 let _ = self.run_git(
564 vec!["pull".into(), "--rebase".into(), "-Xours".into()],
565 warnings,
566 );
567 Ok(())
568 }
569
570 pub fn save_locked_id(&self, id: &id::LockedId) -> Result<()> {
572 let path = self.id_path(&id.to_public_id().id);
573 id.save_to(&path)
574 }
575
576 fn init_local_proofs_repo(&self, id: &Id, warnings: &mut Vec<Warning>) -> Result<()> {
577 self.ensure_proofs_root_exists()?;
578
579 let proof_dir = self.local_proofs_repo_path_for_id(id);
580 if proof_dir.exists() {
581 warn!(
582 "Proof directory `{}` already exists. Will not init.",
583 proof_dir.display()
584 );
585 return Ok(());
586 }
587 if let Err(e) = git2::Repository::init(&proof_dir) {
588 warn!("Can't init repo in {}: {}", proof_dir.display(), e);
589 self.run_git(
590 vec![
591 "init".into(),
592 "--initial-branch=master".into(),
593 proof_dir.into(),
594 ],
595 warnings,
596 )?;
597 }
598 Ok(())
599 }
600
601 pub fn clone_proof_dir_from_git(
605 &self,
606 git_https_url: &str,
607 use_https_push: bool,
608 warnings: &mut Vec<Warning>,
609 ) -> Result<()> {
610 debug_assert!(git_https_url.starts_with("https://"));
611 if git_https_url.starts_with("https://github.com/crev-dev/crev-proofs") {
612 return Err(Error::CouldNotCloneGitHttpsURL(Box::new((
613 git_https_url.into(),
614 "this is a template, fork it first".into(),
615 ))));
616 }
617
618 let proof_dir =
619 self.get_proofs_dir_path_for_url(&Url::new_git(git_https_url.to_owned()))?;
620
621 let push_url = if use_https_push {
622 git_https_url.to_string()
623 } else {
624 match util::git::https_to_git_url(git_https_url) {
625 Some(git_url) => git_url,
626 None => {
627 warnings.push(Warning::GitPushUrl(git_https_url.into()));
628 git_https_url.into()
629 }
630 }
631 };
632
633 if proof_dir.exists() {
634 info!("Using existing repository `{}`", proof_dir.display());
635 match git2::Repository::open(&proof_dir) {
636 Ok(repo) => {
637 repo.remote_set_url("origin", &push_url)?;
638 }
639 Err(_) => {
640 git2::Repository::init_opts(
641 &proof_dir,
642 git2::RepositoryInitOptions::new()
643 .no_reinit(true)
644 .origin_url(git_https_url),
645 )?;
646 }
647 }
648 return Ok(());
649 }
650
651 self.ensure_proofs_root_exists()?;
652
653 match util::git::clone(git_https_url, &proof_dir) {
654 Ok(repo) => {
655 debug!("{} cloned to {}", git_https_url, proof_dir.display());
656 repo.remote_set_url("origin", &push_url)?;
657 }
658 Err(e) => {
659 let error_string = e.to_string();
660 let is_auth_error = e.code() == git2::ErrorCode::Auth
662 || error_string.contains("remote authentication required");
663 return Err(Error::CouldNotCloneGitHttpsURL(Box::new((
664 git_https_url.to_string(),
665 if is_auth_error {
666 "Proof repositories must be publicly-readable without authentication, but this one isn't".into()
667 } else {
668 error_string
669 },
670 ))));
671 }
672 }
673
674 Ok(())
675 }
676
677 pub fn init_repo_readme_using_template(&self) -> Result<()> {
679 const README_MARKER_V0: &str = "CREV_README_MARKER_V0";
680
681 let proof_dir = self.get_proofs_dir_path()?;
682 let path = proof_dir.join("README.md");
683 if path.exists() {
684 if let Some(line) = std::io::BufReader::new(std::fs::File::open(&path)?)
685 .lines()
686 .find(|line| {
687 if let Ok(ref line) = line {
688 line.trim() != ""
689 } else {
690 true
691 }
692 })
693 {
694 if line?.contains(README_MARKER_V0) {
695 return Ok(());
696 }
697 }
698 }
699
700 std::fs::write(
701 proof_dir.join("README.md"),
702 &include_bytes!("../rc/doc/README.md")[..],
703 )?;
704 self.proof_dir_git_add_path(Path::new("README.md"))?;
705 Ok(())
706 }
707
708 fn get_proof_rel_store_path(&self, proof: &proof::Proof, host_salt: &[u8]) -> PathBuf {
710 crate::proof::rel_store_path(proof, host_salt)
711 }
712
713 fn get_cur_url(&self) -> Result<Url> {
715 let url = self.cur_url.lock().unwrap().clone();
716 if let Some(url) = url {
717 Ok(url)
718 } else if let Some(locked_id) = self.read_current_locked_id_opt()? {
719 *self.cur_url.lock().unwrap() = locked_id.url.clone();
720 locked_id.url.ok_or(Error::GitUrlNotConfigured)
721 } else {
722 Err(Error::CurrentIDNotSet)
723 }
724 }
725
726 fn ensure_proofs_root_exists(&self) -> Result<()> {
728 fs::create_dir_all(self.user_proofs_path())?;
729 Ok(())
730 }
731
732 fn local_proofs_repo_path_for_id(&self, id: &Id) -> PathBuf {
733 let Id::Crev { id } = id;
734 let dir_name = format!("local_only_{}", crev_common::base64_encode(&id));
735 let proofs_path = self.user_proofs_path();
736 proofs_path.join(dir_name)
737 }
738
739 fn local_proofs_repo_path(&self) -> Result<PathBuf> {
740 Ok(self.local_proofs_repo_path_for_id(&self.get_current_userid()?))
741 }
742
743 pub fn get_proofs_dir_path_for_url(&self, url: &Url) -> Result<PathBuf> {
745 let proofs_path = self.user_proofs_path();
746 let old_path = proofs_path.join(url.digest().to_string());
747 let new_path = proofs_path.join(sanitize_url_for_fs(&url.url));
748
749 if old_path.exists() {
750 std::fs::rename(&old_path, &new_path)?;
754 }
755
756 Ok(new_path)
757 }
758
759 pub fn get_proofs_dir_path(&self) -> Result<PathBuf> {
763 match self.get_cur_url() {
764 Ok(url) => self.get_proofs_dir_path_for_url(&url),
765 Err(Error::GitUrlNotConfigured) => self.local_proofs_repo_path(),
766 Err(err) => Err(err),
767 }
768 }
769
770 pub fn get_proofs_dir_path_opt(&self) -> Result<Option<PathBuf>> {
772 match self.get_proofs_dir_path() {
773 Ok(p) => Ok(Some(p)),
774 Err(Error::CurrentIDNotSet) => Ok(None),
775 Err(e) => Err(e),
776 }
777 }
778
779 pub fn build_trust_proof(
787 &self,
788 from_id: &PublicId,
789 ids: Vec<Id>,
790 trust_level: TrustLevel,
791 override_: Vec<OverrideItem>,
792 ) -> Result<proof::trust::Trust> {
793 if ids.is_empty() {
794 return Err(Error::NoIdsGiven);
795 }
796
797 let mut db = self.load_db()?;
798 let mut public_ids = Vec::with_capacity(ids.len());
799
800 for id in ids {
801 let url = match db.lookup_url(&id) {
802 crev_wot::UrlOfId::FromSelf(url) | crev_wot::UrlOfId::FromSelfVerified(url) => {
803 Some(url)
804 }
805 crev_wot::UrlOfId::FromOthers(maybe_url) => {
806 let maybe_url = maybe_url.url.clone();
807 let _ = self.fetch_url_into(&maybe_url, &mut db);
809 db.lookup_url(&id).from_self()
810 }
811 crev_wot::UrlOfId::None => None,
812 };
813 if let Some(url) = url {
814 public_ids.push(PublicId::new(id, url.clone()));
815 } else {
816 public_ids.push(PublicId::new_id_only(id));
817 }
818 }
819
820 Ok(from_id.create_trust_proof(&public_ids, trust_level, override_)?)
821 }
822
823 pub fn fetch_url(&self, url: &str) -> Result<()> {
825 let mut db = self.load_db()?;
826 self.fetch_url_into(url, &mut db)
827 }
828
829 pub fn fetch_url_into(&self, url: &str, db: &mut crev_wot::ProofDB) -> Result<()> {
831 info!("Fetching {}... ", url);
832 let dir = self.fetch_remote_git(url)?;
833 self.import_proof_dir_and_print_counts(&dir, url, db)?;
834
835 let mut db = crev_wot::ProofDB::new();
836 let url = Url::new_git(url);
837 let fetch_source = self.get_fetch_source_for_url(url.clone())?;
838 db.import_from_iter(proofs_iter_for_path(dir).map(move |p| (p, fetch_source.clone())));
839 info!("Found proofs from:");
840 for (id, count) in db.all_author_ids() {
841 let tmp;
842 let verified_state = match db.lookup_url(&id).from_self() {
843 Some(verified_url) if verified_url == &url => "verified owner",
844 Some(verified_url) => {
845 tmp = format!("copy from {}", verified_url.url);
846 &tmp
847 }
848 None => "copy from another repo",
849 };
850 info!("{:>8} {} ({})", count, id, verified_state);
851 }
852 Ok(())
853 }
854
855 pub fn trust_set_for_id(
856 &self,
857 for_id: Option<&str>,
858 params: &crev_wot::TrustDistanceParams,
859 db: &crev_wot::ProofDB,
860 ) -> Result<crev_wot::TrustSet> {
861 Ok(
862 if let Some(for_id) = self.get_for_id_from_str_opt(for_id)? {
863 db.calculate_trust_set(&for_id, params)
864 } else {
865 crev_wot::TrustSet::default()
867 },
868 )
869 }
870
871 pub fn fetch_new_trusted(
873 &self,
874 trust_params: crate::TrustDistanceParams,
875 for_id: Option<&str>,
876 warnings: &mut Vec<Warning>,
877 ) -> Result<()> {
878 let mut already_fetched_ids = HashSet::new();
879 let mut already_fetched_urls = remotes_checkouts_iter(self.cache_remotes_path())?
880 .map(|(_, url)| url.url)
881 .collect();
882 let mut db = self.load_db()?;
883 let for_id = self.get_for_id_from_str(for_id)?;
884
885 loop {
886 let trust_set = db.calculate_trust_set(&for_id, &trust_params);
887 let fetched_new = self.fetch_ids_not_fetched_yet(
888 trust_set.iter_trusted_ids().cloned(),
889 &mut already_fetched_ids,
890 &mut already_fetched_urls,
891 &mut db,
892 warnings,
893 );
894 if !fetched_new {
895 break;
896 }
897 }
898 Ok(())
899 }
900
901 pub fn fetch_trusted(
903 &self,
904 trust_params: crate::TrustDistanceParams,
905 for_id: Option<&str>,
906 warnings: &mut Vec<Warning>,
907 ) -> Result<()> {
908 let mut already_fetched_ids = HashSet::new();
909 let mut already_fetched_urls = HashSet::new();
910 let mut db = self.load_db()?;
911 let for_id = self.get_for_id_from_str(for_id)?;
912
913 loop {
914 let trust_set = db.calculate_trust_set(&for_id, &trust_params);
915 if !self.fetch_ids_not_fetched_yet(
916 trust_set.iter_trusted_ids().cloned(),
917 &mut already_fetched_ids,
918 &mut already_fetched_urls,
919 &mut db,
920 warnings,
921 ) {
922 break;
923 }
924 }
925 Ok(())
926 }
927
928 fn fetch_all_ids_recursively(
930 &self,
931 mut already_fetched_urls: HashSet<String>,
932 db: &mut crev_wot::ProofDB,
933 warnings: &mut Vec<Warning>,
934 ) -> Result<()> {
935 let mut already_fetched_ids = HashSet::new();
936
937 loop {
938 if !self.fetch_ids_not_fetched_yet(
939 db.all_known_ids().into_iter(),
940 &mut already_fetched_ids,
941 &mut already_fetched_urls,
942 db,
943 warnings,
944 ) {
945 break;
946 }
947 }
948 Ok(())
949 }
950
951 fn fetch_ids_not_fetched_yet(
953 &self,
954 ids: impl Iterator<Item = Id> + Send,
955 already_fetched_ids: &mut HashSet<Id>,
956 already_fetched_urls: &mut HashSet<String>,
957 db: &mut crev_wot::ProofDB,
958 warnings: &mut Vec<Warning>,
959 ) -> bool {
960 use std::sync::mpsc::channel;
961
962 let mut something_was_fetched = false;
963 let (tx, rx) = channel();
964 let pool = rayon::ThreadPoolBuilder::new()
965 .num_threads(8)
966 .build()
967 .unwrap();
968
969 pool.scope(|scope| {
970 for id in ids {
971 let tx = tx.clone();
972
973 if already_fetched_ids.contains(&id) {
974 continue;
975 }
976
977 if let Some(url) = db.lookup_url(&id).any_unverified() {
978 let url = &url.url;
979
980 if already_fetched_urls.contains(url) {
981 continue;
982 }
983 let url_clone = url.clone();
984 scope.spawn(move |_scope| {
985 tx.send((url_clone.clone(), self.fetch_remote_git(&url_clone)))
986 .expect("send to work");
987 });
988 already_fetched_urls.insert(url.clone());
989 } else {
990 warnings.push(Warning::IdUrlNotKnonw(id.clone()));
991 }
992 already_fetched_ids.insert(id);
993 }
994
995 drop(tx);
996
997 for (url, res) in rx {
998 let dir = match res {
999 Ok(dir) => dir,
1000 Err(e) => {
1001 error!("Error: Failed to get dir for repo {}: {}", url, e);
1002 continue;
1003 }
1004 };
1005 if let Err(e) = self.import_proof_dir_and_print_counts(&dir, &url, db) {
1006 warnings.push(Warning::FetchError(url, e, dir));
1007 continue;
1008 }
1009 something_was_fetched = true;
1010 }
1011 });
1012 something_was_fetched
1013 }
1014
1015 pub fn get_remote_git_cache_path(&self, url: &str) -> Result<PathBuf> {
1017 let digest = crev_common::blake2b256sum(url.as_bytes());
1018 let digest = crev_data::Digest::from(digest);
1019 let old_path = self.cache_remotes_path().join(digest.to_string());
1020 let new_path = self.cache_remotes_path().join(sanitize_url_for_fs(url));
1021
1022 if old_path.exists() {
1023 std::fs::rename(&old_path, &new_path)?;
1027 }
1028
1029 Ok(new_path)
1030 }
1031
1032 fn get_fetch_source_for_url(&self, url: Url) -> Result<crev_wot::FetchSource> {
1034 if let Ok(own_url) = self.get_cur_url() {
1035 if own_url == url {
1036 return Ok(crev_wot::FetchSource::LocalUser);
1037 }
1038 }
1039 Ok(crev_wot::FetchSource::Url(Arc::new(url)))
1040 }
1041
1042 pub fn fetch_remote_git(&self, url: &str) -> Result<PathBuf> {
1048 let dir = self.get_remote_git_cache_path(url)?;
1049
1050 let inner = || {
1051 if dir.exists() {
1052 let repo = git2::Repository::open(&dir)?;
1053 util::git::fetch_and_checkout_git_repo(&repo)
1054 } else {
1055 util::git::clone(url, &dir).map(drop)
1056 }
1057 };
1058 match inner() {
1059 Ok(()) => Ok(dir),
1060 Err(err) if is_unrecoverable(&err) => {
1061 debug!("Deleting {}, because {err}", dir.display());
1062 self.delete_remote_cache_directory(&dir);
1063 Err(err.into())
1064 }
1065 Err(err) => Err(err.into()),
1066 }
1067 }
1068
1069 pub fn import_proof_dir_and_print_counts(
1076 &self,
1077 dir: &Path,
1078 url: &str,
1079 db: &mut crev_wot::ProofDB,
1080 ) -> Result<()> {
1081 let prev_pkg_review_count = db.unique_package_review_proof_count();
1082 let prev_trust_count = db.unique_trust_proof_count();
1083
1084 let fetch_source = self.get_fetch_source_for_url(Url::new_git(url))?;
1085 db.import_from_iter(
1086 proofs_iter_for_path(dir.to_owned()).map(move |p| (p, fetch_source.clone())),
1087 );
1088
1089 let new_pkg_review_count = db.unique_package_review_proof_count() - prev_pkg_review_count;
1090 let new_trust_count = db.unique_trust_proof_count() - prev_trust_count;
1091
1092 let msg = match (new_trust_count > 0, new_pkg_review_count > 0) {
1093 (true, true) => {
1094 format!("new: {new_trust_count} trust, {new_pkg_review_count} package reviews")
1095 }
1096 (true, false) => format!("new: {new_trust_count} trust",),
1097 (false, true) => format!("new: {new_pkg_review_count} package reviews"),
1098 (false, false) => "no updates".into(),
1099 };
1100
1101 info!("{:<60} {}", url, msg);
1102 Ok(())
1103 }
1104
1105 pub fn fetch_all(&self, warnings: &mut Vec<Warning>) -> Result<()> {
1108 let mut fetched_urls = HashSet::new();
1109 let mut db = self.load_db()?;
1110
1111 let dpc_url = "https://github.com/dpc/crev-proofs";
1113 if let Ok(dir) = self
1114 .fetch_remote_git(dpc_url)
1115 .map_err(|e| warnings.push(e.into()))
1116 {
1117 let _ = self
1118 .import_proof_dir_and_print_counts(&dir, dpc_url, &mut db)
1119 .map_err(|e| warnings.push(e.into()));
1120 }
1121 fetched_urls.insert(dpc_url.to_owned());
1122
1123 for entry in fs::read_dir(self.cache_remotes_path())? {
1124 let path = entry?.path();
1125 if !path.is_dir() {
1126 continue;
1127 }
1128
1129 let url = match Self::url_for_repo_at_path(&path) {
1130 Ok(url) => url,
1131 Err(e) => {
1132 warnings.push(Warning::NoRepoUrlAtPath(path, e));
1133 continue;
1134 }
1135 };
1136
1137 let _ = self
1138 .get_fetch_source_for_url(Url::new_git(url))
1139 .map(|fetch_source| {
1140 db.import_from_iter(
1141 proofs_iter_for_path(path.clone()).map(move |p| (p, fetch_source.clone())),
1142 );
1143 })
1144 .map_err(|e| warnings.push(e.into()));
1145 }
1146
1147 self.fetch_all_ids_recursively(fetched_urls, &mut db, warnings)?;
1148
1149 Ok(())
1150 }
1151
1152 pub fn url_for_repo_at_path(repo: &Path) -> Result<String> {
1153 let repo = git2::Repository::open(repo)?;
1154 let remote = repo.find_remote("origin")?;
1155 let url = remote
1156 .url()
1157 .ok_or_else(|| Error::OriginHasNoURL(repo.path().into()))?;
1158 Ok(url.to_string())
1159 }
1160
1161 pub fn run_git(
1163 &self,
1164 args: Vec<OsString>,
1165 warnings: &mut Vec<Warning>,
1166 ) -> Result<std::process::ExitStatus> {
1167 let proof_dir_path = self.get_proofs_dir_path()?;
1168 let id = self.read_current_locked_id()?;
1169 if let Some(u) = id.url {
1170 if !proof_dir_path.exists() {
1171 self.clone_proof_dir_from_git(&u.url, false, warnings)?;
1172 }
1173 } else {
1174 return Err(Error::GitUrlNotConfigured);
1175 }
1176
1177 let status = std::process::Command::new("git")
1178 .args(args)
1179 .current_dir(proof_dir_path)
1180 .status()
1181 .expect("failed to execute git");
1182
1183 Ok(status)
1184 }
1185
1186 pub fn store_config_open_cmd(&self, cmd: String) -> Result<()> {
1188 let mut config = self.load_user_config()?;
1189 config.open_cmd = Some(cmd);
1190 self.store_user_config(&config)?;
1191 Ok(())
1192 }
1193
1194 pub fn proof_dir_git_add_path(&self, rel_path: &Path) -> Result<()> {
1196 let proof_dir = self.get_proofs_dir_path()?;
1197 let repo = git2::Repository::open(proof_dir)?;
1198 let mut index = repo.index()?;
1199
1200 index.add_path(rel_path)?;
1201 index.write()?;
1202 Ok(())
1203 }
1204
1205 pub fn proof_dir_commit(&self, commit_msg: &str) -> Result<()> {
1207 let proof_dir = self.get_proofs_dir_path()?;
1208 let repo = git2::Repository::open(proof_dir)?;
1209 let mut index = repo.index()?;
1210 let tree_id = index.write_tree()?;
1211 let tree = repo.find_tree(tree_id)?;
1212 let commit;
1213 let commit_ref;
1214 let parents: &[_] = if let Ok(head) = repo.head() {
1215 commit = head.peel_to_commit()?;
1216 commit_ref = &commit;
1217 std::slice::from_ref(&commit_ref)
1218 } else {
1219 &[]
1220 };
1221
1222 let signature = repo
1223 .signature()
1224 .or_else(|_| git2::Signature::now("unconfigured", "nobody@crev.dev"))?;
1225
1226 repo.commit(
1227 Some("HEAD"),
1228 &signature,
1229 &signature,
1230 commit_msg,
1231 &tree,
1232 parents,
1233 )?;
1234
1235 Ok(())
1236 }
1237
1238 pub fn show_current_id(&self) -> Result<()> {
1240 if let Some(id) = self.read_current_locked_id_opt()? {
1241 let id = id.to_public_id();
1242 println!("{} {}", id.id, id.url_display());
1243 }
1244 Ok(())
1245 }
1246
1247 pub fn generate_id(
1253 &self,
1254 url: Option<&str>,
1255 use_https_push: bool,
1256 read_new_passphrase: impl FnOnce() -> std::io::Result<String>,
1257 warnings: &mut Vec<Warning>,
1258 ) -> Result<id::LockedId> {
1259 if let Some(url) = url {
1260 self.clone_proof_dir_from_git(url, use_https_push, warnings)?;
1261 }
1262
1263 let unlocked_id = crev_data::id::UnlockedId::generate(url.map(crev_data::Url::new_git));
1264 let passphrase = read_new_passphrase()?;
1265 let locked_id = id::LockedId::from_unlocked_id(&unlocked_id, &passphrase)?;
1266
1267 if url.is_none() {
1268 self.init_local_proofs_repo(&unlocked_id.id.id, warnings)?;
1269 }
1270
1271 self.save_locked_id(&locked_id)?;
1272 self.save_current_id(unlocked_id.as_ref())?;
1273 self.init_repo_readme_using_template()?;
1274 Ok(locked_id)
1275 }
1276
1277 pub fn switch_id(&self, id_str: &str) -> Result<()> {
1279 let id: Id = Id::crevid_from_str(id_str)?;
1280 self.save_current_id(&id)?;
1281
1282 Ok(())
1283 }
1284
1285 pub fn export_locked_id(&self, id_str: Option<String>) -> Result<String> {
1287 let id = if let Some(id_str) = id_str {
1288 let id = Id::crevid_from_str(&id_str)?;
1289 self.read_locked_id(&id)?
1290 } else {
1291 self.read_current_locked_id()?
1292 };
1293
1294 Ok(id.to_string())
1295 }
1296
1297 pub fn import_locked_id(&self, locked_id_serialized: &str) -> Result<PublicId> {
1299 let id = LockedId::from_str(locked_id_serialized)?;
1300 self.save_locked_id(&id)?;
1301 Ok(id.to_public_id())
1302 }
1303
1304 fn all_local_proofs(&self) -> impl Iterator<Item = proof::Proof> {
1306 match self.user_proofs_path_opt() {
1307 Some(path) => {
1308 Box::new(proofs_iter_for_path(path)) as Box<dyn Iterator<Item = proof::Proof>>
1309 }
1310 None => Box::new(vec![].into_iter()),
1311 }
1312 }
1313
1314 #[rustfmt::skip]
1315 fn delete_remote_cache_directory(&self, path_to_delete: &Path) {
1316 let cache_dir = self.cache_remotes_path();
1317 assert!(path_to_delete.starts_with(cache_dir));
1318
1319 let file_name = path_to_delete.file_name().and_then(|f| f.to_str()).unwrap_or_default();
1321 let file_name = format!("{file_name}.deleting");
1322 let tmp_path = path_to_delete.with_file_name(file_name);
1323
1324 let path_to_delete = match std::fs::rename(path_to_delete, &tmp_path) {
1325 Ok(()) => &tmp_path,
1326 Err(_) => path_to_delete,
1327 };
1328 let _ = std::fs::remove_dir_all(path_to_delete);
1329 }
1330}
1331
1332impl ProofStore for Local {
1333 fn insert(&self, proof: &proof::Proof) -> Result<()> {
1334 let rel_store_path = self.get_proof_rel_store_path(
1335 proof,
1336 &self
1337 .user_config
1338 .lock()
1339 .unwrap()
1340 .as_ref()
1341 .expect("User config loaded")
1342 .host_salt,
1343 );
1344 let path = self.get_proofs_dir_path()?.join(&rel_store_path);
1345
1346 fs::create_dir_all(path.parent().expect("Not a root dir"))?;
1347 let mut file = fs::OpenOptions::new()
1348 .append(true)
1349 .create(true)
1350 .write(true)
1351 .open(path)?;
1352
1353 file.write_all(proof.to_string().as_bytes())?;
1354 file.write_all(b"\n")?;
1355 file.flush()?;
1356 drop(file);
1357
1358 self.proof_dir_git_add_path(&rel_store_path)?;
1359
1360 Ok(())
1361 }
1362
1363 fn proofs_iter(&self) -> Result<Box<dyn Iterator<Item = proof::Proof>>> {
1364 Ok(Box::new(self.all_local_proofs()))
1365 }
1366}
1367
1368fn remotes_checkouts_iter(path: PathBuf) -> Result<impl Iterator<Item = (PathBuf, Url)>> {
1370 let dir = std::fs::read_dir(path)?;
1371 Ok(dir
1372 .filter_map(|e| e.ok())
1373 .filter_map(|e| {
1374 let ty = e.file_type().ok()?;
1375 if ty.is_dir() {
1376 Some(e.path())
1377 } else {
1378 None
1379 }
1380 })
1381 .filter_map(move |path| {
1382 let repo = git2::Repository::open(&path).ok()?;
1383 let origin = repo.find_remote("origin").ok()?;
1384 let url = Url::new_git(origin.url()?);
1385 Some((path, url))
1386 }))
1387}
1388
1389fn proofs_iter_for_remotes_checkouts(
1391 path: PathBuf,
1392) -> Result<impl Iterator<Item = (proof::Proof, crev_wot::FetchSource)>> {
1393 Ok(remotes_checkouts_iter(path)?.flat_map(|(path, url)| {
1394 let fetch_source = crev_wot::FetchSource::Url(Arc::new(url));
1395 proofs_iter_for_path(path).map(move |p| (p, fetch_source.clone()))
1396 }))
1397}
1398
1399fn proofs_iter_for_path(path: PathBuf) -> impl Iterator<Item = proof::Proof> {
1401 use std::ffi::OsStr;
1402 let file_iter = walkdir::WalkDir::new(&path)
1403 .into_iter()
1404 .filter_entry(|e| e.file_name().to_str().map_or(true, |f| !f.starts_with('.')))
1406 .map_err(move |e| {
1407 Error::ErrorIteratingLocalProofStore(Box::new((path.clone(), e.to_string())))
1408 })
1409 .filter_map_ok(|entry| {
1410 let path = entry.path();
1411 if !path.is_file() {
1412 return None;
1413 }
1414
1415 let osext_match: &OsStr = "crev".as_ref();
1416 match path.extension() {
1417 Some(osext) if osext == osext_match => Some(path.to_owned()),
1418 _ => None,
1419 }
1420 });
1421
1422 fn parse_proofs(path: &Path) -> Result<Vec<proof::Proof>> {
1423 let mut file = BufReader::new(std::fs::File::open(path)?);
1424 Ok(proof::Proof::parse_from(&mut file)?)
1425 }
1426
1427 file_iter
1428 .filter_map(|maybe_path| {
1429 maybe_path
1430 .map_err(|e| error!("Failed scanning for proofs: {}", e))
1431 .ok()
1432 })
1433 .filter_map(|path| match parse_proofs(&path) {
1434 Ok(proofs) => Some(proofs.into_iter().filter_map(move |proof| {
1435 proof
1436 .verify()
1437 .map_err(|e| {
1438 error!(
1439 "Verification failed for proof signed '{}' in {}: {} ",
1440 proof.signature(),
1441 path.display(),
1442 e
1443 );
1444 })
1445 .ok()
1446 .map(|()| proof)
1447 })),
1448 Err(e) => {
1449 error!("Error parsing proofs in {}: {}", path.display(), e);
1450 None
1451 }
1452 })
1453 .flatten()
1454}
1455
1456#[test]
1457fn local_is_send_sync() {
1458 fn is<T: Send + Sync>() {}
1459 is::<Local>();
1460}