From 6feea9e735e4033f6eae89ddc91bb73d6683a730 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 14 Jul 2026 11:19:34 -0400 Subject: [PATCH 1/2] Escape slashes when formatting JSON --- src/container.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/src/container.rs b/src/container.rs index c3c1fd1b..72f9b457 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,7 +1,39 @@ +use std::io; + use crate::Codec; use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; +#[derive(Default)] +struct ShopifyJsonFormatter; + +impl serde_json::ser::Formatter for ShopifyJsonFormatter { + fn write_string_fragment(&mut self, writer: &mut W, fragment: &str) -> io::Result<()> + where + W: ?Sized + io::Write, + { + let mut start = 0; + for (index, byte) in fragment.bytes().enumerate() { + if byte == b'/' { + writer.write_all(&fragment.as_bytes()[start..index])?; + writer.write_all(br"\/")?; + start = index + 1; + } + } + writer.write_all(&fragment.as_bytes()[start..]) + } +} + +fn to_shopify_json_vec(value: &serde_json::Value) -> Result> { + let mut bytes = Vec::new(); + let formatter = ShopifyJsonFormatter; + let mut serializer = serde_json::Serializer::with_formatter(&mut bytes, formatter); + value + .serialize(&mut serializer) + .map_err(|e| anyhow!("Couldn't serialize JSON: {}", e))?; + Ok(bytes) +} + #[derive(Debug, Clone, Default)] pub enum BytesContainerType { /// Input bytes. @@ -64,8 +96,7 @@ impl BytesContainer { BytesContainerType::Input => { let json = serde_json::from_slice::(&raw) .map_err(|e| anyhow!("Invalid input JSON: {}", e))?; - let minified_buffer = serde_json::to_vec(&json) - .map_err(|e| anyhow!("Couldn't serialize JSON: {}", e))?; + let minified_buffer = to_shopify_json_vec(&json)?; Ok(Self { codec, @@ -136,3 +167,36 @@ impl BytesContainer { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn json_input_escapes_solidus_like_shopify_json() -> Result<()> { + let input = br#"{"id":"gid://shopify/Product/1"}"#.to_vec(); + + let container = BytesContainer::new(BytesContainerType::Input, Codec::Json, input)?; + + assert_eq!( + String::from_utf8(container.raw).unwrap(), + r#"{"id":"gid:\/\/shopify\/Product\/1"}"# + ); + + Ok(()) + } + + #[test] + fn json_input_preserves_object_order() -> Result<()> { + let input = br#"{"b":1,"a":2}"#.to_vec(); + + let container = BytesContainer::new(BytesContainerType::Input, Codec::Json, input)?; + + assert_eq!( + String::from_utf8(container.raw).unwrap(), + r#"{"b":1,"a":2}"# + ); + + Ok(()) + } +} From c9db74074b9021436954bddc675bec43b0d9b135 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 14 Jul 2026 14:57:07 -0400 Subject: [PATCH 2/2] Include other separators --- src/container.rs | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/container.rs b/src/container.rs index 72f9b457..76922aec 100644 --- a/src/container.rs +++ b/src/container.rs @@ -13,11 +13,24 @@ impl serde_json::ser::Formatter for ShopifyJsonFormatter { W: ?Sized + io::Write, { let mut start = 0; - for (index, byte) in fragment.bytes().enumerate() { - if byte == b'/' { - writer.write_all(&fragment.as_bytes()[start..index])?; - writer.write_all(br"\/")?; - start = index + 1; + for (index, character) in fragment.char_indices() { + match character { + '/' => { + writer.write_all(&fragment.as_bytes()[start..index])?; + writer.write_all(br"\/")?; + start = index + character.len_utf8(); + } + '\u{2028}' => { + writer.write_all(&fragment.as_bytes()[start..index])?; + writer.write_all(br"\u2028")?; + start = index + character.len_utf8(); + } + '\u{2029}' => { + writer.write_all(&fragment.as_bytes()[start..index])?; + writer.write_all(br"\u2029")?; + start = index + character.len_utf8(); + } + _ => {} } } writer.write_all(&fragment.as_bytes()[start..]) @@ -186,6 +199,22 @@ mod tests { Ok(()) } + #[test] + fn json_input_escapes_line_and_paragraph_separators_like_shopify_json() -> Result<()> { + let input = "{\"line\":\"before\u{2028}after\",\"paragraph\":\"before\u{2029}after\"}" + .as_bytes() + .to_vec(); + + let container = BytesContainer::new(BytesContainerType::Input, Codec::Json, input)?; + + assert_eq!( + String::from_utf8(container.raw).unwrap(), + r#"{"line":"before\u2028after","paragraph":"before\u2029after"}"# + ); + + Ok(()) + } + #[test] fn json_input_preserves_object_order() -> Result<()> { let input = br#"{"b":1,"a":2}"#.to_vec();