1use crate::Result;
2use crate::daemon_status::DaemonStatus;
3use crate::error::DaemonIdError;
4use crate::pitchfork_toml::CronRetrigger;
5use std::fmt::Display;
6use std::path::PathBuf;
7
8pub fn is_valid_daemon_id(id: &str) -> bool {
21 !id.is_empty()
22 && !id.contains('/')
23 && !id.contains('\\')
24 && !id.contains("..")
25 && !id.contains(' ')
26 && id != "."
27 && id.chars().all(|c| c.is_ascii() && !c.is_ascii_control())
28}
29
30pub fn validate_daemon_id(id: &str) -> Result<()> {
32 if id.is_empty() {
33 return Err(DaemonIdError::Empty.into());
34 }
35 if let Some(sep) = id.chars().find(|&c| c == '/' || c == '\\') {
36 return Err(DaemonIdError::PathSeparator {
37 id: id.to_string(),
38 sep,
39 }
40 .into());
41 }
42 if id.contains("..") {
43 return Err(DaemonIdError::ParentDirRef { id: id.to_string() }.into());
44 }
45 if id.contains(' ') {
46 return Err(DaemonIdError::ContainsSpace { id: id.to_string() }.into());
47 }
48 if id == "." {
49 return Err(DaemonIdError::CurrentDir.into());
50 }
51 if !id.chars().all(|c| c.is_ascii() && !c.is_ascii_control()) {
52 return Err(DaemonIdError::InvalidChars { id: id.to_string() }.into());
53 }
54 Ok(())
55}
56
57#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
58pub struct Daemon {
59 pub id: String,
60 pub title: Option<String>,
61 pub pid: Option<u32>,
62 pub shell_pid: Option<u32>,
63 pub status: DaemonStatus,
64 pub dir: Option<PathBuf>,
65 pub autostop: bool,
66 #[serde(skip_serializing_if = "Option::is_none", default)]
67 pub cron_schedule: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none", default)]
69 pub cron_retrigger: Option<CronRetrigger>,
70 #[serde(skip_serializing_if = "Option::is_none", default)]
71 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
72 #[serde(skip_serializing_if = "Option::is_none", default)]
73 pub last_exit_success: Option<bool>,
74 #[serde(default)]
75 pub retry: u32,
76 #[serde(default)]
77 pub retry_count: u32,
78 #[serde(skip_serializing_if = "Option::is_none", default)]
79 pub ready_delay: Option<u64>,
80 #[serde(skip_serializing_if = "Option::is_none", default)]
81 pub ready_output: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none", default)]
83 pub ready_http: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none", default)]
85 pub ready_port: Option<u16>,
86 #[serde(skip_serializing_if = "Vec::is_empty", default)]
87 pub depends: Vec<String>,
88}
89
90#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
91pub struct RunOptions {
92 pub id: String,
93 pub cmd: Vec<String>,
94 pub force: bool,
95 pub shell_pid: Option<u32>,
96 pub dir: PathBuf,
97 pub autostop: bool,
98 pub cron_schedule: Option<String>,
99 pub cron_retrigger: Option<CronRetrigger>,
100 pub retry: u32,
101 pub retry_count: u32,
102 pub ready_delay: Option<u64>,
103 pub ready_output: Option<String>,
104 pub ready_http: Option<String>,
105 pub ready_port: Option<u16>,
106 pub wait_ready: bool,
107 #[serde(skip_serializing_if = "Vec::is_empty", default)]
108 pub depends: Vec<String>,
109}
110
111impl Display for Daemon {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "{}", self.id)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_valid_daemon_ids() {
123 assert!(is_valid_daemon_id("myapp"));
124 assert!(is_valid_daemon_id("my-app"));
125 assert!(is_valid_daemon_id("my_app"));
126 assert!(is_valid_daemon_id("my.app"));
127 assert!(is_valid_daemon_id("MyApp123"));
128 assert!(is_valid_daemon_id("app@host"));
129 assert!(is_valid_daemon_id("app:8080"));
130 }
131
132 #[test]
133 fn test_invalid_daemon_ids() {
134 assert!(!is_valid_daemon_id(""));
136
137 assert!(!is_valid_daemon_id("../etc/passwd"));
139 assert!(!is_valid_daemon_id("foo/bar"));
140 assert!(!is_valid_daemon_id("foo\\bar"));
141
142 assert!(!is_valid_daemon_id(".."));
144 assert!(!is_valid_daemon_id("foo..bar"));
145
146 assert!(!is_valid_daemon_id("my app"));
148 assert!(!is_valid_daemon_id(" myapp"));
149 assert!(!is_valid_daemon_id("myapp "));
150
151 assert!(!is_valid_daemon_id("."));
153
154 assert!(!is_valid_daemon_id("my\x00app"));
156 assert!(!is_valid_daemon_id("my\napp"));
157 assert!(!is_valid_daemon_id("my\tapp"));
158
159 assert!(!is_valid_daemon_id("myäpp"));
161 assert!(!is_valid_daemon_id("приложение"));
162 }
163
164 #[test]
165 fn test_validate_daemon_id_error_messages() {
166 assert!(validate_daemon_id("myapp").is_ok());
167
168 assert!(
169 validate_daemon_id("")
170 .unwrap_err()
171 .to_string()
172 .contains("daemon ID cannot be empty")
173 );
174 assert!(
175 validate_daemon_id("foo/bar")
176 .unwrap_err()
177 .to_string()
178 .contains("contains path separator")
179 );
180 assert!(
181 validate_daemon_id("foo\\bar")
182 .unwrap_err()
183 .to_string()
184 .contains("contains path separator")
185 );
186 assert!(
187 validate_daemon_id("..")
188 .unwrap_err()
189 .to_string()
190 .contains("contains parent directory reference")
191 );
192 assert!(
193 validate_daemon_id("my app")
194 .unwrap_err()
195 .to_string()
196 .contains("contains spaces")
197 );
198 assert!(
199 validate_daemon_id(".")
200 .unwrap_err()
201 .to_string()
202 .contains("daemon ID cannot be '.'")
203 );
204 assert!(
205 validate_daemon_id("my\x00app")
206 .unwrap_err()
207 .to_string()
208 .contains("non-printable or non-ASCII")
209 );
210 }
211}