Replace the printf implementation

The existing printf implementation is too buggy to back the printf
builtin. Switch to the new implementation based on printf-compat.
This commit is contained in:
ridiculousfish 2023-03-05 19:52:14 -08:00
parent 389d25e30f
commit dad1290337
13 changed files with 49 additions and 1055 deletions

36
fish-rust/Cargo.lock generated
View File

@ -81,7 +81,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "autocxx"
version = "0.23.1"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#f9ed164fed6a35a572d19f1495b0691e4b3fd92b"
dependencies = [
"aquamarine",
"autocxx-macro",
@ -114,7 +114,7 @@ dependencies = [
[[package]]
name = "autocxx-build"
version = "0.23.1"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#f9ed164fed6a35a572d19f1495b0691e4b3fd92b"
dependencies = [
"autocxx-engine",
"env_logger",
@ -125,7 +125,7 @@ dependencies = [
[[package]]
name = "autocxx-engine"
version = "0.23.1"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#f9ed164fed6a35a572d19f1495b0691e4b3fd92b"
dependencies = [
"aquamarine",
"autocxx-bindgen",
@ -154,7 +154,7 @@ dependencies = [
[[package]]
name = "autocxx-macro"
version = "0.23.1"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#f9ed164fed6a35a572d19f1495b0691e4b3fd92b"
dependencies = [
"autocxx-parser",
"proc-macro-error",
@ -166,7 +166,7 @@ dependencies = [
[[package]]
name = "autocxx-parser"
version = "0.23.1"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#311485f38289a352dcaddaad7f819f93f6e7df99"
source = "git+https://github.com/fish-shell/autocxx?branch=fish#f9ed164fed6a35a572d19f1495b0691e4b3fd92b"
dependencies = [
"indexmap",
"itertools 0.10.5",
@ -258,7 +258,7 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.81"
source = "git+https://github.com/fish-shell/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63"
source = "git+https://github.com/fish-shell/cxx?branch=fish#2b1b38264b7d10ffd946ece72b6a0f00830b3769"
dependencies = [
"cc",
"cxxbridge-flags",
@ -270,7 +270,7 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.81"
source = "git+https://github.com/fish-shell/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63"
source = "git+https://github.com/fish-shell/cxx?branch=fish#2b1b38264b7d10ffd946ece72b6a0f00830b3769"
dependencies = [
"cc",
"codespan-reporting",
@ -284,7 +284,7 @@ dependencies = [
[[package]]
name = "cxx-gen"
version = "0.7.81"
source = "git+https://github.com/fish-shell/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63"
source = "git+https://github.com/fish-shell/cxx?branch=fish#2b1b38264b7d10ffd946ece72b6a0f00830b3769"
dependencies = [
"codespan-reporting",
"proc-macro2",
@ -295,12 +295,12 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.81"
source = "git+https://github.com/fish-shell/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63"
source = "git+https://github.com/fish-shell/cxx?branch=fish#2b1b38264b7d10ffd946ece72b6a0f00830b3769"
[[package]]
name = "cxxbridge-macro"
version = "1.0.81"
source = "git+https://github.com/fish-shell/cxx?branch=fish#24d1bac1da6abbc2b483760358676e95262aca63"
source = "git+https://github.com/fish-shell/cxx?branch=fish#2b1b38264b7d10ffd946ece72b6a0f00830b3769"
dependencies = [
"proc-macro2",
"quote",
@ -381,6 +381,7 @@ dependencies = [
"num-traits",
"once_cell",
"pcre2",
"printf-compat",
"rand",
"unixstring",
"widestring",
@ -787,6 +788,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "printf-compat"
version = "0.1.1"
source = "git+https://github.com/fish-shell/printf-compat.git?branch=fish#d5f98dc8ce7a63e6639b08082ffbc6499021260c"
dependencies = [
"bitflags",
"itertools 0.9.0",
"libc",
"widestring",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -813,9 +825,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.53"
version = "1.0.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73"
checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534"
dependencies = [
"unicode-ident",
]

View File

@ -8,6 +8,7 @@ rust-version = "1.67"
widestring-suffix = { path = "./widestring-suffix/" }
pcre2 = { git = "https://github.com/fish-shell/rust-pcre2", branch = "master", default-features = false, features = ["utf32"] }
fast-float = { git = "https://github.com/fish-shell/fast-float-rust", branch="fish" }
printf-compat = { git = "https://github.com/fish-shell/printf-compat.git", branch="fish" }
autocxx = "0.23.1"
bitflags = "1.3.2"

View File

@ -191,7 +191,7 @@ fn abbr_list(opts: &Options, streams: &mut io_streams_t) -> Option<c_int> {
"%ls %ls: Unexpected argument -- '%ls'\n",
CMD,
subcmd,
opts.args[0]
&opts.args[0]
));
return STATUS_INVALID_ARGS;
}

View File

@ -7,7 +7,7 @@ use super::shared::{
use crate::event;
use crate::ffi::parser_t;
use crate::wchar::{wstr, WString};
use crate::wutil::format::printf::sprintf;
use crate::wutil::printf::sprintf;
#[widestrs]
pub fn emit(

View File

@ -7,9 +7,7 @@ use crate::builtins::shared::{
use crate::ffi::parser_t;
use crate::wchar::{wstr, L};
use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
use crate::wutil::{
self, fish_wcstoi_opts, format::printf::sprintf, wgettext_fmt, Options as WcstoiOptions,
};
use crate::wutil::{self, fish_wcstoi_opts, sprintf, wgettext_fmt, Options as WcstoiOptions};
use num_traits::PrimInt;
use once_cell::sync::Lazy;
use rand::rngs::SmallRng;
@ -176,6 +174,6 @@ pub fn random(
// Safe because end was a valid i64 and the result here is in the range start..=end.
let result: i64 = start.checked_add_unsigned(rand * step).unwrap();
streams.out.append(sprintf!(L!("%d\n"), result));
streams.out.append(sprintf!(L!("%lld\n"), result));
return STATUS_CMD_OK;
}

View File

@ -42,7 +42,7 @@ static TTY_TERMSIZE_GEN_COUNT: AtomicU32 = AtomicU32::new(0);
fn var_to_int_or(var: Option<WString>, default: isize) -> isize {
match var {
Some(s) => {
let proposed = fish_wcstoi(s.chars());
let proposed = fish_wcstoi(&s);
if let Ok(proposed) = proposed {
proposed
} else {

View File

@ -1,532 +0,0 @@
// Adapted from https://github.com/tjol/sprintf-rs
// License follows:
//
// Copyright (c) 2021 Thomas Jollans
//
// 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.
use std::convert::{TryFrom, TryInto};
use super::parser::{ConversionSpecifier, ConversionType, NumericParam};
use super::printf::{PrintfError, Result};
use crate::wchar::{wstr, WExt, WString, L};
/// 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<WString>;
/// Get `self` as an integer for use as a field width, if possible.
/// Defaults to None.
fn as_int(&self) -> Option<i32> {
None
}
}
impl Printf for u64 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
let mut base = 10;
let mut digits: Vec<char> = "0123456789".chars().collect();
let mut alt_prefix = L!("");
match spec.conversion_type {
ConversionType::DecInt => {}
ConversionType::HexIntLower => {
base = 16;
digits = "0123456789abcdef".chars().collect();
alt_prefix = L!("0x");
}
ConversionType::HexIntUpper => {
base = 16;
digits = "0123456789ABCDEF".chars().collect();
alt_prefix = L!("0X");
}
ConversionType::OctInt => {
base = 8;
digits = "01234567".chars().collect();
alt_prefix = L!("0");
}
_ => {
return Err(PrintfError::WrongType);
}
}
let prefix = if spec.alt_form {
alt_prefix.to_owned()
} else {
WString::new()
};
// Build the actual number (in reverse)
let mut rev_num = WString::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;
num_str.extend(rev_num.chars().rev());
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');
}
let mut num_str = prefix;
num_str.extend(rev_num.chars().rev());
num_str
} else {
let mut num_str = prefix;
num_str.extend(rev_num.chars().rev());
while num_str.len() < width {
num_str.insert(0, ' ');
}
num_str
};
Ok(formatted)
}
fn as_int(&self) -> Option<i32> {
i32::try_from(*self).ok()
}
}
impl Printf for i64 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
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: &wstr = if negative {
L!("-")
} else if spec.force_sign {
L!("+")
} else if spec.space_sign {
L!(" ")
} else {
L!("")
};
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.chars().position(|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> {
i32::try_from(*self).ok()
}
}
impl Printf for i32 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
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<i32> {
Some(*self)
}
}
impl Printf for u32 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
i32::try_from(*self).ok()
}
}
impl Printf for i16 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
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<i32> {
Some(*self as i32)
}
}
impl Printf for u16 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
Some(*self as i32)
}
}
impl Printf for i8 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
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)
}
_ => Err(PrintfError::WrongType),
}
}
fn as_int(&self) -> Option<i32> {
Some(*self as i32)
}
}
impl Printf for u8 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
Some(*self as i32)
}
}
impl Printf for usize {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
i32::try_from(*self).ok()
}
}
impl Printf for isize {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as u64).format(spec)
}
fn as_int(&self) -> Option<i32> {
i32::try_from(*self).ok()
}
}
impl Printf for f64 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
let mut prefix = WString::new();
let mut number = WString::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 = WString::new();
for _ in 0..precision {
rev_tail_str.push((b'0' + (tail % 10) as u8) as char);
tail /= 10;
}
number.push_str(&int_part.to_string());
number.push('.');
number.extend(rev_tail_str.chars().rev());
if strip_trailing_0s {
while number.ends_with('0') {
number.pop();
}
}
} else {
number.push_str(&format!("{}", normal.round()));
}
number.push(exp_symb);
number.push_str(&format!("{exponent:+03}"));
} 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 = WString::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(&int_part.to_string());
number.push('.');
number.extend(rev_tail_str.chars().rev());
if strip_trailing_0s {
while number.ends_with('0') {
number.pop();
}
}
} 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.insert(0, ' ');
}
full_num
};
Ok(formatted)
}
fn as_int(&self) -> Option<i32> {
None
}
}
impl Printf for f32 {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
(*self as f64).format(spec)
}
}
impl Printf for &wstr {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
if spec.conversion_type == ConversionType::String {
Ok((*self).to_owned())
} else {
Err(PrintfError::WrongType)
}
}
}
impl Printf for &str {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
if spec.conversion_type == ConversionType::String {
add_padding((*self).into(), spec)
} else {
Err(PrintfError::WrongType)
}
}
}
impl Printf for char {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
if spec.conversion_type == ConversionType::Char {
let mut s = WString::new();
s.push(*self);
Ok(s)
} else {
Err(PrintfError::WrongType)
}
}
}
impl Printf for String {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
self.as_str().format(spec)
}
}
impl Printf for WString {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
self.as_utfstr().format(spec)
}
}
impl Printf for &WString {
fn format(&self, spec: &ConversionSpecifier) -> Result<WString> {
self.as_utfstr().format(spec)
}
}
fn add_padding(mut s: WString, spec: &ConversionSpecifier) -> Result<WString> {
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 s.len() < width {
let padding = L!(" ").repeat(width - s.len());
s.insert_utfstr(0, &padding);
};
Ok(s)
}

