running_process/daemon_registration/
validation.rs1#[derive(Debug, thiserror::Error)]
8pub enum PipePathError {
9 #[error("invalid name {name:?}: {reason}")]
11 InvalidName {
12 name: String,
14 reason: &'static str,
16 },
17
18 #[error("derived path exceeds {limit_label} ({len} > {max})")]
20 PathTooLong {
21 len: usize,
23 max: usize,
25 limit_label: &'static str,
27 },
28
29 #[cfg(feature = "client")]
31 #[error(transparent)]
32 Sid(#[from] crate::broker::lifecycle::sid::SidError),
33}
34
35pub fn validate_service_name(name: &str) -> Result<(), PipePathError> {
37 if name.is_empty() {
38 return Err(PipePathError::InvalidName {
39 name: name.into(),
40 reason: "service name must be at least 1 character",
41 });
42 }
43 if name.len() > 64 {
44 return Err(PipePathError::InvalidName {
45 name: name.into(),
46 reason: "service name must be 64 characters or fewer",
47 });
48 }
49 for character in name.chars() {
50 match character {
51 'a'..='z' | '0'..='9' | '-' => {}
52 'A'..='Z' => {
53 return Err(PipePathError::InvalidName {
54 name: name.into(),
55 reason: "uppercase letters are forbidden (case-only \
56 collisions with lowercase names would silently \
57 merge under Windows named-pipe semantics)",
58 });
59 }
60 _ => {
61 return Err(PipePathError::InvalidName {
62 name: name.into(),
63 reason: "only lowercase ASCII letters, digits, and '-' allowed",
64 });
65 }
66 }
67 }
68 Ok(())
69}
70
71#[cfg(feature = "daemon-registration")]
74pub fn validate_version(version: &str) -> Result<(), PipePathError> {
75 if version.is_empty() {
76 return Err(PipePathError::InvalidName {
77 name: version.into(),
78 reason: "version must not be empty",
79 });
80 }
81 let (core, prerelease) = match version.split_once('-') {
82 Some((core, tail)) => (core, Some(tail)),
83 None => (version, None),
84 };
85 let parts: Vec<&str> = core.split('.').collect();
86 if parts.len() != 3 {
87 return Err(PipePathError::InvalidName {
88 name: version.into(),
89 reason: "version core must be MAJOR.MINOR.PATCH",
90 });
91 }
92 for part in &parts {
93 if part.is_empty() || !part.chars().all(|character| character.is_ascii_digit()) {
94 return Err(PipePathError::InvalidName {
95 name: version.into(),
96 reason: "MAJOR/MINOR/PATCH must be non-empty digits",
97 });
98 }
99 }
100 if let Some(tail) = prerelease {
101 if tail.is_empty() {
102 return Err(PipePathError::InvalidName {
103 name: version.into(),
104 reason: "pre-release suffix after '-' must not be empty",
105 });
106 }
107 for character in tail.chars() {
108 match character {
109 'a'..='z' | '0'..='9' | '.' => {}
110 _ => {
111 return Err(PipePathError::InvalidName {
112 name: version.into(),
113 reason: "pre-release tail allows only [a-z0-9.]",
114 });
115 }
116 }
117 }
118 }
119 Ok(())
120}