pub use boatramp_types::project::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ProjectRef<'a>(&'a str);
impl<'a> ProjectRef<'a> {
pub const DEFAULT: ProjectRef<'static> = ProjectRef(DEFAULT_PROJECT);
pub fn new(name: &'a str) -> Self {
ProjectRef(name)
}
pub fn as_str(&self) -> &'a str {
self.0
}
pub fn qualified(&self, base: &str) -> String {
if self.0 == DEFAULT_PROJECT {
base.to_string()
} else {
format!("{}/{base}", self.0)
}
}
}
impl<'a> From<&'a str> for ProjectRef<'a> {
fn from(name: &'a str) -> Self {
ProjectRef(name)
}
}
impl std::fmt::Display for ProjectRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid {kind} name {value:?}: {reason}")]
pub struct InvalidResourceName {
pub kind: &'static str,
pub value: String,
pub reason: &'static str,
}
pub fn validate_resource_name(kind: &'static str, value: &str) -> Result<(), InvalidResourceName> {
let reject = |reason| {
Err(InvalidResourceName {
kind,
value: value.to_string(),
reason,
})
};
if value.is_empty() {
return reject("must not be empty");
}
if value == "." || value == ".." {
return reject("must not be '.' or '..'");
}
for c in value.chars() {
match c {
'/' | '\\' => return reject("must not contain a path separator ('/' or '\\')"),
'*' => return reject("must not contain '*'"),
c if c.is_whitespace() => return reject("must not contain whitespace"),
c if c.is_control() => return reject("must not contain control characters"),
_ => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn project_ref_default_and_wrap() {
assert_eq!(ProjectRef::DEFAULT.as_str(), "default");
assert_eq!(ProjectRef::new("acme").as_str(), "acme");
assert_eq!(ProjectRef::from("shop").to_string(), "shop");
}
#[test]
fn resource_name_validation_rejects_the_dangerous_shapes() {
for ok in ["blog", "my-site", "resize_v2", "a.b", "Blog9"] {
assert!(
validate_resource_name("site", ok).is_ok(),
"{ok} should pass"
);
}
for bad in [
"",
".",
"..",
"a/b",
"a\\b",
"blog/../evil",
"*",
"proj*",
"a b",
"tab\tname",
"ctl\u{0}name",
] {
assert!(
validate_resource_name("site", bad).is_err(),
"{bad:?} should be rejected"
);
}
}
}