use std::sync::LazyLock;
use crate::license;
use crate::tools::git;
use crate::tools::git::TransferProtocol;
use crate::tools::git_hosting_provs::HostingType;
use crate::var::{Confidence, Key};
use crate::{constants, environment::Environment};
use chrono::{DateTime, NaiveDateTime};
use regex::Regex;
use thiserror::Error;
use url::Url;
pub type Result = std::result::Result<Validity, Error>;
pub type Validator = fn(&mut Environment, &str) -> Result;
#[must_use]
pub const fn res_to_confidences(res: &Result) -> [Confidence; 2] {
match &res {
Ok(validity) => [validity.confidence(), 0],
Err(error) => [0, error.confidence()],
}
}
#[derive(Debug)]
pub enum Validity {
High { msg: Option<String> },
Middle { msg: String },
Low { msg: String },
Missing,
Suboptimal {
msg: String,
source: Option<Box<dyn std::error::Error + Sync>>,
},
Unknown,
}
impl Validity {
#[must_use]
pub const fn confidence(&self) -> Confidence {
match self {
Self::High { msg: _ } => 250,
Self::Middle { msg: _ } => 230,
Self::Low { msg: _ } => 210,
Self::Missing => 0,
Self::Suboptimal { msg: _, source: _ } => 200,
Self::Unknown => 100,
}
}
#[must_use]
pub const fn is_good(&self) -> bool {
match self {
Self::High { msg: _ } | Self::Middle { msg: _ } | Self::Low { msg: _ } => true,
Self::Missing | Self::Suboptimal { msg: _, source: _ } | Self::Unknown => false,
}
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("No value found for the required property {0:?}")]
Missing(Key),
#[error("The value '{value}' is unfit for this key, but only just - {msg}")]
AlmostUsableValue { msg: String, value: String },
#[error("The value '{value}' is unfit for this key - {msg}")]
BadValue { msg: String, value: String },
#[error(transparent)]
IO(#[from] std::io::Error),
}
impl Error {
#[must_use]
pub const fn confidence(&self) -> Confidence {
match self {
Self::Missing { .. } => 40,
Self::AlmostUsableValue { .. } => 100,
Self::BadValue { .. } => 50,
Self::IO(_) => 30,
}
}
}
fn missing(environment: &mut Environment, key: Key) -> Result {
if environment.settings.required_keys.contains(&key) {
Err(Error::Missing(key))
} else {
Ok(Validity::Missing)
}
}
fn validate_version(environment: &mut Environment, value: &str) -> Result {
static R_SEM_VERS_RELEASE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)$")
.unwrap()
});
static R_SEM_VERS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$").unwrap()
});
static R_SEM_GIT_VERS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))(-(0|[1-9]\d*)-(g[0-9a-f]{7}))?((-dirty(-broken)?)|-broken(-dirty)?)?$").unwrap()
});
static R_GIT_VERS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^((g[0-9a-f]{7})|([^~^:?\[\*]+))(-(0|[1-9]\d*)-(g[0-9a-f]{7}))?((-dirty(-broken)?)|-broken(-dirty)?)?$").unwrap()
});
static R_GIT_SHA: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^g?[0-9a-f]{7,40}$").unwrap());
static R_GIT_SHA_PREFIX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^g[0-9a-f]{7}").unwrap());
static R_UNKNOWN_VERS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^($|#|//)").unwrap());
if R_SEM_VERS_RELEASE.is_match(value) {
Ok(Validity::Low {
msg: "This is a release version, \
which indicates either that we are on a release commit, \
or that it is imprecise, \
and actually a left-over from the previous release."
.to_owned(),
})
} else if git::is_git_broken_version(value) {
log::warn!(
"Broken project version '{value}'; something is seriously wrong with your git repo"
);
Ok(Validity::Suboptimal {
msg: "This version is broken; something is seriously wrong with your git repo."
.to_owned(),
source: None,
})
} else if git::is_git_dirty_version(value) {
log::warn!("Dirty project version '{value}'; you have uncommitted changes in your project");
if R_GIT_SHA_PREFIX.is_match(value) {
Ok(Validity::Low {
msg: "This version is technically ok - \
having a raw git SHA as base - \
but not a release-version, \
and not human-readable; \
it does not allow direct comparison with other versions.
We trust it because it is dirty, though."
.to_owned(),
})
} else {
Ok(Validity::Middle {
msg: "A git dirty version starting with a tag".to_owned(),
})
}
} else if R_SEM_VERS.is_match(value) {
Ok(Validity::Middle {
msg: "semver".to_owned(),
})
} else if R_GIT_SHA.is_match(value) {
Ok(Validity::Suboptimal {
msg: "This version is technically ok - \
a raw git SHA - \
but not a release-version, \
and not human-readable; \
it does not allow direct comparison with other versions."
.to_owned(),
source: None,
})
} else if R_SEM_GIT_VERS.is_match(value) {
Ok(Validity::Middle {
msg: "A git version starting with a semver tag, \
but not a valid semver version"
.to_owned(),
})
} else if R_GIT_VERS.is_match(value) {
match R_GIT_SHA_PREFIX.find(value) {
Some(mtch) if mtch.range().len() == value.len() => Ok(Validity::Suboptimal {
msg: "This version (a git SHA) is technically ok, \
but not a release-version, and not human-readable"
.to_owned(),
source: None,
}),
Some(_) => Ok(Validity::Suboptimal {
msg: "A git version starting with a SHA \
(instead of a (semver-)tag, which would be preffered)"
.to_owned(),
source: None,
}),
None => {
Ok(Validity::Low {
msg: "A git version starting with/consisting of a non-semver tag".to_owned(),
})
}
}
} else if R_UNKNOWN_VERS.is_match(value) {
missing(environment, Key::Version)
} else {
Err(Error::BadValue {
msg: "Not a valid version".to_owned(),
value: value.to_owned(),
})
}
}
fn validate_license(environment: &mut Environment, value: &str) -> Result {
if value.is_empty() {
missing(environment, Key::License)
} else {
license::validate_spdx_expr(value).map_or_else(
|err| {
match err {
license::Error::NoLicense => Ok(Validity::Suboptimal {
msg: "Not a recognized SPDX license identifier".to_owned(),
source: Some(Box::new(err)),
}),
license::Error::ParsingFailed(_) => Ok(Validity::Suboptimal {
msg: "Not a valid SPDX license expression".to_owned(),
source: Some(Box::new(err)),
}),
license::Error::NotApproved(_) => Ok(Validity::Low {
msg: "Not only approved licenses".to_owned(),
}),
}
},
|()| {
Ok(Validity::High {
msg: Some("Consists of an SPDX license identifier".to_owned()),
})
},
)
}
}
fn validate_licenses(environment: &mut Environment, value: &str) -> Result {
if value.is_empty() {
missing(environment, Key::Licenses)
} else {
for license in value.split(',') {
let license = license.trim();
let res = validate_license(environment, license);
if let Err(err) = res {
return Ok(Validity::Suboptimal {
msg: format!(
"Not all of these are recognized SPDX license identifiers: {value}\n\tspecifically '{license}'",
),
source: Some(Box::new(err)),
});
}
}
Ok(Validity::High {
msg: Some(
"Consists of a list of SPDX license identifiers, separated by ','".to_owned(),
),
})
}
}
fn check_public_url(
_environment: &mut Environment,
value: &str,
allow_ssh: bool,
allow_git: bool,
) -> std::result::Result<Url, Error> {
match Url::parse(value) {
Err(_err) => Err(Error::BadValue {
msg: "Not a valid URL".to_owned(),
value: value.to_owned(),
}),
Ok(url) => {
let mut valid_schemes = vec!["http", "https"];
if allow_ssh {
valid_schemes.push("ssh");
}
if allow_git {
valid_schemes.push("git");
}
if !valid_schemes.contains(&url.scheme()) {
Err(Error::AlmostUsableValue {
msg: format!(
"Should use one of these as protocol(scheme): [{}]",
valid_schemes.join(", ")
),
value: value.to_owned(),
})
} else if url.username() != "" && url.username() != "git" {
Err(Error::AlmostUsableValue {
msg: format!(
"Should be anonymous access, but specifies a user-name: {}",
url.username()
),
value: value.to_owned(),
})
} else if let Some(_pw) = url.password() {
Err(Error::AlmostUsableValue {
msg: "Should be anonymous access, but contains a password".to_owned(),
value: value.to_owned(),
})
} else if let Some(query) = url.query() {
Err(Error::AlmostUsableValue {
msg: format!("Should be a simple URL, but uses query arguments: {query}",),
value: value.to_owned(),
})
} else if let Some(fragment) = url.fragment() {
Err(Error::AlmostUsableValue {
msg: format!("Should be a simple URL, but uses a fragment: {fragment}"),
value: value.to_owned(),
})
} else {
Ok(url)
}
}
}
}
fn check_empty(_environment: &mut Environment, value: &str, part_desc: &str) -> Result {
if value.is_empty() {
Err(Error::BadValue {
msg: format!("{part_desc} can not be empty"),
value: value.to_owned(),
})
} else {
Ok(Validity::Low {
msg: "at least not empty".to_owned(),
})
}
}
fn eval_hosting_type(environment: &Environment, url: &Url) -> HostingType {
environment.settings.hosting_type(url)
}
fn eval_hosting_type_from_hosting_suffix(environment: &mut Environment, url: &Url) -> HostingType {
environment.settings.hosting_type_from_hosting_suffix(url)
}
fn check_url_path(value: &str, url_desc: &str, url: &Url, path_reg: Option<&Regex>) -> Result {
if let (Some(path_reg), Some(host)) = (path_reg, url.host().as_ref()) {
if path_reg.is_match(url.path()) {
Ok(Validity::High {
msg: Some(format!(
r#"For {}, the path part of the {} URL ("{}") matches regex "{}""#,
host,
url_desc,
url.path(),
path_reg.as_str()
)),
})
} else {
Err(Error::AlmostUsableValue {
msg: format!(
r#"For {}, this path part of the {} URL is invalid: "{}"; it should match "{}""#,
host,
url_desc,
url.path(),
path_reg.as_str()
),
value: value.to_owned(),
})
}
} else {
Ok(Validity::Unknown)
}
}
fn check_url_host(value: &str, url_desc: &str, url: &Url, host_reg: Option<&Regex>) -> Result {
if let (Some(host_reg), Some(host)) = (host_reg, url.host().as_ref()) {
let host_str = host.to_string();
if host_reg.is_match(&host_str) {
Ok(Validity::High {
msg: Some(format!(
r#"For {}, the host part of the {} URL ("{}") matches regex "{}""#,
host,
url_desc,
host_str,
host_reg.as_str()
)),
})
} else {
Err(Error::AlmostUsableValue {
msg: format!(
r#"For {}, this host part of the {} URL is invalid: "{}"; it should match "{}""#,
host,
url_desc,
host_str,
host_reg.as_str()
),
value: value.to_owned(),
})
}
} else {
Ok(Validity::Unknown)
}
}
fn validate_repo_web_url(environment: &mut Environment, value: &str) -> Result {
static R_GIT_HUB_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/?$").unwrap());
static R_GIT_LAB_PATH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^/(?P<user>[^/]+)/((?P<structure>[^/]+)/)*(?P<repo>[^/]+)/?$").unwrap()
});
static R_BIT_BUCKET_PATH: LazyLock<Regex> = LazyLock::new(|| (*R_GIT_HUB_PATH).clone());
let url = check_public_url(environment, value, false, false)?;
let hosting_type = eval_hosting_type(environment, &url);
let host_reg: Option<&Regex> = match hosting_type {
HostingType::GitHub => Some(&R_GIT_HUB_PATH),
HostingType::GitLab => Some(&R_GIT_LAB_PATH),
HostingType::BitBucket => Some(&R_BIT_BUCKET_PATH),
_ => None, };
check_url_path(value, "versioned web", &url, host_reg)
}
static R_GIT_HUB_CLONE_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)(\.git)?$").unwrap());
static R_GIT_LAB_CLONE_PATH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^/(?P<user>[^/]+)/((?P<structure>[^/]+)/)*(?P<repo>[^/]+)(\.git)?$").unwrap()
});
static R_BIT_BUCKET_CLONE_PATH: LazyLock<Regex> = LazyLock::new(|| (*R_GIT_HUB_CLONE_PATH).clone());
fn validate_repo_clone_url(_environment: &mut Environment, value: &str) -> Result {
gix_url::parse(value.into())
.map(|_url| Validity::Middle {
msg:
"Nothing wrong with that; but we can/do not check more than that it is a valid URL"
.to_owned(),
})
.map_err(|err| Error::BadValue {
msg: err.to_string(),
value: value.to_owned(),
})
}
fn validate_repo_clone_url_generic(
environment: &mut Environment,
value: &str,
protocol: TransferProtocol,
) -> Result {
let url = check_public_url(
environment,
value,
matches!(protocol, TransferProtocol::Ssh),
matches!(protocol, TransferProtocol::Git),
)?;
if url.scheme() != protocol.scheme_str() {
return Err(Error::BadValue {
msg: format!(
"Wrong URL Scheme; should be '{}', but is '{}'",
protocol.scheme_str(),
url.scheme()
),
value: value.to_owned(),
});
}
let hosting_type = eval_hosting_type(environment, &url);
let host_reg: Option<&Regex> = match hosting_type {
HostingType::GitHub => Some(&R_GIT_HUB_CLONE_PATH),
HostingType::GitLab => Some(&R_GIT_LAB_CLONE_PATH),
HostingType::BitBucket => Some(&R_BIT_BUCKET_CLONE_PATH),
_ => None, };
check_url_path(value, "repo clone", &url, host_reg)
}
fn validate_repo_clone_url_git(environment: &mut Environment, value: &str) -> Result {
validate_repo_clone_url_generic(environment, value, TransferProtocol::Git)
}
fn validate_repo_clone_url_http(environment: &mut Environment, value: &str) -> Result {
validate_repo_clone_url_generic(environment, value, TransferProtocol::Https)
}
fn validate_repo_clone_url_ssh(environment: &mut Environment, value: &str) -> Result {
static R_SSH_CLONE_URL: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?P<user>git@)?(?P<host>[^/:]+)((:|/)(?P<path>.+))?$").unwrap()
});
let url = match check_public_url(environment, value, true, false) {
Ok(url) => {
if url.scheme() != TransferProtocol::Ssh.scheme_str() {
return Err(Error::AlmostUsableValue {
msg: format!(
"Wrong URL Scheme; should be '{}', but is '{}'",
TransferProtocol::Ssh.scheme_str(),
url.scheme()
),
value: value.to_owned(),
});
}
url
}
Err(err_orig) => {
let ssh_value = R_SSH_CLONE_URL.replace(value, "ssh://$host/$path");
match check_public_url(environment, &ssh_value, true, false) {
Ok(url) => url,
Err(_err_ssh) => return Err(err_orig), }
}
};
let hosting_type = eval_hosting_type(environment, &url);
let host_reg: Option<&Regex> = match hosting_type {
HostingType::GitHub => Some(&R_GIT_HUB_CLONE_PATH),
HostingType::GitLab => Some(&R_GIT_LAB_CLONE_PATH),
HostingType::BitBucket => Some(&R_BIT_BUCKET_CLONE_PATH),
_ => None, };
check_url_path(value, "repo clone ssh", &url, host_reg)
}
fn validate_repo_raw_versioned_prefix_url(environment: &mut Environment, value: &str) -> Result {
static R_GIT_HUB_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)$").unwrap());
static R_GIT_LAB_PATH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^/(?P<user>[^/]+)/((?P<structure>[^/]+)/)*(?P<repo>[^/]+)/(-/)?raw$").unwrap()
});
static R_BIT_BUCKET_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/raw$").unwrap());
let url = check_public_url(environment, value, false, false)?;
let hosting_type = eval_hosting_type(environment, &url);
let host_reg: Option<&Regex> = match hosting_type {
HostingType::GitHub => Some(&R_GIT_HUB_PATH),
HostingType::GitLab => Some(&R_GIT_LAB_PATH),
HostingType::BitBucket => Some(&R_BIT_BUCKET_PATH),
_ => None, };
check_url_path(value, "raw versioned prefix", &url, host_reg)
}
fn validate_repo_versioned_file_prefix_url(environment: &mut Environment, value: &str) -> Result {
static R_GIT_HUB_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/blob$").unwrap());
static R_GIT_LAB_PATH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^/(?P<user>[^/]+)/((?P<structure>[^/]+)/)*(?P<repo>[^/]+)/(-/)?blob$").unwrap()
});
static R_BIT_BUCKET_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/src$").unwrap());
let url = check_public_url(environment, value, false, false)?;
let hosting_type = eval_hosting_type(environment, &url);
let host_reg: Option<&Regex> = match hosting_type {
HostingType::GitHub => Some(&R_GIT_HUB_PATH),
HostingType::GitLab => Some(&R_GIT_LAB_PATH),
HostingType::BitBucket => Some(&R_BIT_BUCKET_PATH),
_ => None, };
check_url_path(value, "versioned file prefix", &url, host_reg)
}
fn validate_repo_versioned_dir_prefix_url(environment: &mut Environment, value: &str) -> Result {
static R_GIT_HUB_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/tree$").unwrap());
static R_GIT_LAB_PATH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^/(?P<user>[^/]+)/((?P<structure>[^/]+)/)*(?P<repo>[^/]+)/(-/)?tree$").unwrap()
});
static R_BIT_BUCKET_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/src$").unwrap());
let url = check_public_url(environment, value, false, false)?;
let hosting_type = eval_hosting_type(environment, &url);
let host_reg: Option<&Regex> = match hosting_type {
HostingType::GitHub => Some(&R_GIT_HUB_PATH),
HostingType::GitLab => Some(&R_GIT_LAB_PATH),
HostingType::BitBucket => Some(&R_BIT_BUCKET_PATH),
_ => None, };
check_url_path(value, "versioned dir prefix", &url, host_reg)
}
fn validate_repo_commit_prefix_url(environment: &mut Environment, value: &str) -> Result {
static R_GIT_HUB_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/commit$").unwrap());
static R_GIT_LAB_PATH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^/(?P<user>[^/]+)/((?P<structure>[^/]+)/)*(?P<repo>[^/]+)/(-/)?commit$")
.unwrap()
});
static R_BIT_BUCKET_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/commits$").unwrap());
let url = check_public_url(environment, value, false, false)?;
let hosting_type = eval_hosting_type(environment, &url);
let host_reg: Option<&Regex> = match hosting_type {
HostingType::GitHub => Some(&R_GIT_HUB_PATH),
HostingType::GitLab => Some(&R_GIT_LAB_PATH),
HostingType::BitBucket => Some(&R_BIT_BUCKET_PATH),
_ => None, };
check_url_path(value, "commit prefix", &url, host_reg)
}
fn validate_repo_issues_url(environment: &mut Environment, value: &str) -> Result {
static R_GIT_HUB_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/issues$").unwrap());
static R_GIT_LAB_PATH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^/(?P<user>[^/]+)/((?P<structure>[^/]+)/)*(?P<repo>[^/]+)/(-/)?issues$")
.unwrap()
});
static R_BIT_BUCKET_PATH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^/(?P<user>[^/]+)/(?P<repo>[^/]+)/issues$").unwrap());
let url = check_public_url(environment, value, false, false)?;
let hosting_type = eval_hosting_type(environment, &url);
let host_reg: Option<&Regex> = match hosting_type {
HostingType::GitHub => Some(&R_GIT_HUB_PATH),
HostingType::GitLab => Some(&R_GIT_LAB_PATH),
HostingType::BitBucket => Some(&R_BIT_BUCKET_PATH),
_ => None, };
check_url_path(value, "issues", &url, host_reg)
}
fn validate_build_hosting_url(environment: &mut Environment, value: &str) -> Result {
static R_GIT_HUB_HOST: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(?P<user>[^/.]+)\.github\.io$").unwrap());
static R_GIT_LAB_HOST: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(?P<user>[^/.]+)\.gitlab\.io$").unwrap());
let url = check_public_url(environment, value, false, false)?;
let hosting_type = eval_hosting_type_from_hosting_suffix(environment, &url);
let host_reg: Option<&Regex> = match hosting_type {
HostingType::GitHub => Some(&R_GIT_HUB_HOST),
HostingType::GitLab => Some(&R_GIT_LAB_HOST),
_ => None, };
check_url_host(value, "build hosting", &url, host_reg)
}
fn validate_name(environment: &mut Environment, value: &str) -> Result {
check_empty(environment, value, "Project name (human-readable)")
}
fn validate_name_machine_readable(environment: &mut Environment, value: &str) -> Result {
static R_MACHINE_READABLE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[0-9a-zA-Z_-]+$").unwrap());
check_empty(environment, value, "Project name (machine-readable)")?;
if R_MACHINE_READABLE.is_match(value) {
Ok(Validity::High {
msg: Some(format!("Matches regex '{}'", R_MACHINE_READABLE.as_str())),
})
} else {
Err(Error::BadValue {
msg: format!(
"Name is not machine-readable, does not match '{}'",
R_MACHINE_READABLE.as_str()
),
value: value.to_owned(),
})
}
}
fn check_date(environment: &mut Environment, value: &str, date_desc: &str) -> Result {
if value.is_empty() {
return Err(Error::BadValue {
msg: format!("{date_desc} date can not be empty"),
value: value.to_owned(),
});
}
let parse_err = NaiveDateTime::parse_from_str(value, &environment.settings.date_format)
.err()
.and_then(|_err| DateTime::parse_from_str(value, &environment.settings.date_format).err());
if let Some(err) = parse_err {
Err(Error::BadValue {
msg: format!(
r#"Not a {} date according to the date-format "{}": {}"#,
date_desc, environment.settings.date_format, err
),
value: value.to_owned(),
})
} else {
Ok(Validity::High {
msg: Some(format!(
"Matches the date format '{}'",
environment.settings.date_format
)),
})
}
}
fn validate_version_date(environment: &mut Environment, value: &str) -> Result {
check_date(environment, value, "version")
}
fn validate_build_date(environment: &mut Environment, value: &str) -> Result {
check_date(environment, value, "build")
}
fn validate_build_branch(environment: &mut Environment, value: &str) -> Result {
check_empty(environment, value, "Branch")
}
fn validate_build_tag(environment: &mut Environment, value: &str) -> Result {
check_empty(environment, value, "Tag")
}
fn validate_build_os(environment: &mut Environment, value: &str) -> Result {
check_empty(environment, value, "Build OS") }
fn validate_build_os_family(environment: &mut Environment, value: &str) -> Result {
check_empty(environment, value, "Build OS Family")?;
if constants::VALID_OS_FAMILIES.contains(&value) {
Ok(Validity::High { msg: None })
} else {
Err(Error::BadValue {
msg: format!(
"Only these values are valid: {}",
constants::VALID_OS_FAMILIES.join(", ")
),
value: value.to_owned(),
})
}
}
fn validate_build_arch(environment: &mut Environment, value: &str) -> Result {
check_empty(environment, value, "Build arch")?;
if constants::VALID_ARCHS.contains(&value) {
Ok(Validity::High { msg: None })
} else {
Err(Error::BadValue {
msg: format!(
"Only these values are valid: {}",
constants::VALID_ARCHS.join(", ")
),
value: value.to_owned(),
})
}
}
fn validate_build_number(environment: &mut Environment, value: &str) -> Result {
check_empty(environment, value, "Build number")?;
match value.parse::<i32>() {
Err(_err) => Ok(Validity::Suboptimal {
msg: "It is generally recommended and assumed that the build number is an integer (a positive, whole number)".to_owned(),
source: None,
}),
Ok(_int_value) => Ok(Validity::High { msg: Some("Is a build number (positive integer)".to_owned()) })
}
}
fn validate_ci(environment: &mut Environment, value: &str) -> Result {
check_empty(environment, value, "CI")?;
match value {
"true" => Ok(Validity::High { msg: None }),
"false" => Ok(Validity::Middle {
msg: "Nothing wrong with that, but any 'true' value will get prefference over 'false'"
.to_owned(),
}),
&_ => Err(Error::BadValue {
msg:
r"CI can be 'true', 'false' or be ommitted (None), which get interpreted as 'false'"
.to_owned(),
value: value.to_owned(),
}),
}
}
#[remain::check]
#[must_use]
pub fn get(key: Key) -> Validator {
#[remain::sorted]
match key {
Key::BuildArch => validate_build_arch,
Key::BuildBranch => validate_build_branch,
Key::BuildDate => validate_build_date,
Key::BuildHostingUrl => validate_build_hosting_url,
Key::BuildNumber => validate_build_number,
Key::BuildOs => validate_build_os,
Key::BuildOsFamily => validate_build_os_family,
Key::BuildTag => validate_build_tag,
Key::Ci => validate_ci,
Key::License => validate_license,
Key::Licenses => validate_licenses,
Key::Name => validate_name,
Key::NameMachineReadable => validate_name_machine_readable,
Key::RepoCloneUrl => validate_repo_clone_url,
Key::RepoCloneUrlGit => validate_repo_clone_url_git,
Key::RepoCloneUrlHttp => validate_repo_clone_url_http,
Key::RepoCloneUrlSsh => validate_repo_clone_url_ssh,
Key::RepoCommitPrefixUrl => validate_repo_commit_prefix_url,
Key::RepoIssuesUrl => validate_repo_issues_url,
Key::RepoRawVersionedPrefixUrl => validate_repo_raw_versioned_prefix_url,
Key::RepoVersionedDirPrefixUrl => validate_repo_versioned_dir_prefix_url,
Key::RepoVersionedFilePrefixUrl => validate_repo_versioned_file_prefix_url,
Key::RepoWebUrl => validate_repo_web_url,
Key::Version => validate_version,
Key::VersionDate => validate_version_date,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_good(res: Result) -> bool {
if let Ok(val) = res {
val.is_good()
} else {
false
}
}
fn is_high(res: Result) -> bool {
if let Ok(val) = res {
matches!(&val, &Validity::High { .. })
} else {
false
}
}
fn is_middle(res: Result) -> bool {
if let Ok(val) = res {
matches!(&val, &Validity::Middle { .. })
} else {
false
}
}
fn is_low(res: Result) -> bool {
if let Ok(val) = res {
matches!(&val, &Validity::Low { .. })
} else {
false
}
}
fn is_suboptimal(res: Result) -> bool {
if let Ok(val) = res {
matches!(&val, &Validity::Suboptimal { .. })
} else {
false
}
}
fn is_missing_err(res: Result) -> bool {
if let Err(err) = res {
matches!(&err, &Error::Missing { .. })
} else {
false
}
}
fn is_bad_value(res: Result) -> bool {
if let Err(err) = res {
matches!(&err, &Error::BadValue { .. })
} else {
false
}
}
#[test]
fn test_validate_version() {
let mut environment = Environment::stub();
let full_sha = "cf73ea34fcc785b1ac44ffb20d655c917e77c83d";
for sha_length in 7..full_sha.len() {
assert!(is_suboptimal(validate_version(
&mut environment,
&full_sha[0..sha_length],
)));
}
assert!(is_suboptimal(validate_version(
&mut environment,
"gad8f844"
)));
assert!(is_low(validate_version(&mut environment, "gad8f844-dirty")));
assert!(is_suboptimal(validate_version(
&mut environment,
"gad8f844-broken"
)));
assert!(is_suboptimal(validate_version(
&mut environment,
"gad8f844-dirty-broken"
)));
assert!(is_suboptimal(validate_version(
&mut environment,
"gad8f844-broken-dirty"
)));
assert!(is_middle(validate_version(
&mut environment,
"0.1.19-12-gad8f844"
)));
assert!(is_middle(validate_version(
&mut environment,
"0.1.19-12-gad8f844-dirty"
)));
assert!(is_suboptimal(validate_version(
&mut environment,
"0.1.19-12-gad8f844-broken"
)));
assert!(is_suboptimal(validate_version(
&mut environment,
"0.1.19-12-gad8f844-dirty-broken"
)));
assert!(is_suboptimal(validate_version(
&mut environment,
"0.1.19-12-gad8f844-broken-dirty"
)));
assert!(is_good(validate_version(&mut environment, "0.1.19")));
assert!(is_middle(validate_version(
&mut environment,
"0.1.19-dirty"
)));
assert!(is_suboptimal(validate_version(
&mut environment,
"0.1.19-broken"
)));
assert!(is_suboptimal(validate_version(
&mut environment,
"0.1.19-dirty-broken"
)));
assert!(is_suboptimal(validate_version(
&mut environment,
"0.1.19-broken-dirty"
)));
assert!(is_missing_err(validate_version(&mut environment, "")));
assert!(is_low(validate_version(&mut environment, "gabcdefg")));
assert!(is_low(validate_version(
&mut environment,
"din-spec-3105-0.10.0-202-g9b5ff47"
)));
}
#[test]
fn test_validate_license() {
let mut environment = Environment::stub();
assert!(is_good(validate_license(&mut environment, "GPL-3.0")));
assert!(is_high(validate_license(&mut environment, "GPL-3.0")));
assert!(is_good(validate_license(
&mut environment,
"GPL-3.0-or-later"
)));
assert!(is_good(validate_license(&mut environment, "GPL-2.0")));
assert!(is_good(validate_license(
&mut environment,
"GPL-2.0-or-later"
)));
assert!(is_good(validate_license(&mut environment, "AGPL-3.0")));
assert!(is_good(validate_license(
&mut environment,
"AGPL-3.0-or-later"
)));
assert!(is_good(validate_license(&mut environment, "CC0-1.0")));
assert!(is_low(validate_license(&mut environment, "CC0-1.0")));
assert!(is_suboptimal(validate_license(&mut environment, "CC0-2.0")));
assert!(is_suboptimal(validate_license(&mut environment, "CC02.0")));
assert!(is_suboptimal(validate_license(&mut environment, "GPL")));
assert!(is_suboptimal(validate_license(&mut environment, "AGPL")));
assert!(is_suboptimal(validate_license(
&mut environment,
"Some Unknown License"
)));
assert!(is_missing_err(validate_license(&mut environment, "")));
}
#[test]
fn test_validate_repo_versioned_dir_prefix_url() -> std::result::Result<(), Error> {
let mut environment = Environment::stub();
validate_repo_versioned_dir_prefix_url(
&mut environment,
"https://github.com/hoijui/projvar/tree",
)?;
Ok(())
}
}