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 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 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 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 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 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 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 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}