-
Notifications
You must be signed in to change notification settings - Fork 41
/
main.rs
2088 lines (1859 loc) · 70.7 KB
/
main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
use std::collections::HashMap;
use std::io::{BufRead, BufReader};
use std::ops::Deref;
use std::panic::panic_any;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{fs::File, io, panic, path::PathBuf};
use cargo_metadata::{Metadata, Package, Version};
use clap::{CommandFactory, Parser};
use console::Term;
use errors::{
AggregateCriteriaDescription, AggregateCriteriaDescriptionMismatchError,
AggregateCriteriaImplies, AggregateError, AggregateErrors, AggregateImpliesMismatchError,
AuditAsError, AuditAsErrors, CacheAcquireError, CertifyError, FetchAuditError, LoadTomlError,
NeedsAuditAsError, NeedsAuditAsErrors, ShouldntBeAuditAsError, ShouldntBeAuditAsErrors,
TomlParseError, UserInfoError,
};
use format::{CriteriaName, CriteriaStr, PackageName, PolicyEntry};
use futures_util::future::{join_all, try_join_all};
use indicatif::ProgressDrawTarget;
use lazy_static::lazy_static;
use miette::{miette, Context, Diagnostic, IntoDiagnostic, NamedSource, SourceOffset};
use network::Network;
use out::{progress_bar, IncProgressOnDrop};
use reqwest::Url;
use serde::de::Deserialize;
use serialization::spanned::Spanned;
use thiserror::Error;
use tracing::{error, info, trace, warn};
use crate::cli::*;
use crate::errors::{CommandError, DownloadError, RegenerateExemptionsError};
use crate::format::{
AuditEntry, AuditKind, AuditsFile, ConfigFile, CriteriaEntry, DependencyCriteria,
ExemptedDependency, FetchCommand, ImportsFile, MetaConfig, MetaConfigInstance, PackageStr,
SortedMap, StoreInfo,
};
use crate::out::{indeterminate_spinner, Out, StderrLogWriter, MULTIPROGRESS};
use crate::resolver::{CriteriaMapper, CriteriaNamespace, DepGraph, ResolveDepth};
use crate::storage::{Cache, Store};
mod cli;
mod editor;
pub mod errors;
mod flock;
pub mod format;
pub mod network;
mod out;
pub mod resolver;
mod serialization;
pub mod storage;
#[cfg(test)]
mod tests;
/// Absolutely All The Global Configurations
pub struct Config {
/// Cargo.toml `metadata.vet`
pub metacfg: MetaConfig,
/// `cargo metadata`
pub metadata: Metadata,
/// Freestanding configuration values
_rest: PartialConfig,
}
/// Configuration vars that are available in a free-standing situation
/// (no actual cargo-vet instance to load/query).
pub struct PartialConfig {
/// Details of the CLI invocation (args)
pub cli: Cli,
/// Path to the cache directory we're using
pub cache_dir: PathBuf,
/// Whether we should mock the global cache (for unit testing)
pub mock_cache: bool,
}
// Makes it a bit easier to have both a "partial" and "full" config
impl Deref for Config {
type Target = PartialConfig;
fn deref(&self) -> &Self::Target {
&self._rest
}
}
pub trait PackageExt {
fn is_third_party(&self, policy: &SortedMap<PackageName, PolicyEntry>) -> bool;
}
impl PackageExt for Package {
fn is_third_party(&self, policy: &SortedMap<PackageName, PolicyEntry>) -> bool {
let forced_third_party = policy
.get(&self.name)
.and_then(|policy| policy.audit_as_crates_io)
.unwrap_or(false);
let is_crates_io = self
.source
.as_ref()
.map(|s| s.is_crates_io())
.unwrap_or(false);
forced_third_party || is_crates_io
}
}
const CACHE_DIR_SUFFIX: &str = "cargo-vet";
const CARGO_ENV: &str = "CARGO";
// package.metadata.vet
const PACKAGE_VET_CONFIG: &str = "vet";
// workspace.metadata.vet
const WORKSPACE_VET_CONFIG: &str = "vet";
const DURATION_DAY: Duration = Duration::from_secs(60 * 60 * 24);
/// Trick to let us std::process::exit while still cleaning up
/// by panicking with this type instead of a string.
struct ExitPanic(i32);
type ReportErrorFunc = dyn Fn(&miette::Report) + Send + Sync + 'static;
// XXX: We might be able to get rid of this `lazy_static` after 1.63 due to
// `const Mutex::new` being stabilized.
lazy_static! {
static ref REPORT_ERROR: Mutex<Option<Box<ReportErrorFunc>>> = Mutex::new(None);
}
fn set_report_errors_as_json(out: Arc<dyn Out>) {
*REPORT_ERROR.lock().unwrap() = Some(Box::new(move |error| {
// Manually invoke JSONReportHandler to format the error as a report
// to out_.
let mut report = String::new();
miette::JSONReportHandler::new()
.render_report(&mut report, error.as_ref())
.unwrap();
writeln!(out, r#"{{"error": {}}}"#, report);
}));
}
fn report_error(error: &miette::Report) {
{
let guard = REPORT_ERROR.lock().unwrap();
if let Some(do_report) = &*guard {
do_report(error);
return;
}
}
error!("{:?}", error);
}
fn main() -> Result<(), ()> {
// NOTE: Limit the maximum number of blocking threads to 128, rather than
// the default of 512.
// This may limit concurrency in some cases, but cargo-vet isn't running a
// server, and should avoid consuming all available resources.
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.max_blocking_threads(128)
.enable_all()
.build()
.unwrap();
let _guard = runtime.enter();
// Wrap main up in a catch_panic so that we can use it to implement std::process::exit with
// unwinding, allowing us to silently exit the program while still cleaning up.
let panic_result = std::panic::catch_unwind(real_main);
let main_result = match panic_result {
Ok(main_result) => main_result,
Err(e) => {
if let Some(ExitPanic(code)) = e.downcast_ref::<ExitPanic>() {
// Exit panic, just silently exit with this status
std::process::exit(*code);
} else {
// Normal panic, let it ride
std::panic::resume_unwind(e);
}
}
};
main_result.map_err(|e| {
report_error(&e);
std::process::exit(-1);
})
}
fn real_main() -> Result<(), miette::Report> {
use cli::Commands::*;
let fake_cli = cli::FakeCli::parse();
let cli::FakeCli::Vet(cli) = fake_cli;
//////////////////////////////////////////////////////
// Setup logging / output
//////////////////////////////////////////////////////
// Init the logger (and make trace logging less noisy)
if let Some(log_path) = &cli.log_file {
let log_file = File::create(log_path).unwrap();
tracing_subscriber::fmt::fmt()
.with_max_level(cli.verbose)
.with_target(false)
.without_time()
.with_ansi(false)
.with_writer(log_file)
.init();
} else {
tracing_subscriber::fmt::fmt()
.with_max_level(cli.verbose)
.with_target(false)
.without_time()
.with_ansi(console::colors_enabled_stderr())
.with_writer(StderrLogWriter::new)
.init();
}
// Control how errors are formatted by setting the miette hook. This will
// only be used for errors presented to humans, when formatting an error as
// JSON, it will be handled by a custom `report_error` override, bypassing
// the hook.
let using_log_file = cli.log_file.is_some();
miette::set_hook(Box::new(move |_| {
let graphical_theme = if console::colors_enabled_stderr() && !using_log_file {
miette::GraphicalTheme::unicode()
} else {
miette::GraphicalTheme::unicode_nocolor()
};
Box::new(
miette::MietteHandlerOpts::new()
.graphical_theme(graphical_theme)
.build(),
)
}))
.expect("failed to initialize error handler");
// Now that miette is set up, use it to format panics.
panic::set_hook(Box::new(move |panic_info| {
if panic_info.payload().is::<ExitPanic>() {
return;
}
let payload = panic_info.payload();
let message = if let Some(msg) = payload.downcast_ref::<&str>() {
msg
} else if let Some(msg) = payload.downcast_ref::<String>() {
&msg[..]
} else {
"something went wrong"
};
#[derive(Debug, Error, Diagnostic)]
#[error("{message}")]
pub struct PanicError {
pub message: String,
#[help]
pub help: Option<String>,
}
report_error(
&miette::Report::from(PanicError {
message: message.to_owned(),
help: panic_info
.location()
.map(|loc| format!("at {}:{}:{}", loc.file(), loc.line(), loc.column())),
})
.wrap_err("cargo vet panicked"),
);
}));
// Initialize the MULTIPROGRESS's draw target, so that future progress
// events are rendered to stderr.
MULTIPROGRESS.set_draw_target(ProgressDrawTarget::stderr());
// Setup our output stream
let out: Arc<dyn Out> = if let Some(output_path) = &cli.output_file {
Arc::new(File::create(output_path).unwrap())
} else {
Arc::new(Term::stdout())
};
// If we're outputting JSON, replace the error report method such that it
// writes errors out to the normal output stream as JSON.
if cli.output_format == OutputFormat::Json {
set_report_errors_as_json(out.clone());
}
////////////////////////////////////////////////////
// Potentially handle freestanding commands
////////////////////////////////////////////////////
// TODO: make this configurable
let cache_dir = dirs::cache_dir()
.unwrap_or_else(std::env::temp_dir)
.join(CACHE_DIR_SUFFIX);
let partial_cfg = PartialConfig {
cli,
cache_dir,
mock_cache: false,
};
match &partial_cfg.cli.command {
Some(Aggregate(sub_args)) => return cmd_aggregate(&out, &partial_cfg, sub_args),
Some(HelpMarkdown(sub_args)) => return cmd_help_md(&out, &partial_cfg, sub_args),
Some(Gc(sub_args)) => return cmd_gc(&out, &partial_cfg, sub_args),
_ => {
// Not a freestanding command, time to do full parsing and setup
}
}
///////////////////////////////////////////////////
// Fetch cargo metadata
///////////////////////////////////////////////////
let cli = &partial_cfg.cli;
let cargo_path = std::env::var_os(CARGO_ENV).expect("Cargo failed to set $CARGO, how?");
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.cargo_path(cargo_path);
if let Some(manifest_path) = &cli.manifest_path {
cmd.manifest_path(manifest_path);
}
if !cli.no_all_features {
cmd.features(cargo_metadata::CargoOpt::AllFeatures);
}
if cli.no_default_features {
cmd.features(cargo_metadata::CargoOpt::NoDefaultFeatures);
}
if !cli.features.is_empty() {
cmd.features(cargo_metadata::CargoOpt::SomeFeatures(cli.features.clone()));
}
// We never want cargo-vet to update the Cargo.lock.
// For frozen runs we also don't want to touch the network.
let mut other_options = Vec::new();
if cli.frozen {
other_options.push("--frozen".to_string());
} else {
other_options.push("--locked".to_string());
}
cmd.other_options(other_options);
info!("Running: {:#?}", cmd.cargo_command());
// ERRORS: immediate fatal diagnostic
let metadata = {
let _spinner = indeterminate_spinner("Running", "`cargo metadata`");
cmd.exec()
.into_diagnostic()
.wrap_err("'cargo metadata' exited unsuccessfully")?
};
// trace!("Got Metadata! {:#?}", metadata);
trace!("Got Metadata!");
//////////////////////////////////////////////////////
// Parse out our own configuration
//////////////////////////////////////////////////////
let default_config = MetaConfigInstance {
version: Some(1),
store: Some(StoreInfo {
path: Some(
metadata
.workspace_root
.join(storage::DEFAULT_STORE)
.into_std_path_buf(),
),
}),
};
// FIXME: what is `store.path` relative to here?
let workspace_metacfg = metadata
.workspace_metadata
.get(WORKSPACE_VET_CONFIG)
.map(|cfg| {
// ERRORS: immediate fatal diagnostic
MetaConfigInstance::deserialize(cfg)
.into_diagnostic()
.wrap_err("Workspace had [{WORKSPACE_VET_CONFIG}] but it was malformed")
})
.transpose()?;
// FIXME: what is `store.path` relative to here?
let package_metacfg = metadata
.root_package()
.and_then(|r| r.metadata.get(PACKAGE_VET_CONFIG))
.map(|cfg| {
// ERRORS: immediate fatal diagnostic
MetaConfigInstance::deserialize(cfg)
.into_diagnostic()
.wrap_err("Root package had [{PACKAGE_VET_CONFIG}] but it was malformed")
})
.transpose()?;
if workspace_metacfg.is_some() && package_metacfg.is_some() {
// ERRORS: immediate fatal diagnostic
return Err(miette!("Both a workspace and a package defined [metadata.vet]! We don't know what that means, if you do, let us know!"));
}
let mut metacfgs = vec![default_config];
if let Some(metacfg) = workspace_metacfg {
metacfgs.push(metacfg);
}
if let Some(metacfg) = package_metacfg {
metacfgs.push(metacfg);
}
let metacfg = MetaConfig(metacfgs);
info!("Final Metadata Config: ");
info!(" - version: {}", metacfg.version());
info!(" - store.path: {:#?}", metacfg.store_path());
//////////////////////////////////////////////////////
// Run the actual command
//////////////////////////////////////////////////////
let init = Store::is_init(&metacfg);
if matches!(cli.command, Some(Commands::Init { .. })) {
if init {
// ERRORS: immediate fatal diagnostic
return Err(miette!(
"'cargo vet' already initialized (store found at {})",
metacfg.store_path().display()
));
}
} else if !init {
// ERRORS: immediate fatal diagnostic
return Err(miette!(
"You must run 'cargo vet init' (store not found at {})",
metacfg.store_path().display()
));
}
let cfg = Config {
metacfg,
metadata,
_rest: partial_cfg,
};
use RegenerateSubcommands::*;
match &cfg.cli.command {
None => cmd_check(&out, &cfg, &cfg.cli.check_args),
Some(Check(sub_args)) => cmd_check(&out, &cfg, sub_args),
Some(Init(sub_args)) => cmd_init(&out, &cfg, sub_args),
Some(Certify(sub_args)) => cmd_certify(&out, &cfg, sub_args),
Some(AddExemption(sub_args)) => cmd_add_exemption(&out, &cfg, sub_args),
Some(RecordViolation(sub_args)) => cmd_record_violation(&out, &cfg, sub_args),
Some(Suggest(sub_args)) => cmd_suggest(&out, &cfg, sub_args),
Some(Fmt(sub_args)) => cmd_fmt(&out, &cfg, sub_args),
Some(FetchImports(sub_args)) => cmd_fetch_imports(&out, &cfg, sub_args),
Some(DumpGraph(sub_args)) => cmd_dump_graph(&out, &cfg, sub_args),
Some(Inspect(sub_args)) => cmd_inspect(&out, &cfg, sub_args),
Some(Diff(sub_args)) => cmd_diff(&out, &cfg, sub_args),
Some(Regenerate(Imports(sub_args))) => cmd_regenerate_imports(&out, &cfg, sub_args),
Some(Regenerate(Exemptions(sub_args))) => cmd_regenerate_exemptions(&out, &cfg, sub_args),
Some(Regenerate(AuditAsCratesIo(sub_args))) => {
cmd_regenerate_audit_as(&out, &cfg, sub_args)
}
Some(Aggregate(_)) | Some(HelpMarkdown(_)) | Some(Gc(_)) => unreachable!("handled earlier"),
}
}
fn cmd_init(_out: &Arc<dyn Out>, cfg: &Config, _sub_args: &InitArgs) -> Result<(), miette::Report> {
// Initialize vet
trace!("initializing...");
let mut store = Store::create(cfg)?;
let (config, audits, imports) = init_files(&cfg.metadata, cfg.cli.filter_graph.as_ref());
store.config = config;
store.audits = audits;
store.imports = imports;
fix_audit_as(cfg, &mut store)?;
store.commit()?;
Ok(())
}
pub fn init_files(
metadata: &Metadata,
filter_graph: Option<&Vec<GraphFilter>>,
) -> (ConfigFile, AuditsFile, ImportsFile) {
// Default audits file is empty
let audits = AuditsFile {
criteria: SortedMap::new(),
audits: SortedMap::new(),
};
// Default imports file is empty
let imports = ImportsFile {
audits: SortedMap::new(),
};
// This is the hard one
let config = {
let mut dependencies = SortedMap::new();
let graph = DepGraph::new(metadata, filter_graph, None);
for package in &graph.nodes {
if !package.is_third_party {
// Only care about third-party packages
continue;
}
let criteria = if package.is_dev_only {
vec![format::DEFAULT_POLICY_DEV_CRITERIA.to_string().into()]
} else {
vec![format::DEFAULT_POLICY_CRITERIA.to_string().into()]
};
// NOTE: May have multiple copies of a package!
let item = ExemptedDependency {
version: package.version.clone(),
criteria,
dependency_criteria: DependencyCriteria::new(),
notes: None,
suggest: true,
};
dependencies
.entry(package.name.to_string())
.or_insert(vec![])
.push(item);
}
ConfigFile {
default_criteria: format::get_default_criteria(),
imports: SortedMap::new(),
exemptions: dependencies,
policy: SortedMap::new(),
}
};
(config, audits, imports)
}
fn cmd_inspect(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &InspectArgs,
) -> Result<(), miette::Report> {
let version = &sub_args.version;
let package = &*sub_args.package;
let fetched = {
let network = Network::acquire(cfg);
let store = Store::acquire(cfg, network.as_ref(), false)?;
let cache = Cache::acquire(cfg)?;
// Record this command for magic in `vet certify`
cache.set_last_fetch(FetchCommand::Inspect {
package: package.to_owned(),
version: version.clone(),
});
if sub_args.mode == FetchMode::Sourcegraph {
let url = format!("https://sourcegraph.com/crates/{package}@v{version}");
tokio::runtime::Handle::current()
.block_on(prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
None,
version,
Some(&url),
))
.into_diagnostic()?;
open::that(&url).into_diagnostic().wrap_err_with(|| {
format!("Couldn't open {url} in your browser, try --mode=local?")
})?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
return Ok(());
}
tokio::runtime::Handle::current().block_on(async {
let (pkg, eulas) = tokio::join!(
cache.fetch_package(network.as_ref(), package, version),
prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
None,
version,
None,
),
);
eulas.into_diagnostic()?;
pkg.into_diagnostic()
})?
};
#[cfg(target_family = "unix")]
if let Some(shell) = std::env::var_os("SHELL") {
// Loosely borrowed from cargo crev.
writeln!(out, "Opening nested shell in: {:#?}", fetched);
writeln!(out, "Use `exit` or Ctrl-D to finish.",);
let status = std::process::Command::new(shell)
.current_dir(fetched.clone())
.env("PWD", fetched)
.status()
.map_err(CommandError::CommandFailed)
.into_diagnostic()?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
if let Some(code) = status.code() {
panic_any(ExitPanic(code));
}
return Ok(());
}
writeln!(out, " fetched to {:#?}", fetched);
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
Ok(())
}
fn cmd_certify(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &CertifyArgs,
) -> Result<(), miette::Report> {
// Certify that you have reviewed a crate's source for some version / delta
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
// Grab the last fetch and immediately drop the cache
let last_fetch = Cache::acquire(cfg)?.get_last_fetch();
do_cmd_certify(out, cfg, sub_args, &mut store, network.as_ref(), last_fetch)?;
// Minimize exemptions after adding the new `certify`. This will be used to
// potentially update imports, and remove now-unnecessary exemptions.
// Explicitly disallow new exemptions so that exemptions are only updated
// once we start passing vet.
match resolver::regenerate_exemptions(cfg, &mut store, false, false) {
Ok(()) | Err(RegenerateExemptionsError::ViolationConflict) => {}
}
store.commit()?;
Ok(())
}
fn do_cmd_certify(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &CertifyArgs,
store: &mut Store,
network: Option<&Network>,
last_fetch: Option<FetchCommand>,
) -> Result<(), CertifyError> {
// Before setting up magic, we need to agree on a package
let package = if let Some(package) = &sub_args.package {
package.clone()
} else if let Some(last_fetch) = &last_fetch {
// If we just fetched a package, assume we want to certify it
last_fetch.package().to_owned()
} else {
return Err(CertifyError::CouldntGuessPackage);
};
// FIXME: can/should we check if the version makes sense..?
if !sub_args.force
&& !foreign_packages(&cfg.metadata, &store.config).any(|pkg| pkg.name == *package)
{
return Err(CertifyError::NotAPackage(package));
}
let dependency_criteria = if sub_args.dependency_criteria.is_empty() {
// TODO: look at the current audits to infer this? prompt?
DependencyCriteria::new()
} else {
let mut dep_criteria = DependencyCriteria::new();
for arg in &sub_args.dependency_criteria {
dep_criteria
.entry(arg.dependency.clone())
.or_insert_with(Vec::new)
.push(arg.criteria.clone().into());
}
dep_criteria
};
let kind = if let Some(v1) = &sub_args.version1 {
// If explicit versions were provided, use those
if let Some(v2) = &sub_args.version2 {
// This is a delta audit
AuditKind::Delta {
from: v1.clone(),
to: v2.clone(),
dependency_criteria,
}
} else {
// This is a full audit
AuditKind::Full {
version: v1.clone(),
dependency_criteria,
}
}
} else if let Some(fetch) = last_fetch.filter(|f| f.package() == package) {
// Otherwise, is we just fetched this package, use the version(s) we fetched
match fetch {
FetchCommand::Inspect { version, .. } => AuditKind::Full {
version,
dependency_criteria,
},
FetchCommand::Diff {
version1, version2, ..
} => AuditKind::Delta {
from: version1,
to: version2,
dependency_criteria,
},
}
} else {
return Err(CertifyError::CouldntGuessVersion(package));
};
let (username, who) = if sub_args.who.is_empty() {
let user_info = get_user_info()?;
let who = format!("{} <{}>", user_info.username, user_info.email);
(user_info.username, vec![Spanned::from(who)])
} else {
(
sub_args.who.join(", "),
sub_args
.who
.iter()
.map(|w| Spanned::from(w.clone()))
.collect(),
)
};
let criteria_mapper = CriteriaMapper::new(
&store.audits.criteria,
store.imported_audits(),
&store.config.imports,
);
let criteria_names = if sub_args.criteria.is_empty() {
let (from, to) = match &kind {
AuditKind::Full { version, .. } => (None, version),
AuditKind::Delta { from, to, .. } => (Some(from), to),
_ => unreachable!(),
};
// If we don't have explicit cli criteria, guess the criteria
//
// * Check what would cause `cargo vet` to encounter fewer errors
// * Otherwise check what would cause `cargo vet suggest` to suggest fewer audits
// * Otherwise guess nothing
//
// Regardless of the guess, prompt the user to confirm (just needs to mash enter)
let mut chosen_criteria = guess_audit_criteria(cfg, store, &package, from, to);
// Prompt for criteria
loop {
out.clear_screen()?;
write!(out, "choose criteria to certify for {}", package);
match &kind {
AuditKind::Full { version, .. } => write!(out, ":{}", version),
AuditKind::Delta { from, to, .. } => write!(out, ":{} -> {}", from, to),
AuditKind::Violation { .. } => unreachable!(),
}
writeln!(out);
writeln!(out, " 0. <clear selections>");
let implied_criteria = criteria_mapper.criteria_from_list(&chosen_criteria);
// Iterate over all the local criteria. Note that it's fine for us to do the enumerate
// first, because local criteria are added to the list before foreign criteria, so they
// should be contiguous from 0..N.
let local_criteria = criteria_mapper
.list
.iter()
.enumerate()
.filter(|(_, info)| matches!(info.namespace, CriteriaNamespace::Local));
for (criteria_idx, criteria_info) in local_criteria {
if chosen_criteria.contains(&criteria_info.namespaced_name) {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
out.style().green().apply_to(&criteria_info.namespaced_name)
);
} else if implied_criteria.has_criteria(criteria_idx) {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
out.style()
.yellow()
.apply_to(&criteria_info.namespaced_name)
);
} else {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
&criteria_info.namespaced_name
);
}
}
writeln!(out);
writeln!(
out,
"current selection: {:?}",
criteria_mapper
.criteria_names(&implied_criteria)
.collect::<Vec<_>>()
);
writeln!(out, "(press ENTER to accept the current criteria)");
let input = out.read_line_with_prompt("> ")?;
let input = input.trim();
if input.is_empty() {
if chosen_criteria.is_empty() {
return Err(CertifyError::NoCriteriaChosen);
}
// User done selecting criteria
break;
}
// FIXME: these errors get cleared away right away
let answer = if let Ok(val) = input.parse::<usize>() {
val
} else {
// ERRORS: immediate error print to output for feedback, non-fatal
writeln!(out, "error: not a valid integer");
continue;
};
if answer == 0 {
chosen_criteria.clear();
continue;
}
if answer > criteria_mapper.list.len() {
// ERRORS: immediate error print to output for feedback, non-fatal
writeln!(out, "error: not a valid criteria");
continue;
}
chosen_criteria.push(criteria_mapper.list[answer - 1].namespaced_name.clone());
}
chosen_criteria
} else {
sub_args.criteria.clone()
};
// Round-trip this through the criteria_mapper to clean up `implies` relationships
let criteria_set = criteria_mapper.criteria_from_list(&criteria_names);
let criteria_names = criteria_mapper
.criteria_names(&criteria_set)
.collect::<Vec<_>>();
let what_version = match &kind {
AuditKind::Full { version, .. } => {
format!("version {}", version)
}
AuditKind::Delta { from, to, .. } => {
format!("the changes from version {} to {}", from, to)
}
AuditKind::Violation { .. } => unreachable!(),
};
let statement = format!(
"I, {}, certify that I have audited {} of {} in accordance with the above criteria.",
username, what_version, package,
);
let mut notes = sub_args.notes.clone();
if !sub_args.accept_all {
// Get all the EULAs at once
let eulas = tokio::runtime::Handle::current().block_on(join_all(
criteria_names.iter().map(|criteria| async {
(
*criteria,
eula_for_criteria(network, &store.audits.criteria, criteria).await,
)
}),
));
let mut editor = out.editor("VET_CERTIFY")?;
if let Some(notes) = ¬es {
editor.select_comment_char(notes);
}
editor.add_comments(
"Please read the following criteria and then follow the instructions below:",
)?;
editor.add_text("")?;
for (criteria, eula) in &eulas {
editor.add_comments(&format!("=== BEGIN CRITERIA {:?} ===", criteria))?;
editor.add_comments("")?;
editor.add_comments(eula)?;
editor.add_comments("")?;
editor.add_comments("=== END CRITERIA ===")?;
editor.add_comments("")?;
}
editor.add_comments("Uncomment the following statement:")?;
editor.add_text("")?;
editor.add_comments(&statement)?;
editor.add_text("")?;
editor.add_comments("Add any notes about your audit below this line:")?;
editor.add_text("")?;
if let Some(notes) = ¬es {
editor.add_text(notes)?;
}
let editor_result = editor.edit()?;
// Check to make sure that the statement was uncommented as the first
// line in the parsed file, and remove blank lines between the statement
// and notes.
let new_notes = match editor_result.trim_start().strip_prefix(&statement) {
Some(notes) => notes.trim_start_matches('\n'),
None => {
// FIXME: Might be nice to try to save any notes the user typed
// in and re-try the prompt if the user asks for it, in case
// they wrote some nice notes, but forgot to uncomment the
// statement.
return Err(CertifyError::CouldntFindCertifyStatement);
}
};
// Strip trailing newline if notes would otherwise contain no newlines.
let new_notes = new_notes
.strip_suffix('\n')
.filter(|s| !s.contains('\n'))
.unwrap_or(new_notes);
notes = if new_notes.is_empty() {
None
} else {
Some(new_notes.to_owned())
};
}
let new_entry = AuditEntry {
kind: kind.clone(),
criteria: criteria_names
.iter()
.map(|s| s.to_string().into())
.collect(),
who,
notes,
aggregated_from: vec![],
is_fresh_import: false,
};
store
.audits
.audits
.entry(package.clone())
.or_insert(vec![])
.push(new_entry);
// If we're submitting a full audit, look for a matching exemption entry to remove
if let AuditKind::Full { version, .. } = &kind {
if let Some(exemption_list) = store.config.exemptions.get_mut(&package) {
let cur_criteria_set = criteria_mapper.criteria_from_list(criteria_names);
// Iterate backwards so that we can delete while iterating
// (will only affect indices that we've already visited!)
for idx in (0..exemption_list.len()).rev() {
let entry = &exemption_list[idx];
let entry_criteria_set = criteria_mapper.criteria_from_list(&entry.criteria);
if &entry.version == version && cur_criteria_set.contains(&entry_criteria_set) {
exemption_list.remove(idx);
}
}
if exemption_list.is_empty() {
store.config.exemptions.remove(&package);
}
}
}
Ok(())
}
/// Attempt to guess which criteria are being certified for a given package and
/// audit kind.
///
/// The logic which this method uses to guess the criteria to use is as follows:
///
/// * Check what would cause `cargo vet` to encounter fewer errors
/// * Otherwise check what would cause `cargo vet suggest` to suggest fewer audits
/// * Otherwise guess nothing
fn guess_audit_criteria(
cfg: &Config,
store: &Store,
package: PackageStr<'_>,
from: Option<&Version>,
to: &Version,
) -> Vec<String> {
// Attempt to resolve a normal `cargo vet`, and try to find criteria which
// would heal some errors in that result if it fails.
let criteria = resolver::resolve(
&cfg.metadata,
cfg.cli.filter_graph.as_ref(),
store,
ResolveDepth::Deep,
)
.compute_suggested_criteria(package, from, to);
if !criteria.is_empty() {
return criteria;
}
// If a normal `cargo vet` failed to turn up any criteria, try a more