Skip to main content

relibc/header/time/
strptime.rs

1// `strptime` implementation.
2//
3// See <https://pubs.opengroup.org/onlinepubs/9799919799/functions/strptime.html>.
4
5use crate::header::time::tm;
6use alloc::string::String;
7use core::{
8    ffi::{CStr, c_char, c_int, c_void},
9    ptr,
10    ptr::NonNull,
11    str,
12};
13
14/// cbindgen:ignore
15/// For convenience, we define some helper constants for the C-locale.
16const SHORT_DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
17/// cbindgen:ignore
18const LONG_DAYS: [&str; 7] = [
19    "Sunday",
20    "Monday",
21    "Tuesday",
22    "Wednesday",
23    "Thursday",
24    "Friday",
25    "Saturday",
26];
27/// cbindgen:ignore
28const SHORT_MONTHS: [&str; 12] = [
29    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
30];
31/// cbindgen:ignore
32const LONG_MONTHS: [&str; 12] = [
33    "January",
34    "February",
35    "March",
36    "April",
37    "May",
38    "June",
39    "July",
40    "August",
41    "September",
42    "October",
43    "November",
44    "December",
45];
46
47/// See <https://pubs.opengroup.org/onlinepubs/9799919799/functions/strptime.html>.
48#[unsafe(no_mangle)]
49pub unsafe extern "C" fn strptime(
50    buf: *const c_char,
51    format: *const c_char,
52    tm: *mut tm,
53) -> *mut c_char {
54    // Validate inputs
55    let buf_ptr = if let Some(ptr) = NonNull::new(buf.cast::<c_void>().cast_mut()) {
56        ptr
57    } else {
58        return ptr::null_mut();
59    };
60    //
61    let fmt_ptr = if let Some(ptr) = NonNull::new(format.cast::<c_void>().cast_mut()) {
62        ptr
63    } else {
64        return ptr::null_mut();
65    };
66
67    let tm_ptr = if let Some(ptr) = NonNull::new(tm) {
68        ptr
69    } else {
70        return ptr::null_mut();
71    };
72
73    // Convert raw pointers into slices/strings.
74    let input_str = unsafe {
75        if buf.is_null() {
76            return ptr::null_mut();
77        }
78        match CStr::from_ptr(buf).to_str() {
79            Ok(s) => s,
80            Err(_) => return ptr::null_mut(), // Not a valid UTF-8
81        }
82    };
83
84    let fmt_str = unsafe {
85        if format.is_null() {
86            return ptr::null_mut();
87        }
88        match CStr::from_ptr(format).to_str() {
89            Ok(s) => s,
90            Err(_) => return ptr::null_mut(), // Not a valid UTF-8
91        }
92    };
93
94    // We parse the format specifiers in a loop
95    let mut fmt_chars = fmt_str.chars().peekable();
96    let mut index_in_input = 0;
97
98    while let Some(fc) = fmt_chars.next() {
99        if fc != '%' {
100            // If it's a normal character, we expect it to match exactly in input
101            if input_str.len() <= index_in_input {
102                return ptr::null_mut(); // input ended too soon
103            }
104            let in_char = input_str.as_bytes()[index_in_input] as char;
105            if in_char != fc {
106                // mismatch
107                return ptr::null_mut();
108            }
109            index_in_input += 1;
110            continue;
111        }
112
113        // If we see '%', read the next character
114        let Some(spec) = fmt_chars.next() else {
115            // format string ended abruptly after '%'
116            return ptr::null_mut();
117        };
118
119        // POSIX says `%E` or `%O` are modified specifiers for locale.
120        // We will skip them if they appear (like strftime does) and read the next char.
121        let final_spec = if spec == 'E' || spec == 'O' {
122            match fmt_chars.next() {
123                Some(ch) => ch,
124                None => return ptr::null_mut(),
125            }
126        } else {
127            spec
128        };
129
130        // Handle known specifiers
131        match final_spec {
132            // Whitespace: %n or %t
133            'n' | 't' => {
134                // Skip over any whitespace in the input
135                while index_in_input < input_str.len()
136                    && input_str.as_bytes()[index_in_input].is_ascii_whitespace()
137                {
138                    index_in_input += 1;
139                }
140            }
141
142            // Literal % => "%%"
143            '%' => {
144                if index_in_input >= input_str.len()
145                    || input_str.as_bytes()[index_in_input] as char != '%'
146                {
147                    return ptr::null_mut();
148                }
149                index_in_input += 1;
150            }
151
152            // Day of Month: %d / %e
153            'd' | 'e' => {
154                // parse a 2-digit day (with or without leading zero)
155                let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
156                    Some(v) => v,
157                    None => return ptr::null_mut(),
158                };
159                unsafe {
160                    (*tm).tm_mday = val as c_int;
161                    // Day of month is limited to [1,31] according to the standard
162                    if (*tm).tm_mday < 1 || (*tm).tm_mday > 31 {
163                        return ptr::null_mut();
164                    }
165                }
166                index_in_input += len;
167            }
168
169            // Month: %m
170            'm' => {
171                // parse a 2-digit month
172                let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
173                    Some(v) => v,
174                    None => return ptr::null_mut(),
175                };
176                // tm_mon is 0-based (0 = Jan, 1 = Feb,...)
177                unsafe {
178                    (*tm).tm_mon = (val as c_int) - 1;
179                    if (*tm).tm_mon < 0 || (*tm).tm_mon > 11 {
180                        return ptr::null_mut();
181                    }
182                }
183                index_in_input += len;
184            }
185
186            // Year without century: %y
187            'y' => {
188                // parse a 2-digit year
189                let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
190                    Some(v) => v,
191                    None => return ptr::null_mut(),
192                };
193                // According to POSIX, %y in strptime is [00,99], and the "year" is 1900..1999 for [00..99],
194                // but the standard says: "values in [69..99] refer to 1969..1999, [00..68] => 2000..2068"
195                let fullyear = if val >= 69 { val + 1900 } else { val + 2000 };
196                unsafe {
197                    (*tm).tm_year = (fullyear - 1900) as c_int;
198                }
199                index_in_input += len;
200            }
201
202            // Year with century: %Y
203            'Y' => {
204                // parse up to 4-digit (or more) year
205                // We allow more than 4 digits if needed
206                let (val, len) = match parse_int(&input_str[index_in_input..], 4, true) {
207                    Some(v) => v,
208                    None => return ptr::null_mut(),
209                };
210                unsafe {
211                    (*tm).tm_year = (val as c_int) - 1900;
212                }
213                index_in_input += len;
214            }
215
216            // Hour (00..23): %H
217            'H' => {
218                let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
219                    Some(v) => v,
220                    None => return ptr::null_mut(),
221                };
222                if val > 23 {
223                    return ptr::null_mut();
224                }
225                unsafe {
226                    (*tm).tm_hour = val as c_int;
227                }
228                index_in_input += len;
229            }
230
231            // Hour (01..12): %I
232            'I' => {
233                let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
234                    Some(v) => v,
235                    None => return ptr::null_mut(),
236                };
237                if !(1..=12).contains(&val) {
238                    return ptr::null_mut();
239                }
240                unsafe {
241                    (*tm).tm_hour = val as c_int;
242                }
243                // We’ll interpret AM/PM with %p if it appears later
244                index_in_input += len;
245            }
246
247            // Minute (00..59): %M
248            'M' => {
249                let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
250                    Some(v) => v,
251                    None => return ptr::null_mut(),
252                };
253                if val > 59 {
254                    return ptr::null_mut();
255                }
256                unsafe {
257                    (*tm).tm_min = val as c_int;
258                }
259                index_in_input += len;
260            }
261
262            // Seconds (00..60): %S
263            'S' => {
264                let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
265                    Some(v) => v,
266                    None => return ptr::null_mut(),
267                };
268                if val > 60 {
269                    return ptr::null_mut();
270                }
271                unsafe {
272                    (*tm).tm_sec = val as c_int;
273                }
274                index_in_input += len;
275            }
276
277            // AM/PM: %p
278            'p' => {
279                // Parse either "AM" or "PM" (no case-sensitive)
280                // We'll read up to 2 or 3 letters from input ("AM", "PM")
281                let leftover = &input_str[index_in_input..];
282                let parsed_len = match parse_am_pm(leftover) {
283                    Some((is_pm, used)) => {
284                        if unsafe { (*tm).tm_hour } == 12 {
285                            // 12 AM => 00:xx, 12 PM => 12:xx
286                            unsafe {
287                                (*tm).tm_hour = if is_pm { 12 } else { 0 };
288                            }
289                        } else {
290                            // 1..11 AM => 1..11, 1..11 PM => 13..23
291                            if is_pm {
292                                unsafe {
293                                    (*tm).tm_hour += 12;
294                                }
295                            }
296                        }
297                        used
298                    }
299                    None => return ptr::null_mut(),
300                };
301                index_in_input += parsed_len;
302            }
303
304            // Weekday Name: %a/%A
305            'a' => {
306                // Abbreviated day name (Sun..Sat)
307                let leftover = &input_str[index_in_input..];
308                let parsed_len = match parse_weekday(leftover, true) {
309                    Some((wday, used)) => {
310                        unsafe {
311                            (*tm).tm_wday = wday as c_int;
312                        }
313                        used
314                    }
315                    None => return ptr::null_mut(),
316                };
317                index_in_input += parsed_len;
318            }
319            'A' => {
320                // Full day name (Sunday..Saturday)
321                let leftover = &input_str[index_in_input..];
322                let parsed_len = match parse_weekday(leftover, false) {
323                    Some((wday, used)) => {
324                        unsafe {
325                            (*tm).tm_wday = wday as c_int;
326                        }
327                        used
328                    }
329                    None => return ptr::null_mut(),
330                };
331                index_in_input += parsed_len;
332            }
333
334            // Month Name: %b/%B/%h
335            'b' | 'h' => {
336                // Abbreviated month name
337                let leftover = &input_str[index_in_input..];
338                let parsed_len = match parse_month(leftover, true) {
339                    Some((mon, used)) => {
340                        unsafe {
341                            (*tm).tm_mon = mon as c_int;
342                        }
343                        used
344                    }
345                    None => return ptr::null_mut(),
346                };
347                index_in_input += parsed_len;
348            }
349            'B' => {
350                // Full month name
351                let leftover = &input_str[index_in_input..];
352                let parsed_len = match parse_month(leftover, false) {
353                    Some((mon, used)) => {
354                        unsafe {
355                            (*tm).tm_mon = mon as c_int;
356                        }
357                        used
358                    }
359                    None => return ptr::null_mut(),
360                };
361                index_in_input += parsed_len;
362            }
363
364            // Day of year: %j
365            'j' => {
366                // parse 3-digit day of year [001..366]
367                let (val, len) = match parse_int(&input_str[index_in_input..], 3, false) {
368                    Some(v) => v,
369                    None => return ptr::null_mut(),
370                };
371                if !(1..=366).contains(&val) {
372                    return ptr::null_mut();
373                }
374                // store in tm_yday
375                unsafe {
376                    (*tm).tm_yday = (val - 1) as c_int;
377                }
378                index_in_input += len;
379            }
380
381            // Date shortcuts: %D, %F, etc.
382            'D' => {
383                // Equivalent to "%m/%d/%y"
384                // We can do a mini strptime recursion or manually parse
385                // For simplicity, we'll do it inline here
386                let subfmt = "%m/%d/%y";
387                let used =
388                    match unsafe { apply_subformat(&input_str[index_in_input..], subfmt, tm) } {
389                        Some(v) => v,
390                        None => return ptr::null_mut(),
391                    };
392                index_in_input += used;
393            }
394            'F' => {
395                // Equivalent to "%Y-%m-%d"
396                let subfmt = "%Y-%m-%d";
397                let used =
398                    match unsafe { apply_subformat(&input_str[index_in_input..], subfmt, tm) } {
399                        Some(v) => v,
400                        None => return ptr::null_mut(),
401                    };
402                index_in_input += used;
403            }
404            'T' => {
405                // Equivalent to %H:%M:%S
406                let subfmt = "%H:%M:%S";
407                let used =
408                    match unsafe { apply_subformat(&input_str[index_in_input..], subfmt, tm) } {
409                        Some(v) => v,
410                        None => return ptr::null_mut(),
411                    };
412                index_in_input += used;
413            }
414
415            // TODO : not implemented: %x, %X, %c, %r, %R, etc.
416            // Hint : if you want to implement these, do similarly to %D / %F (or parse manually)
417            'x' | 'X' | 'c' | 'r' | 'R' => {
418                // Return NULL if we don’t want to accept them :
419                return ptr::null_mut();
420            }
421
422            // Timezone: %Z or %z
423            'Z' | 'z' => {
424                // Full/abbrev time zone name or numeric offset
425                // Implementation omitted. Real support is quite complicated.
426                return ptr::null_mut();
427            }
428
429            _ => {
430                // We do not recognize this specifier
431                return ptr::null_mut();
432            }
433        }
434    }
435
436    // If we got here, parsing was successful. Return pointer to the
437    // next unparsed character in `buf`.
438    let ret_ptr = unsafe { buf.add(index_in_input) };
439    ret_ptr.cast_mut()
440}
441
442// Helper / Parsing Logic
443
444/// Parse an integer from the beginning of `input_str`.
445///
446/// - `width` is the maximum number of digits to parse
447/// - `allow_variable_width` indicates if we can parse fewer digits
448///   (e.g., `%Y` can have more than 4 digits, but also might parse "2023" or "12345").
449fn parse_int(input: &str, width: usize, allow_variable: bool) -> Option<(i32, usize)> {
450    let mut val = 0i32;
451    let chars = input.chars();
452    let mut count = 0;
453
454    for c in chars {
455        if !c.is_ascii_digit() {
456            break;
457        }
458
459        // Check for integer overflow
460        val = val
461            .checked_mul(10)?
462            .checked_add(i32::from(c as u8 - b'0'))?;
463
464        count += 1;
465        if count == width && !allow_variable {
466            break;
467        }
468    }
469
470    if count == 0 { None } else { Some((val, count)) }
471}
472
473/// Handle AM/PM. Returns (is_pm, length_consumed).
474/// Accepts "AM", "am", "PM", "pm" case-insensitively.
475fn parse_am_pm(s: &str) -> Option<(bool, usize)> {
476    let trimmed = s.trim_start();
477    // Amount of whitespace skipped; can be 0
478    let diff = s.len() - trimmed.len();
479    let s = trimmed.get(0..2)?;
480
481    if s.eq_ignore_ascii_case("AM") {
482        return Some((false, diff + 2));
483    }
484    if s.eq_ignore_ascii_case("PM") {
485        return Some((true, diff + 2));
486    }
487    None
488}
489
490/// Parse a weekday name from `s`.
491/// - if `abbrev == true`, match short forms: "Mont".."Sun"
492/// - otherwise, match "Monday".."Sunday"
493///
494/// Return (weekday_index, length_consumed).
495fn parse_weekday(s: &str, abbrev: bool) -> Option<(usize, usize)> {
496    let list = if abbrev { &SHORT_DAYS } else { &LONG_DAYS };
497    for (i, name) in list.iter().enumerate() {
498        if s.len() >= name.len()
499            && s.get(0..name.len())
500                .is_some_and(|sub| sub.eq_ignore_ascii_case(name))
501        {
502            return Some((i, name.len()));
503        }
504    }
505    None
506}
507
508/// Parse a month name from `s`.
509/// - If `abbrev == true`, match short forms: "Jan".."Dec"
510/// - Otherwise, match "January".."December"
511///
512/// Return (month_index, length_consumed).
513fn parse_month(s: &str, abbrev: bool) -> Option<(usize, usize)> {
514    let list = if abbrev { &SHORT_MONTHS } else { &LONG_MONTHS };
515    for (i, name) in list.iter().enumerate() {
516        if s.len() >= name.len()
517            && s.get(0..name.len())
518                .is_some_and(|sub| sub.eq_ignore_ascii_case(name))
519        {
520            return Some((i, name.len()));
521        }
522    }
523    None
524}
525
526/// Apply a small subformat (like "%m/%d/%y" or "%Y-%m-%d") to `input`.
527/// Return how many characters of `input` were consumed or None on error.
528unsafe fn apply_subformat(input: &str, subfmt: &str, tm: *mut tm) -> Option<usize> {
529    // We'll do a temporary strptime call on a substring.
530    // Then we see how many chars it consumed. If that call fails, we return None.
531    // Otherwise, we return the count.
532
533    // Convert `input` to a null-terminated buffer temporarily
534    let mut tmpbuf = String::with_capacity(input.len() + 1);
535    tmpbuf.push_str(input);
536    tmpbuf.push('\0');
537
538    let mut tmpfmt = String::with_capacity(subfmt.len() + 1);
539    tmpfmt.push_str(subfmt);
540    tmpfmt.push('\0');
541
542    // We need a copy of the tm, so if partial parse fails, we don't override.
543    let old_tm = unsafe { ptr::read(tm) }; // backup
544
545    let consumed_ptr = unsafe {
546        strptime(
547            tmpbuf.as_ptr().cast::<c_char>(),
548            tmpfmt.as_ptr().cast::<c_char>(),
549            tm,
550        )
551    };
552
553    if consumed_ptr.is_null() {
554        // revert
555        unsafe {
556            *tm = old_tm;
557        }
558        return None;
559    }
560
561    // consumed_ptr - tmpbuf.as_ptr() => # of bytes consumed
562    let diff = (consumed_ptr as usize) - (tmpbuf.as_ptr() as usize);
563    Some(diff)
564}
565
566#[cfg(test)]
567mod tests {
568    use super::parse_am_pm;
569
570    #[test]
571    fn am_pm_parser_works() {
572        let am = "am";
573        let am_expected = Some((false, 2));
574        assert_eq!(am_expected, parse_am_pm(am));
575
576        let pm = "pm";
577        let pm_expected = Some((true, 2));
578        assert_eq!(pm_expected, parse_am_pm(pm));
579
580        let am_caps = "AM";
581        assert_eq!(am_expected, parse_am_pm(am_caps));
582
583        let pm_caps = "PM";
584        assert_eq!(pm_expected, parse_am_pm(pm_caps));
585
586        let am_weird = "aM";
587        assert_eq!(am_expected, parse_am_pm(am_weird));
588
589        let am_prefix = "        \tam";
590        let am_prefix_expected = Some((false, 11));
591        assert_eq!(am_prefix_expected, parse_am_pm(am_prefix));
592
593        let pm_spaces = "        pm        ";
594        let pm_spaces_expected = Some((true, 10));
595        assert_eq!(pm_spaces_expected, parse_am_pm(pm_spaces));
596    }
597}