From 54d26f534028baa050992362e9136b7b0c0da9f8 Mon Sep 17 00:00:00 2001 From: Anthony Ramine Date: Mon, 27 Feb 2017 10:56:16 +0100 Subject: [PATCH 1/4] Avoid allocating a String to serialize a single char --- json/src/ser.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/json/src/ser.rs b/json/src/ser.rs index 30b4df715..1ca40f5d8 100644 --- a/json/src/ser.rs +++ b/json/src/ser.rs @@ -2,6 +2,7 @@ use std::io; use std::num::FpCategory; +use std::str; use serde::ser::{self, Impossible}; use super::error::{Error, ErrorCode, Result}; @@ -1245,11 +1246,16 @@ fn format_escaped_char(wr: &mut W, formatter: &mut F, valu where W: io::Write, F: Formatter, { - // FIXME: this allocation is required in order to be compatible with stable - // rust, which doesn't support encoding a `char` into a stack buffer. - let mut s = String::new(); - s.push(value); - format_escaped_str(wr, formatter, &s) + use std::io::Write; + // A char encoded as UTF-8 takes 4 bytes at most. + let mut buf = [0; 4]; + write!(&mut buf[..], "{}", value).unwrap(); + // Writing a char successfully always produce valid UTF-8. + // Once we do not support Rust <1.15 we will be able to just use + // the method `char::encode_utf8`. + // See https://github.com/serde-rs/json/issues/270. + let slice = unsafe { str::from_utf8_unchecked(&buf[0..value.len_utf8()]) }; + format_escaped_str(wr, formatter, slice) } /// Serialize the given data structure as JSON into the IO stream. From 917c57ee91c8b6dd01a2e5f7a0f2edde1da7de81 Mon Sep 17 00:00:00 2001 From: Anthony Ramine Date: Mon, 27 Feb 2017 01:38:25 +0100 Subject: [PATCH 2/4] Redefine Serializer::collect_str This allows us to format_args!() and friends while avoiding String allocations when serialising to JSON. --- json/Cargo.toml | 2 +- json/src/ser.rs | 61 ++++++++++++++++++++++++++++++++++++++++--- json_tests/Cargo.toml | 2 +- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/json/Cargo.toml b/json/Cargo.toml index bfa6e4c88..a86712df5 100644 --- a/json/Cargo.toml +++ b/json/Cargo.toml @@ -18,7 +18,7 @@ travis-ci = { repository = "serde-rs/json" } preserve_order = ["linked-hash-map"] [dependencies] -serde = "0.9.4" +serde = "0.9.10" num-traits = "0.1.32" linked-hash-map = { version = "0.4.1", optional = true } itoa = "0.3" diff --git a/json/src/ser.rs b/json/src/ser.rs index 1ca40f5d8..00acb399b 100644 --- a/json/src/ser.rs +++ b/json/src/ser.rs @@ -1,5 +1,6 @@ //! Serialize a Rust data structure into JSON data. +use std::fmt; use std::io; use std::num::FpCategory; use std::str; @@ -308,6 +309,50 @@ impl<'a, W, F> ser::Serializer for &'a mut Serializer try!(self.formatter.begin_object_value(&mut self.writer)); self.serialize_map(Some(len)) } + + fn collect_str(self, value: &T) -> Result + where T: fmt::Display, + { + use std::fmt::Write; + + struct Adapter<'ser, W: 'ser, F: 'ser> { + writer: &'ser mut W, + formatter: &'ser mut F, + error: Option, + } + + impl<'ser, W, F> Write for Adapter<'ser, W, F> + where W: io::Write, + F: Formatter, + { + fn write_str(&mut self, s: &str) -> fmt::Result { + assert!(self.error.is_none()); + match format_escaped_str_contents(self.writer, self.formatter, s) { + Ok(()) => Ok(()), + Err(err) => { + self.error = Some(err); + Err(fmt::Error) + } + } + } + } + + try!(self.formatter.begin_string(&mut self.writer)); + { + let mut adapter = Adapter { + writer: &mut self.writer, + formatter: &mut self.formatter, + error: None + }; + match write!(adapter, "{}", value) { + Ok(()) => assert!(adapter.error.is_none()), + Err(_) => { + return Err(adapter.error.expect("there should be an error")); + }, + } + } + self.formatter.end_string(&mut self.writer) + } } #[doc(hidden)] @@ -1179,9 +1224,20 @@ fn format_escaped_str(writer: &mut W, formatter: &mut F, v where W: io::Write, F: Formatter { - let bytes = value.as_bytes(); - try!(formatter.begin_string(writer)); + try!(format_escaped_str_contents(writer, formatter, value)); + try!(formatter.end_string(writer)); + Ok(()) +} + +fn format_escaped_str_contents(writer: &mut W, + formatter: &mut F, + value: &str) + -> Result<()> + where W: io::Write, + F: Formatter, +{ + let bytes = value.as_bytes(); let mut start = 0; @@ -1205,7 +1261,6 @@ fn format_escaped_str(writer: &mut W, formatter: &mut F, v try!(formatter.write_string_fragment(writer, &bytes[start..])); } - try!(formatter.end_string(writer)); Ok(()) } diff --git a/json_tests/Cargo.toml b/json_tests/Cargo.toml index 7e4ff2f70..7069783ff 100644 --- a/json_tests/Cargo.toml +++ b/json_tests/Cargo.toml @@ -8,7 +8,7 @@ publish = false trace-macros = [] [dependencies] -serde = "0.9" +serde = "0.9.10" serde_json = { path = "../json" } serde_derive = "0.9" From 3279a8dcdf2088241901705f2ef4bac05b97f270 Mon Sep 17 00:00:00 2001 From: Anthony Ramine Date: Mon, 27 Feb 2017 01:39:50 +0100 Subject: [PATCH 3/4] Update to serde 0.9.11 for collect_str --- json/Cargo.toml | 2 +- json_tests/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/json/Cargo.toml b/json/Cargo.toml index a86712df5..ca2dae200 100644 --- a/json/Cargo.toml +++ b/json/Cargo.toml @@ -18,7 +18,7 @@ travis-ci = { repository = "serde-rs/json" } preserve_order = ["linked-hash-map"] [dependencies] -serde = "0.9.10" +serde = "0.9.11" num-traits = "0.1.32" linked-hash-map = { version = "0.4.1", optional = true } itoa = "0.3" diff --git a/json_tests/Cargo.toml b/json_tests/Cargo.toml index 7069783ff..bdfc2e784 100644 --- a/json_tests/Cargo.toml +++ b/json_tests/Cargo.toml @@ -8,7 +8,7 @@ publish = false trace-macros = [] [dependencies] -serde = "0.9.10" +serde = "0.9.11" serde_json = { path = "../json" } serde_derive = "0.9" From 1d6a1c665a8fa51feb094d4efb25e21c90d6a354 Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Sun, 5 Mar 2017 17:31:22 -0800 Subject: [PATCH 4/4] Add char tests --- json_tests/tests/test.rs | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/json_tests/tests/test.rs b/json_tests/tests/test.rs index 7af2adb87..938bdcd78 100644 --- a/json_tests/tests/test.rs +++ b/json_tests/tests/test.rs @@ -189,6 +189,25 @@ fn test_write_bool() { test_pretty_encode_ok(tests); } +#[test] +fn test_write_char() { + let tests = &[ + ('n', "\"n\""), + ('"', "\"\\\"\""), + ('\\', "\"\\\\\""), + ('/', "\"/\""), + ('\x08', "\"\\b\""), + ('\x0C', "\"\\f\""), + ('\n', "\"\\n\""), + ('\r', "\"\\r\""), + ('\t', "\"\\t\""), + ('\x0B', "\"\\u000b\""), + ('\u{3A3}', "\"\u{3A3}\""), + ]; + test_encode_ok(tests); + test_pretty_encode_ok(tests); +} + #[test] fn test_write_list() { test_encode_ok(&[ @@ -801,6 +820,29 @@ fn test_parse_bool() { ]); } +#[test] +fn test_parse_char() { + test_parse_err::(vec![ + ("\"ab\"", "invalid value: string \"ab\", expected a character at line 1 column 4"), + ("10", "invalid type: integer `10`, expected a character at line 1 column 2"), + ]); + + test_parse_ok(vec![ + ("\"n\"", 'n'), + ("\"\\\"\"", '"'), + ("\"\\\\\"", '\\'), + ("\"/\"", '/'), + ("\"\\b\"", '\x08'), + ("\"\\f\"", '\x0C'), + ("\"\\n\"", '\n'), + ("\"\\r\"", '\r'), + ("\"\\t\"", '\t'), + ("\"\\u000b\"", '\x0B'), + ("\"\\u000B\"", '\x0B'), + ("\"\u{3A3}\"", '\u{3A3}'), + ]); +} + #[test] fn test_parse_number_errors() { test_parse_err::(vec![