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