diff --git a/Cargo.lock b/Cargo.lock index ab80fb44fd..d0d1a5e22b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base64ct" @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -184,9 +184,9 @@ version = "0.1.0" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libredox" @@ -263,9 +263,9 @@ version = "0.1.0" [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -464,9 +464,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" dependencies = [ "proc-macro2", "quote", @@ -481,15 +481,15 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "version_check" diff --git a/src/header/mod.rs b/src/header/mod.rs index 399a447e73..864c263fbf 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -36,7 +36,7 @@ pub mod libgen; pub mod limits; pub mod locale; // math.h implemented in C -// TODO: monetary.h +pub mod monetary; // TODO: mqueue.h // TODO: ndbm.h pub mod net_if; diff --git a/src/header/monetary/cbindgen.toml b/src/header/monetary/cbindgen.toml new file mode 100644 index 0000000000..8ffbc9b7ab --- /dev/null +++ b/src/header/monetary/cbindgen.toml @@ -0,0 +1,12 @@ +sys_includes = ["stddef.h", "stdint.h", "features.h"] +include_guard = "_RELIBC_MONETARY_H" +language = "C" +style = "tag" +no_includes = true +cpp_compat = true + +[enum] +prefix_with_name = true + + + diff --git a/src/header/monetary/mod.rs b/src/header/monetary/mod.rs new file mode 100644 index 0000000000..f2b4b4af23 --- /dev/null +++ b/src/header/monetary/mod.rs @@ -0,0 +1,142 @@ + +/// monetary.h implementation for Redox, following the POSIX standard. +/// Following https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/monetary.h.html +/// +/// We should provide a strfmon() implementation that formats a monetary value. +/// according to the current locale (TODO). +use alloc::string::{String, ToString}; +use core::{ffi::CStr, ptr, slice, str}; + +extern crate alloc; + +mod strfmon; +#[deny(unsafe_op_in_unsafe_fn)] +#[repr(C)] +struct LocaleMonetaryInfo { + int_curr_symbol: &'static str, + currency_symbol: &'static str, + mon_decimal_point: &'static str, + mon_thousands_sep: &'static str, + mon_grouping: &'static [u8], + positive_sign: &'static str, + negative_sign: &'static str, + int_frac_digits: u8, + frac_digits: u8, + p_cs_precedes: bool, + p_sep_by_space: bool, + p_sign_posn: u8, + n_cs_precedes: bool, + n_sep_by_space: bool, + n_sign_posn: u8, +} + +const DEFAULT_MONETARY: LocaleMonetaryInfo = LocaleMonetaryInfo { + int_curr_symbol: "USD ", + currency_symbol: "$", + mon_decimal_point: ".", + mon_thousands_sep: ",", + mon_grouping: &[3, 3], + positive_sign: "", + negative_sign: "-", + int_frac_digits: 2, + frac_digits: 2, + p_cs_precedes: true, + p_sep_by_space: false, + p_sign_posn: 1, + n_cs_precedes: true, + n_sep_by_space: false, + n_sign_posn: 1, +}; + +#[derive(Default)] +struct FormatFlags { + left_justify: bool, + force_sign: bool, + space_sign: bool, + use_parens: bool, + suppress_symbol: bool, + field_width: Option, + left_precision: Option, + right_precision: Option, + international: bool, +} + +/// Use our own floating point implementation +/// TODO : use num-traits crate +#[inline] +fn my_fabs(x: f64) -> f64 { + if x < 0.0 { + -x + } else { + x + } +} + +#[inline] +fn my_floor(x: f64) -> f64 { + let i = x as i64; + if x < 0.0 && x != i as f64 { + (i - 1) as f64 + } else { + i as f64 + } +} + +#[inline] +fn my_trunc(x: f64) -> f64 { + if x < 0.0 { + -my_floor(-x) + } else { + my_floor(x) + } +} + +#[inline] +fn my_round(x: f64) -> f64 { + my_floor(x + 0.5) +} + +#[inline] +fn my_pow10(n: usize) -> f64 { + let mut result = 1.0; + for _ in 0..n { + result *= 10.0; + } + result +} + +/// TODO : improve grouping implementation +fn apply_grouping(int_str: &str, monetary: &LocaleMonetaryInfo) -> String { + let mut grouped = String::with_capacity(int_str.len() * 2); + let mut count = 0; + let mut group_idx = 0; + + // The grouping array can have up to 4 elements, but the last element is always 0 + for c in int_str.chars().rev() { + if count > 0 && count % monetary.mon_grouping[group_idx] == 0 { + grouped.push_str(monetary.mon_thousands_sep); + // Move to next grouping size if available + if group_idx + 1 < monetary.mon_grouping.len() { + group_idx += 1; + } + } + grouped.push(c); + count += 1; + } + // Reverse the string to get the correct order + grouped.chars().rev().collect() +} + +/// Safe handling of large monetary values. Returns None if the value is too large to format +fn format_value_parts(value: f64, frac_digits: usize) -> Option<(String, i64)> { + let abs_value = my_fabs(value); + if abs_value > (i64::MAX as f64) { + return None; + } + + let int_part = my_trunc(abs_value) as i64; + let scale = my_pow10(frac_digits); + let frac_part = my_round((abs_value - int_part as f64) * scale) as i64; + + Some((int_part.to_string(), frac_part)) +} diff --git a/src/header/monetary/strfmon.rs b/src/header/monetary/strfmon.rs new file mode 100644 index 0000000000..0d660e7733 --- /dev/null +++ b/src/header/monetary/strfmon.rs @@ -0,0 +1,306 @@ +use super::{ + my_fabs, my_pow10, my_round, my_trunc, FormatFlags, LocaleMonetaryInfo, DEFAULT_MONETARY, +}; +use alloc::string::{String, ToString}; +use core::{ffi::CStr, ptr, slice, str}; + +/* + The strfmon() function formats a monetary value according to the format string format and writes the result to the character + array s of size maxsize. + The format string format is a string that can contain plain characters and format specifiers. + The format specifiers start with the character '%' and are followed by one or more flags, an optional field width, + an optional left precision, an optional right precision, and a conversion specifier. + The conversion specifier can be either 'i' or 'n'. The 'i' specifier formats the monetary value according to the + locale-specific rules, while the 'n' specifier formats the monetary value according to the international rules. + + The function returns the number of characters written to the buffer, excluding the null terminator, or -1 if an error occurred. + +*/ +pub unsafe extern "C" fn strfmon( + s: *mut i8, // char * + maxsize: usize, // size_t + format: *const i8, // const char * + mut args: ... +) -> isize { + if s.is_null() || format.is_null() || maxsize == 0 { + // Check for null pointers and zero size + return -1; + } + + let format_str = match unsafe { CStr::from_ptr(format) }.to_str() { + Ok(s) => s, // Convert the format string to a string + Err(_) => return -1, // Return -1 if the format string is not valid + }; + // Buffer to write the formatted string + let buffer = unsafe { slice::from_raw_parts_mut(s as *mut u8, maxsize) }; + let mut pos = 0; + let mut format_chars = format_str.chars().peekable(); + + while let Some(c) = format_chars.next() { + if c != '%' { + if pos >= buffer.len() { + return -1; + } + // Write the character to the buffer + buffer[pos] = c as u8; // Write the character to the buffer + pos += 1; // Move to the next position + continue; // Move to the next character + } + // Handle '%%' as an escape sequence + if format_chars.peek() == Some(&'%') { + if pos >= buffer.len() { + return -1; // Buffer overflow + } + buffer[pos] = b'%'; // Write '%' to the buffer + pos += 1; // Move to the next position + format_chars.next(); // Skip the second '%' + continue; + } + + let mut flags = FormatFlags::default(); + + // Parse flags + while let Some(&next_char) = format_chars.peek() { + match next_char { + '=' => flags.left_justify = true, + '+' => flags.force_sign = true, + ' ' => flags.space_sign = true, + '(' => flags.use_parens = true, + '!' => flags.suppress_symbol = true, + _ => break, + } + format_chars.next(); + } + + // Parse field width + let mut num_str = String::new(); + while let Some(&c) = format_chars.peek() { + if !c.is_ascii_digit() { + break; + } + num_str.push(c); // Add the character to the number string + format_chars.next(); // Move to the next character + } + if !num_str.is_empty() { + flags.field_width = num_str.parse().ok(); // Parse the number string + } + + // Parse left precision + if format_chars.peek() == Some(&'#') { + format_chars.next(); // Skip the '#' + num_str.clear(); // Clear the number string + while let Some(&c) = format_chars.peek() { + if !c.is_ascii_digit() { + // Check if the character is a digit + break; + } + num_str.push(c); + format_chars.next(); + } + flags.left_precision = num_str.parse().ok(); + } + + // Parse right precision + if format_chars.peek() == Some(&'.') { + format_chars.next(); + num_str.clear(); + while let Some(&c) = format_chars.peek() { + if !c.is_ascii_digit() { + break; + } + num_str.push(c); + format_chars.next(); + } + flags.right_precision = num_str.parse().ok(); + } + + match format_chars.next() { + Some('i') => { + flags.international = true; + let value = unsafe { args.arg::() }; + if let Some(written) = + format_monetary(&mut buffer[pos..], value, &DEFAULT_MONETARY, &flags) + { + pos += written; + } else { + return -1; + } + } + Some('n') => { + let value = unsafe { args.arg::() }; // Get the next argument as a f64 + if let Some(written) = + format_monetary(&mut buffer[pos..], value, &DEFAULT_MONETARY, &flags) + { + pos += written; // Move the position by the number of characters written + } else { + return -1; + } + } + _ => return -1, + } + } + + if pos >= buffer.len() { + return -1; + } + buffer[pos] = 0; // Null-terminate the buffer + pos as isize // Return the number of characters written +} + +fn format_monetary( + buffer: &mut [u8], + value: f64, + monetary: &LocaleMonetaryInfo, + flags: &FormatFlags, +) -> Option { + let mut pos = 0; // Initialize the position to 0 + let is_negative = value < 0.0; // Check if the value is negative + let abs_value = my_fabs(value); // Get the absolute value of the number + // Check if the international flag is set + let frac_digits = if flags.international { + flags + .right_precision + .unwrap_or(monetary.int_frac_digits as usize) + } else { + flags + .right_precision + .unwrap_or(monetary.frac_digits as usize) + }; + + let scale = my_pow10(frac_digits); + let mut int_part = my_trunc(abs_value) as i64; + let mut frac_part = my_round((abs_value - int_part as f64) * scale) as i64; + + // Ensure that the fractional part (frac_part) doesn’t overflow due to rounding + // when abs_value is very close to the next integer value. + if frac_part >= scale as i64 { + // Handle carry-over to the integer part + int_part += 1; + frac_part = 0; + } + + let mut int_str = int_part.to_string(); + + if let Some(left_prec) = flags.left_precision { + if int_str.len() > left_prec { + return None; + } + while int_str.len() < left_prec { + int_str.insert(0, '0'); + } + } + + // Apply grouping + let mut grouped = String::with_capacity(int_str.len() * 2); + let mut group_idx = 0; + let mut count = 0; + + // The grouping array can have up to 4 elements, but the last element is always 0 + for c in int_str.chars().rev() { + if count > 0 + && count % monetary.mon_grouping[group_idx.min(monetary.mon_grouping.len() - 1)] == 0 + { + grouped.push_str(monetary.mon_thousands_sep); + } + grouped.push(c); + count += 1; + } + // Reverse the string to get the correct order + let mut result = String::with_capacity(grouped.len() + 20); + + // Add the sign + let sign_posn = if is_negative { + monetary.n_sign_posn + } else { + monetary.p_sign_posn + }; + + // Add the sign + let sign = if is_negative { + monetary.negative_sign + } else if flags.force_sign { + monetary.positive_sign + } else if flags.space_sign { + " " + } else { + "" + }; + + // Add the sign + if sign_posn == 0 { + result.push('('); + } else if sign_posn == 1 { + result.push_str(sign); + } + + // Add the currency symbol + let cs_precedes = if is_negative { + monetary.n_cs_precedes + } else { + monetary.p_cs_precedes + }; + + // Add a space between the currency symbol and the value + let sep_by_space = if is_negative { + monetary.n_sep_by_space + } else { + monetary.p_sep_by_space + }; + + // Add the currency symbol + let symbol = if flags.suppress_symbol { + "" + } else if flags.international { + monetary.int_curr_symbol + } else { + monetary.currency_symbol + }; + + // Add the currency symbol + if cs_precedes { + result.push_str(symbol); + if sep_by_space { + result.push(' '); + } + } + + // Add the value + result.push_str(&grouped.chars().rev().collect::()); + + if frac_digits > 0 { + result.push_str(monetary.mon_decimal_point); + result.push_str(&format!("{:0width$}", frac_part, width = frac_digits)); + } + + if !cs_precedes { + if sep_by_space { + result.push(' '); + } + result.push_str(symbol); + } + + if sign_posn == 0 { + result.push(')'); + } + + if let Some(width) = flags.field_width { + if result.len() < width { + let padding = " ".repeat(width - result.len()); + if flags.left_justify { + result.push_str(&padding); + } else { + result = padding + &result; + } + } + } + + // Write the formatted string to the buffer + for (i, &b) in result.as_bytes().iter().enumerate() { + if pos + i >= buffer.len() { + return None; + } + buffer[pos + i] = b; + } + + Some(pos + result.len()) +}