cargo_deny/
sources.rs

1pub mod cfg;
2mod diags;
3use cfg::ValidConfig;
4pub use diags::Code;
5
6use crate::{
7    LintLevel,
8    diag::{CfgCoord, Check, ErrorSink, Label, Pack},
9};
10
11const CRATES_IO_URL: &str = "https://github.com/rust-lang/crates.io-index";
12
13pub fn check(ctx: crate::CheckCtx<'_, ValidConfig>, sink: impl Into<ErrorSink>) {
14    use bitvec::prelude::*;
15
16    // early out if everything is allowed
17    if ctx.cfg.unknown_registry == LintLevel::Allow && ctx.cfg.unknown_git == LintLevel::Allow {
18        return;
19    }
20
21    let mut sink = sink.into();
22
23    // scan through each crate and check the source of it
24
25    // keep track of which sources are actually encountered, so we can emit a
26    // warning if the user has listed a source that no crates are actually using
27    let mut source_hits: BitVec = BitVec::repeat(false, ctx.cfg.allowed_sources.len());
28    let mut org_hits: BitVec = BitVec::repeat(false, ctx.cfg.allowed_orgs.len());
29
30    let min_git_spec = ctx.cfg.required_git_spec.as_ref().map(|rgs| {
31        (
32            rgs.value,
33            CfgCoord {
34                span: rgs.span,
35                file: ctx.cfg.file_id,
36            },
37        )
38    });
39
40    for krate in ctx.krates.krates() {
41        let source = match &krate.source {
42            Some(source) => source,
43            None => continue,
44        };
45
46        let mut pack = Pack::with_kid(Check::Sources, krate.id.clone());
47
48        let mut sl = None;
49        let label = || {
50            let span = ctx.krate_spans.lock_span(&krate.id);
51            Label::primary(ctx.krate_spans.lock_id, span.source).with_message("source")
52        };
53
54        // get allowed list of sources to check
55        let (lint_level, type_name) = if source.is_registry() {
56            (ctx.cfg.unknown_registry, "registry")
57        } else if let Some(spec) = source.git_spec() {
58            // Ensure the git source has at least the minimum specification
59            if let Some((min, cfg_coord)) = &min_git_spec
60                && spec < *min
61            {
62                pack.push(diags::BelowMinimumRequiredSpec {
63                    src_label: sl.get_or_insert_with(label),
64                    min_spec: *min,
65                    actual_spec: spec,
66                    min_spec_cfg: cfg_coord.clone(),
67                });
68            }
69
70            (ctx.cfg.unknown_git, "git")
71        } else {
72            continue;
73        };
74
75        // check if the source URL is in the list of allowed sources
76        let diag: crate::diag::Diag = if let Some(ind) = ctx
77            .cfg
78            .allowed_sources
79            .iter()
80            .position(|src| krate.matches_url(&src.url.value, src.exact))
81        {
82            source_hits.as_mut_bitslice().set(ind, true);
83
84            // Show the location of the config that allowed this source, unless
85            // it's crates.io since that will be a vast majority of crates and
86            // is the default, so we might not have a real source location anyways
87            if krate.is_crates_io() {
88                continue;
89            }
90
91            diags::ExplicitlyAllowedSource {
92                src_label: sl.get_or_insert_with(label),
93                type_name,
94                allow_cfg: CfgCoord {
95                    file: ctx.cfg.file_id,
96                    span: ctx.cfg.allowed_sources[ind].url.span,
97                },
98            }
99            .into()
100        } else if let Some((orgt, orgname)) = krate.source.as_ref().and_then(|s| {
101            let crate::Source::Git { url, .. } = s else {
102                return None;
103            };
104            get_org(url)
105        }) {
106            let lowered = (!orgname.is_ascii()).then(|| orgname.to_lowercase());
107
108            if let Some(ind) = ctx.cfg.allowed_orgs.iter().position(|(sorgt, sorgn)| {
109                let s = sorgn.value.as_str();
110                if orgt != *sorgt || s.len() != orgname.len() {
111                    return false;
112                }
113
114                if let Some(orgname_lower) = &lowered {
115                    orgname_lower == &s.to_lowercase()
116                } else {
117                    s.eq_ignore_ascii_case(orgname)
118                }
119            }) {
120                org_hits.as_mut_bitslice().set(ind, true);
121                diags::SourceAllowedByOrg {
122                    src_label: sl.get_or_insert_with(label),
123                    org_cfg: CfgCoord {
124                        file: ctx.cfg.file_id,
125                        span: ctx.cfg.allowed_orgs[ind].1.span,
126                    },
127                }
128                .into()
129            } else {
130                diags::SourceNotExplicitlyAllowed {
131                    src_label: sl.get_or_insert_with(label),
132                    lint_level,
133                    type_name,
134                }
135                .into()
136            }
137        } else {
138            diags::SourceNotExplicitlyAllowed {
139                src_label: sl.get_or_insert_with(label),
140                lint_level,
141                type_name,
142            }
143            .into()
144        };
145
146        pack.push(diag);
147        sink.push(pack);
148    }
149
150    let mut pack = Pack::new(Check::Sources);
151
152    for src in source_hits
153        .into_iter()
154        .zip(ctx.cfg.allowed_sources.into_iter())
155        .filter_map(|(hit, src)| if !hit { Some(src) } else { None })
156    {
157        // If someone in is in a situation that they want to disallow crates
158        // from crates.io, they should set the allowed registries manually
159        if src.url.as_ref().as_str() == CRATES_IO_URL {
160            continue;
161        }
162
163        pack.push(diags::UnmatchedAllowSource {
164            severity: ctx.cfg.unused_allowed_source.into(),
165            allow_src_cfg: CfgCoord {
166                span: src.url.span,
167                file: ctx.cfg.file_id,
168            },
169        });
170    }
171
172    for (org_type, orgs) in org_hits
173        .into_iter()
174        .zip(ctx.cfg.allowed_orgs.into_iter())
175        .filter_map(|(hit, src)| if !hit { Some(src) } else { None })
176    {
177        pack.push(diags::UnmatchedAllowOrg {
178            allow_org_cfg: CfgCoord {
179                span: orgs.span,
180                file: ctx.cfg.file_id,
181            },
182            org_type,
183        });
184    }
185
186    if !pack.is_empty() {
187        sink.push(pack);
188    }
189}
190
191#[derive(PartialEq, Eq, Debug, Copy, Clone)]
192pub enum OrgType {
193    Github,
194    Gitlab,
195    Bitbucket,
196}
197
198use std::fmt;
199impl fmt::Display for OrgType {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.write_str(match self {
202            Self::Github => "github.com",
203            Self::Gitlab => "gitlab.com",
204            Self::Bitbucket => "bitbucket.org",
205        })
206    }
207}
208
209fn get_org(url: &url::Url) -> Option<(OrgType, &str)> {
210    url.domain().and_then(|domain| {
211        let org_type = if domain.eq_ignore_ascii_case("github.com") {
212            OrgType::Github
213        } else if domain.eq_ignore_ascii_case("gitlab.com") {
214            OrgType::Gitlab
215        } else if domain.eq_ignore_ascii_case("bitbucket.org") {
216            OrgType::Bitbucket
217        } else {
218            return None;
219        };
220
221        url.path_segments()
222            .and_then(|mut f| f.next())
223            .map(|org| (org_type, org))
224    })
225}