rsonpath_syntax/
str.rs

1//! JSON string types expressible in a JSONPath query.
2//!
3//! This may refer to a member name when used in a name selector,
4//! or a raw string value used for comparison or matching in a filter expression.
5
6/// String value or JSON member name, conforming to the
7/// [RFC7159, section 7](https://www.rfc-editor.org/rfc/rfc7159#section-7)
8///
9/// Represents the UTF-8 bytes defining a string key or value in a JSON object
10/// that can be matched against when executing a query.
11///
12/// # Examples
13///
14/// ```rust
15/// # use rsonpath_syntax::str::JsonString;
16/// let needle = JsonString::new("needle");
17///
18/// assert_eq!(needle.unquoted(), "needle");
19/// assert_eq!(needle.quoted(), "\"needle\"");
20/// ```
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[derive(Clone)]
23pub struct JsonString {
24    quoted: String,
25}
26
27#[derive(Debug)]
28pub(crate) struct JsonStringBuilder {
29    quoted: String,
30}
31
32impl std::fmt::Debug for JsonString {
33    #[inline]
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
35        write!(f, r#"JsonString({})"#, self.quoted)
36    }
37}
38
39impl JsonStringBuilder {
40    pub(crate) fn new() -> Self {
41        Self {
42            quoted: String::from('"'),
43        }
44    }
45
46    pub(crate) fn push(&mut self, char: char) -> &mut Self {
47        self.quoted.push(char);
48        self
49    }
50
51    pub(crate) fn finish(mut self) -> JsonString {
52        self.quoted.push('"');
53        JsonString { quoted: self.quoted }
54    }
55}
56
57impl From<JsonStringBuilder> for JsonString {
58    #[inline(always)]
59    fn from(value: JsonStringBuilder) -> Self {
60        value.finish()
61    }
62}
63
64impl From<&str> for JsonString {
65    #[inline(always)]
66    fn from(value: &str) -> Self {
67        Self::new(value)
68    }
69}
70
71/// Escape mode for the [`escape`] function.
72#[derive(Clone, Copy, PartialEq, Eq, Debug)]
73pub enum EscapeMode {
74    /// Treat the string as within single quotes `'`.
75    SingleQuoted,
76    /// Treat the string as withing double quotes `"`.
77    DoubleQuoted,
78}
79
80/// Escape a string according to JSONPath rules in a given quotation context.
81///
82/// ## Quotes
83///
84/// Processing quotes, `'` and `"`, depends on the `mode`:
85/// - in [`EscapeMode::SingleQuoted`], the string is escaped as if written in a single-quoted
86///   name selector `['<str>']`; single quotes are escaped as `\'`, double-quotes are copied as-is.
87/// - in [`EscapeMode::DoubleQuoted`], the string is escaped as if written in double-quotes,
88///   which is the same as a member name in a JSON document or a double-quoted name selector `["<str>"]`;
89///   double quotes are escaped as `\"`, single quotes are copied as-is.
90///
91/// ### Examples
92///
93/// ```rust
94/// # use rsonpath_syntax::str::{self, EscapeMode};
95/// let result_single = str::escape(r#"'rust' or "rust"\n"#, EscapeMode::SingleQuoted);
96/// let result_double = str::escape(r#"'rust' or "rust"\n"#, EscapeMode::DoubleQuoted);
97/// assert_eq!(result_single, r#"\'rust\' or "rust"\\n"#);
98/// assert_eq!(result_double, r#"'rust' or \"rust\"\\n"#);
99/// ```
100///
101/// ## Control characters
102///
103/// Control characters (U+0000 to U+001F) are escaped as special sequences
104/// where possible, e.g. Form Feed U+000C is escaped as `\f`.
105/// Other control sequences are escaped as a Unicode sequence, e.g.
106/// a null byte is escaped as `\u0000`.
107///
108/// ### Examples
109///
110/// ```rust
111/// # use rsonpath_syntax::str::{self, EscapeMode};
112/// let result = str::escape("\u{08}\u{09}\u{0A}\u{0B}\u{0C}\u{0D}", EscapeMode::DoubleQuoted);
113/// assert_eq!(result, r"\b\t\n\u000b\f\r");
114/// ```
115///
116/// ## Other
117///
118/// Characters that don't have to be escaped are not.
119///
120/// ### Examples
121///
122/// ```rust
123/// # use rsonpath_syntax::str::{self, EscapeMode};
124/// let result = str::escape("🦀", EscapeMode::DoubleQuoted);
125/// assert_eq!(result, "🦀");
126/// ```
127///
128/// Among other things, this means Unicode escapes are only produced
129/// for control characters.
130#[inline]
131#[must_use]
132pub fn escape(str: &str, mode: EscapeMode) -> String {
133    use std::fmt::Write;
134    let mut result = String::new();
135    for c in str.chars() {
136        match c {
137            // # Mode-dependent quote escapes.
138            '\'' if mode == EscapeMode::SingleQuoted => result.push_str(r"\'"),
139            '\'' if mode == EscapeMode::DoubleQuoted => result.push('\''),
140            '"' if mode == EscapeMode::SingleQuoted => result.push('"'),
141            '"' if mode == EscapeMode::DoubleQuoted => result.push_str(r#"\""#),
142            // # Mode-independent escapes.
143            '\\' => result.push_str(r"\\"),
144            // ## Special control sequences.
145            '\u{0008}' => result.push_str(r"\b"),
146            '\u{000C}' => result.push_str(r"\f"),
147            '\n' => result.push_str(r"\n"),
148            '\r' => result.push_str(r"\r"),
149            '\t' => result.push_str(r"\t"),
150            // ## Other control sequences escaped as Unicode escapes.
151            '\u{0000}'..='\u{001F}' => write!(result, "\\u{:0>4x}", c as u8).unwrap(),
152            // # Non-escapable characters.
153            _ => result.push(c),
154        }
155    }
156
157    result
158}
159
160impl JsonString {
161    /// Create a new JSON string from UTF8 input.
162    ///
163    /// # Examples
164    /// ```rust
165    /// # use rsonpath_syntax::str::JsonString;
166    /// let str = JsonString::new(r#"Stri\ng With \u00c9scapes \\n"#);
167    /// assert_eq!(str.unquoted(), r#"Stri\ng With \u00c9scapes \\n"#);
168    /// ```
169    #[inline]
170    #[must_use]
171    pub fn new(string: &str) -> Self {
172        let mut quoted = String::with_capacity(string.len() + 2);
173        quoted.push('"');
174        quoted += string;
175        quoted.push('"');
176        Self { quoted }
177    }
178
179    /// Return the contents of the string.
180    /// # Examples
181    /// ```rust
182    /// # use rsonpath_syntax::str::JsonString;
183    /// let needle = JsonString::new(r#"Stri\ng With \u00c9scapes \\n"#);
184    /// assert_eq!(needle.unquoted(), r#"Stri\ng With \u00c9scapes \\n"#);
185    /// ```
186    #[must_use]
187    #[inline(always)]
188    pub fn unquoted(&self) -> &str {
189        let len = self.quoted.len();
190        debug_assert!(len >= 2);
191        &self.quoted[1..len - 1]
192    }
193
194    /// Return the contents of the string with the leading and trailing
195    /// double quote symbol `"`.
196    /// # Examples
197    /// ```rust
198    /// # use rsonpath_syntax::str::JsonString;
199    /// let needle = JsonString::new(r#"Stri\ng With \u00c9scapes \\n"#);
200    /// assert_eq!(needle.quoted(), r#""Stri\ng With \u00c9scapes \\n""#);
201    /// ```
202    #[must_use]
203    #[inline(always)]
204    pub fn quoted(&self) -> &str {
205        &self.quoted
206    }
207}
208
209impl PartialEq<Self> for JsonString {
210    #[inline(always)]
211    fn eq(&self, other: &Self) -> bool {
212        self.unquoted() == other.unquoted()
213    }
214}
215
216impl Eq for JsonString {}
217
218impl std::hash::Hash for JsonString {
219    #[inline(always)]
220    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
221        self.unquoted().hash(state);
222    }
223}
224
225#[cfg(feature = "arbitrary")]
226#[cfg_attr(docsrs, doc(cfg(feature = "arbitrary")))]
227impl<'a> arbitrary::Arbitrary<'a> for JsonString {
228    #[inline]
229    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
230        let chars = u.arbitrary_iter()?;
231        let mut builder = JsonStringBuilder::new();
232
233        // RFC 7159: All Unicode characters may be placed [in the string],
234        // except for characters that must be escaped: quotation mark,
235        // reverse solidus, and the control characters (U+0000 through U+001F).
236        for c in chars {
237            let c = c?;
238            match c {
239                '\u{0000}'..='\u{001F}' | '\"' | '\\' => {
240                    builder.push('\\');
241                    builder.push(c);
242                }
243                _ => {
244                    builder.push(c);
245                }
246            }
247        }
248
249        Ok(builder.into())
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use pretty_assertions::{assert_eq, assert_ne};
257    use std::{
258        collections::hash_map::DefaultHasher,
259        hash::{Hash, Hasher},
260    };
261    use test_case::test_case;
262
263    #[test_case("dog", "dog"; "dog")]
264    #[test_case("", ""; "empty")]
265    fn equal_json_strings_are_equal(s1: &str, s2: &str) {
266        let string1 = JsonString::new(s1);
267        let string2 = JsonString::new(s2);
268
269        assert_eq!(string1, string2);
270    }
271
272    #[test]
273    fn different_json_strings_are_not_equal() {
274        let string1 = JsonString::new("dog");
275        let string2 = JsonString::new("doc");
276
277        assert_ne!(string1, string2);
278    }
279
280    #[test_case("dog", "dog"; "dog")]
281    #[test_case("", ""; "empty")]
282    fn equal_json_strings_have_equal_hashes(s1: &str, s2: &str) {
283        let string1 = JsonString::new(s1);
284        let string2 = JsonString::new(s2);
285
286        let mut hasher1 = DefaultHasher::new();
287        string1.hash(&mut hasher1);
288        let hash1 = hasher1.finish();
289
290        let mut hasher2 = DefaultHasher::new();
291        string2.hash(&mut hasher2);
292        let hash2 = hasher2.finish();
293
294        assert_eq!(hash1, hash2);
295    }
296}