1use 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}