use super::{
ExtendedBigDecimal, FormatChar, FormatError, OctalParsing,
num_format::{
self, Case, FloatVariant, ForceDecimal, Formatter, NumberAlignment, PositiveSign, Prefix,
UnsignedIntVariant,
},
parse_escape_only,
};
use crate::{
format::FormatArguments,
os_str_as_bytes,
quoting_style::{QuotingStyle, locale_aware_escape_name},
};
use std::{io::Write, num::NonZero, ops::ControlFlow};
#[derive(Debug)]
pub enum Spec {
Char {
position: ArgumentLocation,
width: Option<CanAsterisk<usize>>,
align_left: bool,
},
String {
position: ArgumentLocation,
precision: Option<CanAsterisk<usize>>,
width: Option<CanAsterisk<usize>>,
align_left: bool,
},
EscapedString {
position: ArgumentLocation,
},
QuotedString {
position: ArgumentLocation,
},
SignedInt {
position: ArgumentLocation,
width: Option<CanAsterisk<usize>>,
precision: Option<CanAsterisk<usize>>,
positive_sign: PositiveSign,
alignment: NumberAlignment,
},
UnsignedInt {
position: ArgumentLocation,
variant: UnsignedIntVariant,
width: Option<CanAsterisk<usize>>,
precision: Option<CanAsterisk<usize>>,
alignment: NumberAlignment,
},
Float {
position: ArgumentLocation,
variant: FloatVariant,
case: Case,
force_decimal: ForceDecimal,
width: Option<CanAsterisk<usize>>,
positive_sign: PositiveSign,
alignment: NumberAlignment,
precision: Option<CanAsterisk<usize>>,
},
}
#[derive(Clone, Copy, Debug)]
pub enum ArgumentLocation {
NextArgument,
Position(NonZero<usize>),
}
#[derive(Clone, Copy, Debug)]
pub enum CanAsterisk<T> {
Fixed(T),
Asterisk(ArgumentLocation),
}
enum Length {
Char,
Short,
Long,
LongLong,
IntMaxT,
SizeT,
PtfDiffT,
LongDouble,
}
#[derive(Default, PartialEq, Eq)]
struct Flags {
minus: bool,
plus: bool,
space: bool,
hash: bool,
zero: bool,
quote: bool,
}
impl Flags {
pub fn parse(rest: &mut &[u8], index: &mut usize) -> Self {
let mut flags = Self::default();
while let Some(x) = rest.get(*index) {
match x {
b'-' => flags.minus = true,
b'+' => flags.plus = true,
b' ' => flags.space = true,
b'#' => flags.hash = true,
b'0' => flags.zero = true,
b'\'' => {
flags.quote = true;
}
_ => break,
}
*index += 1;
}
flags
}
fn any(&self) -> bool {
self != &Self::default()
}
}
impl Spec {
pub fn parse<'a>(rest: &mut &'a [u8]) -> Result<Self, &'a [u8]> {
let mut index = 0;
let start = *rest;
let Some(position) = eat_argument_position(rest, &mut index) else {
return Err(&start[..index]);
};
let flags = Flags::parse(rest, &mut index);
let positive_sign = match flags {
Flags { plus: true, .. } => PositiveSign::Plus,
Flags { space: true, .. } => PositiveSign::Space,
_ => PositiveSign::None,
};
let width = eat_asterisk_or_number(rest, &mut index);
let precision = if let Some(b'.') = rest.get(index) {
index += 1;
Some(eat_asterisk_or_number(rest, &mut index).unwrap_or(CanAsterisk::Fixed(0)))
} else {
None
};
let alignment = if flags.minus {
NumberAlignment::Left
} else if flags.zero && precision.is_none() {
NumberAlignment::RightZero
} else {
NumberAlignment::RightSpace
};
let _ = Self::parse_length(rest, &mut index);
let Some(type_spec) = rest.get(index) else {
return Err(&start[..index]);
};
index += 1;
*rest = &start[index..];
Ok(match type_spec {
b'c' => {
if flags.zero || flags.hash || precision.is_some() {
return Err(&start[..index]);
}
Self::Char {
position,
width,
align_left: flags.minus,
}
}
b's' => {
if flags.zero || flags.hash || flags.quote {
return Err(&start[..index]);
}
Self::String {
position,
precision,
width,
align_left: flags.minus,
}
}
b'b' => {
if flags.any() || width.is_some() || precision.is_some() {
return Err(&start[..index]);
}
Self::EscapedString { position }
}
b'q' => {
if flags.any() || width.is_some() || precision.is_some() {
return Err(&start[..index]);
}
Self::QuotedString { position }
}
b'd' | b'i' => {
if flags.hash {
return Err(&start[..index]);
}
Self::SignedInt {
position,
width,
precision,
alignment,
positive_sign,
}
}
c @ (b'u' | b'o' | b'x' | b'X') => {
if *c == b'u' && flags.hash {
return Err(&start[..index]);
}
let prefix = if flags.hash { Prefix::Yes } else { Prefix::No };
let variant = match c {
b'u' => UnsignedIntVariant::Decimal,
b'o' => UnsignedIntVariant::Octal(prefix),
b'x' => UnsignedIntVariant::Hexadecimal(Case::Lowercase, prefix),
b'X' => UnsignedIntVariant::Hexadecimal(Case::Uppercase, prefix),
_ => unreachable!(),
};
Self::UnsignedInt {
position,
variant,
precision,
width,
alignment,
}
}
c @ (b'f' | b'F' | b'e' | b'E' | b'g' | b'G' | b'a' | b'A') => Self::Float {
position,
width,
precision,
variant: match c {
b'f' | b'F' => FloatVariant::Decimal,
b'e' | b'E' => FloatVariant::Scientific,
b'g' | b'G' => FloatVariant::Shortest,
b'a' | b'A' => FloatVariant::Hexadecimal,
_ => unreachable!(),
},
force_decimal: if flags.hash {
ForceDecimal::Yes
} else {
ForceDecimal::No
},
case: if c.is_ascii_uppercase() {
Case::Uppercase
} else {
Case::Lowercase
},
alignment: if flags.zero && !flags.minus {
NumberAlignment::RightZero } else {
alignment
},
positive_sign,
},
_ => return Err(&start[..index]),
})
}
fn parse_length(rest: &mut &[u8], index: &mut usize) -> Option<Length> {
let mut length = None;
loop {
let new_length = rest.get(*index).and_then(|c| {
Some(match c {
b'h' => {
if let Some(b'h') = rest.get(*index + 1) {
*index += 1;
Length::Char
} else {
Length::Short
}
}
b'l' => {
if let Some(b'l') = rest.get(*index + 1) {
*index += 1;
Length::Long
} else {
Length::LongLong
}
}
b'j' => Length::IntMaxT,
b'z' => Length::SizeT,
b't' => Length::PtfDiffT,
b'L' => Length::LongDouble,
_ => return None,
})
});
if new_length.is_some() {
*index += 1;
length = new_length;
} else {
break;
}
}
length
}
pub fn write(
&self,
mut writer: impl Write,
args: &mut FormatArguments,
) -> Result<(), FormatError> {
match self {
Self::Char {
width,
align_left,
position,
} => {
let (width, neg_width) = resolve_asterisk_width(*width, args).unwrap_or_default();
write_padded(
writer,
&[args.next_char(*position)],
width,
*align_left || neg_width,
)
}
Self::String {
width,
align_left,
precision,
position,
} => {
let (width, neg_width) = resolve_asterisk_width(*width, args).unwrap_or_default();
let precision = resolve_asterisk_precision(*precision, args);
let os_str = args.next_string(*position);
let bytes = os_str_as_bytes(os_str)?;
let truncated = match precision {
Some(p) if p < os_str.len() => &bytes[..p],
_ => bytes,
};
write_padded(writer, truncated, width, *align_left || neg_width)
}
Self::EscapedString { position } => {
let os_str = args.next_string(*position);
let bytes = os_str_as_bytes(os_str)?;
let mut parsed = Vec::<u8>::new();
for c in parse_escape_only(bytes, OctalParsing::ThreeDigits) {
match c.write(&mut parsed)? {
ControlFlow::Continue(()) => {}
ControlFlow::Break(()) => {
break;
}
}
}
writer.write_all(&parsed).map_err(FormatError::IoError)
}
Self::QuotedString { position } => {
let s = locale_aware_escape_name(
args.next_string(*position),
QuotingStyle::SHELL_ESCAPE,
);
let bytes = os_str_as_bytes(&s)?;
writer.write_all(bytes).map_err(FormatError::IoError)
}
Self::SignedInt {
width,
precision,
positive_sign,
alignment,
position,
} => {
let (width, neg_width) = resolve_asterisk_width(*width, args).unwrap_or((0, false));
let precision = resolve_asterisk_precision(*precision, args).unwrap_or_default();
let i = args.next_i64(*position);
if precision as u64 > i32::MAX as u64 {
return Err(FormatError::InvalidPrecision(precision.to_string()));
}
num_format::SignedInt {
width,
precision,
positive_sign: *positive_sign,
alignment: if neg_width {
NumberAlignment::Left
} else {
*alignment
},
}
.fmt(writer, i)
.map_err(FormatError::IoError)
}
Self::UnsignedInt {
variant,
width,
precision,
alignment,
position,
} => {
let (width, neg_width) = resolve_asterisk_width(*width, args).unwrap_or((0, false));
let precision = resolve_asterisk_precision(*precision, args).unwrap_or_default();
let i = args.next_u64(*position);
if precision as u64 > i32::MAX as u64 {
return Err(FormatError::InvalidPrecision(precision.to_string()));
}
num_format::UnsignedInt {
variant: *variant,
precision,
width,
alignment: if neg_width {
NumberAlignment::Left
} else {
*alignment
},
}
.fmt(writer, i)
.map_err(FormatError::IoError)
}
Self::Float {
variant,
case,
force_decimal,
width,
positive_sign,
alignment,
precision,
position,
} => {
let (width, neg_width) = resolve_asterisk_width(*width, args).unwrap_or((0, false));
let precision = resolve_asterisk_precision(*precision, args);
let f: ExtendedBigDecimal = args.next_extended_big_decimal(*position);
if precision.is_some_and(|p| p as u64 > i32::MAX as u64) {
return Err(FormatError::InvalidPrecision(
precision.unwrap().to_string(),
));
}
num_format::Float {
width,
precision,
variant: *variant,
case: *case,
force_decimal: *force_decimal,
positive_sign: *positive_sign,
alignment: if neg_width {
NumberAlignment::Left
} else {
*alignment
},
}
.fmt(writer, &f)
.map_err(FormatError::IoError)
}
}
}
}
fn resolve_asterisk_width(
option: Option<CanAsterisk<usize>>,
args: &mut FormatArguments,
) -> Option<(usize, bool)> {
match option {
None => None,
Some(CanAsterisk::Asterisk(loc)) => {
let nb = args.next_i64(loc);
if nb < 0 {
Some((usize::try_from(-(nb as isize)).ok().unwrap_or(0), true))
} else {
Some((usize::try_from(nb).ok().unwrap_or(0), false))
}
}
Some(CanAsterisk::Fixed(w)) => Some((w, false)),
}
}
fn resolve_asterisk_precision(
option: Option<CanAsterisk<usize>>,
args: &mut FormatArguments,
) -> Option<usize> {
match option {
None => None,
Some(CanAsterisk::Asterisk(loc)) => match args.next_i64(loc) {
v if v >= 0 => usize::try_from(v).ok(),
v if v < 0 => Some(0usize),
_ => None,
},
Some(CanAsterisk::Fixed(w)) => Some(w),
}
}
fn write_padded(
mut writer: impl Write,
text: &[u8],
width: usize,
left: bool,
) -> Result<(), FormatError> {
let padlen = width.saturating_sub(text.len());
super::check_width(padlen).map_err(FormatError::IoError)?;
if left {
writer.write_all(text)?;
write!(writer, "{: <padlen$}", "")
} else {
write!(writer, "{: >padlen$}", "")?;
writer.write_all(text)
}
.map_err(FormatError::IoError)
}
fn eat_argument_position(rest: &mut &[u8], index: &mut usize) -> Option<ArgumentLocation> {
let original_index = *index;
if let Some(pos) = eat_number(rest, index) {
if let Some(&b'$') = rest.get(*index) {
*index += 1;
Some(ArgumentLocation::Position(NonZero::new(pos)?))
} else {
*index = original_index;
Some(ArgumentLocation::NextArgument)
}
} else {
*index = original_index;
Some(ArgumentLocation::NextArgument)
}
}
fn eat_asterisk_or_number(rest: &mut &[u8], index: &mut usize) -> Option<CanAsterisk<usize>> {
if let Some(b'*') = rest.get(*index) {
*index += 1;
Some(CanAsterisk::Asterisk(eat_argument_position(rest, index)?))
} else {
eat_number(rest, index).map(CanAsterisk::Fixed)
}
}
fn eat_number(rest: &mut &[u8], index: &mut usize) -> Option<usize> {
match rest[*index..].iter().position(|b| !b.is_ascii_digit()) {
None | Some(0) => None,
Some(i) => {
let num_str = std::str::from_utf8(&rest[*index..(*index + i)]).unwrap();
*index += i;
Some(num_str.parse().unwrap_or(usize::MAX))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
mod resolve_asterisk_width {
use super::*;
use crate::format::FormatArgument;
#[test]
fn no_width() {
assert_eq!(
None,
resolve_asterisk_width(None, &mut FormatArguments::new(&[]))
);
}
#[test]
fn fixed_width() {
assert_eq!(
Some((42, false)),
resolve_asterisk_width(
Some(CanAsterisk::Fixed(42)),
&mut FormatArguments::new(&[])
)
);
}
#[test]
fn asterisks_with_numbers() {
assert_eq!(
Some((42, false)),
resolve_asterisk_width(
Some(CanAsterisk::Asterisk(ArgumentLocation::NextArgument)),
&mut FormatArguments::new(&[FormatArgument::SignedInt(42)]),
)
);
assert_eq!(
Some((42, false)),
resolve_asterisk_width(
Some(CanAsterisk::Asterisk(ArgumentLocation::NextArgument)),
&mut FormatArguments::new(&[FormatArgument::Unparsed("42".into())]),
)
);
assert_eq!(
Some((42, true)),
resolve_asterisk_width(
Some(CanAsterisk::Asterisk(ArgumentLocation::NextArgument)),
&mut FormatArguments::new(&[FormatArgument::SignedInt(-42)]),
)
);
assert_eq!(
Some((42, true)),
resolve_asterisk_width(
Some(CanAsterisk::Asterisk(ArgumentLocation::NextArgument)),
&mut FormatArguments::new(&[FormatArgument::Unparsed("-42".into())]),
)
);
assert_eq!(
Some((2, false)),
resolve_asterisk_width(
Some(CanAsterisk::Asterisk(ArgumentLocation::Position(
NonZero::new(2).unwrap()
))),
&mut FormatArguments::new(&[
FormatArgument::Unparsed("1".into()),
FormatArgument::Unparsed("2".into()),
FormatArgument::Unparsed("3".into())
]),
)
);
}
}
mod resolve_asterisk_precision {
use super::*;
use crate::format::FormatArgument;
#[test]
fn no_width() {
assert_eq!(
None,
resolve_asterisk_precision(None, &mut FormatArguments::new(&[]))
);
}
#[test]
fn fixed_width() {
assert_eq!(
Some(42),
resolve_asterisk_precision(
Some(CanAsterisk::Fixed(42)),
&mut FormatArguments::new(&[])
)
);
}
#[test]
fn asterisks_with_numbers() {
assert_eq!(
Some(42),
resolve_asterisk_precision(
Some(CanAsterisk::Asterisk(ArgumentLocation::NextArgument)),
&mut FormatArguments::new(&[FormatArgument::SignedInt(42)]),
)
);
assert_eq!(
Some(42),
resolve_asterisk_precision(
Some(CanAsterisk::Asterisk(ArgumentLocation::NextArgument)),
&mut FormatArguments::new(&[FormatArgument::Unparsed("42".into())]),
)
);
assert_eq!(
Some(0),
resolve_asterisk_precision(
Some(CanAsterisk::Asterisk(ArgumentLocation::NextArgument)),
&mut FormatArguments::new(&[FormatArgument::SignedInt(-42)]),
)
);
assert_eq!(
Some(0),
resolve_asterisk_precision(
Some(CanAsterisk::Asterisk(ArgumentLocation::NextArgument)),
&mut FormatArguments::new(&[FormatArgument::Unparsed("-42".into())]),
)
);
assert_eq!(
Some(2),
resolve_asterisk_precision(
Some(CanAsterisk::Asterisk(ArgumentLocation::Position(
NonZero::new(2).unwrap()
))),
&mut FormatArguments::new(&[
FormatArgument::Unparsed("1".into()),
FormatArgument::Unparsed("2".into()),
FormatArgument::Unparsed("3".into())
]),
)
);
}
}
}