1//! A section file kept parsed across edits. Edits apply to its object spaces in memory and
2//! seal into one transaction appending one revision per changed space.
3
4use crate::{
5 Chunk, Error, ExGuid, FileType, ObjectData, Reference, ResolvedRevision, RevisionIndex, Stamp,
6 Store, Transaction,
7 active::{ActivePage, Changes, Files},
8 document::Revision,
9 page::Page,
10 protected::{Key, Opened},
11 store::StoreState,
12 write::{
13 Appending, Commit, LiveRevision, PropertyObject, Protection, Sealing, Written, chain_depth,
14 check_transaction, declared, label_payload, node, replacement, unchanged,
15 },
16};
17use bumpalo::Bump;
18use std::collections::{BTreeMap, BTreeSet};
19
20mod versions;
21pub(crate) use versions::HISTORY;
22pub use versions::PageVersion;
23
24type Result<T> = std::result::Result<T, Error>;
25
26/// Holds the bytes a `Section` reads: the image it opened and each transaction it sealed.
27#[derive(Default)]
28pub struct Arena(Bump);
29
30/// A section file as its image and the transactions sealed on it leave it.
31pub struct Section<'a> {
32 arena: &'a Arena,
33 /// The opened image, then the bytes each sealed transaction appended, at their offsets.
34 segments: Vec<(u64, &'a [u8])>,
35 /// The in-place writes of the sealed transactions, in order.
36 patches: Vec<(u64, Vec<u8>)>,
37 state: StoreState,
38 /// Payloads the opened images embed, by identity, as they read.
39 files: &'a [([u8; 16], Result<&'a [u8]>)],
40 /// Payload identities the file-data store declares.
41 declared: BTreeSet<[u8; 16]>,
42 spaces: BTreeMap<ExGuid, Space<'a>>,
43 /// Payloads applied changes embed, in order, until sealed.
44 payloads: Vec<([u8; 16], &'a [u8])>,
45 /// Whether an operation failed after it began changing state; the section must be
46 /// reopened.
47 broken: bool,
48 /// The root object space, which lists the pages.
49 root: ExGuid,
50 /// While an edit applies, the spaces it changed as they were before it.
51 undo: Option<Undo<'a>>,
52 /// A password-protected section's key and what it decoded.
53 unlocked: Option<Opened<'a>>,
54}
55
56#[derive(Default)]
57struct Undo<'a> {
58 /// `None` for a space the edit created.
59 spaces: BTreeMap<ExGuid, Option<Space<'a>>>,
60 payloads: usize,
61}
62
63#[derive(Clone)]
64struct Space<'a> {
65 /// The active revision the file stores; none before a new space's first seal.
66 rid: Option<ExGuid>,
67 state: SpaceState<'a>,
68 /// The version a restore took the open revision from: the revision it depends on, which
69 /// the seal writes even unchanged.
70 restored: Option<ExGuid>,
71 /// Page states restores made versions of, which the seal writes before the open revision.
72 frozen: Vec<Frozen<'a>>,
73 history: Option<History<'a>>,
74 /// Revisions the seal labels as versions, with their contexts.
75 labels: Vec<(ExGuid, ExGuid)>,
76 /// The newest revision a seal appended to the space.
77 newest: Option<ExGuid>,
78}
79
80impl<'a> Space<'a> {
81 fn new(rid: Option<ExGuid>, state: SpaceState<'a>, history: Option<History<'a>>) -> Self {
82 Self {
83 rid,
84 state,
85 restored: None,
86 frozen: Vec::new(),
87 history,
88 labels: Vec::new(),
89 newest: None,
90 }
91 }
92}
93
94/// A page state a restore made a version of: a revision under the identity it chose.
95#[derive(Clone)]
96struct Frozen<'a> {
97 rid: ExGuid,
98 previous: Option<ExGuid>,
99 live: LiveRevision<'a>,
100 revised: Revised,
101}
102
103/// A revision `changed` committed: what it declares, and of those the objects whose bytes it
104/// stores and the objects new to the space.
105#[derive(Clone)]
106struct Revised {
107 commit: Commit,
108 replaced: BTreeSet<ExGuid>,
109 created: BTreeSet<ExGuid>,
110}
111
112/// A page's version history (`versions::HISTORY`), with the objects edits since the last
113/// seal replaced in it.
114#[derive(Clone)]
115struct History<'a> {
116 rid: ExGuid,
117 revision: ResolvedRevision<'a>,
118 depth: usize,
119 pending: BTreeMap<ExGuid, PropertyObject>,
120}
121
122#[derive(Clone)]
123enum SpaceState<'a> {
124 /// As the opened image stores it, until first edited.
125 Stored {
126 revision: ResolvedRevision<'a>,
127 depth: usize,
128 },
129 Open(Box<Open<'a>>),
130}
131
132#[derive(Clone)]
133struct Open<'a> {
134 /// The revision the file stores.
135 file: LiveRevision<'a>,
136 /// `file` with the changes applied since the last seal.
137 page: ActivePage<'a>,
138 /// Objects whose content, reachability or count changed since the last seal.
139 pending: BTreeSet<ExGuid>,
140}
141
142impl<'a> Section<'a> {
143 /// Parses and fully validates a section image.
144 pub fn open(arena: &'a Arena, image: Vec<u8>) -> Result<Self> {
145 Ok(Self::open_with(arena, vec![image], None, current)?)
146 }
147
148 /// `open` for a password-protected section, decoding it under `key`, which edits are
149 /// sealed under too. Decoded bytes live in `arena` until it drops.
150 pub fn unlock(
151 arena: &'a Arena,
152 image: Vec<u8>,
153 key: &Key,
154 ) -> std::result::Result<Self, crate::protected::Error> {
155 Self::open_with(arena, vec![image], Some(key), current)
156 }
157
158 /// Opens an earlier state of a section: each object space in `revisions` at the revision
159 /// given, taken from the first of `images` that stores it, and no other space; the first
160 /// image is the section's. For reading, as a merge reads the state two copies of a section
161 /// last shared, which may lie partly in each copy.
162 pub fn open_at(
163 arena: &'a Arena,
164 images: Vec<Vec<u8>>,
165 revisions: &BTreeMap<ExGuid, ExGuid>,
166 ) -> Result<Self> {
167 let section = Self::open_with(arena, images, None, |id, space| {
168 revisions
169 .get(id)
170 .copied()
171 .filter(|rid| space.revisions.contains_key(rid))
172 })?;
173 if !section.spaces.contains_key(&section.root) {
174 return Err(Error {
175 offset: 0,
176 message: "The earlier state names no revision of the section's root",
177 });
178 }
179 Ok(section)
180 }
181
182 /// `open`, each object space at the revision `revision` chooses in the first image where it
183 /// chooses one; a space it chooses none for is left out. Payloads are read from the image
184 /// that first declares them. A protected section opens under `key`.
185 fn open_with(
186 arena: &'a Arena,
187 images: Vec<Vec<u8>>,
188 key: Option<&Key>,
189 revision: impl Fn(&ExGuid, &crate::ObjectSpace<'_>) -> Option<ExGuid>,
190 ) -> std::result::Result<Self, crate::protected::Error> {
191 let mut opened = None;
192 let mut unlocked = None;
193 let mut spaces = BTreeMap::new();
194 let mut files: Vec<([u8; 16], Result<&'a [u8]>)> = Vec::new();
195 for image in images {
196 let bytes: &'a [u8] = arena.0.alloc_slice_copy(&image);
197 drop(image);
198 let store = Store::parse(bytes)?;
199 if let Some(offset) = store.checksum_mismatches.first() {
200 return Err(Error {
201 offset: *offset,
202 message: "Cannot write a file with transaction checksum damage",
203 }
204 .into());
205 }
206 if store.header.file_type != FileType::Section {
207 return Err(Error {
208 offset: 0,
209 message: "Choose a section file",
210 }
211 .into());
212 }
213 let index = RevisionIndex::parse(&store)?;
214 if let (Some(key), None) = (key, &unlocked) {
215 unlocked = Some(Opened::new(&store, key)?);
216 }
217 // The parse borrows `bytes` only as long as its store; its slices lie in `bytes`,
218 // which the arena keeps.
219 let bound = |resolved| {
220 bind(arena, unlocked.as_ref(), resolved, |part| {
221 Ok(within(bytes, part))
222 })
223 };
224 index.validate_with(|space, rid| bound(index.resolve(space, rid)?))?;
225 for (id, space) in &index.spaces {
226 let Some(rid) = revision(id, space).filter(|_| !spaces.contains_key(id)) else {
227 continue;
228 };
229 let revision = bound(index.resolve(*id, rid)?)?;
230 let history = space
231 .labels
232 .get(&(versions::HISTORY, 1))
233 .map(|history| -> Result<History<'a>> {
234 Ok(History {
235 rid: *history,
236 revision: bound(index.resolve(*id, *history)?)?,
237 depth: chain_depth(&index, *id, *history),
238 pending: BTreeMap::new(),
239 })
240 })
241 .transpose()?;
242 let state = SpaceState::Stored {
243 revision,
244 depth: chain_depth(&index, *id, rid),
245 };
246 spaces.insert(*id, Space::new(Some(rid), state, history));
247 }
248 let known = files.len();
249 for node in store.lists.values().flat_map(|list| &list.nodes) {
250 if node.id != 0x94 || node.freed() {
251 continue;
252 }
253 let (Some(guid), Some(Reference::Data(chunk))) =
254 (node.payload.first_chunk(), node.reference)
255 else {
256 return Err(Error {
257 offset: node.offset,
258 message: "File-data object lacks a data reference",
259 }
260 .into());
261 };
262 // A payload an earlier image declares is the same bytes: it names them.
263 if !files[..known].iter().any(|(declared, ..)| declared == guid) {
264 let (offset, length) = (chunk.offset as usize, chunk.length as usize);
265 let payload = bytes
266 .get(offset..offset + length)
267 .ok_or(Error {
268 offset,
269 message: "Chunk extends outside the data area",
270 })
271 .and_then(|blob| crate::files::payload(blob, offset))
272 .and_then(|payload| match &unlocked {
273 Some(unlocked) => unlocked.file(&arena.0, payload),
274 None => Ok(payload),
275 });
276 files.push((*guid, payload));
277 }
278 }
279 if opened.is_none() {
280 opened = Some((bytes, store.state()?, index.root));
281 }
282 }
283 let (bytes, state, root) = opened.ok_or(Error {
284 offset: 0,
285 message: "Choose a section file",
286 })?;
287 files.sort_by_key(|(guid, ..)| *guid);
288 Ok(Self {
289 arena,
290 segments: vec![(0, bytes)],
291 patches: Vec::new(),
292 state,
293 declared: files.iter().map(|(guid, ..)| *guid).collect(),
294 files: arena.0.alloc_slice_copy(&files),
295 spaces,
296 payloads: Vec::new(),
297 broken: false,
298 root,
299 undo: None,
300 unlocked,
301 })
302 }
303
304 /// The header and length of the image this section's sealed transactions leave.
305 pub fn stamp(&self) -> &Stamp {
306 &self.state.stamp
307 }
308
309 /// The image this section's sealed transactions leave; for tests and seeding caches.
310 pub fn image(&self) -> Vec<u8> {
311 let mut image = self
312 .segments
313 .iter()
314 .flat_map(|(_, bytes)| *bytes)
315 .copied()
316 .collect::<Vec<_>>();
317 for (offset, bytes) in &self.patches {
318 let offset = *offset as usize;
319 image[offset..offset + bytes.len()].copy_from_slice(bytes);
320 }
321 image[..1024].copy_from_slice(&self.state.stamp.header);
322 image
323 }
324
325 /// Each object space with the active revision the sealed image stores for it.
326 pub fn revisions(&self) -> impl Iterator<Item = (ExGuid, ExGuid)> + '_ {
327 self.spaces
328 .iter()
329 .filter_map(|(space, stored)| Some((*space, stored.rid?)))
330 }
331
332 /// Each object space with the revision this section last sealed in it, where one did, or
333 /// its active revision: a seal that changes only a page's versions leaves the page's
334 /// active revision as it was.
335 pub fn newest(&self) -> impl Iterator<Item = (ExGuid, ExGuid)> + '_ {
336 self.spaces
337 .iter()
338 .filter_map(|(space, stored)| Some((*space, stored.newest.or(stored.rid)?)))
339 }
340
341 /// The page an object space holds, with the changes applied since the last seal.
342 pub fn page(&self, space: ExGuid) -> Result<Page> {
343 let stored = self.spaces.get(&space).ok_or(Error {
344 offset: 0,
345 message: "Object space has no active default revision",
346 })?;
347 match &stored.state {
348 SpaceState::Open(open) => one_page(&open.page.view, &open.page.pages),
349 SpaceState::Stored { revision, .. } => {
350 let files = self.files();
351 let (view, _) =
352 Revision::parse(space, revision, FileType::Section, &mut |guid| files(guid))?;
353 one_page(&view, &crate::active::manifest_pages(&view))
354 }
355 }
356 }
357
358 /// The conflict pages of each page that has them, in section order, each page's in the
359 /// order OneNote 2010 lists them under it: the last stored first. Read from the pages'
360 /// manifests and the conflict pages' metadata alone.
361 pub fn conflicts(&mut self) -> Result<Vec<(ExGuid, Vec<crate::ConflictPage>)>> {
362 let listed: Vec<ExGuid> = self.pages()?.into_iter().map(|(space, ..)| space).collect();
363 let element = |object: &crate::Object<'a>| {
364 crate::document::Element::parse_with(object, FileType::Section, &mut |_| {
365 Err(Error {
366 offset: 0,
367 message: "Page metadata holds no payload",
368 })
369 })
370 };
371 let mut conflicts = Vec::new();
372 for page in listed {
373 let revision = self.revision(page)?;
374 let Some(manifest) = revision
375 .roots
376 .get(&1)
377 .and_then(|id| revision.objects.get(id))
378 else {
379 continue;
380 };
381 let mut pages = Vec::new();
382 for space in element(manifest)?.spaces.into_iter().rev() {
383 let Some(metadata) = self.revision(space).ok().and_then(|revision| {
384 revision
385 .roots
386 .get(&2)
387 .and_then(|id| revision.objects.get(id))
388 }) else {
389 continue;
390 };
391 let metadata = element(metadata)?;
392 let crate::document::Kind::ConflictMetadata { title, author, .. } = metadata.kind
393 else {
394 continue;
395 };
396 let created = metadata.extra[0]
397 .iter()
398 .find_map(|field| match field.value {
399 crate::document::FieldValue::Bytes(bytes) if field.id == 0x18001c65 => {
400 bytes.try_into().ok().map(u64::from_le_bytes)
401 }
402 _ => None,
403 });
404 let mut objects = Vec::new();
405 for (id, object) in &self.revision(space)?.objects {
406 let ObjectData::Properties(bytes) = object.data else {
407 continue;
408 };
409 if crate::PropertySets::parse(bytes)?.sets[0]
410 .iter()
411 .any(|property| property.id == 0x88001d96)
412 {
413 objects.push(*id);
414 }
415 }
416 pages.push(crate::ConflictPage {
417 space,
418 title: title.unwrap_or_default(),
419 user: author.unwrap_or_default(),
420 created,
421 objects,
422 });
423 }
424 if !pages.is_empty() {
425 conflicts.push((page, pages));
426 }
427 }
428 Ok(conflicts)
429 }
430
431 /// The page series holding each listed page. Moving a page gives it a series of its
432 /// own, so a page in another series than before was moved.
433 pub fn series(&mut self) -> Result<BTreeMap<ExGuid, ExGuid>> {
434 let view = &self.active(self.root)?.view;
435 let section = view
436 .roots
437 .get(&1)
438 .and_then(|id| view.nodes.get(id))
439 .ok_or(Error {
440 offset: 0,
441 message: "Section root is unavailable",
442 })?;
443 Ok(section
444 .children
445 .iter()
446 .filter_map(|series| Some((*series, view.nodes.get(series)?)))
447 .flat_map(|(series, node)| node.spaces.iter().map(move |page| (*page, series)))
448 .collect())
449 }
450
451 /// Page spaces in section order with the title and outline level (1 at the top) the
452 /// page list shows, read from each page's metadata alone.
453 pub fn pages(&mut self) -> Result<Vec<(ExGuid, String, u32)>> {
454 let root = self.root;
455 let view = &self.active(root)?.view;
456 let section = view
457 .roots
458 .get(&1)
459 .and_then(|id| view.nodes.get(id))
460 .ok_or(Error {
461 offset: 0,
462 message: "Section root is unavailable",
463 })?;
464 let spaces: Vec<ExGuid> = section
465 .children
466 .iter()
467 .filter_map(|series| view.nodes.get(series))
468 .flat_map(|series| series.spaces.clone())
469 .collect();
470 let element = |object: &crate::Object<'a>| {
471 crate::document::Element::parse_with(object, FileType::Section, &mut |_| {
472 Err(Error {
473 offset: 0,
474 message: "Page metadata holds no payload",
475 })
476 })
477 };
478 let mut pages = Vec::new();
479 for space in spaces {
480 let revision = self.revision(space)?;
481 let (mut title, mut level) = (None, 1);
482 if let Some(metadata) = revision
483 .roots
484 .get(&2)
485 .and_then(|id| revision.objects.get(id))
486 && let crate::document::Kind::Metadata {
487 title: stored,
488 level: stored_level,
489 } = element(metadata)?.kind
490 {
491 title = stored;
492 level = stored_level.unwrap_or(1);
493 }
494 if title.is_none() {
495 let manifest = revision
496 .roots
497 .get(&1)
498 .and_then(|id| revision.objects.get(id));
499 if let Some(manifest) = manifest
500 && let Some(page) = element(manifest)?
501 .content
502 .first()
503 .and_then(|id| revision.objects.get(id))
504 && let crate::document::Kind::Page {
505 alternate_title, ..
506 } = element(page)?.kind
507 {
508 title = alternate_title;
509 }
510 }
511 let title = crate::edit::without_fields(&title.unwrap_or_default());
512 pages.push((space, title, level));
513 }
514 Ok(pages)
515 }
516
517 /// Reads a payload of the opened image by identity.
518 fn files(&self) -> Files<'a> {
519 let files = self.files;
520 std::rc::Rc::new(move |guid| {
521 let start = files.partition_point(|(id, _)| *id < guid);
522 let mut declared = files[start..].iter().take_while(|(id, _)| *id == guid);
523 match (declared.next(), declared.next()) {
524 (None, _) => Err(Error {
525 offset: 0,
526 message: "File-data object is not declared",
527 }),
528 (Some((_, payload)), None) => *payload,
529 _ => Err(Error {
530 offset: 0,
531 message: "Duplicate file-data identity",
532 }),
533 }
534 })
535 }
536
537 fn usable(&self) -> Result<()> {
538 if self.broken {
539 return Err(Error {
540 offset: 0,
541 message: "A failed edit left the section to be reopened",
542 });
543 }
544 Ok(())
545 }
546
547 /// The editable state of an object space, opened on first use.
548 pub(crate) fn active(&mut self, space: ExGuid) -> Result<&ActivePage<'a>> {
549 Ok(&self.editable(space)?.page)
550 }
551
552 fn editable(&mut self, id: ExGuid) -> Result<&mut Open<'a>> {
553 let files = self.files();
554 let space = self.spaces.get_mut(&id).ok_or(Error {
555 offset: 0,
556 message: "Object space has no active default revision",
557 })?;
558 if let SpaceState::Stored { revision, depth } = &space.state {
559 let file = LiveRevision::new(revision.clone(), *depth)?;
560 let page = ActivePage::open(id, file.clone(), FileType::Section, files)?;
561 space.state = SpaceState::Open(Box::new(Open {
562 file,
563 page,
564 pending: BTreeSet::new(),
565 }));
566 }
567 let SpaceState::Open(open) = &mut space.state else {
568 unreachable!()
569 };
570 Ok(open)
571 }
572
573 /// Keeps a space as it is for the running edit to return to, the first time it changes it.
574 pub(crate) fn remember(&mut self, space: ExGuid) {
575 if let Some(undo) = &mut self.undo
576 && !undo.spaces.contains_key(&space)
577 {
578 undo.spaces.insert(space, self.spaces.get(&space).cloned());
579 }
580 }
581
582 /// Stores a writer's objects and payloads in a space until the next seal; false when
583 /// it stores nothing. An error leaves the section to be reopened.
584 pub(crate) fn apply_changes(
585 &mut self,
586 space: ExGuid,
587 payloads: &[([u8; 16], &[u8])],
588 changes: Changes,
589 ) -> Result<bool> {
590 self.usable()?;
591 self.remember(space);
592 let arena = &self.arena.0;
593 self.broken = true;
594 let open = self.editable(space)?;
595 let embedded = open.page.payloads.len();
596 let Some(touched) = open.page.store(arena, payloads, changes)? else {
597 self.broken = false;
598 return Ok(false);
599 };
600 open.pending.extend(touched);
601 let embedded = open.page.payloads[embedded..].to_vec();
602 self.payloads.extend(embedded);
603 self.broken = false;
604 Ok(true)
605 }
606
607 /// Creates an object space holding `changes` under `roots` until the next seal, which
608 /// declares it. An error after validation leaves the section to be reopened.
609 pub(crate) fn create_space(
610 &mut self,
611 space: ExGuid,
612 roots: BTreeMap<u32, ExGuid>,
613 changes: Changes,
614 ) -> Result<()> {
615 self.usable()?;
616 if space.guid == [0; 16] || self.spaces.contains_key(&space) {
617 return Err(Error {
618 offset: 0,
619 message: "Choose a new object-space identity in a section file",
620 });
621 }
622 if roots.is_empty() || changes.is_empty() {
623 return Err(Error {
624 offset: 0,
625 message: "New object space needs roots and objects",
626 });
627 }
628 let file = LiveRevision::new(
629 ResolvedRevision {
630 roots,
631 objects: BTreeMap::new(),
632 },
633 0,
634 )?;
635 let mut work = file.clone();
636 let changes = work.prepare(changes, true)?;
637 let mut objects = Vec::new();
638 for (id, change) in changes {
639 let bytes = self.arena.0.alloc_slice_copy(&change.bytes);
640 objects.push((id, declared(change.jcid, bytes, change.global_ids)?));
641 }
642 if let Some(undo) = &mut self.undo {
643 undo.spaces.entry(space).or_insert(None);
644 }
645 self.broken = true;
646 let pending = work.commit(objects)?.touched;
647 let all: Vec<ExGuid> = work.revision.objects.keys().copied().collect();
648 work.settle(all)?;
649 let page = ActivePage::open(space, work, FileType::Section, self.files())?;
650 let state = SpaceState::Open(Box::new(Open {
651 file,
652 page,
653 pending,
654 }));
655 self.spaces.insert(space, Space::new(None, state, None));
656 self.broken = false;
657 Ok(())
658 }
659
660 /// The root object space, which lists the pages.
661 pub fn root(&self) -> ExGuid {
662 self.root
663 }
664
665 /// The stored revision of a space, as edits since the last seal leave it.
666 pub(crate) fn revision(&self, space: ExGuid) -> Result<&ResolvedRevision<'a>> {
667 match self.spaces.get(&space).map(|space| &space.state) {
668 Some(SpaceState::Stored { revision, .. }) => Ok(revision),
669 Some(SpaceState::Open(open)) => Ok(&open.page.live.revision),
670 None => Err(Error {
671 offset: 0,
672 message: "Object space has no active default revision",
673 }),
674 }
675 }
676
677 /// Runs `f`, returning the section to its state before it when `f` fails; `undo`
678 /// saves each space before `f` first changes it, which costs a copy of that space.
679 pub(crate) fn atomically<T, E>(
680 &mut self,
681 undo: bool,
682 f: impl FnOnce(&mut Self) -> std::result::Result<T, E>,
683 ) -> std::result::Result<T, E> {
684 if undo {
685 self.undo = Some(Undo {
686 spaces: BTreeMap::new(),
687 payloads: self.payloads.len(),
688 });
689 }
690 let result = f(self);
691 if let Some(undo) = self.undo.take()
692 && result.is_err()
693 {
694 for (id, space) in undo.spaces {
695 match space {
696 Some(space) => self.spaces.insert(id, space),
697 None => self.spaces.remove(&id),
698 };
699 }
700 self.payloads.truncate(undo.payloads);
701 self.broken = false;
702 }
703 result
704 }
705
706 /// Whether a failed operation left the section to be reopened.
707 pub(crate) fn broken(&self) -> bool {
708 self.broken
709 }
710
711 /// Appends one revision per space changed since the last seal, and the payloads they
712 /// embed, as one transaction on `stamp`; none when nothing changed. An error leaves the
713 /// section to be reopened.
714 pub fn seal(&mut self) -> Result<Option<Transaction>> {
715 self.seal_as(&BTreeMap::new())
716 }
717
718 /// `seal`, each space in `names` taking the revision identity named there, one the space
719 /// does not store, instead of a fresh one: a merge names what it wrote after what it
720 /// merged, so that the next merge knows it holds that.
721 pub fn seal_as(&mut self, names: &BTreeMap<ExGuid, ExGuid>) -> Result<Option<Transaction>> {
722 self.usable()?;
723 if names.iter().any(|(space, name)| {
724 name.guid == [0; 16]
725 || self
726 .spaces
727 .get(space)
728 .is_some_and(|stored| stored.rid == Some(*name))
729 }) {
730 return Err(Error {
731 offset: 0,
732 message: "Name each revision anew",
733 });
734 }
735 self.broken = true;
736 let arena = &self.arena.0;
737 let protection = self
738 .unlocked
739 .as_ref()
740 .map(|unlocked| unlocked as &dyn Protection);
741 let mut appending = Appending::new(self.state.clone());
742 let mut sealed = Vec::new();
743 for (id, space) in &mut self.spaces {
744 // A space's revisions and labels go in one fragment: page states restores made
745 // versions of, the open revision, the version history, then the labels.
746 let mut manifest = Vec::new();
747 for frozen in &space.frozen {
748 appending.manifest(
749 &Sealing {
750 space: *id,
751 previous: frozen.previous,
752 new_space: false,
753 label: (ExGuid::default(), 1),
754 rid: Some(frozen.rid),
755 live: &frozen.live,
756 commit: &frozen.revised.commit,
757 replaced: &frozen.revised.replaced,
758 created: &frozen.revised.created,
759 },
760 &self.segments,
761 protection,
762 &mut manifest,
763 )?;
764 }
765 let mut page = None;
766 if let SpaceState::Open(open) = &mut space.state
767 && (!open.pending.is_empty() || space.restored.is_some())
768 {
769 let file = &mut open.file;
770 let restored = space.restored.is_some();
771 let written = match changed(arena, &open.page.live, &open.pending, file, restored)?
772 {
773 Some(Revised {
774 commit,
775 replaced,
776 created,
777 }) => {
778 let (rid, stored) = appending.manifest(
779 &Sealing {
780 space: *id,
781 previous: space.restored.or(space.rid),
782 new_space: space.rid.is_none(),
783 label: (ExGuid::default(), 1),
784 rid: names.get(id).copied(),
785 live: file,
786 commit: &commit,
787 replaced: &replaced,
788 created: &created,
789 },
790 &self.segments,
791 protection,
792 &mut manifest,
793 )?;
794 if commit.checkpoint {
795 let all: Vec<ExGuid> = file.revision.objects.keys().copied().collect();
796 file.settle(all)?;
797 } else {
798 file.settle(commit.changed.iter().copied())?;
799 }
800 Some((rid, stored, commit))
801 }
802 None => None,
803 };
804 page = Some(written);
805 }
806 let mut history = None;
807 if let Some(stored) = &space.history
808 && !stored.pending.is_empty()
809 {
810 let mut live = LiveRevision::new(stored.revision.clone(), stored.depth)?;
811 let replacements = live.prepare(stored.pending.clone(), true)?;
812 if !replacements.is_empty() {
813 let replaced: BTreeSet<ExGuid> = replacements.keys().copied().collect();
814 let created = replaced
815 .iter()
816 .filter(|id| !live.revision.objects.contains_key(id))
817 .copied()
818 .collect();
819 let mut objects = Vec::new();
820 for (object_id, change) in replacements {
821 let bytes = arena.alloc_slice_copy(&change.bytes);
822 objects.push((object_id, declared(change.jcid, bytes, change.global_ids)?));
823 }
824 let commit = live.commit(objects)?;
825 let (rid, chunks) = appending.manifest(
826 &Sealing {
827 space: *id,
828 previous: Some(stored.rid),
829 new_space: false,
830 label: (versions::HISTORY, 1),
831 rid: None,
832 live: &live,
833 commit: &commit,
834 replaced: &replaced,
835 created: &created,
836 },
837 &self.segments,
838 protection,
839 &mut manifest,
840 )?;
841 if commit.checkpoint {
842 let all: Vec<ExGuid> = live.revision.objects.keys().copied().collect();
843 live.settle(all)?;
844 } else {
845 live.settle(commit.changed.iter().copied())?;
846 }
847 history = Some((rid, chunks, live, commit));
848 }
849 }
850 for (rid, context) in &space.labels {
851 manifest.push(node(0x5d, None, &label_payload(*rid, *context))?);
852 }
853 if !manifest.is_empty() {
854 appending.close(*id, space.rid.is_none(), &manifest)?;
855 }
856 if page.is_some() || !manifest.is_empty() {
857 sealed.push((*id, page, history));
858 }
859 }
860 let payloads: Vec<_> = self
861 .payloads
862 .iter()
863 .filter(|(guid, _)| !self.declared.contains(guid))
864 .copied()
865 .collect();
866 appending.payloads(&payloads, protection)?;
867 #[cfg_attr(not(test), expect(unused_mut))]
868 let mut transaction = appending.finish()?;
869 #[cfg(test)]
870 if let (Some(tamper), Some((transaction, _))) = (tests::TAMPER.get(), &mut transaction) {
871 tamper(transaction);
872 }
873 if let Some((transaction, state)) = &transaction {
874 let mut written = Vec::new();
875 let mut labels = Vec::new();
876 for (id, page, history) in &sealed {
877 let space = &self.spaces[id];
878 for frozen in &space.frozen {
879 written.push(Written {
880 space: *id,
881 rid: frozen.rid,
882 previous: frozen.previous,
883 label: (ExGuid::default(), 1),
884 live: &frozen.live,
885 commit: &frozen.revised.commit,
886 });
887 }
888 if let (Some(Some((rid, _, commit))), SpaceState::Open(open)) = (page, &space.state)
889 {
890 written.push(Written {
891 space: *id,
892 rid: *rid,
893 previous: space.restored.or(space.rid),
894 label: (ExGuid::default(), 1),
895 live: &open.file,
896 commit,
897 });
898 }
899 if let (Some((rid, _, live, commit)), Some(stored)) = (history, &space.history) {
900 written.push(Written {
901 space: *id,
902 rid: *rid,
903 previous: Some(stored.rid),
904 label: (versions::HISTORY, 1),
905 live,
906 commit,
907 });
908 }
909 labels.extend(
910 space
911 .labels
912 .iter()
913 .map(|(rid, context)| (*id, *rid, *context)),
914 );
915 }
916 check_transaction(
917 &self.state,
918 state,
919 transaction,
920 &self.segments,
921 &written,
922 &labels,
923 &payloads,
924 protection,
925 )?;
926 let base = transaction.base.length;
927 let segment: &'a [u8] = arena.alloc_slice_copy(&transaction.append);
928 self.segments.push((base, segment));
929 self.patches.extend(transaction.patches.iter().cloned());
930 self.state = state.clone();
931 self.declared.extend(payloads.iter().map(|(guid, _)| *guid));
932 // An object now reads from where it is stored, or a protected one's clear bytes
933 // from where they are.
934 let unlocked = &self.unlocked;
935 let rebind = |revision: &mut ResolvedRevision<'a>, stored: &[(ExGuid, Chunk)]| {
936 for (object_id, chunk) in stored {
937 let start = (chunk.offset - base) as usize;
938 let bytes = &segment[start..start + chunk.length as usize];
939 let data = &mut revision.objects.get_mut(object_id).unwrap().data;
940 match (unlocked, *data) {
941 (Some(unlocked), ObjectData::Properties(clear)) => {
942 unlocked.sealed(clear, bytes)
943 }
944 _ => *data = ObjectData::Properties(bytes),
945 }
946 }
947 };
948 for (id, page, history) in &mut sealed {
949 let space = self.spaces.get_mut(id).unwrap();
950 space.frozen.clear();
951 space.labels.clear();
952 if let (Some(Some((rid, stored, _))), SpaceState::Open(open)) =
953 (&*page, &mut space.state)
954 {
955 space.rid = Some(*rid);
956 space.restored = None;
957 space.newest = Some(*rid);
958 rebind(&mut open.file.revision, stored);
959 }
960 if let Some((rid, stored, live, _)) = history.take() {
961 let mut revision = live.revision;
962 rebind(&mut revision, &stored);
963 space.history = Some(History {
964 rid,
965 revision,
966 depth: live.depth,
967 pending: BTreeMap::new(),
968 });
969 space.newest = Some(rid);
970 }
971 }
972 }
973 for (id, page, _) in sealed {
974 let Some(revision) = page else {
975 continue;
976 };
977 let SpaceState::Open(open) = &mut self.spaces.get_mut(&id).unwrap().state else {
978 unreachable!()
979 };
980 let mut compare = std::mem::take(&mut open.pending);
981 if let Some((_, _, commit)) = revision {
982 if commit.checkpoint {
983 compare.extend(open.file.revision.objects.keys());
984 } else {
985 compare.extend(commit.touched);
986 }
987 }
988 open.page.adopt(&open.file, compare)?;
989 }
990 self.payloads.clear();
991 self.broken = false;
992 Ok(transaction.map(|(transaction, _)| transaction))
993 }
994
995 /// Applies a transaction sealed on this section's stamp, as reopening the image it
996 /// leaves would; nothing may be applied since the last seal.
997 pub fn replay(&mut self, transaction: &Transaction) -> Result<()> {
998 self.usable()?;
999 if !self.payloads.is_empty()
1000 || self.spaces.values().any(
1001 |space| matches!(&space.state, SpaceState::Open(open) if !open.pending.is_empty()),
1002 )
1003 {
1004 return Err(Error {
1005 offset: 0,
1006 message: "Seal applied changes before replaying a transaction",
1007 });
1008 }
1009 let mut image = self.image();
1010 transaction.apply(&mut image)?;
1011 let key = self
1012 .unlocked
1013 .as_ref()
1014 .map(|unlocked| unlocked.key().clone());
1015 *self = Self::open_with(self.arena, vec![image], key.as_ref(), current)?;
1016 Ok(())
1017 }
1018}
1019
1020/// Commits to `file` the objects `pending` names that `page` changed, with what the revision
1021/// declares, replaces and creates; none when that changes nothing, unless `force`.
1022fn changed<'a>(
1023 arena: &'a Bump,
1024 page: &LiveRevision<'a>,
1025 pending: &BTreeSet<ExGuid>,
1026 file: &mut LiveRevision<'a>,
1027 force: bool,
1028) -> Result<Option<Revised>> {
1029 let mut replacements = BTreeMap::new();
1030 for object_id in pending {
1031 if !page.is_reachable(*object_id) {
1032 continue;
1033 }
1034 let object = &page.revision.objects[object_id];
1035 if let Some(stored) = file.revision.objects.get(object_id)
1036 && unchanged(stored, object)?
1037 {
1038 continue;
1039 }
1040 replacements.insert(*object_id, replacement(*object_id, object)?);
1041 }
1042 let replacements = file.prepare(replacements, true)?;
1043 if replacements.is_empty() && !force {
1044 return Ok(None);
1045 }
1046 let replaced: BTreeSet<ExGuid> = replacements.keys().copied().collect();
1047 let created = replaced
1048 .iter()
1049 .filter(|id| !file.revision.objects.contains_key(id))
1050 .copied()
1051 .collect();
1052 let mut objects = Vec::new();
1053 for (object_id, change) in replacements {
1054 // The page holds these bytes already unless preparing remapped them.
1055 let bytes = match page.revision.objects.get(&object_id).map(|o| o.data) {
1056 Some(ObjectData::Properties(bytes)) if bytes == change.bytes => bytes,
1057 _ => arena.alloc_slice_copy(&change.bytes),
1058 };
1059 objects.push((object_id, declared(change.jcid, bytes, change.global_ids)?));
1060 }
1061 Ok(Some(Revised {
1062 commit: file.commit(objects)?,
1063 replaced,
1064 created,
1065 }))
1066}
1067
1068/// `resolved` with every slice moved into bytes that live as long as the section by `bytes`,
1069/// a protected section's objects decoded under `unlocked`.
1070fn bind<'a>(
1071 arena: &'a Arena,
1072 unlocked: Option<&Opened<'a>>,
1073 resolved: ResolvedRevision<'_>,
1074 bytes: impl Fn(&[u8]) -> Result<&'a [u8]>,
1075) -> Result<ResolvedRevision<'a>> {
1076 let mut objects = BTreeMap::new();
1077 for (id, object) in resolved.objects {
1078 let data = match (object.data, unlocked) {
1079 (ObjectData::Properties(data), _) => ObjectData::Properties(bytes(data)?),
1080 (ObjectData::Encrypted(data), Some(unlocked)) => {
1081 ObjectData::Properties(unlocked.property(&arena.0, bytes(data)?)?)
1082 }
1083 (ObjectData::Encrypted(data), None) => ObjectData::Encrypted(bytes(data)?),
1084 (
1085 ObjectData::File {
1086 reference,
1087 extension,
1088 },
1089 _,
1090 ) => ObjectData::File {
1091 reference: bytes(reference)?,
1092 extension: bytes(extension)?,
1093 },
1094 };
1095 let object = crate::Object {
1096 jcid: object.jcid,
1097 reference_count: object.reference_count,
1098 data,
1099 global_ids: object.global_ids,
1100 };
1101 objects.insert(id, object);
1102 }
1103 Ok(ResolvedRevision {
1104 roots: resolved.roots,
1105 objects,
1106 })
1107}
1108
1109/// The revision a space is current under.
1110fn current(_: &ExGuid, space: &crate::ObjectSpace<'_>) -> Option<ExGuid> {
1111 space.labels.get(&(ExGuid::default(), 1)).copied()
1112}
1113
1114/// `part`, a slice of `image`, with the image's lifetime.
1115fn within<'a>(image: &'a [u8], part: &[u8]) -> &'a [u8] {
1116 let start = part.as_ptr().addr() - image.as_ptr().addr();
1117 &image[start..start + part.len()]
1118}
1119
1120fn one_page(view: &Revision<'_>, pages: &[ExGuid]) -> Result<Page> {
1121 let [page] = pages else {
1122 return Err(Error {
1123 offset: 0,
1124 message: "Choose an object space containing one active page",
1125 });
1126 };
1127 Page::from_revision(view, *page)
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133 use crate::{Insertion, active::tests::Writes, document::Document, write::GUIDS};
1134
1135 thread_local! {
1136 /// Changes a transaction after it is built and before it is checked.
1137 pub(super) static TAMPER: std::cell::Cell<Option<fn(&mut Transaction)>> =
1138 const { std::cell::Cell::new(None) };
1139 }
1140
1141 /// `f` with `fresh_guid` counting from `seed`.
1142 fn seeded<T>(seed: u64, f: impl FnOnce() -> T) -> T {
1143 let outer = GUIDS.replace(Some(seed));
1144 let result = f();
1145 GUIDS.set(outer);
1146 result
1147 }
1148
1149 /// The image the image writer leaves: the `edited` revisions squashed onto the source
1150 /// `index` parsed.
1151 fn squashed(
1152 index: &RevisionIndex<'_>,
1153 edited: &[(ExGuid, &ResolvedRevision<'_>)],
1154 payloads: &[([u8; 16], &[u8])],
1155 ) -> Vec<u8> {
1156 let transaction = crate::write::squash(index, edited, payloads).unwrap();
1157 crate::write::applied(index.store.data, transaction.as_ref()).unwrap()
1158 }
1159
1160 /// The page spaces of an image, fullest first.
1161 fn page_spaces(image: &[u8]) -> Vec<ExGuid> {
1162 let store = Store::parse(image).unwrap();
1163 let index = RevisionIndex::parse(&store).unwrap();
1164 let document = Document::parse(&index).unwrap();
1165 let mut spaces: Vec<_> = document
1166 .pages()
1167 .unwrap()
1168 .into_iter()
1169 .map(|(space, _)| (document.active(space).unwrap().nodes.len(), space))
1170 .collect();
1171 spaces.sort_by(|a, b| b.cmp(a));
1172 spaces.into_iter().map(|(_, space)| space).collect()
1173 }
1174
1175 /// Applies seeded batches of typed-writer changes to the pages of `source` through a
1176 /// `Section` and through the writer that parses the image and squashes a batch into
1177 /// one revision, requiring equal bytes after every seal.
1178 fn differential(
1179 source: &[u8],
1180 seed: u64,
1181 batches: usize,
1182 largest: usize,
1183 pages: usize,
1184 ) -> usize {
1185 let arena = Arena::default();
1186 let mut section = Section::open(&arena, source.to_vec()).unwrap();
1187 let spaces = page_spaces(source);
1188 let mut image = source.to_vec();
1189 let mut writes = Writes(seed);
1190 // New objects count from far above the identities builds draw.
1191 GUIDS.set(Some(1 << 62 | seed << 40));
1192 let (mut sealed, mut checkpoints) = (0, 0);
1193 for batch in 0..batches {
1194 // The writers stamp modification times; a fixed clock keeps the batches alike.
1195 let at = 134_000_000_000_000_000 + batch as u64 * 10_000_000;
1196 crate::create::at(at, || {
1197 let space = spaces[batch % spaces.len().min(pages)];
1198 let store = Store::parse(&image).unwrap();
1199 let index = RevisionIndex::parse(&store).unwrap();
1200 let bump = Bump::new();
1201 let mut legacy = ActivePage::parse(&index, space).unwrap();
1202 for _ in 0..1 + (writes.0 as usize >> 40) % largest {
1203 let Ok(changes) = writes.next(section.active(space).unwrap()) else {
1204 continue;
1205 };
1206 let stored = legacy.write(&bump, &[], changes.clone()).unwrap();
1207 assert_eq!(
1208 section.apply_changes(space, &[], changes).unwrap(),
1209 stored,
1210 "batch {batch}"
1211 );
1212 }
1213 let guids = (batch as u64 + 1) << 32;
1214 let expected = seeded(guids, || {
1215 squashed(&index, &[(space, &legacy.live.revision)], &legacy.payloads)
1216 });
1217 let before = section.stamp().clone();
1218 let chains = depths(&section);
1219 let transaction = seeded(guids, || section.seal().unwrap());
1220 checkpoints += depths(&section)
1221 .iter()
1222 .filter(|(id, depth)| chains.get(id).is_some_and(|before| before > depth))
1223 .count();
1224 let mut written = image.clone();
1225 if let Some(transaction) = &transaction {
1226 assert_eq!(transaction.base, before);
1227 transaction.apply(&mut written).unwrap();
1228 sealed += 1;
1229 }
1230 assert!(written == expected, "batch {batch}");
1231 assert_eq!(Stamp::of(&written).unwrap(), *section.stamp());
1232 drop(legacy);
1233 drop(index);
1234 drop(store);
1235 image = expected;
1236 if batch % 25 == 0 || batch + 1 == batches {
1237 assert!(section.image() == image);
1238 let store = Store::parse(&image).unwrap();
1239 let index = RevisionIndex::parse(&store).unwrap();
1240 index.validate_current().unwrap();
1241 let document = Document::parse(&index).unwrap();
1242 for space in &spaces {
1243 assert_eq!(
1244 section.page(*space).unwrap(),
1245 Page::from_space(&document, *space).unwrap()
1246 );
1247 }
1248 }
1249 });
1250 }
1251 GUIDS.set(None);
1252 assert!(sealed > 0, "no batch of {batches} sealed");
1253 checkpoints
1254 }
1255
1256 /// Chain depths of the spaces a section has opened.
1257 fn depths(section: &Section<'_>) -> BTreeMap<ExGuid, usize> {
1258 section
1259 .spaces
1260 .iter()
1261 .filter_map(|(id, space)| match &space.state {
1262 SpaceState::Open(open) => Some((*id, open.file.depth)),
1263 SpaceState::Stored { .. } => None,
1264 })
1265 .collect()
1266 }
1267
1268 #[test]
1269 fn seals_append_the_bytes_the_image_writer_does() {
1270 let corpus = [
1271 include_bytes!(
1272 "../../../corpus/native/20260905-05/snapshots/02-text/notebook/synthetic.one"
1273 )
1274 .as_slice(),
1275 include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one"),
1276 include_bytes!("../../../corpus/outline-edit/tree/before/notebook/synthetic.one"),
1277 include_bytes!("../../../corpus/paragraph-edit/before/notebook/synthetic.one"),
1278 ];
1279 let shift = crate::sweep::full().unwrap_or(0);
1280 for (seed, source) in (shift..).zip(corpus) {
1281 differential(source, seed, 60, 1, 2);
1282 differential(source, seed + 100, 60, 5, 2);
1283 }
1284 // Seeded identities: the writes pick nodes in identity order.
1285 let created = seeded(7, || {
1286 crate::create_section("model.one", "First", "Author").unwrap()
1287 });
1288 differential(&created, 7 + shift, 60, 3, 1);
1289 }
1290
1291 #[test]
1292 fn wide_batches_regroup_and_alias_as_the_image_writer_does() {
1293 // These seeds make a seal alias read-only objects or group tables across writes
1294 // differently from the writes themselves, so the pages adopt the sealed form.
1295 let corpus = [
1296 include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one")
1297 .as_slice(),
1298 include_bytes!("../../../corpus/outline-edit/tree/before/notebook/synthetic.one"),
1299 include_bytes!("../../../corpus/paragraph-edit/before/notebook/synthetic.one"),
1300 ];
1301 for seed in crate::sweep::seeds(4..9, 5) {
1302 for source in corpus {
1303 differential(source, seed, 30, 12, 3);
1304 }
1305 }
1306 }
1307
1308 #[test]
1309 fn a_long_chain_checkpoints_as_the_image_writer_does() {
1310 // Past 512 revisions of one space, so the chain checkpoints once.
1311 let checkpoints = differential(
1312 include_bytes!(
1313 "../../../corpus/native/20260905-05/snapshots/02-text/notebook/synthetic.one"
1314 ),
1315 11,
1316 560,
1317 2,
1318 1,
1319 );
1320 assert_eq!(checkpoints, 1);
1321 }
1322
1323 #[test]
1324 fn a_created_space_seals_as_the_image_writer_creates_it() {
1325 let source = &crate::create_section("model.one", "First", "Author").unwrap();
1326 let store = Store::parse(source).unwrap();
1327 let index = RevisionIndex::parse(&store).unwrap();
1328 // A page without history, whose objects name no other space or context.
1329 let (copied, revision, reachable) = page_spaces(source)
1330 .into_iter()
1331 .map(|space| {
1332 let revision = index.resolve_active(space).unwrap();
1333 let reachable = revision.reachable().unwrap();
1334 (space, revision, reachable)
1335 })
1336 .find(|(_, revision, reachable)| {
1337 reachable.iter().all(|id| {
1338 let references = revision.objects[id].references().unwrap();
1339 references.object_spaces.is_empty() && references.contexts.is_empty()
1340 })
1341 })
1342 .unwrap();
1343 let objects: Changes = reachable
1344 .iter()
1345 .map(|id| (*id, replacement(*id, &revision.objects[id]).unwrap()))
1346 .collect();
1347 let space = ExGuid {
1348 guid: [7; 16],
1349 n: 0,
1350 };
1351 let expected = seeded(1, || {
1352 crate::write::write_revisions(source, |_| {
1353 Ok(BTreeMap::from([(
1354 space,
1355 crate::write::RevisionEdit::Create {
1356 roots: revision.roots.clone(),
1357 objects: objects.clone(),
1358 },
1359 )]))
1360 })
1361 .unwrap()
1362 });
1363 let arena = Arena::default();
1364 let mut section = Section::open(&arena, source.to_vec()).unwrap();
1365 section
1366 .create_space(space, revision.roots.clone(), objects)
1367 .unwrap();
1368 assert_eq!(section.page(space).unwrap(), section.page(copied).unwrap());
1369 seeded(1, || section.seal().unwrap()).unwrap();
1370 assert!(section.image() == expected);
1371 // The space's next revision depends on the one creating it.
1372 let mut writes = Writes(5);
1373 let mut image = expected;
1374 let mut written = 0;
1375 for batch in 0..6 {
1376 let Ok(changes) = writes.next(section.active(space).unwrap()) else {
1377 continue;
1378 };
1379 written += 1;
1380 section.apply_changes(space, &[], changes.clone()).unwrap();
1381 let guids = (batch + 2) << 32;
1382 let store = Store::parse(&image).unwrap();
1383 let index = RevisionIndex::parse(&store).unwrap();
1384 let bump = Bump::new();
1385 // The root lists no page in this space, so no document reaches it.
1386 let rid = index.active(space).unwrap();
1387 let live = LiveRevision::new(
1388 index.resolve(space, rid).unwrap(),
1389 chain_depth(&index, space, rid),
1390 )
1391 .unwrap();
1392 let store = &store;
1393 let files: Files<'_> = std::rc::Rc::new(|guid| store.file_data(guid));
1394 let mut legacy = ActivePage::open(space, live, FileType::Section, files).unwrap();
1395 legacy.write(&bump, &[], changes).unwrap();
1396 let expected = seeded(guids, || {
1397 let revision = &legacy.live.revision;
1398 squashed(&index, &[(space, revision)], &[])
1399 });
1400 drop(legacy);
1401 drop(index);
1402 image = expected;
1403 seeded(guids, || section.seal().unwrap());
1404 assert!(section.image() == image, "batch {batch}");
1405 }
1406 assert!(written > 2);
1407 let reopened = Section::open(&arena, image).unwrap();
1408 assert_eq!(reopened.page(space).unwrap(), section.page(space).unwrap());
1409 }
1410
1411 #[test]
1412 fn payloads_embed_once_as_the_image_writer_embeds_them() {
1413 let source = include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one");
1414 let space = page_spaces(source)[0];
1415 let payload = ([9; 16], b"payload bytes".as_slice());
1416 let arena = Arena::default();
1417 let mut section = Section::open(&arena, source.to_vec()).unwrap();
1418 let mut image = source.to_vec();
1419 for seed in [1_u64 << 32, 2 << 32] {
1420 assert!(
1421 section
1422 .apply_changes(space, &[payload], Changes::new())
1423 .unwrap()
1424 );
1425 let store = Store::parse(&image).unwrap();
1426 let index = RevisionIndex::parse(&store).unwrap();
1427 let legacy = ActivePage::parse(&index, space).unwrap();
1428 let expected = seeded(seed, || {
1429 let revision = &legacy.live.revision;
1430 squashed(&index, &[(space, revision)], &[payload])
1431 });
1432 let transaction = seeded(seed, || section.seal().unwrap());
1433 assert_eq!(transaction.is_some(), expected != image);
1434 assert!(section.image() == expected);
1435 drop(legacy);
1436 drop(index);
1437 drop(store);
1438 image = expected;
1439 }
1440 let store = Store::parse(&image).unwrap();
1441 assert_eq!(store.file_data(payload.0).unwrap(), payload.1);
1442 }
1443
1444 #[test]
1445 fn a_seal_checks_the_bytes_it_appends() {
1446 fn utf16(text: &str) -> Vec<u8> {
1447 text.encode_utf16().flat_map(u16::to_le_bytes).collect()
1448 }
1449 fn object_bytes(transaction: &mut Transaction) {
1450 let text = utf16("Tampered text");
1451 let at = transaction
1452 .append
1453 .windows(text.len())
1454 .position(|w| w == text)
1455 .unwrap();
1456 transaction.append[at] ^= 1;
1457 }
1458 fn link(transaction: &mut Transaction) {
1459 let (_, bytes) = transaction
1460 .patches
1461 .iter_mut()
1462 .find(|(_, b)| b.len() == 12)
1463 .unwrap();
1464 bytes[0] ^= 8;
1465 }
1466 fn sentinel(transaction: &mut Transaction) {
1467 let (_, bytes) = transaction.patches.last_mut().unwrap();
1468 *bytes.last_mut().unwrap() ^= 1;
1469 }
1470 fn header(transaction: &mut Transaction) {
1471 transaction.header[96] ^= 2;
1472 }
1473 let source = include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one");
1474 let space = page_spaces(source)[0];
1475 let seal = |tamper: Option<fn(&mut Transaction)>| {
1476 let arena = Arena::default();
1477 let mut section = Section::open(&arena, source.to_vec()).unwrap();
1478 let page = section.active(space).unwrap();
1479 let changes = page
1480 .view
1481 .nodes
1482 .iter()
1483 .filter(|(id, node)| {
1484 matches!(node.kind, crate::document::Kind::Outline { .. })
1485 && page.parents.contains_key(id)
1486 })
1487 .find_map(|(outline, _)| {
1488 let paragraph = crate::page::text::new_id().ok()?;
1489 let text = crate::page::text::new_id().ok()?;
1490 Insertion::paragraph(*outline, None, "Tampered text", "Author")
1491 .and_then(|insertion| {
1492 insertion.changes_as(page, paragraph, paragraph, text)
1493 })
1494 .ok()
1495 })
1496 .unwrap();
1497 section.apply_changes(space, &[], changes).unwrap();
1498 TAMPER.set(tamper);
1499 let sealed = section.seal();
1500 TAMPER.set(None);
1501 let error = sealed.as_ref().err().map(|error| error.message);
1502 assert_eq!(section.seal().is_err(), error.is_some());
1503 error
1504 };
1505 assert_eq!(seal(None), None);
1506 for tamper in [object_bytes, link, sentinel, header] {
1507 assert!(seal(Some(tamper)).is_some());
1508 }
1509 }
1510
1511 #[test]
1512 fn a_replayed_transaction_leaves_the_sealing_section() {
1513 let source = include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one");
1514 let space = page_spaces(source)[0];
1515 let arena = Arena::default();
1516 let mut sealing = Section::open(&arena, source.to_vec()).unwrap();
1517 let mut replaying = Section::open(&arena, source.to_vec()).unwrap();
1518 let mut writes = Writes(3);
1519 for _ in 0..8 {
1520 for _ in 0..3 {
1521 if let Ok(changes) = writes.next(sealing.active(space).unwrap()) {
1522 sealing.apply_changes(space, &[], changes).unwrap();
1523 }
1524 }
1525 let Some(transaction) = sealing.seal().unwrap() else {
1526 continue;
1527 };
1528 let queued: Transaction =
1529 serde_json::from_str(&serde_json::to_string(&transaction).unwrap()).unwrap();
1530 assert_eq!(queued, transaction);
1531 replaying.replay(&queued).unwrap();
1532 assert_eq!(replaying.stamp(), sealing.stamp());
1533 assert!(replaying.image() == sealing.image());
1534 assert_eq!(replaying.page(space).unwrap(), sealing.page(space).unwrap());
1535 }
1536 }
1537}
1538
1539#[cfg(test)]
1540mod probe {
1541 use super::*;
1542 use crate::{document::Kind, edit::text_changes};
1543 use std::time::{Duration, Instant};
1544
1545 fn median(mut samples: Vec<Duration>) -> Duration {
1546 samples.sort();
1547 samples[samples.len() / 2]
1548 }
1549
1550 /// The fullest page space of `image` and a rich-text object in its middle.
1551 fn target(image: &[u8]) -> (ExGuid, ExGuid, usize) {
1552 let store = Store::parse(image).unwrap();
1553 let index = RevisionIndex::parse(&store).unwrap();
1554 let document = crate::document::Document::parse(&index).unwrap();
1555 let texts = |space: ExGuid| -> Vec<ExGuid> {
1556 document
1557 .active(space)
1558 .unwrap()
1559 .nodes
1560 .iter()
1561 .filter(|(_, node)| {
1562 matches!(
1563 node.kind,
1564 Kind::RichText {
1565 boilerplate: false,
1566 ..
1567 }
1568 )
1569 })
1570 .map(|(id, _)| *id)
1571 .collect()
1572 };
1573 let (space, _) = document
1574 .pages()
1575 .unwrap()
1576 .into_iter()
1577 .max_by_key(|(space, _)| texts(*space).len())
1578 .unwrap();
1579 let texts = texts(space);
1580 (space, texts[texts.len() / 2], texts.len())
1581 }
1582
1583 /// One-character edits of a large page through a `Section`: `SECTION_PROBE=path cargo test --release -p onestore --lib probe -- --ignored --nocapture`.
1584 #[test]
1585 #[ignore]
1586 fn keystrokes() {
1587 let path = std::env::var("SECTION_PROBE").unwrap_or("/tmp/probe3000.one".into());
1588 let image = std::fs::read(&path).unwrap();
1589 let (space, text, count) = target(&image);
1590 println!(
1591 "{path}: {} bytes, {count} text objects on the page",
1592 image.len()
1593 );
1594
1595 let arena = Arena::default();
1596 let start = Instant::now();
1597 let mut section = Section::open(&arena, image.clone()).unwrap();
1598 println!("Section::open: {:?}", start.elapsed());
1599 let start = Instant::now();
1600 section.active(space).unwrap();
1601 println!("first edit opens the page: {:?}", start.elapsed());
1602 let (mut applies, mut seals, mut sizes) = (Vec::new(), Vec::new(), Vec::new());
1603 for keystroke in 0..200u32 {
1604 let start = Instant::now();
1605 let changes = text_changes(
1606 section.active(space).unwrap(),
1607 text,
1608 keystroke..keystroke,
1609 "x",
1610 )
1611 .unwrap();
1612 section.apply_changes(space, &[], changes).unwrap();
1613 applies.push(start.elapsed());
1614 let start = Instant::now();
1615 let transaction = section.seal().unwrap().unwrap();
1616 seals.push(start.elapsed());
1617 sizes.push((
1618 transaction.append.len(),
1619 transaction
1620 .patches
1621 .iter()
1622 .map(|(_, bytes)| bytes.len())
1623 .sum::<usize>(),
1624 ));
1625 }
1626 println!(
1627 "first seal: appended {} bytes, patched {} bytes",
1628 sizes[0].0, sizes[0].1
1629 );
1630 sizes.sort();
1631 println!(
1632 "Section, one keystroke: apply {:?}, seal {:?}; appended {:?} bytes, patched {:?} bytes (median)",
1633 median(applies),
1634 median(seals),
1635 sizes[sizes.len() / 2].0,
1636 sizes[sizes.len() / 2].1,
1637 );
1638 let start = Instant::now();
1639 let reopened = Section::open(&arena, section.image()).unwrap();
1640 println!("reopen after 200 seals: {:?}", start.elapsed());
1641 assert_eq!(reopened.page(space).unwrap(), section.page(space).unwrap());
1642 }
1643}