From cbc7985181f0391d8b2aa104231e229873aa67ac Mon Sep 17 00:00:00 2001 From: Dillard Robertson Date: Sun, 24 Aug 2025 11:14:25 -0400 Subject: [PATCH 1/7] Allow serializing to fmt::Write (part 1: meat) Since ser::Serializer writes to an implementer of io::Write, the fact that the Serializer is producing valid utf-8 is lost to the type system. This prevents users from using instances of fmt::Write with ser::Serializer. This commit adds a ser::Write trait which is automatically implemented by io::Write instances, and which can be implemented by fmt::Write instances using the FmtWrite wrapper. A large number of mechanical changes are required in order to get ser::Serializer and ser::Formatter to use ser::Write rather than io::Write. For ease of review, this commit only contains the new traits or functions that needed refactoring. The next commit contains the mechanical changes. --- src/ser.rs | 127 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 92 insertions(+), 35 deletions(-) diff --git a/src/ser.rs b/src/ser.rs index 50c06f938..535bcb61e 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -1536,6 +1536,88 @@ pub enum CharEscape { AsciiControl(u8), } +impl CharEscape { + /// The fixed prefix of an escape code + fn fixed_prefix(self) -> &'static str { + use self::CharEscape as CE; + + match self { + CE::Quote => "\\\"", + CE::ReverseSolidus => "\\\\", + CE::Solidus => "\\/", + CE::Backspace => "\\b", + CE::FormFeed => "\\f", + CE::LineFeed => "\\n", + CE::CarriageReturn => "\\r", + CE::Tab => "\\t", + CE::AsciiControl(_) => "\\u00", + } + } +} + +/// This trait allows `Serializer` to write to either an +/// implementer of `io::Write` or an implementer of `fmt::Write` +pub trait Write { + /// Write string directly to writer + fn write_string(&mut self, input: &str) -> io::Result<()>; + + /// Write a json escaped string in one operation + fn write_escape(&mut self, escape: CharEscape) -> io::Result<()>; +} + + +impl Write for T { + #[inline] + fn write_string(&mut self, input: &str) -> io::Result<()> { + self.write_all(input.as_bytes()) + } + + #[inline] + fn write_escape(&mut self, escape: CharEscape) -> io::Result<()> { + use self::CharEscape as CE; + + match escape { + CE::AsciiControl(byte) => { + static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef"; + let bytes = &[ + b'\\', + b'u', + b'0', + b'0', + HEX_DIGITS[(byte >> 4) as usize], + HEX_DIGITS[(byte & 0xF) as usize], + ]; + self.write_all(bytes) + } + _ => self.write_all(escape.fixed_prefix().as_bytes()), + } + } +} + +/// Wrapper allowing an implementer of `fmt::Write` to implement +/// `ser::Write` without violating orphan rules +#[repr(transparent)] +pub struct FmtWrite(pub W); + +impl Write for FmtWrite { + #[inline] + fn write_string(&mut self, input: &str) -> io::Result<()> { + self.0.write_str(input).map_err(io::Error::other) + } + + #[inline] + fn write_escape(&mut self, escape: CharEscape) -> io::Result<()> { + use self::CharEscape as CE; + + match escape { + CE::AsciiControl(byte) => { + write!(&mut self.0, "\\u{:0>4x}", byte).map_err(io::Error::other) + } + _ => self.0.write_str(escape.fixed_prefix()).map_err(io::Error::other), + } + } +} + /// This trait abstracts away serializing the JSON control characters, which allows the user to /// optionally pretty print the JSON output. pub trait Formatter { @@ -1765,37 +1847,9 @@ pub trait Formatter { #[inline] fn write_char_escape(&mut self, writer: &mut W, char_escape: CharEscape) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - use self::CharEscape::*; - - 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'\\', - escape_char, - b'0', - b'0', - HEX_DIGITS[(byte >> 4) as usize], - HEX_DIGITS[(byte & 0xF) as usize], - ]; - writer.write_all(bytes) - } - _ => writer.write_all(&[b'\\', escape_char]), - } + writer.write_escape(char_escape) } /// Writes the representation of a byte array. Formatters can choose whether @@ -1945,21 +1999,24 @@ impl Formatter for CompactFormatter {} pub struct PrettyFormatter<'a> { current_indent: usize, has_value: bool, - indent: &'a [u8], + indent: &'a str, } impl<'a> PrettyFormatter<'a> { /// Construct a pretty printer formatter that defaults to using two spaces for indentation. pub fn new() -> Self { - PrettyFormatter::with_indent(b" ") + PrettyFormatter { + current_indent: 0, + has_value: false, + indent: " ", + } } /// Construct a pretty printer formatter that uses the `indent` string for indentation. pub fn with_indent(indent: &'a [u8]) -> Self { PrettyFormatter { - current_indent: 0, - has_value: false, - indent, + indent: str::from_utf8(indent).unwrap_or(" "), + .. Self::new() } } } From c09ceecc781cc41ddf6e59b319abb91c4f49f7f9 Mon Sep 17 00:00:00 2001 From: Dillard Robertson Date: Sun, 24 Aug 2025 11:15:43 -0400 Subject: [PATCH 2/7] Allow serializing to fmt::Write (part 2: mechanical changes) See parent commit for more information. This commit switches out io::Write for ser::Write, write_all for write_string, and removes casts from strings to byte slices. --- src/ser.rs | 198 ++++++++++++++++++++++++++--------------------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/src/ser.rs b/src/ser.rs index 535bcb61e..51430c416 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -21,7 +21,7 @@ pub struct Serializer { impl Serializer where - W: io::Write, + W: Write, { /// Creates a new JSON serializer. #[inline] @@ -32,7 +32,7 @@ where impl<'a, W> Serializer> where - W: io::Write, + W: Write, { /// Creates a new JSON pretty print serializer. #[inline] @@ -43,7 +43,7 @@ where impl Serializer where - W: io::Write, + W: Write, F: Formatter, { /// Creates a new JSON visitor whose output will be written to the writer @@ -62,7 +62,7 @@ where impl<'a, W, F> ser::Serializer for &'a mut Serializer where - W: io::Write, + W: Write, F: Formatter, { type Ok = (); @@ -419,9 +419,9 @@ where error: Option, } - impl<'ser, W, F> Write for Adapter<'ser, W, F> + impl<'ser, W, F> fmt::Write for Adapter<'ser, W, F> where - W: io::Write, + W: self::Write, F: Formatter, { fn write_str(&mut self, s: &str) -> fmt::Result { @@ -481,7 +481,7 @@ pub enum Compound<'a, W: 'a, F: 'a> { impl<'a, W, F> ser::SerializeSeq for Compound<'a, W, F> where - W: io::Write, + W: Write, F: Formatter, { type Ok = (); @@ -528,7 +528,7 @@ where impl<'a, W, F> ser::SerializeTuple for Compound<'a, W, F> where - W: io::Write, + W: Write, F: Formatter, { type Ok = (); @@ -550,7 +550,7 @@ where impl<'a, W, F> ser::SerializeTupleStruct for Compound<'a, W, F> where - W: io::Write, + W: Write, F: Formatter, { type Ok = (); @@ -572,7 +572,7 @@ where impl<'a, W, F> ser::SerializeTupleVariant for Compound<'a, W, F> where - W: io::Write, + W: Write, F: Formatter, { type Ok = (); @@ -610,7 +610,7 @@ where impl<'a, W, F> ser::SerializeMap for Compound<'a, W, F> where - W: io::Write, + W: Write, F: Formatter, { type Ok = (); @@ -682,7 +682,7 @@ where impl<'a, W, F> ser::SerializeStruct for Compound<'a, W, F> where - W: io::Write, + W: Write, F: Formatter, { type Ok = (); @@ -728,7 +728,7 @@ where impl<'a, W, F> ser::SerializeStructVariant for Compound<'a, W, F> where - W: io::Write, + W: Write, F: Formatter, { type Ok = (); @@ -794,7 +794,7 @@ fn float_key_must_be_finite() -> Error { impl<'a, W, F> ser::Serializer for MapKeySerializer<'a, W, F> where - W: io::Write, + W: Write, F: Formatter, { type Ok = (); @@ -1153,10 +1153,10 @@ where } #[cfg(feature = "arbitrary_precision")] -struct NumberStrEmitter<'a, W: 'a + io::Write, F: 'a + Formatter>(&'a mut Serializer); +struct NumberStrEmitter<'a, W: 'a + Write, F: 'a + Formatter>(&'a mut Serializer); #[cfg(feature = "arbitrary_precision")] -impl<'a, W: io::Write, F: Formatter> ser::Serializer for NumberStrEmitter<'a, W, F> { +impl<'a, W: Write, F: Formatter> ser::Serializer for NumberStrEmitter<'a, W, F> { type Ok = (); type Error = Error; @@ -1330,10 +1330,10 @@ impl<'a, W: io::Write, F: Formatter> ser::Serializer for NumberStrEmitter<'a, W, } #[cfg(feature = "raw_value")] -struct RawValueStrEmitter<'a, W: 'a + io::Write, F: 'a + Formatter>(&'a mut Serializer); +struct RawValueStrEmitter<'a, W: 'a + Write, F: 'a + Formatter>(&'a mut Serializer); #[cfg(feature = "raw_value")] -impl<'a, W: io::Write, F: Formatter> ser::Serializer for RawValueStrEmitter<'a, W, F> { +impl<'a, W: Write, F: Formatter> ser::Serializer for RawValueStrEmitter<'a, W, F> { type Ok = (); type Error = Error; @@ -1625,133 +1625,133 @@ pub trait Formatter { #[inline] fn write_null(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(b"null") + writer.write_string("null") } /// Writes a `true` or `false` value to the specified writer. #[inline] fn write_bool(&mut self, writer: &mut W, value: bool) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let s = if value { - b"true" as &[u8] + "true" } else { - b"false" as &[u8] + "false" }; - writer.write_all(s) + writer.write_string(s) } /// Writes an integer value like `-123` to the specified writer. #[inline] fn write_i8(&mut self, writer: &mut W, value: i8) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes an integer value like `-123` to the specified writer. #[inline] fn write_i16(&mut self, writer: &mut W, value: i16) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes an integer value like `-123` to the specified writer. #[inline] fn write_i32(&mut self, writer: &mut W, value: i32) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes an integer value like `-123` to the specified writer. #[inline] fn write_i64(&mut self, writer: &mut W, value: i64) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes an integer value like `-123` to the specified writer. #[inline] fn write_i128(&mut self, writer: &mut W, value: i128) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes an integer value like `123` to the specified writer. #[inline] fn write_u8(&mut self, writer: &mut W, value: u8) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes an integer value like `123` to the specified writer. #[inline] fn write_u16(&mut self, writer: &mut W, value: u16) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes an integer value like `123` to the specified writer. #[inline] fn write_u32(&mut self, writer: &mut W, value: u32) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes an integer value like `123` to the specified writer. #[inline] fn write_u64(&mut self, writer: &mut W, value: u64) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes an integer value like `123` to the specified writer. #[inline] fn write_u128(&mut self, writer: &mut W, value: u128) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = itoa::Buffer::new(); let s = buffer.format(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes a floating point value like `-31.26e+12` to the specified writer. @@ -1772,11 +1772,11 @@ pub trait Formatter { #[inline] fn write_f32(&mut self, writer: &mut W, value: f32) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = ryu::Buffer::new(); let s = buffer.format_finite(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes a floating point value like `-31.26e+12` to the specified writer. @@ -1797,20 +1797,20 @@ pub trait Formatter { #[inline] fn write_f64(&mut self, writer: &mut W, value: f64) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { let mut buffer = ryu::Buffer::new(); let s = buffer.format_finite(value); - writer.write_all(s.as_bytes()) + writer.write_string(s) } /// Writes a number that has already been rendered to a string. #[inline] fn write_number_str(&mut self, writer: &mut W, value: &str) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(value.as_bytes()) + writer.write_string(value) } /// Called before each series of `write_string_fragment` and @@ -1818,9 +1818,9 @@ pub trait Formatter { #[inline] fn begin_string(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(b"\"") + writer.write_string("\"") } /// Called after each series of `write_string_fragment` and @@ -1828,9 +1828,9 @@ pub trait Formatter { #[inline] fn end_string(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(b"\"") + writer.write_string("\"") } /// Writes a string fragment that doesn't need any escaping to the @@ -1838,9 +1838,9 @@ pub trait Formatter { #[inline] fn write_string_fragment(&mut self, writer: &mut W, fragment: &str) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(fragment.as_bytes()) + writer.write_string(fragment) } /// Writes a character escape code to the specified writer. @@ -1857,7 +1857,7 @@ pub trait Formatter { /// JSON string encoding like hex or base64. fn write_byte_array(&mut self, writer: &mut W, value: &[u8]) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { tri!(self.begin_array(writer)); let mut first = true; @@ -1875,9 +1875,9 @@ pub trait Formatter { #[inline] fn begin_array(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(b"[") + writer.write_string("[") } /// Called after every array. Writes a `]` to the specified @@ -1885,9 +1885,9 @@ pub trait Formatter { #[inline] fn end_array(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(b"]") + writer.write_string("]") } /// Called before every array value. Writes a `,` if needed to @@ -1895,12 +1895,12 @@ pub trait Formatter { #[inline] fn begin_array_value(&mut self, writer: &mut W, first: bool) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { if first { Ok(()) } else { - writer.write_all(b",") + writer.write_string(",") } } @@ -1908,7 +1908,7 @@ pub trait Formatter { #[inline] fn end_array_value(&mut self, _writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { Ok(()) } @@ -1918,9 +1918,9 @@ pub trait Formatter { #[inline] fn begin_object(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(b"{") + writer.write_string("{") } /// Called after every object. Writes a `}` to the specified @@ -1928,21 +1928,21 @@ pub trait Formatter { #[inline] fn end_object(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(b"}") + writer.write_string("}") } /// Called before every object key. #[inline] fn begin_object_key(&mut self, writer: &mut W, first: bool) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { if first { Ok(()) } else { - writer.write_all(b",") + writer.write_string(",") } } @@ -1952,7 +1952,7 @@ pub trait Formatter { #[inline] fn end_object_key(&mut self, _writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { Ok(()) } @@ -1963,16 +1963,16 @@ pub trait Formatter { #[inline] fn begin_object_value(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(b":") + writer.write_string(":") } /// Called after every object value. #[inline] fn end_object_value(&mut self, _writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { Ok(()) } @@ -1982,9 +1982,9 @@ pub trait Formatter { #[inline] fn write_raw_fragment(&mut self, writer: &mut W, fragment: &str) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(fragment.as_bytes()) + writer.write_string(fragment) } } @@ -2031,41 +2031,41 @@ impl<'a> Formatter for PrettyFormatter<'a> { #[inline] fn begin_array(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { self.current_indent += 1; self.has_value = false; - writer.write_all(b"[") + writer.write_string("[") } #[inline] fn end_array(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { self.current_indent -= 1; if self.has_value { - tri!(writer.write_all(b"\n")); + tri!(writer.write_string("\n")); tri!(indent(writer, self.current_indent, self.indent)); } - writer.write_all(b"]") + writer.write_string("]") } #[inline] fn begin_array_value(&mut self, writer: &mut W, first: bool) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - tri!(writer.write_all(if first { b"\n" } else { b",\n" })); + tri!(writer.write_string(if first { "\n" } else { ",\n" })); indent(writer, self.current_indent, self.indent) } #[inline] fn end_array_value(&mut self, _writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { self.has_value = true; Ok(()) @@ -2074,49 +2074,49 @@ impl<'a> Formatter for PrettyFormatter<'a> { #[inline] fn begin_object(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { self.current_indent += 1; self.has_value = false; - writer.write_all(b"{") + writer.write_string("{") } #[inline] fn end_object(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { self.current_indent -= 1; if self.has_value { - tri!(writer.write_all(b"\n")); + tri!(writer.write_string("\n")); tri!(indent(writer, self.current_indent, self.indent)); } - writer.write_all(b"}") + writer.write_string("}") } #[inline] fn begin_object_key(&mut self, writer: &mut W, first: bool) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - tri!(writer.write_all(if first { b"\n" } else { b",\n" })); + tri!(writer.write_string(if first { "\n" } else { ",\n" })); indent(writer, self.current_indent, self.indent) } #[inline] fn begin_object_value(&mut self, writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { - writer.write_all(b": ") + writer.write_string(": ") } #[inline] fn end_object_value(&mut self, _writer: &mut W) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { self.has_value = true; Ok(()) @@ -2125,7 +2125,7 @@ impl<'a> Formatter for PrettyFormatter<'a> { fn format_escaped_str(writer: &mut W, formatter: &mut F, value: &str) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, F: ?Sized + Formatter, { tri!(formatter.begin_string(writer)); @@ -2139,7 +2139,7 @@ fn format_escaped_str_contents( value: &str, ) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, F: ?Sized + Formatter, { let mut bytes = value.as_bytes(); @@ -2330,12 +2330,12 @@ where Ok(string) } -fn indent(wr: &mut W, n: usize, s: &[u8]) -> io::Result<()> +fn indent(wr: &mut W, n: usize, s: &str) -> io::Result<()> where - W: ?Sized + io::Write, + W: ?Sized + Write, { for _ in 0..n { - tri!(wr.write_all(s)); + tri!(wr.write_string(s)); } Ok(()) From 8434f3a5df522814454a2a6cc7963f8d0280946d Mon Sep 17 00:00:00 2001 From: Dillard Robertson Date: Sun, 24 Aug 2025 11:16:35 -0400 Subject: [PATCH 3/7] Allow serializing to fmt::Write (part 3: test) Most of the code is shared between io::Write and fmt::Write, so the existing tests will ensure that fmt::Write implementers work correctly. However, escape codes are handled differently, so this commit adds a test to ensure that io::Write and fmt::Write implementers see the same results. --- tests/test.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test.rs b/tests/test.rs index d41a2336a..201b40af7 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -2558,3 +2558,23 @@ fn test_control_character_search() { "control character (\\u0000-\\u001F) found while parsing a string at line 1 column 2", )]); } + +#[cfg(feature = "std")] +#[test] +fn test_serialize_control_character_to_string() { + // Test that control characters are the same for + // the io::Write and fmt::Write implementations + use serde_json::ser::{FmtWrite, Serializer}; + + let test_string: &str = "\x01\r\n\t"; + + let mut string_writer = String::new(); + let mut serializer = Serializer::new(FmtWrite(&mut string_writer)); + let _ = serializer.serialize_str(test_string); + + let mut io_writer = Vec::::new(); + let mut serializer = Serializer::new(&mut io_writer); + let _ = serializer.serialize_str(test_string); + + assert_eq!(string_writer.as_bytes(), &*io_writer); +} From b0a44bca6da775734e124f3d29c9444c1aa3249d Mon Sep 17 00:00:00 2001 From: Dillard Robertson Date: Sun, 24 Aug 2025 12:11:28 -0400 Subject: [PATCH 4/7] Add a polyfill for io::Error::other. io::Error::other is only supported in 1.74 and above, which is higher than our MSRV. --- src/ser.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ser.rs b/src/ser.rs index 51430c416..5868cdd18 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -1602,7 +1602,7 @@ pub struct FmtWrite(pub W); impl Write for FmtWrite { #[inline] fn write_string(&mut self, input: &str) -> io::Result<()> { - self.0.write_str(input).map_err(io::Error::other) + self.0.write_str(input).map_err(error_other) } #[inline] @@ -1611,13 +1611,18 @@ impl Write for FmtWrite { match escape { CE::AsciiControl(byte) => { - write!(&mut self.0, "\\u{:0>4x}", byte).map_err(io::Error::other) + write!(&mut self.0, "\\u{:0>4x}", byte).map_err(error_other) } - _ => self.0.write_str(escape.fixed_prefix()).map_err(io::Error::other), + _ => self.0.write_str(escape.fixed_prefix()).map_err(error_other), } } } +/// Polyfill for io::Error::other +fn error_other(error: fmt::Error) -> io::Error { + io::Error::new(io::ErrorKind::Other, error) +} + /// This trait abstracts away serializing the JSON control characters, which allows the user to /// optionally pretty print the JSON output. pub trait Formatter { From d4070d610fae05e0266e6e96447419fd032d6d22 Mon Sep 17 00:00:00 2001 From: Dillard Robertson Date: Sun, 24 Aug 2025 12:17:08 -0400 Subject: [PATCH 5/7] Adjust io::Error::other polyfill. Evidently old versions of io::Error only accept static strings as context for the error. --- src/ser.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ser.rs b/src/ser.rs index 5868cdd18..97e8bb311 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -1619,8 +1619,8 @@ impl Write for FmtWrite { } /// Polyfill for io::Error::other -fn error_other(error: fmt::Error) -> io::Error { - io::Error::new(io::ErrorKind::Other, error) +fn error_other(_error: fmt::Error) -> io::Error { + io::Error::new(io::ErrorKind::Other, "Formatting Error") } /// This trait abstracts away serializing the JSON control characters, which allows the user to From e7b4ee346760dc7fcc2f109f437d674ad188d81b Mon Sep 17 00:00:00 2001 From: Dillard Robertson Date: Sun, 24 Aug 2025 12:24:37 -0400 Subject: [PATCH 6/7] Silence complaints that FmtWrite is unused with `alloc` feature. --- src/ser.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ser.rs b/src/ser.rs index 97e8bb311..7032eccc1 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -1557,6 +1557,7 @@ impl CharEscape { /// This trait allows `Serializer` to write to either an /// implementer of `io::Write` or an implementer of `fmt::Write` +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub trait Write { /// Write string directly to writer fn write_string(&mut self, input: &str) -> io::Result<()>; @@ -1596,6 +1597,7 @@ impl Write for T { /// Wrapper allowing an implementer of `fmt::Write` to implement /// `ser::Write` without violating orphan rules +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] #[repr(transparent)] pub struct FmtWrite(pub W); @@ -1619,6 +1621,7 @@ impl Write for FmtWrite { } /// Polyfill for io::Error::other +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn error_other(_error: fmt::Error) -> io::Error { io::Error::new(io::ErrorKind::Other, "Formatting Error") } From 4f69ab5b119f8c75c6047153ac26166d9e8f46d6 Mon Sep 17 00:00:00 2001 From: Dillard Robertson Date: Sun, 24 Aug 2025 19:03:19 -0400 Subject: [PATCH 7/7] Make FmtWriter and error_other conditional on "std" feature. This silences rustc version 1.89 specifically from complaining that these are not constructable/used. This issue is caused by the ser module only being public if the "std" feature is turned on. --- src/ser.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ser.rs b/src/ser.rs index 7032eccc1..af6a00a15 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -1597,10 +1597,12 @@ impl Write for T { /// Wrapper allowing an implementer of `fmt::Write` to implement /// `ser::Write` without violating orphan rules +#[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] #[repr(transparent)] pub struct FmtWrite(pub W); +#[cfg(feature = "std")] impl Write for FmtWrite { #[inline] fn write_string(&mut self, input: &str) -> io::Result<()> { @@ -1621,6 +1623,7 @@ impl Write for FmtWrite { } /// Polyfill for io::Error::other +#[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn error_other(_error: fmt::Error) -> io::Error { io::Error::new(io::ErrorKind::Other, "Formatting Error")