use crates::ttl_cache::TtlCache;
use impl_prelude::*;
use std::collections::hash_set::HashSet;
use std::fmt::{self, Debug};
use std::process::Command;
use std::sync::Mutex;
use std::time::Duration;
#[derive(Debug, Clone, Copy)]
pub enum ValidNameFullNamePolicy {
Required,
Preferred,
Optional,
}
impl Default for ValidNameFullNamePolicy {
fn default() -> Self {
ValidNameFullNamePolicy::Required
}
}
impl ValidNameFullNamePolicy {
fn apply<F>(&self, result: &mut CheckResult, msg: F)
where F: Fn(&str) -> String,
{
match *self {
ValidNameFullNamePolicy::Required => {
result.add_error(msg("required"));
},
ValidNameFullNamePolicy::Preferred => {
result.add_warning(msg("preferred"));
},
ValidNameFullNamePolicy::Optional => {},
}
}
}
const LOCK_POISONED: &str = "DNS cache lock poisoned";
const DEFAULT_TTL_CACHE_SIZE: usize = 100;
lazy_static! {
static ref DEFAULT_TTL_CACHE_HIT_DURATION: Duration = Duration::from_secs(24 * 60 * 60);
static ref DEFAULT_TTL_CACHE_MISS_DURATION: Duration = Duration::from_secs(5 * 60);
}
pub struct ValidName {
full_name_policy: ValidNameFullNamePolicy,
dns_cache: Mutex<TtlCache<String, bool>>,
whitelisted_domains: HashSet<String>,
}
impl Debug for ValidName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("ValidName")
.field("full_name_policy", &self.full_name_policy)
.field("whitelisted_domains", &self.whitelisted_domains)
.finish()
}
}
impl Default for ValidName {
fn default() -> Self {
Self {
full_name_policy: ValidNameFullNamePolicy::default(),
dns_cache: Mutex::new(TtlCache::new(DEFAULT_TTL_CACHE_SIZE)),
whitelisted_domains: HashSet::new(),
}
}
}
impl Clone for ValidName {
fn clone(&self) -> Self {
Self {
full_name_policy: self.full_name_policy,
dns_cache: Mutex::new(TtlCache::new(DEFAULT_TTL_CACHE_SIZE)),
whitelisted_domains: self.whitelisted_domains.clone(),
}
}
}
impl ValidName {
pub fn new() -> Self {
Default::default()
}
pub fn set_full_name_policy(&mut self, policy: ValidNameFullNamePolicy) -> &mut Self {
self.full_name_policy = policy;
self
}
pub fn whitelist_domains<I, D>(&mut self, domains: I) -> &mut Self
where I: IntoIterator<Item = D>,
D: ToString,
{
self.whitelisted_domains
.extend(domains.into_iter().map(|domain| domain.to_string()));
self
}
fn check_name(name: &str) -> bool {
name.find(' ').is_some()
}
fn check_host(domain: &str) -> Option<bool> {
let dig = Command::new("host")
.arg("-t").arg("MX")
.arg(format!("{}.", domain)) .output();
let dig_output = match dig {
Ok(dig_output) => dig_output,
Err(err) => {
error!(target: "git-checks/valid_name",
"failed to construct host command: {:?}",
err);
return None;
},
};
if dig_output.status.success() {
Some(true)
} else {
let output = String::from_utf8_lossy(&dig_output.stdout);
warn!(target: "git-checks/valid_name",
"failed to look up MX record for domain {}: {}",
domain,
output);
if output.contains("connection timed out") {
None
} else {
Some(false)
}
}
}
fn check_email(&self, email: &str) -> bool {
let domain_part = email.splitn(2, '@')
.nth(1);
if let Some(domain) = domain_part {
if self.whitelisted_domains.contains(domain) {
return true;
}
let mut cache = self.dns_cache.lock().expect(LOCK_POISONED);
if let Some(cached_res) = cache.get_mut(domain) {
return *cached_res;
}
Self::check_host(domain)
.map_or(false, |res| {
let duration = if res {
*DEFAULT_TTL_CACHE_HIT_DURATION
} else {
*DEFAULT_TTL_CACHE_MISS_DURATION
};
cache.insert(domain.to_string(), res, duration);
res
})
} else {
false
}
}
fn check_identity(&self, what: &str, who: &str, identity: &Identity) -> CheckResult {
let mut result = CheckResult::new();
if !Self::check_name(&identity.name) {
self.full_name_policy.apply(&mut result, |policy| {
format!("The {} name (`{}`) for {} has no space in it. \
A full name is {} for contribution. Please set the \
`user.name` Git configuration value.",
who,
identity.name,
what,
policy)
});
}
if !self.check_email(&identity.email) {
result.add_error(format!("The {} email (`{}`) for {} has an unknown domain. Please set \
the `user.email` Git configuration value.",
who,
identity.email,
what));
}
result
}
}
impl Check for ValidName {
fn name(&self) -> &str {
"valid-name"
}
fn check(&self, _: &CheckGitContext, commit: &Commit) -> Result<CheckResult> {
let what = format!("commit {}", commit.sha1);
Ok(if commit.author == commit.committer {
self.check_identity(&what, "given", &commit.author)
} else {
let author_res = self.check_identity(&what, "author", &commit.author);
let commiter_res =
self.check_identity(&what, "committer", &commit.committer);
author_res.combine(commiter_res)
})
}
}
impl BranchCheck for ValidName {
fn name(&self) -> &str {
"valid-name"
}
fn check(&self, ctx: &CheckGitContext, _: &CommitId) -> Result<CheckResult> {
Ok(self.check_identity("the topic",
"owner",
ctx.topic_owner()))
}
}
#[cfg(test)]
mod tests {
use checks::ValidName;
use checks::ValidNameFullNamePolicy;
use checks::test::*;
const BAD_TOPIC: &str = "91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8";
const BAD_AUTHOR_NAME: &str = "edac4e5b3a00eac60280a78ee84b5ef8d4cce97a";
#[test]
fn test_valid_name_required() {
let check = ValidName::new();
let result = run_check("test_valid_name_required", BAD_TOPIC, check);
test_result_errors(result, &[
"The given name (`Mononym`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
no space in it. A full name is required for contribution. Please set the `user.name` \
Git configuration value.",
"The given email (`bademail`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
an unknown domain. Please set the `user.email` Git configuration value.",
"The committer email (`bademail@baddomain.invalid`) for commit \
dcd8895d299031d607481b4936478f8de4cc28ae has an unknown domain. Please set the \
`user.email` Git configuration value.",
"The author email (`bademail@baddomain.invalid`) for commit \
9002239437a06e81a58fed07150b215a917028d6 has an unknown domain. Please set the \
`user.email` Git configuration value.",
"The committer email (`bademail`) for commit da71ae048e5a387d6809558d59ad073d0e4fb089 \
has an unknown domain. Please set the `user.email` Git configuration value.",
"The committer name (`Mononym`) for commit 1debf1735a6e28880ef08f13baeea4b71a08a846 \
has no space in it. A full name is required for contribution. Please set the \
`user.name` Git configuration value.",
"The author email (`bademail`) for commit 9de4928f5ec425eef414ee7620d0692fda56ebb0 \
has an unknown domain. Please set the `user.email` Git configuration value.",
"The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
no space in it. A full name is required for contribution. Please set the `user.name` \
Git configuration value.",
]);
}
#[test]
fn test_valid_name_whitelist() {
let mut check = ValidName::new();
check.whitelist_domains(&["baddomain.invalid"]);
let result = run_check("test_valid_name_whitelist", BAD_TOPIC, check);
test_result_errors(result, &[
"The given name (`Mononym`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
no space in it. A full name is required for contribution. Please set the `user.name` \
Git configuration value.",
"The given email (`bademail`) for commit 91d9fceb226bfc0faeb8a4e54b4f0b5a1ffd39e8 has \
an unknown domain. Please set the `user.email` Git configuration value.",
"The committer email (`bademail`) for commit da71ae048e5a387d6809558d59ad073d0e4fb089 \
has an unknown domain. Please set the `user.email` Git configuration value.",
"The committer name (`Mononym`) for commit 1debf1735a6e28880ef08f13baeea4b71a08a846 \
has no space in it. A full name is required for contribution. Please set the \
`user.name` Git configuration value.",
"The author email (`bademail`) for commit 9de4928f5ec425eef414ee7620d0692fda56ebb0 \
has an unknown domain. Please set the `user.email` Git configuration value.",
"The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
no space in it. A full name is required for contribution. Please set the `user.name` \
Git configuration value.",
]);
}
#[test]
fn test_valid_name_preferred() {
let mut check = ValidName::new();
check.set_full_name_policy(ValidNameFullNamePolicy::Preferred);
let result = run_check("test_valid_name_preferred", BAD_AUTHOR_NAME, check);
test_result_warnings(result, &[
"The author name (`Mononym`) for commit edac4e5b3a00eac60280a78ee84b5ef8d4cce97a has \
no space in it. A full name is preferred for contribution. Please set the \
`user.name` Git configuration value.",
]);
}
#[test]
fn test_valid_name_optional() {
let mut check = ValidName::new();
check.set_full_name_policy(ValidNameFullNamePolicy::Optional);
run_check_ok("test_valid_name_optional", BAD_AUTHOR_NAME, check);
}
}