1use crate::error::{Error, Result};
4
5pub fn days_to_date(days: u64) -> (u64, u64, u64) {
8 let z = days + 719468;
9 let era = z / 146097;
10 let doe = z - era * 146097;
11 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
12 let y = yoe + era * 400;
13 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
14 let mp = (5 * doy + 2) / 153;
15 let d = doy - (153 * mp + 2) / 5 + 1;
16 let m = if mp < 10 { mp + 3 } else { mp - 9 };
17 let y = if m <= 2 { y + 1 } else { y };
18 (y, m, d)
19}
20
21pub fn now_iso8601() -> String {
23 let secs = std::time::SystemTime::now()
24 .duration_since(std::time::UNIX_EPOCH)
25 .unwrap_or_default()
26 .as_secs();
27 let days = secs / 86400;
28 let tod = secs % 86400;
29 let (y, m, d) = days_to_date(days);
30 format!(
31 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.000Z",
32 y,
33 m,
34 d,
35 tod / 3600,
36 (tod % 3600) / 60,
37 tod % 60
38 )
39}
40
41pub fn today_date() -> String {
43 let secs = std::time::SystemTime::now()
44 .duration_since(std::time::UNIX_EPOCH)
45 .unwrap_or_default()
46 .as_secs();
47 let days = secs / 86400;
48 let (y, m, d) = days_to_date(days);
49 format!("{:04}-{:02}-{:02}", y, m, d)
50}
51
52pub fn sanitize_entry_id(entry_id: &str) -> String {
55 entry_id.replace(['/', '\\'], "--").replace("..", "")
56}
57
58pub fn validate_filename(name: &str, kind: &str) -> Result<()> {
60 if name.is_empty()
61 || name.contains('/')
62 || name.contains('\\')
63 || name.contains("..")
64 || name.starts_with('.')
65 {
66 return Err(Error::Config(format!(
67 "Invalid {} name \"{}\": must not contain path separators or \"..\"",
68 kind, name
69 )));
70 }
71 Ok(())
72}
73
74pub fn validate_url(url: &str, context: &str) -> Result<String> {
77 let trimmed = url.trim().trim_end_matches('/');
78 if trimmed.is_empty() {
79 return Err(Error::Config(format!("{}: URL must not be empty", context)));
80 }
81
82 let lower = trimmed.to_lowercase();
84 if lower.starts_with("https://") {
85 return Ok(trimmed.to_string());
86 }
87
88 if let Some(host_part) = lower.strip_prefix("http://") {
90 let host = host_part.split('/').next().unwrap_or("");
91 let host_no_port = host.split(':').next().unwrap_or("");
92 if host_no_port == "localhost" || host_no_port == "127.0.0.1" || host_no_port == "[::1]" {
93 return Ok(trimmed.to_string());
94 }
95 return Err(Error::Config(format!(
96 "{}: HTTP URLs are only allowed for localhost. Use HTTPS for remote servers: \"{}\"",
97 context, trimmed
98 )));
99 }
100
101 Err(Error::Config(format!(
102 "{}: URL must use HTTPS (got \"{}\")",
103 context, trimmed
104 )))
105}
106
107pub fn validate_path_within(
110 base: &std::path::Path,
111 target: &std::path::Path,
112 context: &str,
113) -> Result<std::path::PathBuf> {
114 let target_str = target.to_string_lossy();
116 if target_str.contains("..") {
117 return Err(Error::Config(format!(
118 "{}: path traversal not allowed: \"{}\"",
119 context, target_str
120 )));
121 }
122
123 let resolved_base = base.canonicalize().unwrap_or_else(|_| base.to_path_buf());
126 let resolved = if target.exists() {
127 target
128 .canonicalize()
129 .unwrap_or_else(|_| base.join(target.file_name().unwrap_or_default()))
130 } else {
131 resolved_base.join(
133 target.strip_prefix(&resolved_base).unwrap_or(
134 target
135 .file_name()
136 .map(std::path::Path::new)
137 .unwrap_or(target),
138 ),
139 )
140 };
141
142 if !resolved.starts_with(&resolved_base) {
143 return Err(Error::Config(format!(
144 "{}: path escapes allowed directory: \"{}\"",
145 context, target_str
146 )));
147 }
148
149 Ok(resolved)
150}
151
152pub fn atomic_write(
155 path: &std::path::Path,
156 data: &[u8],
157) -> std::result::Result<(), std::io::Error> {
158 let parent = path.parent().unwrap_or(std::path::Path::new("."));
159 let _ = std::fs::create_dir_all(parent);
160
161 let tmp_name = format!(
163 ".{}.tmp.{}",
164 path.file_name().unwrap_or_default().to_string_lossy(),
165 std::process::id()
166 );
167 let tmp_path = parent.join(&tmp_name);
168
169 std::fs::write(&tmp_path, data)?;
170
171 if std::fs::rename(&tmp_path, path).is_err() {
173 let _ = std::fs::remove_file(path);
175 if let Err(_e) = std::fs::rename(&tmp_path, path) {
176 let _ = std::fs::remove_file(&tmp_path);
178 return std::fs::write(path, data);
179 }
180 }
181
182 Ok(())
183}