View File

@ -1,7 +0,0 @@
#[allow(clippy::module_inception)]
mod format;
mod parser;
pub mod printf;
#[cfg(test)]
mod tests;

View File

@ -1,218 +0,0 @@
// Adapted from https://github.com/tjol/sprintf-rs
// License follows:
//
// Copyright (c) 2021 Thomas Jollans
//
// 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.
use super::printf::{PrintfError, Result};
use crate::wchar::{wstr, WExt, WString};
#[derive(Debug, Clone)]
pub enum FormatElement {
Verbatim(WString),
Format(ConversionSpecifier),
}
/// Parsed printf conversion specifier
#[derive(Debug, Clone, Copy)]
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()][super::format::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,
}
pub(crate) fn parse_format_string(fmt: &wstr) -> Result<Vec<FormatElement>> {
// find the first %
let mut res = Vec::new();
let parts: Vec<&wstr> = match fmt.find_char('%') {
Some(i) => vec![&fmt[..i], &fmt[(i + 1)..]],
None => vec![fmt],
};
if !parts[0].is_empty() {
res.push(FormatElement::Verbatim(parts[0].to_owned()));
}
if parts.len() > 1 {
let (spec, rest) = take_conversion_specifier(parts[1])?;
res.push(FormatElement::Format(spec));
res.append(&mut parse_format_string(rest)?);
}
Ok(res)
}
fn take_conversion_specifier(s: &wstr) -> Result<(ConversionSpecifier, &wstr)> {
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::Literal(6),
// 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", "l", "ll", "q", "L", "j", "z", "Z", "t"] {
if s.starts_with(len_spec) {
s = &s[len_spec.len()..];
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);
}
};
Ok((spec, &s[1..]))
}
fn take_numeric_param(s: &wstr) -> (NumericParam, &wstr) {
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),
}
}

