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