agent_skill_installer/
name.rs1use std::fmt;
19
20const MAX_LEN: usize = 64;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum NameError {
27 Empty,
29 TooLong(usize),
31 InvalidChar(char),
33 EdgeHyphen,
35 DoubleHyphen,
37}
38
39impl fmt::Display for NameError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::Empty => f.write_str("a skill name cannot be empty"),
43 Self::TooLong(len) => {
44 write!(
45 f,
46 "a skill name has at most {MAX_LEN} characters, this one has {len}"
47 )
48 }
49 Self::InvalidChar(c) => {
50 write!(
51 f,
52 "a skill name may only contain a-z, 0-9 and '-', not {c:?}"
53 )
54 }
55 Self::EdgeHyphen => f.write_str("a skill name cannot start or end with '-'"),
56 Self::DoubleHyphen => f.write_str("a skill name cannot contain '--'"),
57 }
58 }
59}
60
61impl std::error::Error for NameError {}
62
63pub fn validate_name(name: &str) -> Result<(), NameError> {
68 if name.is_empty() {
69 return Err(NameError::Empty);
70 }
71 let len = name.chars().count();
74 if len > MAX_LEN {
75 return Err(NameError::TooLong(len));
76 }
77 if let Some(c) = name
78 .chars()
79 .find(|c| !(c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '-'))
80 {
81 return Err(NameError::InvalidChar(c));
82 }
83 if name.starts_with('-') || name.ends_with('-') {
84 return Err(NameError::EdgeHyphen);
85 }
86 if name.contains("--") {
87 return Err(NameError::DoubleHyphen);
88 }
89 Ok(())
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn names_the_specification_allows_pass() {
98 let longest = "a".repeat(64);
99 for name in [
100 "devserial",
101 "devknx",
102 "a",
103 "pdf-processing",
104 "x2",
105 longest.as_str(),
106 ] {
107 assert_eq!(validate_name(name), Ok(()), "{name}");
108 }
109 }
110
111 #[test]
112 fn each_rule_refuses_for_its_own_reason() {
113 assert_eq!(validate_name(""), Err(NameError::Empty));
114 assert_eq!(validate_name(&"a".repeat(65)), Err(NameError::TooLong(65)));
115 assert_eq!(validate_name("DevSerial"), Err(NameError::InvalidChar('D')));
116 assert_eq!(
117 validate_name("dev_serial"),
118 Err(NameError::InvalidChar('_'))
119 );
120 assert_eq!(validate_name("-dev"), Err(NameError::EdgeHyphen));
121 assert_eq!(validate_name("dev-"), Err(NameError::EdgeHyphen));
122 assert_eq!(validate_name("dev--serial"), Err(NameError::DoubleHyphen));
123 }
124
125 #[test]
127 fn nothing_that_could_leave_the_directory_passes() {
128 for name in [
129 "..", "../x", "a/b", "a\\b", ".hidden", "a.b", "รค", "a b", "a\0b",
130 ] {
131 assert!(validate_name(name).is_err(), "{name:?} must be refused");
132 }
133 }
134}