Skip to main content

simdjson_rust/
dom.rs

1//! High-level DOM parser wrapping `simdjson_sys`.
2//!
3//! This module provides a safe, ergonomic Rust API over the simdjson DOM
4//! parser. After parsing, the result is available as a zero-copy
5//! [`Value`](crate::tape::Value), or as a raw
6//! [`TapeRef`] for custom traversal.
7
8use simdjson_sys::{SIMDJSON_PADDING, dom_ffi};
9use snafu::prelude::*;
10
11use crate::{error::SimdJsonError, tape::TapeRef};
12
13/// The error type returned by the DOM parser.
14#[derive(Debug, Snafu)]
15#[snafu(visibility(pub(crate)))]
16pub enum Error {
17    #[snafu(display("simdjson error: {source}"))]
18    SimdJson { source: SimdJsonError },
19
20    #[snafu(display(
21        "the JSON input buffer must have at least {} bytes of extra capacity",
22        SIMDJSON_PADDING
23    ))]
24    InsufficientPadding,
25
26    #[snafu(display("tape error: {source}"))]
27    Tape {
28        #[snafu(backtrace)]
29        source: crate::tape::Error,
30    },
31}
32
33pub type Result<T, E = Error> = std::result::Result<T, E>;
34
35impl From<crate::tape::Error> for Error {
36    fn from(source: crate::tape::Error) -> Self {
37        Error::Tape { source }
38    }
39}
40
41/// A simdjson DOM parser.
42///
43/// The parser owns an internal buffer. After calling one of the `parse*`
44/// methods the tape and string buffer remain valid until the next parse call.
45///
46/// # Example
47///
48/// ```rust
49/// use simdjson_rust::dom::Parser;
50///
51/// let mut parser = Parser::default();
52/// let value = parser.parse_str(r#"{"hello": "world"}"#).unwrap();
53/// println!("{value:?}");
54/// ```
55pub struct Parser {
56    inner: cxx::UniquePtr<dom_ffi::parser>,
57    /// Reuse the padded buffer to avoid reallocation on every parse.
58    padded: Vec<u8>,
59    valid: bool,
60}
61
62impl Default for Parser {
63    fn default() -> Self {
64        Self::new(simdjson_sys::SIMDJSON_MAXSIZE_BYTES)
65    }
66}
67
68impl Parser {
69    /// Create a parser that can handle documents up to `max_capacity` bytes.
70    pub fn new(max_capacity: usize) -> Self {
71        Self {
72            inner: dom_ffi::parser_new(max_capacity),
73            padded: Vec::new(),
74            valid: false,
75        }
76    }
77
78    /// Parse a JSON byte slice.
79    ///
80    /// The slice must have at least [`SIMDJSON_PADDING`] bytes of extra
81    /// capacity beyond its length. Use [`parse_str`](Self::parse_str) or
82    /// [`parse_padded`](Self::parse_padded) for convenience variants that
83    /// handle padding automatically.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`Error::SimdJson`] if the parser rejects the input.
88    /// # Safety
89    ///
90    /// The allocation must contain at least `SIMDJSON_PADDING` initialized,
91    /// readable bytes after the slice, valid throughout this call.
92    pub unsafe fn parse_bytes_with_padding(&mut self, json: &[u8]) -> Result<TapeRef<'_>> {
93        self.valid = false;
94        // SAFETY: guaranteed by the caller.
95        let code = unsafe { dom_ffi::parser_parse(self.inner.pin_mut(), json, false) };
96        SimdJsonError::check_code(code).context(SimdJsonSnafu)?;
97        self.valid = true;
98        Ok(self.tape_ref())
99    }
100
101    /// Parse a JSON string, automatically adding the required padding.
102    ///
103    /// The padding is added on a private copy of the string, so the original
104    /// `json` argument does not need to be mutable.
105    pub fn parse_str(&mut self, json: &str) -> Result<TapeRef<'_>> {
106        self.parse_bytes(json.as_bytes())
107    }
108
109    /// Parse a JSON byte slice, automatically adding the required padding.
110    pub fn parse_bytes(&mut self, json: &[u8]) -> Result<TapeRef<'_>> {
111        // simdjson reads up to SIMDJSON_PADDING bytes past the end of the
112        // slice pointer when realloc_if_needed=false. We allocate a buffer
113        // with that extra space, copy the JSON into it, zero the tail, then
114        // pass a slice of the original length. simdjson uses the slice length
115        // as the document length but reads the zero-padded tail unsafely.
116        self.valid = false;
117        let json_len = json.len();
118        self.padded.clear();
119        self.padded.reserve(json_len + SIMDJSON_PADDING);
120        self.padded.extend_from_slice(json);
121        // Zero-fill the SIMDJSON_PADDING extra bytes that simdjson will read.
122        self.padded
123            .extend(std::iter::repeat_n(0u8, SIMDJSON_PADDING));
124
125        // Pass only the JSON length; the zero tail is read past the slice end.
126        // SAFETY: the allocation includes the initialized zero tail above.
127        let code =
128            unsafe { dom_ffi::parser_parse(self.inner.pin_mut(), &self.padded[..json_len], false) };
129        SimdJsonError::check_code(code).context(SimdJsonSnafu)?;
130        self.valid = true;
131        Ok(self.tape_ref())
132    }
133
134    /// Parse a JSON string that is already padded (has `>= SIMDJSON_PADDING`
135    /// bytes of extra space in its allocation).
136    ///
137    /// The string must have `s.capacity() >= s.len() + SIMDJSON_PADDING`.
138    pub fn parse_padded(&mut self, json: &mut String) -> Result<TapeRef<'_>> {
139        self.valid = false;
140        ensure!(
141            json.capacity() - json.len() >= SIMDJSON_PADDING,
142            InsufficientPaddingSnafu
143        );
144        let len = json.len();
145        json.extend(std::iter::repeat_n('\0', SIMDJSON_PADDING));
146        json.truncate(len);
147        // SAFETY: initialized padding remains allocated after truncation.
148        let code = unsafe { dom_ffi::parser_parse(self.inner.pin_mut(), json.as_bytes(), false) };
149        SimdJsonError::check_code(code).context(SimdJsonSnafu)?;
150        self.valid = true;
151        Ok(self.tape_ref())
152    }
153
154    /// Returns a reference to the current tape.
155    ///
156    /// Returns an empty tape before parsing or after a failed parse.
157    pub fn tape_ref(&self) -> TapeRef<'_> {
158        if !self.valid {
159            return TapeRef::new(&[], &[]);
160        }
161        // SAFETY: the last parse succeeded and the parser has not been mutated.
162        let view = unsafe {
163            dom_ffi::parser_get_tape_view(self.inner.as_ref().expect("parser must not be null"))
164        };
165        TapeRef::new(view.tape, view.string_buf)
166    }
167
168    /// Parse a JSON string and return a zero-copy `Value`.
169    pub fn parse_to_value(&mut self, json: &str) -> Result<crate::tape::Value<'_>> {
170        let mut tape = self.parse_str(json)?;
171        Ok(tape.parse_value()?)
172    }
173
174    /// Parse a JSON byte slice and return a zero-copy `Value`.
175    pub fn parse_bytes_to_value(&mut self, json: &[u8]) -> Result<crate::tape::Value<'_>> {
176        let mut tape = self.parse_bytes(json)?;
177        Ok(tape.parse_value()?)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn parse_null_tape() -> Result<()> {
187        let mut p = Parser::default();
188        let tape = p.parse_str("null")?;
189        // First word is always the root 'r' node
190        assert_eq!(tape.tape_type(), Some(crate::tape::TapeType::Root));
191        Ok(())
192    }
193
194    #[test]
195    fn parse_object_tape() -> Result<()> {
196        let mut p = Parser::default();
197        let tape = p.parse_str(r#"{"answer":42}"#)?;
198        // First word is root 'r'
199        assert_eq!(tape.tape_type(), Some(crate::tape::TapeType::Root));
200        Ok(())
201    }
202
203    #[test]
204    fn parse_to_value_works() -> Result<()> {
205        let mut p = Parser::default();
206        let v = p.parse_to_value(r#"{"answer":42,"ok":true,"name":"rust"}"#)?;
207        assert_eq!(v.get("answer").unwrap().as_i64(), Some(42));
208        assert_eq!(v.get("ok").unwrap().as_bool(), Some(true));
209        assert_eq!(v.get("name").unwrap().as_str(), Some("rust"));
210        Ok(())
211    }
212}