diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77d611a09..340b652b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@miri + with: + toolchain: nightly-2025-05-16 # https://github.com/rust-lang/miri/issues/4323 - run: cargo miri setup - run: cargo miri test --target ${{matrix.target}} - run: cargo miri test --target ${{matrix.target}} --features preserve_order,float_roundtrip,arbitrary_precision,raw_value diff --git a/Cargo.toml b/Cargo.toml index 866c31318..67794cdf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "serde_json" -version = "1.0.140" +version = "1.0.141" authors = ["Erick Tryzelaar ", "David Tolnay "] categories = ["encoding", "parser-implementations", "no-std"] description = "A JSON serialization file format" diff --git a/src/lib.rs b/src/lib.rs index a9f82f2b3..5223afbc2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -299,7 +299,7 @@ //! [macro]: crate::json //! [`serde-json-core`]: https://github.com/rust-embedded-community/serde-json-core -#![doc(html_root_url = "https://docs.rs/serde_json/1.0.140")] +#![doc(html_root_url = "https://docs.rs/serde_json/1.0.141")] // Ignored clippy lints #![allow( clippy::collapsible_else_if, @@ -366,6 +366,7 @@ #![deny(missing_docs)] #![no_std] #![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(unknown_lints, mismatched_lifetime_syntaxes)] #[cfg(not(any(feature = "std", feature = "alloc")))] compile_error! { diff --git a/src/read.rs b/src/read.rs index 0748af44c..f90d9f74a 100644 --- a/src/read.rs +++ b/src/read.rs @@ -984,7 +984,7 @@ fn push_wtf8_codepoint(n: u32, scratch: &mut Vec) { scratch.reserve(4); // SAFETY: After the `reserve` call, `scratch` has at least 4 bytes of - // allocated but unintialized memory after its last initialized byte, which + // allocated but uninitialized memory after its last initialized byte, which // is where `ptr` points. All reachable match arms write `encoded_len` bytes // to that region and update the length accordingly, and `encoded_len` is // always <= 4. diff --git a/src/ser.rs b/src/ser.rs index 9b14389c8..de78b34e9 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -7,7 +7,9 @@ use alloc::string::String; use alloc::string::ToString; use alloc::vec::Vec; use core::fmt::{self, Display}; +use core::hint; use core::num::FpCategory; +use core::str; use serde::ser::{self, Impossible, Serialize}; /// A structure for serializing Rust values into JSON. @@ -1534,23 +1536,6 @@ pub enum CharEscape { AsciiControl(u8), } -impl CharEscape { - #[inline] - fn from_escape_table(escape: u8, byte: u8) -> CharEscape { - match escape { - self::BB => CharEscape::Backspace, - self::TT => CharEscape::Tab, - self::NN => CharEscape::LineFeed, - self::FF => CharEscape::FormFeed, - self::RR => CharEscape::CarriageReturn, - self::QU => CharEscape::Quote, - self::BS => CharEscape::ReverseSolidus, - self::UU => CharEscape::AsciiControl(byte), - _ => unreachable!(), - } - } -} - /// This trait abstracts away serializing the JSON control characters, which allows the user to /// optionally pretty print the JSON output. pub trait Formatter { @@ -1784,30 +1769,33 @@ pub trait Formatter { { use self::CharEscape::*; - let s = match char_escape { - Quote => b"\\\"", - ReverseSolidus => b"\\\\", - Solidus => b"\\/", - Backspace => b"\\b", - FormFeed => b"\\f", - LineFeed => b"\\n", - CarriageReturn => b"\\r", - Tab => b"\\t", + let escape_char = match char_escape { + Quote => b'"', + ReverseSolidus => b'\\', + Solidus => b'/', + Backspace => b'b', + FormFeed => b'f', + LineFeed => b'n', + CarriageReturn => b'r', + Tab => b't', + AsciiControl(_) => b'u', + }; + + match char_escape { AsciiControl(byte) => { static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef"; let bytes = &[ b'\\', - b'u', + escape_char, b'0', b'0', HEX_DIGITS[(byte >> 4) as usize], HEX_DIGITS[(byte & 0xF) as usize], ]; - return writer.write_all(bytes); + writer.write_all(bytes) } - }; - - writer.write_all(s) + _ => writer.write_all(&[b'\\', escape_char]), + } } /// Writes the representation of a byte array. Formatters can choose whether @@ -2097,31 +2085,51 @@ where W: ?Sized + io::Write, F: ?Sized + Formatter, { - let bytes = value.as_bytes(); + let mut bytes = value.as_bytes(); - let mut start = 0; + let mut i = 0; + while i < bytes.len() { + let (string_run, rest) = bytes.split_at(i); + let (&byte, rest) = rest.split_first().unwrap(); - for (i, &byte) in bytes.iter().enumerate() { let escape = ESCAPE[byte as usize]; + + i += 1; if escape == 0 { continue; } - if start < i { - tri!(formatter.write_string_fragment(writer, &value[start..i])); + bytes = rest; + i = 0; + + // Safety: string_run is a valid utf8 string, since we only split on ascii sequences + let string_run = unsafe { str::from_utf8_unchecked(string_run) }; + if !string_run.is_empty() { + tri!(formatter.write_string_fragment(writer, string_run)); } - let char_escape = CharEscape::from_escape_table(escape, byte); + let char_escape = match escape { + self::BB => CharEscape::Backspace, + self::TT => CharEscape::Tab, + self::NN => CharEscape::LineFeed, + self::FF => CharEscape::FormFeed, + self::RR => CharEscape::CarriageReturn, + self::QU => CharEscape::Quote, + self::BS => CharEscape::ReverseSolidus, + self::UU => CharEscape::AsciiControl(byte), + // Safety: the escape table does not contain any other type of character. + _ => unsafe { hint::unreachable_unchecked() }, + }; tri!(formatter.write_char_escape(writer, char_escape)); - - start = i + 1; } - if start == bytes.len() { + // Safety: bytes is a valid utf8 string, since we only split on ascii sequences + let string_run = unsafe { str::from_utf8_unchecked(bytes) }; + if string_run.is_empty() { return Ok(()); } - formatter.write_string_fragment(writer, &value[start..]) + formatter.write_string_fragment(writer, string_run) } const BB: u8 = b'b'; // \x08