1#![no_main]
2use libfuzzer_sys::fuzz_target;
3use onestore::{
4 CommitState, ExGuid, OutlineEdit, RevisionIndex, Store, TextAttribute as A,
5 document::{Document, Kind},
6 op::{Edit, PageOp},
7 page::text::new_id,
8};
9use std::sync::LazyLock;
10
11#[path = "../../crates/onestore/tests/support/current.rs"]
12mod current;
13#[path = "../../crates/onestore/tests/support/disk.rs"]
14mod disk;
15#[path = "../../crates/onestore/tests/support/ops.rs"]
16mod ops;
17
18static SOURCE: LazyLock<(Vec<u8>, ExGuid, ExGuid)> = LazyLock::new(|| {
19 let source = onestore::create_section("paragraph.one", "Original", "Author").unwrap();
20 let store = Store::parse(&source).unwrap();
21 let index = RevisionIndex::parse(&store).unwrap();
22 let document = Document::parse(&index).unwrap();
23 let (sid, page) = document.pages().unwrap()[0];
24 let space = &document.spaces[&sid];
25 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
26 let outline = *view.nodes[&page]
27 .children
28 .iter()
29 .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. }))
30 .unwrap();
31 let parent = view.nodes[&outline].children[0];
32 let mut paragraph = ops::paragraph("a🦀 e\u{301} 東京\rEnd");
33 paragraph.level = 2;
34 let text = paragraph.text().unwrap().id;
35 let format = |range, set| PageOp::Format {
36 text,
37 range,
38 set,
39 clear: Vec::new(),
40 };
41 let insert = PageOp::Insert {
42 container: parent,
43 before: None,
44 paragraphs: vec![paragraph],
45 };
46 let bold = format(0..4, vec![A::Bold(true)]);
47 let italic = format(4..7, vec![A::Italic(true), A::Color(Some([12, 34, 56]))]);
48 let (second, ..) = ops::new_outline(144.0, 36.0, "Fixed second");
49 let edited = ops::page_edited(&source, sid, vec![insert, bold, italic, second]).unwrap();
50 (edited, sid, outline)
51});
52
53fuzz_target!(|input: &[u8]| {
54 let (source, sid, outline) = &*SOURCE;
55 if let Ok(edit) = serde_json::from_slice::<Edit>(input)
56 && let Ok(edited) = ops::apply(source, "Paragraph fuzz", edit.ops)
57 {
58 current::current(edited.as_bytes());
59 }
60 let mut persisted = source.clone();
61 let mut caches = std::array::from_fn::<_, 12, _>(|_| source.clone());
62 for step in input.chunks_exact(8).take(20) {
63 let actor = usize::from(step[0]) % caches.len();
64 if step[1] % 3 == 0 {
65 caches[actor].clone_from(&persisted);
66 }
67 let source = &caches[actor];
68 let store = Store::parse(source).unwrap();
69 let index = RevisionIndex::parse(&store).unwrap();
70 let document = Document::parse(&index).unwrap();
71 let space = &document.spaces[sid];
72 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
73 let mut pending = vec![*outline];
74 let mut paragraphs = Vec::new();
75 while let Some(id) = pending.pop() {
76 let node = &view.nodes[&id];
77 pending.extend(node.children.iter().copied());
78 if matches!(node.kind, Kind::Paragraph { .. }) {
79 paragraphs.push(id);
80 }
81 }
82 let characters = |view: &onestore::document::Revision<'_>, id| {
83 view.text_runs(id)
84 .unwrap()
85 .into_iter()
86 .flat_map(|run| {
87 let style = serde_json::to_value(run.format).unwrap();
88 run.text.chars().map(move |c| (c, style.clone()))
89 })
90 .collect::<Vec<_>>()
91 };
92 let pairs: Vec<_> = view
93 .nodes
94 .iter()
95 .flat_map(|(parent, node)| {
96 node.children.windows(2).filter_map(|pair| {
97 (paragraphs.contains(&pair[0])
98 && paragraphs.contains(&pair[1])
99 && view.nodes[&pair[0]].children.is_empty())
100 .then_some((*parent, pair[0], pair[1]))
101 })
102 })
103 .collect();
104 let mut expected_text = Vec::new();
105 let mut expected_graph = Vec::new();
106 let mut expected_layout = None;
107 let mut expected_collapse = None;
108 let edit = if step[1] & 16 != 0 {
109 let mut layout = serde_json::to_value(&view.nodes[outline].layout).unwrap();
110 let (object, operation) = match (step[1] >> 5) % 3 {
111 0 => {
112 let x = (f32::from(step[2]) - 64.0) * 18.0;
113 let y = f32::from(step[3]) * 18.0;
114 layout["x"] = x.into();
115 layout["y"] = y.into();
116 (*outline, OutlineEdit::Position { x, y })
117 }
118 1 => {
119 let points = (2.0 + f32::from(step[2])) * 18.0;
120 let user_set = step[3] & 1 != 0;
121 layout["max_width"] = points.into();
122 layout["width_set_by_user"] = user_set.into();
123 (*outline, OutlineEdit::Width { points, user_set })
124 }
125 _ => {
126 let paragraph = paragraphs[usize::from(step[2]) % paragraphs.len()];
127 let collapsed = step[3] & 1 != 0;
128 expected_collapse = Some((paragraph, collapsed));
129 (paragraph, OutlineEdit::Collapsed(collapsed))
130 }
131 };
132 expected_layout = Some(layout);
133 for (id, node) in &view.nodes {
134 expected_graph.push((*id, node.children.clone(), node.content.clone()));
135 if matches!(node.kind, Kind::RichText { .. }) {
136 expected_text.push((*id, characters(view, *id)));
137 }
138 }
139 ops::page_op(
140 source,
141 *sid,
142 PageOp::Outline {
143 object,
144 edit: operation,
145 },
146 )
147 .unwrap()
148 } else if step[1] & 8 != 0 && !pairs.is_empty() {
149 let (parent, left, right) = pairs[usize::from(step[2]) % pairs.len()];
150 let a = view.nodes[&left].content[0];
151 let b = view.nodes[&right].content[0];
152 let mut expected = characters(view, a);
153 let survivor = if expected.is_empty() { b } else { a };
154 expected.extend(characters(view, b));
155 expected_text.push((survivor, expected));
156 let children: Vec<_> = view.nodes[&parent]
157 .children
158 .iter()
159 .filter(|id| **id != right)
160 .copied()
161 .collect();
162 expected_graph.push((parent, children, view.nodes[&parent].content.clone()));
163 expected_graph.push((left, view.nodes[&right].children.clone(), vec![survivor]));
164 ops::page_op(source, *sid, PageOp::Join { left: a, right: b }).unwrap()
165 } else {
166 let paragraph = paragraphs[usize::from(step[2]) % paragraphs.len()];
167 let text = view.nodes[&paragraph].content[0];
168 let before = characters(view, text);
169 let offsets: Vec<u32> = std::iter::once(0)
170 .chain(before.iter().scan(0, |n, (c, _)| {
171 *n += u32::try_from(c.len_utf16()).unwrap();
172 Some(*n)
173 }))
174 .collect();
175 let offset = u32::from(step[3]) % (offsets.last().unwrap() + 2);
176 let (object, right) = (new_id().unwrap(), new_id().unwrap());
177 let split = PageOp::Split {
178 text,
179 at: offset,
180 paragraph: object,
181 right,
182 lists: Vec::new(),
183 };
184 let edit = ops::page_op(source, *sid, split);
185 let Some(position) = offsets.iter().position(|n| *n == offset) else {
186 assert!(edit.is_err());
187 continue;
188 };
189 expected_text.push((text, before[..position].to_vec()));
190 expected_text.push((right, before[position..].to_vec()));
191 expected_graph.push((paragraph, vec![], vec![text]));
192 expected_graph.push((object, view.nodes[&paragraph].children.clone(), vec![right]));
193 edit.unwrap()
194 };
195 let after_store = Store::parse(edit.as_bytes()).unwrap();
196 let after_index = RevisionIndex::parse(&after_store).unwrap();
197 let after_document = Document::parse(&after_index).unwrap();
198 let space = &after_document.spaces[sid];
199 let after_view = &space.revisions[&space.contexts[&ExGuid::default()]];
200 let layout = &after_view.nodes[outline].layout;
201 if layout.y.unwrap() > 36.0 || (layout.y == Some(36.0) && layout.x.unwrap() > 144.0) {
202 let (_, page) = after_document.pages().unwrap()[0];
203 assert!(matches!(&after_view.nodes[&after_view.roots[&2]].kind,
204 Kind::Metadata { title: Some(title), .. } if title == "Fixed second"));
205 assert!(matches!(&after_view.nodes[&page].kind,
206 Kind::Page { alternate_title: Some(title), .. } if title == "Fixed second"));
207 }
208 if let Some(expected) = expected_layout {
209 assert_eq!(
210 serde_json::to_value(&after_view.nodes[outline].layout).unwrap(),
211 expected
212 );
213 }
214 if let Some((id, expected)) = expected_collapse {
215 let Kind::Paragraph { collapse_state, .. } = after_view.nodes[&id].kind else {
216 panic!()
217 };
218 assert_eq!(collapse_state, Some(u8::from(expected)));
219 }
220 for (id, expected) in expected_text {
221 assert_eq!(characters(after_view, id), expected);
222 }
223 for (id, children, content) in expected_graph {
224 assert_eq!(after_view.nodes[&id].children, children);
225 assert_eq!(after_view.nodes[&id].content, content);
226 }
227 let before = current::current(&persisted);
228 let after = current::current(edit.as_bytes());
229 let mut disk = disk::Disk {
230 visible: persisted.clone(),
231 durable: persisted.clone(),
232 operation: 0,
233 fail_at: (step[4] != 0).then_some(usize::from(u16::from_le_bytes([step[4], step[5]]))),
234 write_limit: if step[6] & 1 == 0 { 17 } else { 4096 },
235 random: u64::from(step[7]) + 1,
236 };
237 let Some(transaction) = &edit.transaction else {
238 continue;
239 };
240 let result = transaction.commit(&mut disk);
241 let observed = current::current(&disk.durable);
242 match result {
243 Ok(()) => assert_eq!(observed, after),
244 Err(error) => {
245 assert!(observed == before || observed == after);
246 match error.state {
247 CommitState::NotCommitted => assert_eq!(observed, before),
248 CommitState::Committed => assert_eq!(observed, after),
249 CommitState::Unknown => {}
250 }
251 }
252 }
253 persisted = disk.durable;
254 }
255});