1use onestore::{
2 ExGuid, RevisionIndex, Store,
3 document::{Document, Kind},
4};
5use std::fs;
6
7#[test]
8fn documented_cell_shading_survives_native_2010_ignoring_its_display() {
9 let bytes =
10 fs::read("../../corpus/m6/cell-shading-control-01/input/notebook/synthetic.one").unwrap();
11 let store = Store::parse(&bytes).unwrap();
12 let index = RevisionIndex::parse(&store).unwrap();
13 let document = Document::parse(&index).unwrap();
14 let colors: Vec<_> = document
15 .spaces
16 .values()
17 .flat_map(|s| s.revisions.values())
18 .flat_map(|r| r.nodes.values())
19 .filter_map(|n| match n.kind {
20 Kind::Cell { shading, .. } => shading,
21 _ => None,
22 })
23 .collect();
24 assert_eq!(colors, [0x0000ffff]);
25}
26
27#[test]
28fn native_saved_collapse_default_preserves_descendants() {
29 for (path, expected) in [
30 (
31 "../../corpus/m6/native-features-01/notebook/Features.one",
32 1,
33 ),
34 (
35 "../../corpus/m6/rust-collapse-control-01/input/notebook/Features.one",
36 0,
37 ),
38 ] {
39 let bytes = fs::read(path).unwrap();
40 let store = Store::parse(&bytes).unwrap();
41 let index = RevisionIndex::parse(&store).unwrap();
42 let document = Document::parse(&index).unwrap();
43 let mut parents = 0;
44 for revision in document.spaces.values().flat_map(|s| s.revisions.values()) {
45 for node in revision.nodes.values() {
46 if let Kind::Paragraph { collapse_state, .. } = node.kind
47 && node.content.iter().any(|id| {
48 matches!(&revision.nodes[id].kind, Kind::RichText { text, .. } if text == "Collapsed parent")
49 })
50 {
51 assert_eq!(collapse_state, Some(expected));
52 assert_eq!(node.children.len(), 1);
53 assert_eq!(revision.nodes[&node.children[0]].children.len(), 1);
54 parents += 1;
55 }
56 }
57 }
58 assert_eq!(parents, 1);
59 }
60}
61
62#[test]
63fn native_math_preserves_run_data_and_distinguishes_plain_text() {
64 let bytes = fs::read("../../corpus/m6/native-math-01/notebook/synthetic.one").unwrap();
65 let store = Store::parse(&bytes).unwrap();
66 let index = RevisionIndex::parse(&store).unwrap();
67 let document = Document::parse(&index).unwrap();
68 let mut equations = 0;
69 let mut inline = 0;
70 for revision in document.spaces.values().flat_map(|s| s.revisions.values()) {
71 for (id, node) in &revision.nodes {
72 let Kind::RichText { text, runs, .. } = &node.kind else {
73 continue;
74 };
75 let resolved = revision.text_runs(*id).unwrap();
76 if text.contains('\u{fdd0}') {
77 equations += 1;
78 assert!(resolved.iter().all(|r| r.format.math == Some(true)));
79 let expected = if text.contains('π‘₯') {
80 assert_eq!(
81 text,
82 "\u{fdd0}π‘₯\u{fdee}2\u{fdef}+\u{fdd0}𝑦\u{fdee}2\u{fdef}=\u{fdd0}𝑧\u{fdee}2\u{fdef}"
83 );
84 11
85 } else {
86 assert_eq!(text, "\u{fdd0}π‘Ž+𝑏\u{fdee}𝑐+𝑑\u{fdef}");
87 3
88 };
89 assert_eq!(runs.len(), expected);
90 for (i, run) in runs.iter().enumerate() {
91 assert_eq!(run.extra_set, Some(i + 1));
92 assert!(!node.extra[run.extra_set.unwrap()].is_empty());
93 }
94 }
95 if text == "Inline 𝛼+𝛽" {
96 inline += 1;
97 assert_eq!(resolved.len(), 2);
98 assert_eq!(resolved[0].text, "Inline ");
99 assert_eq!(resolved[0].format.math, Some(false));
100 assert_eq!(resolved[1].text, "𝛼+𝛽");
101 assert_eq!(resolved[1].format.math, Some(true));
102 assert!(runs.iter().all(|r| r.extra_set.is_none()));
103 }
104 }
105 }
106 assert_eq!((equations, inline), (2, 1));
107}
108
109#[test]
110fn native_origin_control_retains_source_coordinate_frame() {
111 let bytes =
112 fs::read("../../corpus/m6/native-origin-controls-01/input/notebook/synthetic.one").unwrap();
113 let store = Store::parse(&bytes).unwrap();
114 let index = RevisionIndex::parse(&store).unwrap();
115 let document = Document::parse(&index).unwrap();
116 let (revision, page) = document
117 .spaces
118 .values()
119 .flat_map(|space| space.revisions.values())
120 .find_map(|revision| {
121 revision
122 .nodes
123 .values()
124 .find(|node| matches!(node.kind, Kind::Page { .. }))
125 .map(|page| (revision, page))
126 })
127 .unwrap();
128 let Kind::Page {
129 margin_origin_x,
130 margin_origin_y,
131 rtl,
132 ..
133 } = page.kind
134 else {
135 unreachable!()
136 };
137 assert_eq!(margin_origin_x, Some(108.0));
138 assert_eq!(margin_origin_y, Some(-36.0));
139 assert_ne!(rtl, Some(true));
140 let outline = &revision.nodes[&page.children[0]];
141 assert_eq!(outline.layout.x, Some(36.0));
142 assert!((outline.layout.y.unwrap() - 14.4).abs() < 0.002);
143}
144
145#[test]
146fn native_rtl_table_retains_visual_column_order() {
147 let bytes =
148 fs::read("../../corpus/m6/native-page-direction-03/notebook/synthetic.one").unwrap();
149 let store = Store::parse(&bytes).unwrap();
150 let index = RevisionIndex::parse(&store).unwrap();
151 let document = Document::parse(&index).unwrap();
152 let revision = document
153 .spaces
154 .values()
155 .flat_map(|space| space.revisions.values())
156 .find(|revision| {
157 revision.nodes.values().any(|node| {
158 matches!(
159 node.kind,
160 Kind::Page {
161 rtl: Some(true),
162 ..
163 }
164 )
165 })
166 })
167 .unwrap();
168 let table = revision
169 .nodes
170 .values()
171 .find(|node| matches!(node.kind, Kind::Table { .. }))
172 .unwrap();
173 let Kind::Table { widths, locked, .. } = &table.kind else {
174 unreachable!()
175 };
176 assert_eq!(widths, &[144.0, 96.0]);
177 assert_eq!(locked, &[true, true]);
178 let row = &revision.nodes[&table.children[0]];
179 let text: Vec<_> = row
180 .children
181 .iter()
182 .map(|id| {
183 let paragraph = &revision.nodes[&revision.nodes[id].children[0]];
184 revision
185 .text_runs(paragraph.content[0])
186 .unwrap()
187 .iter()
188 .map(|run| run.text)
189 .collect::<String>()
190 })
191 .collect();
192 assert_eq!(text, ["Right cell", "Left cell"]);
193}
194
195#[test]
196fn native_default_template_has_a_named_page_relationship() {
197 let bytes =
198 fs::read("../../corpus/m6/native-template-controls-01/notebook/synthetic.one").unwrap();
199 let store = Store::parse(&bytes).unwrap();
200 let index = RevisionIndex::parse(&store).unwrap();
201 let document = Document::parse(&index).unwrap();
202 let root = &document.spaces[&document.root];
203 let root = &root.revisions[&root.contexts[&ExGuid::default()]];
204 let Kind::Section {
205 default_template: Some(template),
206 } = root.nodes[&root.roots[&1]].kind
207 else {
208 panic!("Missing default template")
209 };
210 let template = &document.spaces[&template];
211 let template = &template.revisions[&template.contexts[&ExGuid::default()]];
212 let Kind::TemplateMetadata { name } = &template.nodes[&template.roots[&2]].kind else {
213 panic!("Missing template metadata")
214 };
215 assert_eq!(name.as_deref(), Some("Hearts"));
216 let manifest = &template.nodes[&template.roots[&1]];
217 assert_eq!(manifest.content.len(), 1);
218 assert!(matches!(
219 template.nodes[&manifest.content[0]].kind,
220 Kind::Page { .. }
221 ));
222}
223
224#[test]
225fn native_paragraph_breaks_preserve_every_stored_character() {
226 let bytes =
227 fs::read("../../corpus/m6/native-break-controls-02/notebook/synthetic.one").unwrap();
228 let store = Store::parse(&bytes).unwrap();
229 let index = RevisionIndex::parse(&store).unwrap();
230 let document = Document::parse(&index).unwrap();
231 let (revision, outline) = document
232 .spaces
233 .values()
234 .flat_map(|s| s.revisions.values())
235 .find_map(|revision| {
236 revision
237 .nodes
238 .values()
239 .find(|node| matches!(node.kind, Kind::Outline { .. }) && node.children.len() == 24)
240 .map(|node| (revision, node))
241 })
242 .unwrap();
243 let expected = [
244 "A", "A\r", "A\r\r", "\rA", "\rA\r", "\r", "\r\r", "A\rB\r", "A\rB\r", "AB", "A\rB\r",
245 "A\rB\r",
246 ];
247 for (case, paragraph) in outline.children.iter().enumerate() {
248 let runs = revision
249 .text_runs(revision.nodes[paragraph].content[0])
250 .unwrap();
251 let visible: Vec<_> = runs
252 .iter()
253 .filter(|r| r.format.hidden != Some(true))
254 .collect();
255 assert_eq!(
256 visible.iter().map(|r| r.text).collect::<String>(),
257 expected[case % 12],
258 "case {case}"
259 );
260 for run in visible {
261 for character in run.text.chars() {
262 assert_eq!(
263 run.link.is_some(),
264 case >= 12 && character != '\r',
265 "case {case}"
266 );
267 }
268 }
269 }
270}
271
272#[test]
273fn referenced_history_contexts_require_history_roots_where_stored() {
274 let bytes = fs::read("../../corpus/m6/native-features-01/notebook/Features.one").unwrap();
275 let store = Store::parse(&bytes).unwrap();
276 let mut index = RevisionIndex::parse(&store).unwrap();
277 let document = Document::parse(&index).unwrap();
278 let (sid, context, current) = document
279 .spaces
280 .iter()
281 .find_map(|(sid, space)| {
282 let current = space.contexts[&ExGuid::default()];
283 let revision = &space.revisions[&current];
284 match revision.nodes[&revision.roots[&1]].kind {
285 Kind::Manifest {
286 history: Some(context),
287 } => Some((*sid, context, current)),
288 _ => None,
289 }
290 })
291 .unwrap();
292 let space = &document.spaces[&sid];
293 let history = &space.revisions[&space.contexts[&context]];
294 assert!(matches!(
295 history.nodes[&history.roots[&1]].kind,
296 Kind::VersionHistory
297 ));
298 assert!(history.nodes[&history.roots[&1]].children.is_empty());
299 index
300 .spaces
301 .get_mut(&sid)
302 .unwrap()
303 .labels
304 .insert((context, 1), current);
305 assert_eq!(
306 Document::parse(&index).unwrap_err().message,
307 "History context does not resolve to version history"
308 );
309 // OneNote's Copy Page To of a page version names a history it never writes
310 // (corpus/page-versions/native/step-03).
311 index
312 .spaces
313 .get_mut(&sid)
314 .unwrap()
315 .labels
316 .remove(&(context, 1));
317 let document = Document::parse(&index).unwrap();
318 assert!(!document.spaces[&sid].contexts.contains_key(&context));
319}
320
321#[test]
322fn native_hyperlink_boundaries_preserve_whitespace_and_literal_labels() {
323 let bytes = fs::read("../../corpus/m6/native-link-controls-01/notebook/Links.one").unwrap();
324 let store = Store::parse(&bytes).unwrap();
325 let index = RevisionIndex::parse(&store).unwrap();
326 let document = Document::parse(&index).unwrap();
327 let (space, outline) = document
328 .spaces
329 .values()
330 .flat_map(|s| s.revisions.values())
331 .find_map(|space| {
332 space.nodes.values().find_map(|node| {
333 (matches!(node.kind, Kind::Outline { .. }) && node.children.len() == 10)
334 .then_some((space, node))
335 })
336 })
337 .unwrap();
338 let expected = [
339 (" label ", "label "),
340 ("\tlabel\t", "label\t"),
341 ("\u{a0}label\u{a0}", "\u{a0}label\u{a0}"),
342 (" \tlabel \t", "label \t"),
343 ("label", "label"),
344 (" ", ""),
345 ("label", "label"),
346 (" ]]> end", "]]> end"),
347 ("<b> & \"quoted\"", "<b> & \"quoted\""),
348 ("\rlabel\r", "label"),
349 ];
350 for (number, (paragraph, (text, linked))) in outline.children.iter().zip(expected).enumerate() {
351 let runs = space.text_runs(space.nodes[paragraph].content[0]).unwrap();
352 let visible: Vec<_> = runs
353 .iter()
354 .filter(|r| r.format.hidden != Some(true))
355 .collect();
356 assert_eq!(visible.iter().map(|r| r.text).collect::<String>(), text);
357 assert_eq!(
358 visible
359 .iter()
360 .filter(|r| r.link.is_some())
361 .map(|r| r.text)
362 .collect::<String>(),
363 linked
364 );
365 let target = format!("https://example.invalid/label/{number}");
366 for run in visible {
367 if let Some(link) = run.link {
368 assert_eq!(link, target);
369 }
370 assert_eq!(run.format.bold, Some(true));
371 assert_eq!(run.format.font_size, Some(14.0));
372 assert_eq!(run.format.underline, Some(false));
373 assert_eq!(run.format.color, Some(0xff000000));
374 }
375 }
376}
377
378#[test]
379fn native_unicode_table_assets_and_coordinates_are_typed() {
380 let bytes =
381 fs::read("../../corpus/native/20260905-05/snapshots/07-table/notebook/synthetic.one")
382 .unwrap();
383 let store = Store::parse(&bytes).unwrap();
384 let index = RevisionIndex::parse(&store).unwrap();
385 let document = Document::parse(&index).unwrap();
386 let nodes: Vec<_> = document
387 .spaces
388 .values()
389 .flat_map(|s| s.revisions.values())
390 .flat_map(|r| r.nodes.values())
391 .collect();
392 assert!(nodes.iter().any(|n| matches!(&n.kind, Kind::RichText { text, .. } if text == "Fictitious: café, 東京, Ω…Ψ±Ψ­Ψ¨Ψ§")));
393 let table = nodes
394 .iter()
395 .find(|n| matches!(n.kind, Kind::Table { .. }))
396 .unwrap();
397 let Kind::Table {
398 rows,
399 columns,
400 widths,
401 ..
402 } = &table.kind
403 else {
404 unreachable!()
405 };
406 assert_eq!(
407 (*rows, *columns, table.children.len(), widths.len()),
408 (Some(1), Some(2), 1, 2)
409 );
410 assert_eq!(
411 nodes
412 .iter()
413 .filter(|n| matches!(n.kind, Kind::Cell { .. }))
414 .count(),
415 2
416 );
417 assert_eq!(
418 nodes
419 .iter()
420 .filter(|n| matches!(n.kind, Kind::Attachment { .. }))
421 .count(),
422 1
423 );
424 assert!(nodes.iter().any(|n| matches!(&n.kind, Kind::File { payload: Some(data), .. } if data.starts_with(b"\x89PNG"))));
425 assert!(nodes.iter().any(|n| matches!(n.kind, Kind::Outline { .. })
426 && n.layout.x == Some(144.0)
427 && n.layout.y == Some(96.0)));
428 for node in nodes {
429 if let Kind::RichText { text, runs, .. } = &node.kind {
430 assert_eq!(runs.first().unwrap().start, 0);
431 assert_eq!(
432 runs.last().unwrap().end as usize,
433 text.encode_utf16().count()
434 );
435 }
436 }
437}
438
439#[test]
440fn encrypted_content_is_accounted_without_fabricated_plaintext() {
441 for path in [
442 "../../corpus/native-encrypted/encrypted-01/notebook/synthetic.one",
443 "../../corpus/native-protected-boundaries/notebook/synthetic.one",
444 ] {
445 let bytes = fs::read(path).unwrap();
446 let store = Store::parse(&bytes).unwrap();
447 let index = RevisionIndex::parse(&store).unwrap();
448 let document = Document::parse(&index).unwrap();
449 assert!(!document.spaces.is_empty());
450 let nodes: Vec<_> = document
451 .spaces
452 .values()
453 .flat_map(|s| s.revisions.values())
454 .flat_map(|r| r.nodes.values())
455 .collect();
456 assert!(!nodes.is_empty());
457 for node in nodes {
458 assert!(matches!(node.kind, Kind::Encrypted { ciphertext } if !ciphertext.is_empty()));
459 }
460 }
461}
462
463#[test]
464fn rust_unicode_and_native_latin1_use_utf16_run_offsets() {
465 let text = "AπŸ˜€e\u{301}東京";
466 let bytes = onestore::create_section("unicode.one", text, "Fictitious").unwrap();
467 let store = Store::parse(&bytes).unwrap();
468 let index = RevisionIndex::parse(&store).unwrap();
469 let document = Document::parse(&index).unwrap();
470 let node = document
471 .spaces
472 .values()
473 .flat_map(|s| s.revisions.values())
474 .flat_map(|r| r.nodes.values())
475 .find(|n| matches!(n.kind, Kind::RichText { .. }))
476 .unwrap();
477 let Kind::RichText {
478 text: actual, runs, ..
479 } = &node.kind
480 else {
481 unreachable!()
482 };
483 assert_eq!(actual, text);
484 assert_eq!(runs[0].end, 7);
485 let expected = format!(
486 "Extended bytes: {}",
487 (128u8..=255)
488 .map(|b| char::from(b).to_string())
489 .collect::<Vec<_>>()
490 .join(" ")
491 );
492 // OneNote 2010 reads TextExtendedAscii as Latin-1 whatever the run's language
493 // (corpus/extended-ascii/{en,ru,ja}/cold).
494 for language in ["en", "ru", "ja"] {
495 let path = format!("../../corpus/extended-ascii/{language}/candidate/synthetic.one");
496 let bytes = fs::read(path).unwrap();
497 let store = Store::parse(&bytes).unwrap();
498 let index = RevisionIndex::parse(&store).unwrap();
499 let document = Document::parse(&index).unwrap();
500 assert!(
501 document
502 .spaces
503 .values()
504 .flat_map(|s| s.revisions.values())
505 .flat_map(|r| r.nodes.values())
506 .any(|n| matches!(&n.kind, Kind::RichText { text, .. } if text == &expected))
507 );
508 }
509}
510
511#[test]
512fn toc_preserves_section_order_and_filename() {
513 let bytes = onestore::create_table_of_contents(
514 "Open Notebook.onetoc2",
515 &[("z.one", [1; 16]), ("a.one", [2; 16])],
516 )
517 .unwrap();
518 let store = Store::parse(&bytes).unwrap();
519 let index = RevisionIndex::parse(&store).unwrap();
520 let document = Document::parse(&index).unwrap();
521 let space = &document.spaces[&document.root];
522 let space = &space.revisions[&space.contexts[&ExGuid::default()]];
523 let root = &space.nodes[&space.roots[&1]];
524 let Kind::Toc { entries, .. } = &root.kind else {
525 panic!("Missing TOC")
526 };
527 let names: Vec<_> = entries
528 .iter()
529 .map(|id| match &space.nodes[id].kind {
530 Kind::Toc { filename, .. } => filename.as_deref().unwrap(),
531 _ => panic!("Missing TOC entry"),
532 })
533 .collect();
534 assert_eq!(names, ["z.one", "a.one"]);
535 assert_ne!(document.root, ExGuid::default());
536}
537
538#[test]
539fn native_printouts_tasks_and_recording_associations_are_typed() {
540 let bytes = fs::read("../../corpus/m6/native-features-01/notebook/Features.one").unwrap();
541 let store = Store::parse(&bytes).unwrap();
542 let index = RevisionIndex::parse(&store).unwrap();
543 let document = Document::parse(&index).unwrap();
544 let mut roles = Vec::new();
545 let mut task = false;
546 let mut recording = false;
547 let mut annotation = false;
548 let mut decimal = false;
549 let mut inherited_strike = false;
550 let media = [
551 0x57, 0x8e, 0x41, 0x9a, 0x64, 0x7f, 0xc5, 0x40, 0x87, 0xd0, 0x8a, 0x8f, 0x7c, 0x1e, 0x54,
552 0x22,
553 ];
554 for space in document.spaces.values().flat_map(|s| s.revisions.values()) {
555 for (oid, node) in &space.nodes {
556 match &node.kind {
557 Kind::Image {
558 alt,
559 background,
560 printout,
561 ..
562 } if alt.as_deref() == Some("Role pattern") => {
563 let border = if *printout == Some(true) { 1.44 } else { 0.0 };
564 assert!((node.layout.max_width.unwrap() + border - 288.0).abs() < 0.002);
565 assert!((node.layout.max_height.unwrap() + border - 192.0).abs() < 0.002);
566 roles.push((*background, *printout));
567 }
568 Kind::Attachment {
569 filename,
570 recording_id,
571 ..
572 } if filename.as_deref() == Some("silence.wav") => {
573 assert_eq!(*recording_id, Some(media));
574 recording = true;
575 }
576 Kind::RichText { text, .. } if text == "Recording annotation" => {
577 assert_eq!(node.media_ids, [media]);
578 assert_eq!(node.media_time_ms, Some(500));
579 annotation = true;
580 }
581 Kind::RichText { text, .. } if text == "Disabled standalone task" => {
582 assert_eq!(node.tags.len(), 1);
583 let tag = &node.tags[0];
584 assert_eq!(
585 (tag.definition, tag.action_type, tag.status),
586 (None, Some(100), 6)
587 );
588 assert_eq!((tag.start, tag.due), (Some(1262390400), Some(1262476800)));
589 assert_eq!(tag.completed, Some(0));
590 assert_eq!(tag.task_id.unwrap()[..4], [0xe3, 0x40, 0xaf, 0x45]);
591 task = true;
592 }
593 Kind::RichText { text, .. } if text == "Inherited strike and spacing" => {
594 let runs = space.text_runs(*oid).unwrap();
595 assert_eq!(runs[0].format.strike, Some(true));
596 assert_eq!(runs[0].format.space_before, Some(9.0));
597 assert_eq!(runs[0].format.space_after, Some(4.5));
598 inherited_strike = true;
599 }
600 Kind::List { format, .. } if format.as_deref() == Some("\u{fffd}\0") => {
601 decimal = true;
602 }
603 _ => {}
604 }
605 }
606 }
607 roles.sort();
608 assert_eq!(
609 roles,
610 [
611 (Some(false), Some(false)),
612 (Some(false), Some(true)),
613 (Some(true), Some(false)),
614 (Some(true), Some(true))
615 ]
616 );
617 assert!(task && recording && annotation && decimal && inherited_strike);
618}
619
620#[test]
621fn native_partial_black_highlight_and_inline_hyperlink_fields_survive() {
622 let bytes = fs::read("../../corpus/m6/native-probes-01/notebook/synthetic.one").unwrap();
623 let store = Store::parse(&bytes).unwrap();
624 let index = RevisionIndex::parse(&store).unwrap();
625 let document = Document::parse(&index).unwrap();
626 let mut black = false;
627 let mut link = false;
628 for space in document.spaces.values().flat_map(|s| s.revisions.values()) {
629 for (oid, node) in &space.nodes {
630 if let Kind::RichText { text, runs, .. } = &node.kind {
631 if text == "Before middle after" {
632 let highlighted: Vec<_> = runs
633 .iter()
634 .filter(|run| {
635 run.format
636 .is_some_and(|id| space.nodes[&id].format.highlight == Some(0))
637 })
638 .collect();
639 assert_eq!(highlighted.len(), 1);
640 assert_eq!((highlighted[0].start, highlighted[0].end), (7, 13));
641 black = true;
642 }
643 if text.starts_with("Before \u{fddf}HYPERLINK") {
644 let units: Vec<_> = text.encode_utf16().collect();
645 let mut visible = String::new();
646 let mut hidden = String::new();
647 for run in runs {
648 let fragment =
649 String::from_utf16(&units[run.start as usize..run.end as usize])
650 .unwrap();
651 if run
652 .format
653 .is_some_and(|id| space.nodes[&id].format.hidden == Some(true))
654 {
655 hidden.push_str(&fragment);
656 } else {
657 visible.push_str(&fragment);
658 }
659 }
660 assert_eq!(visible, "Before Link label after");
661 let resolved = space.text_runs(*oid).unwrap();
662 let links: Vec<_> = resolved
663 .iter()
664 .filter_map(|r| r.link.map(|url| (r.text, url, r.format.bold)))
665 .collect();
666 assert_eq!(
667 links,
668 [(
669 "Link label",
670 "https://example.invalid/fixture?x=1&y=2",
671 Some(true)
672 )]
673 );
674 assert_eq!(
675 resolved
676 .iter()
677 .filter(|r| r.format.hidden != Some(true))
678 .map(|r| r.text)
679 .collect::<String>(),
680 visible
681 );
682 assert_eq!(
683 hidden,
684 "\u{fddf}HYPERLINK \"https://example.invalid/fixture?x=1&y=2\""
685 );
686 link = true;
687 }
688 }
689 }
690 }
691 assert!(black && link);
692}
693
694#[test]
695fn malformed_run_boundaries_fail_after_valid_storage_decoding() {
696 let bytes = fs::read("../../corpus/m6/native-probes-01/notebook/synthetic.one").unwrap();
697 let store = Store::parse(&bytes).unwrap();
698 let index = RevisionIndex::parse(&store).unwrap();
699 let document = Document::parse(&index).unwrap();
700 let (sid, rid, oid) = document
701 .spaces
702 .iter()
703 .find_map(|(sid, space)| {
704 space.revisions.iter().find_map(|(rid, revision)| {
705 revision.nodes.iter().find_map(|(oid, node)| {
706 matches!(&node.kind, Kind::RichText { text, .. } if text.starts_with("BMP "))
707 .then_some((*sid, *rid, *oid))
708 })
709 })
710 })
711 .unwrap();
712 let revision = index.resolve(sid, rid).unwrap();
713 let onestore::ObjectData::Properties(properties) = revision.objects[&oid].data else {
714 unreachable!()
715 };
716 let properties = onestore::PropertySets::parse(properties).unwrap();
717 let onestore::Value::Bytes(boundaries) = properties.sets[0]
718 .iter()
719 .find(|p| p.id == 0x1c001e12)
720 .unwrap()
721 .value
722 else {
723 unreachable!()
724 };
725 // Damage the stored boundaries where they lie, as no writer would store them.
726 let at = boundaries.as_ptr().addr() - bytes.as_ptr().addr();
727 for invalid in [5u32, u32::MAX] {
728 let mut mutated = bytes.to_vec();
729 mutated[at..at + 4].copy_from_slice(&invalid.to_le_bytes());
730 let store = Store::parse(&mutated).unwrap();
731 let index = RevisionIndex::parse(&store).unwrap();
732 index.validate_current().unwrap();
733 let error = Document::parse(&index).unwrap_err();
734 assert_eq!(
735 error.message,
736 if invalid == 5 {
737 "Text-run boundary splits a surrogate pair"
738 } else {
739 "Text-run indices exceed text or are out of order"
740 }
741 );
742 }
743}
744
745#[test]
746fn native_lists_locked_columns_and_named_styles_are_preserved() {
747 let bytes = fs::read("../../corpus/m6/native-structure-01/notebook/synthetic.one").unwrap();
748 let store = Store::parse(&bytes).unwrap();
749 let index = RevisionIndex::parse(&store).unwrap();
750 let document = Document::parse(&index).unwrap();
751 let mut found = (false, false, false);
752 for space in document.spaces.values().flat_map(|s| s.revisions.values()) {
753 for (oid, node) in &space.nodes {
754 match &node.kind {
755 Kind::Table {
756 rows,
757 columns,
758 locked,
759 widths,
760 ..
761 } => {
762 assert_eq!((*rows, *columns), (Some(3), Some(2)));
763 assert_eq!(locked, &[true, false]);
764 assert_eq!(widths[0], 120.0);
765 assert!(widths[1] > 37.0 && widths[1] < 38.0);
766 found.0 = true;
767 }
768 Kind::List {
769 format,
770 restart: Some(3),
771 bullet,
772 ..
773 } => {
774 assert_eq!(format.as_deref(), Some("\u{fffd}\u{1}."));
775 assert_eq!(*bullet, None);
776 found.1 = true;
777 }
778 Kind::RichText { text, .. } if text == "Named paragraph style" => {
779 let runs = space.text_runs(*oid).unwrap();
780 assert_eq!(runs[0].format.bold, Some(true));
781 assert_eq!(runs[0].format.font_size, Some(20.0));
782 assert_eq!(runs[0].format.space_before, Some(288.0));
783 assert_eq!(runs[0].format.space_after, Some(144.0));
784 assert_eq!(runs[0].format.alignment, Some(2));
785 found.2 = true;
786 }
787 _ => {}
788 }
789 }
790 }
791 assert_eq!(found, (true, true, true));
792 let (revision, outer) = document
793 .spaces
794 .values()
795 .flat_map(|s| s.revisions.values())
796 .find_map(|revision| {
797 revision
798 .nodes
799 .values()
800 .find(|node| {
801 matches!(node.kind, Kind::Paragraph { .. }) && !node.children.is_empty()
802 })
803 .map(|node| (revision, node))
804 })
805 .unwrap();
806 assert_eq!(outer.child_level, Some(1));
807 assert_eq!(
808 revision.text_runs(outer.content[0]).unwrap()[0].text,
809 "Outer bullet"
810 );
811 assert_eq!(outer.children.len(), 1);
812 let nested = &revision.nodes[&outer.children[0]];
813 assert_eq!(
814 revision.text_runs(nested.content[0]).unwrap()[0].text,
815 "Nested bullet"
816 );
817}
818
819#[test]
820fn native_conflict_relationship_and_opaque_ink_are_retained() {
821 let bytes =
822 fs::read("../../corpus/collaboration/round-01/offline/notebook/synthetic.one").unwrap();
823 let store = Store::parse(&bytes).unwrap();
824 let index = RevisionIndex::parse(&store).unwrap();
825 let document = Document::parse(&index).unwrap();
826 let (conflict_id, conflict) = document
827 .spaces
828 .iter()
829 .find_map(|(sid, space)| {
830 let revision = &space.revisions[&space.contexts[&ExGuid::default()]];
831 revision.roots.get(&2).and_then(|root| {
832 matches!(revision.nodes[root].kind, Kind::ConflictMetadata { .. })
833 .then_some((sid, revision))
834 })
835 })
836 .unwrap();
837 let Kind::ConflictMetadata {
838 title,
839 author,
840 level,
841 } = &conflict.nodes[&conflict.roots[&2]].kind
842 else {
843 unreachable!()
844 };
845 assert_eq!(title.as_deref(), Some("Native edited while disconnected."));
846 assert_eq!(author.as_deref(), Some("snow"));
847 assert_eq!(*level, Some(1));
848 assert!(
849 document
850 .spaces
851 .iter()
852 .any(|(sid, space)| sid != conflict_id && {
853 let revision = &space.revisions[&space.contexts[&ExGuid::default()]];
854 revision.nodes[&revision.roots[&1]]
855 .spaces
856 .contains(conflict_id)
857 })
858 );
859 let bytes = fs::read("../../corpus/native-ink/cold-ui-ink/notebook/synthetic.one").unwrap();
860 let store = Store::parse(&bytes).unwrap();
861 let index = RevisionIndex::parse(&store).unwrap();
862 let document = Document::parse(&index).unwrap();
863 let ink: Vec<_> = document
864 .spaces
865 .values()
866 .flat_map(|s| s.revisions.values())
867 .flat_map(|r| r.nodes.values())
868 .filter(|node| matches!(node.kind, Kind::Ink { .. }))
869 .collect();
870 assert_eq!(ink.len(), 2);
871 assert!(
872 ink.iter()
873 .all(|node| node.extra.iter().any(|set| !set.is_empty()))
874 );
875}