diff --git a/src/header/locale/cbindgen.toml b/src/header/locale/cbindgen.toml index e99e73db12..edb2300b9c 100644 --- a/src/header/locale/cbindgen.toml +++ b/src/header/locale/cbindgen.toml @@ -7,6 +7,3 @@ cpp_compat = true [enum] prefix_with_name = true - -[export] -include = ["locale_t"] diff --git a/src/header/locale/constants.rs b/src/header/locale/constants.rs new file mode 100644 index 0000000000..0652ba8dad --- /dev/null +++ b/src/header/locale/constants.rs @@ -0,0 +1,16 @@ +use crate::platform::types::c_int; + +pub const LC_COLLATE: c_int = 0; +pub const LC_CTYPE: c_int = 1; +pub const LC_MESSAGES: c_int = 2; +pub const LC_MONETARY: c_int = 3; +pub const LC_NUMERIC: c_int = 4; +pub const LC_TIME: c_int = 5; +pub const LC_ALL: c_int = 6; +pub const LC_COLLATE_MASK: c_int = 0x1; +pub const LC_CTYPE_MASK: c_int = 0x2; +pub const LC_MESSAGES_MASK: c_int = 0x4; +pub const LC_MONETARY_MASK: c_int = 0x8; +pub const LC_NUMERIC_MASK: c_int = 0x10; +pub const LC_TIME_MASK: c_int = 0x20; +pub const LC_ALL_MASK: c_int = 0b111111; diff --git a/src/header/locale/data.rs b/src/header/locale/data.rs new file mode 100644 index 0000000000..645b05d3a4 --- /dev/null +++ b/src/header/locale/data.rs @@ -0,0 +1,346 @@ +use core::str::FromStr; + +use alloc::{ + boxed::Box, + ffi::CString, + string::{String, ToString}, + vec::Vec, +}; + +use super::constants::*; +use crate::platform::types::{c_char, c_int}; + +/// See . +/// this struct is not ordered like in the posix spec for readability +#[repr(C)] +#[derive(Clone)] +pub struct lconv { + pub decimal_point: *mut c_char, + pub thousands_sep: *mut c_char, + pub grouping: *mut c_char, + pub int_curr_symbol: *mut c_char, + pub currency_symbol: *mut c_char, + pub mon_decimal_point: *mut c_char, + pub mon_thousands_sep: *mut c_char, + pub mon_grouping: *mut c_char, + pub positive_sign: *mut c_char, + pub negative_sign: *mut c_char, + pub int_frac_digits: c_char, + pub frac_digits: c_char, + pub p_cs_precedes: c_char, + pub p_sep_by_space: c_char, + pub n_cs_precedes: c_char, + pub n_sep_by_space: c_char, + pub p_sign_posn: c_char, + pub n_sign_posn: c_char, + pub int_p_cs_precedes: c_char, + pub int_p_sep_by_space: c_char, + pub int_n_cs_precedes: c_char, + pub int_n_sep_by_space: c_char, + pub int_p_sign_posn: c_char, + pub int_n_sign_posn: c_char, +} +unsafe impl Sync for lconv {} + +/// "POSIX" or "C" default +pub(crate) const fn posix_lconv() -> lconv { + lconv { + // numeric, non-monetary + decimal_point: b".\0".as_ptr() as *mut c_char, + thousands_sep: b"\0".as_ptr() as *mut c_char, + grouping: b"\0".as_ptr() as *mut c_char, + // local monetary + int_curr_symbol: b"\0".as_ptr() as *mut c_char, + currency_symbol: b"\0".as_ptr() as *mut c_char, + mon_decimal_point: b"\0".as_ptr() as *mut c_char, + mon_thousands_sep: b"\0".as_ptr() as *mut c_char, + mon_grouping: b"\0".as_ptr() as *mut c_char, + positive_sign: b"\0".as_ptr() as *mut c_char, + negative_sign: b"\0".as_ptr() as *mut c_char, + // delimiters, unspecified + int_frac_digits: c_char::MAX, + frac_digits: c_char::MAX, + p_cs_precedes: c_char::MAX, + p_sep_by_space: c_char::MAX, + n_cs_precedes: c_char::MAX, + n_sep_by_space: c_char::MAX, + p_sign_posn: c_char::MAX, + n_sign_posn: c_char::MAX, + // international format + int_p_cs_precedes: c_char::MAX, + int_p_sep_by_space: c_char::MAX, + int_n_cs_precedes: c_char::MAX, + int_n_sep_by_space: c_char::MAX, + int_p_sign_posn: c_char::MAX, + int_n_sign_posn: c_char::MAX, + } +} + +#[repr(C)] +pub(crate) struct LocaleData { + pub name: CString, + pub lconv: lconv, + // Owned memory buffers + pub decimal_point: CString, + pub thousands_sep: CString, + pub grouping: Vec, + pub int_curr_symbol: CString, + pub currency_symbol: CString, + pub mon_decimal_point: CString, + pub mon_thousands_sep: CString, + pub mon_grouping: Vec, + pub positive_sign: CString, + pub negative_sign: CString, +} +unsafe impl Sync for LocaleData {} + +impl LocaleData { + pub fn new(name: CString, toml: RawLocale) -> Box { + let mut data = Box::new(LocaleData { + name, + decimal_point: Self::to_cstring(toml.decimal_point), + thousands_sep: Self::to_cstring(toml.thousands_sep), + grouping: Self::to_grouping(toml.grouping), + int_curr_symbol: Self::to_cstring(toml.int_curr_symbol), + currency_symbol: Self::to_cstring(toml.currency_symbol), + mon_decimal_point: Self::to_cstring(toml.mon_decimal_point), + mon_thousands_sep: Self::to_cstring(toml.mon_thousands_sep), + mon_grouping: Self::to_grouping(toml.mon_grouping), + positive_sign: Self::to_cstring(toml.positive_sign), + negative_sign: Self::to_cstring(toml.negative_sign), + lconv: unsafe { core::mem::zeroed() }, + }); + + data.lconv.int_frac_digits = toml.int_frac_digits.unwrap_or(c_char::MAX); + data.lconv.frac_digits = toml.frac_digits.unwrap_or(c_char::MAX); + data.lconv.p_cs_precedes = toml.p_cs_precedes.unwrap_or(c_char::MAX); + data.lconv.p_sep_by_space = toml.p_sep_by_space.unwrap_or(c_char::MAX); + data.lconv.n_cs_precedes = toml.n_cs_precedes.unwrap_or(c_char::MAX); + data.lconv.n_sep_by_space = toml.n_sep_by_space.unwrap_or(c_char::MAX); + data.lconv.p_sign_posn = toml.p_sign_posn.unwrap_or(c_char::MAX); + data.lconv.n_sign_posn = toml.n_sign_posn.unwrap_or(c_char::MAX); + data.lconv.int_p_cs_precedes = toml.int_p_cs_precedes.unwrap_or(c_char::MAX); + data.lconv.int_p_sep_by_space = toml.int_p_sep_by_space.unwrap_or(c_char::MAX); + data.lconv.int_n_cs_precedes = toml.int_n_cs_precedes.unwrap_or(c_char::MAX); + data.lconv.int_n_sep_by_space = toml.int_n_sep_by_space.unwrap_or(c_char::MAX); + data.lconv.int_p_sign_posn = toml.int_p_sign_posn.unwrap_or(c_char::MAX); + data.lconv.int_n_sign_posn = toml.int_n_sign_posn.unwrap_or(c_char::MAX); + + data.update_lconv_pointers(); + data + } + + pub fn posix() -> Box { + LocaleData::new(CString::from_str("C").unwrap(), RawLocale::default()) + } + + fn update_lconv_pointers(&mut self) { + self.lconv.decimal_point = self.decimal_point.as_ptr() as *mut c_char; + self.lconv.thousands_sep = self.thousands_sep.as_ptr() as *mut c_char; + self.lconv.grouping = self.grouping.as_ptr() as *mut c_char; + self.lconv.int_curr_symbol = self.int_curr_symbol.as_ptr() as *mut c_char; + self.lconv.currency_symbol = self.currency_symbol.as_ptr() as *mut c_char; + self.lconv.mon_decimal_point = self.mon_decimal_point.as_ptr() as *mut c_char; + self.lconv.mon_thousands_sep = self.mon_thousands_sep.as_ptr() as *mut c_char; + self.lconv.mon_grouping = self.mon_grouping.as_ptr() as *mut c_char; + self.lconv.positive_sign = self.positive_sign.as_ptr() as *mut c_char; + self.lconv.negative_sign = self.negative_sign.as_ptr() as *mut c_char; + } + + pub fn copy_category(&mut self, other: &Self, category: c_int) { + match category { + LC_NUMERIC => { + self.decimal_point = other.decimal_point.clone(); + self.thousands_sep = other.thousands_sep.clone(); + self.grouping = other.grouping.clone(); + self.lconv.frac_digits = other.lconv.frac_digits; + } + LC_MONETARY => { + self.int_curr_symbol = other.int_curr_symbol.clone(); + self.currency_symbol = other.currency_symbol.clone(); + self.mon_decimal_point = other.mon_decimal_point.clone(); + self.mon_thousands_sep = other.mon_thousands_sep.clone(); + self.mon_grouping = other.mon_grouping.clone(); + self.positive_sign = other.positive_sign.clone(); + self.negative_sign = other.negative_sign.clone(); + self.lconv.int_frac_digits = other.lconv.int_frac_digits; + self.lconv.p_cs_precedes = other.lconv.p_cs_precedes; + self.lconv.p_sep_by_space = other.lconv.p_sep_by_space; + self.lconv.n_cs_precedes = other.lconv.n_cs_precedes; + self.lconv.n_sep_by_space = other.lconv.n_sep_by_space; + self.lconv.p_sign_posn = other.lconv.p_sign_posn; + self.lconv.n_sign_posn = other.lconv.n_sign_posn; + self.lconv.int_p_cs_precedes = other.lconv.int_p_cs_precedes; + self.lconv.int_p_sep_by_space = other.lconv.int_p_sep_by_space; + self.lconv.int_n_cs_precedes = other.lconv.int_n_cs_precedes; + self.lconv.int_n_sep_by_space = other.lconv.int_n_sep_by_space; + self.lconv.int_p_sign_posn = other.lconv.int_p_sign_posn; + self.lconv.int_n_sign_posn = other.lconv.int_n_sign_posn; + } + LC_ALL => { + *self = other.clone(); + } + _ => {} + } + + self.update_lconv_pointers(); + } + + fn to_cstring(opt: Option) -> CString { + opt.and_then(|s| CString::new(s).ok()) + .unwrap_or_else(|| CString::new("").unwrap()) + } + + fn to_grouping(opt: Option>) -> Vec { + let mut v: Vec = opt + .unwrap_or_default() + .into_iter() + .map(|x| x as u8) + .collect(); + v.push(0); + v + } +} + +impl Clone for LocaleData { + fn clone(&self) -> Self { + let mut data = Self { + name: self.name.clone(), + lconv: self.lconv.clone(), + decimal_point: self.decimal_point.clone(), + thousands_sep: self.thousands_sep.clone(), + grouping: self.grouping.clone(), + int_curr_symbol: self.int_curr_symbol.clone(), + currency_symbol: self.currency_symbol.clone(), + mon_decimal_point: self.mon_decimal_point.clone(), + mon_thousands_sep: self.mon_thousands_sep.clone(), + mon_grouping: self.mon_grouping.clone(), + positive_sign: self.positive_sign.clone(), + negative_sign: self.negative_sign.clone(), + }; + data.update_lconv_pointers(); + data + } +} + +#[derive(Clone)] +pub(crate) struct GlobalLocaleData { + // names per LC_* constant + pub names: [CString; 7], + pub data: LocaleData, +} + +impl GlobalLocaleData { + pub fn new() -> Box { + let data = LocaleData::posix(); + let names = [ + data.name.clone(), + data.name.clone(), + data.name.clone(), + data.name.clone(), + data.name.clone(), + data.name.clone(), + data.name.clone(), + ]; + let mut r = Box::new(GlobalLocaleData { data: *data, names }); + r.data.update_lconv_pointers(); + r + } + + pub fn get_name(&self, category: i32) -> Option<&CString> { + self.names.get(category as usize) + } + pub fn set_name(&mut self, category: i32, name: CString) -> Option<&CString> { + if self.names.get(category as usize).is_some() { + self.names[category as usize] = name; + self.names.get(category as usize) + } else { + None + } + } +} +unsafe impl Sync for GlobalLocaleData {} + +#[derive(Default)] +pub(crate) struct RawLocale { + pub decimal_point: Option, + pub thousands_sep: Option, + pub grouping: Option>, + pub int_curr_symbol: Option, + pub currency_symbol: Option, + pub mon_decimal_point: Option, + pub mon_thousands_sep: Option, + pub mon_grouping: Option>, + pub positive_sign: Option, + pub negative_sign: Option, + pub int_frac_digits: Option, + pub frac_digits: Option, + pub p_cs_precedes: Option, + pub p_sep_by_space: Option, + pub n_cs_precedes: Option, + pub n_sep_by_space: Option, + pub p_sign_posn: Option, + pub n_sign_posn: Option, + pub int_p_cs_precedes: Option, + pub int_p_sep_by_space: Option, + pub int_n_cs_precedes: Option, + pub int_n_sep_by_space: Option, + pub int_p_sign_posn: Option, + pub int_n_sign_posn: Option, +} +impl RawLocale { + pub fn parse(content: &str) -> Self { + let mut locale = RawLocale::default(); + + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + + let mut parts = trimmed.splitn(2, '='); + let key = parts.next().unwrap_or("").trim(); + let val = parts.next().unwrap_or("").trim(); + + if key.is_empty() || val.is_empty() { + continue; + } + + match key { + "decimal_point" => locale.decimal_point = Some(val.to_string()), + "thousands_sep" => locale.thousands_sep = Some(val.to_string()), + "int_curr_symbol" => locale.int_curr_symbol = Some(val.to_string()), + "currency_symbol" => locale.currency_symbol = Some(val.to_string()), + "mon_decimal_point" => locale.mon_decimal_point = Some(val.to_string()), + "mon_thousands_sep" => locale.mon_thousands_sep = Some(val.to_string()), + "positive_sign" => locale.positive_sign = Some(val.to_string()), + "negative_sign" => locale.negative_sign = Some(val.to_string()), + "grouping" => locale.grouping = Some(Self::parse_grouping(val)), + "mon_grouping" => locale.mon_grouping = Some(Self::parse_grouping(val)), + "int_frac_digits" => locale.int_frac_digits = val.parse().ok(), + "frac_digits" => locale.frac_digits = val.parse().ok(), + "p_cs_precedes" => locale.p_cs_precedes = val.parse().ok(), + "p_sep_by_space" => locale.p_sep_by_space = val.parse().ok(), + "n_cs_precedes" => locale.n_cs_precedes = val.parse().ok(), + "n_sep_by_space" => locale.n_sep_by_space = val.parse().ok(), + "p_sign_posn" => locale.p_sign_posn = val.parse().ok(), + "n_sign_posn" => locale.n_sign_posn = val.parse().ok(), + "int_p_cs_precedes" => locale.int_p_cs_precedes = val.parse().ok(), + "int_p_sep_by_space" => locale.int_p_sep_by_space = val.parse().ok(), + "int_n_cs_precedes" => locale.int_n_cs_precedes = val.parse().ok(), + "int_n_sep_by_space" => locale.int_n_sep_by_space = val.parse().ok(), + "int_p_sign_posn" => locale.int_p_sign_posn = val.parse().ok(), + "int_n_sign_posn" => locale.int_n_sign_posn = val.parse().ok(), + _ => {} + } + } + locale + } + + /// parse e.g. "3,3,0" + fn parse_grouping(val: &str) -> Vec { + val.split(',') + .filter_map(|s| s.trim().parse::().ok()) + .collect() + } +} diff --git a/src/header/locale/mod.rs b/src/header/locale/mod.rs index acc250f688..da839d7e32 100644 --- a/src/header/locale/mod.rs +++ b/src/header/locale/mod.rs @@ -1,74 +1,160 @@ //! `locale.h` implementation. //! //! See . +//! See -use core::ptr; +use alloc::{boxed::Box, ffi::CString, string::String}; +use core::{ptr, str::FromStr}; -use crate::platform::types::{c_char, c_int, c_void}; +use crate::{ + c_str::CStr, + error::{Errno, ResultExtPtrMut}, + fs::File, + header::errno, + io::Read, + platform::types::{c_char, c_int, c_void}, + sync::Once, +}; const EMPTY_PTR: *const c_char = "\0" as *const _ as *const c_char; // Can't use &str because of the mutability static mut C_LOCALE: [c_char; 2] = [b'C' as c_char, 0]; -//TODO: locale_t internals +mod constants; +use constants::*; +mod data; +use data::*; +// TODO: No parsing implementation that's compatible with glibc +mod redox; +use redox::*; + pub type locale_t = *mut c_void; - -/// See . -#[repr(C)] -pub struct lconv { - currency_symbol: *const c_char, - decimal_point: *const c_char, - frac_digits: c_char, - grouping: *const c_char, - int_curr_symbol: *const c_char, - int_frac_digits: c_char, - mon_decimal_point: *const c_char, - mon_grouping: *const c_char, - mon_thousands_sep: *const c_char, - negative_sign: *const c_char, - n_cs_precedes: c_char, - n_sep_by_space: c_char, - n_sign_posn: c_char, - positive_sign: *const c_char, - p_cs_precedes: c_char, - p_sep_by_space: c_char, - p_sign_posn: c_char, - thousands_sep: *const c_char, -} -unsafe impl Sync for lconv {} - -// Mutable because POSIX demands a mutable pointer, even though it warns -// against mutating it -static mut CURRENT_LOCALE: lconv = lconv { - currency_symbol: EMPTY_PTR, - decimal_point: ".\0" as *const _ as *const c_char, - frac_digits: c_char::max_value(), - grouping: EMPTY_PTR, - int_curr_symbol: EMPTY_PTR, - int_frac_digits: c_char::max_value(), - mon_decimal_point: EMPTY_PTR, - mon_grouping: EMPTY_PTR, - mon_thousands_sep: EMPTY_PTR, - negative_sign: EMPTY_PTR, - n_cs_precedes: c_char::max_value(), - n_sep_by_space: c_char::max_value(), - n_sign_posn: c_char::max_value(), - positive_sign: EMPTY_PTR, - p_cs_precedes: c_char::max_value(), - p_sep_by_space: c_char::max_value(), - p_sign_posn: c_char::max_value(), - thousands_sep: EMPTY_PTR, -}; +/// constant struct to "C" or "POSIX" locale +/// mutable because POSIX demands a mutable pointer +static mut POSIX_LOCALE: lconv = posix_lconv(); +pub const LC_GLOBAL_LOCALE: locale_t = -1isize as locale_t; +/// process-wide locale, used by setlocale() and localeconv() +static mut GLOBAL_LOCALE: *mut GlobalLocaleData = ptr::null_mut(); +/// thread-wide locale, used by uselocale() and localeconv() +#[thread_local] +static mut THREAD_LOCALE: *mut LocaleData = ptr::null_mut(); /// See . #[unsafe(no_mangle)] pub unsafe extern "C" fn localeconv() -> *mut lconv { - &raw mut CURRENT_LOCALE as *mut _ + let current = uselocale(ptr::null_mut()); + if current == LC_GLOBAL_LOCALE || current.is_null() { + if !GLOBAL_LOCALE.is_null() { + // safety: GLOBAL_LOCALE is never set to null again + &raw mut (*GLOBAL_LOCALE).data.lconv + } else { + &raw mut POSIX_LOCALE + } + } else { + let current = current as *mut LocaleData; + &raw mut (*current).lconv + } } /// See . #[unsafe(no_mangle)] -pub unsafe extern "C" fn setlocale(_option: c_int, _val: *const c_char) -> *mut c_char { - // TODO actually implement - &raw mut C_LOCALE as *mut c_char +pub unsafe extern "C" fn setlocale(category: c_int, locale: *const c_char) -> *mut c_char { + if GLOBAL_LOCALE.is_null() { + let new_global = GlobalLocaleData::new(); + GLOBAL_LOCALE = Box::into_raw(new_global); + }; + let Some(mut global) = GLOBAL_LOCALE.as_mut() else { + return ptr::null_mut(); + }; + + if locale.is_null() { + let Some(name) = global.get_name(category) else { + return ptr::null_mut(); + }; + return name.as_ptr() as *mut c_char; + } + + let name = CStr::from_ptr(locale).to_str().unwrap_or("C"); + + match load_locale_file(name) { + Ok(loc_ptr) => { + global.data.copy_category(&loc_ptr, category); + let Some(name) = global.set_name(category, CString::from_str(name).unwrap()) else { + return ptr::null_mut(); + }; + name.as_ptr() as *mut c_char + } + Err(_) => ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uselocale(newloc: locale_t) -> locale_t { + let old_loc = if THREAD_LOCALE.is_null() { + LC_GLOBAL_LOCALE + } else { + THREAD_LOCALE as locale_t + }; + + if !newloc.is_null() { + THREAD_LOCALE = if newloc == LC_GLOBAL_LOCALE { + ptr::null_mut() + } else { + newloc as *mut LocaleData + }; + } + + old_loc +} + +/// See . +#[unsafe(no_mangle)] +pub unsafe extern "C" fn newlocale(mask: c_int, locale: *const c_char, base: locale_t) -> locale_t { + let name = CStr::from_ptr(locale).to_string_lossy().into_owned(); + let name = name.as_str(); + let mut new_locale = if name == "" || name == "C" || name == "POSIX" { + Ok(LocaleData::posix()) + } else { + load_locale_file(name) + }; + if base != LC_GLOBAL_LOCALE { + // borrowing here + let base = base as *const _ as *const LocaleData; + if let Ok(new_locale) = new_locale.as_mut() { + if let Some(base) = base.as_ref() { + // copy old values if not containing the mask + if (mask & LC_NUMERIC_MASK) == 0 { + new_locale.copy_category(base, LC_NUMERIC); + } + if (mask & LC_MONETARY_MASK) == 0 { + new_locale.copy_category(base, LC_MONETARY); + } + // TODO: other categories? + } + } + } + new_locale.or_errno_null_mut() as *mut _ +} + +/// See . +#[unsafe(no_mangle)] +pub unsafe extern "C" fn freelocale(loc: locale_t) { + if !loc.is_null() && loc != LC_GLOBAL_LOCALE { + drop(Box::from_raw(loc as *mut LocaleData)); + } +} + +/// See . +#[unsafe(no_mangle)] +pub unsafe extern "C" fn duplocale(loc: locale_t) -> locale_t { + if loc.is_null() { + // TODO: errno? + loc + } else if loc == LC_GLOBAL_LOCALE { + Box::into_raw(LocaleData::posix()) as locale_t + } else { + // borrowing here + let loc = loc as *const _ as *const LocaleData; + Box::into_raw(Box::from((*loc).clone())) as locale_t + } } diff --git a/src/header/locale/redox.rs b/src/header/locale/redox.rs new file mode 100644 index 0000000000..88abaf3030 --- /dev/null +++ b/src/header/locale/redox.rs @@ -0,0 +1,32 @@ +use core::str::FromStr; + +use alloc::{boxed::Box, ffi::CString, string::String}; + +use crate::{ + c_str::CStr, + error::Errno, + fs::File, + header::{ + errno, fcntl, + locale::{ + data::{LocaleData, RawLocale}, + locale_t, + }, + }, + io::Read, +}; + +pub fn load_locale_file(name: &str) -> Result, Errno> { + let mut path = String::from("/etc/locale.d/"); + path.push_str(name); + + let path_c = CString::new(path).map_err(|_| Errno(errno::EINVAL))?; + let mut content = String::new(); + + let mut file = File::open(path_c.as_c_str().into(), fcntl::O_RDONLY)?; + file.read_to_string(&mut content) + .map_err(|_| Errno(errno::EIO))?; + + let toml = RawLocale::parse(&content); + Ok(LocaleData::new(CString::from_str(name).unwrap(), toml)) +}