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