cargo_deny/
bans.rs

1pub mod cfg;
2mod diags;
3mod graph;
4
5use self::cfg::{ValidBuildConfig, ValidConfig, ValidTreeSkip};
6use crate::{
7    Kid, Krate, Krates, LintLevel,
8    cfg::{PackageSpec, Reason, Span, Spanned},
9    diag::{self, CfgCoord, FileId, KrateCoord},
10};
11use anyhow::Error;
12pub use diags::Code;
13use krates::cm::DependencyKind;
14use semver::VersionReq;
15use std::fmt;
16
17struct ReqMatch<'vr> {
18    specr: &'vr SpecAndReason,
19    index: usize,
20}
21
22pub(crate) struct SpecAndReason {
23    pub(crate) spec: PackageSpec,
24    pub(crate) reason: Option<Reason>,
25    pub(crate) use_instead: Option<Spanned<String>>,
26    pub(crate) file_id: FileId,
27}
28
29#[cfg(test)]
30impl serde::Serialize for SpecAndReason {
31    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
32    where
33        S: serde::Serializer,
34    {
35        use serde::ser::SerializeMap;
36        let mut map = serializer.serialize_map(Some(3))?;
37        map.serialize_entry("spec", &self.spec)?;
38        map.serialize_entry("reason", &self.reason)?;
39        map.serialize_entry("use-instead", &self.use_instead)?;
40        map.end()
41    }
42}
43
44struct SpecsAndReasons(Vec<SpecAndReason>);
45
46impl SpecsAndReasons {
47    /// Returns the specs that match the specified crate
48    #[inline]
49    fn matches<'s>(&'s self, details: &Krate) -> Option<Vec<ReqMatch<'s>>> {
50        let matches: Vec<_> = self
51            .0
52            .iter()
53            .enumerate()
54            .filter_map(|(index, req)| {
55                crate::match_krate(details, &req.spec).then_some(ReqMatch { specr: req, index })
56            })
57            .collect();
58
59        if matches.is_empty() {
60            None
61        } else {
62            Some(matches)
63        }
64    }
65}
66
67struct SkipRoot {
68    specr: SpecAndReason,
69    skip_crates: Vec<Kid>,
70    skip_hits: BitVec,
71}
72
73use bitvec::prelude::*;
74
75// If trees are being skipped, walk each one down to the specified depth and add
76// each dependency as a skipped crate at the specific version
77struct TreeSkipper {
78    roots: Vec<SkipRoot>,
79}
80
81impl TreeSkipper {
82    fn build(skip_roots: Vec<ValidTreeSkip>, krates: &Krates, cfg_file_id: FileId) -> (Self, Pack) {
83        let mut roots = Vec::with_capacity(skip_roots.len());
84
85        let mut pack = Pack::new(Check::Bans);
86
87        for ts in skip_roots {
88            let num_roots = roots.len();
89
90            for nid in krates.krates_by_name(&ts.spec.name.value).filter_map(|km| {
91                crate::match_req(&km.krate.version, ts.spec.version_req.as_ref())
92                    .then_some(km.node_id)
93            }) {
94                roots.push(Self::build_skip_root(ts.clone(), cfg_file_id, nid, krates));
95            }
96
97            // If no roots were added, add a diagnostic that the user's configuration
98            // is outdated so they can fix or clean it up
99            if roots.len() == num_roots {
100                pack.push(diags::UnmatchedSkipRoot {
101                    skip_root_cfg: CfgCoord {
102                        file: cfg_file_id,
103                        span: ts.spec.name.span,
104                    },
105                });
106            }
107        }
108
109        (Self { roots }, pack)
110    }
111
112    fn build_skip_root(
113        ts: ValidTreeSkip,
114        file_id: FileId,
115        krate_id: krates::NodeId,
116        krates: &Krates,
117    ) -> SkipRoot {
118        let (max_depth, reason) = ts.inner.map_or((usize::MAX, None), |inn| {
119            (inn.depth.unwrap_or(usize::MAX), inn.reason)
120        });
121
122        let mut skip_crates = Vec::with_capacity(10);
123
124        let graph = krates.graph();
125
126        let mut pending = vec![(krate_id, 1)];
127        while let Some((node_id, depth)) = pending.pop() {
128            let pkg_id = if let krates::Node::Krate { id, .. } = &graph[node_id] {
129                id
130            } else {
131                continue;
132            };
133            if let Err(i) = skip_crates.binary_search(pkg_id) {
134                skip_crates.insert(i, pkg_id.clone());
135
136                if depth < max_depth {
137                    for dep in krates.direct_dependencies(node_id) {
138                        pending.push((dep.node_id, depth + 1));
139                    }
140                }
141            }
142        }
143
144        let skip_hits = BitVec::repeat(false, skip_crates.len());
145
146        SkipRoot {
147            specr: SpecAndReason {
148                spec: ts.spec,
149                reason,
150                use_instead: None,
151                file_id,
152            },
153            skip_crates,
154            skip_hits,
155        }
156    }
157
158    fn matches(&mut self, krate: &Krate, pack: &mut Pack) -> bool {
159        let mut skip = false;
160
161        for root in &mut self.roots {
162            if let Ok(i) = root.skip_crates.binary_search(&krate.id) {
163                pack.push(diags::SkippedByRoot {
164                    krate,
165                    skip_root_cfg: &root.specr,
166                });
167
168                root.skip_hits.as_mut_bitslice().set(i, true);
169                skip = true;
170            }
171        }
172
173        skip
174    }
175}
176
177pub struct DupGraph {
178    pub duplicate: String,
179    pub graph: String,
180}
181
182impl fmt::Debug for DupGraph {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        f.write_str(&self.graph)
185    }
186}
187
188pub type OutputGraph = dyn Fn(DupGraph) -> Result<(), Error> + Send + Sync;
189
190use crate::diag::{Check, Diag, Pack, Severity};
191
192pub fn check(
193    ctx: crate::CheckCtx<'_, ValidConfig>,
194    output_graph: Option<Box<OutputGraph>>,
195    sink: impl Into<diag::ErrorSink>,
196) {
197    let ValidConfig {
198        file_id,
199        denied,
200        denied_multiple_versions,
201        allowed,
202        allow_workspace,
203        features,
204        workspace_default_features,
205        external_default_features,
206        skipped,
207        multiple_versions,
208        multiple_versions_include_dev,
209        workspace_dependencies,
210        highlight,
211        tree_skipped,
212        wildcards,
213        allow_wildcard_paths,
214        build,
215    } = ctx.cfg;
216
217    let mut sink = sink.into();
218    let krate_spans = &ctx.krate_spans;
219    let (mut tree_skipper, build_diags) = TreeSkipper::build(tree_skipped, ctx.krates, file_id);
220
221    if !build_diags.is_empty() {
222        sink.push(build_diags);
223    }
224
225    use std::collections::BTreeMap;
226
227    struct BanWrappers {
228        map: BTreeMap<usize, (usize, Vec<Spanned<String>>)>,
229        hits: BitVec,
230    }
231
232    impl BanWrappers {
233        fn new(mut map: BTreeMap<usize, (usize, Vec<Spanned<String>>)>) -> Self {
234            let hits = BitVec::repeat(
235                false,
236                map.values_mut().fold(0, |sum, v| {
237                    v.0 = sum;
238                    sum + v.1.len()
239                }),
240            );
241
242            Self { map, hits }
243        }
244
245        #[inline]
246        fn has_wrappers(&self, i: usize) -> bool {
247            self.map.contains_key(&i)
248        }
249
250        #[inline]
251        fn check(&mut self, i: usize, name: &str) -> Option<Span> {
252            let (offset, wrappers) = &self.map[&i];
253            if let Some(pos) = wrappers.iter().position(|wrapper| wrapper.value == name) {
254                self.hits.set(*offset + pos, true);
255                Some(wrappers[pos].span)
256            } else {
257                None
258            }
259        }
260    }
261
262    let (denied_ids, mut ban_wrappers) = {
263        let mut bw = BTreeMap::new();
264
265        (
266            SpecsAndReasons(
267                denied
268                    .into_iter()
269                    .enumerate()
270                    .map(|(i, kb)| {
271                        let (reason, use_instead) = if let Some(ext) = kb.inner {
272                            if let Some(wrappers) = ext.wrappers.filter(|w| !w.is_empty()) {
273                                bw.insert(i, (0, wrappers));
274                            }
275
276                            (ext.reason, ext.use_instead)
277                        } else {
278                            (None, None)
279                        };
280
281                        SpecAndReason {
282                            spec: kb.spec,
283                            reason,
284                            use_instead,
285                            file_id,
286                        }
287                    })
288                    .collect(),
289            ),
290            BanWrappers::new(bw),
291        )
292    };
293
294    let (feature_ids, features): (Vec<_>, Vec<_>) = features
295        .into_iter()
296        .map(|cf| {
297            (
298                SpecAndReason {
299                    spec: cf.spec,
300                    reason: cf.reason,
301                    use_instead: None,
302                    file_id,
303                },
304                cf.features,
305            )
306        })
307        .unzip();
308
309    let feature_ids = SpecsAndReasons(feature_ids);
310
311    // Keep track of all the crates we skip, and emit a warning if
312    // we encounter a skip that didn't actually match any crate version
313    // so that people can clean up their config files
314    let mut skip_hit: BitVec = BitVec::repeat(false, skipped.len());
315
316    struct MultiDetector<'a> {
317        name: &'a str,
318        dupes: smallvec::SmallVec<[(usize, bool); 4]>,
319        // Keep track of the crates that actually have > 1 version, regardless of skips
320        // if a skip is encountered for a krate that only has 1 version, warn about it
321        krates_with_dupes: Vec<&'a str>,
322    }
323
324    let mut multi_detector = MultiDetector {
325        name: &ctx.krates.krates().next().unwrap().name,
326        dupes: smallvec::SmallVec::new(),
327        krates_with_dupes: Vec::new(),
328    };
329
330    let filtered_krates = if !multiple_versions_include_dev {
331        ctx.krates.krates_filtered(krates::DepKind::Dev)
332    } else {
333        Vec::new()
334    };
335
336    // If we're not counting dev dependencies as duplicates, create a separate
337    // set of krates with dev dependencies filtered out
338    let should_add_dupe = move |kid| {
339        if multiple_versions_include_dev {
340            true
341        } else {
342            filtered_krates
343                .binary_search_by(|krate| krate.id.cmp(kid))
344                .is_ok()
345        }
346    };
347
348    let dmv = SpecsAndReasons(
349        denied_multiple_versions
350            .into_iter()
351            .map(|spec| SpecAndReason {
352                spec,
353                reason: None,
354                use_instead: None,
355                file_id,
356            })
357            .collect(),
358    );
359
360    let allowed = SpecsAndReasons(
361        allowed
362            .into_iter()
363            .map(|all| SpecAndReason {
364                spec: all.spec,
365                reason: all.inner,
366                use_instead: None,
367                file_id,
368            })
369            .collect(),
370    );
371
372    let skipped = SpecsAndReasons(
373        skipped
374            .into_iter()
375            .map(|skip| SpecAndReason {
376                spec: skip.spec,
377                reason: skip.inner,
378                use_instead: None,
379                file_id,
380            })
381            .collect(),
382    );
383
384    // Collect workspace members if allow_workspace is enabled
385    let workspace_members: std::collections::HashSet<Kid> = if allow_workspace {
386        let members: std::collections::HashSet<Kid> = ctx
387            .krates
388            .workspace_members()
389            .filter_map(|node| {
390                if let krates::Node::Krate { id, .. } = node {
391                    Some(id.clone())
392                } else {
393                    None
394                }
395            })
396            .collect();
397
398        members
399    } else {
400        std::collections::HashSet::new()
401    };
402
403    let report_duplicates = |multi_detector: &mut MultiDetector<'_>, sink: &mut diag::ErrorSink| {
404        if multi_detector.dupes.len() != 1 {
405            // Filter out crates that depend on another version of themselves https://github.com/dtolnay/semver-trick
406            multi_detector.dupes.retain(|(index, _)| {
407                let krate = &ctx.krates[*index];
408
409                // We _could_ just see if this crate's dependencies is another
410                // version of itself, but that means if there are other versions
411                // of the crate then the version that is doing the trick is not
412                // reported, so we do the more expensive check for the direct
413                // dependents
414                let direct = ctx
415                    .krates
416                    .direct_dependents(ctx.krates.nid_for_kid(&krate.id).unwrap());
417
418                let res = !direct.iter().all(|dir| dir.krate.name == krate.name);
419
420                if !res {
421                    log::debug!("ignoring duplicate crate '{krate}', its only dependents was another version of itself");
422                }
423
424                res
425            });
426        }
427
428        let skipped = multi_detector
429            .dupes
430            .iter()
431            .filter(|(_, skipped)| *skipped)
432            .count();
433        if multi_detector.dupes.len() > 1 {
434            multi_detector.krates_with_dupes.push(multi_detector.name);
435        }
436
437        if multi_detector.dupes.len() - skipped <= 1 {
438            return;
439        }
440
441        let lint_level = if multi_detector.dupes.iter().any(|(kindex, skipped)| {
442            if *skipped {
443                return false;
444            }
445
446            let krate = &ctx.krates[*kindex];
447            dmv.matches(krate).is_some()
448        }) {
449            LintLevel::Deny
450        } else {
451            multiple_versions
452        };
453
454        let severity = match lint_level {
455            LintLevel::Warn => Severity::Warning,
456            LintLevel::Deny => Severity::Error,
457            LintLevel::Allow => return,
458        };
459
460        let mut all_start = usize::MAX;
461        let mut all_end = 0;
462
463        struct Dupe {
464            /// Unique id, used for printing the actual diagnostic graphs
465            id: Kid,
466            /// Version, for deterministically ordering the duplicates
467            version: semver::Version,
468        }
469
470        let mut kids = smallvec::SmallVec::<[Dupe; 2]>::new();
471
472        for dup in multi_detector
473            .dupes
474            .iter()
475            .filter_map(|(ind, skipped)| (!*skipped).then_some(*ind))
476        {
477            let krate = &ctx.krates[dup];
478
479            let span = &ctx.krate_spans.lock_span(&krate.id).total;
480            all_start = all_start.min(span.start);
481            all_end = all_end.max(span.end);
482
483            if let Err(i) = kids.binary_search_by(|other| match other.version.cmp(&krate.version) {
484                std::cmp::Ordering::Equal => other.id.cmp(&krate.id),
485                ord => ord,
486            }) {
487                kids.insert(
488                    i,
489                    Dupe {
490                        id: krate.id.clone(),
491                        version: krate.version.clone(),
492                    },
493                );
494            }
495        }
496
497        {
498            let mut diag: Diag = diags::Duplicates {
499                krate_name: multi_detector.name,
500                num_dupes: kids.len(),
501                krates_coord: KrateCoord {
502                    file: krate_spans.lock_id,
503                    span: (all_start..all_end).into(),
504                },
505                severity,
506            }
507            .into();
508
509            diag.graph_nodes = kids
510                .into_iter()
511                .map(|dupe| crate::diag::GraphNode {
512                    kid: dupe.id,
513                    feature: None,
514                })
515                .collect();
516
517            let mut pack = Pack::new(Check::Bans);
518            pack.push(diag);
519
520            sink.push(pack);
521        }
522
523        if let Some(og) = &output_graph {
524            match graph::create_graph(
525                multi_detector.name,
526                highlight,
527                ctx.krates,
528                &multi_detector.dupes,
529            ) {
530                Ok(graph) => {
531                    if let Err(err) = og(DupGraph {
532                        duplicate: multi_detector.name.to_owned(),
533                        graph,
534                    }) {
535                        log::error!("{err}");
536                    }
537                }
538                Err(err) => {
539                    log::error!("unable to create graph for {}: {err}", multi_detector.name);
540                }
541            };
542        }
543    };
544
545    enum Sink<'k> {
546        Build(crossbeam::channel::Sender<(usize, &'k Krate, Pack)>),
547        NoBuild(diag::ErrorSink),
548    }
549
550    impl<'k> Sink<'k> {
551        #[inline]
552        fn push(&mut self, index: usize, krate: &'k Krate, pack: Pack) {
553            match self {
554                Self::Build(tx) => tx.send((index, krate, pack)).unwrap(),
555                Self::NoBuild(sink) => {
556                    if !pack.is_empty() {
557                        sink.push(pack);
558                    }
559                }
560            }
561        }
562    }
563
564    let (mut tx, build_config) = if let Some(bc) = build {
565        let (tx, rx) = crossbeam::channel::unbounded();
566
567        (Sink::Build(tx), Some((bc, rx)))
568    } else {
569        (Sink::NoBuild(sink.clone()), None)
570    };
571
572    struct BuildCheckCtx {
573        bypasses: parking_lot::Mutex<BitVec>,
574        diag_packs: parking_lot::Mutex<std::collections::BTreeMap<usize, Pack>>,
575        cargo_home: Option<crate::PathBuf>,
576        build_config: ValidBuildConfig,
577    }
578
579    let build_check_ctx = build_config.map(|(build_config, rx)| {
580        // Make all paths reported in build diagnostics be relative to cargo_home
581        let cargo_home = home::cargo_home()
582            .map_err(|err| {
583                log::error!("unable to locate $CARGO_HOME: {err}");
584                err
585            })
586            .ok()
587            .and_then(|pb| {
588                crate::PathBuf::from_path_buf(pb)
589                    .map_err(|pb| {
590                        log::error!("$CARGO_HOME path '{}' is not utf-8", pb.display());
591                    })
592                    .ok()
593            });
594
595        // Keep track of the individual crate configs so we can emit warnings
596        // if they're configured but not actually used
597        let bypasses =
598            parking_lot::Mutex::<BitVec>::new(BitVec::repeat(false, build_config.bypass.len()));
599
600        (
601            BuildCheckCtx {
602                cargo_home,
603                bypasses,
604                diag_packs: parking_lot::Mutex::new(std::collections::BTreeMap::new()),
605                build_config,
606            },
607            rx,
608        )
609    });
610
611    let mut ws_duplicate_packs = Vec::new();
612
613    rayon::scope(|scope| {
614        scope.spawn(|_| {
615            let last = ctx.krates.len() - 1;
616
617            for (i, krate) in ctx.krates.krates().enumerate() {
618                let mut pack = Pack::with_kid(Check::Bans, krate.id.clone());
619
620                // Check if the crate has been explicitly banned
621                if let Some(matches) = denied_ids.matches(krate) {
622                    let is_workspace_member = workspace_members.contains(&krate.id);
623
624                    for rm in matches {
625                        let ban_cfg = CfgCoord {
626                            file: file_id,
627                            span: rm.specr.spec.name.span,
628                        };
629
630                        // If allow_workspace is enabled and this is a workspace member,
631                        // skip the ban (workspace members take precedence over explicit bans)
632                        if is_workspace_member && allow_workspace {
633                            continue;
634                        }
635
636                        // The crate is banned, but it might be allowed if it's
637                        // wrapped by one or more particular crates
638                        let is_allowed_by_wrapper = if ban_wrappers.has_wrappers(rm.index) {
639                            let nid = ctx.krates.nid_for_kid(&krate.id).unwrap();
640
641                            // Ensure that every single crate that has a direct dependency
642                            // on the banned crate is an allowed wrapper, note we
643                            // check every one even after a failure so we don't get
644                            // extra warnings about unmatched wrappers
645                            let mut all = true;
646                            for src in ctx.krates.direct_dependents(nid) {
647                                let (diag, is_allowed): (Diag, _) =
648                                    match ban_wrappers.check(rm.index, &src.krate.name) {
649                                        Some(span) => (
650                                            diags::BannedAllowedByWrapper {
651                                                ban_cfg: ban_cfg.clone(),
652                                                ban_exception_cfg: CfgCoord {
653                                                    file: file_id,
654                                                    span,
655                                                },
656                                                banned_krate: krate,
657                                                wrapper_krate: src.krate,
658                                            }
659                                            .into(),
660                                            true,
661                                        ),
662                                        None => (
663                                            diags::BannedUnmatchedWrapper {
664                                                ban_cfg: rm.specr,
665                                                banned_krate: krate,
666                                                parent_krate: src.krate,
667                                            }
668                                            .into(),
669                                            false,
670                                        ),
671                                    };
672
673                                pack.push(diag);
674                                all = all && is_allowed;
675                            }
676
677                            all
678                        } else {
679                            false
680                        };
681
682                        if !is_allowed_by_wrapper {
683                            pack.push(diags::ExplicitlyBanned {
684                                krate,
685                                ban_cfg: rm.specr,
686                            });
687                        }
688                    }
689                }
690
691                if !allowed.0.is_empty() {
692                    // Since only allowing specific crates is pretty draconian,
693                    // also emit which allow filters actually passed each crate
694                    let is_workspace_member = workspace_members.contains(&krate.id);
695
696                    match allowed.matches(krate) {
697                        Some(matches) => {
698                            for rm in matches {
699                                pack.push(diags::ExplicitlyAllowed {
700                                    krate,
701                                    allow_cfg: rm.specr,
702                                });
703                            }
704                        }
705                        None => {
706                            // If allow_workspace is enabled and this is a workspace member,
707                            // automatically allow it without requiring explicit configuration
708                            if is_workspace_member && allow_workspace {
709                                // Workspace member is automatically allowed, no diagnostic needed
710                            } else {
711                                pack.push(diags::NotAllowed { krate });
712                            }
713                        }
714                    }
715                }
716
717                let enabled_features = ctx.krates.get_enabled_features(&krate.id).unwrap();
718
719                let default_lint_level = if enabled_features.contains("default") {
720                    if ctx.krates.workspace_members().any(|n| {
721                        if let krates::Node::Krate { id, .. } = n {
722                            id == &krate.id
723                        } else {
724                            false
725                        }
726                    }) {
727                        workspace_default_features.as_ref()
728                    } else {
729                        external_default_features.as_ref()
730                    }
731                } else {
732                    None
733                };
734
735                if let Some(ll) = default_lint_level
736                    && ll.value == LintLevel::Warn
737                {
738                    pack.push(diags::DefaultFeatureEnabled {
739                        krate,
740                        level: ll,
741                        file_id,
742                    });
743                }
744
745                // Check if the crate has had features denied/allowed or are required to be exact
746                if let Some(matches) = feature_ids.matches(krate) {
747                    for rm in matches {
748                        let feature_bans = &features[rm.index];
749
750                        let feature_set_allowed = {
751                            // Gather features that were present, but not explicitly allowed
752                            let not_explicitly_allowed: Vec<_> = enabled_features
753                                .iter()
754                                .filter_map(|ef| {
755                                    if !feature_bans.allow.value.iter().any(|af| &af.value == ef) {
756                                        if ef == "default"
757                                            && let Some(ll) = default_lint_level
758                                            && ll.value != LintLevel::Deny
759                                        {
760                                            return None;
761                                        }
762
763                                        Some(ef.as_str())
764                                    } else {
765                                        None
766                                    }
767                                })
768                                .collect();
769
770                            if feature_bans.exact.value {
771                                // Gather features allowed, but not present
772                                let missing_allowed: Vec<_> = feature_bans
773                                    .allow
774                                    .value
775                                    .iter()
776                                    .filter_map(|af| {
777                                        if !enabled_features.contains(&af.value) {
778                                            Some(CfgCoord {
779                                                file: file_id,
780                                                span: af.span,
781                                            })
782                                        } else {
783                                            None
784                                        }
785                                    })
786                                    .collect();
787
788                                if missing_allowed.is_empty() && not_explicitly_allowed.is_empty() {
789                                    true
790                                } else {
791                                    pack.push(diags::ExactFeaturesMismatch {
792                                        missing_allowed,
793                                        not_allowed: &not_explicitly_allowed,
794                                        exact_coord: CfgCoord {
795                                            file: file_id,
796                                            span: feature_bans.exact.span,
797                                        },
798                                        krate,
799                                    });
800                                    false
801                                }
802                            } else {
803                                // Mark the number of current diagnostics, if we add more
804                                // the check has failed
805                                let diag_count = pack.len();
806
807                                // Add diagnostics if features were explicitly allowed,
808                                // but didn't contain 1 or more features that were enabled
809                                if !feature_bans.allow.value.is_empty() {
810                                    for feature in &not_explicitly_allowed {
811                                        // Since the user has not specified `exact` we
812                                        // can also look at the full tree of features to
813                                        // determine if the feature is covered by an allowed
814                                        // parent feature
815                                        fn has_feature(
816                                            map: &std::collections::BTreeMap<String, Vec<String>>,
817                                            parent: &str,
818                                            feature: &str,
819                                        ) -> bool {
820                                            if let Some(parent) = map.get(parent) {
821                                                parent.iter().any(|f| {
822                                                    let pf =
823                                                        krates::ParsedFeature::from(f.as_str());
824
825                                                    if let krates::Feature::Simple(feat) = pf.feat()
826                                                    {
827                                                        if feat == feature {
828                                                            true
829                                                        } else {
830                                                            has_feature(map, feat, feature)
831                                                        }
832                                                    } else {
833                                                        false
834                                                    }
835                                                })
836                                            } else {
837                                                false
838                                            }
839                                        }
840
841                                        if !feature_bans.allow.value.iter().any(|allowed| {
842                                            has_feature(
843                                                &krate.features,
844                                                allowed.value.as_str(),
845                                                feature,
846                                            )
847                                        }) {
848                                            pack.push(diags::FeatureNotExplicitlyAllowed {
849                                                krate,
850                                                feature,
851                                                allowed: CfgCoord {
852                                                    file: file_id,
853                                                    span: feature_bans.allow.span,
854                                                },
855                                            });
856                                        }
857                                    }
858                                }
859
860                                // If the default feature has been denied at a global
861                                // level but not at the crate level, emit an error with
862                                // the global span, otherwise the crate level setting,
863                                // if the default feature was banned explicitly, takes
864                                // precedence
865                                if let Some(ll) = default_lint_level
866                                    && ll.value == LintLevel::Deny
867                                    && !feature_bans
868                                        .allow
869                                        .value
870                                        .iter()
871                                        .any(|d| d.value == "default")
872                                    && !feature_bans.deny.iter().any(|d| d.value == "default")
873                                {
874                                    pack.push(diags::DefaultFeatureEnabled {
875                                        krate,
876                                        level: ll,
877                                        file_id,
878                                    });
879                                }
880
881                                for feature in feature_bans
882                                    .deny
883                                    .iter()
884                                    .filter(|feat| enabled_features.contains(&feat.value))
885                                {
886                                    pack.push(diags::FeatureBanned {
887                                        krate,
888                                        feature,
889                                        file_id,
890                                    });
891                                }
892
893                                diag_count <= pack.len()
894                            }
895                        };
896
897                        // If the crate isn't actually banned, but does reference
898                        // features that don't exist, emit warnings about them so
899                        // the user can cleanup their config. We _could_ emit these
900                        // warnings if the crate is banned, but feature graphs in
901                        // particular can be massive and adding warnings into the mix
902                        // will just make parsing the error graphs harder
903                        if feature_set_allowed {
904                            for feature in feature_bans
905                                .allow
906                                .value
907                                .iter()
908                                .chain(feature_bans.deny.iter())
909                            {
910                                if !krate.features.contains_key(&feature.value) {
911                                    pack.push(diags::UnknownFeature {
912                                        krate,
913                                        feature,
914                                        file_id,
915                                    });
916                                }
917                            }
918                        }
919                    }
920                } else if let Some(ll) = default_lint_level
921                    && ll.value == LintLevel::Deny
922                {
923                    pack.push(diags::DefaultFeatureEnabled {
924                        krate,
925                        level: ll,
926                        file_id,
927                    });
928                }
929
930                if should_add_dupe(&krate.id) {
931                    if let Some(matches) = skipped.matches(krate) {
932                        if multi_detector.name != krate.name {
933                            report_duplicates(&mut multi_detector, &mut sink);
934
935                            multi_detector.name = &krate.name;
936                            multi_detector.dupes.clear();
937                        }
938
939                        multi_detector.dupes.push((i, true));
940
941                        for rm in matches {
942                            pack.push(diags::Skipped {
943                                krate,
944                                skip_cfg: rm.specr,
945                            });
946
947                            // Mark each skip filter that is hit so that we can report unused
948                            // filters to the user so that they can cleanup their configs as
949                            // their dependency graph changes over time
950                            skip_hit.as_mut_bitslice().set(rm.index, true);
951                        }
952                    } else if !tree_skipper.matches(krate, &mut pack) {
953                        if multi_detector.name != krate.name {
954                            report_duplicates(&mut multi_detector, &mut sink);
955
956                            multi_detector.name = &krate.name;
957                            multi_detector.dupes.clear();
958                        }
959
960                        multi_detector.dupes.push((i, false));
961
962                        'wildcards: {
963                            if wildcards != LintLevel::Allow && !krate.is_git_source() {
964                                let severity = match wildcards {
965                                    LintLevel::Warn => Severity::Warning,
966                                    LintLevel::Deny => Severity::Error,
967                                    LintLevel::Allow => unreachable!(),
968                                };
969
970                                let Some(manifest) = ctx.krate_spans.manifest(&krate.id) else {
971                                    break 'wildcards;
972                                };
973                                let is_private = krate.is_private(&[]);
974                                let mut labels = Vec::new();
975                                let mut pack = Pack::with_kid(Check::Bans, krate.id.clone());
976
977                                for mdep in manifest.deps(false) {
978                                    if mdep.dep.req != VersionReq::STAR {
979                                        continue;
980                                    }
981
982                                    // Wildcards are allowed for path or git dependencies, if the krate
983                                    // is private, or it's only a dev-dependency
984                                    if allow_wildcard_paths
985                                        && !mdep.krate.is_registry()
986                                        && (is_private
987                                            || mdep.dep.kind == DependencyKind::Development)
988                                    {
989                                        continue;
990                                    }
991
992                                    labels.push(
993                                        crate::diag::Label::primary(
994                                            manifest.id,
995                                            mdep.version_req
996                                                .as_ref()
997                                                .map_or(mdep.value_span, |vr| vr.span),
998                                        )
999                                        .with_message("wildcard dependency"),
1000                                    );
1001
1002                                    // If the dependency is a workspace dependency we also want to show
1003                                    // the bad version requirement for the workspace declaration
1004                                    if let Some(workspace) = &mdep.workspace {
1005                                        if !workspace.value {
1006                                            continue;
1007                                        }
1008
1009                                        if let Some(ws_dep) =
1010                                            ctx.krate_spans.workspace_span(&krate.id)
1011                                        {
1012                                            labels.push(
1013                                                crate::diag::Label::secondary(
1014                                                    ctx.krate_spans.workspace_id.unwrap(),
1015                                                    ws_dep
1016                                                        .version
1017                                                        .as_ref()
1018                                                        .map_or(ws_dep.value, |vr| vr.span),
1019                                                )
1020                                                .with_message("workspace dependency"),
1021                                            );
1022                                        } else {
1023                                            // This indicates a bug because we were unable to resolve the workspace dependency
1024                                            // to an appropriate crate, even though cargo did
1025                                            pack.push(diags::UnresolveWorkspaceDependency {
1026                                                manifest,
1027                                                dep: mdep,
1028                                            });
1029                                        }
1030                                    }
1031                                }
1032
1033                                sink.push(pack);
1034
1035                                if !labels.is_empty() {
1036                                    sink.push(diags::Wildcards {
1037                                        krate,
1038                                        severity,
1039                                        labels,
1040                                        allow_wildcard_paths,
1041                                    });
1042                                }
1043                            }
1044                        }
1045                    }
1046                }
1047
1048                if i == last {
1049                    report_duplicates(&mut multi_detector, &mut sink);
1050                }
1051
1052                tx.push(i, krate, pack);
1053            }
1054
1055            drop(tx);
1056        });
1057
1058        scope.spawn(|scope| {
1059            let Some((build_ctx, rx)) = &build_check_ctx else {
1060                return;
1061            };
1062            while let Ok((index, krate, mut pack)) = rx.recv() {
1063                scope.spawn(move |_s| {
1064                    if let Some(bcc) = check_build(
1065                        ctx.cfg.file_id,
1066                        &build_ctx.build_config,
1067                        build_ctx.cargo_home.as_deref(),
1068                        krate,
1069                        ctx.krates,
1070                        &mut pack,
1071                    ) {
1072                        build_ctx.bypasses.lock().set(bcc, true);
1073                    }
1074
1075                    if !pack.is_empty() {
1076                        build_ctx.diag_packs.lock().insert(index, pack);
1077                    }
1078                });
1079            }
1080        });
1081
1082        // Check the workspace to detect dependencies that are used more than once
1083        // but don't use a shared [workspace.[dev-/build-]dependencies] declaration
1084        if let Some(ws_deps) = &workspace_dependencies
1085            && ws_deps.duplicates != LintLevel::Allow
1086        {
1087            scope.spawn(|_| {
1088                check_workspace_duplicates(
1089                    ctx.krates,
1090                    ctx.krate_spans,
1091                    ws_deps,
1092                    &mut ws_duplicate_packs,
1093                );
1094            });
1095        }
1096    });
1097
1098    if let Some((bcc, _)) = build_check_ctx {
1099        for bp in bcc.diag_packs.into_inner().into_values() {
1100            sink.push(bp);
1101        }
1102
1103        let mut pack = Pack::new(Check::Bans);
1104        for ve in bcc
1105            .bypasses
1106            .into_inner()
1107            .into_iter()
1108            .zip(bcc.build_config.bypass.into_iter())
1109            .filter_map(|(hit, ve)| if !hit { Some(ve) } else { None })
1110        {
1111            pack.push(diags::UnmatchedBypass {
1112                unmatched: &ve,
1113                file_id,
1114            });
1115        }
1116
1117        sink.push(pack);
1118    }
1119
1120    for pack in ws_duplicate_packs {
1121        sink.push(pack);
1122    }
1123
1124    if let Some(ws_deps) = workspace_dependencies
1125        && ws_deps.unused != LintLevel::Allow
1126        && let Some(id) = krate_spans
1127            .workspace_id
1128            .filter(|_id| !krate_spans.unused_workspace_deps.is_empty())
1129    {
1130        sink.push(diags::UnusedWorkspaceDependencies {
1131            id,
1132            unused: &krate_spans.unused_workspace_deps,
1133            level: ws_deps.unused,
1134        });
1135    }
1136
1137    let mut pack = Pack::new(Check::Bans);
1138
1139    for (hit, skip) in skip_hit.into_iter().zip(skipped.0.into_iter()) {
1140        if !hit {
1141            pack.push(diags::UnmatchedSkip { skip_cfg: &skip });
1142        } else if multi_detector
1143            .krates_with_dupes
1144            .binary_search(&skip.spec.name.value.as_str())
1145            .is_err()
1146        {
1147            pack.push(diags::UnnecessarySkip { skip_cfg: &skip });
1148        }
1149    }
1150
1151    for wrapper in ban_wrappers
1152        .hits
1153        .into_iter()
1154        .zip(ban_wrappers.map.into_values().flat_map(|(_, w)| w))
1155        .filter_map(|(hit, wrapper)| (!hit).then_some(wrapper))
1156    {
1157        pack.push(diags::UnusedWrapper {
1158            wrapper_cfg: CfgCoord {
1159                file: file_id,
1160                span: wrapper.span,
1161            },
1162        });
1163    }
1164
1165    sink.push(pack);
1166}
1167
1168pub fn check_build(
1169    file_id: FileId,
1170    config: &ValidBuildConfig,
1171    home: Option<&crate::Path>,
1172    krate: &Krate,
1173    krates: &Krates,
1174    pack: &mut Pack,
1175) -> Option<usize> {
1176    use krates::cm::TargetKind;
1177
1178    let build_script_allowed = if let Some(allow_build_scripts) = &config.allow_build_scripts {
1179        let has_build_script = krate
1180            .targets
1181            .iter()
1182            .any(|t| t.kind.contains(&TargetKind::CustomBuild));
1183
1184        !has_build_script
1185            || allow_build_scripts
1186                .iter()
1187                .any(|id| crate::match_krate(krate, id))
1188    } else {
1189        true
1190    };
1191
1192    if build_script_allowed && config.executables == LintLevel::Allow {
1193        return None;
1194    }
1195
1196    #[inline]
1197    fn executes_at_buildtime(krate: &Krate) -> bool {
1198        krate.targets.iter().any(|t| {
1199            t.kind
1200                .iter()
1201                .any(|k| matches!(*k, TargetKind::CustomBuild | TargetKind::ProcMacro))
1202        })
1203    }
1204
1205    fn needs_checking(krate: krates::NodeId, krates: &Krates) -> bool {
1206        if executes_at_buildtime(&krates[krate]) {
1207            return true;
1208        }
1209
1210        for dd in krates.direct_dependents(krate) {
1211            if needs_checking(dd.node_id, krates) {
1212                return true;
1213            }
1214        }
1215
1216        false
1217    }
1218
1219    // Check if the krate is either a proc-macro, has a build-script, OR is a dependency
1220    // of a crate that is/does
1221    if !config.include_workspace
1222        && krates.workspace_members().any(|n| {
1223            if let krates::Node::Krate { id, .. } = n {
1224                id == &krate.id
1225            } else {
1226                false
1227            }
1228        })
1229        || (!config.include_dependencies && !executes_at_buildtime(krate))
1230        || (config.include_dependencies
1231            && !needs_checking(krates.nid_for_kid(&krate.id).unwrap(), krates))
1232    {
1233        return None;
1234    }
1235
1236    let (kc_index, krate_config) = config
1237        .bypass
1238        .iter()
1239        .enumerate()
1240        .find_map(|(i, ae)| crate::match_krate(krate, &ae.spec).then_some((i, ae)))
1241        .unzip();
1242
1243    // If the build script hashes to the same value and required features are not actually
1244    // set on the crate, we can skip it
1245    if let Some(kc) = krate_config
1246        && let Some(bsc) = &kc.build_script
1247        && let Some(path) = krate
1248            .targets
1249            .iter()
1250            .find_map(|t| (t.name == "build-script-build").then_some(&t.src_path))
1251    {
1252        let root = &krate.manifest_path.parent().unwrap();
1253        match validate_file_checksum(path, &bsc.value) {
1254            Ok(_) => {
1255                pack.push(diags::ChecksumMatch {
1256                    path: diags::HomePath { path, root, home },
1257                    checksum: bsc,
1258                    severity: None,
1259                    file_id,
1260                });
1261
1262                // Emit an error if the user specifies features that don't exist
1263                for rfeat in &kc.required_features {
1264                    if !krate.features.contains_key(&rfeat.value) {
1265                        pack.push(diags::UnknownFeature {
1266                            krate,
1267                            feature: rfeat,
1268                            file_id,
1269                        });
1270                    }
1271                }
1272
1273                let enabled = krates.get_enabled_features(&krate.id).unwrap();
1274
1275                let enabled_features: Vec<_> = kc
1276                    .required_features
1277                    .iter()
1278                    .filter(|f| enabled.contains(&f.value))
1279                    .collect();
1280
1281                // If none of the required-features are present then we
1282                // can skip the rest of the check
1283                if enabled_features.is_empty() {
1284                    return kc_index;
1285                }
1286
1287                pack.push(diags::FeaturesEnabled {
1288                    enabled_features,
1289                    file_id,
1290                });
1291            }
1292            Err(err) => {
1293                pack.push(diags::ChecksumMismatch {
1294                    path: diags::HomePath { path, root, home },
1295                    checksum: bsc,
1296                    severity: Some(Severity::Warning),
1297                    error: format!("build script failed checksum: {err:#}"),
1298                    file_id,
1299                });
1300            }
1301        }
1302    }
1303
1304    if !build_script_allowed {
1305        pack.push(diags::BuildScriptNotAllowed { krate });
1306        return kc_index;
1307    }
1308
1309    let root = krate.manifest_path.parent().unwrap();
1310
1311    let (tx, rx) = crossbeam::channel::unbounded();
1312
1313    let (_, checksum_diags) = rayon::join(
1314        || {
1315            // Avoids doing a ton of heap allocations when doing globset matching
1316            let mut matches = Vec::new();
1317            let is_git_src = krate.is_git_source();
1318
1319            let mut allow_hit: BitVec =
1320                BitVec::repeat(false, krate_config.map_or(0, |kc| kc.allow.len()));
1321            let mut glob_hit: BitVec = BitVec::repeat(
1322                false,
1323                krate_config.map_or(0, |kc| {
1324                    kc.allow_globs.as_ref().map_or(0, |ag| ag.patterns.len())
1325                }),
1326            );
1327
1328            for entry in walkdir::WalkDir::new(root)
1329                .sort_by_file_name()
1330                .into_iter()
1331                .filter_entry(|entry| {
1332                    // Skip git folders for git sources, they won't be present in
1333                    // regular packages, and the example scripts in typical
1334                    // clones are...not interesting
1335                    !is_git_src
1336                        || (entry.path().file_name() == Some(std::ffi::OsStr::new(".git"))
1337                            && entry.path().parent() == Some(root.as_std_path()))
1338                })
1339            {
1340                let Ok(entry) = entry else {
1341                    continue;
1342                };
1343
1344                if entry.file_type().is_dir() {
1345                    continue;
1346                }
1347
1348                let absolute_path = match crate::PathBuf::from_path_buf(entry.into_path()) {
1349                    Ok(p) => p,
1350                    Err(path) => {
1351                        pack.push(diags::NonUtf8Path { path: &path });
1352                        continue;
1353                    }
1354                };
1355
1356                let path = &absolute_path;
1357
1358                let Ok(rel_path) = path.strip_prefix(root) else {
1359                    pack.push(diags::NonRootPath { path, root });
1360                    continue;
1361                };
1362
1363                let candidate = globset::Candidate::new(rel_path);
1364
1365                if let Some(kc) = krate_config {
1366                    // First just check if the file has been explicitly allowed without a
1367                    // checksum so we don't even need to bother going more in depth
1368                    let ae = kc
1369                        .allow
1370                        .binary_search_by(|ae| ae.path.value.as_path().cmp(rel_path))
1371                        .ok()
1372                        .map(|i| {
1373                            allow_hit.set(i, true);
1374                            &kc.allow[i]
1375                        });
1376
1377                    if let Some(ae) = ae
1378                        && ae.checksum.is_none()
1379                    {
1380                        pack.push(diags::ExplicitPathAllowance {
1381                            allowed: ae,
1382                            file_id,
1383                        });
1384                        continue;
1385                    }
1386
1387                    // Check if the path matches an allowed glob pattern
1388                    if let Some(ag) = &kc.allow_globs
1389                        && let Some(globs) = ag.matches(&candidate, &mut matches)
1390                    {
1391                        for &i in &matches {
1392                            glob_hit.set(i, true);
1393                        }
1394
1395                        pack.push(diags::GlobAllowance {
1396                            path: diags::HomePath { path, root, home },
1397                            globs,
1398                            file_id,
1399                        });
1400                        continue;
1401                    }
1402
1403                    // If the file had a checksum specified, verify it still matches,
1404                    // otherwise fail
1405                    if let Some(checksum) = ae.as_ref().and_then(|ae| ae.checksum.as_ref()) {
1406                        let _ = tx.send((absolute_path, checksum));
1407                        continue;
1408                    }
1409                }
1410
1411                // Check if the file matches a disallowed glob pattern
1412                if let Some(globs) = config.script_extensions.matches(&candidate, &mut matches) {
1413                    pack.push(diags::DeniedByExtension {
1414                        path: diags::HomePath { path, root, home },
1415                        globs,
1416                        file_id,
1417                    });
1418                    continue;
1419                }
1420
1421                // Save the most ambiguous/expensive check for last, does this look
1422                // like a native executable or script without extension?
1423                let diag: Diag = match check_is_executable(path, !config.include_archives) {
1424                    Ok(None) => continue,
1425                    Ok(Some(exe_kind)) => diags::DetectedExecutable {
1426                        path: diags::HomePath { path, root, home },
1427                        interpreted: config.interpreted,
1428                        exe_kind,
1429                    }
1430                    .into(),
1431                    Err(error) => diags::UnableToCheckPath {
1432                        path: diags::HomePath { path, root, home },
1433                        error,
1434                    }
1435                    .into(),
1436                };
1437
1438                pack.push(diag);
1439            }
1440
1441            if let Some(ae) = krate_config.map(|kc| &kc.allow) {
1442                for ae in allow_hit
1443                    .into_iter()
1444                    .zip(ae.iter())
1445                    .filter_map(|(hit, ae)| if !hit { Some(ae) } else { None })
1446                {
1447                    pack.push(diags::UnmatchedPathBypass {
1448                        unmatched: ae,
1449                        file_id,
1450                    });
1451                }
1452            }
1453
1454            if let Some(vgs) = krate_config.and_then(|kc| kc.allow_globs.as_ref()) {
1455                for gp in glob_hit
1456                    .into_iter()
1457                    .zip(vgs.patterns.iter())
1458                    .filter_map(|(hit, gp)| {
1459                        if !hit && let cfg::GlobPattern::User(gp) = gp {
1460                            return Some(gp);
1461                        }
1462
1463                        None
1464                    })
1465                {
1466                    pack.push(diags::UnmatchedGlob {
1467                        unmatched: gp,
1468                        file_id,
1469                    });
1470                }
1471            }
1472
1473            drop(tx);
1474        },
1475        || {
1476            // Note that since we ship off the checksum validation to a threads the order is
1477            // not guaranteed, so we just put them in a btreemap so they are consistently
1478            // ordered and don't trigger test errors or cause confusing output for users
1479            let checksum_diags = parking_lot::Mutex::new(std::collections::BTreeMap::new());
1480            rayon::scope(|s| {
1481                while let Ok((path, checksum)) = rx.recv() {
1482                    s.spawn(|_s| {
1483                        let absolute_path = path;
1484                        let path = &absolute_path;
1485                        if let Err(err) = validate_file_checksum(&absolute_path, &checksum.value) {
1486                            let diag: Diag = diags::ChecksumMismatch {
1487                                path: diags::HomePath { path, root, home },
1488                                checksum,
1489                                severity: None,
1490                                error: format!("{err:#}"),
1491                                file_id,
1492                            }
1493                            .into();
1494
1495                            checksum_diags.lock().insert(absolute_path, diag);
1496                        } else {
1497                            let diag: Diag = diags::ChecksumMatch {
1498                                path: diags::HomePath { path, root, home },
1499                                checksum,
1500                                severity: None,
1501                                file_id,
1502                            }
1503                            .into();
1504
1505                            checksum_diags.lock().insert(absolute_path, diag);
1506                        }
1507                    });
1508                }
1509            });
1510
1511            checksum_diags.into_inner().into_values()
1512        },
1513    );
1514
1515    for diag in checksum_diags {
1516        pack.push(diag);
1517    }
1518
1519    kc_index
1520}
1521
1522pub(crate) enum ExecutableKind {
1523    Native(goblin::Hint),
1524    Interpreted(String),
1525}
1526
1527fn check_is_executable(
1528    path: &crate::Path,
1529    exclude_archives: bool,
1530) -> anyhow::Result<Option<ExecutableKind>> {
1531    use std::io::Read;
1532
1533    let mut file = std::fs::File::open(path)?;
1534    let mut header = [0u8; 16];
1535    let read = file.read(&mut header)?;
1536    if read != header.len() {
1537        return Ok(None);
1538    }
1539
1540    use goblin::Hint;
1541
1542    match goblin::peek_bytes(&header)
1543        .map_err(|err| anyhow::format_err!("failed to peek bytes: {err}"))?
1544    {
1545        // Archive objects/libraries are not great (generally) to have in
1546        // crate packages, but they are not as easily
1547        Hint::Archive if exclude_archives => Ok(None),
1548        Hint::Unknown(_) => {
1549            // Check for shebang scripts
1550            if header[..2] != [0x23, 0x21] {
1551                return Ok(None);
1552            }
1553
1554            // If we have a shebang, look to see if we have the newline, otherwise we need to read more bytes
1555            let mut hdr = [0u8; 256];
1556            let header = if !header.contains(&b'\n') {
1557                hdr[..16].copy_from_slice(&header);
1558                let read = file.read(&mut hdr[16..])?;
1559                &hdr[..read + 16]
1560            } else {
1561                &header[..]
1562            };
1563
1564            let parse = || {
1565                let line_end = header.iter().position(|b| *b == b'\n')?;
1566                let line = std::str::from_utf8(&header[..line_end]).ok()?;
1567
1568                // If it's a rust file, ignore it if the shebang is actually
1569                // an inner attribute
1570                if path.extension() == Some("rs") && line.starts_with("#![") {
1571                    return None;
1572                }
1573
1574                // Shebangs scripts can't have any spaces in the actual interpreter, but there
1575                // can be an optional space between the shebang and the start of the interpreter
1576                let mut items = line.split(' ');
1577                let maybe_interpreter = items.next()?;
1578                let interpreter = if maybe_interpreter.ends_with("#!") {
1579                    items.next()?
1580                } else {
1581                    maybe_interpreter
1582                };
1583
1584                // Handle (typically) /usr/bin/env being used as level of indirection
1585                // to make running scripts more friendly to run on a variety
1586                // of systems
1587                if interpreter.ends_with("/env") {
1588                    items.next()
1589                } else if let Some((_, bin)) = interpreter.rsplit_once('/') {
1590                    Some(bin)
1591                } else {
1592                    Some(interpreter)
1593                }
1594            };
1595
1596            Ok(parse().map(|s| ExecutableKind::Interpreted(s.to_owned())))
1597        }
1598        Hint::COFF => Ok(None),
1599        hint => Ok(Some(ExecutableKind::Native(hint))),
1600    }
1601}
1602
1603/// Validates the buffer matches the expected SHA-256 checksum
1604fn validate_checksum(
1605    mut stream: impl std::io::Read,
1606    expected: &cfg::Checksum,
1607) -> anyhow::Result<()> {
1608    let digest = {
1609        let mut dc = ring::digest::Context::new(&ring::digest::SHA256);
1610        let mut chunk = [0; 8 * 1024];
1611        loop {
1612            let read = stream.read(&mut chunk)?;
1613            if read == 0 {
1614                break;
1615            }
1616            dc.update(&chunk[..read]);
1617        }
1618        dc.finish()
1619    };
1620
1621    let digest = digest.as_ref();
1622    if digest != expected.0 {
1623        let mut hs = [0u8; 64];
1624        const CHARS: &[u8] = b"0123456789abcdef";
1625        for (i, &byte) in digest.iter().enumerate() {
1626            let i = i * 2;
1627            hs[i] = CHARS[(byte >> 4) as usize];
1628            hs[i + 1] = CHARS[(byte & 0xf) as usize];
1629        }
1630
1631        let digest = std::str::from_utf8(&hs).unwrap();
1632        anyhow::bail!("checksum mismatch, calculated {digest}");
1633    }
1634
1635    Ok(())
1636}
1637
1638#[inline]
1639fn validate_file_checksum(path: &crate::Path, expected: &cfg::Checksum) -> anyhow::Result<()> {
1640    let file = std::fs::File::open(path)?;
1641    validate_checksum(std::io::BufReader::new(file), expected)?;
1642    Ok(())
1643}
1644
1645fn check_workspace_duplicates(
1646    krates: &Krates,
1647    krate_spans: &crate::diag::KrateSpans<'_>,
1648    cfg: &cfg::WorkspaceDepsConfig,
1649    diags: &mut Vec<Pack>,
1650) {
1651    use crate::diag::Label;
1652
1653    // Note this will ignore cases where a dependency is used as more than 1 kind etc,
1654    // but this check is not really meant for such simple cases
1655    if krates.workspace_members().count() <= 1 {
1656        return;
1657    }
1658
1659    // Gather any direct dependencies that are declared more than once
1660    let mut deps = std::collections::BTreeMap::<
1661        _,
1662        Vec<(
1663            &crate::diag::ManifestDep<'_>,
1664            &crate::Krate,
1665            crate::diag::FileId,
1666        )>,
1667    >::new();
1668
1669    for wsm in krates.workspace_members() {
1670        let krates::Node::Krate { id, krate, .. } = wsm else {
1671            continue; /* unreachable */
1672        };
1673
1674        let Some(man) = krate_spans.manifest(id) else {
1675            continue;
1676        };
1677
1678        for mdep in man.deps(true) {
1679            if mdep.dep.path.is_some() && !cfg.include_path_dependencies {
1680                continue;
1681            }
1682
1683            deps.entry(&mdep.krate.id)
1684                .or_default()
1685                .push((mdep, krate, man.id));
1686        }
1687    }
1688
1689    // Strip out any direct dependencies that aren't referenced multiple times in the workspace
1690    // Note this will retain in cases where the dependency is only used
1691    // by 1 crate, but as different dependency kinds, which is still useful
1692    // to catch
1693    deps.retain(|_, parents| parents.len() > 1);
1694
1695    for (kid, parents) in deps {
1696        let total = parents.len();
1697        let mut labels = Vec::new();
1698
1699        if let Some((ws_span, ws_id)) = krate_spans
1700            .workspace_span(kid)
1701            .zip(krate_spans.workspace_id)
1702        {
1703            labels.push(
1704                Label::secondary(ws_id, ws_span.key).with_message(format_args!(
1705                    "{}workspace dependency",
1706                    if ws_span.patched.is_some() {
1707                        "patched "
1708                    } else {
1709                        ""
1710                    }
1711                )),
1712            );
1713
1714            if let Some(patched) = ws_span.patched {
1715                labels.push(
1716                    Label::secondary(ws_id, patched)
1717                        .with_message("note this is the original dependency that is patched"),
1718                );
1719            }
1720
1721            if let Some(rename) = &ws_span.rename {
1722                labels.push(
1723                    Label::secondary(ws_id, rename.span)
1724                        .with_message("note the workspace dependency is renamed"),
1725                );
1726            }
1727        }
1728
1729        let llen = labels.len();
1730        let has_workspace_declaration = llen != 0;
1731
1732        for (mdep, _parent, id) in parents {
1733            // Unfortunately cargo doesn't allow `workspace = false`, so if
1734            // there are situations where the user wants to explicitly opt out
1735            // of the lint for a specific crate/crates/manifest they need to use
1736            // [package.metadata.cargo-deny.workspace-duplicates]
1737            // TODO: ...actually support this https://github.com/EmbarkStudios/cargo-deny/issues/677
1738            if mdep.workspace.is_some() {
1739                continue;
1740            }
1741
1742            labels.push(Label::primary(id, mdep.key_span));
1743
1744            if let Some(rename) = &mdep.rename {
1745                labels.push(
1746                    Label::secondary(id, rename.span)
1747                        .with_message("note the dependency is renamed"),
1748                );
1749            }
1750        }
1751
1752        if llen >= labels.len() {
1753            continue;
1754        }
1755
1756        let Some(duplicate) = krates.node_for_kid(kid) else {
1757            log::error!("failed to find node for {kid}");
1758            continue;
1759        };
1760
1761        let krates::Node::Krate {
1762            krate: duplicate, ..
1763        } = duplicate
1764        else {
1765            log::error!("{kid} pointed a crate feature, this should be impossible");
1766            continue;
1767        };
1768
1769        let mut pack = Pack::with_kid(Check::Bans, duplicate.id.clone());
1770        pack.push(diags::WorkspaceDuplicate {
1771            duplicate,
1772            labels,
1773            severity: cfg.duplicates,
1774            has_workspace_declaration,
1775            total_uses: total,
1776        });
1777        diags.push(pack);
1778    }
1779}