1use std::borrow::Cow;
2use std::marker::PhantomData;
3use std::ops::Range;
4use std::str::FromStr;
5use thiserror::Error;
6
7use crate::internal::{DataType, DataTypeKind};
8
9const MAX_FORMAT_BYTES: usize = 4096;
10const MAX_DIRECTIVE_BYTES: usize = 30;
11const MAX_FIELD_WIDTH: u32 = 31;
12const MAX_PRECISION: u32 = 99;
13
14#[derive(Clone, Debug, PartialEq)]
41pub struct NumericFormat<'a, T> {
42 storage: Cow<'a, str>,
43 marker: PhantomData<fn() -> T>,
44}
45
46impl<'a, T> NumericFormat<'a, T>
47where
48 T: DataTypeKind,
49{
50 pub fn new(storage: impl Into<Cow<'a, str>>) -> Result<Self, NumericFormatError> {
67 let storage = storage.into();
68 validate_format_length(&storage)?;
69 let storage = normalize_wide_integer_length::<T>(storage);
70 validate_numeric_format::<T>(&storage)?;
71 Ok(Self {
72 storage,
73 marker: PhantomData,
74 })
75 }
76
77 pub unsafe fn new_unchecked(storage: impl Into<Cow<'a, str>>) -> Self {
89 Self {
90 storage: storage.into(),
91 marker: PhantomData,
92 }
93 }
94
95 pub fn as_str(&self) -> &str {
97 &self.storage
98 }
99
100 pub fn borrowed(&self) -> NumericFormat<'_, T> {
102 NumericFormat {
103 storage: Cow::Borrowed(self.as_str()),
104 marker: PhantomData,
105 }
106 }
107
108 pub fn into_owned(self) -> NumericFormat<'static, T> {
110 NumericFormat {
111 storage: Cow::Owned(self.storage.into_owned()),
112 marker: PhantomData,
113 }
114 }
115
116 pub fn into_inner(self) -> Cow<'a, str> {
118 self.storage
119 }
120}
121
122impl<T> AsRef<str> for NumericFormat<'_, T>
123where
124 T: DataTypeKind,
125{
126 fn as_ref(&self) -> &str {
127 self.as_str()
128 }
129}
130
131impl<'a, T> TryFrom<&'a str> for NumericFormat<'a, T>
132where
133 T: DataTypeKind,
134{
135 type Error = NumericFormatError;
136
137 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
138 Self::new(value)
139 }
140}
141
142impl<T> TryFrom<String> for NumericFormat<'static, T>
143where
144 T: DataTypeKind,
145{
146 type Error = NumericFormatError;
147
148 fn try_from(value: String) -> Result<Self, Self::Error> {
149 Self::new(value)
150 }
151}
152
153impl<T> FromStr for NumericFormat<'static, T>
154where
155 T: DataTypeKind,
156{
157 type Err = NumericFormatError;
158
159 fn from_str(value: &str) -> Result<Self, Self::Err> {
160 Self::new(value.to_owned())
161 }
162}
163
164#[derive(Clone, Debug, Eq, Error, PartialEq)]
166#[non_exhaustive]
167pub enum NumericFormatError {
168 #[error("numeric format is {length} UTF-8 bytes; at most {maximum} bytes are supported")]
170 FormatTooLong {
171 byte_offset: usize,
172 length: usize,
173 maximum: usize,
174 },
175 #[error("numeric format contains a NUL byte at byte {byte_offset}")]
177 InteriorNul { byte_offset: usize },
178 #[error("numeric format has an incomplete directive at byte {byte_offset}")]
180 UnterminatedDirective { byte_offset: usize },
181 #[error(
183 "numeric format requests more than Dear ImGui's one value argument at byte {byte_offset}"
184 )]
185 MultipleConversions { byte_offset: usize },
186 #[error("numeric format requests a dynamic variadic argument at byte {byte_offset}")]
188 DynamicArgument { byte_offset: usize },
189 #[error("numeric format uses an unsupported positional argument at byte {byte_offset}")]
191 PositionalArgument { byte_offset: usize },
192 #[error("numeric format uses unsupported flag `{flag}` at byte {byte_offset}")]
194 UnsupportedFlag { byte_offset: usize, flag: char },
195 #[error(
197 "numeric format flag `{flag}` is incompatible with `%{conversion}` at byte {byte_offset}"
198 )]
199 IncompatibleFlag {
200 byte_offset: usize,
201 flag: char,
202 conversion: char,
203 },
204 #[error(
206 "numeric format width {width} at byte {byte_offset} exceeds the supported maximum {maximum}"
207 )]
208 WidthTooLarge {
209 byte_offset: usize,
210 width: u32,
211 maximum: u32,
212 },
213 #[error(
215 "numeric format precision {precision} at byte {byte_offset} exceeds the supported maximum {maximum}"
216 )]
217 PrecisionTooLarge {
218 byte_offset: usize,
219 precision: u32,
220 maximum: u32,
221 },
222 #[error(
224 "numeric format uses a length modifier that does not match its value type at byte {byte_offset}"
225 )]
226 UnsupportedLength { byte_offset: usize },
227 #[error("numeric format uses unsupported conversion `%{conversion}` at byte {byte_offset}")]
229 UnsupportedConversion {
230 byte_offset: usize,
231 conversion: char,
232 },
233 #[error("numeric format conversion does not match its value type at byte {byte_offset}")]
235 TypeMismatch { byte_offset: usize },
236 #[error(
238 "numeric format directive at byte {byte_offset} requires {length} bytes on a supported target; Dear ImGui supports at most {maximum}",
239 maximum = MAX_DIRECTIVE_BYTES
240 )]
241 DirectiveTooLong { byte_offset: usize, length: usize },
242}
243
244impl NumericFormatError {
245 pub fn byte_offset(&self) -> usize {
247 match *self {
248 Self::FormatTooLong { byte_offset, .. }
249 | Self::InteriorNul { byte_offset }
250 | Self::UnterminatedDirective { byte_offset }
251 | Self::MultipleConversions { byte_offset }
252 | Self::DynamicArgument { byte_offset }
253 | Self::PositionalArgument { byte_offset }
254 | Self::UnsupportedFlag { byte_offset, .. }
255 | Self::IncompatibleFlag { byte_offset, .. }
256 | Self::WidthTooLarge { byte_offset, .. }
257 | Self::PrecisionTooLarge { byte_offset, .. }
258 | Self::UnsupportedLength { byte_offset }
259 | Self::UnsupportedConversion { byte_offset, .. }
260 | Self::TypeMismatch { byte_offset }
261 | Self::DirectiveTooLong { byte_offset, .. } => byte_offset,
262 }
263 }
264}
265
266fn validate_numeric_format<T>(format: &str) -> Result<(), NumericFormatError>
267where
268 T: DataTypeKind,
269{
270 validate_numeric_format_for_data_type(format, T::KIND)
271}
272
273fn validate_format_length(format: &str) -> Result<(), NumericFormatError> {
274 if format.len() > MAX_FORMAT_BYTES {
275 Err(NumericFormatError::FormatTooLong {
276 byte_offset: MAX_FORMAT_BYTES,
277 length: format.len(),
278 maximum: MAX_FORMAT_BYTES,
279 })
280 } else {
281 Ok(())
282 }
283}
284
285fn normalize_wide_integer_length<'a, T>(storage: Cow<'a, str>) -> Cow<'a, str>
286where
287 T: DataTypeKind,
288{
289 if !matches!(T::KIND, DataType::I64 | DataType::U64) {
290 return storage;
291 }
292
293 let Some(length_range) = find_wide_length_modifier(&storage) else {
294 return storage;
295 };
296 let current = &storage[length_range.clone()];
297 let target = if cfg!(target_env = "msvc") {
298 "I64"
299 } else {
300 "ll"
301 };
302 if current == target {
303 return storage;
304 }
305
306 let mut normalized = storage.into_owned();
307 normalized.replace_range(length_range, target);
308 Cow::Owned(normalized)
309}
310
311fn find_wide_length_modifier(format: &str) -> Option<Range<usize>> {
312 let bytes = format.as_bytes();
313 let mut byte_offset = 0;
314
315 while byte_offset < bytes.len() {
316 if bytes[byte_offset] != b'%' {
317 byte_offset += 1;
318 continue;
319 }
320 byte_offset += 1;
321 if byte_offset < bytes.len() && bytes[byte_offset] == b'%' {
322 byte_offset += 1;
323 continue;
324 }
325
326 while byte_offset < bytes.len()
327 && matches!(bytes[byte_offset], b'-' | b'+' | b' ' | b'#' | b'0')
328 {
329 byte_offset += 1;
330 }
331 if byte_offset < bytes.len() && bytes[byte_offset] == b'*' {
332 byte_offset += 1;
333 } else {
334 while byte_offset < bytes.len() && bytes[byte_offset].is_ascii_digit() {
335 byte_offset += 1;
336 }
337 }
338 if byte_offset < bytes.len() && bytes[byte_offset] == b'.' {
339 byte_offset += 1;
340 if byte_offset < bytes.len() && bytes[byte_offset] == b'*' {
341 byte_offset += 1;
342 } else {
343 while byte_offset < bytes.len() && bytes[byte_offset].is_ascii_digit() {
344 byte_offset += 1;
345 }
346 }
347 }
348
349 if bytes[byte_offset..].starts_with(b"ll") {
350 return Some(byte_offset..byte_offset + 2);
351 }
352 if bytes[byte_offset..].starts_with(b"I64") {
353 return Some(byte_offset..byte_offset + 3);
354 }
355 return None;
356 }
357
358 None
359}
360
361#[derive(Copy, Clone, Debug, Eq, PartialEq)]
362enum ArgumentKind {
363 SignedInteger,
364 UnsignedInteger,
365 SignedWideInteger,
366 UnsignedWideInteger,
367 Double,
368}
369
370impl ArgumentKind {
371 fn from_data_type(data_type: DataType) -> Self {
372 match data_type {
373 DataType::I8 | DataType::I16 | DataType::I32 => Self::SignedInteger,
374 DataType::U8 | DataType::U16 | DataType::U32 => Self::UnsignedInteger,
375 DataType::I64 => Self::SignedWideInteger,
376 DataType::U64 => Self::UnsignedWideInteger,
377 DataType::F32 | DataType::F64 => Self::Double,
378 }
379 }
380
381 fn accepts_conversion(self, conversion: u8) -> bool {
382 match self {
383 Self::SignedInteger | Self::SignedWideInteger => matches!(conversion, b'd' | b'i'),
384 Self::UnsignedInteger | Self::UnsignedWideInteger => {
385 matches!(conversion, b'u' | b'o' | b'x' | b'X')
386 }
387 Self::Double => matches!(conversion, b'e' | b'E' | b'f' | b'F' | b'g' | b'G'),
388 }
389 }
390
391 fn accepts_length(self, length: LengthModifier) -> bool {
392 match self {
393 Self::SignedInteger | Self::UnsignedInteger => length == LengthModifier::None,
394 Self::SignedWideInteger | Self::UnsignedWideInteger => {
395 length == LengthModifier::LongLong
396 || cfg!(target_env = "msvc") && length == LengthModifier::MsvcI64
397 }
398 Self::Double => matches!(length, LengthModifier::None | LengthModifier::Long),
399 }
400 }
401}
402
403#[derive(Copy, Clone, Debug, Eq, PartialEq)]
404enum LengthModifier {
405 None,
406 Char,
407 Short,
408 Long,
409 LongLong,
410 IntMax,
411 Size,
412 PtrDiff,
413 LongDouble,
414 MsvcI32,
415 MsvcI64,
416}
417
418#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
419struct DirectiveFlags {
420 alternate: Option<usize>,
421 explicit_sign: Option<usize>,
422 leading_space: Option<usize>,
423}
424
425impl DirectiveFlags {
426 fn record(&mut self, flag: u8, byte_offset: usize) {
427 match flag {
428 b'#' => {
429 let _ = self.alternate.get_or_insert(byte_offset);
430 }
431 b'+' => {
432 let _ = self.explicit_sign.get_or_insert(byte_offset);
433 }
434 b' ' => {
435 let _ = self.leading_space.get_or_insert(byte_offset);
436 }
437 b'-' | b'0' => {}
438 _ => {}
439 }
440 }
441
442 fn first_incompatible(self, conversion: u8) -> Option<(usize, u8)> {
443 let mut incompatible = [None; 3];
444 if matches!(conversion, b'd' | b'i' | b'u') {
445 incompatible[0] = self.alternate.map(|offset| (offset, b'#'));
446 }
447 if matches!(conversion, b'u' | b'o' | b'x' | b'X') {
448 incompatible[1] = self.explicit_sign.map(|offset| (offset, b'+'));
449 incompatible[2] = self.leading_space.map(|offset| (offset, b' '));
450 }
451 incompatible
452 .into_iter()
453 .flatten()
454 .min_by_key(|(offset, _)| *offset)
455 }
456}
457
458#[derive(Copy, Clone, Debug, Eq, PartialEq)]
459enum DecimalComponent {
460 Width,
461 Precision,
462}
463
464impl DecimalComponent {
465 fn maximum(self) -> u32 {
466 match self {
467 Self::Width => MAX_FIELD_WIDTH,
468 Self::Precision => MAX_PRECISION,
469 }
470 }
471
472 fn too_large(self, byte_offset: usize, value: u32) -> NumericFormatError {
473 match self {
474 Self::Width => NumericFormatError::WidthTooLarge {
475 byte_offset,
476 width: value,
477 maximum: self.maximum(),
478 },
479 Self::Precision => NumericFormatError::PrecisionTooLarge {
480 byte_offset,
481 precision: value,
482 maximum: self.maximum(),
483 },
484 }
485 }
486}
487
488fn validate_numeric_format_for_data_type(
489 format: &str,
490 data_type: DataType,
491) -> Result<(), NumericFormatError> {
492 validate_format_length(format)?;
493 let bytes = format.as_bytes();
494 if let Some(byte_offset) = bytes.iter().position(|byte| *byte == 0) {
495 return Err(NumericFormatError::InteriorNul { byte_offset });
496 }
497
498 let argument_kind = ArgumentKind::from_data_type(data_type);
499 let mut byte_offset = 0;
500 let mut value_conversions = 0;
501
502 while byte_offset < bytes.len() {
503 if bytes[byte_offset] != b'%' {
504 byte_offset += 1;
505 continue;
506 }
507
508 let conversion_start = byte_offset;
509 byte_offset += 1;
510 if byte_offset == bytes.len() {
511 return Err(NumericFormatError::UnterminatedDirective {
512 byte_offset: conversion_start,
513 });
514 }
515 if bytes[byte_offset] == b'%' {
516 byte_offset += 1;
517 continue;
518 }
519
520 value_conversions += 1;
521 if value_conversions > 1 {
522 return Err(NumericFormatError::MultipleConversions {
523 byte_offset: conversion_start,
524 });
525 }
526
527 let mut flags = DirectiveFlags::default();
528 while byte_offset < bytes.len()
529 && matches!(bytes[byte_offset], b'-' | b'+' | b' ' | b'#' | b'0')
530 {
531 flags.record(bytes[byte_offset], byte_offset);
532 byte_offset += 1;
533 }
534 reject_unsupported_flag(bytes, byte_offset)?;
535
536 if byte_offset < bytes.len() && bytes[byte_offset] == b'*' {
537 return Err(NumericFormatError::DynamicArgument { byte_offset });
538 }
539 parse_bounded_decimal(bytes, &mut byte_offset, DecimalComponent::Width)?;
540 reject_positional_argument(bytes, byte_offset)?;
541
542 if byte_offset < bytes.len() && bytes[byte_offset] == b'.' {
543 byte_offset += 1;
544 if byte_offset < bytes.len() && bytes[byte_offset] == b'*' {
545 return Err(NumericFormatError::DynamicArgument { byte_offset });
546 }
547 parse_bounded_decimal(bytes, &mut byte_offset, DecimalComponent::Precision)?;
548 reject_positional_argument(bytes, byte_offset)?;
549 }
550
551 let length_offset = byte_offset;
552 let (length, next_offset) = parse_length_modifier(bytes, byte_offset);
553 byte_offset = next_offset;
554 if byte_offset == bytes.len() {
555 return Err(NumericFormatError::UnterminatedDirective {
556 byte_offset: conversion_start,
557 });
558 }
559 if length == LengthModifier::LongLong {
560 let portable_format_length = format.len().saturating_add(1);
561 if portable_format_length > MAX_FORMAT_BYTES {
562 return Err(NumericFormatError::FormatTooLong {
563 byte_offset: MAX_FORMAT_BYTES,
564 length: portable_format_length,
565 maximum: MAX_FORMAT_BYTES,
566 });
567 }
568 }
569
570 let conversion = bytes[byte_offset];
571 byte_offset += 1;
572 let directive_length = byte_offset - conversion_start;
573 let portable_directive_length = if length == LengthModifier::LongLong {
574 directive_length.saturating_add(1)
575 } else {
576 directive_length
577 };
578 if portable_directive_length > MAX_DIRECTIVE_BYTES {
579 return Err(NumericFormatError::DirectiveTooLong {
580 byte_offset: conversion_start,
581 length: portable_directive_length,
582 });
583 }
584 if !is_supported_numeric_conversion(conversion) {
585 return Err(NumericFormatError::UnsupportedConversion {
586 byte_offset: conversion_start,
587 conversion: char::from(conversion),
588 });
589 }
590 if !argument_kind.accepts_conversion(conversion) {
591 return Err(NumericFormatError::TypeMismatch {
592 byte_offset: conversion_start,
593 });
594 }
595 if !argument_kind.accepts_length(length) {
596 return Err(NumericFormatError::UnsupportedLength {
597 byte_offset: length_offset,
598 });
599 }
600 if let Some((flag_offset, flag)) = flags.first_incompatible(conversion) {
601 return Err(NumericFormatError::IncompatibleFlag {
602 byte_offset: flag_offset,
603 flag: char::from(flag),
604 conversion: char::from(conversion),
605 });
606 }
607 }
608
609 Ok(())
610}
611
612fn parse_bounded_decimal(
613 bytes: &[u8],
614 byte_offset: &mut usize,
615 component: DecimalComponent,
616) -> Result<(), NumericFormatError> {
617 let start = *byte_offset;
618 let mut value = 0_u32;
619
620 while *byte_offset < bytes.len() && bytes[*byte_offset].is_ascii_digit() {
621 let digit = u32::from(bytes[*byte_offset] - b'0');
622 value = value
623 .checked_mul(10)
624 .and_then(|current| current.checked_add(digit))
625 .ok_or_else(|| component.too_large(start, u32::MAX))?;
626 if value > component.maximum() {
627 return Err(component.too_large(start, value));
628 }
629 *byte_offset += 1;
630 }
631
632 Ok(())
633}
634
635fn reject_unsupported_flag(bytes: &[u8], byte_offset: usize) -> Result<(), NumericFormatError> {
636 if byte_offset < bytes.len() && matches!(bytes[byte_offset], b'\'' | b'_') {
637 Err(NumericFormatError::UnsupportedFlag {
638 byte_offset,
639 flag: char::from(bytes[byte_offset]),
640 })
641 } else {
642 Ok(())
643 }
644}
645
646fn reject_positional_argument(bytes: &[u8], byte_offset: usize) -> Result<(), NumericFormatError> {
647 if byte_offset < bytes.len() && bytes[byte_offset] == b'$' {
648 Err(NumericFormatError::PositionalArgument { byte_offset })
649 } else {
650 Ok(())
651 }
652}
653
654fn parse_length_modifier(bytes: &[u8], byte_offset: usize) -> (LengthModifier, usize) {
655 let remaining = &bytes[byte_offset..];
656 if remaining.starts_with(b"hh") {
657 (LengthModifier::Char, byte_offset + 2)
658 } else if remaining.starts_with(b"ll") {
659 (LengthModifier::LongLong, byte_offset + 2)
660 } else if remaining.starts_with(b"I32") {
661 (LengthModifier::MsvcI32, byte_offset + 3)
662 } else if remaining.starts_with(b"I64") {
663 (LengthModifier::MsvcI64, byte_offset + 3)
664 } else if let Some(first) = remaining.first() {
665 let length = match first {
666 b'h' => LengthModifier::Short,
667 b'l' => LengthModifier::Long,
668 b'j' => LengthModifier::IntMax,
669 b'z' => LengthModifier::Size,
670 b't' => LengthModifier::PtrDiff,
671 b'L' => LengthModifier::LongDouble,
672 _ => return (LengthModifier::None, byte_offset),
673 };
674 (length, byte_offset + 1)
675 } else {
676 (LengthModifier::None, byte_offset)
677 }
678}
679
680fn is_supported_numeric_conversion(conversion: u8) -> bool {
681 matches!(
682 conversion,
683 b'd' | b'i' | b'u' | b'o' | b'x' | b'X' | b'e' | b'E' | b'f' | b'F' | b'g' | b'G'
684 )
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690
691 #[test]
692 fn typed_formats_retain_borrowed_and_owned_storage() {
693 let borrowed = NumericFormat::<f32>::new("%.3f").unwrap();
694 assert_eq!(borrowed.as_str(), "%.3f");
695 assert!(matches!(borrowed.clone().into_inner(), Cow::Borrowed(_)));
696
697 let owned = NumericFormat::<u32>::new(String::from("0x%08X")).unwrap();
698 assert_eq!(owned.as_str(), "0x%08X");
699 assert!(matches!(owned.clone().into_inner(), Cow::Owned(_)));
700 assert_eq!(owned.into_inner().as_ref(), "0x%08X");
701 }
702
703 #[test]
704 fn formats_can_be_reborrowed_or_made_owned_without_revalidation() {
705 let text = String::from("%.2f");
706 let borrowed = NumericFormat::<f32>::new(text.as_str()).unwrap();
707 assert_eq!(borrowed.borrowed().as_str(), "%.2f");
708
709 let owned = borrowed.into_owned();
710 drop(text);
711 assert_eq!(owned.as_str(), "%.2f");
712 }
713
714 #[test]
715 fn floating_formats_allow_one_conversion_and_literal_percent_signs() {
716 for format in [
717 "%.3f",
718 "%+08.2e ms",
719 "%.0f%%",
720 "literal %% only",
721 "plain text",
722 "temperature: %.2f °C",
723 ] {
724 assert_eq!(validate_numeric_format::<f32>(format), Ok(()), "{format}");
725 assert_eq!(validate_numeric_format::<f64>(format), Ok(()), "{format}");
726 }
727 assert_eq!(validate_numeric_format::<f64>("%lf"), Ok(()));
728 }
729
730 #[test]
731 fn integer_formats_match_signedness_and_carrier_width() {
732 for format in ["%d", "%08i", "value %d", "%+d", "% d"] {
733 assert_eq!(validate_numeric_format::<i32>(format), Ok(()), "{format}");
734 }
735 for format in ["%u", "%08X", "0%o", "%#x", "%#o"] {
736 assert_eq!(validate_numeric_format::<u32>(format), Ok(()), "{format}");
737 }
738 let signed = NumericFormat::<i64>::new("%lld").unwrap();
739 let unsigned = NumericFormat::<u64>::new("0x%016I64X").unwrap();
740 if cfg!(target_env = "msvc") {
741 assert_eq!(signed.as_str(), "%I64d");
742 assert_eq!(unsigned.as_str(), "0x%016I64X");
743 } else {
744 assert_eq!(signed.as_str(), "%lld");
745 assert_eq!(unsigned.as_str(), "0x%016llX");
746 }
747 }
748
749 #[test]
750 fn formats_cannot_consume_missing_or_mistyped_arguments() {
751 for format in [
752 "%s", "%n", "%p", "%c", "%a", "%*f", "%.*f", "%2$f", "%f %f", "%Lf", "%zu", "%",
753 ] {
754 assert!(validate_numeric_format::<f32>(format).is_err(), "{format}");
755 }
756
757 assert!(validate_numeric_format::<i32>("%lld").is_err());
758 assert!(validate_numeric_format::<i64>("%d").is_err());
759 assert!(validate_numeric_format::<i32>("%u").is_err());
760 assert!(validate_numeric_format::<u32>("%d").is_err());
761 assert!(validate_numeric_format::<f64>("%d").is_err());
762 }
763
764 #[test]
765 fn directives_must_fit_dear_imguis_sanitization_buffer() {
766 let accepted = format!("%{}f", "0".repeat(28));
767 assert_eq!(accepted.len(), 30);
768 assert_eq!(validate_numeric_format::<f64>(&accepted), Ok(()));
769
770 let rejected = format!("%{}f", "0".repeat(29));
771 assert_eq!(rejected.len(), 31);
772 assert!(matches!(
773 validate_numeric_format::<f64>(&rejected),
774 Err(NumericFormatError::DirectiveTooLong { length: 31, .. })
775 ));
776 }
777
778 #[test]
779 fn width_and_precision_are_parsed_with_bounded_arithmetic() {
780 for format in ["%31d", "%031d"] {
781 assert_eq!(validate_numeric_format::<i32>(format), Ok(()), "{format}");
782 }
783 for format in ["%.99f", "%.099f"] {
784 assert_eq!(validate_numeric_format::<f64>(format), Ok(()), "{format}");
785 }
786
787 assert!(matches!(
788 validate_numeric_format::<i32>("%32d"),
789 Err(NumericFormatError::WidthTooLarge {
790 width: 32,
791 maximum: 31,
792 ..
793 })
794 ));
795 assert!(matches!(
796 validate_numeric_format::<f64>("%.100f"),
797 Err(NumericFormatError::PrecisionTooLarge {
798 precision: 100,
799 maximum: 99,
800 ..
801 })
802 ));
803 assert!(matches!(
804 validate_numeric_format::<i32>("%999999999999999999999999999999d"),
805 Err(NumericFormatError::WidthTooLarge { .. })
806 ));
807 }
808
809 #[test]
810 fn flags_must_have_defined_semantics_for_the_conversion() {
811 for format in ["%#d", "%#i"] {
812 assert!(matches!(
813 validate_numeric_format::<i32>(format),
814 Err(NumericFormatError::IncompatibleFlag { flag: '#', .. })
815 ));
816 }
817 for format in ["%#u", "%+u", "% u", "%+o", "% x", "%+X"] {
818 assert!(
819 matches!(
820 validate_numeric_format::<u32>(format),
821 Err(NumericFormatError::IncompatibleFlag { .. })
822 ),
823 "{format}"
824 );
825 }
826 }
827
828 #[test]
829 fn complete_formats_have_a_utf8_byte_limit() {
830 let accepted = format!("{}x", "界".repeat(1365));
831 assert_eq!(accepted.len(), MAX_FORMAT_BYTES);
832 assert_eq!(validate_numeric_format::<f64>(&accepted), Ok(()));
833
834 let rejected = format!("{accepted}x");
835 assert!(matches!(
836 validate_numeric_format::<f64>(&rejected),
837 Err(NumericFormatError::FormatTooLong {
838 byte_offset: MAX_FORMAT_BYTES,
839 length: 4097,
840 maximum: MAX_FORMAT_BYTES,
841 })
842 ));
843
844 let portable_wide = format!("{}%I64d", "x".repeat(4091));
845 assert_eq!(portable_wide.len(), MAX_FORMAT_BYTES);
846 assert!(NumericFormat::<i64>::new(portable_wide).is_ok());
847
848 let target_expansion = format!("{}%lld", "x".repeat(4092));
849 assert_eq!(target_expansion.len(), MAX_FORMAT_BYTES);
850 assert!(matches!(
851 NumericFormat::<i64>::new(target_expansion),
852 Err(NumericFormatError::FormatTooLong {
853 length: 4097,
854 maximum: MAX_FORMAT_BYTES,
855 ..
856 })
857 ));
858 }
859
860 #[test]
861 fn validation_reports_the_first_unsafe_construct() {
862 let error = validate_numeric_format::<f64>("value: %f then %n").unwrap_err();
863 assert_eq!(error.byte_offset(), 15);
864 assert!(matches!(
865 error,
866 NumericFormatError::MultipleConversions { .. }
867 ));
868
869 let error = validate_numeric_format::<f64>("\0%f").unwrap_err();
870 assert_eq!(error.byte_offset(), 0);
871 assert!(matches!(error, NumericFormatError::InteriorNul { .. }));
872 }
873}