1use onestore::{
2 ExGuid, PageCreation, RevisionIndex, Store,
3 document::{Document, Kind},
4 op::{PageOp, SectionOp},
5 page::{Page, PageObject},
6};
7
8#[path = "support/current.rs"]
9mod current;
10#[path = "support/disk.rs"]
11mod disk;
12#[path = "support/ops.rs"]
13mod ops;
14
15const SOURCE: &[u8] =
16 include_bytes!("../../../corpus/page-lifecycle/03-renamed/notebook/Lifecycle.one");
17
18#[test]
19fn native_section_accepts_empty_and_titled_pages_with_preserved_history() {
20 let original = Store::parse(SOURCE).unwrap();
21 let old_index = RevisionIndex::parse(&original).unwrap();
22 let original_pages = Document::parse(&old_index).unwrap().pages().unwrap();
23 let mut expected = original_pages.clone();
24 let mut source = SOURCE.to_vec();
25 for (ordinal, title) in [None, Some(""), Some("New 🦋 é"), Some("New 🦋 é")]
26 .into_iter()
27 .enumerate()
28 {
29 let before = (ordinal == 1).then_some(original_pages[0].0);
30 let intent = PageCreation::new(before, title, "Rust author").unwrap();
31 let serialized = serde_json::to_vec(&intent).unwrap();
32 let retained = serde_json::from_slice(&serialized).unwrap();
33 assert_eq!(intent, retained);
34 let prepared = ops::section_op(&source, SectionOp::Create(retained.clone())).unwrap();
35 let store = Store::parse(prepared.as_bytes()).unwrap();
36 assert_eq!(
37 store.header.transaction_count,
38 original.header.transaction_count + ordinal as u32 + 1
39 );
40 assert_eq!(store.header.file_id, original.header.file_id);
41 let index = RevisionIndex::parse(&store).unwrap();
42 index.validate_current().unwrap();
43 let document = Document::parse(&index).unwrap();
44 let at = if ordinal == 1 { 0 } else { expected.len() };
45 expected.insert(at, (intent.space(), intent.object()));
46 assert_eq!(document.pages().unwrap(), expected);
47 let view = &document.spaces[&intent.space()];
48 let view = &view.revisions[&view.contexts[&ExGuid::default()]];
49 assert!(view.nodes[&intent.object()].children.is_empty());
50 assert_eq!(
51 view.nodes[&intent.object()].structure.len(),
52 usize::from(title.is_some())
53 );
54 let Kind::Metadata {
55 title: actual,
56 level,
57 } = &view.nodes[&view.roots[&2]].kind
58 else {
59 panic!()
60 };
61 assert_eq!(actual.as_deref(), Some(title.unwrap_or_default()));
62 assert_eq!(*level, Some(1));
63 if let Some(text) = intent.title_object() {
64 let Kind::RichText {
65 text, boilerplate, ..
66 } = &view.nodes[&text].kind
67 else {
68 panic!()
69 };
70 assert_eq!(text, title.unwrap());
71 assert!(!boilerplate);
72 }
73 for (sid, space) in &old_index.spaces {
74 for rid in space.revisions.keys() {
75 assert_eq!(
76 format!("{:?}", old_index.resolve(*sid, *rid).unwrap()),
77 format!("{:?}", index.resolve(*sid, *rid).unwrap())
78 );
79 }
80 if *sid != old_index.root {
81 assert_eq!(space.labels, index.spaces[sid].labels);
82 }
83 }
84 assert!(ops::section_op(prepared.as_bytes(), SectionOp::Create(intent.clone())).is_err());
85 source = prepared.as_bytes().to_vec();
86 }
87 if let Some(output) = std::env::var_os("ONESTORE_PAGE_CREATION_OUTPUT") {
88 std::fs::create_dir(&output).unwrap();
89 std::fs::write(std::path::Path::new(&output).join("Lifecycle.one"), &source).unwrap();
90 }
91}
92
93#[test]
94fn created_pages_support_title_edits_and_body_insertion() {
95 let mut source = onestore::create_section("pages.one", "Original", "Author").unwrap();
96 for title in [None, Some(""), Some("Explicit 🦋 é")] {
97 let intent = PageCreation::new(None, title, "Author").unwrap();
98 source = ops::section_op(&source, SectionOp::Create(intent.clone()))
99 .unwrap()
100 .as_bytes()
101 .to_vec();
102 let (add, ..) = ops::new_outline(36.0, 36.0, "Body 🦀 é");
103 source = ops::page_op(&source, intent.space(), add)
104 .unwrap()
105 .as_bytes()
106 .to_vec();
107 let store = Store::parse(&source).unwrap();
108 let index = RevisionIndex::parse(&store).unwrap();
109 let document = Document::parse(&index).unwrap();
110 let view = &document.spaces[&intent.space()];
111 let view = &view.revisions[&view.contexts[&ExGuid::default()]];
112 let Kind::Metadata { title: actual, .. } = &view.nodes[&view.roots[&2]].kind else {
113 panic!()
114 };
115 assert_eq!(
116 actual.as_deref(),
117 Some(title.filter(|s| !s.is_empty()).unwrap_or("Body 🦀 é"))
118 );
119 if let Some(text) = intent.title_object() {
120 source = ops::page_op(
121 &source,
122 intent.space(),
123 PageOp::Text {
124 text,
125 range: 0..title.unwrap().encode_utf16().count() as u32,
126 with: "Renamed".into(),
127 },
128 )
129 .unwrap()
130 .as_bytes()
131 .to_vec();
132 let store = Store::parse(&source).unwrap();
133 let index = RevisionIndex::parse(&store).unwrap();
134 let document = Document::parse(&index).unwrap();
135 let view = &document.spaces[&intent.space()];
136 let view = &view.revisions[&view.contexts[&ExGuid::default()]];
137 let Kind::Metadata { title, .. } = &view.nodes[&view.roots[&2]].kind else {
138 panic!()
139 };
140 assert_eq!(title.as_deref(), Some("Renamed"));
141 }
142 }
143}
144
145/// OneNote 2010 draws level-1 text 27 pt inside an outline whose table stops at entry 0.
146#[test]
147fn created_body_outlines_carry_the_indentation_table_onenote_writes() {
148 let source = onestore::create_section("pages.one", "Original", "Author").unwrap();
149 let store = Store::parse(&source).unwrap();
150 let index = RevisionIndex::parse(&store).unwrap();
151 let document = Document::parse(&index).unwrap();
152 let (space, page) = document.pages().unwrap()[0];
153 let [first] = document.active(space).unwrap().nodes[&page].children[..] else {
154 panic!()
155 };
156 let (add, insertion, _) = ops::new_outline(72.0, 144.0, "Second");
157 let written = ops::page_op(&source, space, add).unwrap();
158 let store = Store::parse(written.as_bytes()).unwrap();
159 let index = RevisionIndex::parse(&store).unwrap();
160 let document = Document::parse(&index).unwrap();
161 let view = document.active(space).unwrap();
162 for outline in [first, insertion] {
163 let Kind::Outline { indents } = &view.nodes[&outline].kind else {
164 panic!()
165 };
166 assert_eq!(indents, &[18.0, 0.0, 27.0, 27.0]);
167 }
168}
169
170#[test]
171fn a_created_title_outline_is_stored_and_read_as_onenote_stores_it() {
172 let source = onestore::create_section("pages.one", "Original", "Author").unwrap();
173 let intent = PageCreation::new(None, Some("Title"), "Author").unwrap();
174 let prepared = ops::section_op(&source, SectionOp::Create(intent.clone())).unwrap();
175 let store = Store::parse(prepared.as_bytes()).unwrap();
176 let index = RevisionIndex::parse(&store).unwrap();
177 let document = Document::parse(&index).unwrap();
178 let view = document.active(intent.space()).unwrap();
179 let [title] = view.nodes[&intent.object()].structure[..] else {
180 panic!()
181 };
182 let [outline] = view.nodes[&title].children[..] else {
183 panic!()
184 };
185 let [paragraph] = view.nodes[&outline].children[..] else {
186 panic!()
187 };
188 let Kind::Outline { indents } = &view.nodes[&outline].kind else {
189 panic!()
190 };
191 assert_eq!(indents, &[18.0, 0.0, 27.0, 27.0]);
192 let flags = |id| -> Vec<u32> {
193 view.nodes[&id].extra[0]
194 .iter()
195 .map(|field| field.id)
196 .filter(|id| id >> 24 == 0x88)
197 .collect()
198 };
199 assert_eq!(
200 flags(outline),
201 [0x88001cf9, 0x88001cb2, 0x88001cb4, 0x88001c91, 0x88001cff]
202 );
203 assert_eq!(flags(paragraph), [0x88001cb2, 0x88001cb4]);
204 let page = Page::from_space(&document, intent.space()).unwrap();
205 let [PageObject::Title(title)] = &page.objects[..] else {
206 panic!()
207 };
208 let [outline] = &title.outlines[..] else {
209 panic!()
210 };
211 assert!(outline.title);
212 assert_eq!(outline.indents, [18.0, 0.0, 27.0, 27.0]);
213 assert_eq!(outline.min_width, Some(162.0));
214 assert_eq!(outline.layout.max_height, Some(21.6));
215}
216
217#[test]
218fn invalid_page_intents_and_nonleading_anchors_reject_before_publication() {
219 for title in ["a\nb", "\r", "\0", "\u{fffc}", "\u{fddf}"] {
220 assert!(PageCreation::new(None, Some(title), "Author").is_err());
221 }
222 assert!(PageCreation::new(None, None, "\0").is_err());
223 assert!(PageCreation::new(Some(ExGuid::default()), None, "Author").is_err());
224 let source = include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one");
225 let store = Store::parse(source).unwrap();
226 let index = RevisionIndex::parse(&store).unwrap();
227 let pages = Document::parse(&index).unwrap().pages().unwrap();
228 let intent = PageCreation::new(Some(pages[4].0), Some("Child"), "Author").unwrap();
229 assert!(ops::section_op(source, SectionOp::Create(intent.clone())).is_err());
230 let mut value = serde_json::to_value(PageCreation::new(None, None, "Author").unwrap()).unwrap();
231 value["guid"] = serde_json::to_value([0_u8; 16]).unwrap();
232 let invalid: PageCreation = serde_json::from_value(value.clone()).unwrap();
233 assert!(ops::section_op(source, SectionOp::Create(invalid.clone())).is_err());
234 value["unknown"] = true.into();
235 assert!(serde_json::from_value::<PageCreation>(value).is_err());
236}
237
238#[test]
239fn new_space_and_section_entry_publish_as_one_complete_action() {
240 let source = onestore::create_section("pages.one", "Original", "Author").unwrap();
241 let intent = PageCreation::new(None, Some("New 🦋 é"), "Author").unwrap();
242 let prepared = ops::section_op(&source, SectionOp::Create(intent.clone())).unwrap();
243 let old = current::current(&source);
244 let new = current::current(prepared.as_bytes());
245 assert_eq!(new.len(), old.len() + 1);
246 for write_limit in [1, 17, 4096] {
247 let mut complete = disk::Disk {
248 visible: source.clone(),
249 durable: source.clone(),
250 operation: 0,
251 fail_at: None,
252 write_limit,
253 random: 1954,
254 };
255 prepared.commit(&mut complete).unwrap();
256 assert_eq!(complete.durable, prepared.as_bytes());
257 for fail_at in 1..=complete.operation {
258 let mut interrupted = disk::Disk {
259 visible: source.clone(),
260 durable: source.clone(),
261 operation: 0,
262 fail_at: Some(fail_at),
263 write_limit,
264 random: 1954 + fail_at as u64,
265 };
266 prepared.commit(&mut interrupted).unwrap_err();
267 let observed = current::current(&interrupted.durable);
268 assert!(
269 observed == old || observed == new,
270 "{write_limit}:{fail_at}"
271 );
272 }
273 }
274}
275
276#[test]
277fn repeated_page_creation_crosses_root_fragments_counters_and_section_checkpoints() {
278 let mut source = onestore::create_section("PageStress.one", "Original", "Author").unwrap();
279 let original = source.clone();
280 let original_store = Store::parse(&original).unwrap();
281 let original_index = RevisionIndex::parse(&original_store).unwrap();
282 let mut expected = Document::parse(&original_index).unwrap().pages().unwrap();
283 let mut checkpoints = 0;
284 for step in 1..=520 {
285 let title = format!("Page {} 🦋 é", step % 7);
286 let intent = PageCreation::new(
287 None,
288 (step % 2 != 0).then_some(title.as_str()),
289 "Stress author",
290 )
291 .unwrap();
292 let prepared = ops::section_op(&source, SectionOp::Create(intent.clone())).unwrap();
293 let mut unpublished = prepared.as_bytes().to_vec();
294 unpublished[96..100].copy_from_slice(&source[96..100]);
295 let old_store = Store::parse(&unpublished).unwrap();
296 let old_index = RevisionIndex::parse(&old_store).unwrap();
297 assert_eq!(
298 Document::parse(&old_index).unwrap().pages().unwrap(),
299 expected
300 );
301 let store = Store::parse(prepared.as_bytes()).unwrap();
302 assert_eq!(store.header.root, original_store.header.root);
303 assert_eq!(
304 store.header.transaction_count,
305 original_store.header.transaction_count + step
306 );
307 let index = RevisionIndex::parse(&store).unwrap();
308 assert_eq!(
309 index.spaces.len(),
310 original_index.spaces.len() + step as usize
311 );
312 expected.push((intent.space(), intent.object()));
313 assert_eq!(Document::parse(&index).unwrap().pages().unwrap(), expected);
314 let section = &index.spaces[&index.root];
315 let rid = section.labels[&(ExGuid::default(), 1)];
316 checkpoints += usize::from(section.revisions[&rid].dependency.is_none());
317 for (sid, space) in &original_index.spaces {
318 for rid in space.revisions.keys() {
319 assert_eq!(
320 format!("{:?}", original_index.resolve(*sid, *rid).unwrap()),
321 format!("{:?}", index.resolve(*sid, *rid).unwrap())
322 );
323 }
324 }
325 source = prepared.as_bytes().to_vec();
326 }
327 assert_eq!(checkpoints, 1);
328 if let Some(output) = std::env::var_os("ONESTORE_PAGE_STRESS_OUTPUT") {
329 std::fs::create_dir(&output).unwrap();
330 std::fs::write(
331 std::path::Path::new(&output).join("PageStress.one"),
332 &source,
333 )
334 .unwrap();
335 }
336}
337
338#[test]
339fn native_changes_on_created_pages_accept_rust_followups() {
340 let initial = include_bytes!(
341 "../../../corpus/page-lifecycle/creation/native/cold/notebook/Lifecycle.one"
342 );
343 let store = Store::parse(initial).unwrap();
344 let index = RevisionIndex::parse(&store).unwrap();
345 let document = Document::parse(&index).unwrap();
346 let mut targets = Vec::new();
347 for (sid, _) in document.pages().unwrap() {
348 let space = &document.spaces[&sid];
349 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
350 let mut title = None;
351 let mut body = None;
352 for (id, node) in &view.nodes {
353 if let Kind::RichText { text, .. } = &node.kind {
354 if text == "New 🦋 é" {
355 title = Some(*id);
356 }
357 if text.starts_with("Native body ") {
358 body = Some(*id);
359 }
360 }
361 }
362 if let (Some(title), Some(body)) = (title, body) {
363 targets.push((sid, title, body));
364 }
365 }
366 assert_eq!(targets.len(), 2);
367 let mut source = initial.to_vec();
368 for (sid, title, body) in targets {
369 for (object, prefix) in [(body, "Rust + "), (title, "Reviewed ")] {
370 source = ops::page_op(
371 &source,
372 sid,
373 PageOp::Text {
374 text: object,
375 range: 0..0,
376 with: prefix.into(),
377 },
378 )
379 .unwrap()
380 .as_bytes()
381 .to_vec();
382 }
383 let store = Store::parse(&source).unwrap();
384 let index = RevisionIndex::parse(&store).unwrap();
385 let document = Document::parse(&index).unwrap();
386 let view = &document.spaces[&sid];
387 let view = &view.revisions[&view.contexts[&ExGuid::default()]];
388 assert!(
389 matches!(&view.nodes[&title].kind, Kind::RichText { text, .. } if text == "Reviewed New 🦋 é")
390 );
391 assert!(
392 matches!(&view.nodes[&body].kind, Kind::RichText { text, .. } if text.starts_with("Rust + Native body "))
393 );
394 assert!(
395 matches!(&view.nodes[&view.roots[&2]].kind, Kind::Metadata { title: Some(title), .. } if title == "Reviewed New 🦋 é")
396 );
397 }
398 if let Some(output) = std::env::var_os("ONESTORE_PAGE_FOLLOWUP_OUTPUT") {
399 std::fs::create_dir(&output).unwrap();
400 std::fs::write(std::path::Path::new(&output).join("Lifecycle.one"), &source).unwrap();
401 }
402}
403
404#[test]
405fn page_creation_preserves_native_features_and_file_data() {
406 for source in [
407 include_bytes!("../../../corpus/native-ink/20260905-ui/notebook/synthetic.one").as_slice(),
408 include_bytes!("../../../corpus/native-external-assets/notebook/synthetic.one").as_slice(),
409 include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one").as_slice(),
410 ] {
411 let before = Store::parse(source).unwrap();
412 let old = RevisionIndex::parse(&before).unwrap();
413 let original = Document::parse(&old).unwrap();
414 let intent = PageCreation::new(None, Some("Preserved features"), "Author").unwrap();
415 let prepared = ops::section_op(source, SectionOp::Create(intent.clone())).unwrap();
416 let after = Store::parse(prepared.as_bytes()).unwrap();
417 let new = RevisionIndex::parse(&after).unwrap();
418 let document = Document::parse(&new).unwrap();
419 assert_eq!(
420 document.pages().unwrap().len(),
421 original.pages().unwrap().len() + 1
422 );
423 for (sid, space) in &old.spaces {
424 for rid in space.revisions.keys() {
425 let original = old.resolve(*sid, *rid).unwrap();
426 let retained = new.resolve(*sid, *rid).unwrap();
427 assert_eq!(format!("{original:?}"), format!("{retained:?}"));
428 for object in original.objects.values() {
429 if let Some(onestore::FileDataReference::Internal(id)) =
430 object.file_reference().unwrap()
431 {
432 assert_eq!(before.file_data(id).unwrap(), after.file_data(id).unwrap());
433 }
434 }
435 }
436 if *sid != old.root {
437 assert_eq!(space.labels, new.spaces[sid].labels);
438 }
439 }
440 }
441}
442
443/// A page created into an empty section as OneNote 2010 creates one (its title's date and
444/// time fields, then a colour) reads back with both; a page kept from elsewhere keeps its
445/// identity and creation time.
446#[test]
447fn dated_pages_in_an_empty_section_read_back_with_their_date_and_colour() {
448 let source = onestore::create_empty_section("New Section 1.one", Some(0x00e4a88a)).unwrap();
449 let store = Store::parse(&source).unwrap();
450 let index = RevisionIndex::parse(&store).unwrap();
451 index.validate_current().unwrap();
452 assert!(Document::parse(&index).unwrap().pages().unwrap().is_empty());
453 let creation = PageCreation::new(None, Some(""), "Rust Author")
454 .unwrap()
455 .dated("Sunday, September 27, 2026", "2:23 AM")
456 .unwrap();
457 let retained: PageCreation =
458 serde_json::from_slice(&serde_json::to_vec(&creation).unwrap()).unwrap();
459 assert_eq!(retained, creation);
460 let space = creation.space();
461 let identity = [7; 16];
462 let kept = PageCreation::new(None, Some("Kept"), "Rust Author")
463 .unwrap()
464 .keeping(identity, 133_700_000_001_234_567)
465 .unwrap();
466 let image = ops::edited(
467 &source,
468 vec![
469 onestore::op::Op::Section(SectionOp::Create(creation.clone())),
470 onestore::op::Op::Page {
471 space,
472 op: PageOp::Color(Some(0x00f2f9d4)),
473 },
474 onestore::op::Op::Section(SectionOp::Create(kept.clone())),
475 ],
476 )
477 .unwrap();
478 let store = Store::parse(&image).unwrap();
479 let index = RevisionIndex::parse(&store).unwrap();
480 index.validate_current().unwrap();
481 let document = Document::parse(&index).unwrap();
482 let page = Page::from_space(&document, space).unwrap();
483 assert_eq!(page.color, Some(0x00f2f9d4));
484 assert_eq!(page.created, Some(creation.created()));
485 let [PageObject::Title(title)] = &page.objects[..] else {
486 panic!("{:?}", page.objects)
487 };
488 let shown: Vec<_> = title.outlines[1]
489 .paragraphs
490 .iter()
491 .map(|paragraph| {
492 let text = paragraph.text().unwrap();
493 (text.date_field, text.text.text().to_owned())
494 })
495 .collect();
496 assert_eq!(title.date, Some(title.outlines[1].id));
497 assert_eq!(
498 shown,
499 [
500 // OneNote's own new pages read the same: the fields' order names their roles.
501 (None, "Sunday, September 27, 2026".to_owned()),
502 (None, "2:23 AM".to_owned()),
503 ]
504 );
505 let kept_page = Page::from_space(&document, kept.space()).unwrap();
506 assert_eq!(kept_page.identity, Some(identity));
507 assert_eq!(kept_page.created, Some(133_700_000_001_234_567));
508 assert_eq!(kept_page.color, None);
509 // Clearing the colour stores OneNote's "No color".
510 let cleared = ops::page_edited(&image, space, vec![PageOp::Color(None)]).unwrap();
511 let store = Store::parse(&cleared).unwrap();
512 let index = RevisionIndex::parse(&store).unwrap();
513 let document = Document::parse(&index).unwrap();
514 assert_eq!(Page::from_space(&document, space).unwrap().color, None);
515}