use std::ffi::CStr;
use std::sync::{Mutex, OnceLock};
use crate::ported::zsh_h::features;
use crate::utils::unmetafy;
use crate::zsh_h::module;
#[cfg(unix)]
static NL_TABLE: &[(&str, libc::nl_item)] = &[
("CODESET", libc::CODESET),
("D_T_FMT", libc::D_T_FMT),
("D_FMT", libc::D_FMT),
("T_FMT", libc::T_FMT),
("RADIXCHAR", libc::RADIXCHAR),
("THOUSEP", libc::THOUSEP),
("YESEXPR", libc::YESEXPR),
("NOEXPR", libc::NOEXPR),
#[cfg(target_os = "linux")]
("CRNCYSTR", libc::CRNCYSTR),
("ABDAY_1", libc::ABDAY_1),
("ABDAY_2", libc::ABDAY_2),
("ABDAY_3", libc::ABDAY_3),
("ABDAY_4", libc::ABDAY_4),
("ABDAY_5", libc::ABDAY_5),
("ABDAY_6", libc::ABDAY_6),
("ABDAY_7", libc::ABDAY_7),
("DAY_1", libc::DAY_1),
("DAY_2", libc::DAY_2),
("DAY_3", libc::DAY_3),
("DAY_4", libc::DAY_4),
("DAY_5", libc::DAY_5),
("DAY_6", libc::DAY_6),
("DAY_7", libc::DAY_7),
("ABMON_1", libc::ABMON_1),
("ABMON_2", libc::ABMON_2),
("ABMON_3", libc::ABMON_3),
("ABMON_4", libc::ABMON_4),
("ABMON_5", libc::ABMON_5),
("ABMON_6", libc::ABMON_6),
("ABMON_7", libc::ABMON_7),
("ABMON_8", libc::ABMON_8),
("ABMON_9", libc::ABMON_9),
("ABMON_10", libc::ABMON_10),
("ABMON_11", libc::ABMON_11),
("ABMON_12", libc::ABMON_12),
("MON_1", libc::MON_1),
("MON_2", libc::MON_2),
("MON_3", libc::MON_3),
("MON_4", libc::MON_4),
("MON_5", libc::MON_5),
("MON_6", libc::MON_6),
("MON_7", libc::MON_7),
("MON_8", libc::MON_8),
("MON_9", libc::MON_9),
("MON_10", libc::MON_10),
("MON_11", libc::MON_11),
("MON_12", libc::MON_12),
("T_FMT_AMPM", libc::T_FMT_AMPM),
("AM_STR", libc::AM_STR),
("PM_STR", libc::PM_STR),
("ERA", libc::ERA),
("ERA_D_FMT", libc::ERA_D_FMT),
("ERA_D_T_FMT", libc::ERA_D_T_FMT),
("ERA_T_FMT", libc::ERA_T_FMT),
("ALT_DIGITS", libc::ALT_DIGITS),
];
#[cfg(unix)]
pub fn liitem(name: &str) -> Option<libc::nl_item> {
NL_TABLE.iter().find(|(n, _)| *n == name).map(|(_, v)| *v) }
#[cfg(not(unix))]
#[allow(unused_variables)]
pub fn liitem(name: &str) -> Option<i32> {
None
}
#[cfg(unix)]
pub fn getlanginfo(name: &str) -> Option<String> {
let mut buf = name.as_bytes().to_vec(); unmetafy(&mut buf); let nameu = std::str::from_utf8(&buf).ok()?;
let elem = liitem(nameu)?; unsafe {
let ptr = libc::nl_langinfo(elem); if ptr.is_null() {
return None; }
let s = CStr::from_ptr(ptr).to_string_lossy().into_owned();
if s.is_empty() {
return None;
}
Some(s) }
}
#[cfg(not(unix))]
pub fn getlanginfo(_name: &str) -> Option<String> {
None
}
pub fn scanlanginfo() -> Vec<(String, String)> {
let mut out = Vec::new();
for &name in NL_NAMES {
if let Some(v) = getlanginfo(name) {
out.push((name.to_string(), v)); }
}
out
}
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 {
0 }
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
*features = featuresarray(m, module_features());
0 }
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
handlefeatures(m, module_features(), enables) }
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 {
0 }
pub fn cleanup_(m: *const module) -> i32 {
setfeatureenables(m, module_features(), None) }
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 {
0 }
pub static NL_NAMES: &[&str] = &[
"CODESET",
"D_T_FMT",
"D_FMT",
"T_FMT",
"RADIXCHAR",
"THOUSEP",
"YESEXPR",
"NOEXPR",
"CRNCYSTR",
"ABDAY_1",
"ABDAY_2",
"ABDAY_3",
"ABDAY_4",
"ABDAY_5",
"ABDAY_6",
"ABDAY_7",
"DAY_1",
"DAY_2",
"DAY_3",
"DAY_4",
"DAY_5",
"DAY_6",
"DAY_7",
"ABMON_1",
"ABMON_2",
"ABMON_3",
"ABMON_4",
"ABMON_5",
"ABMON_6",
"ABMON_7",
"ABMON_8",
"ABMON_9",
"ABMON_10",
"ABMON_11",
"ABMON_12",
"MON_1",
"MON_2",
"MON_3",
"MON_4",
"MON_5",
"MON_6",
"MON_7",
"MON_8",
"MON_9",
"MON_10",
"MON_11",
"MON_12",
"T_FMT_AMPM",
"AM_STR",
"PM_STR",
"ERA",
"ERA_D_FMT",
"ERA_D_T_FMT",
"ERA_T_FMT",
"ALT_DIGITS",
];
static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
vec!["p:langinfo".to_string()]
}
fn handlefeatures(
_m: *const module,
_f: &Mutex<features>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 1]);
}
0
}
fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
0
}
fn module_features() -> &'static Mutex<features> {
MODULE_FEATURES.get_or_init(|| {
Mutex::new(features {
bn_list: None,
bn_size: 0,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 1,
n_abstract: 0,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nl_names_includes_codeset() {
let _g = crate::test_util::global_state_lock();
assert!(NL_NAMES.contains(&"CODESET"));
assert!(NL_NAMES.contains(&"D_T_FMT"));
}
#[cfg(unix)]
#[test]
fn getlanginfo_codeset_is_some() {
let _g = crate::test_util::global_state_lock();
assert!(getlanginfo("CODESET").is_some());
}
#[test]
fn getlanginfo_invalid_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getlanginfo("INVALID_NAME").is_none());
}
#[cfg(unix)]
#[test]
fn liitem_codeset_resolves() {
let _g = crate::test_util::global_state_lock();
assert!(liitem("CODESET").is_some());
assert!(liitem("DOES_NOT_EXIST").is_none());
}
#[cfg(unix)]
#[test]
fn scanlanginfo_emits_items() {
let _g = crate::test_util::global_state_lock();
let v = scanlanginfo();
assert!(!v.is_empty());
assert!(v.iter().any(|(k, _)| k == "CODESET"));
}
#[test]
fn nl_names_covers_canonical_locale_items() {
let _g = crate::test_util::global_state_lock();
for required in [
"CODESET",
"D_T_FMT",
"D_FMT",
"T_FMT",
"T_FMT_AMPM",
"AM_STR",
"PM_STR",
"DAY_1",
"DAY_7",
"ABDAY_1",
"MON_1",
"MON_12",
"RADIXCHAR",
"THOUSEP",
"YESEXPR",
"NOEXPR",
] {
assert!(
NL_NAMES.contains(&required),
"NL_NAMES missing {} — port table truncated?",
required
);
}
}
#[test]
fn nl_names_entries_are_uppercase_identifiers() {
let _g = crate::test_util::global_state_lock();
for &n in NL_NAMES {
assert!(!n.is_empty(), "empty entry in NL_NAMES");
assert!(
n.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_'),
"NL_NAMES entry {:?} contains non-uppercase chars",
n
);
assert!(
!n.starts_with(|c: char| c.is_ascii_digit()),
"NL_NAMES entry {:?} starts with a digit",
n
);
}
}
#[test]
fn nl_names_has_no_duplicates() {
let _g = crate::test_util::global_state_lock();
let unique: std::collections::HashSet<_> = NL_NAMES.iter().copied().collect();
assert_eq!(unique.len(), NL_NAMES.len(), "duplicate entry in NL_NAMES");
}
#[cfg(unix)]
#[test]
fn scanlanginfo_keys_are_subset_of_nl_names() {
let _g = crate::test_util::global_state_lock();
for (k, _) in scanlanginfo() {
assert!(
NL_NAMES.contains(&k.as_str()),
"scanlanginfo emitted {:?} which is not in NL_NAMES",
k
);
}
}
#[cfg(unix)]
#[test]
fn getlanginfo_is_case_sensitive() {
let _g = crate::test_util::global_state_lock();
assert!(getlanginfo("CODESET").is_some());
assert!(
getlanginfo("codeset").is_none(),
"getlanginfo must be case-sensitive per the C source's strcmp lookup"
);
}
#[test]
fn module_lifecycle_shims_all_return_zero() {
let _g = crate::test_util::global_state_lock();
let m: *const crate::ported::zsh_h::module = std::ptr::null();
assert_eq!(setup_(m), 0);
assert_eq!(boot_(m), 0);
assert_eq!(cleanup_(m), 0);
assert_eq!(finish_(m), 0);
}
#[test]
fn langinfo_corpus_codeset_is_known() {
let _g = crate::test_util::global_state_lock();
assert!(liitem("CODESET").is_some(),
"CODESET is a POSIX-standard nl_item key");
}
#[test]
fn langinfo_corpus_day_1_is_known() {
let _g = crate::test_util::global_state_lock();
assert!(liitem("DAY_1").is_some(),
"DAY_1 is a POSIX-standard nl_item key");
}
#[test]
fn langinfo_corpus_radixchar_is_known() {
let _g = crate::test_util::global_state_lock();
assert!(liitem("RADIXCHAR").is_some());
}
#[test]
fn langinfo_corpus_unknown_key_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(liitem("BOGUS_NOT_REAL_KEY").is_none());
assert!(liitem("").is_none());
}
#[test]
fn langinfo_corpus_getlanginfo_codeset_nonempty() {
let _g = crate::test_util::global_state_lock();
let r = getlanginfo("CODESET");
assert!(r.is_some(), "CODESET resolves to a string");
let s = r.unwrap();
assert!(!s.is_empty(), "CODESET non-empty (e.g. 'UTF-8'), got {s:?}");
}
#[test]
fn langinfo_corpus_scanlanginfo_returns_entries() {
let _g = crate::test_util::global_state_lock();
let entries = scanlanginfo();
assert!(!entries.is_empty(),
"scanlanginfo should return some entries");
}
}