Skip to main content

crev_lib/
local.rs

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
33/// Random 32 bytes
34fn generete_salt() -> Vec<u8> {
35    crev_common::rand::random_vec(32)
36}
37
38/// Backfill the host salt
39///
40/// For people that have configs generated when
41/// `host_salt` was not a thing - generate some
42/// form of stable id
43///
44/// TODO: at some point this should no longer be necessary
45fn 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
96/// Local config stored in `~/.config/crev`
97///
98/// This managed IDs, local proof repository, etc.
99pub 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    /// Load config from the environment
109    #[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    /// Load all reviews and trust proofs for the current user
129    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    /// Where the config is stored
145    pub fn config_root(&self) -> &Path {
146        &self.config_path
147    }
148
149    /// Where the data is stored
150    pub fn data_root(&self) -> &Path {
151        &self.data_path
152    }
153
154    /// Where temporary files are stored
155    pub fn cache_root(&self) -> &Path {
156        &self.cache_path
157    }
158
159    /// Fails if it doesn't exist. See `auto_create_or_open()`
160    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        // Before early 2022, proofs were in the config dir instead of the data dir.
169        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    /// Fails if it already exists. See `auto_create_or_open()`
180    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    /// Load the database from disk, or create one if needed.
197    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    /// Load config, and return Id configured as the current one
208    pub fn read_current_id(&self) -> Result<crev_data::Id> {
209        Ok(self.load_user_config()?.get_current_userid()?.clone())
210    }
211
212    /// Load config, and return Id configured as the current one
213    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    /// Calculate `for_id` that is used in a lot of operations
218    ///
219    /// * if `id_str` is given and parses correctly - convert to Id.
220    /// * otherwise return current id
221    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    /// Load config, update which Id is the current one, and save.
234    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        // Change the old, backfilled `host_salt` the first time
245        // the id is being switched
246        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    /// Same as `get_root_path`()
255    pub fn user_dir_path(&self) -> PathBuf {
256        self.config_path.clone()
257    }
258
259    /// Directory where yaml files for user identities are stored
260    pub fn user_ids_path(&self) -> PathBuf {
261        self.user_dir_path().join("ids")
262    }
263
264    /// Like [`Self::user_ids_path`] but checks if the dir exists
265    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    /// Directory where git checkouts for user's own proof repos are stored
272    ///
273    /// This is separate from cache of other people's proofs
274    pub fn user_proofs_path(&self) -> PathBuf {
275        self.data_path.join("proofs")
276    }
277
278    /// Like `user_proofs_path` but checks if the dir exists
279    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    /// Path where this Id is stored as YAML
286    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    /// Returns public Ids which belong to the current user.
295    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    /// Path to crev's config file
311    fn user_config_path(&self) -> PathBuf {
312        self.user_dir_path().join("config.yaml")
313    }
314
315    /// Path where git checkouts of other people's proof repos are stored
316    pub fn cache_remotes_path(&self) -> PathBuf {
317        self.cache_path.join("remotes")
318    }
319
320    /// Cache where metadata about in-progress reviews (etc) is stored
321    fn cache_activity_path(&self) -> PathBuf {
322        self.cache_path.join("activity")
323    }
324
325    /// Path where to put copies of crates' source code
326    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    /// Copy crate for review, neutralizing hidden or dangerous files
339    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    /// Yaml file path for in-progress review metadata
362    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    /// Most recent in-progress review
381    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    /// Save activity (in-progress review) to disk
387    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    /// Load activity (in-progress review) from disk
415    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    /// Just returns the config, doesn't change anything
434    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    /// Writes the config to disk AND sets it as the current one
444    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    /// Id in the config
456    pub fn get_current_userid(&self) -> Result<Id> {
457        self.get_current_userid_opt()?.ok_or(Error::CurrentIDNotSet)
458    }
459
460    /// Id in the config
461    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    /// Just reads the yaml file, doesn't change any state
467    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    /// Just reads the yaml file, doesn't change any state
473    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(&current_id))
476            .transpose()
477    }
478
479    /// Just reads the yaml file, doesn't change any state
480    pub fn read_current_locked_id(&self) -> Result<LockedId> {
481        self.read_current_locked_id_opt()?
482            .ok_or(Error::CurrentIDNotSet)
483    }
484
485    /// Just reads the yaml file and unlocks it, doesn't change any state
486    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(&current_id, passphrase_callback))
492            .transpose()
493    }
494
495    /// Just reads the yaml file and unlocks it, doesn't change anything
496    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    /// Just reads the yaml file and unlocks it, doesn't change anything
505    ///
506    /// Asks for passphrase up to 5 times
507    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    /// Changes the repo URL for the ID. Adopts existing temporary/local repo if any.
534    /// Previous remote URL is abandoned.
535    /// For crev id set-url command.
536    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        // commit uncommitted changes, if there are any. Otherwise the next pull may fail
565        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    /// Writes the Id to disk, doesn't change any state
574    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    /// Git clone or init new remote Github crev-proof repo for the current user.
605    ///
606    /// Saves to `user_proofs_path`, so it's trusted as user's own proof repo.
607    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                // git2 seems to have a bug, and auth error is reported as GenericError
664                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    /// Inits repo in `get_proofs_dir_path()`
681    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    // Get path relative to `get_proofs_dir_path` to store the `proof`
710    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    /// Proof repo URL associated with the current user Id
715    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    /// Creates `user_proofs_path()`
728    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    /// Dir unique to this URL, inside `user_proofs_path()`
745    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            // we used to use less human-friendly path format; move directories
752            // from old to new path
753            // TODO: get rid of this in some point in the future
754            std::fs::rename(&old_path, &new_path)?;
755        }
756
757        Ok(new_path)
758    }
759
760    /// Path where the `proofs` are stored under `git` repository.
761    ///
762    /// This function derives path from current user's URL
763    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    /// This function derives path from current user's URL
772    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    /// Creates new unsigned trust proof object, not edited
781    ///
782    /// Ensures the proof contains valid URLs for Ids where possible.
783    ///
784    /// Currently ignores previous proofs
785    ///
786    /// See `trust.sign_by(ownid)`
787    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                    // Ignore errors - if we weren't able to fetch it, that's OK.
809                    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    /// Fetch other people's proof repository from a git URL, into the current database on disk
825    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    /// Fetch other people's proof repository from a git URL, directly into the given db (and disk too)
831    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                // when running without an id (explicit, or current), just use an empty trust set
867                crev_wot::TrustSet::default()
868            },
869        )
870    }
871
872    /// Fetch only repos that weren't fetched before
873    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    /// Fetch proof repo URLs of trusted Ids
903    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    /// Fetch (and discover) proof repo URLs of all known Ids
930    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    /// True if something was fetched
953    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    /// Per-url directory in `cache_remotes_path()`
1017    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            // we used to use less human-friendly path format; move directories
1025            // from old to new path
1026            // TODO: get rid of this in some point in the future
1027            std::fs::rename(&old_path, &new_path)?;
1028        }
1029
1030        Ok(new_path)
1031    }
1032
1033    /// `LocalUser` if it's current user's URL, or `crev_wot::FetchSource` for the URL.
1034    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    /// Fetch a git proof repository
1044    ///
1045    /// Returns url where it was cloned/fetched
1046    ///
1047    /// Adds the repo to the local proof repo cache.
1048    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    /// Fetches and imports to the given db
1071    ///
1072    /// Same as `fetch_url_into`, but with more stats
1073    ///
1074    /// dir - where the proofs were downloaded to
1075    /// url - url from which it was fetched
1076    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    /// Fetch and discover proof repos. Like `fetch_all_ids_recursively`,
1107    /// but adds `https://github.com/dpc/crev-proofs` and repos in cache that didn't belong to any Ids.
1108    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        // Temporarily hardcode `dpc`'s proof-repo url
1113        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    /// Run arbitrary git command in `get_proofs_dir_path()`
1163    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    /// set `open_cmd` in the config
1188    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    /// The path must be inside `get_proofs_dir_path()`
1196    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    /// Add a commit to user's proof repo
1207    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    /// Prints `read_current_locked_id`
1240    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    /// Generate a new identity in the local config.
1249    ///
1250    /// It's OK if the URL contains other identities. A new one will be added.
1251    ///
1252    /// The callback should provide a passphrase
1253    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    /// Set given Id as the current one
1279    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    /// See `read_locked_id`
1287    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    /// Parse `LockedId`'s YAML and write it to disk. See `save_locked_id`
1299    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    /// All proofs from all local repos, regardless of current user's URL
1306    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        // Try to be atomic by renaming the directory first (so that it won't leave half-deleted dir if the command is interrupted)
1321        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
1368/// Scans cache for checked out repos and their origin urls
1369fn 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
1385/// Scan a directory of git checkouts. Assumes fetch source is the origin URL.
1386fn 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
1395/// Scan a git checkout or any subdirectory obtained from a known URL
1396fn 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        // skip dotfiles, .git dir
1401        .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}