1use std::fmt;
8use std::path::Path;
9
10use serde::de::Error as DeError;
11use serde::de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor};
12
13use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
14use crate::document::html::{HtmlBlock, render_blocks_to_pages};
15use crate::error::{Error, Result};
16
17const MAX_JSON_BYTES: u64 = 64 * 1024 * 1024;
18const MAX_JSON_DEPTH: usize = 100;
19const MAX_JSON_VALUES: usize = 200_000;
20const MAX_JSON_STRING_BYTES: usize = 2 * 1024 * 1024;
21const MAX_JSON_PATH_BYTES: usize = 4 * 1024;
22const MAX_JSON_RENDERED_TEXT_BYTES: usize = 32 * 1024 * 1024;
23const MAX_JSON_OUTPUT_BLOCKS: usize = 200_000;
24
25pub(crate) fn looks_like_json_prefix(prefix: &[u8]) -> bool {
26 let text = String::from_utf8_lossy(prefix);
27 let trimmed = text.trim_start_matches('\u{feff}').trim_start();
28 trimmed.starts_with('{') || trimmed.starts_with('[')
29}
30
31#[derive(Default)]
32struct PreviewState {
33 blocks: Vec<HtmlBlock>,
34 value_count: usize,
35 text_bytes: usize,
36 truncated_paths: usize,
37 normalized_number_count: usize,
38 limit_error: Option<String>,
39}
40
41pub(crate) fn convert(
42 path: &Path,
43 options: &ConvertOptions,
44 sink: &mut dyn PageConsumer,
45) -> Result<Vec<String>> {
46 let bytes = read_limited_file(
47 path,
48 options.max_input_bytes.min(MAX_JSON_BYTES),
49 "JSON input",
50 )?;
51 let text = String::from_utf8(bytes)
52 .map_err(|error| Error::InvalidInput(format!("JSON input must be UTF-8: {error}")))?;
53 let (blocks, warnings) = parse_json_blocks(&text)?;
54 let mut page_sink = JsonPageSink {
55 inner: sink,
56 warnings: &warnings,
57 };
58 render_blocks_to_pages(&blocks, &mut page_sink, options)?;
59 Ok(warnings)
60}
61
62struct JsonPageSink<'a> {
63 inner: &'a mut dyn PageConsumer,
64 warnings: &'a [String],
65}
66
67impl PageConsumer for JsonPageSink<'_> {
68 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
69 page.source_format = "json".into();
70 if page.title.is_empty() {
71 page.title = "JSON data".into();
72 }
73 for warning in self.warnings {
74 page.warn(warning.clone());
75 }
76 self.inner.consume(page)
77 }
78}
79
80pub(crate) fn parse_json_blocks(text: &str) -> Result<(Vec<HtmlBlock>, Vec<String>)> {
81 if text.len() as u64 > MAX_JSON_BYTES {
82 return Err(Error::LimitExceeded(format!(
83 "JSON input exceeds {MAX_JSON_BYTES} bytes"
84 )));
85 }
86 let mut state = PreviewState::default();
87 let mut deserializer = serde_json::Deserializer::from_str(text);
88 let parse_result = JsonSeed {
89 state: &mut state,
90 path: "$".into(),
91 depth: 0,
92 }
93 .deserialize(&mut deserializer);
94 if let Err(error) = parse_result {
95 if let Some(limit) = state.limit_error.take() {
96 return Err(Error::LimitExceeded(limit));
97 }
98 return Err(Error::InvalidInput(format!("invalid JSON input: {error}")));
99 }
100 deserializer
101 .end()
102 .map_err(|error| Error::InvalidInput(format!("invalid trailing JSON data: {error}")))?;
103
104 let mut blocks = vec![HtmlBlock::Heading {
105 level: 1,
106 text: "JSON data".into(),
107 }];
108 blocks.append(&mut state.blocks);
109 let mut warnings = Vec::new();
110 if state.truncated_paths > 0 {
111 warnings.push(format!(
112 "{0} JSON field path(s) were truncated to {1} bytes for display",
113 state.truncated_paths, MAX_JSON_PATH_BYTES
114 ));
115 }
116 if state.normalized_number_count > 0 {
117 warnings.push(format!(
118 "{} JSON decimal/exponent number(s) were normalized for display; original number spelling is not retained",
119 state.normalized_number_count
120 ));
121 }
122 Ok((blocks, warnings))
123}
124
125struct JsonSeed<'a> {
126 state: &'a mut PreviewState,
127 path: String,
128 depth: usize,
129}
130
131impl<'de> DeserializeSeed<'de> for JsonSeed<'_> {
132 type Value = ();
133
134 fn deserialize<D>(self, deserializer: D) -> std::result::Result<(), D::Error>
135 where
136 D: serde::Deserializer<'de>,
137 {
138 if self.depth > MAX_JSON_DEPTH {
139 return self
140 .state
141 .limit::<D::Error>(format!("JSON nesting exceeds {MAX_JSON_DEPTH} levels"));
142 }
143 self.state.value_count = self.state.value_count.saturating_add(1);
144 if self.state.value_count > MAX_JSON_VALUES {
145 return self
146 .state
147 .limit::<D::Error>(format!("JSON contains more than {MAX_JSON_VALUES} values"));
148 }
149 deserializer.deserialize_any(JsonVisitor {
150 state: self.state,
151 path: self.path,
152 depth: self.depth,
153 })
154 }
155}
156
157struct JsonVisitor<'a> {
158 state: &'a mut PreviewState,
159 path: String,
160 depth: usize,
161}
162
163impl<'de> Visitor<'de> for JsonVisitor<'_> {
164 type Value = ();
165
166 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167 formatter.write_str("a JSON value")
168 }
169
170 fn visit_bool<E>(self, value: bool) -> std::result::Result<(), E>
171 where
172 E: de::Error,
173 {
174 self.state
175 .add_scalar::<E>(&self.path, "boolean", if value { "true" } else { "false" })
176 }
177
178 fn visit_i64<E>(self, value: i64) -> std::result::Result<(), E>
179 where
180 E: de::Error,
181 {
182 self.state
183 .add_scalar::<E>(&self.path, "number", &value.to_string())
184 }
185
186 fn visit_u64<E>(self, value: u64) -> std::result::Result<(), E>
187 where
188 E: de::Error,
189 {
190 self.state
191 .add_scalar::<E>(&self.path, "number", &value.to_string())
192 }
193
194 fn visit_f64<E>(self, value: f64) -> std::result::Result<(), E>
195 where
196 E: de::Error,
197 {
198 self.state.normalized_number_count = self.state.normalized_number_count.saturating_add(1);
199 self.state
200 .add_scalar::<E>(&self.path, "number", &value.to_string())
201 }
202
203 fn visit_str<E>(self, value: &str) -> std::result::Result<(), E>
204 where
205 E: de::Error,
206 {
207 self.state.add_json_string::<E>(&self.path, value)
208 }
209
210 fn visit_string<E>(self, value: String) -> std::result::Result<(), E>
211 where
212 E: de::Error,
213 {
214 self.state.add_json_string::<E>(&self.path, &value)
215 }
216
217 fn visit_unit<E>(self) -> std::result::Result<(), E>
218 where
219 E: de::Error,
220 {
221 self.state.add_scalar::<E>(&self.path, "null", "null")
222 }
223
224 fn visit_none<E>(self) -> std::result::Result<(), E>
225 where
226 E: de::Error,
227 {
228 self.state.add_scalar::<E>(&self.path, "null", "null")
229 }
230
231 fn visit_some<D>(self, deserializer: D) -> std::result::Result<(), D::Error>
232 where
233 D: serde::Deserializer<'de>,
234 {
235 JsonSeed {
236 state: self.state,
237 path: self.path,
238 depth: self.depth,
239 }
240 .deserialize(deserializer)
241 }
242
243 fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<(), A::Error>
244 where
245 A: SeqAccess<'de>,
246 {
247 let mut index = 0usize;
248 while let Some(()) = sequence.next_element_seed(JsonSeed {
249 state: self.state,
250 path: format!("{}[{index}]", self.path),
251 depth: self.depth + 1,
252 })? {
253 index = index.saturating_add(1);
254 }
255 if index == 0 {
256 self.state
257 .add_scalar::<A::Error>(&self.path, "array", "[]")?;
258 }
259 Ok(())
260 }
261
262 fn visit_map<A>(self, mut object: A) -> std::result::Result<(), A::Error>
263 where
264 A: MapAccess<'de>,
265 {
266 let mut count = 0usize;
267 while let Some(key) = object.next_key::<String>()? {
268 let path = join_path(&self.path, &key, self.state).map_err(A::Error::custom)?;
269 object.next_value_seed(JsonSeed {
270 state: self.state,
271 path,
272 depth: self.depth + 1,
273 })?;
274 count = count.saturating_add(1);
275 }
276 if count == 0 {
277 self.state
278 .add_scalar::<A::Error>(&self.path, "object", "{}")?;
279 }
280 Ok(())
281 }
282}
283
284impl PreviewState {
285 fn limit<E: de::Error>(&mut self, message: String) -> std::result::Result<(), E> {
286 self.limit_error = Some(message.clone());
287 Err(E::custom(message))
288 }
289
290 fn add_scalar<E: de::Error>(
291 &mut self,
292 path: &str,
293 kind: &str,
294 value: &str,
295 ) -> std::result::Result<(), E> {
296 let line = format!("{path} ({kind}): {value}");
297 self.add_line::<E>(line)
298 }
299
300 fn add_json_string<E: de::Error>(
301 &mut self,
302 path: &str,
303 value: &str,
304 ) -> std::result::Result<(), E> {
305 if value.len() > MAX_JSON_STRING_BYTES {
306 return self.limit::<E>(format!("JSON string exceeds {MAX_JSON_STRING_BYTES} bytes"));
307 }
308 let encoded = serde_json::to_string(value).map_err(E::custom)?;
309 self.add_scalar::<E>(path, "string", &encoded)
310 }
311
312 fn add_line<E: de::Error>(&mut self, line: String) -> std::result::Result<(), E> {
313 if self.blocks.len() >= MAX_JSON_OUTPUT_BLOCKS {
314 return self.limit::<E>(format!(
315 "JSON preview exceeds {MAX_JSON_OUTPUT_BLOCKS} output values"
316 ));
317 }
318 let text_bytes = self.text_bytes.saturating_add(line.len());
319 if text_bytes > MAX_JSON_RENDERED_TEXT_BYTES {
320 return self.limit::<E>(format!(
321 "JSON preview text exceeds {MAX_JSON_RENDERED_TEXT_BYTES} bytes"
322 ));
323 }
324 self.text_bytes = text_bytes;
325 self.blocks.push(HtmlBlock::Paragraph { text: line });
326 Ok(())
327 }
328}
329
330fn join_path(
331 path: &str,
332 key: &str,
333 state: &mut PreviewState,
334) -> std::result::Result<String, &'static str> {
335 if key.len() > MAX_JSON_STRING_BYTES {
336 state.limit_error = Some(format!(
337 "JSON object key exceeds {MAX_JSON_STRING_BYTES} bytes"
338 ));
339 return Err("JSON object key exceeds the size limit");
340 }
341 let simple_key = !key.is_empty()
342 && key.chars().enumerate().all(|(index, character)| {
343 character == '_'
344 || character == '$'
345 || character.is_ascii_alphanumeric() && (index > 0 || !character.is_ascii_digit())
346 });
347 let child = if simple_key {
348 format!("{path}.{key}")
349 } else {
350 let encoded = serde_json::to_string(key).unwrap_or_else(|_| "\"?\"".into());
351 format!("{path}[{encoded}]")
352 };
353 if child.len() > MAX_JSON_PATH_BYTES {
354 state.truncated_paths = state.truncated_paths.saturating_add(1);
355 let mut end = MAX_JSON_PATH_BYTES;
356 while !child.is_char_boundary(end) {
357 end -= 1;
358 }
359 Ok(child[..end].to_owned())
360 } else {
361 Ok(child)
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn previews_nested_json_values_and_escapes_script_like_strings() {
371 let input = r#"{"service":{"enabled":true,"ports":[8080,8081]},"message":"<script>alert(1)</script>","empty":{}}"#;
372 let (blocks, _) = parse_json_blocks(input).unwrap();
373 let text = blocks
374 .iter()
375 .filter_map(|block| match block {
376 HtmlBlock::Heading { text, .. } | HtmlBlock::Paragraph { text } => {
377 Some(text.as_str())
378 }
379 _ => None,
380 })
381 .collect::<Vec<_>>();
382 assert!(text.contains(&"$.service.enabled (boolean): true"));
383 assert!(text.contains(&"$.service.ports[1] (number): 8081"));
384 assert!(text.contains(&"$.empty (object): {}"));
385 assert!(text.contains(&r#"$.message (string): "<script>alert(1)</script>""#));
386 }
387
388 #[test]
389 fn rejects_invalid_json_and_limits_depth() {
390 assert!(matches!(
391 parse_json_blocks("{\"key\":}"),
392 Err(Error::InvalidInput(_))
393 ));
394 let nested = format!(
395 "{}0{}",
396 "[".repeat(MAX_JSON_DEPTH + 2),
397 "]".repeat(MAX_JSON_DEPTH + 2)
398 );
399 assert!(matches!(
400 parse_json_blocks(&nested),
401 Err(Error::LimitExceeded(_))
402 ));
403 }
404
405 #[test]
406 fn warns_when_decimal_number_spelling_is_normalized() {
407 let (blocks, warnings) = parse_json_blocks("{\"ratio\":1.5000}").unwrap();
408 assert!(
409 warnings
410 .iter()
411 .any(|warning| warning.contains("number(s) were normalized"))
412 );
413 assert!(blocks.iter().any(|block| matches!(block, HtmlBlock::Paragraph { text } if text == "$.ratio (number): 1.5")));
414 }
415
416 #[test]
417 fn does_not_confuse_structured_json_with_known_json_formats() {
418 let config = br#"{"service":{"port":8080}}"#;
419 assert!(looks_like_json_prefix(config));
420 assert!(!looks_like_json_prefix(b"not json"));
421 }
422}