Skip to content

Commit

Permalink
feat: make environment name parsing strict (#673)
Browse files Browse the repository at this point in the history
  • Loading branch information
ruben-arts authored Jan 17, 2024
1 parent 73c5bf1 commit 73e97ce
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ indicatif = "0.17.7"
insta = { version = "1.34.0", features = ["yaml"] }
is_executable = "1.0.1"
itertools = "0.12.0"
lazy_static = "1.4.0"
miette = { version = "5.10.0", features = ["fancy", "supports-color", "supports-hyperlinks", "supports-unicode", "terminal_size", "textwrap"] }
minijinja = { version = "1.0.11", features = ["builtins"] }
once_cell = "1.19.0"
Expand Down
100 changes: 96 additions & 4 deletions src/project/manifest/environment.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
use crate::consts;
use crate::utils::spanned::PixiSpanned;
use lazy_static::lazy_static;
use miette::Diagnostic;
use regex::Regex;
use serde::{self, Deserialize, Deserializer};
use std::borrow::Borrow;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use thiserror::Error;

/// The name of an environment. This is either a string or default for the default environment.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -33,16 +38,40 @@ impl Borrow<str> for EnvironmentName {
self.as_str()
}
}
#[derive(Debug, Clone, Error, Diagnostic, PartialEq)]
#[error("Failed to parse environment name '{attempted_parse}', please use only lowercase letters, numbers and dashes")]
pub struct ParseEnvironmentNameError {
/// The string that was attempted to be parsed.
pub attempted_parse: String,
}

impl FromStr for EnvironmentName {
type Err = ParseEnvironmentNameError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static! {
static ref REGEX: Regex = Regex::new(r"^[a-z0-9-]+$").expect("Regex should be able to compile"); // Compile the regex
}

if !REGEX.is_match(s) {
// Return an error if the string does not match the regex
return Err(ParseEnvironmentNameError {
attempted_parse: s.to_string(),
});
}
match s {
consts::DEFAULT_ENVIRONMENT_NAME => Ok(EnvironmentName::Default),
_ => Ok(EnvironmentName::Named(s.to_string())),
}
}
}

impl<'de> Deserialize<'de> for EnvironmentName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
match String::deserialize(deserializer)? {
name if name == consts::DEFAULT_ENVIRONMENT_NAME => Ok(EnvironmentName::Default),
name => Ok(EnvironmentName::Named(name)),
}
let name = String::deserialize(deserializer)?;
EnvironmentName::from_str(&name).map_err(serde::de::Error::custom)
}
}

Expand Down Expand Up @@ -115,3 +144,66 @@ impl<'de> Deserialize<'de> for TomlEnvironmentMapOrSeq {
.deserialize(deserializer)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_environment_name_from_str() {
assert_eq!(
EnvironmentName::from_str("default").unwrap(),
EnvironmentName::Default
);
assert_eq!(
EnvironmentName::from_str("foo").unwrap(),
EnvironmentName::Named("foo".to_string())
);
assert_eq!(
EnvironmentName::from_str("foo_bar").unwrap_err(),
ParseEnvironmentNameError {
attempted_parse: "foo_bar".to_string()
}
);

assert!(EnvironmentName::from_str("foo-bar").is_ok());
assert!(EnvironmentName::from_str("foo1").is_ok());
assert!(EnvironmentName::from_str("py39").is_ok());

assert!(EnvironmentName::from_str("foo bar").is_err());
assert!(EnvironmentName::from_str("foo_bar").is_err());
assert!(EnvironmentName::from_str("foo/bar").is_err());
assert!(EnvironmentName::from_str("foo\\bar").is_err());
assert!(EnvironmentName::from_str("foo:bar").is_err());
assert!(EnvironmentName::from_str("foo;bar").is_err());
assert!(EnvironmentName::from_str("foo?bar").is_err());
assert!(EnvironmentName::from_str("foo!bar").is_err());
assert!(EnvironmentName::from_str("py3.9").is_err());
assert!(EnvironmentName::from_str("py-3.9").is_err());
assert!(EnvironmentName::from_str("py_3.9").is_err());
assert!(EnvironmentName::from_str("py/3.9").is_err());
assert!(EnvironmentName::from_str("py\\3.9").is_err());
assert!(EnvironmentName::from_str("Py").is_err());
assert!(EnvironmentName::from_str("Py3").is_err());
assert!(EnvironmentName::from_str("Py39").is_err());
assert!(EnvironmentName::from_str("Py-39").is_err());
}

#[test]
fn test_environment_name_as_str() {
assert_eq!(EnvironmentName::Default.as_str(), "default");
assert_eq!(EnvironmentName::Named("foo".to_string()).as_str(), "foo");
}

#[test]
fn test_deserialize_environment_name() {
assert_eq!(
serde_json::from_str::<EnvironmentName>("\"default\"").unwrap(),
EnvironmentName::Default
);
assert_eq!(
serde_json::from_str::<EnvironmentName>("\"foo\"").unwrap(),
EnvironmentName::Named("foo".to_string())
);
}
}

0 comments on commit 73e97ce

Please sign in to comment.