| pub mod cfg; |
| mod diags; |
| use cfg::ValidConfig; |
| pub use diags::Code; |
| |
| use crate::{ |
| diag::{CfgCoord, Check, ErrorSink, Label, Pack}, |
| LintLevel, |
| }; |
| |
| const CRATES_IO_URL: &str = "https://github.com/rust-lang/crates.io-index"; |
| |
| pub fn check(ctx: crate::CheckCtx<'_, ValidConfig>, sink: impl Into<ErrorSink>) { |
| use bitvec::prelude::*; |
| |
| // early out if everything is allowed |
| if ctx.cfg.unknown_registry == LintLevel::Allow && ctx.cfg.unknown_git == LintLevel::Allow { |
| return; |
| } |
| |
| let mut sink = sink.into(); |
| |
| // scan through each crate and check the source of it |
| |
| // keep track of which sources are actually encountered, so we can emit a |
| // warning if the user has listed a source that no crates are actually using |
| let mut source_hits: BitVec = BitVec::repeat(false, ctx.cfg.allowed_sources.len()); |
| let mut org_hits: BitVec = BitVec::repeat(false, ctx.cfg.allowed_orgs.len()); |
| |
| let min_git_spec = ctx.cfg.required_git_spec.as_ref().map(|rgs| { |
| ( |
| rgs.value, |
| CfgCoord { |
| span: rgs.span, |
| file: ctx.cfg.file_id, |
| }, |
| ) |
| }); |
| |
| for (i, krate) in ctx.krates.krates().enumerate() { |
| let source = match &krate.source { |
| Some(source) => source, |
| None => continue, |
| }; |
| |
| let mut pack = Pack::with_kid(Check::Sources, krate.id.clone()); |
| |
| let mut sl = None; |
| let label = || { |
| let span = &ctx.krate_spans[i]; |
| Label::primary(ctx.krate_spans.file_id, span.source..span.total.end) |
| .with_message("source") |
| }; |
| |
| // get allowed list of sources to check |
| let (lint_level, type_name) = if source.is_registry() { |
| (ctx.cfg.unknown_registry, "registry") |
| } else if let Some(spec) = source.git_spec() { |
| // Ensure the git source has at least the minimum specification |
| if let Some((min, cfg_coord)) = &min_git_spec { |
| if spec < *min { |
| pack.push(diags::BelowMinimumRequiredSpec { |
| src_label: sl.get_or_insert_with(label), |
| min_spec: *min, |
| actual_spec: spec, |
| min_spec_cfg: cfg_coord.clone(), |
| }); |
| } |
| } |
| |
| (ctx.cfg.unknown_git, "git") |
| } else { |
| continue; |
| }; |
| |
| // check if the source URL is in the list of allowed sources |
| let diag: crate::diag::Diag = if let Some(ind) = ctx |
| .cfg |
| .allowed_sources |
| .iter() |
| .position(|src| krate.matches_url(&src.url.value, src.exact)) |
| { |
| source_hits.as_mut_bitslice().set(ind, true); |
| |
| // Show the location of the config that allowed this source, unless |
| // it's crates.io since that will be a vast majority of crates and |
| // is the default, so we might not have a real source location anyways |
| if krate.is_crates_io() { |
| continue; |
| } |
| |
| diags::ExplicitlyAllowedSource { |
| src_label: sl.get_or_insert_with(label), |
| type_name, |
| allow_cfg: CfgCoord { |
| file: ctx.cfg.file_id, |
| span: ctx.cfg.allowed_sources[ind].url.span, |
| }, |
| } |
| .into() |
| } else if let Some((orgt, orgname)) = krate.source.as_ref().and_then(|s| { |
| let crate::Source::Git { url, .. } = s else { |
| return None; |
| }; |
| get_org(url) |
| }) { |
| if let Some(ind) = ctx |
| .cfg |
| .allowed_orgs |
| .iter() |
| .position(|(sorgt, sorgn)| orgt == *sorgt && sorgn.value.as_str() == orgname) |
| { |
| org_hits.as_mut_bitslice().set(ind, true); |
| diags::SourceAllowedByOrg { |
| src_label: sl.get_or_insert_with(label), |
| org_cfg: CfgCoord { |
| file: ctx.cfg.file_id, |
| span: ctx.cfg.allowed_orgs[ind].1.span, |
| }, |
| } |
| .into() |
| } else { |
| diags::SourceNotExplicitlyAllowed { |
| src_label: sl.get_or_insert_with(label), |
| lint_level, |
| type_name, |
| } |
| .into() |
| } |
| } else { |
| diags::SourceNotExplicitlyAllowed { |
| src_label: sl.get_or_insert_with(label), |
| lint_level, |
| type_name, |
| } |
| .into() |
| }; |
| |
| pack.push(diag); |
| sink.push(pack); |
| } |
| |
| let mut pack = Pack::new(Check::Sources); |
| |
| for src in source_hits |
| .into_iter() |
| .zip(ctx.cfg.allowed_sources.into_iter()) |
| .filter_map(|(hit, src)| if !hit { Some(src) } else { None }) |
| { |
| // If someone in is in a situation that they want to disallow crates |
| // from crates.io, they should set the allowed registries manually |
| if src.url.as_ref().as_str() == CRATES_IO_URL { |
| continue; |
| } |
| |
| pack.push(diags::UnmatchedAllowSource { |
| allow_src_cfg: CfgCoord { |
| span: src.url.span, |
| file: ctx.cfg.file_id, |
| }, |
| }); |
| } |
| |
| for (org_type, orgs) in org_hits |
| .into_iter() |
| .zip(ctx.cfg.allowed_orgs.into_iter()) |
| .filter_map(|(hit, src)| if !hit { Some(src) } else { None }) |
| { |
| pack.push(diags::UnmatchedAllowOrg { |
| allow_org_cfg: CfgCoord { |
| span: orgs.span, |
| file: ctx.cfg.file_id, |
| }, |
| org_type, |
| }); |
| } |
| |
| if !pack.is_empty() { |
| sink.push(pack); |
| } |
| } |
| |
| #[derive(PartialEq, Eq, Debug, Copy, Clone)] |
| pub enum OrgType { |
| Github, |
| Gitlab, |
| Bitbucket, |
| } |
| |
| use std::fmt; |
| impl fmt::Display for OrgType { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| f.write_str(match self { |
| Self::Github => "github.com", |
| Self::Gitlab => "gitlab.com", |
| Self::Bitbucket => "bitbucket.org", |
| }) |
| } |
| } |
| |
| fn get_org(url: &url::Url) -> Option<(OrgType, &str)> { |
| url.domain().and_then(|domain| { |
| let org_type = if domain.eq_ignore_ascii_case("github.com") { |
| OrgType::Github |
| } else if domain.eq_ignore_ascii_case("gitlab.com") { |
| OrgType::Gitlab |
| } else if domain.eq_ignore_ascii_case("bitbucket.org") { |
| OrgType::Bitbucket |
| } else { |
| return None; |
| }; |
| |
| url.path_segments() |
| .and_then(|mut f| f.next()) |
| .map(|org| (org_type, org)) |
| }) |
| } |