| 1 | #![no_main] |
| 2 | use libfuzzer_sys::fuzz_target; |
| 3 | use onestore::{ |
| 4 | CommitState, ExGuid, OutlineEdit, RevisionIndex, Store, TextAttribute as A, |
| 5 | document::{Document, Kind}, |
| 6 | op::{Edit, PageOp}, |
| 7 | page::text::new_id, |
| 8 | }; |
| 9 | use std::sync::LazyLock; |
| 10 | |
| 11 | #[path = "../../crates/onestore/tests/support/current.rs"] |
| 12 | mod current; |
| 13 | #[path = "../../crates/onestore/tests/support/disk.rs"] |
| 14 | mod disk; |
| 15 | #[path = "../../crates/onestore/tests/support/ops.rs"] |
| 16 | mod ops; |
| 17 | |
| 18 | static 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 | |
| 53 | fuzz_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 | }); |