Merge branch 'langinfo' into 'master'

Implement 'langinfo.h' and refactor strftime() to use langinfo constants

See merge request redox-os/relibc!599
This commit is contained in:
Jeremy Soller
2025-01-08 21:31:46 +00:00
7 changed files with 901 additions and 50 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"cmake.ignoreCMakeListsMissing": true
}
+9
View File
@@ -0,0 +1,9 @@
sys_includes = ["stddef.h", "stdint.h", "features.h"]
include_guard = "_RELIBC_LANGINFO_H"
language = "C"
style = "tag"
no_includes = true
cpp_compat = true
[enum]
prefix_with_name = true
+151
View File
@@ -0,0 +1,151 @@
// langinfo.h implementation for Redox, following the POSIX standard.
// Following https://pubs.opengroup.org/onlinepubs/7908799/xsh/langinfo.h.html
//
// TODO : involve loading locale data. Currently, the implementation only supports the "C" locale.
use core::ffi::c_char;
/// POSIX type for items used with `nl_langinfo`
/// In practice, this is an integer index into the string table.
pub type nl_item = i32;
// Static string table for langinfo constants
static STRING_TABLE: [&[u8]; 57] = [
b"UTF-8\0", // CODESET
b"%a %b %e %H:%M:%S %Y\0", // D_T_FMT
b"%m/%d/%y\0", // D_FMT
b"%H:%M:%S\0", // T_FMT
b"%I:%M:%S %p\0", // T_FMT_AMPM
b"AM\0", // AM_STR
b"PM\0", // PM_STR
b"Sunday\0", // DAY_1
b"Monday\0", // DAY_2
b"Tuesday\0", // DAY_3
b"Wednesday\0", // DAY_4
b"Thursday\0", // DAY_5
b"Friday\0", // DAY_6
b"Saturday\0", // DAY_7
b"Sun\0", // ABDAY_1
b"Mon\0", // ABDAY_2
b"Tue\0", // ABDAY_3
b"Wed\0", // ABDAY_4
b"Thu\0", // ABDAY_5
b"Fri\0", // ABDAY_6
b"Sat\0", // ABDAY_7
b"January\0", // MON_1
b"February\0", // MON_2
b"March\0", // MON_3
b"April\0", // MON_4
b"May\0", // MON_5
b"June\0", // MON_6
b"July\0", // MON_7
b"August\0", // MON_8
b"September\0", // MON_9
b"October\0", // MON_10
b"November\0", // MON_11
b"December\0", // MON_12
b"Jan\0", // ABMON_1
b"Feb\0", // ABMON_2
b"Mar\0", // ABMON_3
b"Apr\0", // ABMON_4
b"May\0", // ABMON_5
b"Jun\0", // ABMON_6
b"Jul\0", // ABMON_7
b"Aug\0", // ABMON_8
b"Sep\0", // ABMON_9
b"Oct\0", // ABMON_10
b"Nov\0", // ABMON_11
b"Dec\0", // ABMON_12
b"\0", // ERA
b"\0", // ERA_D_FMT
b"\0", // ERA_D_T_FMT
b"\0", // ERA_T_FMT
b"\0", // ALT_DIGITS
b".\0", // RADIXCHAR
b"\0", // THOUSEP
b".\0", // CRNCYSTR
b"^[yY]\0", // YESEXPR
b"^[nN]\0", // NOEXPR
b"yes\0", // YESSTR
b"no\0", // NOSTR
];
// Item constants
pub const CODESET: nl_item = 0;
pub const D_T_FMT: nl_item = 1;
pub const D_FMT: nl_item = 2;
pub const T_FMT: nl_item = 3;
pub const T_FMT_AMPM: nl_item = 4;
pub const AM_STR: nl_item = 5;
pub const PM_STR: nl_item = 6;
pub const DAY_1: nl_item = 7;
pub const DAY_2: nl_item = 8;
pub const DAY_3: nl_item = 9;
pub const DAY_4: nl_item = 10;
pub const DAY_5: nl_item = 11;
pub const DAY_6: nl_item = 12;
pub const DAY_7: nl_item = 13;
pub const ABDAY_1: nl_item = 14;
pub const ABDAY_2: nl_item = 15;
pub const ABDAY_3: nl_item = 16;
pub const ABDAY_4: nl_item = 17;
pub const ABDAY_5: nl_item = 18;
pub const ABDAY_6: nl_item = 19;
pub const ABDAY_7: nl_item = 20;
pub const MON_1: nl_item = 21;
pub const MON_2: nl_item = 22;
pub const MON_3: nl_item = 23;
pub const MON_4: nl_item = 24;
pub const MON_5: nl_item = 25;
pub const MON_6: nl_item = 26;
pub const MON_7: nl_item = 27;
pub const MON_8: nl_item = 28;
pub const MON_9: nl_item = 29;
pub const MON_10: nl_item = 30;
pub const MON_11: nl_item = 31;
pub const MON_12: nl_item = 32;
pub const ABMON_1: nl_item = 33;
pub const ABMON_2: nl_item = 34;
pub const ABMON_3: nl_item = 35;
pub const ABMON_4: nl_item = 36;
pub const ABMON_5: nl_item = 37;
pub const ABMON_6: nl_item = 38;
pub const ABMON_7: nl_item = 39;
pub const ABMON_8: nl_item = 40;
pub const ABMON_9: nl_item = 41;
pub const ABMON_10: nl_item = 42;
pub const ABMON_11: nl_item = 43;
pub const ABMON_12: nl_item = 44;
pub const ERA: nl_item = 45;
pub const ERA_D_FMT: nl_item = 46;
pub const ERA_D_T_FMT: nl_item = 47;
pub const ERA_T_FMT: nl_item = 48;
pub const ALT_DIGITS: nl_item = 49;
pub const RADIXCHAR: nl_item = 50;
pub const THOUSEP: nl_item = 51;
pub const YESEXPR: nl_item = 52;
pub const NOEXPR: nl_item = 53;
pub const YESSTR: nl_item = 54; // Legaxy
pub const NOSTR: nl_item = 55; // Legacy
pub const CRNCYSTR: nl_item = 56;
/// Get a string from the langinfo table
///
/// # Safety
/// - Caller must ensure `item` is a valid `nl_item` index.
/// - Returns a pointer to a null-terminated string, or an empty string if the item is invalid.
#[no_mangle]
pub unsafe extern "C" fn nl_langinfo(item: nl_item) -> *const c_char {
// Validate the item and perform the lookup
if (item as usize) < STRING_TABLE.len() {
STRING_TABLE[item as usize].as_ptr() as *const c_char
} else {
// Return a pointer to an empty string if the item is invalid
b"\0".as_ptr() as *const c_char
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ pub mod grp;
// TODO: iconv.h
pub mod inttypes;
// iso646.h implemented in C
// TODO: langinfo.h
pub mod langinfo;
// TODO: libintl.h
pub mod libgen;
pub mod limits;
+2 -5
View File
@@ -22,7 +22,9 @@ use core::{
pub use self::constants::*;
pub mod constants;
mod strftime;
mod strptime;
const YEARS_PER_ERA: time_t = 400;
const DAYS_PER_ERA: time_t = 146097;
@@ -465,11 +467,6 @@ pub unsafe extern "C" fn strftime(
}
}
// #[no_mangle]
pub extern "C" fn strptime(buf: *const c_char, format: *const c_char, tm: *mut tm) -> *mut c_char {
unimplemented!();
}
#[no_mangle]
pub unsafe extern "C" fn time(tloc: *mut time_t) -> time_t {
let mut ts = timespec::default();
+151 -44
View File
@@ -1,10 +1,45 @@
// strftime implementation for Redox, following the POSIX standard.
// Following https://pubs.opengroup.org/onlinepubs/7908799/xsh/strftime.html
use alloc::string::String;
use super::tm;
use crate::platform::{self, types::*, WriteByte};
use super::tm;
// We use the langinfo constants
use crate::header::langinfo::{
nl_item,
nl_langinfo,
ABDAY_1,
ABMON_1,
AM_STR,
DAY_1,
MON_1,
PM_STR,
// TODO : other constants if needed
};
/// A helper that calls `nl_langinfo(item)` and converts the returned pointer
/// into a `&str`. If it fails or is null, returns an empty string "".
unsafe fn langinfo_to_str(item: nl_item) -> &'static str {
use core::ffi::CStr;
let ptr = nl_langinfo(item);
if ptr.is_null() {
return "";
}
match CStr::from_ptr(ptr).to_str() {
Ok(s) => s,
Err(_) => "",
}
}
/// Formats time data according to the given `format` string.
///
/// Use `langinfo` for locale-based day/month names,
/// but still hard-codes other aspects of the "C" locale (like numeric/date
/// formats) and ignores `%E` / `%O` variations.
pub unsafe fn strftime<W: WriteByte>(w: &mut W, format: *const c_char, t: *const tm) -> size_t {
/// Helper that actually parses the format string and writes output.
pub unsafe fn inner_strftime<W: WriteByte>(
w: &mut W,
mut format: *const c_char,
@@ -22,11 +57,11 @@ pub unsafe fn strftime<W: WriteByte>(w: &mut W, format: *const c_char, t: *const
}
}};
(recurse $fmt:expr) => {{
let mut fmt = String::with_capacity($fmt.len() + 1);
fmt.push_str($fmt);
fmt.push('\0');
let mut tmp = String::with_capacity($fmt.len() + 1);
tmp.push_str($fmt);
tmp.push('\0');
if !inner_strftime(w, fmt.as_ptr() as *mut c_char, t) {
if !inner_strftime(w, tmp.as_ptr() as *mut c_char, t) {
return false;
}
}};
@@ -36,106 +71,178 @@ pub unsafe fn strftime<W: WriteByte>(w: &mut W, format: *const c_char, t: *const
}
}};
($fmt:expr, $($args:expr),+) => {{
// Would use write!() if I could get the length written
// Could use write!() if we didn't need the exact count
if write!(w, $fmt, $($args),+).is_err() {
return false;
}
}};
}
const WDAYS: [&str; 7] = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const MONTHS: [&str; 12] = [
"January",
"Febuary",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
while *format != 0 {
// If the character isn't '%', just copy it out.
if *format as u8 != b'%' {
w!(byte * format as u8);
format = format.offset(1);
continue;
}
// Skip '%'
format = format.offset(1);
// POSIX says '%E' and '%O' can modify numeric formats for locales,
// but we ignore them in this minimal "C" locale approach.
if *format as u8 == b'E' || *format as u8 == b'O' {
// Ignore because these do nothing without locale
format = format.offset(1);
}
match *format as u8 {
// Literal '%'
b'%' => w!(byte b'%'),
// Newline and tab expansions
b'n' => w!(byte b'\n'),
b't' => w!(byte b'\t'),
b'a' => w!(&WDAYS[(*t).tm_wday as usize][..3]),
b'A' => w!(WDAYS[(*t).tm_wday as usize]),
b'b' | b'h' => w!(&MONTHS[(*t).tm_mon as usize][..3]),
b'B' => w!(MONTHS[(*t).tm_mon as usize]),
// Abbreviated weekday name: %a
b'a' => {
// `ABDAY_1 + tm_wday` is the correct langinfo ID for abbreviated weekdays
let s = langinfo_to_str(ABDAY_1 + (*t).tm_wday as i32);
w!(s);
}
// Full weekday name: %A
b'A' => {
// `DAY_1 + tm_wday` is the correct langinfo ID for full weekdays
let s = langinfo_to_str(DAY_1 + (*t).tm_wday as i32);
w!(s);
}
// Abbreviated month name: %b or %h
b'b' | b'h' => {
let s = langinfo_to_str(ABMON_1 + (*t).tm_mon as i32);
w!(s);
}
// Full month name: %B
b'B' => {
let s = langinfo_to_str(MON_1 + (*t).tm_mon as i32);
w!(s);
}
// Century: %C
b'C' => {
let mut year = (*t).tm_year / 100;
// Round up
if (*t).tm_year % 100 != 0 {
year += 1;
}
w!("{:02}", year + 19);
}
// Day of month: %d
b'd' => w!("{:02}", (*t).tm_mday),
// %D => same as %m/%d/%y
b'D' => w!(recurse "%m/%d/%y"),
// Day of month, space-padded: %e
b'e' => w!("{:2}", (*t).tm_mday),
// ISO 8601 date: %F => %Y-%m-%d
b'F' => w!(recurse "%Y-%m-%d"),
// Hour (00-23): %H
b'H' => w!("{:02}", (*t).tm_hour),
// Hour (01-12): %I
b'I' => w!("{:02}", ((*t).tm_hour + 12 - 1) % 12 + 1),
// Day of year: %j
b'j' => w!("{:03}", (*t).tm_yday),
// etc.
b'k' => w!("{:2}", (*t).tm_hour),
b'l' => w!("{:2}", ((*t).tm_hour + 12 - 1) % 12 + 1),
b'm' => w!("{:02}", (*t).tm_mon + 1),
b'M' => w!("{:02}", (*t).tm_min),
b'p' => w!(if (*t).tm_hour < 12 { "AM" } else { "PM" }),
b'P' => w!(if (*t).tm_hour < 12 { "am" } else { "pm" }),
// AM/PM (uppercase): %p
b'p' => {
// Get "AM" / "PM" from langinfo
if (*t).tm_hour < 12 {
w!(langinfo_to_str(AM_STR));
} else {
w!(langinfo_to_str(PM_STR));
}
}
// am/pm (lowercase): %P
b'P' => {
// Convert the AM_STR / PM_STR to lowercase
if (*t).tm_hour < 12 {
let am = langinfo_to_str(AM_STR).to_ascii_lowercase();
w!(&am);
} else {
let pm = langinfo_to_str(PM_STR).to_ascii_lowercase();
w!(&pm);
}
}
// 12-hour clock with seconds + AM/PM: %r => %I:%M:%S %p
b'r' => w!(recurse "%I:%M:%S %p"),
// 24-hour clock without seconds: %R => %H:%M
b'R' => w!(recurse "%H:%M"),
// Nothing is modified in mktime, but the C standard of course requires a mutable pointer ._.
// Seconds since the Epoch: %s => calls mktime() to convert tm to time_t
b's' => w!("{}", super::mktime(t as *mut tm)),
// Seconds (00-60): %S (unchanged)
b'S' => w!("{:02}", (*t).tm_sec),
// 24-hour clock with seconds: %T => %H:%M:%S
b'T' => w!(recurse "%H:%M:%S"),
// Weekday (1-7, Monday=1): %u
b'u' => w!("{}", ((*t).tm_wday + 7 - 1) % 7 + 1),
// Sunday-based week of year: %U
b'U' => w!("{}", ((*t).tm_yday + 7 - (*t).tm_wday) / 7),
// Weekday (0-6, Sunday=0): %w
b'w' => w!("{}", (*t).tm_wday),
// Monday-based week of year: %W
b'W' => w!("{}", ((*t).tm_yday + 7 - ((*t).tm_wday + 6) % 7) / 7),
// Last two digits of year: %y
b'y' => w!("{:02}", (*t).tm_year % 100),
// Full year: %Y
b'Y' => w!("{}", (*t).tm_year + 1900),
b'z' => w!("+0000"), // TODO
b'Z' => w!("UTC"), // TODO
// Timezone offset: %z => currently hard-coded "+0000"
b'z' => w!("+0000"), // TODO: Add real timezone support
// Timezone name: %Z => currently "UTC"
b'Z' => w!("UTC"), // TODO: Add real timezone name support
// Date+time+TZ: %+
b'+' => w!(recurse "%a %b %d %T %Z %Y"),
// Unrecognized format specifier => fail
_ => return false,
}
// Move past the format specifier
format = format.offset(1);
}
true
}
let mut w = platform::CountingWriter::new(w);
if !inner_strftime(&mut w, format, t) {
// Wrap the writer in a CountingWriter to return how many bytes were written.
let mut cw = platform::CountingWriter::new(w);
if !inner_strftime(&mut cw, format, t) {
return 0;
}
w.written
cw.written
}
+584
View File
@@ -0,0 +1,584 @@
// https://pubs.opengroup.org/onlinepubs/7908799/xsh/strptime.html
use crate::{
header::{string::strlen, time::tm},
platform::types::size_t,
};
use alloc::{string::String, vec::Vec};
use core::{
ffi::{c_char, c_int, c_void, CStr},
mem::MaybeUninit,
ptr,
ptr::NonNull,
slice, str,
};
/// For convenience, we define some helper constants for the C-locale.
static SHORT_DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
static LONG_DAYS: [&str; 7] = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
static SHORT_MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
static LONG_MONTHS: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
#[no_mangle]
pub unsafe extern "C" fn strptime(
buf: *const c_char,
format: *const c_char,
tm: *mut tm,
) -> *mut c_char {
// Validate inputs
let buf_ptr = if let Some(ptr) = NonNull::new(buf as *const c_void as *mut c_void) {
ptr
} else {
return ptr::null_mut();
};
//
let fmt_ptr = if let Some(ptr) = NonNull::new(format as *const c_void as *mut c_void) {
ptr
} else {
return ptr::null_mut();
};
let tm_ptr = if let Some(ptr) = NonNull::new(tm) {
ptr
} else {
return ptr::null_mut();
};
// Convert raw pointers into slices/strings.
let input_str = unsafe {
if buf.is_null() {
return ptr::null_mut();
}
match CStr::from_ptr(buf).to_str() {
Ok(s) => s,
Err(_) => return ptr::null_mut(), // Not a valid UTF-8
}
};
let fmt_str = unsafe {
if format.is_null() {
return ptr::null_mut();
}
match CStr::from_ptr(format).to_str() {
Ok(s) => s,
Err(_) => return ptr::null_mut(), // Not a valid UTF-8
}
};
// Zero-initialize the output `tm` structure
// (equivalent to: tm_sec=0, tm_min=0, tm_hour=0...)
unsafe {
ptr::write_bytes(tm, 0, 1);
}
// We parse the format specifiers in a loop
let mut fmt_chars = fmt_str.chars().peekable();
let mut index_in_input = 0;
while let Some(fc) = fmt_chars.next() {
if fc != '%' {
// If it's a normal character, we expect it to match exactly in input
if input_str.len() <= index_in_input {
return ptr::null_mut(); // input ended too soon
}
let in_char = input_str.as_bytes()[index_in_input] as char;
if in_char != fc {
// mismatch
return ptr::null_mut();
}
index_in_input += 1;
continue;
}
// If we see '%', read the next character
let Some(spec) = fmt_chars.next() else {
// format string ended abruptly after '%'
return ptr::null_mut();
};
// POSIX says `%E` or `%O` are modified specifiers for locale.
// We will skip them if they appear (like strftime does) and read the next char.
let final_spec = if spec == 'E' || spec == 'O' {
match fmt_chars.next() {
Some(ch) => ch,
None => return ptr::null_mut(),
}
} else {
spec
};
// Handle known specifiers
match final_spec {
///////////////////////////
// Whitespace: %n or %t //
///////////////////////////
'n' | 't' => {
// Skip over any whitespace in the input
while index_in_input < input_str.len()
&& input_str.as_bytes()[index_in_input].is_ascii_whitespace()
{
index_in_input += 1;
}
}
///////////////////////////
// Literal % => "%%" //
///////////////////////////
'%' => {
if index_in_input >= input_str.len()
|| input_str.as_bytes()[index_in_input] as char != '%'
{
return ptr::null_mut();
}
index_in_input += 1;
}
///////////////////////////
// Day of Month: %d / %e //
///////////////////////////
'd' | 'e' => {
// parse a 2-digit day (with or without leading zero)
let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
Some(v) => unsafe { v },
None => return ptr::null_mut(),
};
unsafe {
(*tm).tm_mday = val as c_int;
}
index_in_input += len;
}
///////////////////////////
// Month: %m //
///////////////////////////
'm' => {
// parse a 2-digit month
let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
Some(v) => v,
None => return ptr::null_mut(),
};
// tm_mon is 0-based (0 = Jan, 1 = Feb,...)
unsafe {
(*tm).tm_mon = (val as c_int) - 1;
if (*tm).tm_mon < 0 || (*tm).tm_mon > 11 {
return ptr::null_mut();
}
}
index_in_input += len;
}
//////////////////////////////
// Year without century: %y //
//////////////////////////////
'y' => {
// parse a 2-digit year
let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
Some(v) => v,
None => return ptr::null_mut(),
};
// According to POSIX, %y in strptime is [00,99], and the "year" is 1900..1999 for [00..99],
// but the standard says: "values in [69..99] refer to 1969..1999, [00..68] => 2000..2068"
let fullyear = if val >= 69 { val + 1900 } else { val + 2000 };
unsafe {
(*tm).tm_year = (fullyear - 1900) as c_int;
}
index_in_input += len;
}
///////////////////////////
// Year with century: %Y //
///////////////////////////
'Y' => {
// parse up to 4-digit (or more) year
// We allow more than 4 digits if needed
let (val, len) = match parse_int(&input_str[index_in_input..], 4, true) {
Some(v) => v,
None => return ptr::null_mut(),
};
unsafe {
(*tm).tm_year = (val as c_int) - 1900;
}
index_in_input += len;
}
///////////////////////////
// Hour (00..23): %H //
///////////////////////////
'H' => {
let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
Some(v) => v,
None => return ptr::null_mut(),
};
if val > 23 {
return ptr::null_mut();
}
unsafe {
(*tm).tm_hour = val as c_int;
}
index_in_input += len;
}
///////////////////////////
// Hour (01..12): %I //
///////////////////////////
'I' => {
let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
Some(v) => v,
None => return ptr::null_mut(),
};
if val < 1 || val > 12 {
return ptr::null_mut();
}
unsafe {
(*tm).tm_hour = val as c_int;
}
// Well interpret AM/PM with %p if it appears later
index_in_input += len;
}
///////////////////////////
// Minute (00..59): %M //
///////////////////////////
'M' => {
let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
Some(v) => v,
None => return ptr::null_mut(),
};
if val > 59 {
return ptr::null_mut();
}
unsafe {
(*tm).tm_min = val as c_int;
}
index_in_input += len;
}
///////////////////////////
// Seconds (00..60): %S //
///////////////////////////
'S' => {
let (val, len) = match parse_int(&input_str[index_in_input..], 2, false) {
Some(v) => v,
None => return ptr::null_mut(),
};
if val > 60 {
return ptr::null_mut();
}
unsafe {
(*tm).tm_sec = val as c_int;
}
index_in_input += len;
}
///////////////////////////
// AM/PM: %p //
///////////////////////////
'p' => {
// Parse either "AM" or "PM" (no case-sensitive)
// We'll read up to 2 or 3 letters from input ("AM", "PM")
let leftover = &input_str[index_in_input..];
let parsed_len = match parse_am_pm(leftover) {
Some((is_pm, used)) => {
if unsafe { (*tm).tm_hour } == 12 {
// 12 AM => 00:xx, 12 PM => 12:xx
unsafe {
(*tm).tm_hour = if is_pm { 12 } else { 0 };
}
} else {
// 1..11 AM => 1..11, 1..11 PM => 13..23
if is_pm {
unsafe {
(*tm).tm_hour += 12;
}
}
}
used
}
None => return ptr::null_mut(),
};
index_in_input += parsed_len;
}
///////////////////////////
// Weekday Name: %a/%A //
///////////////////////////
'a' => {
// Abbreviated day name (Sun..Sat)
let leftover = &input_str[index_in_input..];
let parsed_len = match parse_weekday(leftover, true) {
Some((wday, used)) => {
unsafe {
(*tm).tm_wday = wday as c_int;
}
used
}
None => return ptr::null_mut(),
};
index_in_input += parsed_len;
}
'A' => {
// Full day name (Sunday..Saturday)
let leftover = &input_str[index_in_input..];
let parsed_len = match parse_weekday(leftover, false) {
Some((wday, used)) => {
unsafe {
(*tm).tm_wday = wday as c_int;
}
used
}
None => return ptr::null_mut(),
};
index_in_input += parsed_len;
}
///////////////////////////
// Month Name: %b/%B/%h //
///////////////////////////
'b' | 'h' => {
// Abbreviated month name
let leftover = &input_str[index_in_input..];
let parsed_len = match parse_month(leftover, true) {
Some((mon, used)) => {
unsafe {
(*tm).tm_mon = mon as c_int;
}
used
}
None => return ptr::null_mut(),
};
index_in_input += parsed_len;
}
'B' => {
// Full month name
let leftover = &input_str[index_in_input..];
let parsed_len = match parse_month(leftover, false) {
Some((mon, used)) => {
unsafe {
(*tm).tm_mon = mon as c_int;
}
used
}
None => return ptr::null_mut(),
};
index_in_input += parsed_len;
}
///////////////////////////
// Day of year: %j //
///////////////////////////
'j' => {
// parse 3-digit day of year [001..366]
let (val, len) = match parse_int(&input_str[index_in_input..], 3, false) {
Some(v) => v,
None => return ptr::null_mut(),
};
if val < 1 || val > 366 {
return ptr::null_mut();
}
// store in tm_yday
unsafe {
(*tm).tm_yday = (val - 1) as c_int;
}
index_in_input += len;
}
//////////////////////////////////
// Date shortcuts: %D, %F, etc. //
//////////////////////////////////
'D' => {
// Equivalent to "%m/%d/%y"
// We can do a mini strptime recursion or manually parse
// For simplicity, we'll do it inline here
let subfmt = "%m/%d/%y";
let used =
match unsafe { apply_subformat(&input_str[index_in_input..], subfmt, tm) } {
Some(v) => v,
None => return ptr::null_mut(),
};
index_in_input += used;
}
'F' => {
// Equivalent to "%Y-%m-%d"
let subfmt = "%Y-%m-%d";
let used =
match unsafe { apply_subformat(&input_str[index_in_input..], subfmt, tm) } {
Some(v) => v,
None => return ptr::null_mut(),
};
index_in_input += used;
}
//////////////////////////////////////////////////////////
// TODO : not implemented: %x, %X, %c, %r, %R, %T, etc. //
//////////////////////////////////////////////////////////
// Hint : if you want to implement these, do similarly to %D / %F (or parse manually)
'x' | 'X' | 'c' | 'r' | 'R' | 'T' => {
// Return NULL if we dont want to accept them :
return ptr::null_mut();
}
///////////////////////////
// Timezone: %Z or %z //
///////////////////////////
'Z' | 'z' => {
// Full/abbrev time zone name or numeric offset
// Implementation omitted. Real support is quite complicated.
return ptr::null_mut();
}
//////////
// else //
//////////
_ => {
// We do not recognize this specifier
return ptr::null_mut();
}
}
}
// If we got here, parsing was successful. Return pointer to the
// next unparsed character in `buf`.
let ret_ptr = unsafe { buf.add(index_in_input) };
ret_ptr as *mut c_char
}
// -----------------------
// Helper / Parsing Logic
// -----------------------
/// Parse an integer from the beginning of `input_str`.
///
/// - `width` is the maximum number of digits to parse
/// - `allow_variable_width` indicates if we can parse fewer digits
/// (e.g., `%Y` can have more than 4 digits, but also might parse "2023" or "12345").
fn parse_int(input: &str, width: usize, allow_variable: bool) -> Option<(i32, usize)> {
let mut val = 0i32;
let mut chars = input.chars();
let mut count = 0;
while let Some(c) = chars.next() {
if !c.is_ascii_digit() {
break;
}
// Check for integer overflow
val = val.checked_mul(10)?.checked_add((c as u8 - b'0') as i32)?;
count += 1;
if count == width && !allow_variable {
break;
}
}
if count == 0 {
None
} else {
Some((val, count))
}
}
/// Handle AM/PM. Returns (is_pm, length_consumed).
/// Accepts "AM", "am", "PM", "pm" case-insensitively.
fn parse_am_pm(s: &str) -> Option<(bool, usize)> {
// Trim leading whitespace?
// For simplicity, we do not. If needed, handle it.
let s_up = s.to_ascii_uppercase();
if s_up.starts_with("AM") {
return Some((false, 2));
}
if s_up.starts_with("PM") {
return Some((true, 2));
}
None
}
/// Parse a weekday name from `s`.
/// - if `abbrev == true`, match short forms: "Mont".."Sun"
/// - otherwise, match "Monday".."Sunday"
/// Return (weekday_index, length_consumed).
fn parse_weekday(s: &str, abbrev: bool) -> Option<(usize, usize)> {
let list = if abbrev { &SHORT_DAYS } else { &LONG_DAYS };
for (i, name) in list.iter().enumerate() {
if s.len() >= name.len() && s[0..name.len()].eq_ignore_ascii_case(name) {
return Some((i, name.len()));
}
}
None
}
/// Parse a month name from `s`.
/// - If `abbrev == true`, match short forms: "Jan".."Dec"
/// - Otherwise, match "January".."December"
/// Return (month_index, length_consumed).
fn parse_month(s: &str, abbrev: bool) -> Option<(usize, usize)> {
let list = if abbrev { &SHORT_MONTHS } else { &LONG_MONTHS };
for (i, name) in list.iter().enumerate() {
if s.len() >= name.len() && s[0..name.len()].eq_ignore_ascii_case(name) {
return Some((i, name.len()));
}
}
None
}
/// Apply a small subformat (like "%m/%d/%y" or "%Y-%m-%d") to `input`.
/// Return how many characters of `input` were consumed or None on error.
unsafe fn apply_subformat(input: &str, subfmt: &str, tm: *mut tm) -> Option<usize> {
// We'll do a temporary strptime call on a substring.
// Then we see how many chars it consumed. If that call fails, we return None.
// Otherwise, we return the count.
// Convert `input` to a null-terminated buffer temporarily
let mut tmpbuf = String::with_capacity(input.len() + 1);
tmpbuf.push_str(input);
tmpbuf.push('\0');
let mut tmpfmt = String::with_capacity(subfmt.len() + 1);
tmpfmt.push_str(subfmt);
tmpfmt.push('\0');
// We need a copy of the tm, so if partial parse fails, we don't override.
let old_tm = unsafe { ptr::read(tm) }; // backup
let consumed_ptr = unsafe {
strptime(
tmpbuf.as_ptr() as *const c_char,
tmpfmt.as_ptr() as *const c_char,
tm,
)
};
if consumed_ptr.is_null() {
// revert
unsafe {
*tm = old_tm;
}
return None;
}
// consumed_ptr - tmpbuf.as_ptr() => # of bytes consumed
let diff = (consumed_ptr as usize) - (tmpbuf.as_ptr() as usize);
Some(diff)
}