-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
date: fix handling of case-change flags in locale format specifiers #9897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,8 +64,103 @@ cfg_langinfo! { | |
| }) | ||
| } | ||
|
|
||
| /// Retrieves the date/time format string from the system locale | ||
| fn get_locale_format_string() -> Option<String> { | ||
| /// Replaces %c, %x, %X with their locale-specific format strings. | ||
| /// | ||
| /// If a flag like `^` is present (e.g., `%^c`), it is distributed to the | ||
| /// sub-specifiers within the locale string. | ||
| pub fn expand_locale_format(format: &str) -> std::borrow::Cow<'_, str> { | ||
| let mut result = String::with_capacity(format.len()); | ||
| let mut chars = format.chars().peekable(); | ||
| let mut modified = false; | ||
|
|
||
| while let Some(c) = chars.next() { | ||
| if c != '%' { | ||
| result.push(c); | ||
| continue; | ||
| } | ||
|
|
||
| // Capture flags | ||
| let mut flags = Vec::new(); | ||
| while let Some(&peek) = chars.peek() { | ||
| match peek { | ||
| '_' | '-' | '0' | '^' | '#' => { | ||
| flags.push(peek); | ||
| chars.next(); | ||
| }, | ||
| _ => break, | ||
| } | ||
| } | ||
|
|
||
| match chars.peek() { | ||
| Some(&spec @ ('c' | 'x' | 'X')) => { | ||
| chars.next(); | ||
|
|
||
| let item = match spec { | ||
| 'c' => libc::D_T_FMT, | ||
| 'x' => libc::D_FMT, | ||
| 'X' => libc::T_FMT, | ||
| _ => unreachable!(), | ||
| }; | ||
|
|
||
| if let Some(s) = get_langinfo(item) { | ||
| // If the user requested uppercase (%^c), distribute that flag | ||
| // to the expanded specifiers. | ||
| let replacement = if flags.contains(&'^') { | ||
| distribute_flag(&s, '^') | ||
| } else { | ||
| s | ||
| }; | ||
| result.push_str(&replacement); | ||
| modified = true; | ||
| } else { | ||
| // Reconstruct original sequence if lookup fails | ||
| result.push('%'); | ||
| result.extend(flags); | ||
| result.push(spec); | ||
| } | ||
| }, | ||
| Some(_) | None => { | ||
| // Not a locale specifier, or end of string. | ||
| // Push captured flags and let loop handle the next char. | ||
| result.push('%'); | ||
| result.extend(flags); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if modified { | ||
| std::borrow::Cow::Owned(result) | ||
| } else { | ||
| std::borrow::Cow::Borrowed(format) | ||
| } | ||
| } | ||
|
|
||
| /// Distributes the given flag to all format specifiers in the string. | ||
| /// | ||
| /// For example, if the format string is "%a %b" and the flag is '^', | ||
| /// the result will be "%^a %^b". Literal percent signs ("%%") are skipped | ||
| /// and left as is. | ||
| fn distribute_flag(fmt: &str, flag: char) -> String { | ||
CrazyRoka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| let mut res = String::with_capacity(fmt.len() * 2); | ||
| let mut chars = fmt.chars().peekable(); | ||
| while let Some(c) = chars.next() { | ||
| res.push(c); | ||
| if c == '%' { | ||
| if let Some(&n) = chars.peek() { | ||
| if n == '%' { | ||
| chars.next(); | ||
| res.push('%'); | ||
| } else { | ||
| res.push(flag); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| res | ||
| } | ||
|
|
||
| /// Retrieves the date/time format string from the system locale (D_T_FMT, D_FMT, T_FMT) | ||
| pub fn get_langinfo(item: libc::nl_item) -> Option<String> { | ||
| // In tests, acquire mutex to prevent race conditions with setlocale() | ||
| // which is process-global and not thread-safe | ||
| #[cfg(test)] | ||
|
|
@@ -76,12 +171,12 @@ cfg_langinfo! { | |
| libc::setlocale(libc::LC_TIME, c"".as_ptr()); | ||
|
|
||
| // Get the date/time format string | ||
| let d_t_fmt_ptr = libc::nl_langinfo(libc::D_T_FMT); | ||
| if d_t_fmt_ptr.is_null() { | ||
| let fmt_ptr = libc::nl_langinfo(item); | ||
| if fmt_ptr.is_null() { | ||
| return None; | ||
| } | ||
|
|
||
| let format = CStr::from_ptr(d_t_fmt_ptr).to_str().ok()?; | ||
| let format = CStr::from_ptr(fmt_ptr).to_str().ok()?; | ||
| if format.is_empty() { | ||
| return None; | ||
| } | ||
|
|
@@ -90,6 +185,11 @@ cfg_langinfo! { | |
| } | ||
| } | ||
|
|
||
| /// Retrieves the date/time format string from the system locale | ||
| fn get_locale_format_string() -> Option<String> { | ||
| get_langinfo(libc::D_T_FMT) | ||
| } | ||
|
|
||
| /// Ensures the format string includes timezone (%Z) | ||
| fn ensure_timezone_in_format(format: &str) -> String { | ||
| if format.contains("%Z") { | ||
|
|
@@ -123,6 +223,18 @@ pub fn get_locale_default_format() -> &'static str { | |
| "%a %b %e %X %Z %Y" | ||
| } | ||
|
|
||
| #[cfg(not(any( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems to be unix - no ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am following the pattern used elsewhere in the file. Do you suggest using |
||
| target_os = "linux", | ||
| target_vendor = "apple", | ||
| target_os = "freebsd", | ||
| target_os = "netbsd", | ||
| target_os = "openbsd", | ||
| target_os = "dragonfly" | ||
| )))] | ||
| pub fn expand_locale_format(format: &str) -> std::borrow::Cow<'_, str> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why this function btw?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is for |
||
| std::borrow::Cow::Borrowed(format) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| cfg_langinfo! { | ||
|
|
@@ -260,4 +372,43 @@ mod tests { | |
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| #[cfg(any( | ||
| target_os = "linux", | ||
| target_vendor = "apple", | ||
| target_os = "freebsd", | ||
| target_os = "netbsd", | ||
| target_os = "openbsd", | ||
| target_os = "dragonfly" | ||
| ))] | ||
| fn test_distribute_flag() { | ||
| // Standard distribution | ||
| assert_eq!(distribute_flag("%a %b", '^'), "%^a %^b"); | ||
| // Ignore literals | ||
| assert_eq!(distribute_flag("foo %a bar", '_'), "foo %_a bar"); | ||
| // Skip escaped percent signs | ||
| assert_eq!(distribute_flag("%% %a", '^'), "%% %^a"); | ||
| // Handle flags that might already exist | ||
| assert_eq!(distribute_flag("%_a", '^'), "%^_a"); | ||
| } | ||
|
|
||
| #[test] | ||
| #[cfg(any( | ||
| target_os = "linux", | ||
| target_vendor = "apple", | ||
| target_os = "freebsd", | ||
| target_os = "netbsd", | ||
| target_os = "openbsd", | ||
| target_os = "dragonfly" | ||
| ))] | ||
| fn test_expand_locale_format_basic() { | ||
| let format = "%^x"; | ||
| let expanded = expand_locale_format(format).to_string().to_lowercase(); | ||
|
|
||
| assert_ne!(expanded, "%^x", "Should have expanded %^x"); | ||
| assert!(expanded.contains("%^d"), "Should contain %^d"); | ||
| assert!(expanded.contains("%^m"), "Should contain %^m"); | ||
| assert!(expanded.contains("%^y"), "Should contain %^y"); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.