/*
* Hurl (https://hurl.dev)
* Copyright (C) 2026 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use std::cmp::PartialEq;
use std::{fmt, io};
/// A fast zero dependency JSON formatter / pretty printer.
/// This is a fast JSON formatter (up to x2 compared to pretty printing with [Serde JSON](https://github.com/serde-rs/json)).
/// This formatter parses and formats JSON input byte by byte and do not require pre UTF-8 validation.
/// UTF-8 validation is done in-place, on the fly, while parsing strings. This implementation try to not allocate
/// anything. It does not try to normalise, remove unnecessary escaping, it just formats the actual input
/// with spaces, newlines and (optionally) color.
///
/// This formatter supports writing to a [`io::Write`] instance (file, standard output), or to a [`fmt::Write`]
/// buffer (string etc...). If the formatting fails, the write buffer may contain some unwanted data
/// from the already read bytes. It's up to the caller to deal with this kind of failure (clear a buffer
/// for instance etc...).
pub struct Formatter<'input> {
/// The JSON input bytes to prettify.
input: &'input [u8],
/// Cursor position in byte offset (starting at 0)
pos: BytePos,
/// Current indentation level (this is maxed by [`MAX_INDENT_LEVEL`])
level: usize,
/// Use color with ANSI escape code when prettifying.
color: Color,
}
/// The maximum indentation level supported before errors.
const MAX_INDENT_LEVEL: usize = 100;
/// A byte position in a bytes stream (0-based index).
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct BytePos(usize);
/// Potential errors raised during formatting.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum FormatError {
/// Unexpected end of file.
Eof,
/// Invalid byte at this position.
InvalidByte(u8, BytePos),
/// The next bytes are not a valid UTF-8 sequence.
InvalidUtf8([u8; 4], usize, BytePos),
/// Invalid escaped byte at this position.
InvalidEscape(u8, BytePos),
/// The maximum indent level has been reached.
MaxIndentLevel(usize, BytePos),
/// Write error occuring when formatting to an [`io::Write`] (file, standard output etc...)
Io(io::ErrorKind),
/// Write error occuring when formatting to an [`fmt::Write`] (string, etc...)
Fmt(fmt::Error),
}
impl fmt::Display for FormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn debug_str_u8(b: u8) -> String {
match char::from(b) {
c if c.is_ascii_graphic() || c == ' ' => {
format!("0x{b:02x?}/'{c}'")
}
_ => format!("0x{b:02x?}"),
}
}
match self {
FormatError::Eof => write!(f, "unexpected end of file"),
FormatError::InvalidByte(byte, pos) => {
let byte = debug_str_u8(*byte);
write!(f, "invalid byte {byte} at offset {}", pos.0)
}
FormatError::InvalidUtf8(bytes, len, pos) => {
let hex = bytes
.iter()
.take(*len)
.map(|b| format!("0x{:02x}", b))
.collect::<Vec<_>>()
.join(" ");
write!(f, "invalid UTF-8 {} bytes {hex} at offset {}", len, pos.0)
}
FormatError::InvalidEscape(byte, pos) => {
let byte = debug_str_u8(*byte);
write!(f, "invalid escaped byte {byte} at offset {}", pos.0)
}
FormatError::MaxIndentLevel(level, pos) => {
write!(f, "maximum indent level {} at offset {}", level, pos.0)
}
FormatError::Io(error) => write!(f, "error writing {error}"),
FormatError::Fmt(error) => write!(f, "error writing {error}"),
}
}
}
impl From<io::Error> for FormatError {
fn from(e: io::Error) -> Self {
FormatError::Io(e.kind())
}
}
impl From<fmt::Error> for FormatError {
fn from(e: fmt::Error) -> Self {
FormatError::Fmt(e)
}
}
/// Whether we prettify JSON with [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code)
/// or not.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Color {
NoColor,
Ansi,
}
/// Is the current string token being processed semantically a "key" or a "value". The color
/// used for prettifying depends on it.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum StringMode {
Key,
Value,
}
type FormatResult<T> = Result<T, FormatError>;
/// Helpers method to prettify to an [`io::Write`] (File, standard output etc...)
pub fn format(input: &[u8], color: Color, out: &mut impl io::Write) -> FormatResult<()> {
let mut formatter = Formatter::new(input, color);
formatter.format(out)
}
impl<'input> Formatter<'input> {
/// Creates a new formater, with JSON `input` bytes to format and colorize.
pub fn new(input: &'input [u8], color: Color) -> Self {
Formatter {
input,
pos: BytePos(0),
level: 0,
color,
}
}
/// Reads the next byte and advances the read position.
#[inline]
fn next_byte(&mut self) -> Option<u8> {
let b = self.peek_byte()?;
self.pos.0 += 1;
Some(b)
}
/// Peeks the next byte without advancing the read position.
#[inline]
fn peek_byte(&mut self) -> Option<u8> {
self.input.get(self.pos.0).copied()
}
/// Reads the next byte, advances the read position and check the read value.
#[inline]
fn expect_byte(&mut self, expected: u8) -> FormatResult<()> {
match self.next_byte() {
Some(b) if b == expected => Ok(()),
Some(b) => Err(FormatError::InvalidByte(b, BytePos(self.pos.0 - 1))),
None => Err(FormatError::Eof),
}
}
/// Increments the indentation level.
fn inc_level(&mut self) -> FormatResult<()> {
if self.level >= MAX_INDENT_LEVEL {
return Err(FormatError::MaxIndentLevel(self.level, self.pos));
}
self.level += 1;
Ok(())
}
/// Decrements the indentation level.
fn dec_level(&mut self) {
self.level -= 1;
}
/// Formats and colorize the JSON input bytes.
pub fn format(&mut self, out: &mut impl io::Write) -> FormatResult<()> {
self.skip_start_bom();
self.skip_whitespace();
self.parse_value(out)?;
self.skip_whitespace();
// End the prettified output with a trailing newline.
self.write_ln(out)?;
// Have we completely consumed our payload?
if let Some(b) = self.peek_byte() {
Err(FormatError::InvalidByte(b, self.pos))
} else {
Ok(())
}
}
/// Skips BOM (Byte Order Mark) at the start of the read buffer.
fn skip_start_bom(&mut self) {
debug_assert!(self.pos.0 == 0);
if self.input.len() < 3 {
return;
}
if self.input[0] == 0xEF && self.input[1] == 0xBB && self.input[2] == 0xBF {
self.pos.0 = 3;
}
}
fn skip_whitespace(&mut self) {
while matches!(self.peek_byte(), Some(b' ' | b'\n' | b'\r' | b'\t')) {
self.pos.0 += 1;
}
}
/// Processes a JSON value.
fn parse_value(&mut self, out: &mut impl io::Write) -> FormatResult<()> {
// From <https://datatracker.ietf.org/doc/html/rfc7159#section-3>:
//
// value = false / null / true / object / array / number / string
// false = %x66.61.6c.73.65 ; false
// null = %x6e.75.6c.6c ; null
// true = %x74.72.75.65 ; true
match self.peek_byte() {
Some(b'"') => self.parse_string(out, StringMode::Value),
Some(b'-' | b'0'..=b'9') => self.parse_number(out),
Some(b'{') => self.parse_object(out),
Some(b'[') => self.parse_array(out),
Some(b't') => self.parse_true(out),
Some(b'f') => self.parse_false(out),
Some(b'n') => self.parse_null(out),
Some(b) => Err(FormatError::InvalidByte(b, self.pos)),
None => Err(FormatError::Eof),
}
}
/// Processes a JSON object.
fn parse_object(&mut self, out: &mut impl io::Write) -> FormatResult<()> {
// From <https://datatracker.ietf.org/doc/html/rfc7159#section-4>:
// object = begin-object [ member *( value-separator member ) ]
// end-object
// member = string name-separator value
self.expect_byte(b'{')?;
// For empty objects, we keep a short compact form:
self.skip_whitespace();
if self.peek_byte() == Some(b'}') {
self.next_byte();
self.write_empty_obj(out)?;
return Ok(());
}
// Now, we have a non-empty object.
self.write_begin_obj(out)?;
self.inc_level()?;
let mut first = true;
loop {
self.skip_whitespace();
if self.peek_byte() == Some(b'}') {
self.next_byte();
self.dec_level();
self.write_ln(out)?;
self.write_indent(out)?;
self.write_end_obj(out)?;
return Ok(());
}
if first {
first = false;
} else {
self.expect_byte(b',')?;
self.skip_whitespace();
self.write_value_sep(out)?;
}
// Parse key
self.write_indent(out)?;
self.parse_string(out, StringMode::Key)?;
// Parse colon
self.skip_whitespace();
self.expect_byte(b':')?;
self.write_name_sep(out)?;
// Parse value
self.skip_whitespace();
self.parse_value(out)?;
}
}
/// Processes a JSON array.
fn parse_array(&mut self, out: &mut impl io::Write) -> FormatResult<()> {
// From <https://datatracker.ietf.org/doc/html/rfc7159#section-4>:
// array = begin-array [ value *( value-separator value ) ] end-array
self.expect_byte(b'[')?;
// For empty arrays, we keep a short compact form:
self.skip_whitespace();
if self.peek_byte() == Some(b']') {
self.next_byte();
self.write_empty_arr(out)?;
return Ok(());
}
// Now, we have a non-empty array.
self.write_begin_arr(out)?;
self.inc_level()?;
let mut first = true;
loop {
self.skip_whitespace();
if self.peek_byte() == Some(b']') {
self.next_byte();
self.dec_level();
self.write_ln(out)?;
self.write_indent(out)?;
self.write_end_arr(out)?;
return Ok(());
}
if first {
first = false;
} else {
self.expect_byte(b',')?;
self.skip_whitespace();
self.write_value_sep(out)?;
}
self.write_indent(out)?;
self.parse_value(out)?;
}
}
/// Processes a JSON string. The string is not normalized, escapes are preserved and the string
/// bytes are validated on-the-fly to be UTF-8 valid.
fn parse_string(&mut self, out: &mut impl io::Write, mode: StringMode) -> FormatResult<()> {
// From <https://datatracker.ietf.org/doc/html/rfc7159#section-8>
let start = self.pos;
self.expect_byte(b'"')?;
while let Some(b) = self.peek_byte() {
match b {
b'"' => {
self.next_byte();
// Flush plain segment before exit.
let string = &self.input[start.0..self.pos.0];
match mode {
StringMode::Key => self.write_key(string, out)?,
StringMode::Value => self.write_value(string, out)?,
};
return Ok(());
}
// Escaping
b'\\' => {
self.next_byte();
match self.next_byte() {
Some(b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't') => {}
Some(b'u') => {
for _ in 0..4 {
let hex = self.next_byte().ok_or(FormatError::Eof)?;
if !(hex as char).is_ascii_hexdigit() {
return Err(FormatError::InvalidByte(
hex,
BytePos(self.pos.0 - 1),
));
}
}
}
Some(b) => return Err(FormatError::InvalidEscape(b, self.pos)),
None => return Err(FormatError::Eof),
}
}
0x00..=0x1F => return Err(FormatError::InvalidByte(b, self.pos)),
_ => {
// Decode valid UTF-8 char
self.next_utf8_char()?;
}
}
}
Err(FormatError::Eof)
}
/// Processes a `true` literal.
fn parse_true(&mut self, out: &mut impl io::Write) -> FormatResult<()> {
for &b in b"true" {
self.expect_byte(b)?;
}
self.write_true(out)?;
Ok(())
}
/// Processes a `false` literal.
fn parse_false(&mut self, out: &mut impl io::Write) -> FormatResult<()> {
for &b in b"false" {
self.expect_byte(b)?;
}
self.write_false(out)?;
Ok(())
}
/// Processes a `null` literal.
fn parse_null(&mut self, out: &mut impl io::Write) -> FormatResult<()> {
for &b in b"null" {
self.expect_byte(b)?;
}
self.write_null(out)?;
Ok(())
}
/// Processes a JSON number.
fn parse_number(&mut self, out: &mut impl io::Write) -> FormatResult<()> {
// From the spec <https://datatracker.ietf.org/doc/html/rfc7159#section-6>:
//
// number = [ minus ] int [ frac ] [ exp ]
// decimal-point = %x2E ; .
// digit1-9 = %x31-39 ; 1-9
// e = %x65 / %x45 ; e E
// exp = e [ minus / plus ] 1*DIGIT
// frac = decimal-point 1*DIGIT
// int = zero / ( digit1-9 *DIGIT )
// minus = %x2D ; -
// plus = %x2B ; +
// zero = %x30 ; 0
let start = self.pos;
if self.peek_byte() == Some(b'-') {
self.next_byte();
}
self.parse_integer()?;
self.parse_fraction()?;
self.parse_exponent()?;
// Finally, write numbers
let digits = &self.input[start.0..self.pos.0];
self.write_number(digits, out)?;
Ok(())
}
/// Processes the integer part of a number.
fn parse_integer(&mut self) -> FormatResult<()> {
match self.peek_byte() {
Some(b'0') => {
self.next_byte();
Ok(())
}
Some(b'1'..=b'9') => {
self.next_byte();
// 0 or more digits
while let Some(b'0'..=b'9') = self.peek_byte() {
self.next_byte();
}
Ok(())
}
Some(b) => Err(FormatError::InvalidByte(b, self.pos)),
None => Err(FormatError::Eof),
}
}
/// Processes the fractional part of a number.
fn parse_fraction(&mut self) -> FormatResult<()> {
if self.peek_byte() == Some(b'.') {
self.next_byte();
// 1 or more digits
match self.peek_byte() {
Some(b'0'..=b'9') => {
self.next_byte();
while let Some(b'0'..=b'9') = self.peek_byte() {
self.next_byte();
}
Ok(())
}
Some(b) => Err(FormatError::InvalidByte(b, self.pos)),
None => Err(FormatError::Eof),
}?;
}
Ok(())
}
/// Processes the exponent part of a number.
fn parse_exponent(&mut self) -> FormatResult<()> {
match self.peek_byte() {
Some(b'e' | b'E') => {
self.next_byte();
if let Some(b'+' | b'-') = self.peek_byte() {
self.next_byte();
}
match self.peek_byte() {
Some(b'0'..=b'9') => {
self.next_byte();
while let Some(b'0'..=b'9') = self.peek_byte() {
self.next_byte();
}
Ok(())
}
Some(b) => Err(FormatError::InvalidByte(b, self.pos)),
None => Err(FormatError::Eof),
}
}
_ => Ok(()),
}
}
/// Read and advances to the next UTF-8 char (may advance 1 to 4 bytes).
/// The code check for UTF-8 validity, reference is from <https://en.wikipedia.org/wiki/UTF-8>
/// Bytes bounds values and logic are extracted from this [table](https://en.wikipedia.org/wiki/UTF-8#Byte_map):
///
/// | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
/// |---|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
/// | 0 | NUL | SOH | STX | ETX | EOT | ENQ | ACK | BEL | BS | HT | LF | VT | FF | CR | SO | SI |
/// | 1 | DLE | DC1 | DC2 | DC3 | DC4 | NAK | SYN | ETB | CAN | EM | SUB | ESC | FS | GS | RS | US |
/// | 2 | SP | ! | " | # | $ | % | & | ' | ( | ) | * | + | , | - | . | / |
/// | 3 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? |
/// | 4 | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O |
/// | 5 | P | Q | R | S | T | U | V | W | X | Y | Z | [ | \ | ] | ^ | _ |
/// | 6 | ` | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
/// | 7 | p | q | r | s | t | u | v | w | x | y | z | { | \| | } | ~ | DEL |
/// | 8 | con | con | con | con | con | con | con | con | con | con | con | con | con | con | con | con |
/// | 9 | con | con | con | con | con | con | con | con | con | con | con | con | con | con | con | con |
/// | A | con | con | con | con | con | con | con | con | con | con | con | con | con | con | con | con |
/// | B | con | con | con | con | con | con | con | con | con | con | con | con | con | con | con | con |
/// | C | ▒ | ▒ | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
/// | D | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
/// | E | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
/// | F | 4 | 4 | 4 | 4 | 4 | ▒ | ▒ | ▒ | ▒ | ▒ | ▒ | ▒ | ▒ | ▒ | ▒ | ▒ |
///
/// - con => Continuation byte
/// - 2 => First byte of 2-byte sequence
/// - 3 => First byte of 3-byte sequence
/// - 4 => First byte of 4-byte sequence
/// - ▒ => not used
fn next_utf8_char(&mut self) -> FormatResult<()> {
#[inline(always)]
fn cont(b: u8) -> bool {
(b & 0xC0) == 0x80
}
let start_pos = self.pos;
// Case 1: Single-byte ASCII character (0xxxxxxx).
let b1 = self.next_byte().ok_or(FormatError::Eof)?;
if b1 < 0x80 {
return Ok(());
}
// Case 2: Two-byte sequence (110xxxxx 10xxxxxx).
let b2 = self.next_byte().ok_or(FormatError::Eof)?;
if b1 < 0xE0 {
return if (0xC2..=0xDF).contains(&b1) && cont(b2) {
Ok(())
} else {
Err(FormatError::InvalidUtf8([b1, b2, 0, 0], 2, start_pos))
};
}
// Case 3: Three-byte sequence (1110xxxx 10xxxxxx 10xxxxxx).
let b3 = self.next_byte().ok_or(FormatError::Eof)?;
if b1 < 0xF0 {
return if match b1 {
// See <https://en.wikipedia.org/wiki/UTF-8#Error_handling>
// Overlong encodings:
// > An overlong encoding (0xE0 followed by less than 0xA0, ...)
0xE0 => (0xA0..=0xBF).contains(&b2) && cont(b3),
// Can't be UTF-16 surrogates:
// > A 3-byte sequence that decodes to a UTF-16 surrogate U+0xD800–0xDFFF (0xED followed by 0xA0 or greater)
0xED => (0x80..=0x9F).contains(&b2) && cont(b3),
// General case: 2 continuation bytes
0xE1..=0xEC | 0xEE..=0xEF => cont(b2) && cont(b3),
_ => false,
} {
Ok(())
} else {
Err(FormatError::InvalidUtf8([b1, b2, b3, 0], 3, self.pos))
};
}
// Case 4: Four-byte sequence (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx).
let b4 = self.next_byte().ok_or(FormatError::Eof)?;
if match b1 {
// See <https://en.wikipedia.org/wiki/UTF-8#Error_handling>
// Overlong encodings:
// > An overlong encoding (..., or 0xF0 followed by less than 0x90)
0xF0 => (0x90..=0xBF).contains(&b2) && cont(b3) && cont(b4),
// Limit to code point 0x10FFFF
// > A 4-byte sequence that decodes to a value greater than U+10FFFF (0xF4 followed by 0x90 or greater)
0xF4 => (0x80..=0x8F).contains(&b2) && cont(b3) && cont(b4),
// General case: 3 continuation bytes
0xF1..=0xF3 => cont(b2) && cont(b3) && cont(b4),
_ => false,
} {
Ok(())
} else {
Err(FormatError::InvalidUtf8([b1, b2, b3, b4], 4, self.pos))
}
}
}
const SPACES: &[u8] = b" ";
/// Methods to print on a [Write], with color, or not.
impl<'input> Formatter<'input> {
fn write_indent(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
let n = self.level * 2;
let full_chunks = n / SPACES.len();
let remainder = n % SPACES.len();
for _ in 0..full_chunks {
out.write_all(SPACES)?;
}
out.write_all(&SPACES[..remainder])?;
Ok(())
}
#[inline]
fn write_ln(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
out.write_all(b"\n")
}
#[inline]
fn write_empty_obj(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[1;39m{}\x1b[0m")
} else {
out.write_all(b"{}")
}
}
#[inline]
fn write_begin_obj(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[1;39m{\x1b[0m\n")
} else {
out.write_all(b"{\n")
}
}
#[inline]
fn write_end_obj(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[1;39m}\x1b[0m")
} else {
out.write_all(b"}")
}
}
#[inline]
fn write_value_sep(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[1;39m,\x1b[0m\n")
} else {
out.write_all(b",\n")
}
}
#[inline]
fn write_name_sep(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[1;39m:\x1b[0m ")
} else {
out.write_all(b": ")
}
}
#[inline]
fn write_empty_arr(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[1;39m[]\x1b[0m")
} else {
out.write_all(b"[]")
}
}
#[inline]
fn write_begin_arr(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[1;39m[\x1b[0m\n")
} else {
out.write_all(b"[\n")
}
}
#[inline]
fn write_end_arr(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[1;39m]\x1b[0m")
} else {
out.write_all(b"]")
}
}
#[inline]
fn write_key(&self, s: &[u8], out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[1;34m")?;
out.write_all(s)?;
out.write_all(b"\x1b[0m")
} else {
out.write_all(s)
}
}
#[inline]
fn write_value(&self, s: &[u8], out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[0;32m")?;
out.write_all(s)?;
out.write_all(b"\x1b[0m")
} else {
out.write_all(s)
}
}
#[inline]
fn write_true(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[0;33mtrue\x1b[0m")
} else {
out.write_all(b"true")
}
}
#[inline]
fn write_false(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[0;33mfalse\x1b[0m")
} else {
out.write_all(b"false")
}
}
#[inline]
fn write_null(&self, out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[0;35mnull\x1b[0m")
} else {
out.write_all(b"null")
}
}
#[inline]
fn write_number(&self, s: &[u8], out: &mut impl io::Write) -> Result<(), io::Error> {
if self.color == Color::Ansi {
out.write_all(b"\x1b[0;36m")?;
out.write_all(s)?;
out.write_all(b"\x1b[0m")
} else {
out.write_all(s)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Helpers method to prettify to an [`fmt::Write`] (String etc...)
fn format_fmt(input: &[u8], color: Color, out: &mut impl fmt::Write) -> FormatResult<()> {
let mut formatter = Formatter::new(input, color);
let mut bytes = Vec::new();
formatter.format(&mut bytes)?;
// If format is successful, we are sure that the bytes are UTF-8 valid.
let str = std::str::from_utf8(&bytes).unwrap();
out.write_str(str)?;
Ok(())
}
#[test]
fn parse_number_ok() {
let datas = [
// Parse some integers
("1234xxxx", "1234"),
("42", "42"),
(
"1233456787766677889778998789988",
"1233456787766677889778998789988",
),
("0000", "0"),
("0", "0"),
("-0", "-0"),
("012345", "0"),
("0abcdef", "0"),
("-10256", "-10256"),
("-012344", "-0"),
// Parse real (with fraction)
("1.000", "1.000"),
("1.7b", "1.7"),
];
for (input, expected) in datas {
let mut formatter = Formatter::new(input.as_bytes(), Color::NoColor);
let mut out = Vec::new();
formatter.parse_number(&mut out).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), expected);
}
}
#[test]
fn parse_number_failed() {
let datas = ["1.", "78980.a", "abc"];
for input in datas {
let mut formatter = Formatter::new(input.as_bytes(), Color::NoColor);
let mut out = Vec::new();
let result = formatter.parse_number(&mut out);
assert!(result.is_err());
}
}
fn assert_against_std(bytes: &[u8], len: usize) {
// We pass the full buffer to the parser, with some trailing bytes
let mut formatter = Formatter::new(bytes, Color::NoColor);
let ret = formatter.next_utf8_char();
// We test against a buffer without trailing
match std::str::from_utf8(&bytes[..len]) {
Ok(str) => {
assert!(ret.is_ok());
assert_eq!(formatter.pos.0, len);
let out = str::from_utf8(&formatter.input[0..formatter.pos.0]).unwrap();
assert_eq!(out, str);
}
Err(_) => {
assert!(ret.is_err());
}
}
}
#[test]
fn try_read_one_byte_to_utf8() {
// Iterate through all 1-byte UTF-8 bytes, even invalid
for b in 0x00..=0xFF {
let bytes = [b, b'x', b'x', b'x'];
assert_against_std(&bytes, 1);
}
}
#[test]
fn try_read_two_bytes_to_utf8() {
// Iterate through all UTF-8 2-bytes: C0..=DF 80..=BF
// It may contains invalid ones (overlong for instance).
for b1 in 0xC0..=0xDF {
for b2 in 0x80..=0xBF {
let bytes = [b1, b2, b'x', b'x', b'x'];
assert_against_std(&bytes, 2);
}
}
}
#[test]
fn try_read_three_bytes_to_utf8() {
// Iterate through all UTF-8 3-bytes: E0..=EF 80..=BF 80..=BF
// It may contains invalid ones (overlong for instance).
for b1 in 0xF0..=0xF7 {
for b2 in 0x80..=0xBF {
for b3 in 0x80..=0xBF {
let bytes = [b1, b2, b3, b'x', b'x', b'x'];
assert_against_std(&bytes, 3);
}
}
}
}
#[test]
fn try_read_four_bytes_to_utf8() {
// Iterate through all UTF-8 4-bytes: F0..=F7 80..=BF 80..=BF 80..=BF
// It may contains invalid ones (overlong for instance).
for b1 in 0xF0..=0xF7 {
for b2 in 0x80..=0xBF {
for b3 in 0x80..=0xBF {
for b4 in 0x80..=0xBF {
let bytes = [b1, b2, b3, b4, b'x', b'x', b'x'];
assert_against_std(&bytes, 4);
}
}
}
}
}
#[test]
fn format_valid_json() {
struct TestData {
input: &'static str,
expected: &'static str,
}
let datas = [
TestData {
input: r#"{"strings":{"english":"Hello, world!","chinese":"你好,世界","japanese":"こんにちは世界","korean":"안녕하세요 세계","arabic":"مرحبا بالعالم","hindi":"नमस्ते दुनिया","russian":"Привет, мир","greek":"Γειά σου Κόσμε","hebrew":"שלום עולם","accented":"Curaçao, naïve, façade, jalapeño"},"numbers":{"zero":0,"positive_int":42,"negative_int":-42,"large_int":1234567890123456789,"small_float":0.000123,"negative_float":-3.14159,"large_float":1.7976931348623157e308,"smallest_float":5e-324,"sci_notation_positive":6.022e23,"sci_notation_negative":-2.99792458e8},"booleans":{"isActive":true,"isDeleted":false},"emojis":{"happy":"😀","sad":"😢","fire":"🔥","rocket":"🚀","earth":"🌍","heart":"❤️","multi":"👩💻🧑🏽🚀👨👩👧👦"},"nothing":null}"#,
expected: r#"{
"strings": {
"english": "Hello, world!",
"chinese": "你好,世界",
"japanese": "こんにちは世界",
"korean": "안녕하세요 세계",
"arabic": "مرحبا بالعالم",
"hindi": "नमस्ते दुनिया",
"russian": "Привет, мир",
"greek": "Γειά σου Κόσμε",
"hebrew": "שלום עולם",
"accented": "Curaçao, naïve, façade, jalapeño"
},
"numbers": {
"zero": 0,
"positive_int": 42,
"negative_int": -42,
"large_int": 1234567890123456789,
"small_float": 0.000123,
"negative_float": -3.14159,
"large_float": 1.7976931348623157e308,
"smallest_float": 5e-324,
"sci_notation_positive": 6.022e23,
"sci_notation_negative": -2.99792458e8
},
"booleans": {
"isActive": true,
"isDeleted": false
},
"emojis": {
"happy": "😀",
"sad": "😢",
"fire": "🔥",
"rocket": "🚀",
"earth": "🌍",
"heart": "❤️",
"multi": "👩💻🧑🏽🚀👨👩👧👦"
},
"nothing": null
}
"#,
},
// From Go Standard library <https://github.com/golang/go/blob/master/src/encoding/json/jsontext/value_test.go>
// Primitives
TestData {
input: r#"{
"numbers": [333333333.33333329, 1E30, 4.50,
2e-3, 0.000000000000000000000000001, -0],
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
"literals": [null, true, false]
}"#,
expected: r#"{
"numbers": [
333333333.33333329,
1E30,
4.50,
2e-3,
0.000000000000000000000000001,
-0
],
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
"literals": [
null,
true,
false
]
}
"#,
},
TestData {
input: r#"{
"\u20ac": "Euro Sign",
"\r": "Carriage Return",
"\ufb33": "Hebrew Letter Dalet With Dagesh",
"1": "One",
"\ud83d\ude00": "Emoji: Grinning Face",
"\u0080": "Control",
"\u00f6": "Latin Small Letter O With Diaeresis"
}"#,
expected: r#"{
"\u20ac": "Euro Sign",
"\r": "Carriage Return",
"\ufb33": "Hebrew Letter Dalet With Dagesh",
"1": "One",
"\ud83d\ude00": "Emoji: Grinning Face",
"\u0080": "Control",
"\u00f6": "Latin Small Letter O With Diaeresis"
}
"#,
},
// LargeIntegers
TestData {
input: " [ -9223372036854775808 , 9223372036854775807 ] ",
expected: r#"[
-9223372036854775808,
9223372036854775807
]
"#,
},
// Duplicates
TestData {
input: r#" { "0" : 0 , "1" : 1 , "0" : 0 }"#,
expected: r#"{
"0": 0,
"1": 1,
"0": 0
}
"#,
},
// From jq "torture" tests <https://github.com/jqlang/jq/blob/master/tests/torture/input0.json>
TestData {
input: "[0,1,[12,22,[34,[45,56],7]],[]]",
expected: r#"[
0,
1,
[
12,
22,
[
34,
[
45,
56
],
7
]
],
[]
]
"#,
},
TestData {
input: r#"{"a":[{"b":[]},{},[2]]}"#,
expected: r#"{
"a": [
{
"b": []
},
{},
[
2
]
]
}
"#,
},
TestData {
input: " { }",
expected: "{}\n",
},
TestData {
input: r#"{"X":{},"Y":{},"X":{}} "#,
expected: r#"{
"X": {},
"Y": {},
"X": {}
}
"#,
},
];
for TestData { input, expected } in datas {
let mut out = String::new();
format_fmt(input.as_bytes(), Color::NoColor, &mut out).unwrap();
assert_eq!(out, expected);
}
}
// From Go Standard library <https://github.com/golang/go/blob/master/src/encoding/json/jsontext/decode_test.go>
#[test]
fn error_on_invalid() {
struct TestData {
name: &'static str,
input: &'static [u8],
expected_err: FormatError,
expected_message: &'static str,
}
let datas = [
TestData {
name: "Invalid start",
input: b" #",
expected_err: FormatError::InvalidByte(35, BytePos(1)),
expected_message: "invalid byte 0x23/'#' at offset 1",
},
TestData {
name: "Extra comma",
input: b" null , null ",
expected_err: FormatError::InvalidByte(44, BytePos(6)),
expected_message: "invalid byte 0x2c/',' at offset 6",
},
TestData {
name: "Truncated null",
input: b" nul",
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Invalid null",
input: b"nulL",
expected_err: FormatError::InvalidByte(76, BytePos(3)),
expected_message: "invalid byte 0x4c/'L' at offset 3",
},
TestData {
name: "Truncated false",
input: b"fals",
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Invalid false",
input: b"falsE",
expected_err: FormatError::InvalidByte(69, BytePos(4)),
expected_message: "invalid byte 0x45/'E' at offset 4",
},
TestData {
name: "Truncated true",
input: b"tru",
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Invalid true",
input: b"truE",
expected_err: FormatError::InvalidByte(69, BytePos(3)),
expected_message: "invalid byte 0x45/'E' at offset 3",
},
TestData {
name: "Invalid string",
input: br#""start"#,
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Truncated string",
input: br#""start"#,
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Invalid string",
input: b"\"ok \x00",
expected_err: FormatError::InvalidByte(0, BytePos(4)),
expected_message: "invalid byte 0x00 at offset 4",
},
TestData {
name: "Invalid UTF-8",
input: b"\"living\xde\xad\xbe\xef\"",
expected_err: FormatError::InvalidUtf8([0xbe, 0xef, 0, 0], 2, BytePos(9)),
expected_message: "invalid UTF-8 2 bytes 0xbe 0xef at offset 9",
},
TestData {
name: "Truncated number",
input: b"0.",
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Invalid number",
input: b"0.e",
expected_err: FormatError::InvalidByte(101, BytePos(2)),
expected_message: "invalid byte 0x65/'e' at offset 2",
},
TestData {
name: "Truncated number after start",
input: b"{",
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Truncated number after start",
input: b"{",
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Truncated number after name",
input: br#"{"0""#,
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Truncated number after colon",
input: br#"{"0":"#,
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Truncated number after value",
input: br#"{"0":0"#,
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Truncated number after comma",
input: br#"{"0":0,"#,
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Invalid object missing colon",
input: br#" { "fizz" "buzz" } "#,
expected_err: FormatError::InvalidByte(34, BytePos(10)),
expected_message: "invalid byte 0x22/'\"' at offset 10",
},
TestData {
name: "Invalid object missing colon got comma",
input: br#" { "fizz" , "buzz" } "#,
expected_err: FormatError::InvalidByte(44, BytePos(10)),
expected_message: "invalid byte 0x2c/',' at offset 10",
},
TestData {
name: "Invalid object missing colon got hash",
input: br#" { "fizz" # "buzz" } "#,
expected_err: FormatError::InvalidByte(35, BytePos(10)),
expected_message: "invalid byte 0x23/'#' at offset 10",
},
TestData {
name: "Invalid object missing comma",
input: br#" { "fizz" : "buzz" "gazz" } "#,
expected_err: FormatError::InvalidByte(34, BytePos(19)),
expected_message: "invalid byte 0x22/'\"' at offset 19",
},
TestData {
name: "Invalid object missing comma got colon",
input: br#" { "fizz" : "buzz" : "gazz" } "#,
expected_err: FormatError::InvalidByte(58, BytePos(19)),
expected_message: "invalid byte 0x3a/':' at offset 19",
},
TestData {
name: "Invalid object missing comma got hash",
input: br#" { "fizz" : "buzz" # "gazz" } "#,
expected_err: FormatError::InvalidByte(35, BytePos(19)),
expected_message: "invalid byte 0x23/'#' at offset 19",
},
TestData {
name: "Invalid object extra comma after start",
input: br#" { , } "#,
expected_err: FormatError::InvalidByte(44, BytePos(3)),
expected_message: "invalid byte 0x2c/',' at offset 3",
},
TestData {
name: "Invalid object extra comma after value",
input: br#" { "fizz" : "buzz" , } "#,
expected_err: FormatError::InvalidByte(125, BytePos(21)),
expected_message: "invalid byte 0x7d/'}' at offset 21",
},
TestData {
name: "Invalid object invalid name got null",
input: br#" { null : null } "#,
expected_err: FormatError::InvalidByte(110, BytePos(3)),
expected_message: "invalid byte 0x6e/'n' at offset 3",
},
TestData {
name: "Invalid object invalid name got false",
input: br#" { false : false } "#,
expected_err: FormatError::InvalidByte(102, BytePos(3)),
expected_message: "invalid byte 0x66/'f' at offset 3",
},
TestData {
name: "Invalid object invalid name got true",
input: br#" { true : true } "#,
expected_err: FormatError::InvalidByte(116, BytePos(3)),
expected_message: "invalid byte 0x74/'t' at offset 3",
},
TestData {
name: "Invalid object invalid name got number",
input: br#" { 0 : 0 } "#,
expected_err: FormatError::InvalidByte(48, BytePos(3)),
expected_message: "invalid byte 0x30/'0' at offset 3",
},
TestData {
name: "Invalid object invalid name got object",
input: br#" { {} : {} } "#,
expected_err: FormatError::InvalidByte(123, BytePos(3)),
expected_message: "invalid byte 0x7b/'{' at offset 3",
},
TestData {
name: "Invalid object invalid name got array",
input: br#" { [] : [] } "#,
expected_err: FormatError::InvalidByte(91, BytePos(3)),
expected_message: "invalid byte 0x5b/'[' at offset 3",
},
TestData {
name: "Invalid object mismatching delim",
input: br#" { ] "#,
expected_err: FormatError::InvalidByte(93, BytePos(3)),
expected_message: "invalid byte 0x5d/']' at offset 3",
},
TestData {
name: "Invalid object mismatching delim",
input: br#" { ] "#,
expected_err: FormatError::InvalidByte(93, BytePos(3)),
expected_message: "invalid byte 0x5d/']' at offset 3",
},
TestData {
name: "Truncated array after start",
input: b"[",
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Truncated array after value",
input: b"[0",
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Truncated array after comma",
input: b"[0,",
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Invalid array missing comma",
input: br#"[ "fizz" "buzz" ] "#,
expected_err: FormatError::InvalidByte(34, BytePos(9)),
expected_message: "invalid byte 0x22/'\"' at offset 9",
},
TestData {
name: "Invalid array mismatching delim",
input: b" [ } ",
expected_err: FormatError::InvalidByte(125, BytePos(3)),
expected_message: "invalid byte 0x7d/'}' at offset 3",
},
TestData {
name: "Invalid delim after top level",
input: br#" "", "#,
expected_err: FormatError::InvalidByte(44, BytePos(3)),
expected_message: "invalid byte 0x2c/',' at offset 3",
},
TestData {
name: "Invalid delim after begin object",
input: b"{:",
expected_err: FormatError::InvalidByte(58, BytePos(1)),
expected_message: "invalid byte 0x3a/':' at offset 1",
},
TestData {
name: "Invalid delim after object name",
input: br#"{"","#,
expected_err: FormatError::InvalidByte(44, BytePos(3)),
expected_message: "invalid byte 0x2c/',' at offset 3",
},
TestData {
name: "Valid delim after object name",
input: br#"{"":"#,
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Invalid delim after object value",
input: br#"{"":"":"#,
expected_err: FormatError::InvalidByte(58, BytePos(6)),
expected_message: "invalid byte 0x3a/':' at offset 6",
},
TestData {
name: "Valid delim after object value",
input: br#"{"":"","#,
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Invalid delim after begin array",
input: b"[,",
expected_err: FormatError::InvalidByte(44, BytePos(1)),
expected_message: "invalid byte 0x2c/',' at offset 1",
},
TestData {
name: "Invalid delim after array value",
input: br#"["":"#,
expected_err: FormatError::InvalidByte(58, BytePos(3)),
expected_message: "invalid byte 0x3a/':' at offset 3",
},
TestData {
name: "Valid delim after array value",
input: br#"["","#,
expected_err: FormatError::Eof,
expected_message: "unexpected end of file",
},
TestData {
name: "Error position",
input: b"\"a\xff000\"",
expected_err: FormatError::InvalidUtf8([255, 48, 48, 48], 4, BytePos(6)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x30 0x30 0x30 at offset 6",
},
TestData {
name: "Error position /0",
input: b" [ \"a\xff111\" ] ",
expected_err: FormatError::InvalidUtf8([255, 49, 49, 49], 4, BytePos(9)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x31 0x31 0x31 at offset 9",
},
TestData {
name: "Error position /1",
input: b" [ \"a1\" , \"b\xff111\" ] ",
expected_err: FormatError::InvalidUtf8([255, 49, 49, 49], 4, BytePos(16)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x31 0x31 0x31 at offset 16",
},
TestData {
name: "Error position /0/0",
input: b" [ [ \"a\xff222\" ] ] ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(11)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 11",
},
TestData {
name: "Error position /1/0",
input: b" [ \"a1\" , [ \"a\xff222\" ] ] ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(18)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 18",
},
TestData {
name: "Error position /0/1",
input: b" [ [ \"a2\" , \"b\xff222\" ] ] ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(18)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 18",
},
TestData {
name: "Error position /1/1",
input: b" [ \"a1\" , [ \"a2\" , \"b\xff222\" ] ] ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(25)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 25",
},
TestData {
name: "Error position /a1-",
input: b" { \"a\xff111\" : \"b1\" } ",
expected_err: FormatError::InvalidUtf8([255, 49, 49, 49], 4, BytePos(9)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x31 0x31 0x31 at offset 9",
},
TestData {
name: "Error position /a1",
input: b" { \"a1\" : \"b\xff111\" } ",
expected_err: FormatError::InvalidUtf8([255, 49, 49, 49], 4, BytePos(16)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x31 0x31 0x31 at offset 16",
},
TestData {
name: "Error position /c1-",
input: b" { \"a1\" : \"b1\" , \"c\xff111\" : \"d1\" } ",
expected_err: FormatError::InvalidUtf8([255, 49, 49, 49], 4, BytePos(23)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x31 0x31 0x31 at offset 23",
},
TestData {
name: "Error position /c1",
input: b"{ \"a1\" : \"b1\" , \"c1\" : \"d\xff111\" } ",
expected_err: FormatError::InvalidUtf8([255, 49, 49, 49], 4, BytePos(29)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x31 0x31 0x31 at offset 29",
},
TestData {
name: "Error position /a1/a2-",
input: b" { \"a1\" : { \"a\xff222\" : \"b2\" } } ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(18)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 18",
},
TestData {
name: "Error position /a1/a2",
input: b" { \"a1\" : { \"a2\" : \"b\xff222\" } } ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(25)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 25",
},
TestData {
name: "Error position /a1/c2-",
input: b" { \"a1\" : { \"a2\" : \"b2\" , \"c\xff222\" : \"d2\" } } ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(32)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 32",
},
TestData {
name: "Error position /a1/c2",
input: b" { \"a1\" : { \"a2\" : \"b2\" , \"c2\" : \"d\xff222\" } } ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(39)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 39",
},
TestData {
name: "Error position /1/a2",
input: b" [ \"a1\" , { \"a2\" : \"b\xff222\" } ] ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(25)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 25",
},
TestData {
name: "Error position /c1/1",
input: b" { \"a1\" : \"b1\" , \"c1\" : [ \"a2\" , \"b\xff222\" ] } ",
expected_err: FormatError::InvalidUtf8([255, 50, 50, 50], 4, BytePos(39)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x32 0x32 0x32 at offset 39",
},
TestData {
name: "Error position /0/a1/1/c3/1",
input: b" [ { \"a1\" : [ \"a2\" , { \"a3\" : \"b3\" , \"c3\" : [ \"a4\" , \"b\xff444\" ] } ] } ] ",
expected_err: FormatError::InvalidUtf8([255, 52, 52, 52], 4, BytePos(59)),
expected_message: "invalid UTF-8 4 bytes 0xff 0x34 0x34 0x34 at offset 59",
},
];
for TestData {
name,
input,
expected_err,
expected_message,
} in datas
{
let mut out = String::new();
let ret = format_fmt(input, Color::NoColor, &mut out);
let err = ret.unwrap_err();
assert_eq!(err, expected_err, "{name}");
assert_eq!(err.to_string(), expected_message, "{name}");
}
}
}