diff --git a/Cargo.lock b/Cargo.lock index ab80fb44fd..a33799b9f1 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,15 @@ 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 = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -263,9 +269,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", ] @@ -363,6 +369,7 @@ dependencies = [ "generic-rt", "goblin", "libc", + "libm", "md-5", "memchr", "pbkdf2", @@ -464,9 +471,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 +488,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/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/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..733f752448 --- /dev/null +++ b/src/header/monetary/mod.rs @@ -0,0 +1,103 @@ +/// 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}; + +use libm::{fabs, floor, pow, round, trunc}; + +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, +} + +/// 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; + + 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; + } + + 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 = fabs(value); + if abs_value > (i64::MAX as f64) { + // Check if the value is too large to format + return None; + } + + 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 new file mode 100644 index 0000000000..06d40f62c0 --- /dev/null +++ b/src/header/monetary/strfmon.rs @@ -0,0 +1,321 @@ +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}; + +/// 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, // 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 { + 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, + Err(_) => return -1, // Invalid format 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; // Buffer overflow + } + buffer[pos] = c as u8; // Write the character to the buffer + pos += 1; + continue; + } + + // Handle `%%` escape sequence + if format_chars.peek() == Some(&'%') { + if pos >= buffer.len() { + return -1; // Buffer overflow + } + 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 (`+`, `(`, ...) + 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, // Stop when no more flags are found + } + 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); + format_chars.next(); + } + if !num_str.is_empty() { + flags.field_width = num_str.parse().ok(); // Parse as integer + } + + // Parse left precision (`#`) + if format_chars.peek() == Some(&'#') { + 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; + } + num_str.push(c); + format_chars.next(); + } + flags.left_precision = num_str.parse().ok(); // Parse as integer + } + + // Parse right precision indicated by `.` + if format_chars.peek() == Some(&'.') { + 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; + } + num_str.push(c); + format_chars.next(); + } + 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::() }; // Get the argument as f64 + if let Some(written) = + format_monetary(&mut buffer[pos..], value, &DEFAULT_MONETARY, &flags) + { + pos += written; // Update the position + } else { + return -1; // Formatting failed + } + } + Some('n') => { + // 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; // Update the position + } else { + return -1; // Failed to format the value + } + } + _ => return -1, + } + } + + // Ensure there is space for the null terminator + if pos >= buffer.len() { + return -1; // Buffer overflow + } + buffer[pos] = 0; // Null-terminate the buffer + 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 { + // 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 + .unwrap_or(monetary.int_frac_digits as usize) + } else { + flags + .right_precision + .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); + + // 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; + + // 4) handle carry-over if frac_part equals or exceeds scale after rounding + if frac_part >= scale as i64 { + 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; + } + // Right-align the number in a field of `left_prec` width, padded with '0' + int_str = format!("{:0>width$}", int_str, width = left_prec); + } + + // 7) build the final formatted output in a temporary String + let mut result = String::with_capacity(int_str.len() * 2 + 20); + + // 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_cs_precedes, + monetary.p_sep_by_space, + monetary.p_sign_posn, + ) + }; + + // 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) => " ", + _ => "", + }; + + // 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 { + monetary.int_curr_symbol + } else { + monetary.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 { + result.push(' '); + } + } + + // 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); + // 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 { + 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 { + if result.len() < width { + let padding = " ".repeat(width - result.len()); + result = if flags.left_justify { + result + &padding + } else { + padding + &result + }; + } + } + + // 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()); + + // 16) return how many bytes we wrote + Some(result.len()) +}