1#[path = "support/ops.rs"]
2mod ops;
3use onestore::{
4 ExGuid, FileDataReference, IdStream, ObjectData, PropertySets, RevisionIndex, Store, Value,
5 document::Document,
6};
7use std::collections::{BTreeMap, BTreeSet};
8
9fn compare_copy(left: &Store<'_>, right: &Store<'_>, source: ExGuid, copied: ExGuid) {
10 let indexes = [left, right].map(|store| RevisionIndex::parse(store).unwrap());
11 let mut contexts = vec![(source, ExGuid::default(), copied, ExGuid::default())];
12 let mut mapped_contexts = BTreeMap::new();
13 let mut inverse_contexts = BTreeMap::new();
14 while let Some((ls, lc, rs, rc)) = contexts.pop() {
15 if let Some(previous) = mapped_contexts.insert((ls, lc), (rs, rc)) {
16 assert_eq!(previous, (rs, rc));
17 continue;
18 }
19 assert_eq!(inverse_contexts.insert((rs, rc), (ls, lc)), None);
20 let revisions =
21 [(ls, lc, &indexes[0]), (rs, rc, &indexes[1])].map(|(sid, context, index)| {
22 index
23 .resolve(sid, index.spaces[&sid].labels[&(context, 1)])
24 .unwrap()
25 });
26 assert_eq!(
27 revisions[0].roots.keys().collect::<Vec<_>>(),
28 revisions[1].roots.keys().collect::<Vec<_>>()
29 );
30 let manifest = &revisions[1].objects[&revisions[1].roots[&1]];
31 let copied_time = if manifest.jcid == 0x60037 {
32 let pages = manifest.references().unwrap().objects;
33 assert_eq!(pages.len(), 1);
34 let ObjectData::Properties(bytes) = revisions[1].objects[&pages[0]].data else {
35 panic!()
36 };
37 let properties = PropertySets::parse(bytes).unwrap();
38 let Value::Bytes(time) = properties.sets[0]
39 .iter()
40 .find(|p| p.id == 0x14001d7a)
41 .unwrap()
42 .value
43 else {
44 panic!()
45 };
46 Some(time)
47 } else {
48 None
49 };
50 let mut pending: Vec<_> = revisions[0]
51 .roots
52 .iter()
53 .map(|(role, id)| (*id, revisions[1].roots[role]))
54 .collect();
55 let mut mapped = BTreeMap::new();
56 let mut inverse = BTreeMap::new();
57 while let Some((a, b)) = pending.pop() {
58 if let Some(previous) = mapped.insert(a, b) {
59 assert_eq!(previous, b);
60 continue;
61 }
62 if let Some(previous) = inverse.insert(b, a) {
63 let originals = [&revisions[0].objects[&previous], &revisions[0].objects[&a]];
64 assert!(originals.iter().all(|object| object.jcid == 0x120001));
65 assert_eq!(originals[0].data, originals[1].data);
66 for object in originals {
67 let references = object.references().unwrap();
68 assert!(references.objects.is_empty());
69 assert!(references.object_spaces.is_empty());
70 assert!(references.contexts.is_empty());
71 }
72 }
73 let objects = [&revisions[0].objects[&a], &revisions[1].objects[&b]];
74 assert_eq!(objects[0].jcid, objects[1].jcid);
75 match (objects[0].data, objects[1].data) {
76 (ObjectData::Properties(a), ObjectData::Properties(b)) => {
77 let sets = [a, b].map(|bytes| PropertySets::parse(bytes).unwrap());
78 let mut nested = vec![(0, 0)];
79 while let Some((a, b)) = nested.pop() {
80 let fields = [(0, a), (1, b)].map(|(side, set)| {
81 let properties = &sets[side].sets[set];
82 let fields = properties
83 .iter()
84 .map(|p| (p.id, &p.value))
85 .collect::<BTreeMap<_, _>>();
86 assert_eq!(fields.len(), properties.len());
87 fields
88 });
89 assert_eq!(
90 fields[0].keys().collect::<Vec<_>>(),
91 fields[1].keys().collect::<Vec<_>>()
92 );
93 for (id, value) in &fields[0] {
94 let other = fields[1][id];
95 match (value, other) {
96 (
97 Value::References {
98 stream,
99 compact_ids,
100 },
101 Value::References {
102 stream: other_stream,
103 compact_ids: other_ids,
104 },
105 ) => {
106 assert_eq!(stream, other_stream);
107 assert_eq!(compact_ids.len(), other_ids.len());
108 for (a, b) in
109 compact_ids.chunks_exact(4).zip(other_ids.chunks_exact(4))
110 {
111 let ids = [a, b].map(|bytes| {
112 u32::from_le_bytes(bytes.try_into().unwrap())
113 });
114 let [a, b] = [0, 1].map(|side| ExGuid {
115 guid: objects[side].global_ids[&(ids[side] >> 8)],
116 n: ids[side] & 255,
117 });
118 match stream {
119 IdStream::Objects => pending.push((a, b)),
120 IdStream::Contexts => contexts.push((ls, a, rs, b)),
121 IdStream::ObjectSpaces => contexts.push((
122 a,
123 ExGuid::default(),
124 b,
125 ExGuid::default(),
126 )),
127 }
128 }
129 }
130 (Value::Sets(a), Value::Sets(b)) => {
131 assert_eq!(a.len(), b.len());
132 nested.extend(a.clone().zip(b.clone()));
133 }
134 (Value::Bytes(_), Value::Bytes(level))
135 if objects[0].jcid == 0x20030
136 && *id == 0x14001dff
137 && lc == ExGuid::default() =>
138 {
139 assert_eq!(*level, 1_u32.to_le_bytes());
140 }
141 (Value::Bytes(_), Value::Bytes(time))
142 if matches!(
143 objects[0].jcid,
144 0x6000b
145 | 0x6000c
146 | 0x6002c
147 | 0x60022
148 | 0x60012
149 | 0x60023
150 | 0x60024
151 ) && *id == 0x14001d7a
152 && lc == ExGuid::default() =>
153 {
154 if value != &other {
155 assert_eq!(Some(*time), copied_time);
156 }
157 }
158 _ => assert_eq!(
159 value, &other,
160 "property {id:#x}, jcid {:#x}, context {lc}, source set {a}, copied set {b}",
161 objects[0].jcid
162 ),
163 }
164 }
165 }
166 }
167 (ObjectData::File { extension: a, .. }, ObjectData::File { extension: b, .. }) => {
168 assert_eq!(a, b);
169 let references =
170 objects.map(|object| object.file_reference().unwrap().unwrap());
171 let [
172 FileDataReference::Internal(a),
173 FileDataReference::Internal(b),
174 ] = references
175 else {
176 panic!("The copy fixture requires its external payloads");
177 };
178 assert_eq!(left.file_data(a).unwrap(), right.file_data(b).unwrap());
179 }
180 _ => panic!("Copied object representations differ"),
181 }
182 }
183 }
184}
185
186#[test]
187fn native_parent_removal_retains_history_and_copies_the_complete_page() {
188 let bytes = [
189 include_bytes!("../../../corpus/page-lifecycle/05-reordered/notebook/Lifecycle.one").as_slice(),
190 include_bytes!("../../../corpus/page-lifecycle/06-deleted-parent/notebook/Lifecycle.one").as_slice(),
191 include_bytes!("../../../corpus/page-lifecycle/06-deleted-parent/notebook/OneNote_RecycleBin/OneNote_DeletedPages.one").as_slice(),
192 ];
193 let stores = bytes.map(|b| Store::parse(b).unwrap());
194 let indexes = stores.each_ref().map(|s| RevisionIndex::parse(s).unwrap());
195 let documents = indexes.each_ref().map(|i| Document::parse(i).unwrap());
196 let sid = documents[0].pages().unwrap()[4].0;
197 assert!(!documents[1].spaces.contains_key(&sid));
198 assert_eq!(
199 indexes[0].spaces.keys().collect::<Vec<_>>(),
200 indexes[1].spaces.keys().collect::<Vec<_>>()
201 );
202 for (space, revisions) in &indexes[0].spaces {
203 for rid in revisions.revisions.keys() {
204 assert_eq!(
205 format!("{:?}", indexes[0].resolve(*space, *rid).unwrap()),
206 format!("{:?}", indexes[1].resolve(*space, *rid).unwrap())
207 );
208 }
209 }
210 let current = indexes[1]
211 .resolve(sid, indexes[1].spaces[&sid].labels[&(ExGuid::default(), 1)])
212 .unwrap();
213 let ObjectData::Properties(manifest) = current.objects[&current.roots[&1]].data else {
214 panic!()
215 };
216 assert!(PropertySets::parse(manifest).unwrap().sets[0].is_empty());
217 let ObjectData::Properties(metadata) = current.objects[&current.roots[&2]].data else {
218 panic!()
219 };
220 assert!(
221 PropertySets::parse(metadata).unwrap().sets[0]
222 .iter()
223 .any(|p| p.id == 0x88001de9)
224 );
225 let copied = documents[2].pages().unwrap();
226 assert_eq!(copied.len(), 1);
227 compare_copy(&stores[0], &stores[2], sid, copied[0].0);
228}
229
230fn compare_labels(before: &RevisionIndex<'_>, after: &RevisionIndex<'_>) {
231 assert_eq!(
232 before.spaces.keys().collect::<Vec<_>>(),
233 after.spaces.keys().collect::<Vec<_>>()
234 );
235 for (sid, space) in &before.spaces {
236 assert_eq!(
237 space.labels.keys().collect::<Vec<_>>(),
238 after.spaces[sid].labels.keys().collect::<Vec<_>>()
239 );
240 for (label, rid) in &space.labels {
241 assert_eq!(
242 format!("{:?}", before.resolve(*sid, *rid).unwrap()),
243 format!(
244 "{:?}",
245 after
246 .resolve(*sid, after.spaces[sid].labels[label])
247 .unwrap()
248 ),
249 "cold context {sid}/{label:?}"
250 );
251 }
252 }
253}
254
255fn native_case(name: &str) {
256 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
257 .join("../../corpus/page-lifecycle/removal")
258 .join(name);
259 let request: serde_json::Value =
260 serde_json::from_slice(&std::fs::read(root.join("removal.json")).unwrap()).unwrap();
261 let selected: BTreeSet<usize> = serde_json::from_value(request["selected"].clone()).unwrap();
262 let bytes = ["native/before", "native/after", "cold"]
263 .map(|phase| std::fs::read(root.join(phase).join("notebook/Lifecycle.one")).unwrap());
264 let stores = bytes.each_ref().map(|bytes| Store::parse(bytes).unwrap());
265 let indexes = stores.each_ref().map(|store| {
266 assert!(store.checksum_mismatches.is_empty());
267 let index = RevisionIndex::parse(store).unwrap();
268 index.validate_current().unwrap();
269 index
270 });
271 let documents = indexes
272 .each_ref()
273 .map(|index| Document::parse(index).unwrap());
274 let before = documents[0].pages().unwrap();
275 let retained: Vec<_> = before
276 .iter()
277 .enumerate()
278 .filter(|(index, _)| !selected.contains(index))
279 .map(|(_, page)| *page)
280 .collect();
281 assert_eq!(documents[1].pages().unwrap(), retained);
282 assert_eq!(documents[2].pages().unwrap(), retained);
283 let groups = documents.each_ref().map(|document| {
284 let space = &document.spaces[&document.root];
285 let revision = &space.revisions[&space.contexts[&ExGuid::default()]];
286 revision.nodes[&revision.roots[&1]]
287 .children
288 .iter()
289 .map(|id| (*id, revision.nodes[id].spaces.clone()))
290 .collect::<Vec<_>>()
291 });
292 let mut expected: Vec<(ExGuid, Vec<ExGuid>)> = Vec::new();
293 for (sid, _) in &retained {
294 let space = &documents[0].spaces[sid];
295 let revision = &space.revisions[&space.contexts[&ExGuid::default()]];
296 let onestore::document::Kind::Metadata { level, .. } =
297 revision.nodes[&revision.roots[&2]].kind
298 else {
299 panic!()
300 };
301 if expected.is_empty() || level == Some(1) {
302 let (series, _) = groups[0]
303 .iter()
304 .find(|(_, pages)| pages.contains(sid))
305 .unwrap();
306 expected.push((*series, Vec::new()));
307 }
308 expected.last_mut().unwrap().1.push(*sid);
309 }
310 assert_eq!(groups[1], expected);
311 assert_eq!(groups[2], expected);
312 for (phase, index) in indexes[1..].iter().enumerate() {
313 assert_eq!(
314 indexes[0].spaces.keys().collect::<Vec<_>>(),
315 index.spaces.keys().collect::<Vec<_>>()
316 );
317 let mut discarded = 0;
318 for (sid, space) in &indexes[0].spaces {
319 for rid in space.revisions.keys() {
320 if !index.spaces[sid].revisions.contains_key(rid) {
321 discarded += 1;
322 continue;
323 }
324 assert_eq!(
325 format!("{:?}", indexes[0].resolve(*sid, *rid).unwrap()),
326 format!("{:?}", index.resolve(*sid, *rid).unwrap()),
327 "historical revision {sid}/{rid}"
328 );
329 }
330 for ((context, role), rid) in &space.labels {
331 if *context == ExGuid::default()
332 || selected.iter().any(|ordinal| before[*ordinal].0 == *sid)
333 {
334 continue;
335 }
336 let retained = index.spaces[sid].labels[&(*context, *role)];
337 assert_eq!(
338 format!("{:?}", indexes[0].resolve(*sid, *rid).unwrap()),
339 format!("{:?}", index.resolve(*sid, retained).unwrap()),
340 "labeled history {sid}/{context}/{role}"
341 );
342 }
343 }
344 assert_eq!(
345 discarded,
346 match name {
347 "ink" => 9,
348 "features" => 8,
349 _ => 0,
350 },
351 "native compaction phase {phase}"
352 );
353 }
354 compare_labels(&indexes[1], &indexes[2]);
355 let active = |side: usize, sid| {
356 indexes[side]
357 .resolve(
358 sid,
359 indexes[side].spaces[&sid].labels[&(ExGuid::default(), 1)],
360 )
361 .unwrap()
362 };
363 for (ordinal, (sid, _)) in before.iter().enumerate() {
364 let old = active(0, *sid);
365 let after = active(1, *sid);
366 assert_eq!(old.roots, after.roots);
367 if selected.contains(&ordinal) {
368 let ObjectData::Properties(manifest) = after.objects[&after.roots[&1]].data else {
369 panic!()
370 };
371 assert!(PropertySets::parse(manifest).unwrap().sets[0].is_empty());
372 let ObjectData::Properties(metadata) = after.objects[&after.roots[&2]].data else {
373 panic!()
374 };
375 assert!(
376 PropertySets::parse(metadata).unwrap().sets[0]
377 .iter()
378 .any(|p| p.id == 0x88001de9)
379 );
380 } else {
381 assert_eq!(
382 old.objects.keys().collect::<Vec<_>>(),
383 after.objects.keys().collect::<Vec<_>>()
384 );
385 for (id, object) in &old.objects {
386 if *id != old.roots[&2] {
387 assert_eq!(
388 format!("{object:?}"),
389 format!("{:?}", after.objects[id]),
390 "survivor {sid}/{id}"
391 );
392 } else {
393 let fields = [object, &after.objects[id]].map(|object| {
394 let ObjectData::Properties(bytes) = object.data else {
395 panic!()
396 };
397 PropertySets::parse(bytes).unwrap().sets[0]
398 .iter()
399 .filter(|p| p.id != 0x14001dff)
400 .map(|p| (p.id, format!("{:?}", p.value)))
401 .collect::<BTreeMap<_, _>>()
402 });
403 assert_eq!(fields[0], fields[1]);
404 use onestore::document::Kind;
405 let levels = [0, 1].map(|side| {
406 let space = &documents[side].spaces[sid];
407 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
408 let Kind::Metadata { level, .. } = view.nodes[id].kind else {
409 panic!()
410 };
411 level
412 });
413 assert_eq!(
414 levels[1],
415 if retained.first().map(|p| p.0) == Some(*sid) {
416 Some(1)
417 } else {
418 levels[0]
419 }
420 );
421 }
422 }
423 }
424 }
425 if request["permanent"].as_bool().unwrap() {
426 assert!(
427 !root
428 .join("native/after/notebook/OneNote_RecycleBin")
429 .exists()
430 );
431 return;
432 }
433 let recycle_bytes = std::fs::read(
434 root.join("native/after/notebook/OneNote_RecycleBin/OneNote_DeletedPages.one"),
435 )
436 .unwrap();
437 let recycle = Store::parse(&recycle_bytes).unwrap();
438 let recycled_index = RevisionIndex::parse(&recycle).unwrap();
439 recycled_index.validate_current().unwrap();
440 let recycled_document = Document::parse(&recycled_index).unwrap();
441 let cold_recycle_bytes =
442 std::fs::read(root.join("cold/notebook/OneNote_RecycleBin/OneNote_DeletedPages.one"))
443 .unwrap();
444 let cold_recycle = Store::parse(&cold_recycle_bytes).unwrap();
445 let cold_index = RevisionIndex::parse(&cold_recycle).unwrap();
446 cold_index.validate_current().unwrap();
447 compare_labels(&recycled_index, &cold_index);
448 let pages = recycled_document.pages().unwrap();
449 assert_eq!(pages.len(), selected.len());
450 let identity = |index: &RevisionIndex<'_>, sid| {
451 let revision = index
452 .resolve(sid, index.spaces[&sid].labels[&(ExGuid::default(), 1)])
453 .unwrap();
454 let ObjectData::Properties(bytes) = revision.objects[&revision.roots[&2]].data else {
455 panic!()
456 };
457 let sets = PropertySets::parse(bytes).unwrap();
458 let Value::Bytes(guid) = sets.sets[0]
459 .iter()
460 .find(|p| p.id == 0x1c001c30)
461 .unwrap()
462 .value
463 else {
464 panic!()
465 };
466 guid.to_vec()
467 };
468 let mut copies = BTreeMap::new();
469 for (sid, _) in pages {
470 assert!(copies.insert(identity(&recycled_index, sid), sid).is_none());
471 }
472 for ordinal in selected {
473 let sid = before[ordinal].0;
474 let copied = copies.remove(&identity(&indexes[0], sid)).unwrap();
475 compare_copy(&stores[0], &recycle, sid, copied);
476 compare_copy(&stores[0], &cold_recycle, sid, copied);
477 }
478 assert!(copies.is_empty());
479}
480
481macro_rules! native_cases {
482 ($($name:ident => $case:literal),* $(,)?) => {$(
483 #[test]
484 fn $name() { native_case($case); }
485 )*};
486}
487
488native_cases! {
489 trailing => "trailing", parent => "parent", child => "child", grandchild => "grandchild",
490 group => "group", all => "all", permanent_parent => "permanent-parent",
491 leading_parent => "leading-parent", leading_group => "leading-group",
492 features => "features", ink => "ink",
493}
494
495#[test]
496fn copy_comparison_rejects_text_and_payload_changes() {
497 let original = include_bytes!(
498 "../../../corpus/page-lifecycle/removal/ink/native/before/notebook/Lifecycle.one"
499 );
500 let copied = include_bytes!(
501 "../../../corpus/page-lifecycle/removal/ink/native/after/notebook/OneNote_RecycleBin/OneNote_DeletedPages.one"
502 );
503 let stores = [original.as_slice(), copied.as_slice()].map(|bytes| Store::parse(bytes).unwrap());
504 let indexes = stores
505 .each_ref()
506 .map(|store| RevisionIndex::parse(store).unwrap());
507 let documents = indexes
508 .each_ref()
509 .map(|index| Document::parse(index).unwrap());
510 let [source_sid, copy_sid] = documents
511 .each_ref()
512 .map(|document| document.pages().unwrap()[0].0);
513 compare_copy(&stores[0], &stores[1], source_sid, copy_sid);
514 let space = &documents[1].spaces[&copy_sid];
515 let revision = &space.revisions[&space.contexts[&ExGuid::default()]];
516 let text = revision
517 .nodes
518 .iter()
519 .find_map(|(oid, node)| {
520 matches!(
521 node.kind,
522 onestore::document::Kind::RichText {
523 boilerplate: false,
524 ..
525 }
526 )
527 .then_some(*oid)
528 })
529 .unwrap();
530 let changed = ops::page_edited(
531 copied,
532 copy_sid,
533 vec![onestore::op::PageOp::Text {
534 text,
535 range: 0..0,
536 with: "Changed ".into(),
537 }],
538 )
539 .unwrap();
540 let changed_store = Store::parse(&changed).unwrap();
541 RevisionIndex::parse(&changed_store)
542 .unwrap()
543 .validate_current()
544 .unwrap();
545 assert!(
546 std::panic::catch_unwind(|| compare_copy(&stores[0], &changed_store, source_sid, copy_sid))
547 .is_err()
548 );
549
550 let raw = indexes[1]
551 .resolve(
552 copy_sid,
553 indexes[1].spaces[&copy_sid].labels[&(ExGuid::default(), 1)],
554 )
555 .unwrap();
556 let attachment = revision
557 .nodes
558 .values()
559 .find_map(|node| match node.kind {
560 onestore::document::Kind::Attachment { container, .. } => container,
561 _ => None,
562 })
563 .unwrap();
564 let Some(FileDataReference::Internal(guid)) =
565 raw.objects[&attachment].file_reference().unwrap()
566 else {
567 panic!()
568 };
569 let payload = stores[1].file_data(guid).unwrap();
570 assert!(!payload.is_empty());
571 let offset = payload.as_ptr().addr() - copied.as_ptr().addr();
572 let mut changed = copied.to_vec();
573 changed[offset] ^= 1;
574 let changed_store = Store::parse(&changed).unwrap();
575 assert!(changed_store.checksum_mismatches.is_empty());
576 RevisionIndex::parse(&changed_store)
577 .unwrap()
578 .validate_current()
579 .unwrap();
580 assert_ne!(changed_store.file_data(guid).unwrap(), payload);
581 assert!(
582 std::panic::catch_unwind(|| compare_copy(&stores[0], &changed_store, source_sid, copy_sid))
583 .is_err()
584 );
585}