Skip to main content

document_svg/document/
jsonmergepatch.rs

1//! Bounded RFC 7396 JSON Merge Patch previews.
2//!
3//! Merge Patch documents mimic a target JSON object: non-null members replace
4//! or merge values and null members remove them. This adapter renders affected
5//! paths, actions and value types only; it never applies a patch or displays
6//! value payloads.
7
8use serde_json::Value;
9use std::path::Path;
10
11use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
12use crate::document::html::{HtmlBlock, render_blocks_to_pages};
13use crate::error::{Error, Result};
14use crate::table::{TableAlign, TableData};
15
16const MAX_MERGEPATCH_BYTES: u64 = 64 * 1024 * 1024;
17const MAX_MERGEPATCH_DEPTH: usize = 100;
18const MAX_MERGEPATCH_VALUES: usize = 300_000;
19const MAX_MERGEPATCH_OPERATIONS: usize = 200_000;
20const MAX_MERGEPATCH_STRING_BYTES: usize = 2 * 1024 * 1024;
21
22struct MergePatchPageSink<'a> {
23    inner: &'a mut dyn PageConsumer,
24    warnings: &'a [String],
25}
26
27impl PageConsumer for MergePatchPageSink<'_> {
28    fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
29        page.source_format = "jsonmergepatch".into();
30        if page.title.is_empty() {
31            page.title = "JSON Merge Patch".into();
32        }
33        page.description =
34            "RFC 7396 JSON Merge Patch paths are rendered as inert metadata; target documents and values are not modified or displayed".into();
35        for warning in self.warnings {
36            page.warn(warning.clone());
37        }
38        self.inner.consume(page)
39    }
40}
41
42pub(crate) fn convert(
43    path: &Path,
44    options: &ConvertOptions,
45    sink: &mut dyn PageConsumer,
46) -> Result<Vec<String>> {
47    let bytes = read_limited_file(
48        path,
49        options.max_input_bytes.min(MAX_MERGEPATCH_BYTES),
50        "JSON Merge Patch input",
51    )?;
52    let text = String::from_utf8(bytes).map_err(|error| {
53        Error::InvalidInput(format!("JSON Merge Patch must be UTF-8 JSON: {error}"))
54    })?;
55    let (table, metadata, warnings) = parse(&text)?;
56    let blocks = vec![
57        HtmlBlock::Heading {
58            level: 1,
59            text: "JSON Merge Patch".into(),
60        },
61        HtmlBlock::Paragraph { text: metadata },
62        HtmlBlock::Table(table),
63    ];
64    let mut page_sink = MergePatchPageSink {
65        inner: sink,
66        warnings: &warnings,
67    };
68    render_blocks_to_pages(&blocks, &mut page_sink, options)?;
69    Ok(warnings)
70}
71
72#[derive(Default)]
73struct Summary {
74    rows: Vec<Vec<String>>,
75    set: usize,
76    delete: usize,
77    merge: usize,
78}
79
80fn parse(text: &str) -> Result<(TableData, String, Vec<String>)> {
81    if text.len() as u64 > MAX_MERGEPATCH_BYTES {
82        return Err(Error::LimitExceeded(format!(
83            "JSON Merge Patch exceeds {MAX_MERGEPATCH_BYTES} bytes"
84        )));
85    }
86    preflight_depth(text)?;
87    let value: Value = serde_json::from_str(text)
88        .map_err(|error| Error::InvalidInput(format!("invalid JSON Merge Patch: {error}")))?;
89    let mut values = 0usize;
90    count_values(&value, 0, &mut values)?;
91    let mut summary = Summary::default();
92    collect_paths(&value, "/", 0, &mut summary)?;
93    if summary.rows.is_empty() {
94        summary.rows.push(vec![
95            "/".into(),
96            "(empty patch)".into(),
97            "object".into(),
98            "0".into(),
99        ]);
100    }
101    let metadata = format!(
102        "Paths: {}\nSet/replace: {}\nDelete: {}\nNested merge: {}",
103        summary.rows.len(),
104        summary.set,
105        summary.delete,
106        summary.merge
107    );
108    let warnings = vec![
109        "JSON Merge Patch values are omitted; patches are never applied, target documents and JSON Pointers are not evaluated, and no external resource is opened".into(),
110        "RFC 7396 null deletion and object merge semantics remain inert metadata; arrays and scalar roots are summarized as replacements".into(),
111    ];
112    Ok((
113        TableData {
114            headers: vec![
115                "Path".into(),
116                "Action".into(),
117                "Value type".into(),
118                "Depth".into(),
119            ],
120            rows: summary.rows,
121            alignments: vec![TableAlign::Left; 4],
122            raw_source: String::new(),
123        },
124        metadata,
125        warnings,
126    ))
127}
128
129fn collect_paths(value: &Value, path: &str, depth: usize, summary: &mut Summary) -> Result<()> {
130    if depth > MAX_MERGEPATCH_DEPTH {
131        return Err(Error::LimitExceeded(format!(
132            "JSON Merge Patch nesting exceeds {MAX_MERGEPATCH_DEPTH} levels"
133        )));
134    }
135    match value {
136        Value::Object(map) => {
137            for (key, child) in map {
138                if summary.rows.len() >= MAX_MERGEPATCH_OPERATIONS {
139                    return Err(Error::LimitExceeded(format!(
140                        "JSON Merge Patch paths exceed {MAX_MERGEPATCH_OPERATIONS}"
141                    )));
142                }
143                let child_path = format!("{}{}", path.trim_end_matches('/'), pointer_segment(key));
144                if child.is_null() {
145                    summary.delete = summary.delete.saturating_add(1);
146                    summary.rows.push(vec![
147                        truncate(&child_path),
148                        "delete".into(),
149                        "null".into(),
150                        (depth + 1).to_string(),
151                    ]);
152                } else if child.is_object() {
153                    summary.merge = summary.merge.saturating_add(1);
154                    summary.rows.push(vec![
155                        truncate(&child_path),
156                        "merge".into(),
157                        "object".into(),
158                        (depth + 1).to_string(),
159                    ]);
160                    collect_paths(child, &child_path, depth + 1, summary)?;
161                } else {
162                    summary.set = summary.set.saturating_add(1);
163                    summary.rows.push(vec![
164                        truncate(&child_path),
165                        "set".into(),
166                        json_type(child).into(),
167                        (depth + 1).to_string(),
168                    ]);
169                }
170            }
171        }
172        _ => {
173            summary.set = summary.set.saturating_add(1);
174            summary.rows.push(vec![
175                truncate(path),
176                "set".into(),
177                json_type(value).into(),
178                depth.to_string(),
179            ]);
180        }
181    }
182    Ok(())
183}
184
185fn pointer_segment(value: &str) -> String {
186    format!("/{}", value.replace('~', "~0").replace('/', "~1"))
187}
188
189fn json_type(value: &Value) -> &'static str {
190    match value {
191        Value::Null => "null",
192        Value::Bool(_) => "boolean",
193        Value::Number(_) => "number",
194        Value::String(_) => "string",
195        Value::Array(_) => "array",
196        Value::Object(_) => "object",
197    }
198}
199
200fn truncate(value: &str) -> String {
201    if value.len() <= MAX_MERGEPATCH_STRING_BYTES {
202        return value.to_owned();
203    }
204    let mut end = MAX_MERGEPATCH_STRING_BYTES;
205    while !value.is_char_boundary(end) {
206        end -= 1;
207    }
208    format!("{}…", &value[..end])
209}
210
211fn count_values(value: &Value, depth: usize, count: &mut usize) -> Result<()> {
212    if depth > MAX_MERGEPATCH_DEPTH {
213        return Err(Error::LimitExceeded(format!(
214            "JSON Merge Patch nesting exceeds {MAX_MERGEPATCH_DEPTH} levels"
215        )));
216    }
217    *count = count.saturating_add(1);
218    if *count > MAX_MERGEPATCH_VALUES {
219        return Err(Error::LimitExceeded(format!(
220            "JSON Merge Patch contains more than {MAX_MERGEPATCH_VALUES} values"
221        )));
222    }
223    match value {
224        Value::Array(values) => {
225            for item in values {
226                count_values(item, depth + 1, count)?;
227            }
228        }
229        Value::Object(map) => {
230            for item in map.values() {
231                count_values(item, depth + 1, count)?;
232            }
233        }
234        Value::String(value) if value.len() > MAX_MERGEPATCH_STRING_BYTES => {
235            return Err(Error::LimitExceeded(format!(
236                "JSON Merge Patch string exceeds {MAX_MERGEPATCH_STRING_BYTES} bytes"
237            )));
238        }
239        _ => {}
240    }
241    Ok(())
242}
243
244fn preflight_depth(text: &str) -> Result<()> {
245    let mut depth = 0usize;
246    let mut quoted = false;
247    let mut escaped = false;
248    for byte in text.bytes() {
249        if quoted {
250            if escaped {
251                escaped = false;
252            } else if byte == b'\\' {
253                escaped = true;
254            } else if byte == b'"' {
255                quoted = false;
256            }
257            continue;
258        }
259        match byte {
260            b'"' => quoted = true,
261            b'{' | b'[' => {
262                depth += 1;
263                if depth > MAX_MERGEPATCH_DEPTH {
264                    return Err(Error::LimitExceeded(format!(
265                        "JSON Merge Patch nesting exceeds {MAX_MERGEPATCH_DEPTH} levels"
266                    )));
267                }
268            }
269            b'}' | b']' => depth = depth.saturating_sub(1),
270            _ => {}
271        }
272    }
273    Ok(())
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn summarizes_merge_patch_without_values() {
282        let (table, metadata, warnings) = parse(
283            r#"{"title":"Hello","author":{"familyName":null,"givenName":"John"},"tags":["example"],"token":"very-secret"}"#,
284        )
285        .unwrap();
286        assert!(metadata.contains("Paths: 6"));
287        assert!(table.rows.iter().any(|row| row[1] == "set"));
288        assert!(table.rows.iter().any(|row| row[1] == "delete"));
289        assert!(table.rows.iter().any(|row| row[1] == "merge"));
290        assert!(
291            !table
292                .rows
293                .iter()
294                .flatten()
295                .any(|value| value.contains("secret"))
296        );
297        assert!(warnings.iter().any(|warning| warning.contains("never")));
298    }
299}