1#![no_main]
2use libfuzzer_sys::fuzz_target;
3use onestore::{ObjectData, Reference, RevisionIndex, Store, document::Document};
4use std::sync::LazyLock;
5
6static SOURCES: LazyLock<Vec<Vec<u8>>> = LazyLock::new(|| {
7 let mut sources = vec![
8 include_bytes!("../../corpus/append/round-01/complex/notebook/synthetic.one").to_vec(),
9 include_bytes!("../../corpus/m6/native-probes-01/notebook/synthetic.one").to_vec(),
10 include_bytes!("../../corpus/m6/native-structure-01/notebook/synthetic.one").to_vec(),
11 include_bytes!("../../corpus/m6/native-features-01/notebook/Features.one").to_vec(),
12 include_bytes!("../../corpus/m6/native-template-controls-01/notebook/synthetic.one")
13 .to_vec(),
14 include_bytes!("../../corpus/m6/native-break-controls-02/notebook/synthetic.one").to_vec(),
15 include_bytes!("../../corpus/m6/native-empty-link-01/notebook/synthetic.one").to_vec(),
16 include_bytes!("../../corpus/m6/native-origin-controls-01/input/notebook/synthetic.one")
17 .to_vec(),
18 include_bytes!("../../corpus/m6/native-page-direction-03/notebook/synthetic.one").to_vec(),
19 include_bytes!("../../corpus/m6/native-math-01/notebook/synthetic.one").to_vec(),
20 include_bytes!("../../corpus/collaboration/round-01/offline/notebook/synthetic.one")
21 .to_vec(),
22 ];
23 if let Some(paths) = std::env::var_os("ONESTORE_DOCUMENT_SEEDS") {
24 sources.extend(std::env::split_paths(&paths).map(|path| std::fs::read(path).unwrap()));
25 }
26 sources
27});
28static RANGES: LazyLock<Vec<(usize, std::ops::Range<usize>, Vec<usize>)>> = LazyLock::new(|| {
29 let mut ranges = Vec::new();
30 for (source_index, source) in SOURCES.iter().enumerate() {
31 let store = Store::parse(source).unwrap();
32 let index = RevisionIndex::parse(&store).unwrap();
33 let document = Document::parse(&index).unwrap();
34 for (sid, space) in &document.spaces {
35 for rid in space.revisions.keys() {
36 let revision = index.resolve(*sid, *rid).unwrap();
37 for oid in revision.reachable().unwrap() {
38 let object = &revision.objects[&oid];
39 if let ObjectData::Properties(bytes) = object.data {
40 let start = bytes.as_ptr().addr() - source.as_ptr().addr();
41 let hashes = store
42 .lists
43 .values()
44 .flat_map(|list| &list.nodes)
45 .filter_map(|node| {
46 if matches!(node.id, 0xc2 | 0xc4 | 0xc5)
47 && let Some(Reference::Data(chunk)) = node.reference
48 && chunk.offset == start as u64
49 && chunk.length == bytes.len() as u64
50 {
51 Some(
52 node.payload.as_ptr().addr() - source.as_ptr().addr()
53 + node.payload.len()
54 - 16,
55 )
56 } else {
57 None
58 }
59 })
60 .collect();
61 if !ranges.iter().any(|(s, r, _)| {
62 *s == source_index && *r == (start..start + bytes.len())
63 }) {
64 ranges.push((source_index, start..start + bytes.len(), hashes));
65 }
66 }
67 }
68 }
69 }
70 }
71 ranges
72});
73
74fuzz_target!(|data: &[u8]| {
75 if data.len() < 4 {
76 return;
77 }
78 let (source, range, hashes) =
79 &RANGES[usize::from(u16::from_le_bytes([data[0], data[1]])) % RANGES.len()];
80 let offset = usize::from(u16::from_le_bytes([data[2], data[3]])) % range.len();
81 let mut bytes = SOURCES[*source].to_vec();
82 let size = (range.len() - offset).min(data.len() - 4);
83 bytes[range.start + offset..range.start + offset + size].copy_from_slice(&data[4..4 + size]);
84 let digest = md5::compute(&bytes[range.clone()]).0;
85 for offset in hashes {
86 bytes[*offset..*offset + 16].copy_from_slice(&digest);
87 }
88 let store = Store::parse(&bytes).unwrap();
89 if let Ok(index) = RevisionIndex::parse(&store) {
90 if let Ok(document) = Document::parse(&index) {
91 for space in document.spaces.values().flat_map(|s| s.revisions.values()) {
92 for (id, node) in &space.nodes {
93 if matches!(node.kind, onestore::document::Kind::RichText { .. }) {
94 let _ = space.text_runs(*id);
95 }
96 }
97 }
98 }
99 }
100});