1use std::path::Path;
8
9pub fn safe_filename(name: &str) -> Option<String> {
21 let name = name.trim();
22 if name.is_empty() {
23 return None;
24 }
25
26 let p = Path::new(name);
27 let stem = p.file_name()?;
28 let s = stem.to_string_lossy();
29
30 if s == "." || s == ".." || s.is_empty() {
32 return None;
33 }
34
35 Some(s.into_owned())
36}
37
38pub fn validate_http_url(raw: &str) -> Result<url::Url, String> {
43 let trimmed = raw.trim();
44 if trimmed.is_empty() {
45 return Err("URL is empty".into());
46 }
47
48 let parsed = url::Url::parse(trimmed).map_err(|e| format!("Invalid URL: {}", e))?;
49
50 match parsed.scheme() {
51 "http" | "https" => Ok(parsed),
52 other => Err(format!(
53 "Unsupported URL scheme '{}'. Only http:// and https:// are allowed.",
54 other
55 )),
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
66 fn safe_filename_extracts_basename() {
67 assert_eq!(safe_filename("photo.jpg"), Some("photo.jpg".into()));
68 }
69
70 #[test]
71 fn safe_filename_strips_directory_components() {
72 assert_eq!(safe_filename("sub/dir/file.jpg"), Some("file.jpg".into()));
73 }
74
75 #[test]
76 fn safe_filename_strips_traversal_to_basename() {
77 assert_eq!(safe_filename("../../../etc/passwd"), Some("passwd".into()));
80 }
81
82 #[test]
83 fn safe_filename_rejects_empty() {
84 assert_eq!(safe_filename(""), None);
85 assert_eq!(safe_filename(" "), None);
86 }
87
88 #[test]
89 fn safe_filename_rejects_dot_dot() {
90 assert_eq!(safe_filename(".."), None);
91 assert_eq!(safe_filename("."), None);
92 }
93
94 #[test]
97 fn validate_accepts_https() {
98 let result = validate_http_url("https://immich.example.com");
99 assert!(result.is_ok());
100 assert_eq!(result.unwrap().scheme(), "https");
101 }
102
103 #[test]
104 fn validate_accepts_http() {
105 let result = validate_http_url("http://192.168.1.1:2283");
106 assert!(result.is_ok());
107 }
108
109 #[test]
110 fn validate_rejects_file_scheme() {
111 let result = validate_http_url("file:///etc/passwd");
112 assert!(result.is_err());
113 assert!(result.unwrap_err().contains("file"));
114 }
115
116 #[test]
117 fn validate_rejects_javascript_scheme() {
118 let result = validate_http_url("javascript:alert(1)");
119 assert!(result.is_err());
120 }
121
122 #[test]
123 fn validate_rejects_empty() {
124 assert!(validate_http_url("").is_err());
125 assert!(validate_http_url(" ").is_err());
126 }
127
128 #[test]
129 fn validate_trims_whitespace() {
130 let result = validate_http_url(" https://immich.local ");
131 assert!(result.is_ok());
132 }
133}