| 1 | #![no_main] |
| 2 | use libfuzzer_sys::fuzz_target; |
| 3 | use onestore::{ObjectData, Reference, RevisionIndex, Store, document::Document}; |
| 4 | use std::sync::LazyLock; |
| 5 | |
| 6 | static 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 | }); |
| 28 | static 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 | |
| 74 | fuzz_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 | }); |