From 610909bd2bdd13a7b1fc828c932700435e339297 Mon Sep 17 00:00:00 2001 From: Guillaume Gielly Date: Mon, 23 Dec 2024 21:32:56 +0100 Subject: [PATCH 1/7] Implement 'monetary.h', including initial structure and strfmon() function --- Cargo.lock | 28 +-- src/header/mod.rs | 2 +- src/header/monetary/cbindgen.toml | 12 ++ src/header/monetary/mod.rs | 142 ++++++++++++++ src/header/monetary/strfmon.rs | 306 ++++++++++++++++++++++++++++++ 5 files changed, 475 insertions(+), 15 deletions(-) create mode 100644 src/header/monetary/cbindgen.toml create mode 100644 src/header/monetary/mod.rs create mode 100644 src/header/monetary/strfmon.rs 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()) +} From f20c0552dd0f63f894272ad031a9908caec985e7 Mon Sep 17 00:00:00 2001 From: Guillaume Gielly Date: Mon, 23 Dec 2024 21:56:45 +0100 Subject: [PATCH 2/7] Enhance monetary formatting implementation and improve comments for clarity --- src/header/monetary/mod.rs | 29 ++++++++++++++++------------- src/header/monetary/strfmon.rs | 9 ++++++--- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/header/monetary/mod.rs b/src/header/monetary/mod.rs index f2b4b4af23..2916ad256d 100644 --- a/src/header/monetary/mod.rs +++ b/src/header/monetary/mod.rs @@ -1,8 +1,7 @@ - /// 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. +/// 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}; @@ -62,7 +61,7 @@ struct FormatFlags { } /// Use our own floating point implementation -/// TODO : use num-traits crate +/// TODO : maybe we can use num-traits crate #[inline] fn my_fabs(x: f64) -> f64 { if x < 0.0 { @@ -105,32 +104,36 @@ fn my_pow10(n: usize) -> f64 { result } -/// TODO : improve grouping implementation +/// Formats a monetary value according to the current locale. 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; + let current_grouping = &monetary.mon_grouping; + let separator = monetary.mon_thousands_sep; - // 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; + for c in int_str.chars() { + if count > 0 { + let current_group = current_grouping[group_idx.min(current_grouping.len() - 1)]; + if current_group > 0 && count % current_group == 0 { + grouped.push_str(separator); + if group_idx + 1 < current_grouping.len() { + group_idx += 1; + } } } grouped.push(c); count += 1; } - // Reverse the string to get the correct order - grouped.chars().rev().collect() + + grouped } /// 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) { + // Check if the value is too large to format return None; } diff --git a/src/header/monetary/strfmon.rs b/src/header/monetary/strfmon.rs index 0d660e7733..3feda48182 100644 --- a/src/header/monetary/strfmon.rs +++ b/src/header/monetary/strfmon.rs @@ -12,9 +12,10 @@ use core::{ffi::CStr, ptr, slice, str}; 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. + TODO : better handling error : always return -1 if an error occurred */ pub unsafe extern "C" fn strfmon( s: *mut i8, // char * @@ -208,7 +209,7 @@ fn format_monetary( // Reverse the string to get the correct order let mut result = String::with_capacity(grouped.len() + 20); - // Add the sign + // Add the sign position let sign_posn = if is_negative { monetary.n_sign_posn } else { @@ -226,7 +227,7 @@ fn format_monetary( "" }; - // Add the sign + // Add the sign at the beginning if sign_posn == 0 { result.push('('); } else if sign_posn == 1 { @@ -273,6 +274,7 @@ fn format_monetary( } if !cs_precedes { + // Add the currency symbol after the value if sep_by_space { result.push(' '); } @@ -284,6 +286,7 @@ fn format_monetary( } if let Some(width) = flags.field_width { + // Check if the field width is specified if result.len() < width { let padding = " ".repeat(width - result.len()); if flags.left_justify { From 4bcf77911982afbb34749c8e50f0213dc125e083 Mon Sep 17 00:00:00 2001 From: Guillaume Gielly Date: Thu, 26 Dec 2024 19:10:57 +0100 Subject: [PATCH 3/7] Remove useless custon floatingpoint and use libm instead --- Cargo.lock | 7 +++++ Cargo.toml | 17 +++++++---- src/header/monetary/mod.rs | 54 ++++------------------------------ src/header/monetary/strfmon.rs | 16 +++++----- 4 files changed, 32 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0d1a5e22b..a33799b9f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,12 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -363,6 +369,7 @@ dependencies = [ "generic-rt", "goblin", "libc", + "libm", "md-5", "memchr", "pbkdf2", diff --git a/Cargo.toml b/Cargo.toml index f7ff066279..a6adf74745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,14 +36,17 @@ memchr = { version = "2.2.0", default-features = false } plain = "0.2" unicode-width = "0.1" __libc_only_for_layout_checks = { package = "libc", version = "0.2.149", optional = true } -md5-crypto = { package = "md-5", version = "0.10.6", default-features = false} +md5-crypto = { package = "md-5", version = "0.10.6", default-features = false } sha-crypt = { version = "0.5", default-features = false } base64ct = { version = "1.6", default-features = false, features = ["alloc"] } -bcrypt-pbkdf = { version = "0.10", default-features = false, features = ["alloc"] } -scrypt = { version = "0.11", default-features = false, features = ["simple"]} -pbkdf2 = { version = "0.12", features = ["sha2"]} +bcrypt-pbkdf = { version = "0.10", default-features = false, features = [ + "alloc", +] } +scrypt = { version = "0.11", default-features = false, features = ["simple"] } +pbkdf2 = { version = "0.12", features = ["sha2"] } sha2 = { version = "0.10", default-features = false } generic-rt = { path = "generic-rt" } +libm = "0.2" [dependencies.goblin] version = "0.7" @@ -62,7 +65,9 @@ sc = "0.2.3" redox_syscall = "0.5.8" redox-rt = { path = "redox-rt" } redox-path = "0.2" -redox_event = { git = "https://gitlab.redox-os.org/redox-os/event.git", default-features = false, features = ["redox_syscall"] } +redox_event = { git = "https://gitlab.redox-os.org/redox-os/event.git", default-features = false, features = [ + "redox_syscall", +] } [features] default = ["check_against_libc_crate"] @@ -76,4 +81,4 @@ panic = "abort" panic = "abort" [patch.crates-io] -cc-11 = { git = "https://github.com/tea/cc-rs", branch="riscv-abi-arch-fix", package = "cc" } +cc-11 = { git = "https://github.com/tea/cc-rs", branch = "riscv-abi-arch-fix", package = "cc" } diff --git a/src/header/monetary/mod.rs b/src/header/monetary/mod.rs index 2916ad256d..733f752448 100644 --- a/src/header/monetary/mod.rs +++ b/src/header/monetary/mod.rs @@ -6,6 +6,8 @@ use alloc::string::{String, ToString}; use core::{ffi::CStr, ptr, slice, str}; +use libm::{fabs, floor, pow, round, trunc}; + extern crate alloc; mod strfmon; @@ -60,50 +62,6 @@ struct FormatFlags { international: bool, } -/// Use our own floating point implementation -/// TODO : maybe we can 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 -} - /// Formats a monetary value according to the current locale. fn apply_grouping(int_str: &str, monetary: &LocaleMonetaryInfo) -> String { let mut grouped = String::with_capacity(int_str.len() * 2); @@ -131,15 +89,15 @@ fn apply_grouping(int_str: &str, monetary: &LocaleMonetaryInfo) -> String { /// 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); + let abs_value = fabs(value); if abs_value > (i64::MAX as f64) { // Check if the value is too large to format 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; + let int_part = trunc(abs_value) as i64; + let scale = pow(10.0, frac_digits as f64); + let frac_part = 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 index 3feda48182..d5eab2c889 100644 --- a/src/header/monetary/strfmon.rs +++ b/src/header/monetary/strfmon.rs @@ -1,8 +1,7 @@ -use super::{ - my_fabs, my_pow10, my_round, my_trunc, FormatFlags, LocaleMonetaryInfo, DEFAULT_MONETARY, -}; +use super::{FormatFlags, LocaleMonetaryInfo, DEFAULT_MONETARY}; use alloc::string::{String, ToString}; use core::{ffi::CStr, ptr, slice, str}; +use libm::{fabs, floor, pow, round, trunc}; /* The strfmon() function formats a monetary value according to the format string format and writes the result to the character @@ -156,8 +155,8 @@ fn format_monetary( ) -> 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 abs_value = fabs(value); // Get the absolute value of the number + let frac_digits = if flags.international { flags .right_precision @@ -168,9 +167,9 @@ fn format_monetary( .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; + let scale = pow(10.0, frac_digits as f64); + let mut int_part = trunc(abs_value) as i64; + let mut frac_part = 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. @@ -243,6 +242,7 @@ fn format_monetary( // Add a space between the currency symbol and the value let sep_by_space = if is_negative { + //let scale = my_pow10(frac_digits); monetary.n_sep_by_space } else { monetary.p_sep_by_space From ed4c1f089e8f00763abdb9ebd6e8ac72347031da Mon Sep 17 00:00:00 2001 From: Guillaume Gielly Date: Thu, 26 Dec 2024 19:33:16 +0100 Subject: [PATCH 4/7] Refactor strfmon() --- src/header/monetary/strfmon.rs | 124 +++++++++++++++++---------------- 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/src/header/monetary/strfmon.rs b/src/header/monetary/strfmon.rs index d5eab2c889..d56a273862 100644 --- a/src/header/monetary/strfmon.rs +++ b/src/header/monetary/strfmon.rs @@ -1,65 +1,64 @@ use super::{FormatFlags, LocaleMonetaryInfo, DEFAULT_MONETARY}; use alloc::string::{String, ToString}; -use core::{ffi::CStr, ptr, slice, str}; +use core::{ffi::CStr, ptr, result, slice, str}; use libm::{fabs, floor, pow, round, trunc}; -/* - 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. - - TODO : better handling error : always return -1 if an error occurred -*/ +/// 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 can contain plain characters and format specifiers. +/// +/// Returns: +/// - The number of characters written (excluding the null terminator), or +/// - -1 if an error occurs (e.g., invalid input, buffer overflow). pub unsafe extern "C" fn strfmon( - s: *mut i8, // char * - maxsize: usize, // size_t - format: *const i8, // const char * - mut args: ... + s: *mut i8, // Output buffer + maxsize: usize, // Maximum size of the buffer + format: *const i8, // Format string + mut args: ... // Variadic arguments for monetary values ) -> isize { + // Validate input pointers and buffer size if s.is_null() || format.is_null() || maxsize == 0 { - // Check for null pointers and zero size - return -1; + return -1; // Invalid input } + // Convert the format string from C to string 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 + Ok(s) => s, + Err(_) => return -1, // Invalid format string }; - // Buffer to write the formatted string + + // Create a mutable slice for the output buffer 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(); + // Parse the format string while let Some(c) = format_chars.next() { + // Handle plain characters (non-`%`) if c != '%' { if pos >= buffer.len() { - return -1; + return -1; // Buffer overflow } - // 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 + pos += 1; + continue; } - // Handle '%%' as an escape sequence + + // Handle `%%` 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 '%' + buffer[pos] = b'%'; // Write a literal `%` + pos += 1; + format_chars.next(); // Skip the second `%` continue; } + // Parse format specifiers let mut flags = FormatFlags::default(); - // Parse flags + // Parse flags (`+`, `(`, ...) while let Some(&next_char) = format_chars.peek() { match next_char { '=' => flags.left_justify = true, @@ -67,7 +66,7 @@ pub unsafe extern "C" fn strfmon( ' ' => flags.space_sign = true, '(' => flags.use_parens = true, '!' => flags.suppress_symbol = true, - _ => break, + _ => break, // Stop when no more flags are found } format_chars.next(); } @@ -78,32 +77,31 @@ pub unsafe extern "C" fn strfmon( 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 + num_str.push(c); + format_chars.next(); } if !num_str.is_empty() { - flags.field_width = num_str.parse().ok(); // Parse the number string + flags.field_width = num_str.parse().ok(); // Parse as integer } - // Parse left precision + // Parse left precision (`#`) if format_chars.peek() == Some(&'#') { - format_chars.next(); // Skip the '#' - num_str.clear(); // Clear the number string + format_chars.next(); // Skip `#` + num_str.clear(); // Clear the string for precision 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(); + flags.left_precision = num_str.parse().ok(); // Parse as integer } - // Parse right precision + // Parse right precision indicated by `.` if format_chars.peek() == Some(&'.') { - format_chars.next(); - num_str.clear(); + format_chars.next(); // Skip `.` + num_str.clear(); // Clear the string for precision while let Some(&c) = format_chars.peek() { if !c.is_ascii_digit() { break; @@ -111,37 +109,41 @@ pub unsafe extern "C" fn strfmon( num_str.push(c); format_chars.next(); } - flags.right_precision = num_str.parse().ok(); + flags.right_precision = num_str.parse().ok(); // Parse as integer } + // Handle conversion specifiers (`i` or `n`) match format_chars.next() { Some('i') => { + // International formatting flags.international = true; - let value = unsafe { args.arg::() }; + let value = unsafe { args.arg::() }; // Get the argument as f64 if let Some(written) = format_monetary(&mut buffer[pos..], value, &DEFAULT_MONETARY, &flags) { - pos += written; + pos += written; // Update the position } else { - return -1; + return -1; // Formatting failed } } Some('n') => { - let value = unsafe { args.arg::() }; // Get the next argument as a f64 + // Locale-specific formatting + let value = unsafe { args.arg::() }; // Get the argument as 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 + pos += written; // Update the position } else { - return -1; + return -1; // Failed to format the value } } _ => return -1, } } + // Ensure there is space for the null terminator if pos >= buffer.len() { - return -1; + return -1; // Buffer overflow } buffer[pos] = 0; // Null-terminate the buffer pos as isize // Return the number of characters written @@ -153,9 +155,9 @@ fn format_monetary( 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 = fabs(value); // Get the absolute value of the number + let mut pos = 0; + let is_negative = value < 0.0; + let abs_value = fabs(value); let frac_digits = if flags.international { flags @@ -197,10 +199,12 @@ fn format_monetary( // 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); + if count > 0 { + let current_group = monetary.mon_grouping.get(group_idx).copied().unwrap_or(0); + if current_group > 0 && count % current_group == 0 { + grouped.push_str(monetary.mon_thousands_sep); + group_idx += 1; // Move to the next grouping size + } } grouped.push(c); count += 1; @@ -242,7 +246,7 @@ fn format_monetary( // Add a space between the currency symbol and the value let sep_by_space = if is_negative { - //let scale = my_pow10(frac_digits); + let scale = pow(10.0, frac_digits as f64); monetary.n_sep_by_space } else { monetary.p_sep_by_space From 2f96ac181a18ab2a97bcc2f2e7f84ee484bf184f Mon Sep 17 00:00:00 2001 From: Guillaume Gielly Date: Thu, 26 Dec 2024 20:35:43 +0100 Subject: [PATCH 5/7] Refactor and logic of format_monetary() --- src/header/monetary/strfmon.rs | 159 +++++++++++++++++---------------- 1 file changed, 81 insertions(+), 78 deletions(-) diff --git a/src/header/monetary/strfmon.rs b/src/header/monetary/strfmon.rs index d56a273862..540ec1003c 100644 --- a/src/header/monetary/strfmon.rs +++ b/src/header/monetary/strfmon.rs @@ -1,4 +1,4 @@ -use super::{FormatFlags, LocaleMonetaryInfo, DEFAULT_MONETARY}; +use super::{apply_grouping, FormatFlags, LocaleMonetaryInfo, DEFAULT_MONETARY}; use alloc::string::{String, ToString}; use core::{ffi::CStr, ptr, result, slice, str}; use libm::{fabs, floor, pow, round, trunc}; @@ -8,8 +8,8 @@ use libm::{fabs, floor, pow, round, trunc}; /// The format string can contain plain characters and format specifiers. /// /// Returns: -/// - The number of characters written (excluding the null terminator), or -/// - -1 if an error occurs (e.g., invalid input, buffer overflow). +/// - The number of characters written (excluding the null terminator), or -1 if +/// an error occurs (e.g., invalid input, buffer overflow) pub unsafe extern "C" fn strfmon( s: *mut i8, // Output buffer maxsize: usize, // Maximum size of the buffer @@ -149,16 +149,29 @@ pub unsafe extern "C" fn strfmon( pos as isize // Return the number of characters written } +/// Formats a monetary value into the given `buffer` using locale-specific rules +/// from `monetary` and formatting options from `flags`. +/// Returns `Some(len)` if successful, where `len` is the number of bytes written, +/// or `None` on error. +/// +/// # Parameters +/// - `buffer`: The output slice in which to write the formatted string +/// - `value`: The numeric value to format as monetary +/// - `monetary`: Locale-specific formatting rules, including symbols, separators, +/// and grouping sizes +/// - `flags`: Additional formatting options +/// fn format_monetary( buffer: &mut [u8], value: f64, monetary: &LocaleMonetaryInfo, flags: &FormatFlags, ) -> Option { - let mut pos = 0; + // 1) determine sign and absolute value let is_negative = value < 0.0; let abs_value = fabs(value); + // 2) figure out how many fractionals digits to use let frac_digits = if flags.international { flags .right_precision @@ -169,90 +182,65 @@ fn format_monetary( .unwrap_or(monetary.frac_digits as usize) }; + // 3) split the value into integer and fractional parts let scale = pow(10.0, frac_digits as f64); let mut int_part = trunc(abs_value) as i64; let mut frac_part = 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. + // 4) handle carry-over if frac_part equals or exceeds scale after rounding if frac_part >= scale as i64 { - // Handle carry-over to the integer part int_part += 1; frac_part = 0; } + // 5) convert the integer part to string let mut int_str = int_part.to_string(); + // 6) apply left precision if specified (padding with '0') + // So if left_precision is 5 and int_str is "42", it becomes "00042". if let Some(left_prec) = flags.left_precision { if int_str.len() > left_prec { + // The integer part is too large to fit the precision return None; } - while int_str.len() < left_prec { - int_str.insert(0, '0'); - } + // Right-align the number in a field of `left_prec` width, padded with '0' + int_str = format!("{:0>width$}", int_str, width = left_prec); } - // Apply grouping - let mut grouped = String::with_capacity(int_str.len() * 2); - let mut group_idx = 0; - let mut count = 0; + // 7) build the final formatted output in a temporary String + let mut result = String::with_capacity(int_str.len() * 2 + 20); - // 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 { - let current_group = monetary.mon_grouping.get(group_idx).copied().unwrap_or(0); - if current_group > 0 && count % current_group == 0 { - grouped.push_str(monetary.mon_thousands_sep); - group_idx += 1; // Move to the next grouping size - } - } - 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 position - let sign_posn = if is_negative { - monetary.n_sign_posn + // 7a) determine currency symbol placement and sign rules + let (cs_precedes, sep_by_space, sign_posn) = if is_negative { + ( + monetary.n_cs_precedes, + monetary.n_sep_by_space, + monetary.n_sign_posn, + ) } else { - monetary.p_sign_posn + ( + monetary.p_cs_precedes, + monetary.p_sep_by_space, + 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 { - "" + // 7b) determine which sign to display + // - negative sign if value is negative + // - positive_sign if user forced sign + // - space if space_sign is set and value is positive + // - empty otherwise + let sign = match (is_negative, flags.force_sign, flags.space_sign) { + (true, _, _) => monetary.negative_sign, + (false, true, _) => monetary.positive_sign, + (false, false, true) => " ", + _ => "", }; - // Add the sign at the beginning - 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 { - let scale = pow(10.0, frac_digits as f64); - monetary.n_sep_by_space - } else { - monetary.p_sep_by_space - }; - - // Add the currency symbol + // 7c) choose which currency symbol to display + // - maybe empty if suppressed + // - int_curr_symbol for international format + // - currency_symbol for local format let symbol = if flags.suppress_symbol { "" } else if flags.international { @@ -261,7 +249,14 @@ fn format_monetary( monetary.currency_symbol }; - // Add the currency symbol + // 8) add opening parenthesis if sign position is 0 + if sign_posn == 0 { + result.push('('); + } else if sign_posn == 1 { + result.push_str(sign); + } + + // 9) add currency symbol if it precedes the amount if cs_precedes { result.push_str(symbol); if sep_by_space { @@ -269,45 +264,53 @@ fn format_monetary( } } - // Add the value - result.push_str(&grouped.chars().rev().collect::()); + // 10) group the integer string and append it + let grouped = apply_grouping(&int_str, monetary); + result.push_str(&grouped); + // 11) append the fractional part, if any if frac_digits > 0 { result.push_str(monetary.mon_decimal_point); - result.push_str(&format!("{:0width$}", frac_part, width = frac_digits)); + // Zero-pad fractional part to the specified width + result.push_str(&format!("{:0>width$}", frac_part, width = frac_digits)); } + // 12) if the currency symbol follows the amount, add it now if !cs_precedes { - // Add the currency symbol after the value if sep_by_space { result.push(' '); } result.push_str(symbol); } + // 13) if sign_posn == 0, close the parenthesis if sign_posn == 0 { result.push(')'); } + // 14) checks if the user specified a total field width + // - if the final result is shorter, we padd it + // - if `left_justify` is true, padd on the right otherwise padd on the left if let Some(width) = flags.field_width { - // Check if the field width is specified if result.len() < width { let padding = " ".repeat(width - result.len()); if flags.left_justify { + // Left-justify: add padding at the end result.push_str(&padding); } else { + // Right-justify: add padding at the beginning 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; + // 15) write the final string to the buffer + if result.len() > buffer.len() { + // Not enough space in the output buffer + return None; } + buffer[..result.len()].copy_from_slice(result.as_bytes()); - Some(pos + result.len()) + // 16) return how many bytes we wrote + Some(result.len()) } From 1658d16b4e5686d757f845ceff076793d4aedc92 Mon Sep 17 00:00:00 2001 From: Guillaume Gielly Date: Thu, 26 Dec 2024 20:40:49 +0100 Subject: [PATCH 6/7] Prevents buffer overflow on 'scale' --- src/header/monetary/strfmon.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/header/monetary/strfmon.rs b/src/header/monetary/strfmon.rs index 540ec1003c..c2f0f3e1fd 100644 --- a/src/header/monetary/strfmon.rs +++ b/src/header/monetary/strfmon.rs @@ -184,6 +184,13 @@ fn format_monetary( // 3) split the value into integer and fractional parts let scale = pow(10.0, frac_digits as f64); + + // Check for overflow + let max_safe_int = (i64::MAX as f64) / scale; + if abs_value >= max_safe_int { + return None; + } + let mut int_part = trunc(abs_value) as i64; let mut frac_part = round((abs_value - int_part as f64) * scale) as i64; From ff940da76816336ae9ac1cdf44997b01de5e5759 Mon Sep 17 00:00:00 2001 From: Guillaume Gielly Date: Thu, 26 Dec 2024 20:49:30 +0100 Subject: [PATCH 7/7] Simplify text alignement --- src/header/monetary/strfmon.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/header/monetary/strfmon.rs b/src/header/monetary/strfmon.rs index c2f0f3e1fd..06d40f62c0 100644 --- a/src/header/monetary/strfmon.rs +++ b/src/header/monetary/strfmon.rs @@ -301,13 +301,11 @@ fn format_monetary( if let Some(width) = flags.field_width { if result.len() < width { let padding = " ".repeat(width - result.len()); - if flags.left_justify { - // Left-justify: add padding at the end - result.push_str(&padding); + result = if flags.left_justify { + result + &padding } else { - // Right-justify: add padding at the beginning - result = padding + &result; - } + padding + &result + }; } }