1#![no_main]
2use libfuzzer_sys::fuzz_target;
3use onestore::{
4 Arena, Reference, RevisionIndex, Section, Store,
5 protected::{Key, Limits, UnlockedSection},
6};
7use std::{ops::Range, sync::LazyLock};
8
9type Source = (Vec<u8>, String, Vec<Range<usize>>, Key);
10
11static SOURCES: LazyLock<Vec<Source>> = LazyLock::new(|| {
12 [
13 (&include_bytes!("../../corpus/native-encrypted/encrypted-01/notebook/synthetic.one")[..], include_str!("../../corpus/native-encrypted/manifest.json")),
14 (&include_bytes!("../../corpus/native-protected-boundaries/notebook/synthetic.one")[..], include_str!("../../corpus/native-protected-boundaries/manifest.json")),
15 ].into_iter().map(|(bytes, manifest)| {
16 let manifest: serde_json::Value = serde_json::from_str(manifest).unwrap();
17 let store = Store::parse(bytes).unwrap();
18 let mut ranges: Vec<_> = store.lists.values().flat_map(|list| &list.nodes).filter_map(|node| {
19 let Reference::Data(chunk) = node.reference? else { return None; };
20 let start = usize::try_from(chunk.offset).unwrap();
21 let end = start + usize::try_from(chunk.length).unwrap();
22 (start < end).then_some(start..end)
23 }).collect();
24 ranges.sort_by_key(|range| (range.start, range.end));
25 ranges.dedup();
26 let password = manifest["password"].as_str().unwrap().to_owned();
27 let key = Key::open(bytes, &password).unwrap();
28 (bytes.to_vec(), password, ranges, key)
29 }).collect()
30});
31
32fuzz_target!(|data: &[u8]| {
33 if data.len() < 8 {
34 return;
35 }
36 let (source, password, ranges, key) = &SOURCES[usize::from(data[0]) % SOURCES.len()];
37 let mut bytes = source.clone();
38 let mut password = password.clone();
39 let mode = data[1] % 3;
40 if mode == 2 {
41 password.push_str(&String::from_utf8_lossy(&data[8..]));
42 } else {
43 let range = if mode == 0 {
44 0..bytes.len()
45 } else {
46 ranges[usize::from(u16::from_le_bytes([data[2], data[3]])) % ranges.len()].clone()
47 };
48 let offset = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize % range.len();
49 let count = (range.len() - offset).min(data.len() - 8);
50 bytes[range.start + offset..range.start + offset + count]
51 .copy_from_slice(&data[8..8 + count]);
52 }
53 // The kept-open section decodes the same bytes under the section's key.
54 if let Ok(mut section) = Section::unlock(&Arena::default(), bytes.clone(), key) {
55 for (space, ..) in section.pages().unwrap_or_default() {
56 let _ = section.page(space);
57 }
58 }
59 let Ok(store) = Store::parse(&bytes) else {
60 return;
61 };
62 let Ok(index) = RevisionIndex::parse(&store) else {
63 return;
64 };
65 let result = UnlockedSection::open(
66 &index,
67 &password,
68 Limits {
69 kdf_rounds: 200000,
70 decoded_bytes: 4 * 1024 * 1024,
71 object_visits: 20000,
72 },
73 );
74 if mode == 2 && data.len() > 8 {
75 assert!(result.is_err());
76 }
77 if let Ok(unlocked) = result {
78 let document = unlocked.document().unwrap();
79 let _ = document.pages();
80 for revision in document.spaces.values().flat_map(|s| s.revisions.values()) {
81 for (id, node) in &revision.nodes {
82 if matches!(node.kind, onestore::document::Kind::RichText { .. }) {
83 let _ = revision.text_runs(*id);
84 }
85 }
86 }
87 }
88});