1use crate::error::StorageError;
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
16pub struct WorktreeName(String);
17
18impl WorktreeName {
19 pub fn parse(raw: &str) -> Result<WorktreeName, StorageError> {
24 let refuse = |detail: &str| StorageError::Worktree {
25 op: "name a worktree",
26 detail: format!("{raw}: {detail}"),
27 };
28 if raw.is_empty() {
29 return Err(refuse("a tree with no name cannot be released either"));
30 }
31 if raw.starts_with('.') {
32 return Err(refuse("names do not start with a dot"));
33 }
34 if !raw
35 .chars()
36 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
37 {
38 return Err(refuse("letters, digits, dot, underscore and dash only"));
39 }
40 Ok(WorktreeName(raw.to_owned()))
41 }
42
43 #[must_use]
44 pub fn as_str(&self) -> &str {
45 &self.0
46 }
47}
48
49#[cfg(test)]
50#[allow(
51 clippy::unwrap_used,
52 clippy::expect_used,
53 clippy::panic,
54 clippy::indexing_slicing,
55 reason = "test code"
56)]
57mod tests {
58 use super::*;
59 #[test]
60 fn a_name_that_could_walk_out_of_its_directory_is_not_a_name() {
61 for raw in ["", ".hidden", "../escape", "node/1", "node 1", "nöde"] {
62 assert!(
63 WorktreeName::parse(raw).is_err(),
64 "{raw:?} is not a worktree name"
65 );
66 }
67 assert_eq!(
68 WorktreeName::parse("node-1.2_3").unwrap().as_str(),
69 "node-1.2_3"
70 );
71 }
72}