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