Skip to content

Commit 7ece75e

Browse files
authored
Escape slashes when formatting JSON (#598)
* Escape slashes when formatting JSON * Include other separators
1 parent 72435dc commit 7ece75e

1 file changed

Lines changed: 95 additions & 2 deletions

File tree

src/container.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,52 @@
1+
use std::io;
2+
13
use crate::Codec;
24
use anyhow::{anyhow, Result};
35
use serde::{Deserialize, Serialize};
46

7+
#[derive(Default)]
8+
struct ShopifyJsonFormatter;
9+
10+
impl serde_json::ser::Formatter for ShopifyJsonFormatter {
11+
fn write_string_fragment<W>(&mut self, writer: &mut W, fragment: &str) -> io::Result<()>
12+
where
13+
W: ?Sized + io::Write,
14+
{
15+
let mut start = 0;
16+
for (index, character) in fragment.char_indices() {
17+
match character {
18+
'/' => {
19+
writer.write_all(&fragment.as_bytes()[start..index])?;
20+
writer.write_all(br"\/")?;
21+
start = index + character.len_utf8();
22+
}
23+
'\u{2028}' => {
24+
writer.write_all(&fragment.as_bytes()[start..index])?;
25+
writer.write_all(br"\u2028")?;
26+
start = index + character.len_utf8();
27+
}
28+
'\u{2029}' => {
29+
writer.write_all(&fragment.as_bytes()[start..index])?;
30+
writer.write_all(br"\u2029")?;
31+
start = index + character.len_utf8();
32+
}
33+
_ => {}
34+
}
35+
}
36+
writer.write_all(&fragment.as_bytes()[start..])
37+
}
38+
}
39+
40+
fn to_shopify_json_vec(value: &serde_json::Value) -> Result<Vec<u8>> {
41+
let mut bytes = Vec::new();
42+
let formatter = ShopifyJsonFormatter;
43+
let mut serializer = serde_json::Serializer::with_formatter(&mut bytes, formatter);
44+
value
45+
.serialize(&mut serializer)
46+
.map_err(|e| anyhow!("Couldn't serialize JSON: {}", e))?;
47+
Ok(bytes)
48+
}
49+
550
#[derive(Debug, Clone, Default)]
651
pub enum BytesContainerType {
752
/// Input bytes.
@@ -64,8 +109,7 @@ impl BytesContainer {
64109
BytesContainerType::Input => {
65110
let json = serde_json::from_slice::<serde_json::Value>(&raw)
66111
.map_err(|e| anyhow!("Invalid input JSON: {}", e))?;
67-
let minified_buffer = serde_json::to_vec(&json)
68-
.map_err(|e| anyhow!("Couldn't serialize JSON: {}", e))?;
112+
let minified_buffer = to_shopify_json_vec(&json)?;
69113

70114
Ok(Self {
71115
codec,
@@ -136,3 +180,52 @@ impl BytesContainer {
136180
}
137181
}
138182
}
183+
184+
#[cfg(test)]
185+
mod tests {
186+
use super::*;
187+
188+
#[test]
189+
fn json_input_escapes_solidus_like_shopify_json() -> Result<()> {
190+
let input = br#"{"id":"gid://shopify/Product/1"}"#.to_vec();
191+
192+
let container = BytesContainer::new(BytesContainerType::Input, Codec::Json, input)?;
193+
194+
assert_eq!(
195+
String::from_utf8(container.raw).unwrap(),
196+
r#"{"id":"gid:\/\/shopify\/Product\/1"}"#
197+
);
198+
199+
Ok(())
200+
}
201+
202+
#[test]
203+
fn json_input_escapes_line_and_paragraph_separators_like_shopify_json() -> Result<()> {
204+
let input = "{\"line\":\"before\u{2028}after\",\"paragraph\":\"before\u{2029}after\"}"
205+
.as_bytes()
206+
.to_vec();
207+
208+
let container = BytesContainer::new(BytesContainerType::Input, Codec::Json, input)?;
209+
210+
assert_eq!(
211+
String::from_utf8(container.raw).unwrap(),
212+
r#"{"line":"before\u2028after","paragraph":"before\u2029after"}"#
213+
);
214+
215+
Ok(())
216+
}
217+
218+
#[test]
219+
fn json_input_preserves_object_order() -> Result<()> {
220+
let input = br#"{"b":1,"a":2}"#.to_vec();
221+
222+
let container = BytesContainer::new(BytesContainerType::Input, Codec::Json, input)?;
223+
224+
assert_eq!(
225+
String::from_utf8(container.raw).unwrap(),
226+
r#"{"b":1,"a":2}"#
227+
);
228+
229+
Ok(())
230+
}
231+
}

0 commit comments

Comments
 (0)