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