Repository: not-fl3/nanoserde
Pull request #47 (2022-08-22 13:17:14)

Fix invalid JSON serialization of control characters
In JSON strings, control characters need to be escaped

![image](https://user-images.githubusercontent.com/21220820/185930674-359adcf8-e0fb-4109-af47-89586d5020d1.png)
[StackOverflow](https://stackoverflow.com/a/27516892/9946772)

nanoserde did this slightly wrong:
- it escaped null as `\0`, which doesn't exist: `\u0000` should be used
- it fails to escape `\b` and `\f` as well as any control character that is escaped to `\uXXXX`

You can see a reference implementation in serde_json:
https://github.com/serde-rs/json/blob/44d9c53e2507636c0c2afee0c9c132095dddb7df/src/ser.rs#L2073-L2139

This PR fixes the escaping rules to be correct and adds an integration test to verify this

not-fl3: Thanks for PR! :+1: 

Patch:
diff --git a/src/serde_json.rs b/src/serde_json.rs
index 1b07b94..2c62395 100644
--- a/src/serde_json.rs
+++ b/src/serde_json.rs
@@ -768,30 +768,17 @@ impl SerJson for String {
         s.out.push('"');
         for c in self.chars() {
             match c {
-                '\n' => {
-                    s.out.push('\\');
-                    s.out.push('n');
-                }
-                '\r' => {
-                    s.out.push('\\');
-                    s.out.push('r');
-                }
-                '\t' => {
-                    s.out.push('\\');
-                    s.out.push('t');
-                }
-                '\0' => {
-                    s.out.push('\\');
-                    s.out.push('0');
-                }
-                '\\' => {
-                    s.out.push('\\');
-                    s.out.push('\\');
-                }
-                '"' => {
-                    s.out.push('\\');
-                    s.out.push('"');
+                '\x08' => s.out += "\\b",
+                '\x0C' => s.out += "\\f",
+                '\n' => s.out += "\\n",
+                '\r' => s.out += "\\r",
+                '\t' => s.out += "\\t",
+                _ if c.is_ascii_control() => {
+                    use std::fmt::Write as _;
+                    let _ = write!(s.out, "\\u{:04x}", c as u32);
                 }
+                '\\' => s.out += "\\\\",
+                '"' => s.out += "\\\"",
                 _ => s.out.push(c),
             }
         }
@@ -861,9 +848,12 @@ where
     }
 }
 
-impl<T, const N: usize> DeJson for [T; N] where T: DeJson {
-    fn de_json(o:&mut DeJsonState, d:&mut Chars) -> Result<Self, DeJsonErr> {
-        unsafe{
+impl<T, const N: usize> DeJson for [T; N]
+where
+    T: DeJson,
+{
+    fn de_json(o: &mut DeJsonState, d: &mut Chars) -> Result<Self, DeJsonErr> {
+        unsafe {
             let mut to = std::mem::MaybeUninit::<[T; N]>::uninit();
             let top: *mut T = std::mem::transmute(&mut to);
             de_json_array_impl_inner(top, N, o, d)?;


Test patch:
diff --git a/tests/json.rs b/tests/json.rs
index bf8bfec..eb3daf6 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -656,3 +656,12 @@ fn tuple_struct2() {
 
     assert!(test == test_deserialized);
 }
+
+#[test]
+fn control_characters() {
+    let string = ('\0'..=' ').collect::<String>();
+    // Generated with serde_json
+    let escaped = r#""\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f ""#;
+
+    assert_eq!(SerJson::serialize_json(&string), escaped);
+}