use crate::ported::zsh_h::features;
use crate::ported::zsh_h::{
hashnode, param, HashTable, Param, ScanFunc, PM_READONLY, PM_SCALAR,
};
use crate::utils::unmetafy;
use crate::zsh_h::module;
use std::ffi::CStr;
use std::sync::{Mutex, OnceLock};
#[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(_ht: *mut HashTable, name: &str) -> Option<Param> {
let mut buf = name.as_bytes().to_vec(); unmetafy(&mut buf); let nameu = std::str::from_utf8(&buf).ok()?;
let elem = liitem(nameu)?; let listr = unsafe {
let ptr = libc::nl_langinfo(elem); if ptr.is_null() {
return None; }
CStr::from_ptr(ptr).to_string_lossy().into_owned() };
Some(Box::new(param {
node: hashnode {
next: None,
nam: name.to_string(),
flags: PM_SCALAR as i32 | PM_READONLY as i32, },
u_data: 0,
u_tied: None,
u_arr: None,
u_str: Some(listr), u_val: 0,
u_dval: 0.0,
u_hash: None,
gsu_s: None,
gsu_i: None,
gsu_f: None,
gsu_a: None,
gsu_h: None,
base: 0,
width: 0,
env: None,
ename: None,
old: None,
level: 0,
}))
}
#[cfg(not(unix))]
#[allow(unused_variables)]
pub fn getlanginfo(_ht: *mut HashTable, _name: &str) -> Option<Param> {
None
}
pub fn scanlanginfo(_ht: *mut HashTable, func: Option<ScanFunc>, flags: i32) {
let f = match func {
Some(f) => f,
None => return,
};
for &name in NL_NAMES {
if let Some(pm) = getlanginfo(std::ptr::null_mut(), name) {
let node_box = Box::new(pm.node.clone());
f(&node_box, flags); }
}
}
#[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::*;
fn getli(name: &str) -> Option<String> {
getlanginfo(std::ptr::null_mut(), name).and_then(|p| p.u_str)
}
fn scanli() -> Vec<(String, String)> {
use std::cell::RefCell;
thread_local! {
static KEYS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
KEYS.with(|k| k.borrow_mut().clear());
fn cb(node: &crate::ported::zsh_h::HashNode, _flags: i32) {
KEYS.with(|k| k.borrow_mut().push(node.nam.clone()));
}
scanlanginfo(std::ptr::null_mut(), Some(cb), 0);
KEYS.with(|k| {
k.borrow()
.iter()
.map(|name| (name.clone(), getli(name).unwrap_or_default()))
.collect()
})
}
#[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!(getli("CODESET").is_some());
}
#[test]
fn getlanginfo_invalid_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getli("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 = scanli();
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 scanli() {
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!(getli("CODESET").is_some());
assert!(
getli("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 = getli("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 = scanli();
assert!(
!entries.is_empty(),
"scanlanginfo should return some entries"
);
}
#[test]
#[cfg(unix)]
fn liitem_canonical_posix_names_resolve() {
let _g = crate::test_util::global_state_lock();
assert!(liitem("CODESET").is_some(), "CODESET is POSIX-required");
assert!(liitem("D_FMT").is_some(), "D_FMT is POSIX-required");
assert!(liitem("T_FMT").is_some(), "T_FMT is POSIX-required");
}
#[test]
#[cfg(unix)]
fn liitem_is_deterministic() {
let _g = crate::test_util::global_state_lock();
for name in &["CODESET", "BOGUS_XYZ", "", "AM_STR"] {
let first = liitem(name);
for _ in 0..5 {
assert_eq!(liitem(name), first, "{:?} must be pure", name);
}
}
}
#[test]
#[cfg(unix)]
fn getlanginfo_empty_name_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getli("").is_none());
}
#[test]
#[cfg(unix)]
fn getlanginfo_unknown_name_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getli("zzz_never_a_real_key").is_none());
}
#[test]
#[cfg(unix)]
fn scanlanginfo_includes_codeset() {
let _g = crate::test_util::global_state_lock();
let entries = scanli();
let has_codeset = entries.iter().any(|(k, _)| k == "CODESET");
assert!(has_codeset, "POSIX CODESET must appear in scan output");
}
#[test]
#[cfg(unix)]
fn scanlanginfo_is_deterministic_for_static_locale() {
let _g = crate::test_util::global_state_lock();
let a = scanli();
let b = scanli();
assert_eq!(a, b, "two consecutive scans must agree");
}
#[test]
fn langinfo_setup_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(setup_(std::ptr::null()), 0);
}
#[test]
fn langinfo_boot_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(boot_(std::ptr::null()), 0);
}
#[cfg(unix)]
#[test]
fn liitem_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(liitem("").is_none(), "empty name → None");
}
#[cfg(unix)]
#[test]
fn liitem_is_deterministic_full_sweep() {
let _g = crate::test_util::global_state_lock();
for s in ["CODESET", "DAY_1", "MON_1", "RADIXCHAR", "__unknown_xyz__"] {
let first = liitem(s);
for _ in 0..3 {
assert_eq!(liitem(s), first, "liitem({:?}) must be deterministic", s);
}
}
}
#[cfg(unix)]
#[test]
fn getlanginfo_returns_option_string_type() {
let _g = crate::test_util::global_state_lock();
let _: Option<String> = getli("CODESET");
}
#[cfg(unix)]
#[test]
fn getlanginfo_deterministic_for_codeset() {
let _g = crate::test_util::global_state_lock();
let first = getli("CODESET");
for _ in 0..3 {
assert_eq!(
getli("CODESET"),
first,
"getli('CODESET') must be deterministic"
);
}
}
#[test]
fn scanlanginfo_returns_vec_tuple_type() {
let _g = crate::test_util::global_state_lock();
let _: Vec<(String, String)> = scanli();
}
#[cfg(unix)]
#[test]
fn scanlanginfo_full_sweep_deterministic() {
let _g = crate::test_util::global_state_lock();
let first = scanli();
for _ in 0..3 {
assert_eq!(
scanli(),
first,
"scanlanginfo must be fully deterministic"
);
}
}
#[test]
fn langinfo_cleanup_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(cleanup_(std::ptr::null()), 0);
}
#[test]
fn langinfo_finish_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(finish_(std::ptr::null()), 0);
}
#[cfg(unix)]
#[test]
fn scanlanginfo_entries_are_ascii_keys() {
let _g = crate::test_util::global_state_lock();
let entries = scanli();
for (k, _v) in &entries {
assert!(k.is_ascii(), "key {:?} must be ASCII", k);
}
}
#[test]
fn langinfo_setup_cleanup_round_trip_safe() {
let _g = crate::test_util::global_state_lock();
assert_eq!(setup_(std::ptr::null()), 0);
assert_eq!(cleanup_(std::ptr::null()), 0);
assert_eq!(setup_(std::ptr::null()), 0);
assert_eq!(cleanup_(std::ptr::null()), 0);
}
#[cfg(unix)]
#[test]
fn liitem_empty_string_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(liitem("").is_none(), "empty langinfo item must be None");
}
#[cfg(unix)]
#[test]
fn liitem_nonsense_name_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(
liitem("___definitely_not_a_langinfo_item_xyz___").is_none(),
"unknown langinfo name must be None"
);
}
#[cfg(unix)]
#[test]
fn liitem_codeset_returns_some() {
let _g = crate::test_util::global_state_lock();
assert!(
liitem("CODESET").is_some(),
"CODESET must resolve on every POSIX libc"
);
}
#[cfg(unix)]
#[test]
fn getlanginfo_empty_name_returns_none_alt() {
let _g = crate::test_util::global_state_lock();
assert!(
getli("").is_none(),
"empty langinfo name must yield None"
);
}
#[cfg(unix)]
#[test]
fn getlanginfo_nonsense_name_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getli("___bogus_langinfo_xyz___").is_none());
}
#[cfg(unix)]
#[test]
fn scanlanginfo_no_empty_keys() {
let _g = crate::test_util::global_state_lock();
for (k, _v) in scanli() {
assert!(!k.is_empty(), "no scanlanginfo entry may have empty key");
}
}
#[cfg(unix)]
#[test]
fn scanlanginfo_no_duplicate_keys() {
let _g = crate::test_util::global_state_lock();
let entries = scanli();
let mut seen = std::collections::HashSet::new();
for (k, _) in &entries {
assert!(
seen.insert(k.clone()),
"duplicate langinfo key {:?} in scanlanginfo output",
k
);
}
}
#[cfg(unix)]
#[test]
fn scanlanginfo_keys_are_uppercase_ascii() {
let _g = crate::test_util::global_state_lock();
for (k, _) in scanli() {
assert!(
k.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_'),
"langinfo key {:?} must be uppercase ASCII + digits + underscore",
k
);
}
}
#[test]
fn langinfo_setup_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = setup_(std::ptr::null());
}
#[test]
fn langinfo_features_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let mut v: Vec<String> = Vec::new();
let _: i32 = features_(std::ptr::null(), &mut v);
}
#[test]
fn langinfo_boot_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = boot_(std::ptr::null());
}
#[test]
fn langinfo_setup_idempotent_alt_pin() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(setup_(std::ptr::null()), 0);
}
}
#[test]
fn langinfo_cleanup_idempotent_alt_pin() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(cleanup_(std::ptr::null()), 0);
}
}
#[test]
fn langinfo_finish_idempotent_alt_pin() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(finish_(std::ptr::null()), 0);
}
}
#[test]
fn langinfo_cleanup_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = cleanup_(std::ptr::null());
}
#[test]
fn langinfo_finish_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = finish_(std::ptr::null());
}
#[test]
fn liitem_empty_name_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(liitem("").is_none(), "empty name must yield None");
}
#[test]
fn liitem_unknown_name_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(
liitem("__never_real_li_item_xyz__").is_none(),
"unknown name must yield None"
);
}
#[test]
fn getlanginfo_empty_name_returns_none_alt_2() {
let _g = crate::test_util::global_state_lock();
assert!(getli("").is_none(), "empty name must yield None");
}
#[test]
fn getlanginfo_deterministic_for_same_input() {
let _g = crate::test_util::global_state_lock();
let a = getli("CODESET");
let b = getli("CODESET");
assert_eq!(a, b, "getli(CODESET) must be deterministic");
}
#[test]
fn scanlanginfo_returns_vec_tuple_string_type() {
let _g = crate::test_util::global_state_lock();
let _: Vec<(String, String)> = scanli();
}
#[test]
fn scanlanginfo_deterministic_repeated_calls() {
let _g = crate::test_util::global_state_lock();
let a = scanli();
let b = scanli();
assert_eq!(a, b, "scanlanginfo must be deterministic");
}
#[test]
fn langinfo_enables_with_some_non_empty_no_panic() {
let _g = crate::test_util::global_state_lock();
let mut e: Option<Vec<i32>> = Some(vec![1, 2, 3]);
let _ = enables_(std::ptr::null(), &mut e);
}
#[test]
fn langinfo_setup_boot_finish_chain_returns_zero_each() {
let _g = crate::test_util::global_state_lock();
let null = std::ptr::null();
assert_eq!(setup_(null), 0);
assert_eq!(boot_(null), 0);
assert_eq!(finish_(null), 0);
}
}