View File

@ -1,151 +0,0 @@
// Adapted from https://github.com/tjol/sprintf-rs
// License follows:
//
// Copyright (c) 2021 Thomas Jollans
//
// 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.
pub use super::format::Printf;
use super::parser::{parse_format_string, ConversionType, FormatElement, NumericParam};
use crate::wchar::{wstr, WString};
/// Error type
#[derive(Debug, Clone, Copy)]
pub enum PrintfError {
/// Error parsing the format string
ParseError,
/// Incorrect type passed as an argument
WrongType,
/// Too many arguments passed
TooManyArgs,
/// Too few arguments passed
NotEnoughArgs,
/// Other error (should never happen)
Unknown,
}
pub type Result<T> = std::result::Result<T, PrintfError>;
/// 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: &wstr, args: &[&dyn Printf]) -> Result<WString> {
vsprintfp(&parse_format_string(format)?, args)
}
fn vsprintfp(format: &[FormatElement], args: &[&dyn Printf]) -> Result<WString> {
let mut res = WString::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_utfstr(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_utfstr(&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);
/// assert_eq!(s, "forty-two = 42");
///
/// Wrapper around [vsprintf].
macro_rules! sprintf {
// Variant which allows a string literal.
(
$fmt:literal, // format string
$($arg:expr),* // arguments
$(,)? // optional trailing comma
) => {
crate::wutil::format::printf::vsprintf(&crate::wchar::L!($fmt), &[$( &($arg) as &dyn crate::wutil::format::printf::Printf),* ][..]).expect("Invalid format string and/or arguments")
};
// Variant which allows a runtime format string, which must be of type &wstr.
(
$fmt:expr, // format string
$($arg:expr),* // arguments
$(,)? // optional trailing comma
) => {
crate::wutil::format::printf::vsprintf($fmt, &[$( &($arg) as &dyn crate::wutil::format::printf::Printf),* ][..]).expect("Invalid format string and/or arguments")
};
}
pub(crate) use sprintf;
#[cfg(test)]
mod tests {
use super::*;
use crate::wchar::L;
// Test basic printf with both literals and wide strings.
#[test]
fn test_sprintf() {
assert_eq!(sprintf!("Hello, %s!", "world"), "Hello, world!");
assert_eq!(sprintf!(L!("Hello, %ls!"), "world"), "Hello, world!");
assert_eq!(sprintf!(L!("Hello, %ls!"), L!("world")), "Hello, world!");
}
}

View File

@ -1,124 +0,0 @@
// Adapted from https://github.com/tjol/sprintf-rs
// License follows:
//
// Copyright (c) 2021 Thomas Jollans
//
// 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.
use super::printf::{sprintf, Printf};
use crate::wchar::{widestrs, WString, L};
fn check_fmt<T: Printf>(nfmt: &str, arg: T, expected: &str) {
let fmt: WString = nfmt.into();
let our_result = sprintf!(&fmt, arg);
assert_eq!(our_result, expected);
}
fn check_fmt_2<T: Printf, T2: Printf>(nfmt: &str, arg: T, arg2: T2, expected: &str) {
let fmt: WString = nfmt.into();
let our_result = sprintf!(&fmt, arg, arg2);
assert_eq!(our_result, expected);
}
#[test]
fn test_int() {
check_fmt("%d", 12, "12");
check_fmt("~%d~", 148, "~148~");
check_fmt("00%dxx", -91232, "00-91232xx");
check_fmt("%x", -9232, "ffffdbf0");
check_fmt("%X", 432, "1B0");
check_fmt("%09X", 432, "0000001B0");
check_fmt("%9X", 432, " 1B0");
check_fmt("%+9X", 492, " 1EC");
check_fmt("% #9x", 4589, " 0x11ed");
check_fmt("%2o", 4, " 4");
check_fmt("% 12d", -4, " -4");
check_fmt("% 12d", 48, " 48");
check_fmt("%ld", -4_i64, "-4");
check_fmt("%lX", -4_i64, "FFFFFFFFFFFFFFFC");
check_fmt("%ld", 48_i64, "48");
check_fmt("%-8hd", -12_i16, "-12 ");
}
#[test]
fn test_float() {
check_fmt("%f", -46.38, "-46.380000");
check_fmt("%012.3f", 1.2, "00000001.200");
check_fmt("%012.3e", 1.7, "0001.700e+00");
check_fmt("%e", 1e300, "1.000000e+300");
check_fmt("%012.3g%%!", 2.6, "0000000002.6%!");
check_fmt("%012.5G", -2.69, "-00000002.69");
check_fmt("%+7.4f", 42.785, "+42.7850");
check_fmt("{}% 7.4E", 493.12, "{} 4.9312E+02");
check_fmt("% 7.4E", -120.3, "-1.2030E+02");
check_fmt("%-10F", f64::INFINITY, "INF ");
check_fmt("%+010F", f64::INFINITY, " +INF");
check_fmt("% f", f64::NAN, " nan");
check_fmt("%+f", f64::NAN, "+nan");
check_fmt("%.1f", 999.99, "1000.0");
check_fmt("%.1f", 9.99, "10.0");
check_fmt("%.1e", 9.99, "1.0e+01");
check_fmt("%.2f", 9.99, "9.99");
check_fmt("%.2e", 9.99, "9.99e+00");
check_fmt("%.3f", 9.99, "9.990");
check_fmt("%.3e", 9.99, "9.990e+00");
check_fmt("%.1g", 9.99, "1e+01");
check_fmt("%.1G", 9.99, "1E+01");
check_fmt("%.1f", 2.99, "3.0");
check_fmt("%.1e", 2.99, "3.0e+00");
check_fmt("%.1g", 2.99, "3");
check_fmt("%.1f", 2.599, "2.6");
check_fmt("%.1e", 2.599, "2.6e+00");
check_fmt("%.1g", 2.599, "3");
}
#[test]
fn test_str() {
check_fmt(
"test %% with string: %s yay\n",
"FOO",
"test % with string: FOO yay\n",
);
check_fmt("test char %c", '~', "test char ~");
check_fmt_2("%*ls", 5, "^", " ^");
}
#[test]
#[widestrs]
fn test_str_concat() {
assert_eq!(sprintf!("%s-%ls"L, "abc", "def"L), "abc-def"L);
assert_eq!(sprintf!("%s-%ls"L, "abc", "def"L), "abc-def"L);
}
#[test]
#[should_panic]
fn test_bad_format() {
sprintf!(L!("%s"), 123);
}
#[test]
#[should_panic]
fn test_missing_arg() {
sprintf!(L!("%s-%s"), "abc");
}
#[test]
#[should_panic]
fn test_too_many_args() {
sprintf!(L!("%d"), 1, 2, 3);
}

View File

@ -1,21 +1,20 @@
pub mod errors;
pub mod format;
pub mod gettext;
mod normalize_path;
pub mod printf;
pub mod wcstod;
pub mod wcstoi;
mod wrealpath;
use std::io::Write;
use crate::wchar::{wstr, WString};
pub(crate) use format::printf::sprintf;
pub(crate) use gettext::{wgettext, wgettext_fmt};
pub use normalize_path::*;
pub(crate) use printf::sprintf;
pub use wcstoi::*;
pub use wrealpath::*;
/// Port of the wide-string wperror from `src/wutil.cpp` but for rust `&str`.
use std::io::Write;
pub fn perror(s: &str) {
let e = errno::errno().0;
let mut stderr = std::io::stderr().lock();

View File

@ -0,0 +1,16 @@
// Re-export sprintf macro.
pub(crate) use printf_compat::sprintf;
#[cfg(test)]
mod tests {
use super::*;
use crate::wchar::L;
// Test basic sprintf with both literals and wide strings.
#[test]
fn test_sprintf() {
assert_eq!(sprintf!("Hello, %s!", "world"), "Hello, world!");
assert_eq!(sprintf!(L!("Hello, %ls!"), "world"), "Hello, world!");
assert_eq!(sprintf!(L!("Hello, %ls!"), L!("world")), "Hello, world!");
}
}