1use onestore::{ExGuid, FileDataReference, ObjectData, PropertySets, RevisionIndex, Store, Value};
2use std::fs;
3
4const TABLE: &str = "../../corpus/native/20260905-05/snapshots/07-table/notebook/synthetic.one";
5
6#[test]
7fn persisted_identities_preserve_native_byte_order_and_canonical_form() {
8 let text = "{00112233-4455-6677-8899-AABBCCDDEEFF},42";
9 let id: ExGuid = text.parse().unwrap();
10 assert_eq!(
11 id.guid,
12 [
13 0x33, 0x22, 0x11, 0, 0x55, 0x44, 0x77, 0x66, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
14 0xff
15 ]
16 );
17 assert_eq!(id.n, 42);
18 assert_eq!(text.to_lowercase().parse::<ExGuid>().unwrap(), id);
19 assert_eq!(id.to_string(), text);
20 let mut random = 7_u64;
21 for n in 0..1024 {
22 let guid = std::array::from_fn(|_| {
23 random = random.wrapping_mul(6364136223846793005).wrapping_add(1);
24 random.to_le_bytes()[0]
25 });
26 let id = ExGuid { guid, n };
27 assert_eq!(id.to_string().parse::<ExGuid>().unwrap(), id);
28 assert_eq!(
29 serde_json::from_str::<ExGuid>(&serde_json::to_string(&id).unwrap()).unwrap(),
30 id
31 );
32 }
33 for id in [
34 ExGuid::default(),
35 ExGuid {
36 guid: [255; 16],
37 n: u32::MAX,
38 },
39 ] {
40 assert_eq!(id.to_string().parse::<ExGuid>().unwrap(), id);
41 }
42 for at in 0..text.len() {
43 let mut changed = text.as_bytes().to_vec();
44 changed[at] = b'?';
45 assert!(
46 String::from_utf8(changed)
47 .unwrap()
48 .parse::<ExGuid>()
49 .is_err()
50 );
51 }
52 for changed in [
53 text.replace(",42", ",+42"),
54 text.replace(",42", ",042"),
55 text.replace(",42", ",4294967296"),
56 text.replace(",42", ",-1"),
57 text.replace("00", "+0"),
58 text.replace("00", "é"),
59 "{00000000-0000-0000-0000-000000000000},1".to_owned(),
60 format!(" {text}"),
61 format!("{text} "),
62 ] {
63 assert!(changed.parse::<ExGuid>().is_err(), "{changed}");
64 assert!(serde_json::from_str::<ExGuid>(&serde_json::to_string(&changed).unwrap()).is_err());
65 }
66}
67
68#[test]
69fn native_encryption_remains_opaque() {
70 for path in [
71 "../../corpus/native-encrypted/encrypted-01/notebook/synthetic.one",
72 "../../corpus/native-protected-boundaries/notebook/synthetic.one",
73 ] {
74 let bytes = fs::read(path).unwrap();
75 let store = Store::parse(&bytes).unwrap();
76 assert!(store.checksum_mismatches.is_empty());
77 let index = RevisionIndex::parse(&store).unwrap();
78 let mut encrypted = 0;
79 for (osid, space) in &index.spaces {
80 for (rid, revision) in &space.revisions {
81 let resolved = index.resolve(*osid, *rid).unwrap();
82 if revision.encrypted {
83 encrypted += 1;
84 assert!(
85 resolved
86 .objects
87 .values()
88 .any(|object| matches!(object.data, ObjectData::Encrypted(_)))
89 );
90 assert_eq!(
91 resolved.reachable().unwrap_err().message,
92 "Encrypted property references are unavailable"
93 );
94 }
95 }
96 }
97 assert!(encrypted > 0);
98 }
99}
100
101#[test]
102fn external_payload_names_cannot_escape_the_onefiles_directory() {
103 for (reference, valid) in [
104 ("<file>2a83ae62-6754-4383-8e2b-4033ff3cfba1.onebin", true),
105 (
106 "<file>../2a83ae62-6754-4383-8e2b-4033ff3cfba1.onebin",
107 false,
108 ),
109 (
110 "<file>C:\\2a83ae62-6754-4383-8e2b-4033ff3cfba1.onebin",
111 false,
112 ),
113 (
114 "<file>2a83ae62-6754-4383-8e2b-4033ff3cfba1.onebin:stream",
115 false,
116 ),
117 ("<ifndf>{2a83ae62-6754-4383-8e2b-4033ff3cfba1}", true),
118 ("<invfdo>", true),
119 ("<invfdo>garbage", false),
120 ] {
121 let bytes: Vec<_> = reference
122 .encode_utf16()
123 .flat_map(u16::to_le_bytes)
124 .collect();
125 let object = onestore::Object {
126 jcid: 0x80036,
127 reference_count: 1,
128 data: ObjectData::File {
129 reference: &bytes,
130 extension: &[],
131 },
132 global_ids: Default::default(),
133 };
134 assert_eq!(object.file_reference().is_ok(), valid, "{reference}");
135 }
136}
137
138#[test]
139fn active_revision_contains_current_text_and_exact_attachment_bytes() {
140 let bytes = fs::read(TABLE).unwrap();
141 let store = Store::parse(&bytes).unwrap();
142 let index = RevisionIndex::parse(&store).unwrap();
143 let mut strings = Vec::new();
144 let mut attachments = Vec::new();
145 let mut pages = 0;
146 for (osid, space) in &index.spaces {
147 let rid = space.labels[&(ExGuid::default(), 1)];
148 let revision = index.resolve(*osid, rid).unwrap();
149 for oid in revision.reachable().unwrap() {
150 let object = &revision.objects[&oid];
151 pages += usize::from(object.jcid == 0x6000b);
152 if let Some(FileDataReference::Internal(guid)) = object.file_reference().unwrap()
153 && object.jcid == 0x80036
154 {
155 attachments.push(store.file_data(guid).unwrap());
156 }
157 if object.jcid != 0x6000e {
158 continue;
159 }
160 let ObjectData::Properties(data) = object.data else {
161 panic!()
162 };
163 let props = PropertySets::parse(data).unwrap();
164 let unicode = props.sets[0]
165 .iter()
166 .find(|property| property.id == 0x1c001c22);
167 if let Some(property) = unicode {
168 let Value::Bytes(data) = property.value else {
169 panic!()
170 };
171 let text = String::from_utf16(
172 &data
173 .chunks_exact(2)
174 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
175 .collect::<Vec<_>>(),
176 )
177 .unwrap();
178 strings.push(text.strip_suffix('\0').unwrap().to_owned());
179 } else if let Some(property) = props.sets[0]
180 .iter()
181 .find(|property| property.id == 0x1c003498)
182 {
183 let Value::Bytes(data) = property.value else {
184 panic!()
185 };
186 strings.push(String::from_utf8(data.to_vec()).unwrap());
187 }
188 }
189 }
190 strings.sort();
191 assert_eq!(
192 strings,
193 [
194 "Fictitious positioned outline.",
195 "Fictitious: café, 東京, مرحبا",
196 "Left cell",
197 "Right cell"
198 ]
199 );
200 assert_eq!(pages, 1);
201 assert_eq!(
202 attachments,
203 [fs::read("../../corpus/native/20260905-05/assets/fictitious-attachment.txt").unwrap()]
204 );
205}
206
207#[test]
208fn self_dependent_revision_is_rejected() {
209 let mut bytes = fs::read(TABLE).unwrap();
210 let store = Store::parse(&bytes).unwrap();
211 let node = store
212 .lists
213 .values()
214 .flat_map(|list| &list.nodes)
215 .find(|node| node.id == 0x1e)
216 .unwrap();
217 let start = node.payload.as_ptr().addr() - bytes.as_ptr().addr();
218 bytes.copy_within(start..start + 20, start + 20);
219 let store = Store::parse(&bytes).unwrap();
220 assert_eq!(
221 RevisionIndex::parse(&store).unwrap_err().message,
222 "Revision dependency is not an earlier revision"
223 );
224}
225
226#[test]
227fn damaged_read_only_object_is_rejected_by_its_hash() {
228 let mut bytes = fs::read(TABLE).unwrap();
229 let store = Store::parse(&bytes).unwrap();
230 let node = store
231 .lists
232 .values()
233 .flat_map(|list| &list.nodes)
234 .find(|node| node.id == 0xc4)
235 .unwrap();
236 let onestore::Reference::Data(chunk) = node.reference.unwrap() else {
237 panic!()
238 };
239 bytes[usize::try_from(chunk.offset).unwrap()] ^= 1;
240 let store = Store::parse(&bytes).unwrap();
241 let index = RevisionIndex::parse(&store).unwrap();
242 assert!(
243 index
244 .spaces
245 .iter()
246 .any(|(osid, space)| space.revisions.keys().any(|rid| {
247 index
248 .resolve(*osid, *rid)
249 .is_err_and(|error| error.message == "Read-only object checksum mismatch")
250 }))
251 );
252}
253
254#[test]
255fn reference_counts_and_graph_cycles_are_checked_independently_of_chunk_hashes() {
256 let bytes = fs::read(TABLE).unwrap();
257 let store = Store::parse(&bytes).unwrap();
258 let index = RevisionIndex::parse(&store).unwrap();
259 let space = &index.spaces[&index.root];
260 let rid = space.labels[&(ExGuid::default(), 1)];
261 let mut revision = index.resolve(index.root, rid).unwrap();
262 revision.reachable().unwrap();
263 let root = revision.roots[&1];
264 revision.objects.get_mut(&root).unwrap().reference_count += 1;
265 assert_eq!(
266 revision.reachable().unwrap_err().message,
267 "Stored object reference count disagrees with the reachable graph"
268 );
269 let mut data = 0x80000001_u32.to_le_bytes().to_vec();
270 data.extend_from_slice(&(0x100 | root.n).to_le_bytes());
271 data.extend_from_slice(&1_u16.to_le_bytes());
272 data.extend_from_slice(&0x20000001_u32.to_le_bytes());
273 let object = revision.objects.get_mut(&root).unwrap();
274 object.data = ObjectData::Properties(&data);
275 object.global_ids = std::sync::Arc::new([(1, root.guid)].into());
276 assert_eq!(
277 revision.reachable().unwrap_err().message,
278 "Object references form a cycle"
279 );
280}