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 #[serde(skip_serializing_if = "Option::is_none", default)]
66 pub cmd: Option<Vec<String>>,
67 pub autostop: bool,
68 #[serde(skip_serializing_if = "Option::is_none", default)]
69 pub cron_schedule: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none", default)]
71 pub cron_retrigger: Option<CronRetrigger>,
72 #[serde(skip_serializing_if = "Option::is_none", default)]
73 pub last_cron_triggered: Option<chrono::DateTime<chrono::Local>>,
74 #[serde(skip_serializing_if = "Option::is_none", default)]
75 pub last_exit_success: Option<bool>,
76 #[serde(default)]
77 pub retry: u32,
78 #[serde(default)]
79 pub retry_count: u32,
80 #[serde(skip_serializing_if = "Option::is_none", default)]
81 pub ready_delay: Option<u64>,
82 #[serde(skip_serializing_if = "Option::is_none", default)]
83 pub ready_output: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none", default)]
85 pub ready_http: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none", default)]
87 pub ready_port: Option<u16>,
88 #[serde(skip_serializing_if = "Option::is_none", default)]
89 pub ready_cmd: Option<String>,
90 #[serde(skip_serializing_if = "Vec::is_empty", default)]
91 pub depends: Vec<String>,
92}
93
94#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
95pub struct RunOptions {
96 pub id: String,
97 pub cmd: Vec<String>,
98 pub force: bool,
99 pub shell_pid: Option<u32>,
100 pub dir: PathBuf,
101 pub autostop: bool,
102 pub cron_schedule: Option<String>,
103 pub cron_retrigger: Option<CronRetrigger>,
104 pub retry: u32,
105 pub retry_count: u32,
106 pub ready_delay: Option<u64>,
107 pub ready_output: Option<String>,
108 pub ready_http: Option<String>,
109 pub ready_port: Option<u16>,
110 pub ready_cmd: Option<String>,
111 pub wait_ready: bool,
112 #[serde(skip_serializing_if = "Vec::is_empty", default)]
113 pub depends: Vec<String>,
114}
115
116impl Display for Daemon {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 write!(f, "{}", self.id)
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_valid_daemon_ids() {
128 assert!(is_valid_daemon_id("myapp"));
129 assert!(is_valid_daemon_id("my-app"));
130 assert!(is_valid_daemon_id("my_app"));
131 assert!(is_valid_daemon_id("my.app"));
132 assert!(is_valid_daemon_id("MyApp123"));
133 assert!(is_valid_daemon_id("app@host"));
134 assert!(is_valid_daemon_id("app:8080"));
135 }
136
137 #[test]
138 fn test_invalid_daemon_ids() {
139 assert!(!is_valid_daemon_id(""));
141
142 assert!(!is_valid_daemon_id("../etc/passwd"));
144 assert!(!is_valid_daemon_id("foo/bar"));
145 assert!(!is_valid_daemon_id("foo\\bar"));
146
147 assert!(!is_valid_daemon_id(".."));
149 assert!(!is_valid_daemon_id("foo..bar"));
150
151 assert!(!is_valid_daemon_id("my app"));
153 assert!(!is_valid_daemon_id(" myapp"));
154 assert!(!is_valid_daemon_id("myapp "));
155
156 assert!(!is_valid_daemon_id("."));
158
159 assert!(!is_valid_daemon_id("my\x00app"));
161 assert!(!is_valid_daemon_id("my\napp"));
162 assert!(!is_valid_daemon_id("my\tapp"));
163
164 assert!(!is_valid_daemon_id("myäpp"));
166 assert!(!is_valid_daemon_id("приложение"));
167 }
168
169 #[test]
170 fn test_validate_daemon_id_error_messages() {
171 assert!(validate_daemon_id("myapp").is_ok());
172
173 assert!(
174 validate_daemon_id("")
175 .unwrap_err()
176 .to_string()
177 .contains("daemon ID cannot be empty")
178 );
179 assert!(
180 validate_daemon_id("foo/bar")
181 .unwrap_err()
182 .to_string()
183 .contains("contains path separator")
184 );
185 assert!(
186 validate_daemon_id("foo\\bar")
187 .unwrap_err()
188 .to_string()
189 .contains("contains path separator")
190 );
191 assert!(
192 validate_daemon_id("..")
193 .unwrap_err()
194 .to_string()
195 .contains("contains parent directory reference")
196 );
197 assert!(
198 validate_daemon_id("my app")
199 .unwrap_err()
200 .to_string()
201 .contains("contains spaces")
202 );
203 assert!(
204 validate_daemon_id(".")
205 .unwrap_err()
206 .to_string()
207 .contains("daemon ID cannot be '.'")
208 );
209 assert!(
210 validate_daemon_id("my\x00app")
211 .unwrap_err()
212 .to_string()
213 .contains("non-printable or non-ASCII")
214 );
215 }
216}