1use crate::model::{
23 sort_keys_owned, Container, Invariant, Island, Line, LineKind, Loss, Mark,
24 MarkKind, Content, Usv,
25};
26use serde_json::{Map, Value};
27use std::borrow::Cow;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum ParseError {
34 Shape(&'static str),
36 Json(String),
38 Invalid(crate::model::Invariant),
40}
41
42impl std::fmt::Display for ParseError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 ParseError::Shape(s) => write!(f, "content json shape: {s}"),
46 ParseError::Json(s) => write!(f, "content json parse: {s}"),
47 ParseError::Invalid(inv) => write!(f, "content invariant: {inv:?}"),
48 }
49 }
50}
51impl std::error::Error for ParseError {}
52
53impl Content {
54 pub fn to_canonical_json(&self) -> String {
60 to_canonical_value(self).to_string()
61 }
62
63 pub fn from_canonical_json(s: &str) -> Result<Content, ParseError> {
69 let v: Value = serde_json::from_str(s).map_err(|e| ParseError::Json(e.to_string()))?;
70 from_canonical_value(&v)
71 }
72
73 fn to_value(&self) -> Value {
74 let mut root = Map::new();
75 root.insert("text".into(), Value::String(self.text.clone()));
76 root.insert(
77 "lines".into(),
78 Value::Array(self.lines.iter().map(line_to_value).collect()),
79 );
80 root.insert(
81 "marks".into(),
82 Value::Array(self.marks.iter().map(mark_to_value).collect()),
83 );
84 root.insert(
85 "islands".into(),
86 Value::Array(self.islands.iter().map(island_to_value).collect()),
87 );
88 Value::Object(root)
89 }
90
91 fn from_value(v: &Value) -> Result<Content, ParseError> {
92 let obj = v.as_object().ok_or(ParseError::Shape("root not object"))?;
93 let text = obj
94 .get("text")
95 .and_then(Value::as_str)
96 .ok_or(ParseError::Shape("text"))?
97 .to_string();
98 let lines = arr(obj, "lines")?
99 .iter()
100 .map(line_from_value)
101 .collect::<Result<_, _>>()?;
102 let marks = arr(obj, "marks")?
103 .iter()
104 .map(mark_from_value)
105 .collect::<Result<_, _>>()?;
106 let islands = arr(obj, "islands")?
107 .iter()
108 .map(island_from_value)
109 .collect::<Result<_, _>>()?;
110 Ok(Content {
111 text,
112 lines,
113 marks,
114 islands,
115 })
116 }
117}
118
119pub fn to_canonical_value(rt: &Content) -> Value {
127 let mut rt = rt.clone();
128 rt.normalize();
129 sort_keys_owned(rt.to_value())
130}
131
132pub fn from_canonical_value(v: &Value) -> Result<Content, ParseError> {
138 let mut rt = Content::from_value(v)?;
139 rt.normalize();
140 rt.validate().map_err(ParseError::Invalid)?;
141 Ok(rt)
142}
143
144fn bag_from_wire(
155 o: &Map<String, Value>,
156 key: &'static str,
157 what: &'static str,
158) -> Result<Value, ParseError> {
159 let Some(v) = o.get(key) else {
160 return Ok(Value::Null);
161 };
162 crate::model::check_json_depth(v, what).map_err(ParseError::Invalid)?;
163 Ok(v.clone())
164}
165
166pub(crate) fn usv_from(v: Option<&Value>, what: &'static str) -> Result<Usv, ParseError> {
171 let n = v.and_then(Value::as_u64).ok_or(ParseError::Shape(what))?;
172 Usv::try_from(n).map_err(|_| ParseError::Shape(what))
173}
174
175fn arr<'a>(obj: &'a Map<String, Value>, key: &'static str) -> Result<&'a Vec<Value>, ParseError> {
176 obj.get(key)
177 .and_then(Value::as_array)
178 .ok_or(ParseError::Shape(key))
179}
180
181fn as_slice(v: &Value) -> &[Value] {
185 v.as_array().map(Vec::as_slice).unwrap_or_default()
186}
187
188fn arr_or_empty<'a>(v: &'a Value, key: &str) -> &'a [Value] {
190 v.get(key).map(as_slice).unwrap_or_default()
191}
192
193fn fold_legacy_attrs<'a>(
215 o: &'a Map<String, Value>,
216 tag: &str,
217 reserved: &[&str],
218 what: &'static str,
219) -> Result<Cow<'a, Map<String, Value>>, ParseError> {
220 let Some(bag @ Value::Object(attrs)) = o.get("attrs") else {
221 return Ok(Cow::Borrowed(o));
222 };
223 if attrs.is_empty() || !reserved.contains(&tag) {
224 return Ok(Cow::Borrowed(o));
225 }
226 crate::model::check_json_depth(bag, what).map_err(ParseError::Invalid)?;
227 let mut folded = o.clone();
228 for (k, v) in attrs {
229 folded.entry(k.clone()).or_insert_with(|| v.clone());
230 }
231 Ok(Cow::Owned(folded))
232}
233
234pub fn line_kind_to_value(kind: &LineKind) -> Value {
241 let mut m = Map::new();
242 match kind {
243 LineKind::Para => {
244 m.insert("kind".into(), "para".into());
245 }
246 LineKind::Heading { level } => {
247 m.insert("kind".into(), "heading".into());
248 m.insert("level".into(), Value::from(*level));
249 }
250 LineKind::Code { lang } => {
251 m.insert("kind".into(), "code".into());
252 if let Some(l) = lang {
253 m.insert("lang".into(), Value::String(l.clone()));
254 }
255 }
256 LineKind::Island => {
257 m.insert("kind".into(), "island".into());
258 }
259 LineKind::Rule => {
260 m.insert("kind".into(), "rule".into());
261 }
262 LineKind::Unknown { tag, attrs } => {
266 m.insert("kind".into(), Value::String(tag.clone()));
267 m.insert("attrs".into(), attrs.clone());
268 }
269 }
270 Value::Object(m)
271}
272
273pub fn line_kind_from_value(v: &Value) -> Result<LineKind, ParseError> {
277 let o = v.as_object().ok_or(ParseError::Shape("line"))?;
278 let tag = o
281 .get("kind")
282 .and_then(Value::as_str)
283 .ok_or(ParseError::Shape("line kind"))?;
284 let o = fold_legacy_attrs(o, tag, Content::RESERVED_LINE_KINDS, "line attrs")?;
285 match tag {
286 "para" => Ok(LineKind::Para),
287 "heading" => {
288 let level = o
289 .get("level")
290 .and_then(Value::as_u64)
291 .ok_or(ParseError::Shape("heading level"))?;
292 if !(1..=6).contains(&level) {
293 return Err(ParseError::Shape("heading level"));
294 }
295 Ok(LineKind::Heading { level: level as u8 })
296 }
297 "code" => Ok(LineKind::Code {
298 lang: o.get("lang").and_then(Value::as_str).map(str::to_string),
299 }),
300 "island" => Ok(LineKind::Island),
301 "rule" => Ok(LineKind::Rule),
302 other => Ok(LineKind::Unknown {
306 tag: other.to_string(),
307 attrs: bag_from_wire(&o, "attrs", "line attrs")?,
308 }),
309 }
310}
311
312fn line_to_value(line: &Line) -> Value {
313 let Value::Object(mut m) = line_kind_to_value(&line.kind) else {
314 unreachable!("line_kind_to_value always returns an object")
315 };
316 m.insert(
317 "containers".into(),
318 Value::Array(line.containers.iter().map(container_to_value).collect()),
319 );
320 if line.continues {
323 m.insert("continues".into(), Value::Bool(true));
324 }
325 Value::Object(m)
326}
327
328fn line_from_value(v: &Value) -> Result<Line, ParseError> {
329 let o = v.as_object().ok_or(ParseError::Shape("line"))?;
330 let kind = line_kind_from_value(v)?;
331 let containers = o
332 .get("containers")
333 .and_then(Value::as_array)
334 .ok_or(ParseError::Shape("containers"))?
335 .iter()
336 .map(container_from_value)
337 .collect::<Result<_, _>>()?;
338 let continues = o.get("continues").and_then(Value::as_bool).unwrap_or(false);
339 Ok(Line {
340 kind,
341 containers,
342 continues,
343 })
344}
345
346pub fn container_to_value(c: &Container) -> Value {
350 let mut m = Map::new();
351 match c {
352 Container::ListItem {
353 ordered,
354 start,
355 ordinal,
356 } => {
357 m.insert("container".into(), "list_item".into());
358 m.insert("ordered".into(), Value::Bool(*ordered));
359 m.insert("start".into(), Value::from(*start));
360 m.insert("ordinal".into(), Value::from(*ordinal));
361 }
362 Container::Quote => {
363 m.insert("container".into(), "quote".into());
364 }
365 Container::Unknown { tag, attrs } => {
366 m.insert("container".into(), Value::String(tag.clone()));
367 m.insert("attrs".into(), attrs.clone());
368 }
369 }
370 Value::Object(m)
371}
372
373pub fn container_from_value(v: &Value) -> Result<Container, ParseError> {
376 let o = v.as_object().ok_or(ParseError::Shape("container"))?;
377 let tag = o
378 .get("container")
379 .and_then(Value::as_str)
380 .ok_or(ParseError::Shape("container kind"))?;
381 let o = fold_legacy_attrs(o, tag, Content::RESERVED_CONTAINERS, "container attrs")?;
382 match tag {
383 "list_item" => Ok(Container::ListItem {
384 ordered: o.get("ordered").and_then(Value::as_bool).unwrap_or(false),
385 start: o.get("start").and_then(Value::as_u64).unwrap_or(1),
386 ordinal: o.get("ordinal").and_then(Value::as_u64).unwrap_or(0),
387 }),
388 "quote" => Ok(Container::Quote),
389 other => Ok(Container::Unknown {
392 tag: other.to_string(),
393 attrs: bag_from_wire(&o, "attrs", "container attrs")?,
394 }),
395 }
396}
397
398pub fn mark_to_value(mark: &Mark) -> Value {
404 let mut m = Map::new();
405 m.insert("start".into(), Value::from(mark.start));
406 m.insert("end".into(), Value::from(mark.end));
407 match &mark.kind {
408 MarkKind::Strong => {
409 m.insert("type".into(), "strong".into());
410 }
411 MarkKind::Emph => {
412 m.insert("type".into(), "emph".into());
413 }
414 MarkKind::Underline => {
415 m.insert("type".into(), "underline".into());
416 }
417 MarkKind::Strike => {
418 m.insert("type".into(), "strike".into());
419 }
420 MarkKind::Code => {
421 m.insert("type".into(), "code".into());
422 }
423 MarkKind::Link { url } => {
424 m.insert("type".into(), "link".into());
425 m.insert("url".into(), Value::String(url.clone()));
426 }
427 MarkKind::Anchor { id } => {
428 m.insert("type".into(), "anchor".into());
429 m.insert("id".into(), Value::String(id.clone()));
430 }
431 MarkKind::Unknown { tag, attrs } => {
432 m.insert("type".into(), Value::String(tag.clone()));
433 m.insert("attrs".into(), attrs.clone());
434 }
435 }
436 Value::Object(m)
437}
438
439struct MarkShape<'a> {
442 fields: &'a Map<String, Value>,
443 start: Usv,
444 end: Usv,
445 ty: &'a str,
446}
447
448fn mark_shape(v: &Value) -> Result<MarkShape<'_>, ParseError> {
455 let fields = v.as_object().ok_or(ParseError::Shape("mark"))?;
456 let start = usv_from(fields.get("start"), "mark start")?;
457 let end = usv_from(fields.get("end"), "mark end")?;
458 let ty = fields
459 .get("type")
460 .and_then(Value::as_str)
461 .ok_or(ParseError::Shape("mark type"))?;
462 Ok(MarkShape {
463 fields,
464 start,
465 end,
466 ty,
467 })
468}
469
470pub fn mark_from_value(v: &Value) -> Result<Mark, ParseError> {
474 let MarkShape {
475 fields: o,
476 start,
477 end,
478 ty,
479 } = mark_shape(v)?;
480 let o = fold_legacy_attrs(o, ty, Content::RESERVED_MARK_TYPES, "mark attrs")?;
483 let kind = match ty {
484 "strong" => MarkKind::Strong,
485 "emph" => MarkKind::Emph,
486 "underline" => MarkKind::Underline,
487 "strike" => MarkKind::Strike,
488 "code" => MarkKind::Code,
489 "link" => MarkKind::Link {
490 url: o
491 .get("url")
492 .and_then(Value::as_str)
493 .unwrap_or_default()
494 .to_string(),
495 },
496 "anchor" => MarkKind::Anchor {
497 id: o
498 .get("id")
499 .and_then(Value::as_str)
500 .unwrap_or_default()
501 .to_string(),
502 },
503 other => MarkKind::Unknown {
506 tag: other.to_string(),
507 attrs: bag_from_wire(&o, "attrs", "mark attrs")?,
508 },
509 };
510 Ok(Mark { start, end, kind })
511}
512
513pub(crate) fn line_kind_from_authored_value(v: &Value) -> Result<LineKind, ParseError> {
542 reject_line_kind_attrs(v)?;
543 line_kind_from_value(v)
544}
545
546pub(crate) fn container_from_authored_value(v: &Value) -> Result<Container, ParseError> {
549 reject_container_attrs(v)?;
550 container_from_value(v)
551}
552
553pub(crate) fn mark_from_authored_value(v: &Value) -> Result<Mark, ParseError> {
555 reject_mark_attrs(v)?;
556 mark_from_value(v)
557}
558
559pub(crate) fn reject_unreadable_mark(v: &Value) -> Result<(), ParseError> {
567 reject_mark_attrs(v)?;
568 mark_shape(v)?;
569 Ok(())
570}
571
572pub fn from_authored_value(v: &Value) -> Result<Content, ParseError> {
581 reject_reserved_attrs_deep(v)?;
582 from_canonical_value(v)
583}
584
585fn reject_reserved_attrs_deep(v: &Value) -> Result<(), ParseError> {
594 for line in arr_or_empty(v, "lines") {
595 reject_line_kind_attrs(line)?;
596 for c in arr_or_empty(line, "containers") {
597 reject_container_attrs(c)?;
598 }
599 }
600 for m in arr_or_empty(v, "marks") {
603 reject_mark_attrs(m)?;
604 }
605 for island in arr_or_empty(v, "islands") {
610 let ty = island.get("type").and_then(Value::as_str).unwrap_or_default();
611 match crate::island::KnownIslandType::parse(ty) {
612 Some(crate::island::KnownIslandType::Table) => {
613 let Some(props) = island.get("props") else {
614 continue;
615 };
616 for cell in table_cell_values(props) {
617 for m in arr_or_empty(cell, "marks") {
618 reject_unreadable_mark(m)?;
619 }
620 }
621 }
622 Some(crate::island::KnownIslandType::Image) | None => {}
624 }
625 }
626 Ok(())
627}
628
629fn reject_line_kind_attrs(v: &Value) -> Result<(), ParseError> {
630 reject_reserved_attrs(
631 v,
632 "kind",
633 Content::RESERVED_LINE_KINDS,
634 "attrs beside built-in kind",
635 )
636}
637
638fn reject_container_attrs(v: &Value) -> Result<(), ParseError> {
639 reject_reserved_attrs(
640 v,
641 "container",
642 Content::RESERVED_CONTAINERS,
643 "attrs beside built-in container",
644 )
645}
646
647fn reject_mark_attrs(v: &Value) -> Result<(), ParseError> {
648 reject_reserved_attrs(
649 v,
650 "type",
651 Content::RESERVED_MARK_TYPES,
652 "attrs beside built-in mark type",
653 )
654}
655
656fn reject_reserved_attrs(
664 v: &Value,
665 discriminant: &str,
666 reserved: &[&str],
667 err: &'static str,
668) -> Result<(), ParseError> {
669 let Some(o) = v.as_object() else {
670 return Ok(());
671 };
672 if !o.contains_key("attrs") {
673 return Ok(());
674 }
675 let Some(tag) = o.get(discriminant).and_then(Value::as_str) else {
676 return Ok(());
677 };
678 if reserved.contains(&tag) {
679 return Err(ParseError::Shape(err));
680 }
681 Ok(())
682}
683
684pub fn parse_cell(v: &Value) -> (String, Vec<Mark>) {
698 let text = v
699 .get("text")
700 .and_then(Value::as_str)
701 .unwrap_or_default()
702 .to_string();
703 let marks = v
704 .get("marks")
705 .and_then(Value::as_array)
706 .map(|arr| arr.iter().filter_map(|m| mark_from_value(m).ok()).collect())
707 .unwrap_or_default();
708 (text, marks)
709}
710
711pub(crate) fn cell_to_value(text: &str, marks: &[Mark]) -> Value {
715 let mut m = Map::new();
716 m.insert("text".into(), Value::String(text.to_string()));
717 m.insert(
718 "marks".into(),
719 Value::Array(marks.iter().map(mark_to_value).collect()),
720 );
721 Value::Object(m)
722}
723
724pub(crate) fn table_cell_values(props: &Value) -> impl Iterator<Item = &Value> {
728 let header = arr_or_empty(props, "header").iter();
729 let rows = arr_or_empty(props, "rows")
730 .iter()
731 .flat_map(|row| as_slice(row).iter());
732 header.chain(rows)
733}
734
735pub(crate) fn table_cells(props: &Value) -> Vec<(String, Vec<Mark>)> {
738 table_cell_values(props).map(parse_cell).collect()
739}
740
741pub(crate) fn normalize_table_props(props: &mut Value) {
758 let cols = table_cols(props);
759 let Some(obj) = props.as_object_mut() else {
760 return;
761 };
762 let header = obj.entry("header").or_insert_with(|| Value::Array(vec![]));
763 if !header.is_array() {
767 *header = Value::Array(vec![]);
768 }
769 pad_row(header, cols);
770 if let Some(h) = header.as_array_mut() {
771 h.iter_mut().for_each(canon_cell);
772 }
773 let aligns = obj.entry("aligns").or_insert_with(|| Value::Array(vec![]));
774 if let Some(a) = aligns.as_array_mut() {
775 while a.len() < cols {
776 a.push(Value::String("none".into()));
777 }
778 }
779 if let Some(rows) = obj.get_mut("rows").and_then(Value::as_array_mut) {
780 for row in rows.iter_mut() {
781 pad_row(row, cols);
782 if let Some(r) = row.as_array_mut() {
783 r.iter_mut().for_each(canon_cell);
784 }
785 }
786 }
787}
788
789fn table_cols(props: &Value) -> usize {
792 let arr_len = |k: &str| props.get(k).and_then(Value::as_array).map(|a| a.len());
793 let header = arr_len("header").unwrap_or(0);
794 let aligns = arr_len("aligns").unwrap_or(0);
795 let widest_row = props
796 .get("rows")
797 .and_then(Value::as_array)
798 .map(|rows| {
799 rows.iter()
800 .map(|r| r.as_array().map(|a| a.len()).unwrap_or(0))
801 .max()
802 .unwrap_or(0)
803 })
804 .unwrap_or(0);
805 header.max(aligns).max(widest_row)
806}
807
808fn pad_row(v: &mut Value, cols: usize) {
811 if let Some(arr) = v.as_array_mut() {
812 while arr.len() < cols {
813 arr.push(cell_to_value("", &[]));
814 }
815 }
816}
817
818fn canon_cell(cell: &mut Value) {
826 let (text, marks) = parse_cell(cell);
827 let text = if text.contains(['\n', '\r']) {
828 text.replace(['\n', '\r'], " ")
829 } else {
830 text
831 };
832 let canon = cell_to_value(&text, &crate::model::normalize_marks(marks));
833 match (cell.as_object_mut(), canon) {
834 (Some(o), Value::Object(fields)) => o.extend(fields),
837 (_, canon) => *cell = canon,
839 }
840}
841
842pub(crate) fn table_shape_error(props: &Value) -> Option<Invariant> {
846 if props.get("header").is_some_and(|h| !h.is_array()) {
851 return Some(Invariant::TableHeaderNotArray);
852 }
853 let cols = props
854 .get("header")
855 .and_then(Value::as_array)
856 .map(|a| a.len())
857 .unwrap_or(0);
858 let aligns = props
859 .get("aligns")
860 .and_then(Value::as_array)
861 .map(|a| a.len())
862 .unwrap_or(0);
863 if aligns != cols {
864 return Some(Invariant::TableAlignsMismatch { aligns, cols });
865 }
866 if let Some(rows) = props.get("rows").and_then(Value::as_array) {
867 for (i, row) in rows.iter().enumerate() {
868 let width = row.as_array().map(|a| a.len()).unwrap_or(0);
869 if width != cols {
870 return Some(Invariant::TableRaggedRow {
871 row: i,
872 width,
873 cols,
874 });
875 }
876 }
877 }
878 for (i, (text, _)) in table_cells(props).iter().enumerate() {
879 if text.contains('\n') || text.contains('\r') {
880 return Some(Invariant::TableCellNewline { cell: i });
881 }
882 }
883 None
884}
885
886pub(crate) fn island_to_value(island: &Island) -> Value {
889 let mut m = Map::new();
890 m.insert("id".into(), Value::String(island.id.clone()));
891 m.insert("type".into(), Value::String(island.island_type.clone()));
892 m.insert("props".into(), island.props.clone());
893 m.insert("loss".into(), island.loss.as_str().into());
894 Value::Object(m)
895}
896
897pub(crate) fn island_from_value(v: &Value) -> Result<Island, ParseError> {
898 let o = v.as_object().ok_or(ParseError::Shape("island"))?;
899 Ok(Island {
900 id: o
901 .get("id")
902 .and_then(Value::as_str)
903 .ok_or(ParseError::Shape("island id"))?
904 .to_string(),
905 island_type: o
906 .get("type")
907 .and_then(Value::as_str)
908 .ok_or(ParseError::Shape("island type"))?
909 .to_string(),
910 props: bag_from_wire(o, "props", "island props")?,
911 loss: o
916 .get("loss")
917 .and_then(Value::as_str)
918 .map_or(Loss::LOSSLESS, Loss::new),
919 })
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925 use crate::model::{Fidelity, Line, LineKind};
926
927 fn sample() -> Content {
928 Content {
929 text: "hello world".into(),
930 lines: vec![Line {
931 kind: LineKind::Para,
932 containers: vec![],
933 continues: false,
934 }],
935 marks: vec![
936 Mark {
937 start: 6,
938 end: 11,
939 kind: MarkKind::Strong,
940 },
941 Mark {
942 start: 0,
943 end: 5,
944 kind: MarkKind::Emph,
945 },
946 ],
947 islands: vec![],
948 }
949 }
950
951 #[test]
956 fn deep_container_nesting_is_rejected_at_decode() {
957 let containers = vec![r#"{"container":"quote"}"#; 20_000].join(",");
958 let json = format!(
959 r#"{{"text":"hi","lines":[{{"kind":"para","containers":[{containers}]}}],"marks":[],"islands":[]}}"#
960 );
961 assert!(matches!(
962 Content::from_canonical_json(&json),
963 Err(ParseError::Invalid(Invariant::NestingTooDeep { .. }))
964 ));
965 }
966
967 fn nested_arrays(depth: usize) -> Value {
974 let mut v = Value::Null;
975 for _ in 0..depth {
976 v = Value::Array(vec![v]);
977 }
978 v
979 }
980
981 #[test]
988 fn deep_json_payload_is_rejected_at_decode_on_the_value_lane() {
989 let deep = nested_arrays(1_000);
990 let cases: [(Value, &'static str); 4] = [
991 (
992 serde_json::json!({"text":"\u{fffc}","lines":[{"kind":"island","containers":[]}],
993 "marks":[],"islands":[{"id":"i1","type":"widget","loss":"lossless","props":deep}]}),
994 "island props",
995 ),
996 (
997 serde_json::json!({"text":"x","lines":[{"kind":"para","containers":[]}],
998 "marks":[{"start":0,"end":1,"type":"sparkle","attrs":deep}],"islands":[]}),
999 "mark attrs",
1000 ),
1001 (
1002 serde_json::json!({"text":"x","lines":[{"kind":"callout","containers":[],"attrs":deep}],
1003 "marks":[],"islands":[]}),
1004 "line attrs",
1005 ),
1006 (
1007 serde_json::json!({"text":"x","lines":[{"kind":"para",
1008 "containers":[{"container":"indent","attrs":deep}]}],"marks":[],"islands":[]}),
1009 "container attrs",
1010 ),
1011 ];
1012 for (v, what) in cases {
1013 assert_eq!(
1014 from_canonical_value(&v),
1015 Err(ParseError::Invalid(Invariant::JsonTooDeep {
1016 what,
1017 max: crate::MAX_JSON_DEPTH,
1018 })),
1019 "{what} accepted a 1 000-deep payload"
1020 );
1021 assert!(matches!(
1024 from_authored_value(&v),
1025 Err(ParseError::Invalid(Invariant::JsonTooDeep { .. }))
1026 ));
1027 }
1028 }
1029
1030 #[test]
1037 fn json_depth_cap_admits_every_storable_payload() {
1038 let content = |props: Value| {
1039 serde_json::json!({"text":"\u{fffc}","lines":[{"kind":"island","containers":[]}],
1040 "marks":[],"islands":[{"id":"i1","type":"widget","loss":"lossless","props":props}]})
1041 };
1042 assert!(from_canonical_value(&content(nested_arrays(crate::MAX_JSON_DEPTH))).is_ok());
1043 assert!(from_canonical_value(&content(nested_arrays(crate::MAX_JSON_DEPTH + 1))).is_err());
1044
1045 let mut storable = 0;
1050 for d in 1..=crate::MAX_JSON_DEPTH + 8 {
1051 let v = content(nested_arrays(d));
1052 if Content::from_canonical_json(&v.to_string()).is_ok() {
1053 storable = d;
1054 assert!(
1055 from_canonical_value(&v).is_ok(),
1056 "the bag cap refused a {d}-deep props the string lane accepts"
1057 );
1058 }
1059 }
1060 assert!(
1061 storable > 0 && storable <= crate::MAX_JSON_DEPTH,
1062 "string lane's deepest storable props was {storable}"
1063 );
1064 }
1065
1066 #[test]
1071 fn deep_json_payload_is_rejected_before_the_legacy_attrs_fold() {
1072 let mut deep = Value::Null;
1073 for _ in 0..1_000 {
1074 deep = serde_json::json!({"a": deep});
1075 }
1076 let v = serde_json::json!({"text":"x","lines":[{"kind":"para","containers":[],"attrs":deep}],
1078 "marks":[],"islands":[]});
1079 assert_eq!(
1080 from_canonical_value(&v),
1081 Err(ParseError::Invalid(Invariant::JsonTooDeep {
1082 what: "line attrs",
1083 max: crate::MAX_JSON_DEPTH,
1084 }))
1085 );
1086 }
1087
1088 #[test]
1091 fn deep_json_payload_is_rejected_on_the_op_wire() {
1092 let deep = nested_arrays(1_000);
1093 let op = serde_json::json!({"op":"add","start":0,"end":1,"type":"sparkle","attrs":deep});
1094 assert!(matches!(
1095 crate::ops::mark_op_from_value(&op),
1096 Err(ParseError::Invalid(Invariant::JsonTooDeep { .. }))
1097 ));
1098 let op = serde_json::json!({"op":"setKind","line":0,"kind":"callout","attrs":deep});
1099 assert!(matches!(
1100 crate::ops::line_op_from_value(&op),
1101 Err(ParseError::Invalid(Invariant::JsonTooDeep { .. }))
1102 ));
1103 }
1104
1105 #[test]
1111 fn out_of_range_wire_position_is_refused() {
1112 let json = r#"{"text":"hello","lines":[{"kind":"para","containers":[]}],"marks":[{"start":4294967301,"end":4294967302,"type":"strong"}],"islands":[]}"#;
1113 assert!(Content::from_canonical_json(json).is_err());
1114 assert!(usv_from(Some(&Value::from(u64::MAX)), "x").is_ok() || usize::BITS < 64);
1115 assert!(usv_from(Some(&Value::from(-1i64)), "x").is_err());
1116 }
1117
1118 #[test]
1119 fn island_props_key_order_does_not_leak() {
1120 let mut one = Content::empty();
1121 one.text = "\u{FFFC}".into();
1122 one.lines = vec![Line {
1123 kind: LineKind::Island,
1124 containers: vec![],
1125 continues: false,
1126 }];
1127 one.islands = vec![Island {
1128 id: "i1".into(),
1129 island_type: "table".into(),
1130 props: serde_json::json!({"b": 1, "a": 2}),
1131 loss: Loss::LOSSLESS,
1132 }];
1133 let mut two = one.clone();
1134 two.islands[0].props = serde_json::json!({"a": 2, "b": 1}); assert_eq!(one.to_canonical_json(), two.to_canonical_json());
1136 }
1137
1138 #[test]
1139 fn golden_bytes_are_feature_independent() {
1140 let rt = sample();
1144 assert_eq!(
1145 rt.to_canonical_json(),
1146 r#"{"islands":[],"lines":[{"containers":[],"kind":"para"}],"marks":[{"end":5,"start":0,"type":"emph"},{"end":11,"start":6,"type":"strong"}],"text":"hello world"}"#
1147 );
1148 }
1149
1150 #[test]
1151 fn from_canonical_json_rejects_invalid() {
1152 let bad =
1154 r#"{"text":"a\nb","lines":[{"kind":"para","containers":[]}],"marks":[],"islands":[]}"#;
1155 assert!(matches!(
1156 Content::from_canonical_json(bad),
1157 Err(ParseError::Invalid(_))
1158 ));
1159 }
1160
1161 #[test]
1162 fn reserved_unknown_tag_rejected() {
1163 let mut rt = Content::empty();
1166 rt.text = "abcd".into();
1167 rt.marks = vec![Mark {
1168 start: 0,
1169 end: 4,
1170 kind: MarkKind::Unknown {
1171 tag: "strong".into(),
1172 attrs: serde_json::json!({}),
1173 },
1174 }];
1175 assert!(matches!(
1176 rt.validate(),
1177 Err(crate::model::Invariant::ReservedUnknownTag(_))
1178 ));
1179 }
1180
1181 #[test]
1186 fn unknown_loss_class_round_trips_and_reads_unrepresentable() {
1187 let json = concat!(
1188 r#"{"islands":[{"id":"i1","loss":"partial","props":{},"type":"widget"}],"#,
1189 r#""lines":[{"containers":[],"kind":"island"}],"marks":[],"text":""}"#
1190 );
1191 let rt = Content::from_canonical_json(json).unwrap();
1192 assert_eq!(rt.islands[0].loss, Loss::new("partial"));
1193 assert_eq!(rt.islands[0].loss.fidelity(), Fidelity::Unrepresentable);
1194 assert_eq!(rt.to_canonical_json(), json);
1195 }
1196
1197 #[test]
1202 fn a_built_in_class_name_has_one_spelling() {
1203 assert_eq!(Loss::new("lossless"), Loss::LOSSLESS);
1204 let mut rt = Content::empty();
1205 rt.text = "\u{FFFC}".into();
1206 rt.lines = vec![Line {
1207 kind: LineKind::Island,
1208 containers: vec![],
1209 continues: false,
1210 }];
1211 rt.islands = vec![Island {
1212 id: "i1".into(),
1213 island_type: "widget".into(),
1214 props: serde_json::json!({}),
1215 loss: Loss::new("lossless"),
1216 }];
1217 assert_eq!(rt.validate(), Ok(()));
1218 let back = Content::from_canonical_json(&rt.to_canonical_json()).unwrap();
1219 assert_eq!(back.islands[0].loss, rt.islands[0].loss);
1220 assert_eq!(back.islands[0].loss.fidelity(), Fidelity::Lossless);
1221 }
1222
1223 #[test]
1226 fn every_fidelity_level_round_trips_through_its_class() {
1227 for &f in Fidelity::ALL {
1228 assert_eq!(Loss::new(f.as_str()).fidelity(), f);
1229 }
1230 }
1231
1232 #[test]
1237 fn unknown_line_kind_and_container_round_trip_opaque() {
1238 let json = concat!(
1239 r#"{"islands":[],"lines":[{"attrs":{"variant":"warn"},"containers":"#,
1240 r#"[{"attrs":{"depth":2},"container":"indent"}],"kind":"callout"}],"#,
1241 r#""marks":[],"text":"heads up"}"#
1242 );
1243 let rt = Content::from_canonical_json(json).unwrap();
1244 assert_eq!(
1245 rt.lines[0].kind,
1246 LineKind::Unknown {
1247 tag: "callout".into(),
1248 attrs: serde_json::json!({"variant": "warn"}),
1249 }
1250 );
1251 assert_eq!(
1252 rt.lines[0].containers,
1253 vec![Container::Unknown {
1254 tag: "indent".into(),
1255 attrs: serde_json::json!({"depth": 2}),
1256 }]
1257 );
1258 assert_eq!(rt.to_canonical_json(), json);
1259 let bare = r#"{"islands":[],"lines":[{"containers":[],"kind":"footnote"}],"marks":[],"text":"x"}"#;
1261 let rt = Content::from_canonical_json(bare).unwrap();
1262 assert_eq!(
1263 rt.lines[0].kind,
1264 LineKind::Unknown {
1265 tag: "footnote".into(),
1266 attrs: Value::Null,
1267 }
1268 );
1269 for bad in [
1272 r#"{"islands":[],"lines":[{"containers":[]}],"marks":[],"text":"x"}"#,
1273 r#"{"islands":[],"lines":[{"containers":[{"container":7}],"kind":"para"}],"marks":[],"text":"x"}"#,
1274 ] {
1275 assert!(matches!(
1276 Content::from_canonical_json(bad),
1277 Err(ParseError::Shape(_))
1278 ));
1279 }
1280 }
1281
1282 #[test]
1286 fn reserved_block_vocabulary_names_rejected() {
1287 let mut rt = Content::empty();
1288 rt.text = "abcd".into();
1289 rt.lines[0].kind = LineKind::Unknown {
1290 tag: "heading".into(),
1291 attrs: serde_json::json!({}),
1292 };
1293 assert_eq!(
1294 rt.validate(),
1295 Err(Invariant::ReservedUnknownLineKind("heading".into()))
1296 );
1297 rt.lines[0].kind = LineKind::Para;
1298 rt.lines[0].containers = vec![Container::Unknown {
1299 tag: "quote".into(),
1300 attrs: serde_json::json!({}),
1301 }];
1302 assert_eq!(
1303 rt.validate(),
1304 Err(Invariant::ReservedUnknownContainer("quote".into()))
1305 );
1306 }
1307
1308 #[test]
1318 fn authored_lane_rejects_attrs_beside_a_built_in_name() {
1319 let bad = [
1320 r#"{"islands":[],"lines":[{"attrs":{"tone":"warn"},"containers":[],"kind":"para"}],"marks":[],"text":"x"}"#,
1322 r#"{"islands":[],"lines":[{"containers":[{"attrs":{},"container":"quote"}],"kind":"para"}],"marks":[],"text":"x"}"#,
1324 r#"{"islands":[],"lines":[{"containers":[],"kind":"para"}],"marks":[{"attrs":{},"end":1,"start":0,"type":"strong"}],"text":"x"}"#,
1326 concat!(
1328 r#"{"islands":[{"id":"i1","loss":"lossless","props":{"aligns":["none"],"#,
1329 r#""header":[{"marks":[{"attrs":{},"end":1,"start":0,"type":"emph"}],"text":"h"}],"#,
1330 r#""rows":[[{"marks":[],"text":"r"}]]},"type":"table"}],"#,
1331 r#""lines":[{"containers":[],"kind":"island"}],"marks":[],"text":""}"#
1332 ),
1333 concat!(
1335 r#"{"islands":[{"id":"i1","loss":"lossless","props":{"aligns":["none"],"#,
1336 r#""header":[{"marks":[{"end":1,"start":0}],"text":"h"}],"#,
1337 r#""rows":[[{"marks":[],"text":"r"}]]},"type":"table"}],"#,
1338 r#""lines":[{"containers":[],"kind":"island"}],"marks":[],"text":""}"#
1339 ),
1340 ];
1341 for json in bad {
1342 let v: Value = serde_json::from_str(json).unwrap();
1343 assert!(
1344 matches!(from_authored_value(&v), Err(ParseError::Shape(_))),
1345 "accepted: {json}"
1346 );
1347 assert!(
1350 Content::from_canonical_json(json).is_ok(),
1351 "storage lane rejected: {json}"
1352 );
1353 }
1354 let rt = Content::from_canonical_json(bad[4]).unwrap();
1357 assert!(rt.islands[0].props["header"][0]["marks"]
1358 .as_array()
1359 .unwrap()
1360 .is_empty());
1361 }
1362
1363 #[test]
1368 fn authored_lane_leaves_opaque_attrs_payload_alone() {
1369 let json = concat!(
1370 r#"{"islands":[],"lines":[{"attrs":{"nested":{"attrs":{},"type":"link"}},"#,
1371 r#""containers":[],"kind":"callout"}],"marks":[],"text":"x"}"#
1372 );
1373 let v: Value = serde_json::from_str(json).unwrap();
1374 let rt = from_authored_value(&v).unwrap();
1375 assert_eq!(rt.to_canonical_json(), json);
1376 }
1377
1378 #[test]
1381 fn unknown_block_attrs_key_order_does_not_leak() {
1382 let mut one = Content::empty();
1383 one.text = "hi".into();
1384 one.lines[0].kind = LineKind::Unknown {
1385 tag: "callout".into(),
1386 attrs: serde_json::json!({"b": 1, "a": 2}),
1387 };
1388 one.lines[0].containers = vec![Container::Unknown {
1389 tag: "indent".into(),
1390 attrs: serde_json::json!({"y": 1, "x": 2}),
1391 }];
1392 let mut two = one.clone();
1393 two.lines[0].kind = LineKind::Unknown {
1394 tag: "callout".into(),
1395 attrs: serde_json::json!({"a": 2, "b": 1}),
1396 };
1397 two.lines[0].containers = vec![Container::Unknown {
1398 tag: "indent".into(),
1399 attrs: serde_json::json!({"x": 2, "y": 1}),
1400 }];
1401 assert_eq!(one.to_canonical_json(), two.to_canonical_json());
1402 one.normalize();
1403 two.normalize();
1404 assert_eq!(one, two, "normalize canonicalizes the live model too");
1405 }
1406
1407 #[test]
1408 fn unknown_mark_round_trips_opaque() {
1409 let mut rt = Content::empty();
1410 rt.text = "abcd".into();
1411 rt.marks = vec![Mark {
1412 start: 0,
1413 end: 4,
1414 kind: MarkKind::Unknown {
1415 tag: "highlight".into(),
1416 attrs: serde_json::json!({"color": "yellow"}),
1417 },
1418 }];
1419 let json = rt.to_canonical_json();
1420 let back = Content::from_canonical_json(&json).unwrap();
1421 assert_eq!(back.marks[0].kind, rt.marks[0].kind);
1422 }
1423
1424 #[test]
1431 fn built_in_decoders_read_the_legacy_attrs_form() {
1432 let cases: [(Value, LineKind); 2] = [
1433 (
1434 serde_json::json!({"kind": "heading", "attrs": {"level": 2}}),
1435 LineKind::Heading { level: 2 },
1436 ),
1437 (
1438 serde_json::json!({"kind": "code", "attrs": {"lang": "rust"}}),
1439 LineKind::Code {
1440 lang: Some("rust".into()),
1441 },
1442 ),
1443 ];
1444 for (v, want) in cases {
1445 assert_eq!(line_kind_from_value(&v).unwrap(), want);
1446 }
1447 let item = serde_json::json!({
1448 "container": "list_item",
1449 "attrs": {"ordered": true, "start": 3, "ordinal": 1}
1450 });
1451 assert_eq!(
1452 container_from_value(&item).unwrap(),
1453 Container::ListItem {
1454 ordered: true,
1455 start: 3,
1456 ordinal: 1,
1457 }
1458 );
1459 let link = serde_json::json!({"start": 0, "end": 1, "type": "link", "attrs": {"url": "u"}});
1460 assert_eq!(
1461 mark_from_value(&link).unwrap().kind,
1462 MarkKind::Link { url: "u".into() }
1463 );
1464 let both = serde_json::json!({"kind": "heading", "level": 3, "attrs": {"level": 2}});
1466 assert_eq!(
1467 line_kind_from_value(&both).unwrap(),
1468 LineKind::Heading { level: 3 }
1469 );
1470 let unknown = serde_json::json!({"kind": "callout", "attrs": {"kind": "heading", "level": 2}});
1474 assert_eq!(
1475 line_kind_from_value(&unknown).unwrap(),
1476 LineKind::Unknown {
1477 tag: "callout".into(),
1478 attrs: serde_json::json!({"kind": "heading", "level": 2}),
1479 }
1480 );
1481 let legacy = r#"{"islands":[],"lines":[{"attrs":{"level":2},"containers":[],"kind":"heading"}],"marks":[],"text":"hi"}"#;
1485 assert_eq!(
1486 Content::from_canonical_json(legacy)
1487 .unwrap()
1488 .to_canonical_json(),
1489 r#"{"islands":[],"lines":[{"containers":[],"kind":"heading","level":2}],"marks":[],"text":"hi"}"#
1490 );
1491 }
1492
1493 #[test]
1498 fn unknown_holds_the_last_mark_ordinal() {
1499 let all = [
1500 MarkKind::Strong,
1501 MarkKind::Emph,
1502 MarkKind::Underline,
1503 MarkKind::Strike,
1504 MarkKind::Code,
1505 MarkKind::Link { url: "u".into() },
1506 MarkKind::Anchor { id: "a".into() },
1507 MarkKind::Unknown {
1508 tag: "kbd".into(),
1509 attrs: Value::Null,
1510 },
1511 ];
1512 for k in &all {
1516 match k {
1517 MarkKind::Strong
1518 | MarkKind::Emph
1519 | MarkKind::Underline
1520 | MarkKind::Strike
1521 | MarkKind::Code
1522 | MarkKind::Link { .. }
1523 | MarkKind::Anchor { .. }
1524 | MarkKind::Unknown { .. } => {}
1525 }
1526 }
1527 let ords: Vec<u8> = all.iter().map(MarkKind::ord).collect();
1528 assert_eq!(ords, (0..all.len() as u8).collect::<Vec<_>>());
1529 assert!(matches!(all.last(), Some(MarkKind::Unknown { .. })));
1530 }
1531
1532 #[test]
1537 fn formatting_class_membership_decides_adjacent_union() {
1538 let mut rt = Content::empty();
1539 rt.text = "abcd".into();
1540 let unknown = |start, end| Mark {
1541 start,
1542 end,
1543 kind: MarkKind::Unknown {
1544 tag: "kbd".into(),
1545 attrs: serde_json::json!({}),
1546 },
1547 };
1548 rt.marks = vec![unknown(0, 2), unknown(2, 4)];
1549 rt.normalize();
1550 assert_eq!(rt.marks.len(), 2);
1551 rt.marks = vec![
1552 Mark {
1553 start: 0,
1554 end: 2,
1555 kind: MarkKind::Strong,
1556 },
1557 Mark {
1558 start: 2,
1559 end: 4,
1560 kind: MarkKind::Strong,
1561 },
1562 ];
1563 rt.normalize();
1564 assert_eq!(rt.marks.len(), 1);
1565 }
1566
1567 #[test]
1573 fn reserved_growth_flips_authored_acceptance() {
1574 let doc = |kind: &str| {
1575 serde_json::json!({
1576 "islands": [],
1577 "lines": [{"attrs": {"level": 2}, "containers": [], "kind": kind}],
1578 "marks": [],
1579 "text": "hi",
1580 })
1581 };
1582 assert!(from_authored_value(&doc("callout")).is_ok());
1584 assert!(matches!(
1586 from_authored_value(&doc("heading")),
1587 Err(ParseError::Shape(_))
1588 ));
1589 }
1590}