Skip to main content

running_process/daemon_registration/
validation.rs

1//! Shared validation for frozen v1 registration records.
2
3/// Errors that prevent using a valid registration name or broker path.
4///
5/// The legacy broker's pipe helpers re-export this exact type when `client`
6/// is selected. Registration itself only needs the `InvalidName` form.
7#[derive(Debug, thiserror::Error)]
8pub enum PipePathError {
9    /// A name argument failed validation.
10    #[error("invalid name {name:?}: {reason}")]
11    InvalidName {
12        /// The offending input.
13        name: String,
14        /// Why it was rejected.
15        reason: &'static str,
16    },
17
18    /// The derived path exceeds a platform-specific bound.
19    #[error("derived path exceeds {limit_label} ({len} > {max})")]
20    PathTooLong {
21        /// Length we tried to produce.
22        len: usize,
23        /// Platform-specific cap.
24        max: usize,
25        /// "Windows MAX_PATH" / "macOS sun_path" / etc.
26        limit_label: &'static str,
27    },
28
29    /// Failure to compute the per-user SID hash for a legacy broker pipe.
30    #[cfg(feature = "client")]
31    #[error(transparent)]
32    Sid(#[from] crate::broker::lifecycle::sid::SidError),
33}
34
35/// Validate a service name against `[a-z0-9-]{1,64}`.
36pub 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/// Validate a semver-like version string against
72/// `^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.]+)?$`.
73#[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}