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