1use crate::{
2 Error, ExGuid,
3 create::{current_timestamps, properties, string},
4 document::{FieldValue, Kind},
5 op::content::{NATIVE_INDENTS, measurement_bytes},
6 write::{PropertyObject, fresh_guid},
7};
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::{BTreeMap, BTreeSet},
11 sync::Arc,
12};
13
14fn invalid(message: &'static str) -> Error {
15 Error { offset: 0, message }
16}
17
18pub(crate) fn metadata_id(space: ExGuid) -> ExGuid {
19 let mut id = ExGuid {
20 guid: space.guid,
21 n: 1,
22 };
23 for (byte, salt) in id.guid.iter_mut().zip([
24 0x31, 0xc0, 0xa8, 0x22, 0, 0x36, 0xee, 0x42, 0xb7, 0x14, 0xd7, 0xac, 0xda, 0x24, 0x35, 0xe8,
25 ]) {
26 *byte ^= salt;
27 }
28 id
29}
30
31pub(crate) fn metadata_guid(metadata: &crate::document::Element<'_>) -> Result<[u8; 16], Error> {
32 metadata.extra[0]
33 .iter()
34 .find_map(|field| match field.value {
35 FieldValue::Bytes(bytes) if field.id == 0x1c001c30 => bytes.try_into().ok(),
36 _ => None,
37 })
38 .ok_or_else(|| invalid("Page metadata needs its page identifier"))
39}
40
41pub(crate) fn set_references(
42 object: &mut PropertyObject,
43 property: u32,
44 ids: &[ExGuid],
45) -> Result<(), Error> {
46 let mut references = Vec::new();
47 for id in ids {
48 references.extend_from_slice(&object.reference(*id)?);
49 }
50 let source = PropertyObject {
51 jcid: object.jcid,
52 bytes: properties(&[(property, references)])?,
53 global_ids: Arc::clone(&object.global_ids),
54 };
55 object.copy_property(&source, property)
56}
57
58/// A conflict page (MS-ONE 2.1.2): the version of a page a merge could not take, kept
59/// read-only under the page it conflicts with.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ConflictPage {
62 pub space: ExGuid,
63 pub title: String,
64 /// Whose version it is (`ConflictingUserName`).
65 pub user: String,
66 /// FILETIME of its `TopologyCreationTimeStamp`, when the merge made it.
67 pub created: Option<u64>,
68 /// The conflict objects on it (`IsConflictObjectForRender`), which OneNote highlights.
69 pub objects: Vec<ExGuid>,
70}
71
72/// Whether an edit preserves position or moves before a page (None appends).
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(deny_unknown_fields)]
75pub enum PagePosition {
76 Keep,
77 Before(Option<ExGuid>),
78}
79
80/// A page's explicit position and indentation, with a stable identity for a new series.
81/// Moving a page does not implicitly select its following subpages.
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct PageEdit {
85 guid: [u8; 16],
86 space: ExGuid,
87 level: u32,
88 position: PagePosition,
89}
90
91impl PageEdit {
92 /// Changes one page's indentation without changing its position.
93 pub fn set_level(space: ExGuid, level: u32) -> Result<Self, Error> {
94 Self::new(space, level, PagePosition::Keep)
95 }
96
97 /// Moves before an existing page space, or appends when `before` is None.
98 pub fn move_to(space: ExGuid, before: Option<ExGuid>, level: u32) -> Result<Self, Error> {
99 Self::new(space, level, PagePosition::Before(before))
100 }
101
102 pub fn space(&self) -> ExGuid {
103 self.space
104 }
105
106 pub fn level(&self) -> u32 {
107 self.level
108 }
109
110 pub fn position(&self) -> PagePosition {
111 self.position
112 }
113
114 /// Revises placement while retaining the selected page and allocated series identity.
115 pub fn reposition(&self, position: PagePosition, level: u32) -> Result<Self, Error> {
116 let edit = Self {
117 position,
118 level,
119 ..self.clone()
120 };
121 edit.validate()?;
122 Ok(edit)
123 }
124
125 fn new(space: ExGuid, level: u32, position: PagePosition) -> Result<Self, Error> {
126 let edit = Self {
127 guid: fresh_guid()?,
128 space,
129 level,
130 position,
131 };
132 edit.validate()?;
133 Ok(edit)
134 }
135
136 fn validate(&self) -> Result<(), Error> {
137 if self.guid == [0; 16] || self.space.guid == [0; 16] || !(1..=3).contains(&self.level) {
138 return Err(invalid(
139 "Choose an existing page space and indentation level 1 through 3",
140 ));
141 }
142 if let PagePosition::Before(Some(before)) = self.position
143 && (before.guid == [0; 16] || before == self.space)
144 {
145 return Err(invalid("Choose a different page as the movement anchor"));
146 }
147 Ok(())
148 }
149}
150
151/// What page edits read of one page space: its metadata element and the stored manifest
152/// and metadata objects.
153pub(crate) struct PageParts<'a> {
154 pub metadata: crate::document::Element<'a>,
155 pub manifest: (ExGuid, PropertyObject),
156 pub stored_metadata: (ExGuid, PropertyObject),
157}
158
159/// Page moves and removals on a section kept open: the objects each changed space stores.
160pub(crate) fn section_changes(
161 section: &mut crate::Section<'_>,
162 edits: &[PageEdit],
163 removals: &[ExGuid],
164) -> Result<Vec<(ExGuid, crate::active::Changes)>, Error> {
165 let root = section.root();
166 let (view, raw) = {
167 let page = section.active(root)?;
168 (page.view.clone(), page.live.revision.clone())
169 };
170 let section_id = *view
171 .roots
172 .get(&1)
173 .ok_or_else(|| invalid("Section root is unavailable"))?;
174 let pages: Vec<ExGuid> = view.nodes[&section_id]
175 .children
176 .iter()
177 .flat_map(|series| view.nodes[series].spaces.clone())
178 .collect();
179 let section = &*section;
180 let parts = |sid: ExGuid| -> Result<PageParts<'_>, Error> {
181 let revision = section.revision(sid)?;
182 let (manifest, stored) = (revision.roots[&1], revision.roots[&2]);
183 let element = |id| {
184 crate::document::Element::parse_with(
185 &revision.objects[id],
186 crate::FileType::Section,
187 &mut |_| Err(invalid("Page metadata holds no payload")),
188 )
189 };
190 if element(&manifest)?.content.len() != 1 {
191 return Err(invalid(
192 "Each page space must contain one page with a valid level",
193 ));
194 }
195 Ok(PageParts {
196 metadata: element(&stored)?,
197 manifest: (
198 manifest,
199 PropertyObject::from_object(&revision.objects[&manifest])?,
200 ),
201 stored_metadata: (
202 stored,
203 PropertyObject::from_object(&revision.objects[&stored])?,
204 ),
205 })
206 };
207 Ok(
208 page_changes(root, &view, &raw, &pages, parts, edits, removals)?
209 .unwrap_or_default()
210 .into_iter()
211 .filter(|(_, changes)| !changes.is_empty())
212 .collect(),
213 )
214}
215
216/// The objects page moves, indentation and removals change in each space; none when the
217/// order and levels stay.
218fn page_changes<'a>(
219 root: ExGuid,
220 view: &crate::document::Revision<'_>,
221 raw: &crate::ResolvedRevision<'_>,
222 pages: &[ExGuid],
223 parts: impl Fn(ExGuid) -> Result<PageParts<'a>, Error>,
224 edits: &[PageEdit],
225 removals: &[ExGuid],
226) -> Result<Option<BTreeMap<ExGuid, crate::active::Changes>>, Error> {
227 if !edits.is_empty() && !removals.is_empty() {
228 return Err(invalid(
229 "Choose page placement or page removal for one operation",
230 ));
231 }
232 let removed: BTreeSet<_> = removals.iter().copied().collect();
233 if removed.len() != removals.len() || removed.iter().any(|sid| sid.guid == [0; 16]) {
234 return Err(invalid("Choose distinct existing page spaces for removal"));
235 }
236 let mut selected = BTreeMap::new();
237 let mut guids = BTreeSet::new();
238 for edit in edits {
239 edit.validate()?;
240 if selected.insert(edit.space, edit).is_some() || !guids.insert(edit.guid) {
241 return Err(invalid(
242 "Use distinct page and edit identities within a batch",
243 ));
244 }
245 }
246 let section_id = view.roots[&1];
247 let section_node = &view.nodes[&section_id];
248 if !matches!(section_node.kind, Kind::Section { .. })
249 || section_node.extra[0]
250 .iter()
251 .any(|field| field.id == 0x88001cde)
252 {
253 return Err(invalid("Choose an editable section for page editing"));
254 }
255 let mut order = Vec::new();
256 let mut levels = BTreeMap::new();
257 let mut page_guids = BTreeMap::new();
258 let mut stored = BTreeMap::new();
259 for sid in pages {
260 let page = parts(*sid)?;
261 let Kind::Metadata { level, .. } = page.metadata.kind else {
262 return Err(invalid("Choose ordinary pages with page metadata"));
263 };
264 let level = level.unwrap_or(1);
265 if !(1..=3).contains(&level) || levels.insert(*sid, level).is_some() {
266 return Err(invalid(
267 "Each page space must contain one page with a valid level",
268 ));
269 }
270 if page_guids
271 .insert(metadata_guid(&page.metadata)?, *sid)
272 .is_some()
273 {
274 return Err(invalid("Pages must have distinct metadata identifiers"));
275 }
276 order.push(*sid);
277 stored.insert(*sid, page);
278 }
279 let original_order = order.clone();
280 let original_levels = levels.clone();
281 let mut series_by_head = BTreeMap::new();
282 let mut copies = BTreeMap::new();
283 let mut copy_ids = BTreeSet::new();
284 let mut stale = BTreeSet::new();
285 for oid in &section_node.children {
286 let series = &view.nodes[oid];
287 // OneNote leaves the losing side's series empty after a merge; the rewrite drops it.
288 let Some(head) = series.spaces.first() else {
289 continue;
290 };
291 if levels.get(head) != Some(&1)
292 || series.spaces[1..]
293 .iter()
294 .any(|sid| levels.get(sid).is_none_or(|level| *level == 1))
295 {
296 return Err(invalid(
297 "A page series must start with its only top-level page",
298 ));
299 }
300 if let Some(retained) = series.spaces.iter().find(|sid| !removed.contains(sid)) {
301 series_by_head.insert(*retained, *oid);
302 }
303 if let Some(field) = series.extra[0].iter().find(|field| field.id == 0x24003442) {
304 let FieldValue::Objects(ids) = &field.value else {
305 return Err(invalid("Page metadata copies must be object references"));
306 };
307 // OneNote can leave a page's former copy beside its new one (MS-ONE 2.2.81, note
308 // 9): the last copy of each page stays and the series is rewritten to match.
309 let mismatched = ids.len() != series.spaces.len();
310 if mismatched {
311 stale.insert(*oid);
312 }
313 for id in ids {
314 if !copy_ids.insert(*id)
315 || !matches!(
316 view.nodes.get(id).map(|n| &n.kind),
317 Some(Kind::Metadata { .. })
318 )
319 {
320 return Err(invalid("Each page needs its own ordinary metadata copy"));
321 }
322 let sid = page_guids
323 .get(&metadata_guid(&view.nodes[id])?)
324 .filter(|sid| series.spaces.contains(sid))
325 .ok_or_else(|| invalid("Metadata copy must identify a page in its series"))?;
326 if copies.insert(*sid, *id).is_some() && !mismatched {
327 return Err(invalid("Each page needs its own ordinary metadata copy"));
328 }
329 }
330 }
331 }
332 for sid in &removed {
333 if !levels.contains_key(sid) {
334 return Err(invalid("The selected page is no longer in the section"));
335 }
336 }
337 order.retain(|sid| !removed.contains(sid));
338 if !removed.is_empty()
339 && let Some(first) = order.first()
340 {
341 levels.insert(*first, 1);
342 }
343 for edit in edits {
344 let Some(at) = order.iter().position(|sid| *sid == edit.space) else {
345 return Err(invalid("The selected page is no longer in the section"));
346 };
347 levels.insert(edit.space, edit.level);
348 if let PagePosition::Before(before) = edit.position {
349 order.remove(at);
350 let position = match before {
351 None => order.len(),
352 Some(before) => order
353 .iter()
354 .position(|sid| *sid == before)
355 .ok_or_else(|| invalid("The movement anchor is no longer in the section"))?,
356 };
357 order.insert(position, edit.space);
358 }
359 }
360 if order.first().is_some_and(|sid| levels[sid] != 1) {
361 return Err(invalid(
362 "The section's first page must have indentation level 1",
363 ));
364 }
365 if order == original_order && levels == original_levels {
366 return Ok(None);
367 }
368 let mut groups: Vec<Vec<ExGuid>> = Vec::new();
369 for sid in order {
370 if levels[&sid] == 1 {
371 groups.push(Vec::new());
372 }
373 groups.last_mut().unwrap().push(sid);
374 }
375 let mut changes = BTreeMap::new();
376 for sid in &removed {
377 let page = &stored[sid];
378 let (manifest_id, mut manifest) = page.manifest.clone();
379 if manifest.jcid != 0x60037 {
380 return Err(invalid("Choose ordinary pages for removal"));
381 }
382 manifest.bytes = properties(&[])?;
383 let (metadata_id, mut metadata) = page.stored_metadata.clone();
384 metadata.set(&[(0x88001de9, &[])])?;
385 changes.insert(
386 *sid,
387 BTreeMap::from([(manifest_id, manifest), (metadata_id, metadata)]),
388 );
389 }
390 let mut replacements = BTreeMap::new();
391 let mut children = Vec::new();
392 for spaces in groups {
393 let head = spaces[0];
394 // A moved page heads a series of its own, as OneNote's drag gives one.
395 let moved = selected
396 .get(&head)
397 .is_some_and(|edit| matches!(edit.position, PagePosition::Before(_)));
398 let old_id = series_by_head.get(&head).copied().filter(|_| !moved);
399 let (id, mut series) = if let Some(id) = old_id {
400 (id, PropertyObject::from_object(&raw.objects[&id])?)
401 } else {
402 let edit = selected
403 .get(&head)
404 .ok_or_else(|| invalid("A new series must start at an edited page"))?;
405 let id = ExGuid {
406 guid: edit.guid,
407 n: 1,
408 };
409 if raw.objects.contains_key(&id) {
410 return Err(invalid("The new page-series identity already exists"));
411 }
412 (
413 id,
414 PropertyObject {
415 jcid: 0x60008,
416 bytes: properties(&[(0x1c001c30, edit.guid.to_vec())])?,
417 global_ids: Arc::new(BTreeMap::from([(0, edit.guid)])),
418 },
419 )
420 };
421 children.push(id);
422 let membership_changed =
423 old_id.is_none_or(|old| view.nodes[&old].spaces != spaces || stale.contains(&old));
424 if !membership_changed
425 && !spaces.iter().any(|sid| {
426 levels[sid] != original_levels[sid]
427 || copies.get(sid).is_some_and(|id| {
428 matches!(view.nodes[id].kind, Kind::Metadata { level, .. }
429 if level.unwrap_or(1) != levels[sid])
430 })
431 })
432 {
433 continue;
434 }
435 let mut metadata_ids = Vec::new();
436 for sid in &spaces {
437 let (page_id, original) = stored[sid].stored_metadata.clone();
438 if old_id.is_none() && *sid == head {
439 series.copy_property(&original, 0x18001c65)?;
440 }
441 if levels[sid] != original_levels[sid] {
442 let mut metadata = original.clone();
443 metadata.set(&[(0x14001dff, &levels[sid].to_le_bytes())])?;
444 changes.insert(*sid, BTreeMap::from([(page_id, metadata)]));
445 }
446 let copy_id = copies
447 .get(sid)
448 .copied()
449 .unwrap_or_else(|| metadata_id(*sid));
450 let mut copy = match raw.objects.get(&copy_id) {
451 Some(object) => {
452 if object.jcid != 0x20030
453 || (!copies.contains_key(sid) && copy_ids.contains(&copy_id))
454 {
455 return Err(invalid("Page metadata identities overlap"));
456 }
457 PropertyObject::from_object(object)?
458 }
459 None => original,
460 };
461 copy.set(&[(0x14001dff, &levels[sid].to_le_bytes())])?;
462 copy.reference(copy_id)?;
463 if replacements.insert(copy_id, copy).is_some() {
464 return Err(invalid("Page metadata identities overlap"));
465 }
466 metadata_ids.push(copy_id);
467 }
468 if membership_changed || spaces.iter().any(|sid| !copies.contains_key(sid)) {
469 set_references(&mut series, 0x2c001d63, &spaces)?;
470 set_references(&mut series, 0x24003442, &metadata_ids)?;
471 if replacements.insert(id, series).is_some() {
472 return Err(invalid("Page-series and metadata identities overlap"));
473 }
474 }
475 }
476 if children != section_node.children {
477 let mut parent = PropertyObject::from_object(&raw.objects[&section_id])?;
478 set_references(&mut parent, 0x24001c20, &children)?;
479 replacements.insert(section_id, parent);
480 }
481 changes.insert(root, replacements);
482 Ok(Some(changes))
483}
484
485/// A page creation's changes to the root space, and the new space's roots and objects.
486pub(crate) type Creation = (
487 crate::active::Changes,
488 BTreeMap<u32, ExGuid>,
489 crate::active::Changes,
490);
491
492/// An empty top-level page with stable identities and creation time.
493/// Retain the intent across retries; an existing page identity rejects duplicate creation.
494#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
495#[serde(deny_unknown_fields)]
496pub struct PageCreation {
497 guid: [u8; 16],
498 series_guid: [u8; 16],
499 before: Option<ExGuid>,
500 title: Option<String>,
501 author: String,
502 created: u32,
503 /// The title's date and time fields' text, for a titled page.
504 #[serde(default, skip_serializing_if = "Option::is_none")]
505 date: Option<[String; 2]>,
506 /// The page identity and creation time (FILETIME) a page keeps from elsewhere.
507 #[serde(default, skip_serializing_if = "Option::is_none")]
508 kept: Option<([u8; 16], u64)>,
509 /// The title's font, OneNote's Default font face; none is Calibri.
510 #[serde(default, skip_serializing_if = "Option::is_none")]
511 title_font: Option<String>,
512 /// The title's colour, OneNote's Default font colour as COLORREF; none is automatic.
513 #[serde(default, skip_serializing_if = "Option::is_none")]
514 title_color: Option<u32>,
515}
516
517impl PageCreation {
518 /// Appends a page, or inserts before the first page space of an existing series.
519 /// `Some("")` creates an empty title field; `None` creates a page without a title node.
520 /// The page has no body outlines or generated date/time text and does not apply a template.
521 pub fn new(before: Option<ExGuid>, title: Option<&str>, author: &str) -> Result<Self, Error> {
522 let page = Self {
523 guid: fresh_guid()?,
524 series_guid: fresh_guid()?,
525 before,
526 title: title.map(str::to_owned),
527 author: author.to_owned(),
528 created: current_timestamps()?.0,
529 date: None,
530 kept: None,
531 title_font: None,
532 title_color: None,
533 };
534 page.validate()?;
535 Ok(page)
536 }
537
538 /// Gives a titled page's title OneNote 2010's date and time fields, showing `date` and
539 /// `time`: the creation time (`created`) as the user's long date and short time.
540 pub fn dated(mut self, date: &str, time: &str) -> Result<Self, Error> {
541 if self.title.is_none() {
542 return Err(invalid("Only a titled page shows its date"));
543 }
544 self.date = Some([date.to_owned(), time.to_owned()]);
545 self.validate()?;
546 Ok(self)
547 }
548
549 /// Sets the title in `font` and `color` (COLORREF; none is automatic), as OneNote 2010
550 /// sets a new page's `PageTitle` quick style in its Default font's face and colour,
551 /// keeping the size (`corpus/default-font`).
552 pub fn titled_in(mut self, font: &str, color: Option<u32>) -> Result<Self, Error> {
553 self.title_font = Some(font.to_owned());
554 self.title_color = color;
555 self.validate()?;
556 Ok(self)
557 }
558
559 /// Keeps `identity`, the page identity internal links name, and `created`, as OneNote
560 /// 2010 keeps both for a page it moves to the notebook's recycle bin.
561 pub fn keeping(mut self, identity: [u8; 16], created: u64) -> Result<Self, Error> {
562 if identity == [0; 16] {
563 return Err(invalid("Keep an existing page identity"));
564 }
565 let seconds = (created / 10_000_000)
566 .checked_sub(11_644_473_600 + 315_532_800)
567 .and_then(|seconds| u32::try_from(seconds).ok())
568 .ok_or_else(|| invalid("The kept creation time is outside OneNote's range"))?;
569 self.created = seconds;
570 self.kept = Some((identity, created));
571 Ok(self)
572 }
573
574 /// Creates the page in the object space `{guid},1`, its series named after it, instead of
575 /// under fresh identities: a merge that recreates a page, or must create the same page
576 /// each time it runs, names it.
577 pub fn in_space(mut self, guid: [u8; 16]) -> Result<Self, Error> {
578 self.guid = guid;
579 self.series_guid = guid.map(|byte| byte ^ 0x5a);
580 self.validate()?;
581 Ok(self)
582 }
583
584 /// Who creates the page.
585 pub fn author(&self) -> &str {
586 &self.author
587 }
588
589 /// When the page was created, FILETIME, which its date and time fields show.
590 pub fn created(&self) -> u64 {
591 match self.kept {
592 Some((_, created)) => created,
593 None => (u64::from(self.created) + 315_532_800 + 11_644_473_600) * 10_000_000,
594 }
595 }
596
597 /// The page identity internal links name as `page-id`.
598 fn identity(&self) -> [u8; 16] {
599 self.kept.map_or(self.guid, |(identity, _)| identity)
600 }
601
602 pub fn space(&self) -> ExGuid {
603 ExGuid {
604 guid: self.guid,
605 n: 1,
606 }
607 }
608
609 pub fn object(&self) -> ExGuid {
610 ExGuid {
611 guid: self.guid,
612 n: 12,
613 }
614 }
615
616 pub fn title_object(&self) -> Option<ExGuid> {
617 self.title.as_ref().map(|_| ExGuid {
618 guid: self.guid,
619 n: 16,
620 })
621 }
622
623 /// The page the new page goes before; `None` appends it.
624 pub fn before(&self) -> Option<ExGuid> {
625 self.before
626 }
627
628 /// Changes the insertion anchor while retaining all identities and creation metadata.
629 pub fn reposition(&self, before: Option<ExGuid>) -> Result<Self, Error> {
630 let mut page = self.clone();
631 page.before = before;
632 page.validate()?;
633 Ok(page)
634 }
635
636 fn validate(&self) -> Result<(), Error> {
637 if self.guid == [0; 16]
638 || self.series_guid == [0; 16]
639 || self.guid == self.series_guid
640 || self.author.contains('\0')
641 || self.before.is_some_and(|id| id.guid == [0; 16])
642 || self
643 .title
644 .iter()
645 .chain(self.date.iter().flatten())
646 .any(|text| text.contains(['\0', '\r', '\n', '\u{fffc}', '\u{fddf}']))
647 || self
648 .title_font
649 .as_ref()
650 .is_some_and(|font| font.is_empty() || font.contains('\0'))
651 {
652 return Err(invalid(
653 "Use a new page identity, an existing page anchor, and ordinary single-line title text",
654 ));
655 }
656 Ok(())
657 }
658
659 /// The root space's changes creating this page, and the new space's roots and objects.
660 pub(crate) fn changes(&self, section: &mut crate::Section<'_>) -> Result<Creation, Error> {
661 self.validate()?;
662 if section.revision(self.space()).is_ok() {
663 return Err(invalid(
664 "This page identity already exists; reconcile the original creation",
665 ));
666 }
667 let root = section.root();
668 let page = section.active(root)?;
669 self.creation(&page.view, &page.live.revision)
670 }
671
672 /// The object space of a conflict page this creation makes, as OneNote 2010 stores one:
673 /// the page's space with `jcidConflictPageMetaData` (MS-ONE 2.2.35) as its metadata,
674 /// naming this creation's author as the conflicting user and `title` as the page's.
675 /// Returns the roots, the objects and the metadata, which the conflicting page's
676 /// manifest also keeps a copy of.
677 pub(crate) fn conflict(
678 &self,
679 section: &mut crate::Section<'_>,
680 title: &str,
681 level: u32,
682 ) -> Result<
683 (
684 BTreeMap<u32, ExGuid>,
685 crate::active::Changes,
686 PropertyObject,
687 ),
688 Error,
689 > {
690 if self.before.is_some() {
691 return Err(invalid("A conflict page is not placed in the page list"));
692 }
693 let (_, roots, mut objects) = self.changes(section)?;
694 let timestamp = self.created();
695 let initials = crate::create::initials(&self.author);
696 let metadata = PropertyObject {
697 jcid: 0x20038,
698 bytes: properties(&[
699 (0x1c001cf3, string(title)),
700 (0x1c001c30, self.identity().to_vec()),
701 (0x14001dff, level.to_le_bytes().to_vec()),
702 (0x14001d82, 40_u32.to_le_bytes().to_vec()),
703 (0x1400348b, 40_u32.to_le_bytes().to_vec()),
704 (0x18001c65, timestamp.to_le_bytes().to_vec()),
705 (0x1c001d9e, string(&self.author)),
706 (0x1c001d9f, string(&initials)),
707 ])?,
708 global_ids: Arc::new(BTreeMap::from([(0, self.guid)])),
709 };
710 objects.insert(roots[&2], metadata.clone());
711 Ok((roots, objects, metadata))
712 }
713
714 /// The title's second outline as OneNote 2010 writes it for a new page (object numbers,
715 /// jcids and properties): a read-only outline of two paragraphs, the date then the
716 /// time, in the `PageDateTime` style.
717 fn date_fields(&self, date: &str, time: &str) -> Vec<(u32, u32, crate::op::Values)> {
718 let reference = |n: u32| n.to_le_bytes().to_vec();
719 let modified = || (0x14001d7a, self.created.to_le_bytes().to_vec());
720 let yes = |id: u32| (id | 1 << 31, Vec::new());
721 let paragraph = |text: u32| {
722 vec![
723 modified(),
724 (0x20001d79, reference(17)),
725 (0x20001d78, reference(17)),
726 (0x14001d09, self.created.to_le_bytes().to_vec()),
727 (0x0c001c03, vec![1]),
728 (0x24001c1f, reference(text)),
729 yes(0x08001cb2),
730 yes(0x08001d0c),
731 (0x08001c34, Vec::new()),
732 ]
733 };
734 let text = |value: &str, role: u32| {
735 // OneNote keeps Latin-1 text as TextExtendedAscii, other text as Unicode.
736 let content = match value
737 .chars()
738 .map(|c| u8::try_from(c).ok())
739 .collect::<Option<Vec<u8>>>()
740 {
741 Some(latin) => (0x1c003498, latin),
742 None => (0x1c001c22, string(value)),
743 };
744 vec![
745 modified(),
746 (0x2000342c, reference(23)),
747 yes(0x08001cde),
748 yes(0x08001c00),
749 yes(0x08001c88),
750 yes(0x08001d0c),
751 yes(role),
752 (0x10001cfe, 0x409_u16.to_le_bytes().to_vec()),
753 (0x1c001cc8, vec![0, 0, 0, 0, 3]),
754 content,
755 (0x24001e13, reference(19)),
756 yes(0x080034dd),
757 ]
758 };
759 let no = |id: u32| (id, Vec::new());
760 vec![
761 (
762 20,
763 0x6000c,
764 vec![
765 modified(),
766 (0x24001c20, [reference(21), reference(24)].concat()),
767 (
768 0x1c001c12,
769 measurement_bytes(&NATIVE_INDENTS, 4).expect("Native indents encode"),
770 ),
771 (0x0c001c13, vec![0]),
772 (0x0c001c03, vec![1]),
773 (0x14001c1c, 0.6_f32.to_le_bytes().to_vec()),
774 yes(0x08001cb5),
775 yes(0x08001c00),
776 yes(0x08001cb2),
777 yes(0x08001cde),
778 yes(0x08001d0c),
779 (0x14001c3e, reference(0)),
780 (0x14001c84, reference(0xc)),
781 ],
782 ),
783 (21, 0x6000d, paragraph(22)),
784 (22, 0x6000e, text(date, 0x08001cb5)),
785 (
786 23,
787 0x12004d,
788 vec![
789 (0x1c00345a, string("PageDateTime")),
790 no(0x08001c04),
791 no(0x08001c05),
792 no(0x08001c06),
793 (0x14001c0c, 0x0080_8080_u32.to_le_bytes().to_vec()),
794 (0x1c001c0a, string("Calibri")),
795 (0x10001c0b, 20_u16.to_le_bytes().to_vec()),
796 (0x1400342e, 0_f32.to_le_bytes().to_vec()),
797 (0x1400342f, 0_f32.to_le_bytes().to_vec()),
798 (0x14003430, 0_f32.to_le_bytes().to_vec()),
799 no(0x08001c07),
800 (0x14001c0d, 0xff00_0000_u32.to_le_bytes().to_vec()),
801 no(0x08001c09),
802 no(0x08001c08),
803 ],
804 ),
805 (24, 0x6000d, paragraph(25)),
806 (25, 0x6000e, text(time, 0x08001c87)),
807 ]
808 }
809
810 fn creation(
811 &self,
812 view: &crate::document::Revision<'_>,
813 raw: &crate::ResolvedRevision<'_>,
814 ) -> Result<Creation, Error> {
815 {
816 let section_id = view.roots[&1];
817 let section_node = &view.nodes[&section_id];
818 if !matches!(section_node.kind, Kind::Section { .. })
819 || section_node.extra[0]
820 .iter()
821 .any(|field| field.id == 0x88001cde)
822 {
823 return Err(invalid("Choose an editable section for the new page"));
824 }
825 let position = match self.before {
826 None => section_node.children.len(),
827 Some(before) => section_node
828 .children
829 .iter()
830 .position(|id| view.nodes[id].spaces.first() == Some(&before))
831 .ok_or_else(|| {
832 invalid("Insert before the first page of an existing series, or append")
833 })?,
834 };
835 let id = |n| ExGuid { guid: self.guid, n };
836 let reference = |n: u32| n.to_le_bytes().to_vec();
837 let timestamp = self.created().to_le_bytes().to_vec();
838 let modified = || (0x14001d7a, self.created.to_le_bytes().to_vec());
839 let mut table = BTreeMap::from([(0, self.guid)]);
840 let metadata_id = metadata_id(self.space());
841 table.insert(1, metadata_id.guid);
842 let table = Arc::new(table);
843 let object = |jcid, values: Vec<_>| -> Result<_, Error> {
844 Ok(PropertyObject {
845 jcid,
846 bytes: properties(&values)?,
847 global_ids: Arc::clone(&table),
848 })
849 };
850 let title = self.title.as_deref().unwrap_or_default().trim_start();
851 let metadata = vec![
852 (0x1c001c30, self.identity().to_vec()),
853 (0x1c001cf3, string(title)),
854 (0x14001d82, reference(40)),
855 (0x1400348b, reference(40)),
856 (0x14001dff, reference(1)),
857 (0x18001c65, timestamp.clone()),
858 ];
859 let mut page = [
860 vec![
861 modified(),
862 (0x1c001d75, string(&self.author)),
863 (0x1c001df8, string(&crate::create::initials(&self.author))),
864 (0x1c001d3c, string("")),
865 ],
866 crate::create::page_margins(),
867 ]
868 .concat();
869 if self.title.is_some() {
870 page.push((0x24001d5f, reference(13)));
871 }
872 let mut objects = BTreeMap::from([
873 (id(10), object(0x60037, vec![(0x24001c1f, reference(12))])?),
874 (id(11), object(0x20030, metadata.clone())?),
875 (id(12), object(0x6000b, page)?),
876 ]);
877 if let Some(title) = &self.title {
878 for (n, jcid, values) in [
879 (
880 13,
881 0x6002c,
882 vec![
883 modified(),
884 (
885 0x24001c20,
886 [
887 reference(14),
888 self.date.iter().flat_map(|_| reference(20)).collect(),
889 ]
890 .concat(),
891 ),
892 (0x14001c14, 0_f32.to_le_bytes().to_vec()),
893 (0x14001c15, 0_f32.to_le_bytes().to_vec()),
894 (0x14001c3e, reference(0x0009_000c)),
895 (0x14001c84, reference(0)),
896 (0x14001cf1, reference(0)),
897 ],
898 ),
899 (
900 14,
901 0x6000c,
902 vec![
903 modified(),
904 (0x24001c20, reference(15)),
905 (0x1c001c12, measurement_bytes(&NATIVE_INDENTS, 4)?),
906 (0x0c001c13, vec![0]),
907 (0x0c001c03, vec![1]),
908 (0x14001c1c, 0.6_f32.to_le_bytes().to_vec()),
909 (0x88001cf9, vec![]),
910 (0x88001cb2, vec![]),
911 (0x88001cb4, vec![]),
912 (0x88001c91, vec![]),
913 (0x14001cec, 4.5_f32.to_le_bytes().to_vec()),
914 (0x88001cff, vec![]),
915 (0x14001c3e, reference(0)),
916 (0x14001c84, reference(0xc)),
917 ],
918 ),
919 (
920 15,
921 0x6000d,
922 vec![
923 modified(),
924 (0x24001c1f, reference(16)),
925 (0x0c001c03, vec![1]),
926 (0x20001d78, reference(17)),
927 (0x20001d79, reference(17)),
928 (0x14001d09, self.created.to_le_bytes().to_vec()),
929 (0x88001cb2, vec![]),
930 (0x88001cb4, vec![]),
931 ],
932 ),
933 (
934 16,
935 0x6000e,
936 vec![
937 modified(),
938 (0x1c001c22, string(title)),
939 (0x24001e13, reference(19)),
940 (0x2000342c, reference(18)),
941 (0x10001cfe, 0x409_u16.to_le_bytes().to_vec()),
942 (0x88001cb4, vec![]),
943 ],
944 ),
945 (17, 0x120001, crate::create::author_properties(&self.author)),
946 (
947 18,
948 0x12004d,
949 [
950 (0x1c00345a, string("PageTitle")),
951 (
952 0x1c001c0a,
953 string(self.title_font.as_deref().unwrap_or("Calibri")),
954 ),
955 (0x10001c0b, 34_u16.to_le_bytes().to_vec()),
956 ]
957 .into_iter()
958 .chain(
959 self.title_color
960 .map(|color| (0x14001c0c, color.to_le_bytes().to_vec())),
961 )
962 .collect(),
963 ),
964 (19, 0x12004d, vec![(0x14001c3b, reference(0x409))]),
965 ] {
966 objects.insert(id(n), object(jcid, values)?);
967 }
968 if let Some([date, time]) = &self.date {
969 for (n, jcid, values) in self.date_fields(date, time) {
970 objects.insert(id(n), object(jcid, values)?);
971 }
972 }
973 }
974 let series = object(
975 0x60008,
976 vec![
977 (0x1c001c30, self.series_guid.to_vec()),
978 (0x18001c65, timestamp),
979 (0x2c001d63, reference(1)),
980 (0x24003442, reference(257)),
981 ],
982 )?;
983 if raw.objects.contains_key(&id(2)) || raw.objects.contains_key(&metadata_id) {
984 return Err(invalid("The new page's section identities already exist"));
985 }
986 let mut parent = PropertyObject::from_object(&raw.objects[&section_id])?;
987 let mut children = section_node.children.clone();
988 children.insert(position, id(2));
989 let mut references = Vec::new();
990 for child in children {
991 references.extend_from_slice(&parent.reference(child)?);
992 }
993 parent.set(&[(0x24001c20, &references)])?;
994 Ok((
995 BTreeMap::from([
996 (section_id, parent),
997 (id(2), series),
998 (metadata_id, object(0x20030, metadata)?),
999 ]),
1000 BTreeMap::from([(1, id(10)), (2, id(11))]),
1001 objects,
1002 ))
1003 }
1004 }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010 use crate::{
1011 RevisionIndex, Store,
1012 document::Document,
1013 op::{Edit, Op, OpError, SectionOp},
1014 write::{RevisionEdit, write_revisions},
1015 };
1016
1017 /// Page moves and removals through a section, sealed: the image they leave.
1018 fn edit_pages(
1019 source: &[u8],
1020 edits: &[PageEdit],
1021 removals: &[ExGuid],
1022 ) -> Result<Vec<u8>, OpError> {
1023 let arena = crate::Arena::default();
1024 let mut section = crate::Section::open(&arena, source.to_vec()).map_err(OpError::Failed)?;
1025 let mut ops = vec![Op::Section(SectionOp::Pages(edits.to_vec()))];
1026 if !removals.is_empty() {
1027 ops.push(Op::Section(SectionOp::Delete(removals.to_vec())));
1028 }
1029 section.apply(
1030 "Author",
1031 &Edit {
1032 at: 133_700_000_000_000_000,
1033 ops,
1034 },
1035 )?;
1036 section.seal().map_err(OpError::Failed)?;
1037 Ok(section.image())
1038 }
1039
1040 #[test]
1041 fn page_movement_restores_an_optional_metadata_cache_from_history() {
1042 let source =
1043 include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one");
1044 let store = Store::parse(source).unwrap();
1045 let index = RevisionIndex::parse(&store).unwrap();
1046 let document = Document::parse(&index).unwrap();
1047 let sid = document.pages().unwrap()[3].0;
1048 let view = document.active(document.root).unwrap();
1049 let series_id = view.nodes[&view.roots[&1]].children[3];
1050 let copy_id = metadata_id(sid);
1051 for unknown in [false, true] {
1052 let source = if unknown {
1053 write_revisions(source, |index| {
1054 let raw = index.resolve_active(index.root)?;
1055 let mut copy = PropertyObject::from_object(&raw.objects[&copy_id])?;
1056 copy.set(&[(0x1400abcd, &77_u32.to_le_bytes())])?;
1057 Ok(BTreeMap::from([(
1058 index.root,
1059 RevisionEdit::Update(BTreeMap::from([(copy_id, copy)])),
1060 )]))
1061 })
1062 .unwrap()
1063 } else {
1064 source.to_vec()
1065 };
1066 let source = write_revisions(&source, |index| {
1067 let raw = index.resolve_active(index.root)?;
1068 let mut series = PropertyObject::from_object(&raw.objects[&series_id])?;
1069 series.remove(&[0x24003442])?;
1070 Ok(BTreeMap::from([(
1071 index.root,
1072 RevisionEdit::Update(BTreeMap::from([(series_id, series)])),
1073 )]))
1074 })
1075 .unwrap();
1076 for changed in [false, true] {
1077 for unrelated_edit in [false, true] {
1078 let result = write_revisions(&source, |index| {
1079 let raw = index.resolve_active(index.root)?;
1080 assert!(!raw.reachable()?.contains(&copy_id));
1081 let mut copy = PropertyObject::from_object(&raw.objects[&copy_id])?;
1082 if changed {
1083 copy.set(&[(0x14001dff, &2_u32.to_le_bytes())])?;
1084 }
1085 let mut objects = BTreeMap::from([(copy_id, copy)]);
1086 if unrelated_edit {
1087 let mut series = PropertyObject::from_object(&raw.objects[&series_id])?;
1088 series.set(&[(0x1400abcd, &88_u32.to_le_bytes())])?;
1089 objects.insert(series_id, series);
1090 }
1091 Ok(BTreeMap::from([(
1092 index.root,
1093 RevisionEdit::Update(objects),
1094 )]))
1095 });
1096 assert_eq!(
1097 result.unwrap_err().message,
1098 "Edited object is not reachable in the resulting revision"
1099 );
1100 }
1101 }
1102 let written =
1103 edit_pages(&source, &[PageEdit::set_level(sid, 2).unwrap()], &[]).unwrap();
1104 let store = Store::parse(&written).unwrap();
1105 let index = RevisionIndex::parse(&store).unwrap();
1106 index.validate_current().unwrap();
1107 assert_eq!(Document::parse(&index).unwrap().pages().unwrap().len(), 9);
1108 let current = index
1109 .resolve(
1110 index.root,
1111 index.spaces[&index.root].labels[&(ExGuid::default(), 1)],
1112 )
1113 .unwrap();
1114 assert!(current.reachable().unwrap().contains(&copy_id));
1115 let copy = PropertyObject::from_object(&current.objects[&copy_id]).unwrap();
1116 let properties = crate::PropertySets::parse(&copy.bytes).unwrap();
1117 assert_eq!(unknown, properties.sets[0].iter().any(|field| field.id == 0x1400abcd
1118 && matches!(field.value, crate::Value::Bytes(bytes) if bytes == 77_u32.to_le_bytes())));
1119 let old_store = Store::parse(&source).unwrap();
1120 let old_index = RevisionIndex::parse(&old_store).unwrap();
1121 for (space, revisions) in &old_index.spaces {
1122 for revision in revisions.revisions.keys() {
1123 assert_eq!(
1124 format!("{:?}", old_index.resolve(*space, *revision).unwrap()),
1125 format!("{:?}", index.resolve(*space, *revision).unwrap())
1126 );
1127 }
1128 }
1129 if !unknown && let Some(output) = std::env::var_os("ONESTORE_PAGE_MOVEMENT_OUTPUT") {
1130 let output = std::path::Path::new(&output);
1131 assert!(output.is_absolute());
1132 for (name, bytes) in [
1133 ("optional-cache-source", source),
1134 ("optional-cache", written),
1135 ] {
1136 let path = output.join(name);
1137 std::fs::create_dir_all(&path).unwrap();
1138 std::fs::write(path.join("Lifecycle.one"), bytes).unwrap();
1139 }
1140 }
1141 }
1142 }
1143
1144 #[test]
1145 fn page_movement_rejects_empty_manifests_and_series() {
1146 let source =
1147 include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one");
1148 let store = Store::parse(source).unwrap();
1149 let index = RevisionIndex::parse(&store).unwrap();
1150 let document = Document::parse(&index).unwrap();
1151 let sid = document.pages().unwrap()[3].0;
1152 for empty_series in [false, true] {
1153 let invalid_source = write_revisions(source, |index| {
1154 let target = if empty_series { index.root } else { sid };
1155 let raw = index.resolve_active(target)?;
1156 let id = if empty_series {
1157 let view = document.active(document.root).unwrap();
1158 view.nodes[&view.roots[&1]].children[3]
1159 } else {
1160 raw.roots[&1]
1161 };
1162 let mut object = PropertyObject::from_object(&raw.objects[&id])?;
1163 set_references(
1164 &mut object,
1165 if empty_series { 0x2c001d63 } else { 0x24001c1f },
1166 &[],
1167 )?;
1168 Ok(BTreeMap::from([(
1169 target,
1170 RevisionEdit::Update(BTreeMap::from([(id, object)])),
1171 )]))
1172 })
1173 .unwrap();
1174 assert!(
1175 edit_pages(
1176 &invalid_source,
1177 &[PageEdit::set_level(sid, 2).unwrap()],
1178 &[]
1179 )
1180 .is_err()
1181 );
1182 }
1183 }
1184
1185 #[test]
1186 fn page_moves_retain_unknown_series_and_metadata_properties() {
1187 let source =
1188 include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one");
1189 let store = Store::parse(source).unwrap();
1190 let index = RevisionIndex::parse(&store).unwrap();
1191 let document = Document::parse(&index).unwrap();
1192 let pages = document.pages().unwrap();
1193 let view = document.active(document.root).unwrap();
1194 let series_id = view.nodes[&view.roots[&1]].children[3];
1195 let field = view.nodes[&series_id].extra[0]
1196 .iter()
1197 .find(|f| f.id == 0x24003442)
1198 .unwrap();
1199 let FieldValue::Objects(copies) = &field.value else {
1200 panic!()
1201 };
1202 let copy_id = copies[0];
1203 let source = write_revisions(source, |index| {
1204 let raw = index.resolve_active(index.root)?;
1205 let mut objects = BTreeMap::new();
1206 for id in [series_id, copy_id] {
1207 let mut object = PropertyObject::from_object(&raw.objects[&id])?;
1208 object.set(&[(0x1400abcd, &77_u32.to_le_bytes())])?;
1209 objects.insert(id, object);
1210 }
1211 Ok(BTreeMap::from([(
1212 index.root,
1213 RevisionEdit::Update(objects),
1214 )]))
1215 })
1216 .unwrap();
1217 for edits in [
1218 vec![PageEdit::set_level(pages[3].0, 2).unwrap()],
1219 pages[3..6]
1220 .iter()
1221 .enumerate()
1222 .map(|(i, (sid, _))| PageEdit::move_to(*sid, None, i as u32 + 1).unwrap())
1223 .collect(),
1224 ] {
1225 let written = edit_pages(&source, &edits, &[]).unwrap();
1226 let old_store = Store::parse(&source).unwrap();
1227 let new_store = Store::parse(&written).unwrap();
1228 let old = RevisionIndex::parse(&old_store).unwrap();
1229 let new = RevisionIndex::parse(&new_store).unwrap();
1230 let old_id = old.spaces[&old.root].labels[&(ExGuid::default(), 1)];
1231 let new_id = new.spaces[&new.root].labels[&(ExGuid::default(), 1)];
1232 assert_eq!(
1233 format!("{:?}", old.resolve(old.root, old_id).unwrap()),
1234 format!("{:?}", new.resolve(new.root, old_id).unwrap())
1235 );
1236 let current = new.resolve(new.root, new_id).unwrap();
1237 for id in [series_id, copy_id] {
1238 let object = PropertyObject::from_object(&current.objects[&id]).unwrap();
1239 let properties = crate::PropertySets::parse(&object.bytes).unwrap();
1240 assert!(properties.sets[0].iter().any(|field| field.id == 0x1400abcd
1241 && matches!(field.value, crate::Value::Bytes(bytes) if bytes == 77_u32.to_le_bytes())));
1242 }
1243 }
1244 }
1245}