sprintf-0.4.2/.cargo_vcs_info.json0000644000000001360000000000100125240ustar { "git": { "sha1": "507ab8d8a97b21c2d60cc3d9de156117fff014fd" }, "path_in_vcs": "" }sprintf-0.4.2/.github/workflows/build_and_test.yml000064400000000000000000000005661046102023000204230ustar 00000000000000name: Rust on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build_and_test: name: Build and test runs-on: ubuntu-latest steps: - name: Check out uses: actions/checkout@v4 - name: Build run: cargo build --release --all-features - name: Test run: cargo test --release --all-features sprintf-0.4.2/.gitignore000064400000000000000000000000231046102023000132770ustar 00000000000000/target Cargo.lock sprintf-0.4.2/CHANGELOG.md000064400000000000000000000054171046102023000131340ustar 00000000000000# Changelog ## v0.4.2 (2025-05-17) * Add support for truncating string arguments (e.g. `%1.3s`) (PR [#15][PR15], thanks to [Samuel Collins (sd-collins)][sd-collins]) [PR15]: https://github.com/tjol/sprintf-rs/pull/15 ## v0.4.1 (2025-04-30) * Update to thiserror 2.0 (PR [#11][PR11], thanks to [Samuel Collins (sd-collins)][sd-collins]) * Add support for padding strings and chars (e.g. `%10s`) (PR [#13][PR13], thanks to [Samuel Collins (sd-collins)][sd-collins]) [PR11]: https://github.com/tjol/sprintf-rs/pull/11 [PR13]: https://github.com/tjol/sprintf-rs/pull/13 ## v0.4.0 (2024-12-05) * `FormatElement` now borrows a `&str` instead of owning a `String`, leading to fewer allocations and a significant performance improvement (PR [#10][PR10], thanks to [Samuel Collins (sd-collins)][sd-collins]). - this is a __breaking API change__ to the lower-level v0.2 API - the original and primary (v0.1) API is unchanged [PR10]: https://github.com/tjol/sprintf-rs/pull/10 [sd-collins]: https://github.com/sd-collins ## v0.3.1 (2024-07-14) * pointer types can be formatted with `%p` in the same way as `usize` (PR [#9][PR9], thanks to [FlΓ‘vio J. Saraiva (flaviojs)][flaviojs]) [PR9]: https://github.com/tjol/sprintf-rs/pull/9 ## v0.3.0 (2024-05-31) * More standard string and character types (PR [#7][PR7], thanks to [FlΓ‘vio J. Saraiva (flaviojs)][flaviojs]) * Support `CString` and `&CStr` for `%s`, assuming the're UTF-8 encoded * Support `u8` and `i8` (ASCII), `u16` (UCS-2) and `u32` (UCS-4) for `%c` [PR7]: https://github.com/tjol/sprintf-rs/pull/7 [flaviojs]: https://github.com/flaviojs ## v0.2.1 (2024-02-12) * Fix accidental backwards-incompatible API change in v0.2.0 ## v0.2.0 (2024-02-12) * Expose the some of the `sprintf::parser` module in the API to allow other to use the `parse_format_string` function (PR [#5][PR5], thanks to [David Alexander Bjerremose (DaBs)][DaBs]) * `PrintfError` now implements `std::error::Error` [PR5]: https://github.com/tjol/sprintf-rs/pull/5 [DaBs]: https://github.com/DaBs ## v0.1.4 (2023-09-10) * Fix parsing of `ll` length specifier (PR [#4][PR4], thanks to [Ido Yariv (codido)][codido]) [PR4]: https://github.com/tjol/sprintf-rs/pull/4 [codido]: https://github.com/codido ## v0.1.3 (2022-09-23) * Fix float rounding: 9.99 should round to 10.0, not 9.0. (Issue [#2][bug2], thanks to [Nicholas Ritchie][NicholasWMRitchie]) [bug2]: https://github.com/tjol/sprintf-rs/issues/2 [NicholasWMRitchie]: https://github.com/NicholasWMRitchie ## v0.1.2 (2021-11-06) * Fix formatting of large floats (PR [#1][PR1], thanks to [Kuba (pierd)][pierd] [PR1]: https://github.com/tjol/sprintf-rs/pull/1 [pierd]: https://github.com/pierd ## v0.1.1 (2021-08-30) * Fix bug in padding of fixed-width fields ## v0.1.0 (2021-08-24) * Initial release sprintf-0.4.2/COPYING000064400000000000000000000020551046102023000123510ustar 00000000000000Copyright (c) 2021-2025 Thomas Jollans et al. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.sprintf-0.4.2/Cargo.lock0000644000000033570000000000100105070ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "sprintf" version = "0.4.2" dependencies = [ "libc", "thiserror", ] [[package]] name = "syn" version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" sprintf-0.4.2/Cargo.toml0000644000000021330000000000100105210ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "sprintf" version = "0.4.2" authors = ["Thomas Jollans "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Clone of C s(n)printf in Rust" readme = "README.md" keywords = [ "printf", "text", "string", ] categories = [ "template-engine", "wasm", ] license = "MIT" repository = "https://github.com/tjol/sprintf-rs" [lib] name = "sprintf" path = "src/lib.rs" [[test]] name = "compare_to_libc" path = "tests/compare_to_libc.rs" [dependencies.thiserror] version = "2.0" [dev-dependencies.libc] version = "0.2" sprintf-0.4.2/Cargo.toml.orig000064400000000000000000000007201046102023000142020ustar 00000000000000[package] name = "sprintf" version = "0.4.2" edition = "2018" authors = ["Thomas Jollans "] license = "MIT" description = "Clone of C s(n)printf in Rust" repository = "https://github.com/tjol/sprintf-rs" keywords = ["printf", "text", "string"] categories = ["template-engine", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] thiserror = "2.0" [dev-dependencies] libc = "0.2" sprintf-0.4.2/README.md000064400000000000000000000012261046102023000125740ustar 00000000000000# sprintf-rs **a clone of C sprintf in Rust** This crate was created out of a desire to provide C printf-style formatting in a WASM program, where there is no libc. **Note:** *You're probably better off using standard Rust string formatting instead of this crate unless you specificaly need printf compatibility.* This crate implements a dynamically type-checked function `vsprintf` and macro `sprintf!`. Usage example: ```rust use sprintf::sprintf; let s = sprintf!("%d + %d = %d\n", 3, 9, 3+9).unwrap(); assert_eq!(s, "3 + 9 = 12\n"); ``` `libc` is a dev dependency as it is used in the tests to compare results. `std` is used for some maths functions. sprintf-0.4.2/src/format.rs000064400000000000000000000475171046102023000137570ustar 00000000000000use std::convert::{TryFrom, TryInto}; use std::ffi::{CStr, CString}; use crate::{ parser::{ConversionSpecifier, ConversionType, NumericParam}, PrintfError, Result, }; /// Trait for types that can be formatted using printf strings /// /// Implemented for the basic types and shouldn't need implementing for /// anything else. pub trait Printf { /// Format `self` based on the conversion configured in `spec`. fn format(&self, spec: &ConversionSpecifier) -> Result; /// Get `self` as an integer for use as a field width, if possible. fn as_int(&self) -> Option; } impl Printf for u64 { fn format(&self, spec: &ConversionSpecifier) -> Result { let mut base = 10; let mut digits: Vec = "0123456789".chars().collect(); let mut alt_prefix = ""; match spec.conversion_type { ConversionType::DecInt => {} ConversionType::HexIntLower => { base = 16; digits = "0123456789abcdef".chars().collect(); alt_prefix = "0x"; } ConversionType::HexIntUpper => { base = 16; digits = "0123456789ABCDEF".chars().collect(); alt_prefix = "0X"; } ConversionType::OctInt => { base = 8; digits = "01234567".chars().collect(); alt_prefix = "0"; } _ => { return Err(PrintfError::WrongType); } } let prefix = if spec.alt_form { alt_prefix.to_owned() } else { String::new() }; // Build the actual number (in reverse) let mut rev_num = String::new(); let mut n = *self; while n > 0 { let digit = n % base; n /= base; rev_num.push(digits[digit as usize]); } if rev_num.is_empty() { rev_num.push('0'); } // Take care of padding let width: usize = match spec.width { NumericParam::Literal(w) => w, _ => { return Err(PrintfError::Unknown); // should not happen at this point!! } } .try_into() .unwrap_or_default(); let formatted = if spec.left_adj { let mut num_str = prefix + &rev_num.chars().rev().collect::(); while num_str.len() < width { num_str.push(' '); } num_str } else if spec.zero_pad { while prefix.len() + rev_num.len() < width { rev_num.push('0'); } prefix + &rev_num.chars().rev().collect::() } else { let mut num_str = prefix + &rev_num.chars().rev().collect::(); while num_str.len() < width { num_str = " ".to_owned() + &num_str; } num_str }; Ok(formatted) } fn as_int(&self) -> Option { i32::try_from(*self).ok() } } impl Printf for i64 { fn format(&self, spec: &ConversionSpecifier) -> Result { match spec.conversion_type { // signed integer format ConversionType::DecInt => { // do I need a sign prefix? let negative = *self < 0; let abs_val = self.abs(); let sign_prefix = if negative { "-" } else if spec.force_sign { "+" } else if spec.space_sign { " " } else { "" } .to_owned(); let mut mod_spec = *spec; mod_spec.width = match spec.width { NumericParam::Literal(w) => NumericParam::Literal(w - sign_prefix.len() as i32), _ => { return Err(PrintfError::Unknown); } }; let formatted = (abs_val as u64).format(&mod_spec)?; // put the sign a after any leading spaces let mut actual_number = &formatted[0..]; let mut leading_spaces = &formatted[0..0]; if let Some(first_non_space) = formatted.find(|c| c != ' ') { actual_number = &formatted[first_non_space..]; leading_spaces = &formatted[0..first_non_space]; } Ok(leading_spaces.to_owned() + &sign_prefix + actual_number) } // unsigned-only formats ConversionType::HexIntLower | ConversionType::HexIntUpper | ConversionType::OctInt => { (*self as u64).format(spec) } _ => Err(PrintfError::WrongType), } } fn as_int(&self) -> Option { i32::try_from(*self).ok() } } impl Printf for i32 { fn format(&self, spec: &ConversionSpecifier) -> Result { match spec.conversion_type { // signed integer format ConversionType::DecInt => (*self as i64).format(spec), // unsigned-only formats ConversionType::HexIntLower | ConversionType::HexIntUpper | ConversionType::OctInt => { (*self as u32).format(spec) } _ => Err(PrintfError::WrongType), } } fn as_int(&self) -> Option { Some(*self) } } impl Printf for u32 { fn format(&self, spec: &ConversionSpecifier) -> Result { match spec.conversion_type { ConversionType::Char => { if let Some(c) = char::from_u32(*self) { c.format(spec) } else { Err(PrintfError::WrongType) } } _ => (*self as u64).format(spec), } } fn as_int(&self) -> Option { i32::try_from(*self).ok() } } impl Printf for i16 { fn format(&self, spec: &ConversionSpecifier) -> Result { match spec.conversion_type { // signed integer format ConversionType::DecInt => (*self as i64).format(spec), // unsigned-only formats ConversionType::HexIntLower | ConversionType::HexIntUpper | ConversionType::OctInt => { (*self as u16).format(spec) } _ => Err(PrintfError::WrongType), } } fn as_int(&self) -> Option { Some(*self as i32) } } impl Printf for u16 { fn format(&self, spec: &ConversionSpecifier) -> Result { match spec.conversion_type { ConversionType::Char => { if let Some(Ok(c)) = char::decode_utf16([*self]).next() { c.format(spec) } else { Err(PrintfError::WrongType) } } _ => (*self as u64).format(spec), } } fn as_int(&self) -> Option { Some(*self as i32) } } impl Printf for i8 { fn format(&self, spec: &ConversionSpecifier) -> Result { match spec.conversion_type { // signed integer format ConversionType::DecInt => (*self as i64).format(spec), // unsigned-only formats ConversionType::HexIntLower | ConversionType::HexIntUpper | ConversionType::OctInt => { (*self as u8).format(spec) } // c_char ConversionType::Char => (*self as u8).format(spec), _ => Err(PrintfError::WrongType), } } fn as_int(&self) -> Option { Some(*self as i32) } } impl Printf for u8 { fn format(&self, spec: &ConversionSpecifier) -> Result { match spec.conversion_type { ConversionType::Char => { if self.is_ascii() { char::from(*self).format(spec) } else { Err(PrintfError::WrongType) } } _ => (*self as u64).format(spec), } } fn as_int(&self) -> Option { Some(*self as i32) } } impl Printf for usize { fn format(&self, spec: &ConversionSpecifier) -> Result { (*self as u64).format(spec) } fn as_int(&self) -> Option { i32::try_from(*self).ok() } } impl Printf for isize { fn format(&self, spec: &ConversionSpecifier) -> Result { (*self as u64).format(spec) } fn as_int(&self) -> Option { i32::try_from(*self).ok() } } impl Printf for f64 { fn format(&self, spec: &ConversionSpecifier) -> Result { let mut prefix = String::new(); let mut number = String::new(); // set up the sign if self.is_sign_negative() { prefix.push('-'); } else if spec.space_sign { prefix.push(' '); } else if spec.force_sign { prefix.push('+'); } if self.is_finite() { let mut use_scientific = false; let mut exp_symb = 'e'; let mut strip_trailing_0s = false; let mut abs = self.abs(); let mut exponent = abs.log10().floor() as i32; let mut precision = match spec.precision { NumericParam::Literal(p) => p, _ => { return Err(PrintfError::Unknown); } }; if precision <= 0 { precision = 0; } match spec.conversion_type { ConversionType::DecFloatLower | ConversionType::DecFloatUpper => { // default } ConversionType::SciFloatLower => { use_scientific = true; } ConversionType::SciFloatUpper => { use_scientific = true; exp_symb = 'E'; } ConversionType::CompactFloatLower | ConversionType::CompactFloatUpper => { if spec.conversion_type == ConversionType::CompactFloatUpper { exp_symb = 'E' } strip_trailing_0s = true; if precision == 0 { precision = 1; } // exponent signifies significant digits - we must round now // to (re)calculate the exponent let rounding_factor = 10.0_f64.powf((precision - 1 - exponent) as f64); let rounded_fixed = (abs * rounding_factor).round(); abs = rounded_fixed / rounding_factor; exponent = abs.log10().floor() as i32; if exponent < -4 || exponent >= precision { use_scientific = true; precision -= 1; } else { // precision specifies the number of significant digits precision -= 1 + exponent; } } _ => { return Err(PrintfError::WrongType); } } if use_scientific { let mut normal = abs / 10.0_f64.powf(exponent as f64); if precision > 0 { let mut int_part = normal.trunc(); let mut exp_factor = 10.0_f64.powf(precision as f64); let mut tail = ((normal - int_part) * exp_factor).round() as u64; while tail >= exp_factor as u64 { // Overflow, must round int_part += 1.0; tail -= exp_factor as u64; if int_part >= 10.0 { // keep same precision - which means changing exponent exponent += 1; exp_factor /= 10.0; normal /= 10.0; int_part = normal.trunc(); tail = ((normal - int_part) * exp_factor).round() as u64; } } let mut rev_tail_str = String::new(); for _ in 0..precision { rev_tail_str.push((b'0' + (tail % 10) as u8) as char); tail /= 10; } number.push_str(&format!("{}", int_part)); number.push('.'); number.push_str(&rev_tail_str.chars().rev().collect::()); if strip_trailing_0s { number = number.trim_end_matches('0').to_owned(); } } else { number.push_str(&format!("{}", normal.round())); } number.push(exp_symb); number.push_str(&format!("{:+03}", exponent)); } else { if precision > 0 { let mut int_part = abs.trunc(); let exp_factor = 10.0_f64.powf(precision as f64); let mut tail = ((abs - int_part) * exp_factor).round() as u64; let mut rev_tail_str = String::new(); if tail >= exp_factor as u64 { // overflow - we must round up int_part += 1.0; tail -= exp_factor as u64; // no need to change the exponent as we don't have one // (not scientific notation) } for _ in 0..precision { rev_tail_str.push((b'0' + (tail % 10) as u8) as char); tail /= 10; } number.push_str(&format!("{}", int_part)); number.push('.'); number.push_str(&rev_tail_str.chars().rev().collect::()); if strip_trailing_0s { number = number.trim_end_matches('0').to_owned(); } } else { number.push_str(&format!("{}", abs.round())); } } } else { // not finite match spec.conversion_type { ConversionType::DecFloatLower | ConversionType::SciFloatLower | ConversionType::CompactFloatLower => { if self.is_infinite() { number.push_str("inf") } else { number.push_str("nan") } } ConversionType::DecFloatUpper | ConversionType::SciFloatUpper | ConversionType::CompactFloatUpper => { if self.is_infinite() { number.push_str("INF") } else { number.push_str("NAN") } } _ => { return Err(PrintfError::WrongType); } } } // Take care of padding let width: usize = match spec.width { NumericParam::Literal(w) => w, _ => { return Err(PrintfError::Unknown); // should not happen at this point!! } } .try_into() .unwrap_or_default(); let formatted = if spec.left_adj { let mut full_num = prefix + &number; while full_num.len() < width { full_num.push(' '); } full_num } else if spec.zero_pad && self.is_finite() { while prefix.len() + number.len() < width { prefix.push('0'); } prefix + &number } else { let mut full_num = prefix + &number; while full_num.len() < width { full_num = " ".to_owned() + &full_num; } full_num }; Ok(formatted) } fn as_int(&self) -> Option { None } } impl Printf for f32 { fn format(&self, spec: &ConversionSpecifier) -> Result { (*self as f64).format(spec) } fn as_int(&self) -> Option { None } } impl Printf for &str { fn format(&self, spec: &ConversionSpecifier) -> Result { if spec.conversion_type == ConversionType::String { let mut s = String::new(); // Take care of precision, putting the truncated string in `content` let precision: usize = match spec.precision { NumericParam::Literal(p) => p, _ => { return Err(PrintfError::Unknown); // should not happen at this point!! } } .try_into() .unwrap_or_default(); let content_len = { let mut content_len = precision.min(self.len()); while !self.is_char_boundary(content_len) { content_len -= 1; } content_len }; let content = &self[..content_len]; // Pad to width if needed, putting the padded string in `s` let width: usize = match spec.width { NumericParam::Literal(w) => w, _ => { return Err(PrintfError::Unknown); // should not happen at this point!! } } .try_into() .unwrap_or_default(); if spec.left_adj { s.push_str(content); while s.len() < width { s.push(' '); } } else { while s.len() + content.len() < width { s.push(' '); } s.push_str(content); } Ok(s) } else { Err(PrintfError::WrongType) } } fn as_int(&self) -> Option { None } } impl Printf for char { fn format(&self, spec: &ConversionSpecifier) -> Result { if spec.conversion_type == ConversionType::Char { let mut s = String::new(); let width: usize = match spec.width { NumericParam::Literal(w) => w, _ => { return Err(PrintfError::Unknown); // should not happen at this point!! } } .try_into() .unwrap_or_default(); if spec.left_adj { s.push(*self); while s.len() < width { s.push(' '); } } else { while s.len() + self.len_utf8() < width { s.push(' '); } s.push(*self); } Ok(s) } else { Err(PrintfError::WrongType) } } fn as_int(&self) -> Option { None } } impl Printf for String { fn format(&self, spec: &ConversionSpecifier) -> Result { (self as &str).format(spec) } fn as_int(&self) -> Option { None } } impl Printf for &CStr { fn format(&self, spec: &ConversionSpecifier) -> Result { if let Ok(s) = self.to_str() { s.format(spec) } else { Err(PrintfError::WrongType) } } fn as_int(&self) -> Option { None } } impl Printf for CString { fn format(&self, spec: &ConversionSpecifier) -> Result { self.as_c_str().format(spec) } fn as_int(&self) -> Option { None } } impl Printf for *const T { fn format(&self, spec: &ConversionSpecifier) -> Result { (*self as usize).format(spec) } fn as_int(&self) -> Option { None } } impl Printf for *mut T { fn format(&self, spec: &ConversionSpecifier) -> Result { (*self as usize).format(spec) } fn as_int(&self) -> Option { None } } sprintf-0.4.2/src/lib.rs000064400000000000000000000104121046102023000132150ustar 00000000000000//! Libc s(n)printf clone written in Rust, so you can use printf-style //! formatting without a libc (e.g. in WebAssembly). //! //! **Note:** *You're probably better off using standard Rust string formatting //! instead of thie crate unless you specificaly need printf compatibility.* //! //! It follows the standard C semantics, except: //! //! * Locale-aware UNIX extensions (`'` and GNU’s `I`) are not supported. //! * `%a`/`%A` (hexadecimal floating point) are currently not implemented. //! * Length modifiers (`h`, `l`, etc.) are checked, but ignored. The passed //! type is used instead. //! //! Usage example: //! //! use sprintf::sprintf; //! let s = sprintf!("%d + %d = %d\n", 3, 9, 3+9).unwrap(); //! assert_eq!(s, "3 + 9 = 12\n"); //! //! The types of the arguments are checked at runtime. //! use thiserror::Error; mod format; pub mod parser; pub use format::Printf; use parser::{parse_format_string, FormatElement}; #[doc(hidden)] pub use parser::{ConversionSpecifier, ConversionType, NumericParam}; /// Error type #[derive(Debug, Clone, Copy, Error, PartialEq, Eq)] pub enum PrintfError { /// Error parsing the format string #[error("Error parsing the format string")] ParseError, /// Incorrect type passed as an argument #[error("Incorrect type passed as an argument")] WrongType, /// Too many arguments passed #[error("Too many arguments passed")] TooManyArgs, /// Too few arguments passed #[error("Too few arguments passed")] NotEnoughArgs, /// Other error (should never happen) #[error("Other error (should never happen)")] Unknown, } pub type Result = std::result::Result; /// Format a string. (Roughly equivalent to `vsnprintf` or `vasprintf` in C) /// /// Takes a printf-style format string `format` and a slice of dynamically /// typed arguments, `args`. /// /// use sprintf::{vsprintf, Printf}; /// let n = 16; /// let args: Vec<&dyn Printf> = vec![&n]; /// let s = vsprintf("%#06x", &args).unwrap(); /// assert_eq!(s, "0x0010"); /// /// See also: [sprintf] pub fn vsprintf(format: &str, args: &[&dyn Printf]) -> Result { vsprintfp(&parse_format_string(format)?, args) } /// Format a string using [`parser::FormatElement`]s /// /// Like [vsprintf], except that it doesn't parse the format string. pub fn vsprintfp(format: &[FormatElement], args: &[&dyn Printf]) -> Result { let mut res = String::new(); let mut args = args; let mut pop_arg = || { if args.is_empty() { Err(PrintfError::NotEnoughArgs) } else { let a = args[0]; args = &args[1..]; Ok(a) } }; for elem in format { match elem { FormatElement::Verbatim(s) => { res.push_str(s); } FormatElement::Format(spec) => { if spec.conversion_type == ConversionType::PercentSign { res.push('%'); } else { let mut completed_spec = *spec; if spec.width == NumericParam::FromArgument { completed_spec.width = NumericParam::Literal( pop_arg()?.as_int().ok_or(PrintfError::WrongType)?, ) } if spec.precision == NumericParam::FromArgument { completed_spec.precision = NumericParam::Literal( pop_arg()?.as_int().ok_or(PrintfError::WrongType)?, ) } res.push_str(&pop_arg()?.format(&completed_spec)?); } } } } if args.is_empty() { Ok(res) } else { Err(PrintfError::TooManyArgs) } } /// Format a string. (Roughly equivalent to `snprintf` or `asprintf` in C) /// /// Takes a printf-style format string `format` and a variable number of /// additional arguments. /// /// use sprintf::sprintf; /// let s = sprintf!("%s = %*d", "forty-two", 4, 42).unwrap(); /// assert_eq!(s, "forty-two = 42"); /// /// Wrapper around [vsprintf]. #[macro_export] macro_rules! sprintf { ($fmt:expr, $($arg:expr),*) => { sprintf::vsprintf($fmt, &[$( &($arg) as &dyn sprintf::Printf),* ][..]) }; } sprintf-0.4.2/src/parser.rs000064400000000000000000000160021046102023000137440ustar 00000000000000//! Parse printf format strings use crate::{PrintfError, Result}; /// A part of a format string: either a string of characters to be included /// verbatim, or a format specifier that should be replaced based on an argument /// to the [vsprintf](crate::vsprintf) call. #[derive(Debug, Clone, PartialEq, Eq)] pub enum FormatElement<'a> { /// Some characters that are copied to the output as-is Verbatim(&'a str), /// A format specifier Format(ConversionSpecifier), } /// Parsed printf conversion specifier #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ConversionSpecifier { /// flag `#`: use `0x`, etc? pub alt_form: bool, /// flag `0`: left-pad with zeros? pub zero_pad: bool, /// flag `-`: left-adjust (pad with spaces on the right) pub left_adj: bool, /// flag `' '` (space): indicate sign with a space? pub space_sign: bool, /// flag `+`: Always show sign? (for signed numbers) pub force_sign: bool, /// field width pub width: NumericParam, /// floating point field precision pub precision: NumericParam, /// data type pub conversion_type: ConversionType, } /// Width / precision parameter #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NumericParam { /// The literal width Literal(i32), /// Get the width from the previous argument /// /// This should never be passed to [Printf::format()][crate::Printf::format()]. FromArgument, } /// Printf data type #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConversionType { /// `d`, `i`, or `u` DecInt, /// `o` OctInt, /// `x` or `p` HexIntLower, /// `X` HexIntUpper, /// `e` SciFloatLower, /// `E` SciFloatUpper, /// `f` DecFloatLower, /// `F` DecFloatUpper, /// `g` CompactFloatLower, /// `G` CompactFloatUpper, /// `c` Char, /// `s` String, /// `%` PercentSign, } /// Parses a string to a vector of [FormatElement] /// /// Takes a printf-style format string `fmt` /// /// use sprintf::parser::{ /// parse_format_string, ConversionSpecifier, ConversionType, FormatElement, NumericParam, /// }; /// let fmt = "Hello %#06x"; /// let parsed = parse_format_string(fmt).unwrap(); /// assert_eq!(parsed[0], FormatElement::Verbatim("Hello ")); /// assert_eq!( /// parsed[1], /// FormatElement::Format(ConversionSpecifier { /// alt_form: true, /// zero_pad: true, /// left_adj: false, /// space_sign: false, /// force_sign: false, /// width: NumericParam::Literal(6), /// precision: NumericParam::Literal(6), /// conversion_type: ConversionType::HexIntLower, /// }) /// ); /// pub fn parse_format_string(fmt: &str) -> Result> { // find the first % let mut res = Vec::new(); let mut rem = fmt; while !rem.is_empty() { if let Some((verbatim_prefix, rest)) = rem.split_once('%') { if !verbatim_prefix.is_empty() { res.push(FormatElement::Verbatim(verbatim_prefix)); } let (spec, rest) = take_conversion_specifier(rest)?; res.push(FormatElement::Format(spec)); rem = rest; } else { res.push(FormatElement::Verbatim(rem)); break; } } Ok(res) } fn take_conversion_specifier(s: &str) -> Result<(ConversionSpecifier, &str)> { let mut spec = ConversionSpecifier { alt_form: false, zero_pad: false, left_adj: false, space_sign: false, force_sign: false, width: NumericParam::Literal(0), precision: NumericParam::FromArgument, // Placeholder - must not be returned! // ignore length modifier conversion_type: ConversionType::DecInt, }; let mut s = s; // parse flags loop { match s.chars().next() { Some('#') => { spec.alt_form = true; } Some('0') => { spec.zero_pad = true; } Some('-') => { spec.left_adj = true; } Some(' ') => { spec.space_sign = true; } Some('+') => { spec.force_sign = true; } _ => { break; } } s = &s[1..]; } // parse width let (w, mut s) = take_numeric_param(s); spec.width = w; // parse precision if matches!(s.chars().next(), Some('.')) { s = &s[1..]; let (p, s2) = take_numeric_param(s); spec.precision = p; s = s2; } // check length specifier for len_spec in ["hh", "h", "ll", "l", "q", "L", "j", "z", "Z", "t"] { if s.starts_with(len_spec) { s = s.strip_prefix(len_spec).ok_or(PrintfError::ParseError)?; break; // only allow one length specifier } } // parse conversion type spec.conversion_type = match s.chars().next() { Some('i') | Some('d') | Some('u') => ConversionType::DecInt, Some('o') => ConversionType::OctInt, Some('x') => ConversionType::HexIntLower, Some('X') => ConversionType::HexIntUpper, Some('e') => ConversionType::SciFloatLower, Some('E') => ConversionType::SciFloatUpper, Some('f') => ConversionType::DecFloatLower, Some('F') => ConversionType::DecFloatUpper, Some('g') => ConversionType::CompactFloatLower, Some('G') => ConversionType::CompactFloatUpper, Some('c') | Some('C') => ConversionType::Char, Some('s') | Some('S') => ConversionType::String, Some('p') => { spec.alt_form = true; ConversionType::HexIntLower } Some('%') => ConversionType::PercentSign, _ => { return Err(PrintfError::ParseError); } }; if spec.precision == NumericParam::FromArgument { // If precision is not specified, set to default value let p = if spec.conversion_type == ConversionType::String { // Default to max limit (aka no limit) for strings i32::MAX } else { // Default to 6 for all other types 6 }; spec.precision = NumericParam::Literal(p); } Ok((spec, &s[1..])) } fn take_numeric_param(s: &str) -> (NumericParam, &str) { match s.chars().next() { Some('*') => (NumericParam::FromArgument, &s[1..]), Some(digit) if ('1'..='9').contains(&digit) => { let mut s = s; let mut w = 0; loop { match s.chars().next() { Some(digit) if ('0'..='9').contains(&digit) => { w = 10 * w + (digit as i32 - '0' as i32); } _ => { break; } } s = &s[1..]; } (NumericParam::Literal(w), s) } _ => (NumericParam::Literal(0), s), } } sprintf-0.4.2/tests/compare_to_libc.rs000064400000000000000000000133461046102023000161540ustar 00000000000000// The libc crate on Windows doesn't have snprintf #![cfg(not(windows))] use std::convert::{TryFrom, TryInto}; use std::ffi::CString; use std::mem::size_of; use std::os::raw::c_char; use libc::snprintf; use sprintf::*; fn check_fmt(fmt: &str, arg: T) { let our_result = sprintf!(fmt, arg).unwrap(); let mut buf = vec![0_u8; our_result.len() + 1]; let cfmt = CString::new(fmt).unwrap(); let clen: usize = unsafe { snprintf( buf.as_mut_ptr() as *mut c_char, buf.len(), cfmt.as_ptr(), arg, ) } .try_into() .unwrap(); buf.truncate(clen); // drop the final '\0', etc. let c_result = String::from_utf8(buf).unwrap(); assert_eq!(our_result, c_result); } fn check_fmt_s(fmt: &str, arg: &str) { let our_result = sprintf!(fmt, arg).unwrap(); let mut buf = vec![0_u8; our_result.len() + 1]; let cfmt = CString::new(fmt).unwrap(); let carg = CString::new(arg).unwrap(); let clen: usize = unsafe { snprintf( buf.as_mut_ptr() as *mut c_char, buf.len(), cfmt.as_ptr(), carg.as_ptr(), ) } .try_into() .unwrap(); buf.truncate(clen); // drop the final '\0', etc. let c_result = String::from_utf8(buf).unwrap(); assert_eq!(our_result, c_result); } #[test] fn test_int() { check_fmt("%d", 12); check_fmt("~%d~", 148); check_fmt("00%dxx", -91232); check_fmt("%x", -9232); check_fmt("%X", 432); check_fmt("%09X", 432); check_fmt("%9X", 432); check_fmt("%+9X", 492); check_fmt("% #9x", 4589); check_fmt("%2o", 4); check_fmt("% 12d", -4); check_fmt("% 12d", 48); check_fmt("%ld", -4_i64); check_fmt("%lX", -4_i64); check_fmt("%ld", 48_i64); check_fmt("%-8hd", -12_i16); check_fmt("%llx", 0x0123456789abcdef_u64); } #[test] fn test_float() { check_fmt("%f", -46.38); check_fmt("%012.3f", 1.2); check_fmt("%012.3e", 1.7); check_fmt("%e", 1e300); check_fmt("%012.3g%%!", 2.6); check_fmt("%012.5G", -2.69); check_fmt("%+7.4f", 42.785); check_fmt("{}% 7.4E", 493.12); check_fmt("% 7.4E", -120.3); check_fmt("%-10F", f64::INFINITY); check_fmt("%+010F", f64::INFINITY); check_fmt("%.1f", 999.99); check_fmt("%.1f", 9.99); check_fmt("%.1e", 9.99); check_fmt("%.2f", 9.99); check_fmt("%.2e", 9.99); check_fmt("%.3f", 9.99); check_fmt("%.3e", 9.99); check_fmt("%.1g", 9.99); check_fmt("%.1G", 9.99); check_fmt("%.1f", 2.99); check_fmt("%.1e", 2.99); check_fmt("%.1g", 2.99); check_fmt("%.1f", 2.599); check_fmt("%.1e", 2.599); check_fmt("%.1g", 2.599); // MacOS libc behaves differently from glibc for nan. glibc is the reference implementation. if cfg!(target_env = "gnu") { check_fmt("% f", f64::NAN); check_fmt("%+f", f64::NAN); } else { assert_eq!(sprintf!("% f", f64::NAN).unwrap(), " nan"); assert_eq!(sprintf!("%+f", f64::NAN).unwrap(), "+nan"); } } #[test] fn test_str() { check_fmt_s("test %% with string: %s yay\n", "FOO"); check_fmt_s( "%s", "testing with a slightly longer string to make sure it doesn't truncate", ); check_fmt("test char %c", '~'); let c_string = CString::new("test").unwrap(); check_fmt("%s", c_string.as_c_str()); check_fmt("%s", c_string); check_fmt_s("%4s", "A"); check_fmt_s("%4s", "π’€€"); // multi-byte character test (4 bytes) check_fmt_s("%-4sX", "A"); check_fmt_s("%-4sX", "π’€€"); // multi-byte character test (4 bytes) check_fmt_s("%1.3s", "ABCDEFG"); check_fmt_s("%1.4s", "π’€€π’€€"); // multi-byte character test (4 bytes per char) check_fmt_s("%8.4s", "ABCDEFG"); // glibc does not handle UTF-8 strings correctly when truncating, but we cannot produce malformed UTF-8 // strings in Rust. Instead, we round down to the nearest character boundary. assert_eq!(sprintf!("%1.1s", "π’€€π’€€π’€€").unwrap(), " "); assert_eq!(sprintf!("%1.2s", "π’€€π’€€π’€€").unwrap(), " "); assert_eq!(sprintf!("%1.3s", "π’€€π’€€π’€€").unwrap(), " "); assert_eq!(sprintf!("%1.4s", "π’€€π’€€π’€€").unwrap(), "π’€€"); assert_eq!(sprintf!("%1.5s", "π’€€π’€€π’€€").unwrap(), "π’€€"); assert_eq!(sprintf!("%1.6s", "π’€€π’€€π’€€").unwrap(), "π’€€"); assert_eq!(sprintf!("%1.7s", "π’€€π’€€π’€€").unwrap(), "π’€€"); assert_eq!(sprintf!("%1.8s", "π’€€π’€€π’€€").unwrap(), "π’€€π’€€"); } #[test] fn test_char() { check_fmt("%c", 'x'); check_fmt("%c", b'x'); check_fmt("%c", b'x' as c_char); check_fmt("%c", u16::try_from('x').unwrap()); check_fmt("%c", u32::try_from('x').unwrap()); check_fmt("%4c", 'A'); check_fmt("%-4cX", 'A'); } #[test] fn test_sanity() { // u8 must not misinterpret bytes from multi-byte UTF-8 characters let bytes = "βˆ†".as_bytes(); assert!(bytes.len() > 1); assert_eq!(sprintf!("%c", bytes[0]), Err(PrintfError::WrongType)); } #[test] fn test_ptr() { let buf: [u8; 4] = [0; 4]; let ptr_const: *const u8 = buf.as_ptr(); let ptr_mut: *mut u8 = ptr_const.cast_mut(); // pointer: expects usize and pointer to have the same size assert_eq!(size_of::(), size_of::<*const u8>(),); check_fmt("%p", ptr_const); check_fmt("%p", ptr_mut); // numeric: works the same as libc if you use the correct length specifier if size_of::() == size_of::() { check_fmt("%llx", ptr_const); check_fmt("%llx", ptr_mut); check_fmt("%#llx", ptr_const); check_fmt("%#llx", ptr_mut); } else if size_of::() == size_of::() { check_fmt("%x", ptr_const); check_fmt("%x", ptr_mut); check_fmt("%#x", ptr_const); check_fmt("%#x", ptr_mut); } }