isoloom_spec/syntax/step/
words.rs1use std::{borrow::Cow, fmt, str::FromStr};
8
9use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11
12use super::super::names::{Text, name_type};
13use crate::Error;
14
15name_type!(
16 UnitName,
19 "unit name",
20 "^[A-Za-z0-9][A-Za-z0-9:_.@-]{0,127}$",
21 "a unit is letters, digits and `:_.@-`, starting with a letter or digit, at most 128 characters (`nginx`, `php8.2-fpm`)",
22 |s| s.len() <= 128 && s.starts_with(|c: char| c.is_ascii_alphanumeric()) && s.chars().all(|c| c.is_ascii_alphanumeric() || ":_.@-".contains(c))
23);
24
25name_type!(
26 UserName,
28 "user name",
29 "^[A-Za-z_][A-Za-z0-9_.-]{0,31}$",
30 "a user is letters, digits and `_.-`, starting with a letter or `_`, at most 32 characters (`www-data`)",
31 |s| s.len() <= 32 && s.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') && s.chars().all(|c| c.is_ascii_alphanumeric() || "_.-".contains(c))
32);
33
34name_type!(
35 GroupName,
37 "group name",
38 "^[A-Za-z_]([A-Za-z0-9_. -]{0,62}[A-Za-z0-9_.-])?$",
39 "a group is letters, digits, `_.-` and inner spaces, starting with a letter or `_`, at most 64 characters (`www-data`, `Remote Desktop Users`)",
40 |s| s.len() <= 64
41 && s.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
42 && !s.ends_with(' ')
43 && s.chars().all(|c| c.is_ascii_alphanumeric() || "_.- ".contains(c))
44);
45
46name_type!(
47 PackageName,
50 "package name",
51 "^[A-Za-z0-9][A-Za-z0-9+._:-]{0,127}$",
52 "a package is letters, digits and `+._:-`, starting with a letter or digit, at most 128 characters (`nginx`); its version goes in `version`",
53 |s| s.len() <= 128 && s.starts_with(|c: char| c.is_ascii_alphanumeric()) && s.chars().all(|c| c.is_ascii_alphanumeric() || "+._:-".contains(c))
54);
55
56name_type!(
57 RepositoryName,
60 "repository name",
61 "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
62 "a repository's name is kebab-case, at most 63 characters (`nginx`, `pgdg`)",
63 |s| s.len() <= 63
64 && s.starts_with(|c: char| c.is_ascii_lowercase())
65 && !s.ends_with('-')
66 && !s.contains("--")
67 && s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
68);
69
70name_type!(
71 AbsPath,
74 "path in the machine",
75 "^(/|[A-Za-z]:[\\\\/])[^\\n]*$",
76 "a path in the machine is absolute (`/etc/app.toml`), or a drive path on Windows (`C:\\app`)",
77 |s| (s.starts_with('/') || drive(s)) && !s.contains(['\n', '\0'])
78);
79
80name_type!(
81 ProjectPath,
84 "project path",
85 "^(?![/\\\\]|[A-Za-z]:[/\\\\])(?!(.*[/\\\\])?\\.\\.([/\\\\]|$))[^\\n]+$",
86 "a project path is relative to the project folder and stays inside it: no leading `/`, no `..` (`provision/web.sh`)",
87 |s| !s.is_empty() && !s.starts_with(['/', '\\']) && !drive(s) && !s.split(['/', '\\']).any(|part| part == "..") && !s.contains(['\n', '\0'])
88);
89
90name_type!(
91 HttpsUrl,
93 "URL",
94 "^https://[^\\s/]+\\S*$",
95 "a URL is `https://<host>/<path>`, with no spaces: every fetch goes over TLS",
96 |s| s
97 .strip_prefix("https://")
98 .is_some_and(|rest| !rest.starts_with('/') && !rest.is_empty() && !rest.contains(char::is_whitespace))
99);
100
101name_type!(
102 Sha256,
104 "SHA-256",
105 "^[0-9a-f]{64}$",
106 "a SHA-256 is 64 lowercase hex digits, as `sha256sum` prints it",
107 |s| s.len() == 64 && s.chars().all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
108);
109
110name_type!(
111 AptWord,
113 "apt suite or component",
114 "^[A-Za-z0-9][A-Za-z0-9._/-]{0,63}$",
115 "a suite or component is letters, digits and `._/-`, starting with a letter or digit (`bookworm`, `main`)",
116 |s| s.len() <= 64 && s.starts_with(|c: char| c.is_ascii_alphanumeric()) && s.chars().all(|c| c.is_ascii_alphanumeric() || "._/-".contains(c))
117);
118
119fn drive(s: &str) -> bool {
121 matches!(s.as_bytes(), [d, b':', b'\\' | b'/', ..] if d.is_ascii_alphabetic())
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
127pub struct PackageVersion(String);
128
129impl PackageVersion {
130 #[must_use]
132 pub fn as_str(&self) -> &str {
133 &self.0
134 }
135}
136
137impl FromStr for PackageVersion {
138 type Err = Error;
139
140 fn from_str(value: &str) -> Result<PackageVersion, Error> {
146 let ok = value.len() <= 128
147 && value.starts_with(|c: char| c.is_ascii_alphanumeric())
148 && value.chars().all(|c| c.is_ascii_alphanumeric() || ".+~:_-".contains(c));
149 if ok {
150 Ok(PackageVersion(value.to_owned()))
151 } else {
152 Err(Error::Name {
153 what: "package version",
154 value: value.to_owned(),
155 rule: "a version is letters, digits and `.+~:_-`, as the package manager writes it (`1.30.6-1~bookworm`)",
156 })
157 }
158 }
159}
160
161impl fmt::Display for PackageVersion {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 f.write_str(&self.0)
164 }
165}
166
167impl AsRef<str> for PackageVersion {
168 fn as_ref(&self) -> &str {
169 &self.0
170 }
171}
172
173impl Serialize for PackageVersion {
174 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
175 s.serialize_str(&self.0)
176 }
177}
178
179impl<'de> Deserialize<'de> for PackageVersion {
180 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<PackageVersion, D::Error> {
181 d.deserialize_any(VersionText)?.parse().map_err(serde::de::Error::custom)
182 }
183}
184
185struct VersionText;
187
188impl serde::de::Visitor<'_> for VersionText {
189 type Value = String;
190
191 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
192 f.write_str("a version, quoted (`\"1.30.6-1~bookworm\"`)")
193 }
194
195 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<String, E> {
196 Ok(v.to_owned())
197 }
198
199 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<String, E> {
200 Ok(v.to_string())
201 }
202
203 fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<String, E> {
204 Err(E::custom(format!("quote the version (`\"{v}\"`): read as a number, it loses its zeros")))
205 }
206}
207
208impl JsonSchema for PackageVersion {
209 fn schema_name() -> Cow<'static, str> {
210 "PackageVersion".into()
211 }
212
213 fn inline_schema() -> bool {
214 true
215 }
216
217 fn json_schema(_: &mut SchemaGenerator) -> Schema {
218 json_schema!({ "type": ["string", "integer"], "pattern": "^[A-Za-z0-9][A-Za-z0-9.+~:_-]{0,127}$" })
219 }
220}
221
222#[cfg(test)]
223mod tests;