| 1 | #![no_main] |
| 2 | use libfuzzer_sys::fuzz_target; |
| 3 | use onestore::{ |
| 4 | CommitState, ExGuid, RevisionIndex, Store, TextAttribute as A, |
| 5 | document::{Document, Kind}, |
| 6 | op::{Edit, Op, PageOp}, |
| 7 | }; |
| 8 | use std::sync::LazyLock; |
| 9 | |
| 10 | #[path = "../../crates/onestore/tests/support/current.rs"] |
| 11 | mod current; |
| 12 | #[path = "../../crates/onestore/tests/support/disk.rs"] |
| 13 | mod disk; |
| 14 | #[path = "../../crates/onestore/tests/support/ops.rs"] |
| 15 | mod ops; |
| 16 | |
| 17 | static SOURCE: LazyLock<(Vec<u8>, ExGuid, ExGuid)> = LazyLock::new(|| { |
| 18 | let bytes = include_bytes!( |
| 19 | "../../corpus/native/20260905-05/snapshots/03-format-unicode/notebook/synthetic.one" |
| 20 | ) |
| 21 | .to_vec(); |
| 22 | let store = Store::parse(&bytes).unwrap(); |
| 23 | let index = RevisionIndex::parse(&store).unwrap(); |
| 24 | let document = Document::parse(&index).unwrap(); |
| 25 | let (space, page) = document.pages().unwrap()[0]; |
| 26 | let s = &document.spaces[&space]; |
| 27 | let view = &s.revisions[&s.contexts[&ExGuid::default()]]; |
| 28 | let outline = *view.nodes[&page] |
| 29 | .children |
| 30 | .iter() |
| 31 | .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. })) |
| 32 | .unwrap(); |
| 33 | (bytes, space, outline) |
| 34 | }); |
| 35 | |
| 36 | fuzz_target!(|input: &[u8]| { |
| 37 | let (source, space, outline) = &*SOURCE; |
| 38 | if let Ok(edit) = serde_json::from_slice::<Edit>(input) |
| 39 | && let Ok(edited) = ops::apply(source, "Insertion fuzz", edit.ops) |
| 40 | { |
| 41 | current::current(edited.as_bytes()); |
| 42 | } |
| 43 | let mut persisted = source.clone(); |
| 44 | let mut caches = std::array::from_fn::<_, 12, _>(|_| source.clone()); |
| 45 | for step in input.chunks_exact(8).take(12) { |
| 46 | let actor = usize::from(step[0]) % caches.len(); |
| 47 | if step[1] % 4 == 0 { |
| 48 | caches[actor].clone_from(&persisted); |
| 49 | } |
| 50 | let source = &caches[actor]; |
| 51 | let text = |
| 52 | ["", "ab", "🦀e\u{301}東京\rEnd", "same style same style"][usize::from(step[2]) % 4]; |
| 53 | let offsets: Vec<u32> = std::iter::once(0) |
| 54 | .chain(text.chars().scan(0, |n, c| { |
| 55 | *n += c.len_utf16() as u32; |
| 56 | Some(*n) |
| 57 | })) |
| 58 | .collect(); |
| 59 | let first = usize::from(step[3]) % offsets.len(); |
| 60 | let second = usize::from(step[4]) % offsets.len(); |
| 61 | let (start, end) = (first.min(second), first.max(second)); |
| 62 | let (insert, text_object) = if step[1] & 1 == 0 { |
| 63 | let paragraph = ops::paragraph(text); |
| 64 | let id = paragraph.text().unwrap().id; |
| 65 | let insert = PageOp::Insert { |
| 66 | container: *outline, |
| 67 | before: None, |
| 68 | paragraphs: vec![paragraph], |
| 69 | }; |
| 70 | (insert, id) |
| 71 | } else { |
| 72 | let (add, _, id) = ops::new_outline(72.0, 144.0, text); |
| 73 | (add, id) |
| 74 | }; |
| 75 | let enabled = step[5] & 1 != 0; |
| 76 | let mut page_ops = vec![insert]; |
| 77 | if start != end || text.is_empty() { |
| 78 | page_ops.push(PageOp::Format { |
| 79 | text: text_object, |
| 80 | range: offsets[start]..offsets[end], |
| 81 | set: vec![A::Bold(enabled), A::FontSize(18.0)], |
| 82 | clear: Vec::new(), |
| 83 | }); |
| 84 | } |
| 85 | let edit = Edit { |
| 86 | at: ops::now(), |
| 87 | ops: page_ops |
| 88 | .into_iter() |
| 89 | .map(|op| Op::Page { space: *space, op }) |
| 90 | .collect(), |
| 91 | }; |
| 92 | let encoded = serde_json::to_vec(&edit).unwrap(); |
| 93 | let edit: Edit = serde_json::from_slice(&encoded).unwrap(); |
| 94 | let edit = ops::apply(source, "Insertion fuzz", edit.ops).unwrap(); |
| 95 | let store = Store::parse(edit.as_bytes()).unwrap(); |
| 96 | let index = RevisionIndex::parse(&store).unwrap(); |
| 97 | let document = Document::parse(&index).unwrap(); |
| 98 | let s = &document.spaces[space]; |
| 99 | let view = &s.revisions[&s.contexts[&ExGuid::default()]]; |
| 100 | let actual: Vec<_> = view |
| 101 | .text_runs(text_object) |
| 102 | .unwrap() |
| 103 | .into_iter() |
| 104 | .flat_map(|run| { |
| 105 | run.text.chars().map(move |c| { |
| 106 | ( |
| 107 | c, |
| 108 | run.format.bold.unwrap_or(false), |
| 109 | run.format.font_size.unwrap(), |
| 110 | ) |
| 111 | }) |
| 112 | }) |
| 113 | .collect(); |
| 114 | let expected: Vec<_> = text |
| 115 | .chars() |
| 116 | .enumerate() |
| 117 | .map(|(i, c)| { |
| 118 | if (start..end).contains(&i) { |
| 119 | (c, enabled, 18.0) |
| 120 | } else { |
| 121 | (c, false, 11.0) |
| 122 | } |
| 123 | }) |
| 124 | .collect(); |
| 125 | assert_eq!(actual, expected); |
| 126 | let before = current::current(&persisted); |
| 127 | let after = current::current(edit.as_bytes()); |
| 128 | let mut disk = disk::Disk { |
| 129 | visible: persisted.clone(), |
| 130 | durable: persisted.clone(), |
| 131 | operation: 0, |
| 132 | fail_at: (step[6] != 0).then_some(usize::from(step[6])), |
| 133 | write_limit: if step[7] & 1 == 0 { 17 } else { 4096 }, |
| 134 | random: u64::from(step[7]) + 1, |
| 135 | }; |
| 136 | let result = edit.commit(&mut disk); |
| 137 | let observed = current::current(&disk.durable); |
| 138 | match result { |
| 139 | Ok(()) => assert_eq!(observed, after), |
| 140 | Err(error) => { |
| 141 | assert!(observed == before || observed == after); |
| 142 | if error.state == CommitState::NotCommitted { |
| 143 | assert_eq!(observed, before); |
| 144 | } |
| 145 | if error.state == CommitState::Committed { |
| 146 | assert_eq!(observed, after); |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | persisted = disk.durable; |
| 151 | } |
| 152 | }); |