crev_lib/
local.rs

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
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 {
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
100/// Local config stored in `~/.config/crev`
101///
102/// This managed IDs, local proof repository, etc.
103pub 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    /// Load config from the environment
113    #[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    /// Load all reviews and trust proofs for the current user
133    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    /// Where the config is stored
149    pub fn config_root(&self) -> &Path {
150        &self.config_path
151    }
152
153    /// Where the data is stored
154    pub fn data_root(&self) -> &Path {
155        &self.data_path
156    }
157
158    /// Where temporary files are stored
159    pub fn cache_root(&self) -> &Path {
160        &self.cache_path
161    }
162
163    /// Fails if it doesn't exist. See `auto_create_or_open()`
164    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        // Before early 2022, proofs were in the config dir instead of the data dir.
173        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    /// Fails if it already exists. See `auto_create_or_open()`
184    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    /// Load the database from disk, or create one if needed.
201    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    /// Load config, and return Id configured as the current one
212    pub fn read_current_id(&self) -> Result<crev_data::Id> {
213        Ok(self.load_user_config()?.get_current_userid()?.clone())
214    }
215
216    /// Load config, and return Id configured as the current one
217    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    /// Calculate `for_id` that is used in a lot of operations
222    ///
223    /// * if `id_str` is given and parses correctly - convert to Id.
224    /// * otherwise return current id
225    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    /// Load config, update which Id is the current one, and save.
238    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        // Change the old, backfilled `host_salt` the first time
249        // the id is being switched
250        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    /// Same as `get_root_path`()
259    pub fn user_dir_path(&self) -> PathBuf {
260        self.config_path.clone()
261    }
262
263    /// Directory where yaml files for user identities are stored
264    pub fn user_ids_path(&self) -> PathBuf {
265        self.user_dir_path().join("ids")
266    }
267
268    /// Like [`Self::user_ids_path`] but checks if the dir exists
269    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    /// Directory where git checkouts for user's own proof repos are stored
276    ///
277    /// This is separate from cache of other people's proofs
278    pub fn user_proofs_path(&self) -> PathBuf {
279        self.data_path.join("proofs")
280    }
281
282    /// Like `user_proofs_path` but checks if the dir exists
283    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    /// Path where this Id is stored as YAML
290    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    /// Returns public Ids which belong to the current user.
299    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    /// Path to crev's config file
315    fn user_config_path(&self) -> PathBuf {
316        self.user_dir_path().join("config.yaml")
317    }
318
319    /// Path where git checkouts of other people's proof repos are stored
320    pub fn cache_remotes_path(&self) -> PathBuf {
321        self.cache_path.join("remotes")
322    }
323
324    /// Cache where metadata about in-progress reviews (etc) is stored
325    fn cache_activity_path(&self) -> PathBuf {
326        self.cache_path.join("activity")
327    }
328
329    /// Path where to put copies of crates' source code
330    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    /// Copy crate for review, neutralizing hidden or dangerous files
343    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    /// Yaml file path for in-progress review metadata
363    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    /// Most recent in-progress review
382    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    /// Save activity (in-progress review) to disk
388    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    /// Load activity (in-progress review) from disk
412    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    /// Just returns the config, doesn't change anything
431    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    /// Writes the config to disk AND sets it as the current one
441    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    /// Id in the config
453    pub fn get_current_userid(&self) -> Result<Id> {
454        self.get_current_userid_opt()?.ok_or(Error::CurrentIDNotSet)
455    }
456
457    /// Id in the config
458    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    /// Just reads the yaml file, doesn't change any state
464    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    /// Just reads the yaml file, doesn't change any state
470    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(&current_id))
473            .transpose()
474    }
475
476    /// Just reads the yaml file, doesn't change any state
477    pub fn read_current_locked_id(&self) -> Result<LockedId> {
478        self.read_current_locked_id_opt()?
479            .ok_or(Error::CurrentIDNotSet)
480    }
481
482    /// Just reads the yaml file and unlocks it, doesn't change any state
483    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(&current_id, passphrase_callback))
489            .transpose()
490    }
491
492    /// Just reads the yaml file and unlocks it, doesn't change anything
493    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    /// Just reads the yaml file and unlocks it, doesn't change anything
502    ///
503    /// Asks for passphrase up to 5 times
504    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    /// Changes the repo URL for the ID. Adopts existing temporary/local repo if any.
531    /// Previous remote URL is abandoned.
532    /// For crev id set-url command.
533    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        // commit uncommitted changes, if there are any. Otherwise the next pull may fail
562        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    /// Writes the Id to disk, doesn't change any state
571    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    /// Git clone or init new remote Github crev-proof repo for the current user.
602    ///
603    /// Saves to `user_proofs_path`, so it's trusted as user's own proof repo.
604    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                // git2 seems to have a bug, and auth error is reported as GenericError
661                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    /// Inits repo in `get_proofs_dir_path()`
678    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    // Get path relative to `get_proofs_dir_path` to store the `proof`
709    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    /// Proof repo URL associated with the current user Id
714    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    /// Creates `user_proofs_path()`
727    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    /// Dir unique to this URL, inside `user_proofs_path()`
744    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            // we used to use less human-friendly path format; move directories
751            // from old to new path
752            // TODO: get rid of this in some point in the future
753            std::fs::rename(&old_path, &new_path)?;
754        }
755
756        Ok(new_path)
757    }
758
759    /// Path where the `proofs` are stored under `git` repository.
760    ///
761    /// This function derives path from current user's URL
762    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    /// This function derives path from current user's URL
771    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    /// Creates new unsigned trust proof object, not edited
780    ///
781    /// Ensures the proof contains valid URLs for Ids where possible.
782    ///
783    /// Currently ignores previous proofs
784    ///
785    /// See `trust.sign_by(ownid)`
786    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                    // Ignore errors - if we weren't able to fetch it, that's OK.
808                    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    /// Fetch other people's proof repository from a git URL, into the current database on disk
824    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    /// Fetch other people's proof repository from a git URL, directly into the given db (and disk too)
830    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                // when running without an id (explicit, or current), just use an empty trust set
866                crev_wot::TrustSet::default()
867            },
868        )
869    }
870
871    /// Fetch only repos that weren't fetched before
872    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    /// Fetch proof repo URLs of trusted Ids
902    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    /// Fetch (and discover) proof repo URLs of all known Ids
929    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    /// True if something was fetched
952    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    /// Per-url directory in `cache_remotes_path()`
1016    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            // we used to use less human-friendly path format; move directories
1024            // from old to new path
1025            // TODO: get rid of this in some point in the future
1026            std::fs::rename(&old_path, &new_path)?;
1027        }
1028
1029        Ok(new_path)
1030    }
1031
1032    /// `LocalUser` if it's current user's URL, or `crev_wot::FetchSource` for the URL.
1033    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    /// Fetch a git proof repository
1043    ///
1044    /// Returns url where it was cloned/fetched
1045    ///
1046    /// Adds the repo to the local proof repo cache.
1047    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    /// Fetches and imports to the given db
1070    ///
1071    /// Same as `fetch_url_into`, but with more stats
1072    ///
1073    /// dir - where the proofs were downloaded to
1074    /// url - url from which it was fetched
1075    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    /// Fetch and discover proof repos. Like `fetch_all_ids_recursively`,
1106    /// but adds `https://github.com/dpc/crev-proofs` and repos in cache that didn't belong to any Ids.
1107    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        // Temporarily hardcode `dpc`'s proof-repo url
1112        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    /// Run arbitrary git command in `get_proofs_dir_path()`
1162    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    /// set `open_cmd` in the config
1187    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    /// The path must be inside `get_proofs_dir_path()`
1195    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    /// Add a commit to user's proof repo
1206    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    /// Prints `read_current_locked_id`
1239    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    /// Generate a new identity in the local config.
1248    ///
1249    /// It's OK if the URL contains other identities. A new one will be added.
1250    ///
1251    /// The callback should provide a passphrase
1252    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    /// Set given Id as the current one
1278    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    /// See `read_locked_id`
1286    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    /// Parse `LockedId`'s YAML and write it to disk. See `save_locked_id`
1298    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    /// All proofs from all local repos, regardless of current user's URL
1305    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        // Try to be atomic by renaming the directory first (so that it won't leave half-deleted dir if the command is interrupted)
1320        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
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() {
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
1389/// Scan a directory of git checkouts. Assumes fetch source is the origin URL.
1390fn 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
1399/// Scan a git checkout or any subdirectory obtained from a known URL
1400fn 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        // skip dotfiles, .git dir
1405        .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}