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:
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cmake.ignoreCMakeListsMissing": true
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
// We’ll 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 don’t 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)
|
||||
}
|
||||
Reference in New Issue
Block a user