1use onestore::{Chunk, FileDataReference, Reference, RevisionIndex, Store};
2use std::{fs, path::Path};
3
4const TEXT: &str = "../../corpus/native/20260905-05/snapshots/02-text/notebook/synthetic.one";
5
6#[test]
7fn native_file_data_alignment_is_relative_to_the_object() {
8 let source =
9 fs::read("../../corpus/m6/native-template-controls-01/notebook/synthetic.one").unwrap();
10 let store = Store::parse(&source).unwrap();
11 let node = store
12 .lists
13 .values()
14 .flat_map(|l| &l.nodes)
15 .find(|n| n.id == 0x94)
16 .unwrap();
17 let guid = node.payload[node.payload.len() - 16..].try_into().unwrap();
18 let Some(Reference::Data(chunk)) = node.reference else {
19 panic!("Missing native image container")
20 };
21 assert_eq!(chunk.offset % 8, 4);
22 let payload = store.file_data(guid).unwrap();
23 assert_eq!(payload.len(), 10056);
24 assert_eq!(
25 format!("{:x}", md5::compute(payload)),
26 "fec8f53bd6db9305eabac4cd4b71fb08"
27 );
28 let blob = store.chunk_data(chunk).unwrap();
29 for alignment in 0..8 {
30 let mut bytes = source.clone();
31 let offset = bytes.len().next_multiple_of(8) + alignment;
32 bytes.resize(offset, 0);
33 bytes.extend_from_slice(blob);
34 let mut relocated = Store::parse(&bytes).unwrap();
35 let node = relocated
36 .lists
37 .values_mut()
38 .flat_map(|l| &mut l.nodes)
39 .find(|n| n.id == 0x94)
40 .unwrap();
41 node.reference = Some(Reference::Data(Chunk {
42 offset: offset as u64,
43 length: chunk.length,
44 }));
45 assert_eq!(
46 relocated.file_data(guid).unwrap(),
47 payload,
48 "alignment {alignment}"
49 );
50 }
51 let mut corrupt = source.clone();
52 corrupt[(chunk.offset + chunk.length - 1) as usize] ^= 1;
53 assert!(Store::parse(&corrupt).unwrap().file_data(guid).is_err());
54}
55
56fn files(path: &Path, result: &mut Vec<std::path::PathBuf>) {
57 for entry in fs::read_dir(path).unwrap() {
58 let path = entry.unwrap().path();
59 if path.is_dir() {
60 files(&path, result);
61 } else if path
62 .extension()
63 .is_some_and(|ext| ext == "one" || ext == "onetoc2")
64 {
65 result.push(path);
66 }
67 }
68}
69
70#[test]
71fn native_corpus_has_committed_lists_and_matching_checksums() {
72 let mut paths = Vec::new();
73 for path in [
74 "../../corpus/native",
75 "../../corpus/native-ink",
76 "../../corpus/native-delete",
77 ] {
78 files(Path::new(path), &mut paths);
79 }
80 assert!(paths.len() >= 35);
81 for path in paths {
82 let bytes = fs::read(&path).unwrap();
83 let store =
84 Store::parse(&bytes).unwrap_or_else(|error| panic!("{}: {error}", path.display()));
85 assert!(store.checksum_mismatches.is_empty(), "{}", path.display());
86 assert!(!store.lists.is_empty());
87 let index = RevisionIndex::parse(&store)
88 .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
89 index
90 .validate_current()
91 .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
92 for (osid, space) in &index.spaces {
93 for rid in space.revisions.keys() {
94 let revision = index
95 .resolve(*osid, *rid)
96 .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
97 revision
98 .reachable()
99 .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
100 for object in revision.objects.values() {
101 if let Some(FileDataReference::Internal(guid)) =
102 object.file_reference().unwrap()
103 {
104 store.file_data(guid).unwrap();
105 }
106 }
107 }
108 }
109 }
110}
111
112#[test]
113fn compressed_nil_references_are_normalized_before_scaling() {
114 let source = fs::read(TEXT).unwrap();
115 let store = Store::parse(&source).unwrap();
116 let node = store
117 .lists
118 .values()
119 .flat_map(|list| &list.nodes)
120 .find(|node| node.id == 0x84)
121 .unwrap();
122 for (format, width) in [(0_u32, 8_usize), (1, 4), (2, 2), (3, 4)] {
123 let mut bytes = source.clone();
124 let header = u32::from_le_bytes(bytes[node.offset..node.offset + 4].try_into().unwrap());
125 let changed = (header & !((3 << 23) | (3 << 25))) | (format << 23) | (2 << 25);
126 bytes[node.offset..node.offset + 4].copy_from_slice(&changed.to_le_bytes());
127 bytes[node.offset + 4..node.offset + 4 + width].fill(0xff);
128 bytes[node.offset + 4 + width] = 0;
129 let parsed = Store::parse(&bytes).unwrap();
130 let changed = parsed
131 .lists
132 .values()
133 .flat_map(|list| &list.nodes)
134 .find(|candidate| candidate.offset == node.offset)
135 .unwrap();
136 assert_eq!(
137 changed.reference,
138 Some(onestore::Reference::Data(onestore::Chunk {
139 offset: u64::MAX,
140 length: 0
141 }))
142 );
143 }
144}
145
146#[test]
147fn native_log_prefixes_define_readable_states() {
148 let mut bytes =
149 fs::read("../../corpus/native/20260905-05/snapshots/00-empty/notebook/synthetic.one")
150 .unwrap();
151 let transactions = Store::parse(&bytes).unwrap().header.transaction_count;
152 assert_eq!(transactions, 4);
153 for count in 1..=transactions {
154 bytes[96..100].copy_from_slice(&count.to_le_bytes());
155 let store =
156 Store::parse(&bytes).unwrap_or_else(|error| panic!("transaction {count}: {error}"));
157 assert!(store.checksum_mismatches.is_empty());
158 }
159}
160
161#[test]
162fn unused_links_and_tail_bytes_are_ignored() {
163 let mut bytes = fs::read(TEXT).unwrap();
164 let store = Store::parse(&bytes).unwrap();
165 let expected: Vec<_> = store
166 .lists
167 .iter()
168 .map(|(id, list)| (*id, list.nodes.len()))
169 .collect();
170 let tails: Vec<_> = store
171 .lists
172 .values()
173 .map(|list| *list.fragments.last().unwrap())
174 .collect();
175 for tail in tails {
176 let end = usize::try_from(tail.offset + tail.length).unwrap();
177 bytes[end - 20..end - 8].fill(0xfe);
178 }
179 bytes.extend_from_slice(&[0xff; 4096]);
180 let store = Store::parse(&bytes).unwrap();
181 assert_eq!(
182 store
183 .lists
184 .iter()
185 .map(|(id, list)| (*id, list.nodes.len()))
186 .collect::<Vec<_>>(),
187 expected
188 );
189}
190
191#[test]
192fn malformed_structure_is_rejected() {
193 let source = fs::read(TEXT).unwrap();
194 let root = usize::try_from(Store::parse(&source).unwrap().header.root.offset).unwrap();
195 for (offset, replacement) in [
196 (96, vec![0; 4]),
197 (160, vec![0xff; 12]),
198 (root, vec![0; 8]),
199 (root + 12, 1_u32.to_le_bytes().to_vec()),
200 (root + 16, vec![0; 4]),
201 ] {
202 let mut bytes = source.clone();
203 bytes[offset..offset + replacement.len()].copy_from_slice(&replacement);
204 assert!(Store::parse(&bytes).is_err(), "offset {offset:#x}");
205 }
206 for end in 0..1024 {
207 assert!(Store::parse(&source[..end]).is_err());
208 }
209}
210
211#[test]
212fn bit_changes_in_header_and_first_chunks_do_not_panic() {
213 let mut bytes = fs::read(TEXT).unwrap();
214 for index in 0..4096 {
215 for bit in 0..8 {
216 bytes[index] ^= 1 << bit;
217 let _ = Store::parse(&bytes);
218 bytes[index] ^= 1 << bit;
219 }
220 }
221}
222
223#[test]
224fn checksum_damage_is_reported_without_discarding_committed_content() {
225 let mut bytes = fs::read(TEXT).unwrap();
226 bytes[2060] ^= 1;
227 let store = Store::parse(&bytes).unwrap();
228 assert_eq!(store.checksum_mismatches[0], 2056);
229 assert_eq!(store.header.transaction_count, 16);
230}
231
232#[test]
233fn native_toc_continuation_excludes_the_flushed_entry_from_crc() {
234 let path = "../../corpus/m6/native-structure-01/notebook/Open Notebook.onetoc2";
235 let mut bytes = fs::read(path).unwrap();
236 let store = Store::parse(&bytes).unwrap();
237 assert_eq!(store.header.transaction_log.length, 44);
238 assert_eq!(
239 u64::from_le_bytes(bytes[0x640..0x648].try_into().unwrap()),
240 0xac4
241 );
242 assert!(store.checksum_mismatches.is_empty());
243 assert_eq!(&bytes[0xacc..0xad4], &[1, 0, 0, 0, 0x29, 0x9d, 0xf0, 0x68]);
244 bytes[0xad0] ^= 1;
245 assert_eq!(Store::parse(&bytes).unwrap().checksum_mismatches, [0xacc]);
246}