use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileName(String);
impl ProfileName {
pub fn parse(name: &str) -> Result<ProfileName, ProfileNameError> {
let name = name.trim();
if name.is_empty() {
return Err(ProfileNameError::new("profile name must not be empty"));
}
if name == "." || name == ".." {
return Err(ProfileNameError::new(
"profile name must not be `.` or `..`",
));
}
if name.contains(['/', '\\']) || name.contains('\0') {
return Err(ProfileNameError::new(
"profile name must not contain path separators",
));
}
let mut components = Path::new(name).components();
match (components.next(), components.next()) {
(Some(std::path::Component::Normal(part)), None)
if part == std::ffi::OsStr::new(name) => {}
_ => {
return Err(ProfileNameError::new(
"profile name must be a single path component",
));
}
}
Ok(ProfileName(name.to_owned()))
}
#[must_use]
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ProfileName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[non_exhaustive]
pub struct ProfileNameError {
reason: &'static str,
}
impl ProfileNameError {
fn new(reason: &'static str) -> ProfileNameError {
ProfileNameError { reason }
}
}
impl std::fmt::Debug for ProfileNameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProfileNameError")
.field("reason", &self.reason)
.finish()
}
}
impl std::fmt::Display for ProfileNameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.reason)
}
}
impl std::error::Error for ProfileNameError {}
#[cfg(test)]
mod tests {
use super::ProfileName;
#[test]
fn accepts_simple_names() {
assert_eq!(ProfileName::parse("dev").unwrap().as_str(), "dev");
assert_eq!(ProfileName::parse(" prod ").unwrap().as_str(), "prod");
}
#[test]
fn rejects_traversal_and_separators() {
for bad in ["", ".", "..", "a/b", "a\\b", "../x", "x\0y"] {
assert!(ProfileName::parse(bad).is_err(), "should reject {bad:?}");
}
}
}