1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3#[derive(Default)]
5pub struct DateConfig {
6 pub date_string: Option<String>,
8 pub date_file: Option<String>,
10 pub iso_format: Option<IsoFormat>,
12 pub rfc_email: bool,
14 pub rfc_3339: Option<Rfc3339Format>,
16 pub reference_file: Option<String>,
18 pub set_string: Option<String>,
20 pub utc: bool,
22 pub format: Option<String>,
24}
25
26#[derive(Clone, Debug, PartialEq)]
28pub enum IsoFormat {
29 Date,
30 Hours,
31 Minutes,
32 Seconds,
33 Ns,
34}
35
36#[derive(Clone, Debug, PartialEq)]
38pub enum Rfc3339Format {
39 Date,
40 Seconds,
41 Ns,
42}
43
44pub fn parse_iso_format(s: &str) -> Result<IsoFormat, String> {
46 match s {
47 "" | "date" => Ok(IsoFormat::Date),
48 "hours" => Ok(IsoFormat::Hours),
49 "minutes" => Ok(IsoFormat::Minutes),
50 "seconds" => Ok(IsoFormat::Seconds),
51 "ns" => Ok(IsoFormat::Ns),
52 _ => Err(format!("invalid ISO 8601 format: '{}'", s)),
53 }
54}
55
56pub fn parse_rfc3339_format(s: &str) -> Result<Rfc3339Format, String> {
58 match s {
59 "date" => Ok(Rfc3339Format::Date),
60 "seconds" => Ok(Rfc3339Format::Seconds),
61 "ns" => Ok(Rfc3339Format::Ns),
62 _ => Err(format!("invalid RFC 3339 format: '{}'", s)),
63 }
64}
65
66pub fn format_date(time: &SystemTime, format: &str, utc: bool) -> String {
71 let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
72 let secs = dur.as_secs() as i64;
73 let nanos = dur.subsec_nanos();
74
75 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
76 if utc {
77 unsafe {
78 libc::gmtime_r(&secs, &mut tm);
79 }
80 } else {
81 unsafe {
82 libc::localtime_r(&secs, &mut tm);
83 }
84 }
85
86 let mut result = String::with_capacity(format.len() * 2);
89 let chars: Vec<char> = format.chars().collect();
90 let mut i = 0;
91
92 while i < chars.len() {
93 if chars[i] == '%' && i + 1 < chars.len() {
94 let modifier = if i + 2 < chars.len()
96 && (chars[i + 1] == '-' || chars[i + 1] == '_' || chars[i + 1] == '0')
97 && chars[i + 2].is_ascii_alphabetic()
98 {
99 let m = chars[i + 1];
100 i += 1; Some(m)
102 } else {
103 None
104 };
105
106 match chars[i + 1] {
107 's' => {
108 result.push_str(&secs.to_string());
112 i += 2;
113 }
114 'N' => {
115 result.push_str(&format!("{:09}", nanos));
117 i += 2;
118 }
119 'q' => {
120 let month = tm.tm_mon; let quarter = (month / 3) + 1;
123 result.push_str(&quarter.to_string());
124 i += 2;
125 }
126 'P' => {
127 let ampm = if tm.tm_hour < 12 { "am" } else { "pm" };
129 result.push_str(ampm);
130 i += 2;
131 }
132 'Z' if utc => {
133 result.push_str("UTC");
135 i += 2;
136 }
137 'n' => {
138 result.push('\n');
139 i += 2;
140 }
141 't' => {
142 result.push('\t');
143 i += 2;
144 }
145 _ => {
146 let spec = format!("%{}", chars[i + 1]);
148 let formatted = strftime_single(&tm, &spec);
149 let formatted = if let Some(mod_char) = modifier {
151 apply_format_modifier(&formatted, mod_char)
152 } else {
153 formatted
154 };
155 result.push_str(&formatted);
156 i += 2;
157 }
158 }
159 } else {
160 result.push(chars[i]);
161 i += 1;
162 }
163 }
164
165 result
166}
167
168fn strftime_single(tm: &libc::tm, fmt: &str) -> String {
170 let c_fmt = match std::ffi::CString::new(fmt) {
171 Ok(c) => c,
172 Err(_) => return String::new(),
173 };
174 let mut buf = vec![0u8; 128];
175 let len = unsafe {
176 libc::strftime(
177 buf.as_mut_ptr() as *mut libc::c_char,
178 buf.len(),
179 c_fmt.as_ptr(),
180 tm,
181 )
182 };
183 if len == 0 && !fmt.is_empty() && fmt != "%%" {
184 return String::new();
187 }
188 buf.truncate(len);
189 String::from_utf8_lossy(&buf).into_owned()
190}
191
192fn apply_format_modifier(formatted: &str, modifier: char) -> String {
197 match modifier {
198 '-' => {
199 let trimmed = formatted.trim_start_matches(['0', ' ']);
201 if trimmed.is_empty() {
202 "0".to_string()
203 } else {
204 trimmed.to_string()
205 }
206 }
207 '_' => {
208 let mut result = String::with_capacity(formatted.len());
210 let mut leading = true;
211 for ch in formatted.chars() {
212 if leading && ch == '0' {
213 result.push(' ');
214 } else {
215 leading = false;
216 result.push(ch);
217 }
218 }
219 result
220 }
221 '0' => {
222 let mut result = String::with_capacity(formatted.len());
224 let mut leading = true;
225 for ch in formatted.chars() {
226 if leading && ch == ' ' {
227 result.push('0');
228 } else {
229 leading = false;
230 result.push(ch);
231 }
232 }
233 result
234 }
235 _ => formatted.to_string(),
236 }
237}
238
239pub fn format_iso(time: &SystemTime, precision: &IsoFormat, utc: bool) -> String {
241 match precision {
242 IsoFormat::Date => format_date(time, "%Y-%m-%d", utc),
243 IsoFormat::Hours => {
244 let date_part = format_date(time, "%Y-%m-%dT%H", utc);
245 let tz = format_timezone_colon(time, utc);
246 format!("{}{}", date_part, tz)
247 }
248 IsoFormat::Minutes => {
249 let date_part = format_date(time, "%Y-%m-%dT%H:%M", utc);
250 let tz = format_timezone_colon(time, utc);
251 format!("{}{}", date_part, tz)
252 }
253 IsoFormat::Seconds => {
254 let date_part = format_date(time, "%Y-%m-%dT%H:%M:%S", utc);
255 let tz = format_timezone_colon(time, utc);
256 format!("{}{}", date_part, tz)
257 }
258 IsoFormat::Ns => {
259 let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
260 let nanos = dur.subsec_nanos();
261 let date_part = format_date(time, "%Y-%m-%dT%H:%M:%S", utc);
262 let tz = format_timezone_colon(time, utc);
263 format!("{},{:09}{}", date_part, nanos, tz)
264 }
265 }
266}
267
268pub fn format_rfc_email(time: &SystemTime, utc: bool) -> String {
270 format_date(time, "%a, %d %b %Y %H:%M:%S %z", utc)
271}
272
273pub fn format_rfc3339(time: &SystemTime, precision: &Rfc3339Format, utc: bool) -> String {
275 match precision {
276 Rfc3339Format::Date => format_date(time, "%Y-%m-%d", utc),
277 Rfc3339Format::Seconds => {
278 let date_part = format_date(time, "%Y-%m-%d %H:%M:%S", utc);
279 let tz = format_timezone_colon(time, utc);
280 format!("{}{}", date_part, tz)
281 }
282 Rfc3339Format::Ns => {
283 let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
284 let nanos = dur.subsec_nanos();
285 let date_part = format_date(time, "%Y-%m-%d %H:%M:%S", utc);
286 let tz = format_timezone_colon(time, utc);
287 format!("{}.{:09}{}", date_part, nanos, tz)
288 }
289 }
290}
291
292fn format_timezone_colon(time: &SystemTime, utc: bool) -> String {
294 if utc {
295 return "+00:00".to_string();
296 }
297 let raw = format_date(time, "%z", false);
298 if raw.len() >= 5 {
300 format!("{}:{}", &raw[..3], &raw[3..5])
301 } else {
302 raw
303 }
304}
305
306pub fn parse_date_string(s: &str, utc: bool) -> Result<SystemTime, String> {
314 let s = s.trim();
315
316 if let Some(epoch_str) = s.strip_prefix('@') {
318 let secs: i64 = epoch_str
319 .trim()
320 .parse()
321 .map_err(|_| format!("invalid date '@{}'", epoch_str))?;
322 if secs >= 0 {
323 return Ok(UNIX_EPOCH + Duration::from_secs(secs as u64));
324 } else {
325 return Ok(UNIX_EPOCH - Duration::from_secs((-secs) as u64));
326 }
327 }
328
329 let now = SystemTime::now();
330
331 match s.to_lowercase().as_str() {
333 "now" | "today" => return Ok(now),
334 "yesterday" => {
335 return Ok(now - Duration::from_secs(86400));
336 }
337 "tomorrow" => {
338 return Ok(now + Duration::from_secs(86400));
339 }
340 _ => {}
341 }
342
343 if let Some(result) = try_parse_relative(s, &now) {
345 return Ok(result);
346 }
347
348 if let Some(result) = try_parse_time_only(s, utc) {
350 return Ok(result);
351 }
352
353 if let Some(result) = try_parse_iso(s, utc) {
355 return Ok(result);
356 }
357
358 Err(format!("invalid date '{}'", s))
359}
360
361fn try_parse_time_only(s: &str, utc: bool) -> Option<SystemTime> {
364 let s = s.trim();
365 let parts: Vec<&str> = s.split_whitespace().collect();
366 if parts.is_empty() || parts.len() > 2 {
367 return None;
368 }
369
370 let time_str = parts[0];
371 let tz_str = if parts.len() == 2 {
372 Some(parts[1])
373 } else {
374 None
375 };
376
377 let time_fields: Vec<&str> = time_str.split(':').collect();
379 if time_fields.len() < 2 || time_fields.len() > 3 {
380 return None;
381 }
382
383 let hour: u32 = time_fields[0].parse().ok()?;
384 let minute: u32 = time_fields[1].parse().ok()?;
385 let second: u32 = if time_fields.len() == 3 {
386 time_fields[2].parse().ok()?
387 } else {
388 0
389 };
390
391 if hour > 23 || minute > 59 || second > 60 {
392 return None;
393 }
394
395 let mut use_utc = utc;
397 let mut tz_offset_secs: i64 = 0;
398 if let Some(tz) = tz_str {
399 if tz.eq_ignore_ascii_case("UTC") || tz == "Z" {
400 use_utc = true;
401 } else if (tz.starts_with('+') || tz.starts_with('-')) && (tz.len() == 5 || tz.len() == 3) {
402 let sign: i64 = if tz.starts_with('-') { -1 } else { 1 };
403 let digits = &tz[1..];
404 if !digits.chars().all(|c| c.is_ascii_digit()) {
405 return None;
406 }
407 let (oh, om) = if digits.len() == 4 {
408 let h: i64 = digits[..2].parse().ok()?;
409 let m: i64 = digits[2..].parse().ok()?;
410 (h, m)
411 } else {
412 let h: i64 = digits.parse().ok()?;
413 (h, 0)
414 };
415 tz_offset_secs = sign * (oh * 3600 + om * 60);
416 use_utc = true;
417 } else {
418 return None;
419 }
420 }
421
422 let now = SystemTime::now();
424 let now_secs = now.duration_since(UNIX_EPOCH).ok()?.as_secs() as i64;
425
426 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
427 if use_utc {
428 let now_t = now_secs as libc::time_t;
429 unsafe {
430 libc::gmtime_r(&now_t, &mut tm);
431 }
432 } else {
433 let now_t = now_secs as libc::time_t;
434 unsafe {
435 libc::localtime_r(&now_t, &mut tm);
436 }
437 }
438
439 tm.tm_hour = hour as i32;
440 tm.tm_min = minute as i32;
441 tm.tm_sec = second as i32;
442 tm.tm_isdst = -1;
443
444 let epoch_secs = if use_utc {
445 unsafe { libc::timegm(&mut tm) }
446 } else {
447 unsafe { libc::mktime(&mut tm) }
448 };
449 if epoch_secs == -1 {
450 return None;
451 }
452
453 let final_secs = epoch_secs as i64 - tz_offset_secs;
455
456 if final_secs >= 0 {
457 Some(UNIX_EPOCH + Duration::from_secs(final_secs as u64))
458 } else {
459 Some(UNIX_EPOCH - Duration::from_secs((-final_secs) as u64))
460 }
461}
462
463fn try_parse_relative(s: &str, now: &SystemTime) -> Option<SystemTime> {
465 let lower = s.to_lowercase();
466 let parts: Vec<&str> = lower.split_whitespace().collect();
467
468 if parts.len() < 2 {
469 return None;
470 }
471
472 let is_ago = parts.last().map_or(false, |&p| p == "ago");
473 let num_str = parts[0].trim_start_matches('+');
474 let amount: i64 = num_str.parse().ok()?;
475
476 let unit_idx = 1;
477 if unit_idx >= parts.len() {
478 return None;
479 }
480 let unit = parts[unit_idx];
481
482 let seconds = match unit.trim_end_matches('s') {
483 "second" => amount,
484 "minute" => amount * 60,
485 "hour" => amount * 3600,
486 "day" => amount * 86400,
487 "week" => amount * 86400 * 7,
488 "month" => amount * 86400 * 30,
489 "year" => amount * 86400 * 365,
490 _ => return None,
491 };
492
493 let duration = Duration::from_secs(seconds.unsigned_abs());
494 if is_ago || seconds < 0 {
495 Some(*now - duration)
496 } else {
497 Some(*now + duration)
498 }
499}
500
501fn try_parse_iso(s: &str, utc: bool) -> Option<SystemTime> {
503 let s = s.replace('T', " ");
505 let parts: Vec<&str> = s.splitn(2, ' ').collect();
506 let date_part = parts[0];
507 let time_part = if parts.len() > 1 {
508 parts[1]
509 } else {
510 "00:00:00"
511 };
512
513 let date_fields: Vec<&str> = date_part.split('-').collect();
514 if date_fields.len() != 3 {
515 return None;
516 }
517
518 let year: i32 = date_fields[0].parse().ok()?;
519 let month: u32 = date_fields[1].parse().ok()?;
520 let day: u32 = date_fields[2].parse().ok()?;
521
522 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
523 return None;
524 }
525
526 let time_clean = time_part
528 .split('+')
529 .next()
530 .unwrap_or(time_part)
531 .split('Z')
532 .next()
533 .unwrap_or(time_part);
534 let time_fields: Vec<&str> = time_clean.split(':').collect();
535 let hour: u32 = time_fields
536 .first()
537 .and_then(|s| s.parse().ok())
538 .unwrap_or(0);
539 let minute: u32 = time_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
540 let second: u32 = time_fields
541 .get(2)
542 .and_then(|s| s.split('.').next())
543 .and_then(|s| s.parse().ok())
544 .unwrap_or(0);
545
546 if hour > 23 || minute > 59 || second > 60 {
547 return None;
548 }
549
550 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
552 tm.tm_year = year - 1900;
553 tm.tm_mon = month as i32 - 1;
554 tm.tm_mday = day as i32;
555 tm.tm_hour = hour as i32;
556 tm.tm_min = minute as i32;
557 tm.tm_sec = second as i32;
558 tm.tm_isdst = -1; let epoch_secs = if utc {
561 unsafe { libc::timegm(&mut tm) }
562 } else {
563 unsafe { libc::mktime(&mut tm) }
564 };
565 if epoch_secs == -1 {
566 return None;
567 }
568
569 if epoch_secs >= 0 {
570 Some(UNIX_EPOCH + Duration::from_secs(epoch_secs as u64))
571 } else {
572 Some(UNIX_EPOCH - Duration::from_secs((-epoch_secs) as u64))
573 }
574}
575
576pub fn file_mod_time(path: &str) -> Result<SystemTime, String> {
578 std::fs::metadata(path)
579 .map_err(|e| format!("{}: {}", path, e))?
580 .modified()
581 .map_err(|e| format!("{}: {}", path, e))
582}
583
584pub fn default_format() -> &'static str {
587 "%a %b %e %H:%M:%S %Z %Y"
588}