authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 22:14:25-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-27 04:23:49-07:00
loge0b4334a77134228b5f773ce7cb9227fc2dd027d
treef4ba24b7af1affa009b2dc066a076ae37687589d
parent710848e64a97302d4f0df26f496f6d1d3de7e809
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

perf: page writes in one revision, and saves off the main thread

The page writer keeps its step-by-step object placement but simulates each step's write and read-back in memory, building one revision per write instead of one per step; output is byte-identical (505 corpus sections, 8052 page edits, generated pages up to 2000 paragraphs). A 250-paragraph import into a 4.6 MB section drops from 22 s to 0.15 s and 2000 paragraphs from 38 minutes to 0.7 s. Section::queue_save saves on a thread, chaining consecutive saves of a page, and saved() reports outcomes; Snowbound saves through it, so an edit no longer re-reads and rewrites the section on the main thread. Assisted-by: claude-opus-5.5 Assisted-by: claude-opus-5

20 files changed, 2297 insertions(+), 1195 deletions(-)

crates/notebook/README.md+8-3
......@@ -67,10 +67,15 @@ author)` queues the edited model: `Save::Queued(id)` is durable locally,
6767`Save::Unchanged` means the model equals the stored page, and `Save::Stale`
6868means the stored page no longer matches `before` because the section changed
6969underneath the editor, so the page must be reloaded before saving again.
70`queue_save(space, before, after, author)` does the same on the section's save
71thread and returns at once, so an editor's frame never waits on the write;
72`saved()` drains the outcomes. A queued save whose `before` is the previous
73one's `after` continues it, needing no reload in between, and a run of such
74saves waiting together is written once.
7075`events()` drains what the synchronization thread reported since the last
71poll: refreshes, attempt outcomes and unreachable files; `notify` runs on that
72thread whenever an event is available so the application can wake its event
73loop. `close()` stops publication.
76poll: refreshes, attempt outcomes and unreachable files; `notify` runs
77whenever an event or save outcome is available so the application can wake its
78event loop. `close()` finishes queued saves and stops publication.
7479
7580`Event::Unreachable` retains an `io::Error`: callers can distinguish permission
7681denial, missing targets, timeouts, and connection failures through `kind()` without
crates/notebook/examples/save_probe.rs created+108
......@@ -0,0 +1,108 @@
1//! Times importing a generated page into a copy of a section, then saving a one-character
2//! edit of it: `save_probe SECTION PARAGRAPHS`.
3
4use notebook::session::Section;
5use onestore::{
6 document::{Format, Layout},
7 page::{
8 Outline, Page, PageObject, PageParagraph, Paragraph, ParagraphContent, TextObject,
9 text::{Edit, new_id},
10 },
11};
12use std::time::{Duration, Instant};
13
14fn main() -> Result<(), Box<dyn std::error::Error>> {
15 let mut args = std::env::args().skip(1);
16 let (Some(source), Some(count)) = (args.next(), args.next()) else {
17 return Err("usage: save_probe SECTION PARAGRAPHS".into());
18 };
19 let count: usize = count.parse()?;
20 let directory = tempfile::tempdir()?;
21 let file = directory.path().join("probe.one");
22 std::fs::write(&file, std::fs::read(source)?)?;
23 let section = Section::open(&file, directory.path().join("cache"), || {})?;
24 let words = [
25 "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog",
26 ];
27 let paragraph = |i: usize| -> Result<PageParagraph, Box<dyn std::error::Error>> {
28 let text: Vec<&str> = (0..1 + i % 17).map(|j| words[(i * 7 + j) % 8]).collect();
29 Ok(PageParagraph {
30 id: new_id()?,
31 parent: None,
32 level: 1,
33 style: None,
34 format: Format::default(),
35 content: ParagraphContent::Text(TextObject {
36 id: new_id()?,
37 date_field: None,
38 text: Paragraph::new(text.join(" "), Format::default()),
39 tags: Vec::new(),
40 }),
41 lists: Vec::new(),
42 tags: Vec::new(),
43 media: Default::default(),
44 collapsed: false,
45 })
46 };
47 let imported = Page {
48 title: "Probe".into(),
49 identity: None,
50 created: None,
51 margin_origin: [0.0; 2],
52 objects: vec![PageObject::Outline(Outline {
53 id: new_id()?,
54 title: false,
55 min_width: None,
56 layout: Layout {
57 x: Some(36.0),
58 y: Some(86.4),
59 ..Layout::default()
60 },
61 indents: Vec::new(),
62 paragraphs: (0..count).map(paragraph).collect::<Result<_, _>>()?,
63 unsupported: Vec::new(),
64 })],
65 definitions: Default::default(),
66 };
67 let start = Instant::now();
68 let space = section.import_page(&imported, "Probe")?;
69 println!("import {count}: {:?}", start.elapsed());
70 let before = section.page(space)?;
71 let edit = |page: &Page, word: &str| -> Result<Page, Box<dyn std::error::Error>> {
72 let mut after = page.clone();
73 let text = after
74 .objects
75 .iter_mut()
76 .find_map(|object| match object {
77 PageObject::Outline(outline) if outline.paragraphs.len() == count => {
78 outline.paragraphs[count / 2].text_mut()
79 }
80 _ => None,
81 })
82 .ok_or("the imported outline is missing")?;
83 let format = text.text.format_at(0)?.clone();
84 text.text.apply(Edit {
85 range: 0..0,
86 replacement: Paragraph::new(word.into(), format),
87 })?;
88 Ok(after)
89 };
90 let after = edit(&before, "x")?;
91 let start = Instant::now();
92 section.save(space, &before, &after, "Probe")?;
93 println!("save: {:?}", start.elapsed());
94 let next = edit(&after, "y")?;
95 let start = Instant::now();
96 section.queue_save(space, after, next, "Probe")?;
97 println!("queue_save: {:?}", start.elapsed());
98 let deadline = start + Duration::from_secs(600);
99 while Instant::now() < deadline {
100 if let Some((_, save)) = section.saved().pop() {
101 println!("queued save done: {:?} {save:?}", start.elapsed());
102 break;
103 }
104 std::thread::sleep(Duration::from_millis(1));
105 }
106 section.close()?;
107 Ok(())
108}
crates/notebook/src/lib.rs+17-4
......@@ -274,9 +274,20 @@ impl Replica {
274274 after: &Page,
275275 author: &str,
276276 ) -> Result<Option<u64>> {
277 Ok(self.save_image(source, space, after, author)?.0)
278 }
279
280 /// `save`, with the working image it leaves.
281 pub(crate) fn save_image(
282 &self,
283 source: &[u8],
284 space: ExGuid,
285 after: &Page,
286 author: &str,
287 ) -> Result<(Option<u64>, Vec<u8>)> {
277288 let prepared = PreparedEdit::page(source, space, after, author)?;
278289 if prepared.as_bytes() == source {
279 return Ok(None);
290 return Ok((None, source.to_vec()));
280291 }
281292 let before = page_of(source, space)?.ok_or_else(|| {
282293 io::Error::new(
......@@ -332,11 +343,12 @@ impl Replica {
332343 transaction.commit()?;
333344 drop(connection);
334345 self.wake_sync();
335 return Ok(Some(u64::try_from(id).map_err(io::Error::other)?));
346 let id = u64::try_from(id).map_err(io::Error::other)?;
347 return Ok((Some(id), prepared.as_bytes().to_vec()));
336348 }
337349 }
338350 }
339 self.record(
351 let id = self.record(
340352 source,
341353 space,
342354 Operation::Page(PageIntent {
......@@ -345,7 +357,8 @@ impl Replica {
345357 author: author.to_owned(),
346358 }),
347359 &prepared,
348 )
360 )?;
361 Ok((id, prepared.as_bytes().to_vec()))
349362 }
350363
351364 /// Queues a new page and its section entry with stable identities for dependent edits.
crates/notebook/src/session.rs+158-19
......@@ -9,14 +9,15 @@ use onestore::{
99 page::Page,
1010};
1111use std::{
12 collections::BTreeMap,
12 collections::{BTreeMap, VecDeque},
1313 io,
1414 io::Write,
1515 path::{Path, PathBuf},
1616 sync::{
17 Arc,
18 mpsc::{self, Receiver},
17 Arc, Mutex,
18 mpsc::{self, Receiver, Sender},
1919 },
20 thread::{self, JoinHandle},
2021 time::Duration,
2122};
2223
......@@ -751,12 +752,23 @@ pub struct QueuedEdit {
751752 pub status: EditStatus,
752753}
753754
754/// A section file with its replica and background publication.
755/// A section file with its replica, background saves and background publication.
755756pub struct Section {
756757 file: PathBuf,
757758 replica: Arc<Replica>,
758759 worker: Option<SyncWorker>,
759760 events: Receiver<Event>,
761 saves: Option<Sender<PageSave>>,
762 saved: Receiver<(ExGuid, std::result::Result<Save, String>)>,
763 saver: Option<JoinHandle<()>>,
764}
765
766/// An edited page `Section::queue_save` hands to the save thread.
767struct PageSave {
768 space: ExGuid,
769 before: Page,
770 after: Page,
771 author: String,
760772}
761773
762774impl Section {
......@@ -830,6 +842,26 @@ impl Section {
830842 ) -> Result<Self> {
831843 let replica = Arc::new(replica);
832844 let (sender, events) = mpsc::channel();
845 let (saved_sender, saved) = mpsc::channel();
846 let notify = Arc::new(Mutex::new(notify));
847 let signal = move || {
848 if let Ok(notify) = notify.lock() {
849 notify();
850 }
851 };
852 let (saves, requests) = mpsc::channel();
853 let saver = {
854 let (replica, signal) = (Arc::clone(&replica), signal.clone());
855 thread::Builder::new()
856 .name("onestore-save".into())
857 .spawn(move || {
858 save_pages(&replica, &requests, |outcome| {
859 if saved_sender.send(outcome).is_ok() {
860 signal();
861 }
862 });
863 })?
864 };
833865 let worker = replica.start_sync(Duration::from_secs(2), connect, move |result| {
834866 let event = match result {
835867 Ok(None) => Event::Refreshed,
......@@ -847,7 +879,7 @@ impl Section {
847879 Err(error) => Event::Failed(error.to_string()),
848880 };
849881 if sender.send(event).is_ok() {
850 notify();
882 signal();
851883 }
852884 })?;
853885 Ok(Self {
......@@ -855,6 +887,9 @@ impl Section {
855887 replica,
856888 worker: Some(worker),
857889 events,
890 saves: Some(saves),
891 saved,
892 saver: Some(saver),
858893 })
859894 }
860895
......@@ -951,20 +986,23 @@ impl Section {
951986 /// Saves an edited page. `before` is the model the edit started from; a stored page
952987 /// that differs from it means the section changed underneath the editor.
953988 pub fn save(&self, space: ExGuid, before: &Page, after: &Page, author: &str) -> Result<Save> {
954 loop {
955 let snapshot = self.replica.snapshot()?;
956 let store = Store::parse(&snapshot)?;
957 let index = RevisionIndex::parse(&store)?;
958 if Page::from_space(&Document::parse(&index)?, space)? != *before {
959 return Ok(Save::Stale);
960 }
961 match self.replica.save(&snapshot, space, after, author) {
962 Ok(Some(id)) => return Ok(Save::Queued(id)),
963 Ok(None) => return Ok(Save::Unchanged),
964 Err(Error::Io(error)) if error.kind() == io::ErrorKind::ResourceBusy => {}
965 Err(error) => return Err(error),
966 }
967 }
989 Ok(save(&self.replica, space, before, None, after, author)?.0)
990 }
991
992 /// `save` on the section's save thread, returning at once; `saved` reports the outcome.
993 /// Saves queue in order, and one whose `before` is the previous save's `after` continues
994 /// it: pass each save's `after` as the next one's `before`.
995 pub fn queue_save(&self, space: ExGuid, before: Page, after: Page, author: &str) -> Result<()> {
996 let save = PageSave {
997 space,
998 before,
999 after,
1000 author: author.to_owned(),
1001 };
1002 self.saves
1003 .as_ref()
1004 .and_then(|saves| saves.send(save).ok())
1005 .ok_or_else(|| io::Error::other("The save thread stopped").into())
9681006 }
9691007
9701008 pub fn status(&self, id: u64) -> Result<Option<EditStatus>> {
......@@ -1043,6 +1081,12 @@ impl Section {
10431081 self.events.try_iter().collect()
10441082 }
10451083
1084 /// Outcomes of queued saves since the last poll, oldest first; `notify` runs for each.
1085 /// Consecutive saves continuing one another finish as one, reported once.
1086 pub fn saved(&self) -> Vec<(ExGuid, std::result::Result<Save, String>)> {
1087 self.saved.try_iter().collect()
1088 }
1089
10461090 /// Requests a synchronization attempt now.
10471091 pub fn wake(&self) {
10481092 if let Some(worker) = &self.worker {
......@@ -1058,6 +1102,12 @@ impl Section {
10581102 /// Dropping instead requests cancellation without waiting; the worker retains
10591103 /// cache ownership until that operation finishes. Remote calls must be bounded.
10601104 pub fn close(mut self) -> Result<()> {
1105 drop(self.saves.take());
1106 if let Some(saver) = self.saver.take() {
1107 saver
1108 .join()
1109 .map_err(|_| io::Error::other("The save thread panicked"))?;
1110 }
10611111 match self.worker.take() {
10621112 Some(worker) => worker.stop(),
10631113 None => Ok(()),
......@@ -1065,6 +1115,95 @@ impl Section {
10651115 }
10661116}
10671117
1118/// Saves `after` unless the stored page no longer matches `before`, returning the working
1119/// image the save leaves. `known` is the image a previous save of `before` left: while the
1120/// working image equals it the page is current unread, and otherwise the page `known`
1121/// stores stands in for `before`.
1122fn save(
1123 replica: &Replica,
1124 space: ExGuid,
1125 before: &Page,
1126 known: Option<&[u8]>,
1127 after: &Page,
1128 author: &str,
1129) -> Result<(Save, Vec<u8>)> {
1130 let stored = |image: &[u8]| -> Result<Page> {
1131 let store = Store::parse(image)?;
1132 Ok(Page::from_space(
1133 &Document::parse(&RevisionIndex::parse(&store)?)?,
1134 space,
1135 )?)
1136 };
1137 loop {
1138 let snapshot = replica.snapshot()?;
1139 if known != Some(snapshot.as_slice()) {
1140 let current = stored(&snapshot)?;
1141 let expected = match known {
1142 Some(image) => current == stored(image)?,
1143 None => current == *before,
1144 };
1145 if !expected {
1146 return Ok((Save::Stale, snapshot));
1147 }
1148 }
1149 match replica.save_image(&snapshot, space, after, author) {
1150 Ok((Some(id), image)) => return Ok((Save::Queued(id), image)),
1151 Ok((None, image)) => return Ok((Save::Unchanged, image)),
1152 Err(Error::Io(error)) if error.kind() == io::ErrorKind::ResourceBusy => {}
1153 Err(error) => return Err(error),
1154 }
1155 }
1156}
1157
1158/// Runs queued saves until the section drops its sender, folding each run of saves that
1159/// continue one another into one.
1160fn save_pages(
1161 replica: &Replica,
1162 requests: &Receiver<PageSave>,
1163 report: impl Fn((ExGuid, std::result::Result<Save, String>)),
1164) {
1165 let mut queue = VecDeque::new();
1166 // The last save's page and the working image it left.
1167 let mut last: Option<(ExGuid, Page, Vec<u8>)> = None;
1168 loop {
1169 if queue.is_empty() {
1170 match requests.recv() {
1171 Ok(request) => queue.push_back(request),
1172 Err(_) => return,
1173 }
1174 }
1175 queue.extend(requests.try_iter());
1176 let mut request = queue.pop_front().unwrap();
1177 while let Some(next) = queue.front()
1178 && (next.space, &next.before, &next.author)
1179 == (request.space, &request.after, &request.author)
1180 {
1181 request.after = queue.pop_front().unwrap().after;
1182 }
1183 let known = last
1184 .take()
1185 .filter(|(space, after, _)| *space == request.space && *after == request.before);
1186 let result = save(
1187 replica,
1188 request.space,
1189 &request.before,
1190 known.as_ref().map(|(_, _, image)| image.as_slice()),
1191 &request.after,
1192 &request.author,
1193 );
1194 let save = match result {
1195 Ok((save, image)) => {
1196 if save != Save::Stale {
1197 last = Some((request.space, request.after, image));
1198 }
1199 Ok(save)
1200 }
1201 Err(error) => Err(error.to_string()),
1202 };
1203 report((request.space, save));
1204 }
1205}
1206
10681207/// The section file itself as the publication target, under OneNote-compatible exclusion.
10691208struct FileRemote(PathBuf);
10701209
crates/notebook/tests/session.rs+80
......@@ -232,6 +232,86 @@ fn an_external_change_refreshes_the_page_and_stales_a_save_from_the_old_model()
232232 section.close().unwrap();
233233}
234234
235#[test]
236fn queued_saves_continue_each_other_detect_stale_pages_and_finish_before_close() {
237 let directory = tempfile::tempdir().unwrap();
238 let file = directory.path().join("notes.one");
239 let cache = directory.path().join("cache");
240 std::fs::write(
241 &file,
242 onestore::create_section("notes.one", "Original", "Author").unwrap(),
243 )
244 .unwrap();
245 let (section, _) = open(&file, &cache);
246 let space = section.pages().unwrap()[0].0;
247 let mut before = section.page(space).unwrap();
248 for word in ["one ", "two ", "three ", "four ", "five "] {
249 let after = edited(&before, word);
250 section
251 .queue_save(space, before, after.clone(), "Editor")
252 .unwrap();
253 before = after;
254 }
255 let deadline = Instant::now() + Duration::from_secs(20);
256 loop {
257 let mut stored = section.page(space).unwrap();
258 stored.title = before.title.clone();
259 if stored == before {
260 break;
261 }
262 assert!(Instant::now() < deadline, "the saves did not arrive");
263 for (_, save) in section.saved() {
264 assert!(matches!(save, Ok(Save::Queued(_))), "{save:?}");
265 }
266 std::thread::sleep(Duration::from_millis(20));
267 }
268 while !section.pending().unwrap().is_empty() {
269 assert!(Instant::now() < deadline, "the saves were not published");
270 std::thread::sleep(Duration::from_millis(20));
271 }
272 let native = edited(&section.page(space).unwrap(), "Native ");
273 let deadline = Instant::now() + Duration::from_secs(20);
274 loop {
275 let bytes = onestore::read_file(&file).unwrap();
276 match PreparedEdit::page(&bytes, space, &native, "Native")
277 .unwrap()
278 .commit_file(&file)
279 {
280 Ok(()) => break,
281 Err(error) if error.error.kind() == std::io::ErrorKind::WouldBlock => {
282 assert!(Instant::now() < deadline);
283 std::thread::sleep(Duration::from_millis(20));
284 }
285 Err(error) => panic!("{error:?}"),
286 }
287 }
288 section.wake();
289 while !section.page(space).unwrap().objects.eq(&native.objects) {
290 assert!(Instant::now() < deadline, "the refresh did not arrive");
291 wait(&section, |event| matches!(event, Event::Refreshed));
292 }
293 let stale = edited(&before, "Local ");
294 section.queue_save(space, before, stale, "Editor").unwrap();
295 // Outcomes of the earlier saves may still be arriving.
296 while !section
297 .saved()
298 .into_iter()
299 .any(|saved| saved == (space, Ok(Save::Stale)))
300 {
301 assert!(Instant::now() < deadline, "the stale save was not reported");
302 std::thread::sleep(Duration::from_millis(20));
303 }
304 let native = section.page(space).unwrap();
305 let after = edited(&native, "Local ");
306 section
307 .queue_save(space, native, after.clone(), "Editor")
308 .unwrap();
309 section.close().unwrap();
310 let (section, _) = open(&file, &cache);
311 assert_same(section.page(space).unwrap(), &after);
312 section.close().unwrap();
313}
314
235315#[test]
236316fn a_notebook_directory_lists_its_sections_and_opens_them() {
237317 let directory = tempfile::tempdir().unwrap();
crates/onestore/Cargo.toml+1
......@@ -9,6 +9,7 @@ protected = ["dep:aes", "dep:cbc", "dep:roxmltree", "dep:sha1", "dep:subtle", "d
99
1010[dependencies]
1111base64 = "0.22.1"
12bumpalo = "3.20.3"
1213serde_json = "1.0.151"
1314md5 = "0.8.1"
1415getrandom = "0.4.3"
crates/onestore/src/active.rs created+374
......@@ -0,0 +1,374 @@
1use crate::{
2 Error, ExGuid, Object, RevisionIndex, Store,
3 document::{Document, Element, Revision, Space},
4 write::{Commit, LiveRevision, PropertyObject, chain_depth, declared, differing},
5};
6use bumpalo::Bump;
7use std::collections::{BTreeMap, BTreeSet};
8
9type Result<T> = std::result::Result<T, Error>;
10
11/// Objects a writer stores in one revision of a page space.
12pub(crate) type Changes = BTreeMap<ExGuid, PropertyObject>;
13
14/// A page space's active revision with the views its writers read, kept as appending
15/// their revisions and parsing the result would leave it.
16pub(crate) struct ActivePage<'a> {
17 store: &'a Store<'a>,
18 pub pages: Vec<ExGuid>,
19 pub live: LiveRevision<'a>,
20 /// Elements of the reachable objects.
21 pub view: Revision<'a>,
22 /// `view.parents(&pages)`.
23 pub parents: BTreeMap<ExGuid, Vec<ExGuid>>,
24 /// Elements of `view` carrying the page-title flag.
25 titles: BTreeSet<ExGuid>,
26 /// Payloads embedded by revisions `write` applied, in order.
27 pub payloads: Vec<([u8; 16], &'a [u8])>,
28}
29
30impl<'a> ActivePage<'a> {
31 pub(crate) fn parse(index: &'a RevisionIndex<'a>, space: ExGuid) -> Result<Self> {
32 index.validate_current()?;
33 let mut document = Document::parse(index)?;
34 let pages = document.pages_in(space)?;
35 let view = document
36 .spaces
37 .remove(&space)
38 .and_then(Space::into_active)
39 .ok_or(Error {
40 offset: 0,
41 message: "The active page is unavailable",
42 })?;
43 let rid = index.active(space)?;
44 let live = LiveRevision::new(index.resolve(space, rid)?, chain_depth(index, space, rid))?;
45 let parents = view.parents(&pages)?;
46 let titles = view
47 .nodes
48 .iter()
49 .filter(|(_, node)| is_title(node))
50 .map(|(id, _)| *id)
51 .collect();
52 Ok(Self {
53 store: index.store,
54 pages,
55 live,
56 view,
57 parents,
58 titles,
59 payloads: Vec::new(),
60 })
61 }
62
63 /// The element of an object, reading payloads `write` embedded as well as stored ones.
64 pub(crate) fn element<'o>(&self, object: &Object<'o>) -> Result<Element<'o>>
65 where
66 'a: 'o,
67 {
68 Element::parse_with(object, self.store, &mut |guid| match self
69 .payloads
70 .iter()
71 .find(|(id, _)| *id == guid)
72 {
73 Some((_, payload)) => Ok(*payload),
74 None => self.store.file_data(guid),
75 })
76 }
77
78 /// `parents`, after checking that `object` and its ancestors are editable.
79 pub(crate) fn editable_parents(
80 &self,
81 object: ExGuid,
82 ) -> Result<&BTreeMap<ExGuid, Vec<ExGuid>>> {
83 crate::edit::check_editable(&self.view, &self.parents, &self.pages, object)?;
84 Ok(&self.parents)
85 }
86
87 /// `edit::page_title` of the view with `overlay` replacing or adding elements that
88 /// neither carry the title flag nor detach any element that does.
89 pub(crate) fn title(
90 &self,
91 overlay: &BTreeMap<ExGuid, Element<'_>>,
92 text_update: Option<(ExGuid, &str)>,
93 ) -> Result<Option<(ExGuid, bool, String)>> {
94 crate::edit::title_of(
95 |id| overlay.get(&id).or_else(|| self.view.nodes.get(&id)),
96 &self.parents,
97 self.titles.iter().copied(),
98 &self.pages,
99 text_update,
100 )
101 }
102
103 /// Stores a writer's objects and payloads as the next revision; false when it stores
104 /// nothing, as appending it would leave the image unchanged.
105 pub(crate) fn write(
106 &mut self,
107 arena: &'a Bump,
108 payloads: &[([u8; 16], &[u8])],
109 changes: Changes,
110 ) -> Result<bool> {
111 let changes = self.live.prepare(changes, true)?;
112 for (guid, payload) in payloads {
113 self.payloads.push((*guid, arena.alloc_slice_copy(payload)));
114 }
115 if changes.is_empty() {
116 return Ok(!payloads.is_empty());
117 }
118 let mut objects = Vec::new();
119 for (id, change) in changes {
120 let bytes = arena.alloc_slice_copy(&change.bytes);
121 objects.push((id, declared(change.jcid, bytes, change.global_ids)?));
122 }
123 let replaced: BTreeSet<ExGuid> = objects.iter().map(|(id, _)| *id).collect();
124 let Commit {
125 checkpoint,
126 changed,
127 touched,
128 } = self.live.commit(objects)?;
129 if checkpoint {
130 let all: Vec<ExGuid> = self.live.revision.objects.keys().copied().collect();
131 self.live.settle(all)?;
132 } else {
133 self.live.settle(changed)?;
134 }
135 self.refresh(&replaced, touched)?;
136 Ok(true)
137 }
138
139 /// Brings the view, parents and titles up to date with the revision for the objects a
140 /// commit touched, which include those it replaced.
141 fn refresh(&mut self, replaced: &BTreeSet<ExGuid>, touched: BTreeSet<ExGuid>) -> Result<()> {
142 let mut attach = Vec::new();
143 let mut detach = Vec::new();
144 let mut gone = BTreeMap::new();
145 for id in touched {
146 let reachable = self.live.is_reachable(id);
147 let viewed = self.view.nodes.contains_key(&id);
148 if reachable == viewed && !(reachable && replaced.contains(&id)) {
149 continue;
150 }
151 let fresh = if reachable {
152 Some(self.element(&self.live.revision.objects[&id])?)
153 } else {
154 None
155 };
156 let old = self.view.nodes.remove(&id);
157 // An element leaving the view detaches with its last parent.
158 if let Some(node) = &fresh
159 && (self.parents.contains_key(&id) || self.pages.contains(&id))
160 {
161 let before: Vec<ExGuid> = old.iter().flat_map(edges).collect();
162 let after: Vec<ExGuid> = edges(node).collect();
163 let (removed, added) = differing(&before, &after);
164 detach.extend(removed.iter().map(|child| (id, *child)));
165 attach.extend(added.iter().map(|child| (id, *child)));
166 }
167 self.titles.remove(&id);
168 if let Some(node) = fresh {
169 if is_title(&node) {
170 self.titles.insert(id);
171 }
172 self.view.nodes.insert(id, node);
173 } else if let Some(node) = old {
174 gone.insert(id, node);
175 }
176 }
177 let node = |nodes: &BTreeMap<ExGuid, Element<'a>>, id: ExGuid| {
178 nodes
179 .get(&id)
180 .or_else(|| gone.get(&id))
181 .map(|node| edges(node).collect::<Vec<_>>())
182 .ok_or(Error {
183 offset: 0,
184 message: "Page content is unavailable",
185 })
186 };
187 // Attaching before detaching keeps a moved subtree from being walked twice.
188 while let Some((parent, child)) = attach.pop() {
189 let parents = self.parents.entry(child).or_default();
190 parents.push(parent);
191 if parents.len() == 1 && !self.pages.contains(&child) {
192 attach.extend(
193 node(&self.view.nodes, child)?
194 .into_iter()
195 .map(|grandchild| (child, grandchild)),
196 );
197 }
198 }
199 while let Some((parent, child)) = detach.pop() {
200 let parents = self.parents.get_mut(&child).unwrap();
201 let at = parents.iter().position(|id| *id == parent).unwrap();
202 parents.remove(at);
203 if parents.is_empty() {
204 self.parents.remove(&child);
205 if !self.pages.contains(&child) {
206 detach.extend(
207 node(&self.view.nodes, child)?
208 .into_iter()
209 .map(|grandchild| (child, grandchild)),
210 );
211 }
212 }
213 }
214 Ok(())
215 }
216}
217
218fn edges<'n>(node: &'n Element<'_>) -> impl Iterator<Item = ExGuid> + 'n {
219 node.children
220 .iter()
221 .chain(&node.content)
222 .chain(&node.structure)
223 .copied()
224}
225
226fn is_title(node: &Element<'_>) -> bool {
227 node.extra
228 .first()
229 .is_some_and(|fields| fields.iter().any(|field| field.id == 0x88001cb4))
230}
231
232/// Writes one revision of a page space: `changes` computes its objects from the active page.
233pub(crate) fn write(
234 source: &[u8],
235 space: ExGuid,
236 changes: impl FnOnce(&ActivePage<'_>) -> Result<Changes>,
237) -> Result<Vec<u8>> {
238 let store = Store::parse(source)?;
239 let index = RevisionIndex::parse(&store)?;
240 let changes = changes(&ActivePage::parse(&index, space)?)?;
241 if changes.is_empty() {
242 return Ok(source.to_vec());
243 }
244 crate::write::write_revision_on(&index, space, |_| Ok(changes))
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::{Insertion, ParagraphSplit, TextAttribute, TreeEdit, document::Kind};
251
252 /// `active` equals the page `image` stores, views included.
253 fn assert_stores(active: &ActivePage<'_>, image: &[u8], space: ExGuid) {
254 let store = Store::parse(image).unwrap();
255 let index = RevisionIndex::parse(&store).unwrap();
256 let read = ActivePage::parse(&index, space).unwrap();
257 assert_eq!(read.live.revision.roots, active.live.revision.roots);
258 let objects = |page: &ActivePage<'_>| -> Vec<String> {
259 page.live
260 .revision
261 .objects
262 .iter()
263 .map(|(id, object)| format!("{id} {object:?}"))
264 .collect()
265 };
266 assert_eq!(objects(&read), objects(active));
267 assert_eq!(format!("{:?}", read.view), format!("{:?}", active.view));
268 let parents = |page: &ActivePage<'_>| -> Vec<(ExGuid, Vec<ExGuid>)> {
269 page.parents
270 .iter()
271 .map(|(child, parents)| {
272 let mut parents = parents.clone();
273 parents.sort();
274 (*child, parents)
275 })
276 .collect()
277 };
278 assert_eq!(parents(&read), parents(active));
279 assert_eq!(read.titles, active.titles);
280 }
281
282 /// Applies `steps` random writes to the first page of `source` both in memory and by
283 /// appending each revision, comparing the two as it goes.
284 fn check_writes(source: &[u8], steps: usize) {
285 let mut image = source.to_vec();
286 let store = Store::parse(source).unwrap();
287 let index = RevisionIndex::parse(&store).unwrap();
288 let (space, _) = Document::parse(&index).unwrap().pages().unwrap()[0];
289 let arena = Bump::new();
290 let mut active = ActivePage::parse(&index, space).unwrap();
291 let mut seed = 7_u64;
292 let mut pick = |count: usize| {
293 seed = seed
294 .wrapping_mul(6364136223846793005)
295 .wrapping_add(1442695040888963407);
296 (seed >> 33) as usize % count.max(1)
297 };
298 for step in 0..steps {
299 let nodes = &active.view.nodes;
300 let listed = |kind: fn(&Kind<'_>) -> bool| -> Vec<ExGuid> {
301 nodes
302 .iter()
303 .filter(|(id, node)| kind(&node.kind) && active.parents.contains_key(id))
304 .map(|(id, _)| *id)
305 .collect()
306 };
307 let paragraphs = listed(|kind| matches!(kind, Kind::Paragraph { .. }));
308 let containers = listed(|kind| {
309 matches!(
310 kind,
311 Kind::Paragraph { .. } | Kind::Outline { .. } | Kind::Cell { .. }
312 )
313 });
314 let texts = listed(|kind| matches!(kind, Kind::RichText { .. }));
315 let paragraph = paragraphs[pick(paragraphs.len())];
316 let container = containers[pick(containers.len())];
317 let anchor = nodes[&container].children.get(pick(4)).copied();
318 let text = texts[pick(texts.len())];
319 let words = ["", "a", "Two words", "東京 🦀", "longer text here"];
320 let word = words[pick(words.len())];
321 let changes = match pick(8) {
322 0..=3 => Insertion::paragraph(container, anchor, word, "Author")
323 .and_then(|insertion| insertion.changes(&active)),
324 4 => TreeEdit::move_to(paragraph, container, anchor, "Author")
325 .and_then(|edit| edit.changes(&active)),
326 5 => TreeEdit::delete(paragraph, "Author").and_then(|edit| edit.changes(&active)),
327 6 => ParagraphSplit::new(text, pick(3) as u32, "Author")
328 .and_then(|split| split.changes(&active)),
329 _ => crate::formatting::format_changes(
330 &active,
331 text,
332 0..pick(2) as u32,
333 &[TextAttribute::Bold(pick(2) == 0)],
334 ),
335 };
336 let Ok(changes) = changes else {
337 continue;
338 };
339 let store = Store::parse(&image).unwrap();
340 let index = RevisionIndex::parse(&store).unwrap();
341 let written =
342 crate::write::write_revision_on(&index, space, |_| Ok(changes.clone())).unwrap();
343 assert_eq!(
344 active.write(&arena, &[], changes).unwrap(),
345 written != image,
346 "step {step}"
347 );
348 image = written;
349 if step % 40 == 0 {
350 assert_stores(&active, &image, space);
351 }
352 }
353 assert_stores(&active, &image, space);
354 }
355
356 #[test]
357 fn writes_leave_the_page_that_appending_and_reading_their_revisions_does() {
358 // Past 512 revisions, so the chain checkpoints once.
359 check_writes(
360 include_bytes!(
361 "../../../corpus/native/20260905-05/snapshots/02-text/notebook/synthetic.one"
362 ),
363 720,
364 );
365 check_writes(
366 include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one"),
367 160,
368 );
369 check_writes(
370 &crate::create_section("model.one", "First", "Author").unwrap(),
371 160,
372 );
373 }
374}
crates/onestore/src/document.rs+28-9
......@@ -46,13 +46,13 @@ impl<'a> Space<'a> {
4646 }
4747}
4848
49#[derive(Debug, Serialize)]
49#[derive(Debug, Clone, Serialize)]
5050pub struct Revision<'a> {
5151 pub roots: BTreeMap<u32, ExGuid>,
5252 pub nodes: BTreeMap<ExGuid, Element<'a>>,
5353}
5454
55#[derive(Debug, Serialize)]
55#[derive(Debug, Clone, Serialize)]
5656pub struct Element<'a> {
5757 pub jcid: u32,
5858 pub children: Vec<ExGuid>,
......@@ -490,13 +490,13 @@ pub enum Kind<'a> {
490490 Unknown,
491491}
492492
493#[derive(Debug, PartialEq, Serialize)]
493#[derive(Debug, Clone, PartialEq, Serialize)]
494494pub struct Field<'a> {
495495 pub id: u32,
496496 pub value: FieldValue<'a>,
497497}
498498
499#[derive(Debug, PartialEq, Serialize)]
499#[derive(Debug, Clone, PartialEq, Serialize)]
500500pub enum FieldValue<'a> {
501501 NoData,
502502 Bytes(#[serde(serialize_with = "hex")] &'a [u8]),
......@@ -648,6 +648,29 @@ impl<'a, 'o> Fields<'a, 'o> {
648648}
649649
650650fn references(bytes: &[u8], ids: &BTreeMap<u32, [u8; 16]>) -> Result<Vec<ExGuid>> {
651 // A long list, such as a large outline's children, reads a dense table as an array.
652 if bytes.len() >= 256
653 && bytes.len().is_multiple_of(4)
654 && let Some((last, _)) = ids.last_key_value()
655 && *last as usize + 1 == ids.len()
656 {
657 let table: Vec<[u8; 16]> = ids.values().copied().collect();
658 return bytes
659 .chunks_exact(4)
660 .enumerate()
661 .map(|(i, bytes)| {
662 let raw = u32::from_le_bytes(bytes.try_into().unwrap());
663 let guid = table.get((raw >> 8) as usize).ok_or(Error {
664 offset: i * 4,
665 message: "Compact ID refers to a missing global ID",
666 })?;
667 Ok(ExGuid {
668 guid: *guid,
669 n: raw & 0xff,
670 })
671 })
672 .collect();
673 }
651674 let mut cursor = Cursor { bytes, offset: 0 };
652675 let mut result = Vec::new();
653676 while !cursor.bytes.is_empty() {
......@@ -906,11 +929,7 @@ impl<'a> Document<'a> {
906929}
907930
908931impl<'a> Element<'a> {
909 pub(crate) fn parse(object: &Object<'a>, store: &Store<'a>) -> Result<Self> {
910 Self::parse_with(object, store, &mut |id| store.file_data(id))
911 }
912
913 fn parse_with(
932 pub(crate) fn parse_with(
914933 object: &Object<'a>,
915934 store: &Store<'a>,
916935 file_data: &mut impl FnMut([u8; 16]) -> Result<&'a [u8]>,
crates/onestore/src/edit.rs+87-57
......@@ -1,7 +1,8 @@
11use crate::{
2 Error, ExGuid, Object, ObjectData, PropertySets, RevisionIndex, Store,
2 Error, ExGuid, Object, ObjectData, PropertySets,
3 active::{ActivePage, Changes},
34 create::string,
4 document::{Document, Element, Kind},
5 document::{Element, Kind},
56 write::PropertyObject,
67};
78use std::{collections::BTreeMap, ops::Range, sync::Arc};
......@@ -35,28 +36,31 @@ pub fn replace_text(
3536 range: Range<u32>,
3637 replacement: &str,
3738) -> Result<Vec<u8>, Error> {
38 let invalid = |message| Error { offset: 0, message };
3939 if range.start > range.end || replacement.contains(['\0', '\n', '\r', '\u{fffc}']) {
40 return Err(invalid(
41 "Use a valid text range and ordinary paragraph text",
42 ));
40 return Err(Error {
41 offset: 0,
42 message: "Use a valid text range and ordinary paragraph text",
43 });
4344 }
44 let store = Store::parse(source)?;
45 let index = RevisionIndex::parse(&store)?;
46 index.validate_current()?;
47 let rid = index.active(space)?;
48 let raw = index.resolve(space, rid)?;
49 if !raw.reachable()?.contains(&object) {
45 crate::active::write(source, space, |active| {
46 text_changes(active, object, range, replacement)
47 })
48}
49
50/// `replace_text` on an active page.
51pub(crate) fn text_changes(
52 active: &ActivePage<'_>,
53 object: ExGuid,
54 range: Range<u32>,
55 replacement: &str,
56) -> Result<Changes, Error> {
57 let invalid = |message| Error { offset: 0, message };
58 let raw = &active.live.revision;
59 if !active.live.is_reachable(object) {
5060 return Err(invalid("Object is not reachable in the active revision"));
5161 }
52 let document = Document::parse(&index)?;
53 let revision = document
54 .spaces
55 .get(&space)
56 .and_then(|s| s.revisions.get(&rid))
57 .ok_or_else(|| invalid("The active document revision is unavailable"))?;
58 let pages = document.pages_in(space)?;
59 let parents = editable_parents(revision, &pages, object)?;
62 let revision = &active.view;
63 let parents = active.editable_parents(object)?;
6064 let node = revision
6165 .nodes
6266 .get(&object)
......@@ -129,7 +133,7 @@ pub fn replace_text(
129133 ));
130134 };
131135 if &text[start..end] == replacement {
132 return Ok(source.to_vec());
136 return Ok(Changes::new());
133137 }
134138 let added = u32::try_from(replacement.encode_utf16().count())
135139 .map_err(|_| invalid("Replacement text exceeds the UTF-16 offset range"))?;
......@@ -235,9 +239,9 @@ pub fn replace_text(
235239 pending.extend(parents.get(&id).into_iter().flatten().copied());
236240 }
237241 let Some((page, automatic, title_text)) =
238 page_title(revision, &pages, Some((object, &changed)))?
242 active.title(&BTreeMap::new(), Some((object, &changed)))?
239243 else {
240 return crate::write::replace_objects(&index, space, &edits);
244 return crate::write::patched(raw, &edits);
241245 };
242246 let Kind::Page {
243247 alternate_title, ..
......@@ -296,7 +300,7 @@ pub fn replace_text(
296300 &[]
297301 },
298302 });
299 crate::write::replace_objects(&index, space, &edits)
303 crate::write::patched(raw, &edits)
300304}
301305
302306pub(crate) fn editable_parents(
......@@ -304,8 +308,19 @@ pub(crate) fn editable_parents(
304308 pages: &[ExGuid],
305309 object: ExGuid,
306310) -> Result<std::collections::BTreeMap<ExGuid, Vec<ExGuid>>, Error> {
307 let invalid = |message| Error { offset: 0, message };
308311 let parents = revision.parents(pages)?;
312 check_editable(revision, &parents, pages, object)?;
313 Ok(parents)
314}
315
316/// Requires `object` on an active page with neither it nor an ancestor read-only.
317pub(crate) fn check_editable(
318 revision: &crate::document::Revision<'_>,
319 parents: &BTreeMap<ExGuid, Vec<ExGuid>>,
320 pages: &[ExGuid],
321 object: ExGuid,
322) -> Result<(), Error> {
323 let invalid = |message| Error { offset: 0, message };
309324 if !parents.contains_key(&object) && !pages.contains(&object) {
310325 return Err(invalid("Select content on an active editable page"));
311326 }
......@@ -323,22 +338,40 @@ pub(crate) fn editable_parents(
323338 }
324339 pending.extend(parents.get(&id).into_iter().flatten().copied());
325340 }
326 Ok(parents)
341 Ok(())
327342}
328343
329344pub(crate) fn page_title(
330345 revision: &crate::document::Revision<'_>,
331346 pages: &[ExGuid],
332347 text_update: Option<(ExGuid, &str)>,
348) -> Result<Option<(ExGuid, bool, String)>, Error> {
349 title_of(
350 |id| revision.nodes.get(&id),
351 &revision.parents(pages)?,
352 revision.nodes.keys().copied(),
353 pages,
354 text_update,
355 )
356}
357
358/// `page_title` over the elements `node` finds, whose parents from `pages` are `parents`;
359/// `candidates` lists, in order, every element that may be title text.
360pub(crate) fn title_of<'n>(
361 node: impl Fn(ExGuid) -> Option<&'n Element<'n>>,
362 parents: &BTreeMap<ExGuid, Vec<ExGuid>>,
363 candidates: impl Iterator<Item = ExGuid>,
364 pages: &[ExGuid],
365 text_update: Option<(ExGuid, &str)>,
333366) -> Result<Option<(ExGuid, bool, String)>, Error> {
334367 let invalid = |message| Error { offset: 0, message };
335 let parents = revision.parents(pages)?;
336 let seen: std::collections::BTreeSet<_> = parents.keys().chain(pages).copied().collect();
337 let titles: Vec<_> = revision
338 .nodes
339 .iter()
340 .filter_map(|(id, node)| {
341 if !seen.contains(id) || !node.extra[0].iter().any(|field| field.id == 0x88001cb4) {
368 let titles: Vec<_> = candidates
369 .filter_map(|id| {
370 if !parents.contains_key(&id) && !pages.contains(&id) {
371 return None;
372 }
373 let node = node(id)?;
374 if !node.extra[0].iter().any(|field| field.id == 0x88001cb4) {
342375 return None;
343376 }
344377 match &node.kind {
......@@ -346,7 +379,7 @@ pub(crate) fn page_title(
346379 text,
347380 boilerplate: false,
348381 ..
349 } => Some((*id, text.as_str())),
382 } => Some((id, text.as_str())),
350383 _ => None,
351384 }
352385 })
......@@ -369,15 +402,15 @@ pub(crate) fn page_title(
369402 let [page] = pages else {
370403 return Err(invalid("Title editing requires a single active page"));
371404 };
372 let Kind::Page { rtl, .. } = &revision.nodes[page].kind else {
405 let Kind::Page { rtl, .. } = &node(*page).unwrap().kind else {
373406 unreachable!()
374407 };
375408 let mut title_text = title_line(title_text);
376409 if automatic {
377 let mut roots = revision.nodes[page].children.clone();
410 let mut roots = node(*page).unwrap().children.clone();
378411 roots.sort_by(|a, b| {
379 let a = &revision.nodes[a].layout;
380 let b = &revision.nodes[b].layout;
412 let a = &node(*a).unwrap().layout;
413 let b = &node(*b).unwrap().layout;
381414 a.y.unwrap_or(0.0)
382415 .total_cmp(&b.y.unwrap_or(0.0))
383416 .then_with(|| {
......@@ -394,12 +427,12 @@ pub(crate) fn page_title(
394427 if !seen.insert(id) {
395428 continue;
396429 }
397 let node = &revision.nodes[&id];
430 let element = node(id).unwrap();
398431 if let Kind::RichText {
399432 text,
400433 boilerplate: false,
401434 ..
402 } = &node.kind
435 } = &element.kind
403436 {
404437 title_text = automatic_title(
405438 if let Some((_, changed)) = text_update.filter(|(object, _)| *object == id) {
......@@ -412,40 +445,37 @@ pub(crate) fn page_title(
412445 break;
413446 }
414447 }
415 if *rtl == Some(true) && matches!(node.kind, Kind::Row) {
416 pending.extend(node.children.iter().copied());
448 if *rtl == Some(true) && matches!(element.kind, Kind::Row) {
449 pending.extend(element.children.iter().copied());
417450 } else {
418 pending.extend(node.children.iter().rev().copied());
451 pending.extend(element.children.iter().rev().copied());
419452 }
420 pending.extend(node.content.iter().rev().copied());
421 pending.extend(node.structure.iter().rev().copied());
453 pending.extend(element.content.iter().rev().copied());
454 pending.extend(element.structure.iter().rev().copied());
422455 }
423456 }
424457 Ok(Some((*page, automatic, title_text.to_owned())))
425458}
426459
460/// Updates the page title for `changed` objects made on `view`, a copy of the page's view.
427461pub(crate) fn update_title(
428 store: &Store<'_>,
429 raw: &crate::ResolvedRevision<'_>,
462 active: &ActivePage<'_>,
430463 view: crate::document::Revision<'_>,
431 pages: &[ExGuid],
432464 changed: &mut BTreeMap<ExGuid, PropertyObject>,
433465) -> Result<(), Error> {
434466 let invalid = |message| Error { offset: 0, message };
467 let (raw, pages) = (&active.live.revision, &active.pages);
435468 // Shorten the moved view's lifetime to the changed property buffers.
436469 let mut view = view;
437470 for (id, object) in changed.iter() {
438471 view.nodes.insert(
439472 *id,
440 Element::parse(
441 &Object {
442 jcid: object.jcid,
443 reference_count: 0,
444 data: ObjectData::Properties(&object.bytes),
445 global_ids: Arc::clone(&object.global_ids),
446 },
447 store,
448 )?,
473 active.element(&Object {
474 jcid: object.jcid,
475 reference_count: 0,
476 data: ObjectData::Properties(&object.bytes),
477 global_ids: Arc::clone(&object.global_ids),
478 })?,
449479 );
450480 }
451481 let title = page_title(&view, pages, None)?;
crates/onestore/src/formatting.rs+95-89
......@@ -1,9 +1,9 @@
11use crate::{
2 Error, ExGuid, PropertySets, RevisionIndex, Store,
2 Error, ExGuid, PropertySets,
3 active::{ActivePage, Changes},
34 create::{current_timestamps, properties, string},
4 document::{Document, Kind},
5 edit::editable_parents,
6 write::{PropertyObject, fresh_guid, write_revision_on},
5 document::Kind,
6 write::{PropertyObject, fresh_guid},
77};
88use serde::{Deserialize, Serialize};
99use std::{
......@@ -98,19 +98,26 @@ pub(crate) fn format_text(
9898 range: Range<u32>,
9999 attributes: &[TextAttribute],
100100) -> Result<Vec<u8>, Error> {
101 crate::active::write(source, space, |active| {
102 format_changes(active, object, range, attributes)
103 })
104}
105
106/// `format_text` on an active page.
107pub(crate) fn format_changes(
108 active: &ActivePage<'_>,
109 object: ExGuid,
110 range: Range<u32>,
111 attributes: &[TextAttribute],
112) -> Result<Changes, Error> {
101113 if attributes.is_empty() || range.start > range.end {
102114 return Err(invalid(
103115 "Select a text range and at least one formatting attribute",
104116 ));
105117 }
106118 let values = attribute_values(attributes)?;
107 let store = Store::parse(source)?;
108 let index = RevisionIndex::parse(&store)?;
109 index.validate_current()?;
110 let document = Document::parse(&index)?;
111 let view = document.active(space)?;
112 let pages = document.pages_in(space)?;
113 let parents = editable_parents(view, &pages, object)?;
119 let view = &active.view;
120 let parents = active.editable_parents(object)?;
114121 let node = &view.nodes[&object];
115122 let Kind::RichText {
116123 text,
......@@ -172,87 +179,86 @@ pub(crate) fn format_text(
172179 }
173180 }
174181 let modified = current_timestamps()?.0.to_le_bytes();
175 write_revision_on(&index, space, |raw| {
176 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
177 let fields = PropertySets::parse(&target.bytes)?;
178 if fields.sets[0].iter().any(|p| p.id == 0x24003458) {
179 return Err(invalid("This text object contains associated run objects"));
180 }
181 if segments.len() != runs.len() && fields.sets[0].iter().any(|p| p.id == 0x40003499) {
182 return Err(invalid(
183 "Formatting boundaries cannot split preserved run data",
184 ));
185 }
186 let mut styles = BTreeMap::new();
187 let mut changed = BTreeMap::new();
188 let mut references = Vec::new();
189 let mut ends = Vec::new();
190 let mut updated = false;
191 let changes: Vec<_> = values
192 .iter()
193 .map(|(id, value)| (*id, value.as_slice()))
194 .collect();
195 for &(i, end, selected) in &segments {
196 let previous = runs[i].format;
197 let key = (previous, selected);
198 let id = if let Some(id) = styles.get(&key) {
199 *id
200 } else if let Some(id) = previous.filter(|_| !selected) {
182 let raw = &active.live.revision;
183 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
184 let fields = PropertySets::parse(&target.bytes)?;
185 if fields.sets[0].iter().any(|p| p.id == 0x24003458) {
186 return Err(invalid("This text object contains associated run objects"));
187 }
188 if segments.len() != runs.len() && fields.sets[0].iter().any(|p| p.id == 0x40003499) {
189 return Err(invalid(
190 "Formatting boundaries cannot split preserved run data",
191 ));
192 }
193 let mut styles = BTreeMap::new();
194 let mut changed = BTreeMap::new();
195 let mut references = Vec::new();
196 let mut ends = Vec::new();
197 let mut updated = false;
198 let changes: Vec<_> = values
199 .iter()
200 .map(|(id, value)| (*id, value.as_slice()))
201 .collect();
202 for &(i, end, selected) in &segments {
203 let previous = runs[i].format;
204 let key = (previous, selected);
205 let id = if let Some(id) = styles.get(&key) {
206 *id
207 } else if let Some(id) = previous.filter(|_| !selected) {
208 id
209 } else {
210 let mut style = match previous {
211 Some(id) => PropertyObject::from_object(&raw.objects[&id])?,
212 None => PropertyObject {
213 jcid: 0x12004d,
214 bytes: properties(&[])?,
215 global_ids: Arc::new(BTreeMap::new()),
216 },
217 };
218 if selected {
219 style.set(&changes)?;
220 }
221 if let Some(id) = previous
222 .filter(|id| raw.objects[id].data == crate::ObjectData::Properties(&style.bytes))
223 {
224 styles.insert(key, id);
201225 id
202226 } else {
203 let mut style = match previous {
204 Some(id) => PropertyObject::from_object(&raw.objects[&id])?,
205 None => PropertyObject {
206 jcid: 0x12004d,
207 bytes: properties(&[])?,
208 global_ids: Arc::new(BTreeMap::new()),
209 },
210 };
211 if selected {
212 style.set(&changes)?;
213 }
214 if let Some(id) = previous.filter(|id| {
215 raw.objects[id].data == crate::ObjectData::Properties(&style.bytes)
216 }) {
217 styles.insert(key, id);
218 id
219 } else {
220 if !PropertySets::parse(&style.bytes)?.sets[0]
221 .iter()
222 .any(|p| p.id == 0x14001c3b)
223 {
224 style.set(&[(
225 0x14001c3b,
226 &resolved[i].format.language.unwrap_or(0x409).to_le_bytes(),
227 )])?;
228 }
229 let id = ExGuid {
230 guid: fresh_guid()?,
231 n: 1,
232 };
233 style.reference(id)?;
234 changed.insert(id, style);
235 styles.insert(key, id);
236 id
227 if !PropertySets::parse(&style.bytes)?.sets[0]
228 .iter()
229 .any(|p| p.id == 0x14001c3b)
230 {
231 style.set(&[(
232 0x14001c3b,
233 &resolved[i].format.language.unwrap_or(0x409).to_le_bytes(),
234 )])?;
237235 }
238 };
239 updated |= selected && previous != Some(id);
240 references.extend_from_slice(&target.reference(id)?);
241 ends.extend_from_slice(&end.to_le_bytes());
242 }
243 if !updated {
244 return Ok(BTreeMap::new());
245 }
246 ends.truncate(ends.len() - 4);
247 target.set(&[
248 (0x24001e13, &references),
249 (0x1c001e12, &ends),
250 (0x14001d7a, &modified),
251 ])?;
252 changed.insert(object, target);
253 touch_ancestors(raw, &parents, object, &modified, &mut changed)?;
254 Ok(changed)
255 })
236 let id = ExGuid {
237 guid: fresh_guid()?,
238 n: 1,
239 };
240 style.reference(id)?;
241 changed.insert(id, style);
242 styles.insert(key, id);
243 id
244 }
245 };
246 updated |= selected && previous != Some(id);
247 references.extend_from_slice(&target.reference(id)?);
248 ends.extend_from_slice(&end.to_le_bytes());
249 }
250 if !updated {
251 return Ok(BTreeMap::new());
252 }
253 ends.truncate(ends.len() - 4);
254 target.set(&[
255 (0x24001e13, &references),
256 (0x1c001e12, &ends),
257 (0x14001d7a, &modified),
258 ])?;
259 changed.insert(object, target);
260 touch_ancestors(raw, parents, object, &modified, &mut changed)?;
261 Ok(changed)
256262}
257263
258264pub(crate) fn touch_ancestors(
crates/onestore/src/insertion.rs+66-79
......@@ -1,9 +1,9 @@
11use crate::{
2 Error, ExGuid, Object, ObjectData, RevisionIndex, Store,
2 Error, ExGuid, Object, ObjectData,
3 active::{ActivePage, Changes},
34 create::{current_timestamps, default_text_style, properties, string},
4 document::{Document, Element, Kind},
5 edit::{editable_parents, page_title},
6 write::{PropertyObject, fresh_guid, write_revision_on},
5 document::Kind,
6 write::{PropertyObject, fresh_guid},
77};
88use serde::{Deserialize, Serialize};
99use std::{
......@@ -191,20 +191,15 @@ impl Insertion {
191191
192192 pub(crate) fn apply(&self, source: &[u8], space: ExGuid) -> Result<Vec<u8>, Error> {
193193 self.validate()?;
194 let store = Store::parse(source)?;
195 let index = RevisionIndex::parse(&store)?;
196 index.validate_current()?;
197 let mut document = Document::parse(&index)?;
198 let pages = document.pages_in(space)?;
199 let [page] = pages.as_slice() else {
194 crate::active::write(source, space, |active| self.changes(active))
195 }
196
197 pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> {
198 let [page] = active.pages.as_slice() else {
200199 return Err(invalid("Insertion requires a single active page"));
201200 };
202 let mut view = document
203 .spaces
204 .remove(&space)
205 .and_then(crate::document::Space::into_active)
206 .ok_or_else(|| invalid("The active page is unavailable"))?;
207 let parents = editable_parents(&view, &pages, self.parent)?;
201 let view = &active.view;
202 let parents = active.editable_parents(self.parent)?;
208203 let parent = &view.nodes[&self.parent];
209204 let position = match self.placement {
210205 Placement::Paragraph { before } => {
......@@ -363,82 +358,74 @@ impl Insertion {
363358 },
364359 );
365360 }
366 let raw = index.resolve_active(space)?;
361 let raw = &active.live.revision;
367362 if new.keys().any(|id| raw.objects.contains_key(id)) {
368363 return Err(invalid(
369364 "An insertion identity is already present; reconcile the existing edit",
370365 ));
371366 }
372 // Drop the semantic view before moving the property bytes it borrows.
367 // Drop the overlay before moving the property bytes it borrows.
373368 let title = {
374 view.nodes
375 .get_mut(&self.parent)
376 .unwrap()
377 .children
378 .insert(position, self.object());
369 let mut overlay = BTreeMap::new();
370 let mut parent = view.nodes[&self.parent].clone();
371 parent.children.insert(position, self.object());
372 overlay.insert(self.parent, parent);
379373 for (id, object) in &new {
380 view.nodes.insert(
374 overlay.insert(
381375 *id,
382 Element::parse(
383 &Object {
384 jcid: object.jcid,
385 reference_count: 0,
386 data: ObjectData::Properties(&object.bytes),
387 global_ids: Arc::clone(&object.global_ids),
388 },
389 &store,
390 )?,
376 active.element(&Object {
377 jcid: object.jcid,
378 reference_count: 0,
379 data: ObjectData::Properties(&object.bytes),
380 global_ids: Arc::clone(&object.global_ids),
381 })?,
391382 );
392383 }
393 let title = page_title(&view, &pages, None)?;
394 drop(view);
395 title
384 active.title(&overlay, None)?
396385 };
397 write_revision_on(&index, space, |raw| {
398 let mut changed = new;
399 for id in &ancestors {
400 let mut object = PropertyObject::from_object(&raw.objects[id])?;
401 object.set(&[(0x14001d7a, &modified)])?;
402 changed.insert(*id, object);
403 }
404 let parent = changed.get_mut(&self.parent).unwrap();
386 let mut changed = new;
387 for id in &ancestors {
388 let mut object = PropertyObject::from_object(&raw.objects[id])?;
389 object.set(&[(0x14001d7a, &modified)])?;
390 changed.insert(*id, object);
391 }
392 let parent = changed.get_mut(&self.parent).unwrap();
393 let properties = crate::PropertySets::parse(&parent.bytes)?;
394 let existing = properties.sets[0].iter().find(|p| p.id == 0x24001c20);
395 let mut ids = match existing.map(|p| &p.value) {
396 Some(crate::Value::References { compact_ids, .. }) => compact_ids.to_vec(),
397 None => Vec::new(),
398 _ => return Err(invalid("The parent has an invalid child list")),
399 };
400 let child = parent.reference(self.object())?;
401 if position > ids.len() / 4 {
402 return Err(invalid("The parent has an invalid child list"));
403 }
404 ids.splice(position * 4..position * 4, child);
405 parent.set(&[(0x24001c20, &ids)])?;
406 if matches!(self.placement, Placement::Paragraph { .. }) {
405407 let properties = crate::PropertySets::parse(&parent.bytes)?;
406 let existing = properties.sets[0].iter().find(|p| p.id == 0x24001c20);
407 let mut ids = match existing.map(|p| &p.value) {
408 Some(crate::Value::References { compact_ids, .. }) => compact_ids.to_vec(),
409 None => Vec::new(),
410 _ => return Err(invalid("The parent has an invalid child list")),
411 };
412 let child = parent.reference(self.object())?;
413 if position > ids.len() / 4 {
414 return Err(invalid("The parent has an invalid child list"));
415 }
416 ids.splice(position * 4..position * 4, child);
417 parent.set(&[(0x24001c20, &ids)])?;
418 if matches!(self.placement, Placement::Paragraph { .. }) {
419 let properties = crate::PropertySets::parse(&parent.bytes)?;
420 if !properties.sets[0].iter().any(|p| p.id == 0x0c001c03) {
421 parent.set(&[(0x0c001c03, &[1])])?;
422 }
408 if !properties.sets[0].iter().any(|p| p.id == 0x0c001c03) {
409 parent.set(&[(0x0c001c03, &[1])])?;
423410 }
424 if let Some((page, automatic, title)) = title {
425 let metadata = raw
426 .roots
427 .get(&2)
428 .ok_or_else(|| invalid("Page title metadata is unavailable"))?;
429 if raw.objects[metadata].jcid != 0x20030 {
430 return Err(invalid("Page title metadata is unavailable"));
431 }
432 let title = string(&title);
433 let mut metadata_object = PropertyObject::from_object(&raw.objects[metadata])?;
434 metadata_object.set(&[(0x1c001cf3, &title)])?;
435 changed.insert(*metadata, metadata_object);
436 changed
437 .get_mut(&page)
438 .unwrap()
439 .set(&[(0x1c001d3c, if automatic { &title } else { &[0, 0] })])?;
411 }
412 if let Some((page, automatic, title)) = title {
413 let metadata = raw
414 .roots
415 .get(&2)
416 .ok_or_else(|| invalid("Page title metadata is unavailable"))?;
417 if raw.objects[metadata].jcid != 0x20030 {
418 return Err(invalid("Page title metadata is unavailable"));
440419 }
441 Ok(changed)
442 })
420 let title = string(&title);
421 let mut metadata_object = PropertyObject::from_object(&raw.objects[metadata])?;
422 metadata_object.set(&[(0x1c001cf3, &title)])?;
423 changed.insert(*metadata, metadata_object);
424 changed
425 .get_mut(&page)
426 .unwrap()
427 .set(&[(0x1c001d3c, if automatic { &title } else { &[0, 0] })])?;
428 }
429 Ok(changed)
443430 }
444431}
crates/onestore/src/lib.rs+1
......@@ -1,6 +1,7 @@
11#![forbid(unsafe_code)]
22#![doc = include_str!("../README.md")]
33
4mod active;
45mod bytes;
56mod commit;
67mod create;
crates/onestore/src/objects.rs+6-1
......@@ -82,6 +82,11 @@ impl Object<'_> {
8282
8383impl ResolvedRevision<'_> {
8484 pub fn reachable(&self) -> Result<BTreeSet<ExGuid>> {
85 Ok(self.checked_counts()?.into_keys().collect())
86 }
87
88 /// Incoming reference counts of the reachable objects, which must equal those stored.
89 pub(crate) fn checked_counts(&self) -> Result<BTreeMap<ExGuid, u32>> {
8590 let incoming = self.reference_counts()?;
8691 for (id, count) in &incoming {
8792 if self.objects[id].reference_count != *count {
......@@ -91,7 +96,7 @@ impl ResolvedRevision<'_> {
9196 });
9297 }
9398 }
94 Ok(incoming.into_keys().collect())
99 Ok(incoming)
95100 }
96101
97102 pub(crate) fn reference_counts(&self) -> Result<BTreeMap<ExGuid, u32>> {
crates/onestore/src/outline.rs+39-42
......@@ -1,9 +1,10 @@
11use crate::{
2 Error, ExGuid, RevisionIndex, Store,
2 Error, ExGuid,
3 active::{ActivePage, Changes},
34 create::current_timestamps,
4 document::{Document, Kind},
5 edit::{editable_parents, update_title},
6 write::{PropertyObject, write_revision_on},
5 document::Kind,
6 edit::update_title,
7 write::PropertyObject,
78};
89use serde::{Deserialize, Serialize};
910use std::collections::{BTreeMap, BTreeSet};
......@@ -30,6 +31,10 @@ impl OutlineEdit {
3031 space: ExGuid,
3132 object: ExGuid,
3233 ) -> Result<Vec<u8>, Error> {
34 crate::active::write(source, space, |active| self.changes(active, object))
35 }
36
37 pub(crate) fn changes(self, active: &ActivePage<'_>, object: ExGuid) -> Result<Changes, Error> {
3338 let invalid = |message| Error { offset: 0, message };
3439 let values = match self {
3540 Self::Position { x, y } => {
......@@ -54,20 +59,12 @@ impl OutlineEdit {
5459 }
5560 Self::Collapsed(value) => vec![(0x0c001c11, vec![u8::from(value)])],
5661 };
57 let store = Store::parse(source)?;
58 let index = RevisionIndex::parse(&store)?;
59 index.validate_current()?;
60 let mut document = Document::parse(&index)?;
61 let pages = document.pages_in(space)?;
62 let pages = &active.pages;
6263 let [page] = pages.as_slice() else {
6364 return Err(invalid("Outline editing requires a single active page"));
6465 };
65 let view = document
66 .spaces
67 .remove(&space)
68 .and_then(crate::document::Space::into_active)
69 .unwrap();
70 let parents = editable_parents(&view, &pages, object)?;
66 let view = &active.view;
67 let parents = active.editable_parents(object)?;
7168 let node = &view.nodes[&object];
7269 match self {
7370 Self::Collapsed(_) => {
......@@ -105,34 +102,33 @@ impl OutlineEdit {
105102 pending.extend(parents.get(&id).into_iter().flatten().copied());
106103 }
107104 let modified = current_timestamps()?.0.to_le_bytes();
108 write_revision_on(&index, space, |raw| {
109 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
110 target.set(
111 &values
112 .iter()
113 .map(|(id, data)| (*id, data.as_slice()))
114 .collect::<Vec<_>>(),
115 )?;
116 if matches!(self, Self::Width { .. }) {
117 target.remove(&[0x14001cdb])?;
118 }
119 if raw.objects[&object].data == crate::ObjectData::Properties(&target.bytes) {
120 return Ok(BTreeMap::new());
121 }
122 target.set(&[(0x14001d7a, &modified)])?;
123 let mut changed = BTreeMap::from([(object, target)]);
124 for id in ancestors {
125 if id != object {
126 let mut ancestor = PropertyObject::from_object(&raw.objects[&id])?;
127 ancestor.set(&[(0x14001d7a, &modified)])?;
128 changed.insert(id, ancestor);
129 }
130 }
131 if matches!(self, Self::Position { .. }) {
132 update_title(&store, raw, view, &pages, &mut changed)?;
105 let raw = &active.live.revision;
106 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
107 target.set(
108 &values
109 .iter()
110 .map(|(id, data)| (*id, data.as_slice()))
111 .collect::<Vec<_>>(),
112 )?;
113 if matches!(self, Self::Width { .. }) {
114 target.remove(&[0x14001cdb])?;
115 }
116 if raw.objects[&object].data == crate::ObjectData::Properties(&target.bytes) {
117 return Ok(BTreeMap::new());
118 }
119 target.set(&[(0x14001d7a, &modified)])?;
120 let mut changed = BTreeMap::from([(object, target)]);
121 for id in ancestors {
122 if id != object {
123 let mut ancestor = PropertyObject::from_object(&raw.objects[&id])?;
124 ancestor.set(&[(0x14001d7a, &modified)])?;
125 changed.insert(id, ancestor);
133126 }
134 Ok(changed)
135 })
127 }
128 if matches!(self, Self::Position { .. }) {
129 update_title(active, view.clone(), &mut changed)?;
130 }
131 Ok(changed)
136132 }
137133}
138134
......@@ -140,6 +136,7 @@ impl OutlineEdit {
140136mod tests {
141137 use super::*;
142138 use crate::write::write_revision;
139 use crate::{RevisionIndex, Store, document::Document};
143140
144141 #[test]
145142 fn protection_on_the_target_or_ancestor_prevents_layout_edits() {
crates/onestore/src/page/write.rs+572-572
......@@ -1,5 +1,6 @@
11//! Publishes an edited page model by lowering the difference from the stored page onto
2//! the typed writers, then squashing their transactions into one revision per space.
2//! the typed writers, whose revisions accumulate in memory, then squashing them into one
3//! revision per space.
34
45use super::{
56 Attachment, Image, Ink, Math, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table,
......@@ -7,9 +8,11 @@ use super::{
78use crate::{
89 Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets,
910 RevisionIndex, Store, TextAttribute, TreeEdit, Value,
10 document::{Document, Format, Kind, Tag},
11 active::{ActivePage, Changes},
12 document::{Format, Kind, Tag},
1113 write::{PropertyObject, RevisionEdit},
1214};
15use bumpalo::Bump;
1316use std::{
1417 collections::{BTreeMap, BTreeSet},
1518 ops::Range,
......@@ -339,38 +342,36 @@ pub(crate) fn write_page(
339342 if author.contains('\0') {
340343 return Err(invalid("Choose an author name without NUL"));
341344 }
342 // The parsed source is released before the writers parse their own images.
343 let (page, before, existing) = {
344 let store = Store::parse(source)?;
345 let index = RevisionIndex::parse(&store)?;
346 index.validate_current()?;
347 let document = Document::parse(&index)?;
348 let pages = document.pages_in(space)?;
349 let [page] = pages.as_slice() else {
350 return Err(invalid("Choose an object space containing one active page"));
351 };
352 let existing = index
353 .resolve_active(space)?
354 .objects
355 .keys()
356 .copied()
357 .collect();
358 (*page, Page::from_space(&document, space)?, existing)
345 let store = Store::parse(source)?;
346 let index = RevisionIndex::parse(&store)?;
347 let arena = Bump::new();
348 let active = ActivePage::parse(&index, space)?;
349 let [page] = active.pages[..] else {
350 return Err(invalid("Choose an object space containing one active page"));
359351 };
352 let before = Page::from_revision(&active.view, page)?;
360353 let mut lowering = Lowering {
361 image: source.to_vec(),
354 active,
355 arena: &arena,
362356 current: Some(before.clone()),
363 space,
357 edited: false,
364358 page,
365359 author,
366360 alias: BTreeMap::new(),
367361 built: BTreeSet::new(),
368362 };
369 lowering.run(&before, after, &existing)?;
370 if lowering.image == source {
371 return Ok(lowering.image);
372 }
373 squash(source, &lowering.image, &lowering.alias, None)
363 lowering.run(&before, after)?;
364 if !lowering.edited {
365 return Ok(source.to_vec());
366 }
367 let active = &lowering.active;
368 squash(
369 &index,
370 &[(space, &active.live.revision)],
371 &active.payloads,
372 &lowering.alias,
373 None,
374 )
374375}
375376
376377/// Direct children of every container, in model order, plus lookups by identity.
......@@ -461,10 +462,13 @@ impl<'a> View<'a> {
461462}
462463
463464struct Lowering<'a> {
464 image: Vec<u8>,
465 /// The page as `image` stores it, until a writer changes the image.
465 active: ActivePage<'a>,
466 /// Holds the objects the typed writers store for as long as `active` reads them.
467 arena: &'a Bump,
468 /// The page as `active` stores it, until a writer changes it.
466469 current: Option<Page>,
467 space: ExGuid,
470 /// Whether a writer stored anything.
471 edited: bool,
468472 page: ExGuid,
469473 author: &'a str,
470474 /// Model identities of new objects mapped to the identities the typed writers allocated.
......@@ -473,7 +477,7 @@ struct Lowering<'a> {
473477 built: BTreeSet<ExGuid>,
474478}
475479
476impl Lowering<'_> {
480impl<'a> Lowering<'a> {
477481 fn id(&self, model: ExGuid) -> ExGuid {
478482 self.alias.get(&model).copied().unwrap_or(model)
479483 }
......@@ -488,31 +492,35 @@ impl Lowering<'_> {
488492 Ok(written)
489493 }
490494
491 fn apply(&mut self, edit: impl FnOnce(&[u8]) -> Result<Vec<u8>, Error>) -> Result<(), Error> {
492 let image = edit(&self.image)?;
493 if image != self.image {
494 self.image = image;
495 /// Stores the next revision a typed writer computes from the page as stored so far.
496 fn write(
497 &mut self,
498 changes: impl FnOnce(&ActivePage<'a>) -> Result<Changes, Error>,
499 ) -> Result<(), Error> {
500 self.write_with(&[], changes)
501 }
502
503 fn write_with(
504 &mut self,
505 payloads: &[([u8; 16], &[u8])],
506 changes: impl FnOnce(&ActivePage<'a>) -> Result<Changes, Error>,
507 ) -> Result<(), Error> {
508 let changes = changes(&self.active)?;
509 if self.active.write(self.arena, payloads, changes)? {
495510 self.current = None;
511 self.edited = true;
496512 }
497513 Ok(())
498514 }
499515
500516 fn current(&mut self) -> Result<Page, Error> {
501517 if self.current.is_none() {
502 let store = Store::parse(&self.image)?;
503 let index = RevisionIndex::parse(&store)?;
504 let document = Document::parse(&index)?;
505 self.current = Some(Page::from_space(&document, self.space)?);
518 self.current = Some(Page::from_revision(&self.active.view, self.page)?);
506519 }
507520 Ok(self.current.clone().unwrap())
508521 }
509522
510 fn run(
511 &mut self,
512 before: &Page,
513 after: &Page,
514 existing: &BTreeSet<ExGuid>,
515 ) -> Result<(), Error> {
523 fn run(&mut self, before: &Page, after: &Page) -> Result<(), Error> {
516524 if after.created != before.created || after.margin_origin != before.margin_origin {
517525 return Err(invalid("Page creation time and margins cannot be edited"));
518526 }
......@@ -540,7 +548,7 @@ impl Lowering<'_> {
540548 for id in new.outlines.keys().chain(new.paragraphs.keys()) {
541549 if !old.outlines.contains_key(id)
542550 && !old.paragraphs.contains_key(id)
543 && existing.contains(id)
551 && self.active.live.revision.objects.contains_key(id)
544552 {
545553 return Err(invalid(
546554 "A new model identity already exists in the section",
......@@ -730,8 +738,7 @@ impl Lowering<'_> {
730738 }
731739 };
732740 let insertion = Insertion::outline(self.page, x, y, text, self.author)?;
733 let space = self.space;
734 self.apply(|image| insertion.apply(image, space))?;
741 self.write(|active| insertion.changes(active))?;
735742 let outline_id = insertion.object();
736743 self.alias.insert(*id, outline_id);
737744 self.alias.insert(
......@@ -809,8 +816,7 @@ impl Lowering<'_> {
809816 continue;
810817 }
811818 let split = ParagraphSplit::new(previous.id, offset, self.author)?;
812 let space = self.space;
813 self.apply(|image| split.apply(image, space))?;
819 self.write(|active| split.changes(active))?;
814820 self.alias.insert(right, split.object());
815821 self.alias.insert(next.id, split.text_object());
816822 let list = placed.get_mut(container).unwrap();
......@@ -855,8 +861,7 @@ impl Lowering<'_> {
855861 ));
856862 }
857863 let join = ParagraphJoin::new(previous.id, removed.id, self.author)?;
858 let space = self.space;
859 self.apply(|image| join.apply(image, space))?;
864 self.write(|active| join.changes(active))?;
860865 consumed.insert(right);
861866 placed.get_mut(container).unwrap().retain(|id| *id != right);
862867 placed.remove(&right);
......@@ -905,8 +910,7 @@ impl Lowering<'_> {
905910 next.map(|n| self.id(n)),
906911 self.author,
907912 )?;
908 let space = self.space;
909 self.apply(|image| edit.apply(image, space))?;
913 self.write(|active| edit.changes(active))?;
910914 }
911915 next = Some(*id);
912916 }
......@@ -1037,7 +1041,6 @@ impl Lowering<'_> {
10371041 values.push((0x1c001d9d, crate::create::string(path)));
10381042 }
10391043 let holder = self.id(*paragraph_id);
1040 let space = self.space;
10411044 let mut payloads: Vec<([u8; 16], &[u8])> = vec![(payload_guid, bytes)];
10421045 let mut preview = None;
10431046 if let Some(icon) = &attachment.preview {
......@@ -1054,35 +1057,34 @@ impl Lowering<'_> {
10541057 payload_reference(guid),
10551058 ));
10561059 }
1057 self.apply(|current| {
1058 crate::write::write_revision_with_payloads(current, space, &payloads, |raw| {
1059 let mut changed = BTreeMap::new();
1060 let mut file = PropertyObject::file(file_id, &reference, extension)?;
1061 file.jcid = 0x80036;
1062 changed.insert(file_id, file);
1063 let mut node = PropertyObject {
1064 jcid: 0x60035,
1065 bytes: crate::create::properties(&values)?,
1066 global_ids: std::sync::Arc::new(BTreeMap::from([(0, attachment_id.guid)])),
1067 };
1068 node.reference(attachment_id)?;
1069 let container = node.reference(file_id)?;
1070 node.set(&[(0x20001d9b, &container)])?;
1071 if let Some((icon_id, icon_reference)) = &preview {
1072 changed.insert(
1073 *icon_id,
1074 PropertyObject::file(*icon_id, icon_reference, ".png")?,
1075 );
1076 let icon = node.reference(*icon_id)?;
1077 node.set(&[(0x20001c3f, &icon)])?;
1078 }
1079 changed.insert(attachment_id, node);
1080 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1081 let content = object.reference(attachment_id)?;
1082 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;
1083 changed.insert(holder, object);
1084 Ok(changed)
1085 })
1060 self.write_with(&payloads, |active| {
1061 let raw = &active.live.revision;
1062 let mut changed = BTreeMap::new();
1063 let mut file = PropertyObject::file(file_id, &reference, extension)?;
1064 file.jcid = 0x80036;
1065 changed.insert(file_id, file);
1066 let mut node = PropertyObject {
1067 jcid: 0x60035,
1068 bytes: crate::create::properties(&values)?,
1069 global_ids: std::sync::Arc::new(BTreeMap::from([(0, attachment_id.guid)])),
1070 };
1071 node.reference(attachment_id)?;
1072 let container = node.reference(file_id)?;
1073 node.set(&[(0x20001d9b, &container)])?;
1074 if let Some((icon_id, icon_reference)) = &preview {
1075 changed.insert(
1076 *icon_id,
1077 PropertyObject::file(*icon_id, icon_reference, ".png")?,
1078 );
1079 let icon = node.reference(*icon_id)?;
1080 node.set(&[(0x20001c3f, &icon)])?;
1081 }
1082 changed.insert(attachment_id, node);
1083 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1084 let content = object.reference(attachment_id)?;
1085 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;
1086 changed.insert(holder, object);
1087 Ok(changed)
10861088 })?;
10871089 }
10881090 Ok(())
......@@ -1201,49 +1203,47 @@ impl Lowering<'_> {
12011203 }
12021204 let (modified, filetime) = crate::create::current_timestamps()?;
12031205 let modified = modified.to_le_bytes();
1204 let (space, page) = (self.space, self.page);
1205 self.apply(|current| {
1206 crate::write::write_revision(current, space, |raw| {
1207 let mut changed = BTreeMap::new();
1208 let mut data_object = PropertyObject {
1209 jcid: 0x2003b,
1210 bytes: crate::create::properties(&[])?,
1211 global_ids: std::sync::Arc::new(BTreeMap::from([(0, data.guid)])),
1212 };
1213 data_object.reference(data)?;
1214 let references =
1215 write_strokes(&mut changed, &mut data_object, &strokes, 0, filetime)?;
1216 data_object.set(&[(0x24003416, &references)])?;
1217 changed.insert(data, data_object);
1218 let mut object = PropertyObject {
1219 jcid: 0x60014,
1220 bytes: crate::create::properties(&[
1221 (0x14001d7a, modified.to_vec()),
1222 (0x14001d4e, 1u32.to_le_bytes().to_vec()),
1223 ])?,
1224 global_ids: std::sync::Arc::new(BTreeMap::from([(0, container.guid)])),
1225 };
1226 object.reference(container)?;
1227 let data_reference = object.reference(data)?;
1228 object.set(&[(0x20003415, &data_reference)])?;
1229 changed.insert(container, object);
1230 match holder {
1231 Some(holder) => {
1232 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1233 let content = object.reference(container)?;
1234 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;
1235 changed.insert(holder, object);
1236 }
1237 None => {
1238 let mut object = PropertyObject::from_object(&raw.objects[&page])?;
1239 let mut children = page_children(&object)?;
1240 children.extend(object.reference(container)?);
1241 object.set(&[(0x24001c20, &children), (0x14001d7a, &modified)])?;
1242 changed.insert(page, object);
1243 }
1206 let page = self.page;
1207 self.write(|active| {
1208 let raw = &active.live.revision;
1209 let mut changed = BTreeMap::new();
1210 let mut data_object = PropertyObject {
1211 jcid: 0x2003b,
1212 bytes: crate::create::properties(&[])?,
1213 global_ids: std::sync::Arc::new(BTreeMap::from([(0, data.guid)])),
1214 };
1215 data_object.reference(data)?;
1216 let references = write_strokes(&mut changed, &mut data_object, &strokes, 0, filetime)?;
1217 data_object.set(&[(0x24003416, &references)])?;
1218 changed.insert(data, data_object);
1219 let mut object = PropertyObject {
1220 jcid: 0x60014,
1221 bytes: crate::create::properties(&[
1222 (0x14001d7a, modified.to_vec()),
1223 (0x14001d4e, 1u32.to_le_bytes().to_vec()),
1224 ])?,
1225 global_ids: std::sync::Arc::new(BTreeMap::from([(0, container.guid)])),
1226 };
1227 object.reference(container)?;
1228 let data_reference = object.reference(data)?;
1229 object.set(&[(0x20003415, &data_reference)])?;
1230 changed.insert(container, object);
1231 match holder {
1232 Some(holder) => {
1233 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1234 let content = object.reference(container)?;
1235 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;
1236 changed.insert(holder, object);
12441237 }
1245 Ok(changed)
1246 })
1238 None => {
1239 let mut object = PropertyObject::from_object(&raw.objects[&page])?;
1240 let mut children = page_children(&object)?;
1241 children.extend(object.reference(container)?);
1242 object.set(&[(0x24001c20, &children), (0x14001d7a, &modified)])?;
1243 changed.insert(page, object);
1244 }
1245 }
1246 Ok(changed)
12471247 })
12481248 }
12491249
......@@ -1266,47 +1266,41 @@ impl Lowering<'_> {
12661266 }
12671267 }
12681268 let container = self.id(ink.id);
1269 let data = {
1270 let store = Store::parse(&self.image)?;
1271 let index = RevisionIndex::parse(&store)?;
1272 let document = Document::parse(&index)?;
1273 let revision = document
1274 .spaces
1275 .get(&self.space)
1276 .and_then(crate::document::Space::active)
1277 .ok_or_else(|| invalid("The page space is unavailable"))?;
1278 match revision.nodes.get(&container).map(|node| &node.kind) {
1279 Some(crate::document::Kind::Ink {
1280 data: Some(data), ..
1281 }) => *data,
1282 _ => return Err(invalid("Stored ink has no stroke data to rewrite")),
1283 }
1269 let data = match self
1270 .active
1271 .view
1272 .nodes
1273 .get(&container)
1274 .map(|node| &node.kind)
1275 {
1276 Some(crate::document::Kind::Ink {
1277 data: Some(data), ..
1278 }) => *data,
1279 _ => return Err(invalid("Stored ink has no stroke data to rewrite")),
12841280 };
12851281 let (modified, filetime) = crate::create::current_timestamps()?;
12861282 let modified = modified.to_le_bytes();
1287 let space = self.space;
1288 self.apply(|current| {
1289 crate::write::write_revision(current, space, |raw| {
1290 let mut changed = BTreeMap::new();
1291 let mut data_object = PropertyObject::from_object(&raw.objects[&data])?;
1292 let mut references = Vec::new();
1293 for id in &kept {
1294 references.extend(data_object.reference(*id)?);
1295 }
1296 references.extend(write_strokes(
1297 &mut changed,
1298 &mut data_object,
1299 &added,
1300 kept.len(),
1301 filetime,
1302 )?);
1303 data_object.set(&[(0x24003416, &references)])?;
1304 changed.insert(data, data_object);
1305 let mut object = PropertyObject::from_object(&raw.objects[&container])?;
1306 object.set(&[(0x14001d7a, &modified)])?;
1307 changed.insert(container, object);
1308 Ok(changed)
1309 })
1283 self.write(|active| {
1284 let raw = &active.live.revision;
1285 let mut changed = BTreeMap::new();
1286 let mut data_object = PropertyObject::from_object(&raw.objects[&data])?;
1287 let mut references = Vec::new();
1288 for id in &kept {
1289 references.extend(data_object.reference(*id)?);
1290 }
1291 references.extend(write_strokes(
1292 &mut changed,
1293 &mut data_object,
1294 &added,
1295 kept.len(),
1296 filetime,
1297 )?);
1298 data_object.set(&[(0x24003416, &references)])?;
1299 changed.insert(data, data_object);
1300 let mut object = PropertyObject::from_object(&raw.objects[&container])?;
1301 object.set(&[(0x14001d7a, &modified)])?;
1302 changed.insert(container, object);
1303 Ok(changed)
13101304 })
13111305 }
13121306
......@@ -1349,44 +1343,38 @@ impl Lowering<'_> {
13491343 values.push((0x08001d13 | (1 << 31), Vec::new()));
13501344 }
13511345 values.push((0x08001d85, Vec::new()));
1352 let (space, page) = (self.space, self.page);
1346 let page = self.page;
13531347 let payload: &[u8] = bytes;
1354 self.apply(|current| {
1355 crate::write::write_revision_with_payloads(
1356 current,
1357 space,
1358 &[(payload_guid, payload)],
1359 |raw| {
1360 let mut changed = BTreeMap::new();
1361 let file = PropertyObject::file(file_id, &reference, extension)?;
1362 changed.insert(file_id, file);
1363 let mut picture = PropertyObject {
1364 jcid: 0x60011,
1365 bytes: crate::create::properties(&values)?,
1366 global_ids: std::sync::Arc::new(BTreeMap::from([(0, image_id.guid)])),
1367 };
1368 picture.reference(image_id)?;
1369 let container = picture.reference(file_id)?;
1370 picture.set(&[(0x20001c3f, &container)])?;
1371 changed.insert(image_id, picture);
1372 match holder {
1373 Some(holder) => {
1374 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1375 let content = object.reference(image_id)?;
1376 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;
1377 changed.insert(holder, object);
1378 }
1379 None => {
1380 let mut object = PropertyObject::from_object(&raw.objects[&page])?;
1381 let mut children = page_children(&object)?;
1382 children.extend(object.reference(image_id)?);
1383 object.set(&[(0x24001c20, &children), (0x14001d7a, &modified)])?;
1384 changed.insert(page, object);
1385 }
1386 }
1387 Ok(changed)
1388 },
1389 )
1348 self.write_with(&[(payload_guid, payload)], |active| {
1349 let raw = &active.live.revision;
1350 let mut changed = BTreeMap::new();
1351 let file = PropertyObject::file(file_id, &reference, extension)?;
1352 changed.insert(file_id, file);
1353 let mut picture = PropertyObject {
1354 jcid: 0x60011,
1355 bytes: crate::create::properties(&values)?,
1356 global_ids: std::sync::Arc::new(BTreeMap::from([(0, image_id.guid)])),
1357 };
1358 picture.reference(image_id)?;
1359 let container = picture.reference(file_id)?;
1360 picture.set(&[(0x20001c3f, &container)])?;
1361 changed.insert(image_id, picture);
1362 match holder {
1363 Some(holder) => {
1364 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1365 let content = object.reference(image_id)?;
1366 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;
1367 changed.insert(holder, object);
1368 }
1369 None => {
1370 let mut object = PropertyObject::from_object(&raw.objects[&page])?;
1371 let mut children = page_children(&object)?;
1372 children.extend(object.reference(image_id)?);
1373 object.set(&[(0x24001c20, &children), (0x14001d7a, &modified)])?;
1374 changed.insert(page, object);
1375 }
1376 }
1377 Ok(changed)
13901378 })
13911379 }
13921380
......@@ -1434,18 +1422,17 @@ impl Lowering<'_> {
14341422 None => removed.extend([0x140034cd, 0x140034ce]),
14351423 }
14361424 }
1437 let (space, object) = (self.space, self.id(attachment.id));
1438 self.apply(|current| {
1439 crate::write::write_revision(current, space, |raw| {
1440 let mut node = PropertyObject::from_object(&raw.objects[&object])?;
1441 node.remove(&removed)?;
1442 let values: Vec<(u32, &[u8])> = values
1443 .iter()
1444 .map(|(id, bytes)| (*id, bytes.as_slice()))
1445 .collect();
1446 node.set(&values)?;
1447 Ok(BTreeMap::from([(object, node)]))
1448 })
1425 let object = self.id(attachment.id);
1426 self.write(|active| {
1427 let raw = &active.live.revision;
1428 let mut node = PropertyObject::from_object(&raw.objects[&object])?;
1429 node.remove(&removed)?;
1430 let values: Vec<(u32, &[u8])> = values
1431 .iter()
1432 .map(|(id, bytes)| (*id, bytes.as_slice()))
1433 .collect();
1434 node.set(&values)?;
1435 Ok(BTreeMap::from([(object, node)]))
14491436 })
14501437 }
14511438
......@@ -1466,18 +1453,17 @@ impl Lowering<'_> {
14661453 None => removed.push(0x1c001e58),
14671454 }
14681455 }
1469 let (space, object) = (self.space, self.id(image.id));
1470 self.apply(|current| {
1471 crate::write::write_revision(current, space, |raw| {
1472 let mut picture = PropertyObject::from_object(&raw.objects[&object])?;
1473 picture.remove(&removed)?;
1474 let values: Vec<(u32, &[u8])> = values
1475 .iter()
1476 .map(|(id, bytes)| (*id, bytes.as_slice()))
1477 .collect();
1478 picture.set(&values)?;
1479 Ok(BTreeMap::from([(object, picture)]))
1480 })
1456 let object = self.id(image.id);
1457 self.write(|active| {
1458 let raw = &active.live.revision;
1459 let mut picture = PropertyObject::from_object(&raw.objects[&object])?;
1460 picture.remove(&removed)?;
1461 let values: Vec<(u32, &[u8])> = values
1462 .iter()
1463 .map(|(id, bytes)| (*id, bytes.as_slice()))
1464 .collect();
1465 picture.set(&values)?;
1466 Ok(BTreeMap::from([(object, picture)]))
14811467 })
14821468 }
14831469
......@@ -1540,8 +1526,7 @@ impl Lowering<'_> {
15401526 };
15411527 let insertion =
15421528 Insertion::paragraph(self.id(*container), anchor, text, self.author)?;
1543 let space = self.space;
1544 self.apply(|image| insertion.apply(image, space))?;
1529 self.write(|active| insertion.changes(active))?;
15451530 self.alias.insert(*id, insertion.object());
15461531 if let Some(text_id) = text_id {
15471532 self.alias.insert(text_id, insertion.text_object());
......@@ -1549,8 +1534,7 @@ impl Lowering<'_> {
15491534 } else if !kept.contains(id) {
15501535 let edit =
15511536 TreeEdit::move_to(self.id(*id), self.id(*container), anchor, self.author)?;
1552 let space = self.space;
1553 self.apply(|image| edit.apply(image, space))?;
1537 self.write(|active| edit.changes(active))?;
15541538 }
15551539 next = Some(*id);
15561540 }
......@@ -1607,19 +1591,17 @@ impl Lowering<'_> {
16071591 values.push((0x1c001c12, measurement_bytes(&cell.indents, 4)?));
16081592 }
16091593 }
1610 let (space, object) = (self.space, self.id(cell.id));
1611 self.apply(|current| {
1612 crate::write::write_revision(current, space, |raw| {
1613 let mut object_properties =
1614 PropertyObject::from_object(&raw.objects[&object])?;
1615 object_properties.remove(&removed)?;
1616 let values: Vec<(u32, &[u8])> = values
1617 .iter()
1618 .map(|(id, bytes)| (*id, bytes.as_slice()))
1619 .collect();
1620 object_properties.set(&values)?;
1621 Ok(BTreeMap::from([(object, object_properties)]))
1622 })
1594 let object = self.id(cell.id);
1595 self.write(|active| {
1596 let raw = &active.live.revision;
1597 let mut object_properties = PropertyObject::from_object(&raw.objects[&object])?;
1598 object_properties.remove(&removed)?;
1599 let values: Vec<(u32, &[u8])> = values
1600 .iter()
1601 .map(|(id, bytes)| (*id, bytes.as_slice()))
1602 .collect();
1603 object_properties.set(&values)?;
1604 Ok(BTreeMap::from([(object, object_properties)]))
16231605 })?;
16241606 }
16251607 }
......@@ -1755,56 +1737,54 @@ impl Lowering<'_> {
17551737 }
17561738 let holder = self.id(*paragraph_id);
17571739 let is_new = previous.is_none();
1758 let space = self.space;
1759 self.apply(|image| {
1760 crate::write::write_revision(image, space, |raw| {
1761 let mut changed: BTreeMap<ExGuid, PropertyObject> = BTreeMap::new();
1762 for (id, jcid, values) in &created {
1763 let mut node = PropertyObject {
1764 jcid: *jcid,
1765 bytes: crate::create::properties(values)?,
1766 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
1767 };
1768 node.reference(*id)?;
1769 changed.insert(*id, node);
1770 }
1771 let mut row_refs = Vec::new();
1772 for (row_id, cells) in &rows {
1773 let mut row = match changed.remove(row_id) {
1774 Some(row) => row,
1775 None => PropertyObject::from_object(&raw.objects[row_id])?,
1776 };
1777 let mut cell_refs = Vec::new();
1778 for cell in cells {
1779 cell_refs.extend_from_slice(&row.reference(*cell)?);
1780 }
1781 row.set(&[(0x24001c20, &cell_refs), (0x14001d7a, &modified)])?;
1782 changed.insert(*row_id, row);
1783 }
1784 let mut table_object = match changed.remove(&table_id) {
1785 Some(object) => object,
1786 None => PropertyObject::from_object(&raw.objects[&table_id])?,
1740 self.write(|active| {
1741 let raw = &active.live.revision;
1742 let mut changed: BTreeMap<ExGuid, PropertyObject> = BTreeMap::new();
1743 for (id, jcid, values) in &created {
1744 let mut node = PropertyObject {
1745 jcid: *jcid,
1746 bytes: crate::create::properties(values)?,
1747 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
17871748 };
1788 for (row_id, _) in &rows {
1789 row_refs.extend_from_slice(&table_object.reference(*row_id)?);
1790 }
1791 table_object.remove(&[0x1c001d7d, 0x08001d5e])?;
1792 table_object.set(
1793 &table_values
1794 .iter()
1795 .map(|(id, bytes)| (*id, bytes.as_slice()))
1796 .collect::<Vec<_>>(),
1797 )?;
1798 table_object.set(&[(0x24001c20, &row_refs)])?;
1799 changed.insert(table_id, table_object);
1800 if is_new {
1801 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1802 let reference = object.reference(table_id)?;
1803 object.set(&[(0x24001c1f, &reference), (0x14001d7a, &modified)])?;
1804 changed.insert(holder, object);
1749 node.reference(*id)?;
1750 changed.insert(*id, node);
1751 }
1752 let mut row_refs = Vec::new();
1753 for (row_id, cells) in &rows {
1754 let mut row = match changed.remove(row_id) {
1755 Some(row) => row,
1756 None => PropertyObject::from_object(&raw.objects[row_id])?,
1757 };
1758 let mut cell_refs = Vec::new();
1759 for cell in cells {
1760 cell_refs.extend_from_slice(&row.reference(*cell)?);
18051761 }
1806 Ok(changed)
1807 })
1762 row.set(&[(0x24001c20, &cell_refs), (0x14001d7a, &modified)])?;
1763 changed.insert(*row_id, row);
1764 }
1765 let mut table_object = match changed.remove(&table_id) {
1766 Some(object) => object,
1767 None => PropertyObject::from_object(&raw.objects[&table_id])?,
1768 };
1769 for (row_id, _) in &rows {
1770 row_refs.extend_from_slice(&table_object.reference(*row_id)?);
1771 }
1772 table_object.remove(&[0x1c001d7d, 0x08001d5e])?;
1773 table_object.set(
1774 &table_values
1775 .iter()
1776 .map(|(id, bytes)| (*id, bytes.as_slice()))
1777 .collect::<Vec<_>>(),
1778 )?;
1779 table_object.set(&[(0x24001c20, &row_refs)])?;
1780 changed.insert(table_id, table_object);
1781 if is_new {
1782 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1783 let reference = object.reference(table_id)?;
1784 object.set(&[(0x24001c1f, &reference), (0x14001d7a, &modified)])?;
1785 changed.insert(holder, object);
1786 }
1787 Ok(changed)
18081788 })?;
18091789 }
18101790 Ok(())
......@@ -1846,8 +1826,7 @@ impl Lowering<'_> {
18461826 continue;
18471827 }
18481828 let edit = TreeEdit::delete(*id, self.author)?;
1849 let space = self.space;
1850 self.apply(|image| edit.apply(image, space))?;
1829 self.write(|active| edit.changes(active))?;
18511830 }
18521831 let kept: BTreeSet<ExGuid> = page_images(new.page)
18531832 .map(|(id, _)| id)
......@@ -1859,8 +1838,7 @@ impl Lowering<'_> {
18591838 {
18601839 if !kept.contains(&id) {
18611840 let edit = TreeEdit::delete(id, self.author)?;
1862 let space = self.space;
1863 self.apply(|image| edit.apply(image, space))?;
1841 self.write(|active| edit.changes(active))?;
18641842 }
18651843 }
18661844 for id in old.outlines.keys() {
......@@ -1869,8 +1847,7 @@ impl Lowering<'_> {
18691847 return Err(invalid("Title outlines cannot be removed"));
18701848 }
18711849 let edit = TreeEdit::delete(*id, self.author)?;
1872 let space = self.space;
1873 self.apply(|image| edit.apply(image, space))?;
1850 self.write(|active| edit.changes(active))?;
18741851 }
18751852 }
18761853 Ok(())
......@@ -1943,56 +1920,54 @@ impl Lowering<'_> {
19431920 ends.truncate(ends.len() - 4);
19441921 let has_objects = sets.iter().any(|set| !set.is_empty());
19451922 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
1946 let space = self.space;
1947 self.apply(|current| {
1948 crate::write::write_revision(current, space, |raw| {
1949 let mut changed = BTreeMap::new();
1950 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
1951 let mut references = Vec::new();
1952 let mut created: Vec<(Values, ExGuid)> = Vec::new();
1953 for values in &styles {
1954 let id = match created.iter().find(|(known, _)| known == values) {
1955 Some((_, id)) => *id,
1956 None => {
1957 let id = ExGuid {
1958 guid: crate::write::fresh_guid()?,
1959 n: 1,
1960 };
1961 let mut style = PropertyObject {
1962 jcid: 0x12004d,
1963 bytes: crate::create::properties(values)?,
1964 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
1965 };
1966 style.reference(id)?;
1967 changed.insert(id, style);
1968 created.push((values.clone(), id));
1969 id
1970 }
1971 };
1972 references.extend(target.reference(id)?);
1973 }
1974 target.remove(&[0x1c003498, 0x40003499])?;
1923 self.write(|active| {
1924 let raw = &active.live.revision;
1925 let mut changed = BTreeMap::new();
1926 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
1927 let mut references = Vec::new();
1928 let mut created: Vec<(Values, ExGuid)> = Vec::new();
1929 for values in &styles {
1930 let id = match created.iter().find(|(known, _)| known == values) {
1931 Some((_, id)) => *id,
1932 None => {
1933 let id = ExGuid {
1934 guid: crate::write::fresh_guid()?,
1935 n: 1,
1936 };
1937 let mut style = PropertyObject {
1938 jcid: 0x12004d,
1939 bytes: crate::create::properties(values)?,
1940 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
1941 };
1942 style.reference(id)?;
1943 changed.insert(id, style);
1944 created.push((values.clone(), id));
1945 id
1946 }
1947 };
1948 references.extend(target.reference(id)?);
1949 }
1950 target.remove(&[0x1c003498, 0x40003499])?;
1951 target.set(&[
1952 (0x1c001c22, &encoded),
1953 (0x1c001e12, &ends),
1954 (0x24001e13, &references),
1955 (0x14001d7a, &modified),
1956 ])?;
1957 if has_objects {
1958 target.set_sets(0x40003499, 0x44000811, &sets)?;
1959 }
1960 if all_math {
1961 // The flags and language marker OneNote's equation editor leaves on
1962 // every equation text object.
19751963 target.set(&[
1976 (0x1c001c22, &encoded),
1977 (0x1c001e12, &ends),
1978 (0x24001e13, &references),
1979 (0x14001d7a, &modified),
1964 (0x10001cfe, &0x7f_u16.to_le_bytes()),
1965 (0x14001c3e, &1u32.to_le_bytes()),
1966 (0x14001c84, &1u32.to_le_bytes()),
19801967 ])?;
1981 if has_objects {
1982 target.set_sets(0x40003499, 0x44000811, &sets)?;
1983 }
1984 if all_math {
1985 // The flags and language marker OneNote's equation editor leaves on
1986 // every equation text object.
1987 target.set(&[
1988 (0x10001cfe, &0x7f_u16.to_le_bytes()),
1989 (0x14001c3e, &1u32.to_le_bytes()),
1990 (0x14001c84, &1u32.to_le_bytes()),
1991 ])?;
1992 }
1993 changed.insert(object, target);
1994 Ok(changed)
1995 })
1968 }
1969 changed.insert(object, target);
1970 Ok(changed)
19961971 })?;
19971972 }
19981973 Ok(())
......@@ -2016,8 +1991,8 @@ impl Lowering<'_> {
20161991 continue;
20171992 }
20181993 let (range, replacement) = text_edit(stored.text.text(), text.text.text())?;
2019 let (space, object) = (self.space, stored.id);
2020 self.apply(|image| crate::replace_text(image, space, object, range, &replacement))?;
1994 let object = stored.id;
1995 self.write(|active| crate::edit::text_changes(active, object, range, &replacement))?;
20211996 }
20221997 Ok(())
20231998 }
......@@ -2132,70 +2107,56 @@ impl Lowering<'_> {
21322107 }
21332108 nodes.push((node_id, values));
21342109 }
2135 let (space, object) = (self.space, self.id(*id));
2136 self.apply(|image| {
2137 let store = Store::parse(image)?;
2138 let index = RevisionIndex::parse(&store)?;
2139 let document = Document::parse(&index)?;
2140 let parents = crate::edit::editable_parents(
2141 document.active(space)?,
2142 &document.pages_in(space)?,
2143 object,
2144 )?;
2110 let object = self.id(*id);
2111 self.write(|active| {
2112 let parents = active.editable_parents(object)?;
21452113 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
2146 crate::write::write_revision(image, space, |raw| {
2147 let mut changed = BTreeMap::new();
2148 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2149 let mut references = Vec::new();
2150 for (list, values) in &nodes {
2151 let mut node = match raw.objects.get(list) {
2152 Some(existing) => {
2153 if existing.jcid != 0x60012 {
2154 return Err(invalid(
2155 "A list definition identity belongs to another object",
2156 ));
2157 }
2158 let mut node = PropertyObject::from_object(existing)?;
2159 node.remove(&[
2160 0x1c001c1a, 0x1c001c52, 0x14001cb7, 0x10001d0e, 0x0c001cc0,
2161 0x1c001c0a, 0x10001c0b, 0x14001c0c, 0x14001c3b, 0x08001c04,
2162 0x08001c05,
2163 ])?;
2164 node
2114 let raw = &active.live.revision;
2115 let mut changed = BTreeMap::new();
2116 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2117 let mut references = Vec::new();
2118 for (list, values) in &nodes {
2119 let mut node = match raw.objects.get(list) {
2120 Some(existing) => {
2121 if existing.jcid != 0x60012 {
2122 return Err(invalid(
2123 "A list definition identity belongs to another object",
2124 ));
21652125 }
2166 None => PropertyObject {
2167 jcid: 0x60012,
2168 bytes: crate::create::properties(&[])?,
2169 global_ids: std::sync::Arc::new(BTreeMap::from([(0, list.guid)])),
2170 },
2171 };
2172 node.set(
2173 &values
2174 .iter()
2175 .map(|(id, bytes)| (*id, bytes.as_slice()))
2176 .collect::<Vec<_>>(),
2177 )?;
2178 node.set(&[(0x14001d7a, &modified)])?;
2179 node.reference(*list)?;
2180 references.extend_from_slice(&target.reference(*list)?);
2181 changed.insert(*list, node);
2182 }
2183 if references.is_empty() {
2184 target.remove(&[0x24001c26])?;
2185 } else {
2186 target.set(&[(0x24001c26, &references)])?;
2187 }
2188 target.set(&[(0x14001d7a, &modified)])?;
2189 changed.insert(object, target);
2190 crate::formatting::touch_ancestors(
2191 raw,
2192 &parents,
2193 object,
2194 &modified,
2195 &mut changed,
2126 let mut node = PropertyObject::from_object(existing)?;
2127 node.remove(&[
2128 0x1c001c1a, 0x1c001c52, 0x14001cb7, 0x10001d0e, 0x0c001cc0,
2129 0x1c001c0a, 0x10001c0b, 0x14001c0c, 0x14001c3b, 0x08001c04,
2130 0x08001c05,
2131 ])?;
2132 node
2133 }
2134 None => PropertyObject {
2135 jcid: 0x60012,
2136 bytes: crate::create::properties(&[])?,
2137 global_ids: std::sync::Arc::new(BTreeMap::from([(0, list.guid)])),
2138 },
2139 };
2140 node.set(
2141 &values
2142 .iter()
2143 .map(|(id, bytes)| (*id, bytes.as_slice()))
2144 .collect::<Vec<_>>(),
21962145 )?;
2197 Ok(changed)
2198 })
2146 node.set(&[(0x14001d7a, &modified)])?;
2147 node.reference(*list)?;
2148 references.extend_from_slice(&target.reference(*list)?);
2149 changed.insert(*list, node);
2150 }
2151 if references.is_empty() {
2152 target.remove(&[0x24001c26])?;
2153 } else {
2154 target.set(&[(0x24001c26, &references)])?;
2155 }
2156 target.set(&[(0x14001d7a, &modified)])?;
2157 changed.insert(object, target);
2158 crate::formatting::touch_ancestors(raw, parents, object, &modified, &mut changed)?;
2159 Ok(changed)
21992160 })?;
22002161 }
22012162 Ok(())
......@@ -2330,51 +2291,42 @@ impl Lowering<'_> {
23302291 }
23312292 sets.push((index, fields));
23322293 }
2333 let space = self.space;
2334 self.apply(|image| {
2335 let store = Store::parse(image)?;
2336 let index = RevisionIndex::parse(&store)?;
2337 let document = Document::parse(&index)?;
2338 let parents = crate::edit::editable_parents(
2339 document.active(space)?,
2340 &document.pages_in(space)?,
2294 self.write(|active| {
2295 let parents = active.editable_parents(object)?;
2296 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
2297 let raw = &active.live.revision;
2298 let mut changed = BTreeMap::new();
2299 for (id, values) in &definitions {
2300 let Some(values) = values else {
2301 continue;
2302 };
2303 let mut node = PropertyObject {
2304 jcid: 0x120043,
2305 bytes: crate::create::properties(values)?,
2306 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
2307 };
2308 node.reference(*id)?;
2309 changed.insert(*id, node);
2310 }
2311 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2312 let mut encoded = Vec::new();
2313 for (index, fields) in &sets {
2314 let reference = target.reference(definitions[*index].0)?;
2315 let mut set = vec![(0x20003488, reference.to_vec())];
2316 set.extend(fields.iter().cloned());
2317 encoded.push(set);
2318 }
2319 target.set_sets(0x40003489, 0x44000811, &encoded)?;
2320 target.set(&[(0x14001d7a, &modified)])?;
2321 changed.insert(object, target);
2322 crate::formatting::touch_ancestors(
2323 raw,
2324 parents,
23412325 object,
2326 &modified,
2327 &mut changed,
23422328 )?;
2343 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
2344 crate::write::write_revision(image, space, |raw| {
2345 let mut changed = BTreeMap::new();
2346 for (id, values) in &definitions {
2347 let Some(values) = values else {
2348 continue;
2349 };
2350 let mut node = PropertyObject {
2351 jcid: 0x120043,
2352 bytes: crate::create::properties(values)?,
2353 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
2354 };
2355 node.reference(*id)?;
2356 changed.insert(*id, node);
2357 }
2358 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2359 let mut encoded = Vec::new();
2360 for (index, fields) in &sets {
2361 let reference = target.reference(definitions[*index].0)?;
2362 let mut set = vec![(0x20003488, reference.to_vec())];
2363 set.extend(fields.iter().cloned());
2364 encoded.push(set);
2365 }
2366 target.set_sets(0x40003489, 0x44000811, &encoded)?;
2367 target.set(&[(0x14001d7a, &modified)])?;
2368 changed.insert(object, target);
2369 crate::formatting::touch_ancestors(
2370 raw,
2371 &parents,
2372 object,
2373 &modified,
2374 &mut changed,
2375 )?;
2376 Ok(changed)
2377 })
2329 Ok(changed)
23782330 })?;
23792331 }
23802332 }
......@@ -2444,25 +2396,24 @@ impl Lowering<'_> {
24442396 }
24452397 Some(values)
24462398 };
2447 let (space, object) = (self.space, self.id(text.id));
2448 self.apply(|image| {
2449 crate::write::write_revision(image, space, |raw| {
2450 let mut changed = BTreeMap::new();
2451 if let Some(values) = &values {
2452 let mut node = PropertyObject {
2453 jcid: 0x12004d,
2454 bytes: crate::create::properties(values)?,
2455 global_ids: std::sync::Arc::new(BTreeMap::from([(0, style_id.guid)])),
2456 };
2457 node.reference(style_id)?;
2458 changed.insert(style_id, node);
2459 }
2460 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2461 let reference = target.reference(style_id)?;
2462 target.set(&[(0x2000342c, &reference)])?;
2463 changed.insert(object, target);
2464 Ok(changed)
2465 })
2399 let object = self.id(text.id);
2400 self.write(|active| {
2401 let raw = &active.live.revision;
2402 let mut changed = BTreeMap::new();
2403 if let Some(values) = &values {
2404 let mut node = PropertyObject {
2405 jcid: 0x12004d,
2406 bytes: crate::create::properties(values)?,
2407 global_ids: std::sync::Arc::new(BTreeMap::from([(0, style_id.guid)])),
2408 };
2409 node.reference(style_id)?;
2410 changed.insert(style_id, node);
2411 }
2412 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2413 let reference = target.reference(style_id)?;
2414 target.set(&[(0x2000342c, &reference)])?;
2415 changed.insert(object, target);
2416 Ok(changed)
24662417 })?;
24672418 }
24682419 Ok(())
......@@ -2531,55 +2482,41 @@ impl Lowering<'_> {
25312482 "Generated title fields cannot be formatted as ordinary text",
25322483 ));
25332484 }
2534 let (space, object) = (self.space, stored.id);
2535 self.apply(|image| {
2536 let store = Store::parse(image)?;
2537 let index = RevisionIndex::parse(&store)?;
2538 let document = Document::parse(&index)?;
2539 let parents = crate::edit::editable_parents(
2540 document.active(space)?,
2541 &document.pages_in(space)?,
2542 object,
2543 )?;
2485 let object = stored.id;
2486 self.write(|active| {
2487 let parents = active.editable_parents(object)?;
25442488 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
2545 crate::write::write_revision(image, space, |raw| {
2546 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2547 target.set(
2548 &values
2489 let raw = &active.live.revision;
2490 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2491 target.set(
2492 &values
2493 .iter()
2494 .map(|(id, bytes)| (*id, bytes.as_slice()))
2495 .collect::<Vec<_>>(),
2496 )?;
2497 if let Some((_, alignment)) = values.iter().find(|(id, _)| *id == 0x0c003477) {
2498 for property in [0x14001c3e, 0x14001c84] {
2499 let fields = PropertySets::parse(&target.bytes)?;
2500 let previous = fields.sets[0]
25492501 .iter()
2550 .map(|(id, bytes)| (*id, bytes.as_slice()))
2551 .collect::<Vec<_>>(),
2552 )?;
2553 if let Some((_, alignment)) = values.iter().find(|(id, _)| *id == 0x0c003477) {
2554 for property in [0x14001c3e, 0x14001c84] {
2555 let fields = PropertySets::parse(&target.bytes)?;
2556 let previous = fields.sets[0]
2557 .iter()
2558 .find(|field| field.id == property)
2559 .map(|field| match field.value {
2560 Value::Bytes(bytes) => bytes
2561 .try_into()
2562 .map(u32::from_le_bytes)
2563 .map_err(|_| invalid("Invalid paragraph layout alignment")),
2564 _ => Err(invalid("Invalid paragraph layout alignment")),
2565 })
2566 .transpose()?
2567 .unwrap_or(0);
2568 let value = (previous & !7) | (u32::from(alignment[0]) + 1);
2569 target.set(&[(property, &value.to_le_bytes())])?;
2570 }
2502 .find(|field| field.id == property)
2503 .map(|field| match field.value {
2504 Value::Bytes(bytes) => bytes
2505 .try_into()
2506 .map(u32::from_le_bytes)
2507 .map_err(|_| invalid("Invalid paragraph layout alignment")),
2508 _ => Err(invalid("Invalid paragraph layout alignment")),
2509 })
2510 .transpose()?
2511 .unwrap_or(0);
2512 let value = (previous & !7) | (u32::from(alignment[0]) + 1);
2513 target.set(&[(property, &value.to_le_bytes())])?;
25712514 }
2572 target.set(&[(0x14001d7a, &modified)])?;
2573 let mut changed = BTreeMap::from([(object, target)]);
2574 crate::formatting::touch_ancestors(
2575 raw,
2576 &parents,
2577 object,
2578 &modified,
2579 &mut changed,
2580 )?;
2581 Ok(changed)
2582 })
2515 }
2516 target.set(&[(0x14001d7a, &modified)])?;
2517 let mut changed = BTreeMap::from([(object, target)]);
2518 crate::formatting::touch_ancestors(raw, parents, object, &modified, &mut changed)?;
2519 Ok(changed)
25832520 })?;
25842521 }
25852522 Ok(())
......@@ -2646,9 +2583,9 @@ impl Lowering<'_> {
26462583 if attributes.is_empty() {
26472584 continue;
26482585 }
2649 let (space, object) = (self.space, stored.id);
2650 self.apply(|image| {
2651 crate::formatting::format_text(image, space, object, range, &attributes)
2586 let object = stored.id;
2587 self.write(|active| {
2588 crate::formatting::format_changes(active, object, range, &attributes)
26522589 })?;
26532590 }
26542591 }
......@@ -2664,9 +2601,9 @@ impl Lowering<'_> {
26642601 .get(&self.id(*id))
26652602 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?;
26662603 if stored.collapsed != paragraph.collapsed {
2667 let (space, object) = (self.space, self.id(*id));
2604 let object = self.id(*id);
26682605 let edit = OutlineEdit::Collapsed(paragraph.collapsed);
2669 self.apply(|image| edit.apply(image, space, object))?;
2606 self.write(|active| edit.changes(active, object))?;
26702607 }
26712608 }
26722609 for (id, outline) in &new.outlines {
......@@ -2677,7 +2614,7 @@ impl Lowering<'_> {
26772614 .outlines
26782615 .get(&self.id(*id))
26792616 .ok_or_else(|| invalid("An outline is missing after text edits"))?;
2680 let (space, object) = (self.space, self.id(*id));
2617 let object = self.id(*id);
26812618 // A new outline takes the model's indentation table (a copied outline keeps
26822619 // its levels' offsets); a stored table stays as it is.
26832620 if !old.outlines.contains_key(id)
......@@ -2685,12 +2622,11 @@ impl Lowering<'_> {
26852622 && outline.indents != stored.indents
26862623 {
26872624 let indents = measurement_bytes(&outline.indents, 4)?;
2688 self.apply(|current| {
2689 crate::write::write_revision(current, space, |raw| {
2690 let mut node = PropertyObject::from_object(&raw.objects[&object])?;
2691 node.set(&[(0x1c001c12, &indents)])?;
2692 Ok(BTreeMap::from([(object, node)]))
2693 })
2625 self.write(|active| {
2626 let raw = &active.live.revision;
2627 let mut node = PropertyObject::from_object(&raw.objects[&object])?;
2628 node.set(&[(0x1c001c12, &indents)])?;
2629 Ok(BTreeMap::from([(object, node)]))
26942630 })?;
26952631 }
26962632 if (outline.layout.x, outline.layout.y) != (stored.layout.x, stored.layout.y) {
......@@ -2698,7 +2634,7 @@ impl Lowering<'_> {
26982634 return Err(invalid("An outline position needs both coordinates"));
26992635 };
27002636 let edit = OutlineEdit::Position { x, y };
2701 self.apply(|image| edit.apply(image, space, object))?;
2637 self.write(|active| edit.changes(active, object))?;
27022638 }
27032639 if (outline.layout.max_width, outline.layout.width_set_by_user)
27042640 != (stored.layout.max_width, stored.layout.width_set_by_user)
......@@ -2711,7 +2647,7 @@ impl Lowering<'_> {
27112647 points,
27122648 user_set: outline.layout.width_set_by_user == Some(true),
27132649 };
2714 self.apply(|image| edit.apply(image, space, object))?;
2650 self.write(|active| edit.changes(active, object))?;
27152651 }
27162652 }
27172653 Ok(())
......@@ -2755,6 +2691,9 @@ fn kept_set(
27552691 "Images and unsupported objects cannot be reordered",
27562692 ));
27572693 }
2694 if sequence.windows(2).all(|pair| pair[0].1 < pair[1].1) {
2695 return Ok(sequence.into_iter().map(|(id, _)| id).collect());
2696 }
27582697 // Longest increasing subsequence over `after` positions, forced through immovable items.
27592698 let n = sequence.len();
27602699 let mut best = vec![1usize; n];
......@@ -2926,12 +2865,24 @@ fn attributes(current: &Format, target: &Format, fresh: bool) -> Result<Vec<Text
29262865 Ok(out)
29272866}
29282867
2929/// Rewrites every revision the typed writers appended as one transaction on `source`,
2930/// renaming writer-allocated identities to the model's. A protected `source` takes the
2931/// revisions its plaintext twin gained.
2868/// Payload identities the file-data store of `store` declares, in order.
2869pub(crate) fn declared_payloads(store: &Store<'_>) -> Vec<[u8; 16]> {
2870 store
2871 .lists
2872 .values()
2873 .flat_map(|list| &list.nodes)
2874 .filter(|node| node.id == 0x94)
2875 .filter_map(|node| node.payload.get(..16).and_then(|g| g.try_into().ok()))
2876 .collect()
2877}
2878
2879/// Writes the spaces the typed writers `edited` as one transaction on the validated
2880/// `source`, renaming writer-allocated identities to the model's and embedding the
2881/// `payloads` it lacks. A protected `source` takes the revisions its plaintext twin gained.
29322882pub(crate) fn squash(
2933 source: &[u8],
2934 applied: &[u8],
2883 source: &RevisionIndex<'_>,
2884 edited: &[(ExGuid, &crate::ResolvedRevision<'_>)],
2885 payloads: &[([u8; 16], &[u8])],
29352886 alias: &BTreeMap<ExGuid, ExGuid>,
29362887 protection: Option<&dyn crate::write::Protection>,
29372888) -> Result<Vec<u8>, Error> {
......@@ -2939,44 +2890,19 @@ pub(crate) fn squash(
29392890 .iter()
29402891 .map(|(model, image)| (*image, *model))
29412892 .collect();
2942 let declared = |store: &Store<'_>| -> Vec<[u8; 16]> {
2943 store
2944 .lists
2945 .values()
2946 .flat_map(|list| &list.nodes)
2947 .filter(|node| node.id == 0x94)
2948 .filter_map(|node| node.payload.get(..16).and_then(|g| g.try_into().ok()))
2949 .collect()
2950 };
2951 let existing = declared(&Store::parse(source)?);
2952 let applied_store = Store::parse(applied)?;
2953 let applied_index = RevisionIndex::parse(&applied_store)?;
2954 // Payloads the typed edits embedded travel into the squashed transaction as well.
2955 let mut payloads = Vec::new();
2956 for guid in declared(&applied_store) {
2957 if !existing.contains(&guid) {
2958 payloads.push((guid, applied_store.file_data(guid)?));
2959 }
2960 }
2893 let existing = declared_payloads(source.store);
2894 let payloads: Vec<_> = payloads
2895 .iter()
2896 .filter(|(guid, _)| !existing.contains(guid))
2897 .copied()
2898 .collect();
29612899 let edit = |index: &RevisionIndex<'_>| {
29622900 let mut changes = BTreeMap::new();
2963 for sid in applied_index.spaces.keys() {
2964 let Some(space) = index.spaces.get(sid) else {
2965 // The twin's scaffold spaces are not the section's.
2966 if protection.is_some() {
2967 continue;
2968 }
2969 return Err(invalid("Page edits cannot create object spaces"));
2970 };
2971 let after_rid = applied_index.active(*sid)?;
2972 if space.labels.get(&(ExGuid::default(), 1)) == Some(&after_rid) {
2973 continue;
2974 }
2901 for (sid, after) in edited {
29752902 let before = match protection {
29762903 Some(protection) => protection.resolve(*sid, index.active(*sid)?)?,
29772904 None => index.resolve_active(*sid)?,
29782905 };
2979 let after = applied_index.resolve(*sid, after_rid)?;
29802906 if before.roots != after.roots {
29812907 return Err(invalid("Page edits cannot change revision roots"));
29822908 }
......@@ -2994,7 +2920,7 @@ pub(crate) fn squash(
29942920 {
29952921 let entries = match object.data {
29962922 ObjectData::Properties(bytes) => crate::write::table_entries(bytes)?,
2997 _ => BTreeSet::new(),
2923 _ => Vec::new(),
29982924 };
29992925 if entries
30002926 .iter()
......@@ -3044,12 +2970,8 @@ pub(crate) fn squash(
30442970 }
30452971 Ok(changes)
30462972 };
3047 let validate = protection.is_none();
3048 let output = crate::write::build(source, &payloads, protection, validate, edit)?;
3049 // The parsed images are released before the result is parsed.
3050 drop(applied_index);
3051 drop(applied_store);
3052 crate::write::check(&output, validate)?;
2973 let output = crate::write::build_on(source, &payloads, protection, edit)?;
2974 crate::write::check(&output, protection.is_none())?;
30532975 Ok(output)
30542976}
30552977
......@@ -3076,3 +2998,81 @@ fn remap(object: &mut PropertyObject, rename: &BTreeMap<ExGuid, ExGuid>) -> Resu
30762998 }
30772999 Ok(())
30783000}
3001
3002#[cfg(test)]
3003mod tests {
3004 use super::*;
3005 use crate::page::{PageParagraph, Paragraph, TextObject, text::new_id};
3006
3007 /// A new page in a new section and the page with `count` paragraphs added in an outline.
3008 fn generated(count: usize) -> (Vec<u8>, ExGuid, Page) {
3009 let section = crate::create_section("big.one", "First", "Author").unwrap();
3010 let creation = crate::PageCreation::new(None, Some("Big"), "Author").unwrap();
3011 let source = crate::PreparedEdit::create_page(&section, &creation)
3012 .unwrap()
3013 .as_bytes()
3014 .to_vec();
3015 let space = creation.space();
3016 let store = Store::parse(&source).unwrap();
3017 let index = RevisionIndex::parse(&store).unwrap();
3018 let mut page =
3019 Page::from_space(&crate::document::Document::parse(&index).unwrap(), space).unwrap();
3020 let paragraph = |i: usize| PageParagraph {
3021 id: new_id().unwrap(),
3022 parent: None,
3023 level: 1,
3024 style: None,
3025 format: Format::default(),
3026 content: ParagraphContent::Text(TextObject {
3027 id: new_id().unwrap(),
3028 date_field: None,
3029 text: Paragraph::new(format!("Paragraph {i}"), Format::default()),
3030 tags: Vec::new(),
3031 }),
3032 lists: Vec::new(),
3033 tags: Vec::new(),
3034 media: Default::default(),
3035 collapsed: false,
3036 };
3037 page.objects.push(PageObject::Outline(Outline {
3038 id: new_id().unwrap(),
3039 title: false,
3040 min_width: None,
3041 layout: crate::document::Layout {
3042 x: Some(36.0),
3043 y: Some(86.4),
3044 ..Default::default()
3045 },
3046 indents: Vec::new(),
3047 paragraphs: (0..count).map(paragraph).collect(),
3048 unsupported: Vec::new(),
3049 }));
3050 (source, space, page)
3051 }
3052
3053 #[test]
3054 fn a_page_write_builds_one_revision_however_many_paragraphs_it_adds() {
3055 for count in [10, 40] {
3056 let (source, space, after) = generated(count);
3057 let builds = crate::write::BUILDS.with(std::cell::Cell::get);
3058 let written = write_page(&source, space, &after, "Author").unwrap();
3059 assert_eq!(crate::write::BUILDS.with(std::cell::Cell::get) - builds, 1);
3060 let store = Store::parse(&written).unwrap();
3061 let index = RevisionIndex::parse(&store).unwrap();
3062 let read = Page::from_space(&crate::document::Document::parse(&index).unwrap(), space)
3063 .unwrap();
3064 let texts = |page: &Page| -> Vec<String> {
3065 page.objects
3066 .iter()
3067 .filter_map(|object| match object {
3068 PageObject::Outline(outline) => Some(outline),
3069 _ => None,
3070 })
3071 .flat_map(|outline| &outline.paragraphs)
3072 .filter_map(|paragraph| Some(paragraph.text()?.text.text().to_owned()))
3073 .collect()
3074 };
3075 assert_eq!(texts(&read), texts(&after));
3076 }
3077 }
3078}
crates/onestore/src/paragraph.rs+31-39
......@@ -1,9 +1,10 @@
11use crate::{
2 Error, ExGuid, Object, ObjectData, PropertySets, RevisionIndex, Store,
2 Error, ExGuid, Object, ObjectData, PropertySets,
3 active::{ActivePage, Changes},
34 create::{current_timestamps, properties, string},
4 document::{Document, Element, Kind},
5 edit::{editable_parents, update_title},
6 write::{PropertyObject, fresh_guid, write_revision_on},
5 document::{Element, Kind},
6 edit::update_title,
7 write::{PropertyObject, fresh_guid},
78};
89use serde::{Deserialize, Serialize};
910use std::{
......@@ -80,20 +81,15 @@ impl ParagraphSplit {
8081 "Choose paragraph text and an author name without NUL",
8182 ));
8283 }
83 let store = Store::parse(source)?;
84 let index = RevisionIndex::parse(&store)?;
85 index.validate_current()?;
86 let mut document = Document::parse(&index)?;
87 let pages = document.pages_in(space)?;
88 let [_] = pages.as_slice() else {
84 crate::active::write(source, space, |active| self.changes(active))
85 }
86
87 pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> {
88 let [_] = active.pages.as_slice() else {
8989 return Err(invalid("Splitting requires a single active page"));
9090 };
91 let view = document
92 .spaces
93 .remove(&space)
94 .and_then(crate::document::Space::into_active)
95 .ok_or_else(|| invalid("The active page is unavailable"))?;
96 let parents = editable_parents(&view, &pages, self.text)?;
91 let view = &active.view;
92 let parents = active.editable_parents(self.text)?;
9793 let [paragraph] = parents
9894 .get(&self.text)
9995 .map(Vec::as_slice)
......@@ -138,8 +134,8 @@ impl ParagraphSplit {
138134 }
139135 pending.extend(parents.get(&id).into_iter().flatten().copied());
140136 }
141 let raw = index.resolve_active(space)?;
142 let (text, runs) = ordinary_text(&view, &raw, self.text)?;
137 let raw = &active.live.revision;
138 let (text, runs) = ordinary_text(view, raw, self.text)?;
143139 let node = &view.nodes[&self.text];
144140 let length = u32::try_from(text.encode_utf16().count())
145141 .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?;
......@@ -251,8 +247,8 @@ impl ParagraphSplit {
251247 };
252248 object.set(&[(0x14001d7a, &modified)])?;
253249 }
254 update_title(&store, &raw, view, &pages, &mut changed)?;
255 write_revision_on(&index, space, |_| Ok(changed))
250 update_title(active, view.clone(), &mut changed)?;
251 Ok(changed)
256252 }
257253}
258254
......@@ -406,21 +402,17 @@ impl ParagraphJoin {
406402 "Choose two distinct text objects and an author name without NUL",
407403 ));
408404 }
409 let store = Store::parse(source)?;
410 let index = RevisionIndex::parse(&store)?;
411 index.validate_current()?;
412 let mut document = Document::parse(&index)?;
413 let pages = document.pages_in(space)?;
405 crate::active::write(source, space, |active| self.changes(active))
406 }
407
408 pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> {
409 let pages = &active.pages;
414410 if pages.len() != 1 {
415411 return Err(invalid("Joining requires a single active page"));
416412 }
417 let view = document
418 .spaces
419 .remove(&space)
420 .and_then(crate::document::Space::into_active)
421 .ok_or_else(|| invalid("The active page is unavailable"))?;
422 let parents = editable_parents(&view, &pages, self.left)?;
423 editable_parents(&view, &pages, self.right)?;
413 let view = &active.view;
414 let parents = active.editable_parents(self.left)?;
415 active.editable_parents(self.right)?;
424416 let parent = |id| -> Result<ExGuid, Error> {
425417 let [parent] = parents.get(&id).map(Vec::as_slice).unwrap_or_default() else {
426418 return Err(invalid("Select text with a unique editable parent"));
......@@ -482,9 +474,9 @@ impl ParagraphJoin {
482474 "Joining these child indentation levels requires a hierarchy edit",
483475 ));
484476 }
485 let raw = index.resolve_active(space)?;
486 let (a, a_runs) = ordinary_text(&view, &raw, self.left)?;
487 let (b, b_runs) = ordinary_text(&view, &raw, self.right)?;
477 let raw = &active.live.revision;
478 let (a, a_runs) = ordinary_text(view, raw, self.left)?;
479 let (b, b_runs) = ordinary_text(view, raw, self.right)?;
488480 let length = u32::try_from(a.encode_utf16().count())
489481 .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?;
490482 let right_length = u32::try_from(b.encode_utf16().count())
......@@ -531,8 +523,8 @@ impl ParagraphJoin {
531523 references.extend_from_slice(&target.reference(id)?);
532524 ends.extend_from_slice(&run.end.to_le_bytes());
533525 }
534 let left_base = character_properties(&view, &raw, self.left)?;
535 let right_base = character_properties(&view, &raw, self.right)?;
526 let left_base = character_properties(view, raw, self.left)?;
527 let right_base = character_properties(view, raw, self.right)?;
536528 for (i, run) in b_runs.iter().enumerate() {
537529 if run.start == run.end && i != b_runs.len() - 1 {
538530 continue;
......@@ -681,8 +673,8 @@ impl ParagraphJoin {
681673 object.set(&[(0x14001d7a, &modified)])?;
682674 pending.extend(parents.get(&id).into_iter().flatten().copied());
683675 }
684 update_title(&store, &raw, view, &pages, &mut changed)?;
685 write_revision_on(&index, space, |_| Ok(changed))
676 update_title(active, view.clone(), &mut changed)?;
677 Ok(changed)
686678 }
687679}
688680
crates/onestore/src/protected/mod.rs+28-7
......@@ -407,15 +407,36 @@ pub(crate) fn write_page(
407407 }
408408 let twin = unlocked.twin()?;
409409 let applied = Zeroizing::new(crate::page::write::write_page(&twin, space, page, author)?);
410 // Squash skips the spaces the source lacks as the twin's scaffold; the writers added none.
411 let spaces = |image: &[u8]| -> Result<Vec<ExGuid>> {
412 let store = crate::Store::parse(image)?;
413 Ok(RevisionIndex::parse(&store)?.spaces.into_keys().collect())
414 };
415 if spaces(&twin)? != spaces(&applied)? {
410 let applied_store = crate::Store::parse(&applied)?;
411 let applied_index = RevisionIndex::parse(&applied_store)?;
412 let twin_store = crate::Store::parse(&twin)?;
413 if RevisionIndex::parse(&twin_store)?
414 .spaces
415 .keys()
416 .ne(applied_index.spaces.keys())
417 {
416418 return Err(invalid("Page edits cannot create object spaces"));
417419 }
418 let written = crate::page::write::squash(source, &applied, &BTreeMap::new(), Some(&unlocked))?;
420 // The twin's scaffold spaces are not the section's.
421 let mut edited = Vec::new();
422 for (sid, space) in &index.spaces {
423 let rid = applied_index.active(*sid)?;
424 if space.labels.get(&(ExGuid::default(), 1)) != Some(&rid) {
425 edited.push((*sid, applied_index.resolve(*sid, rid)?));
426 }
427 }
428 let edited: Vec<_> = edited.iter().map(|(sid, after)| (*sid, after)).collect();
429 let payloads = crate::page::write::declared_payloads(&applied_store)
430 .into_iter()
431 .map(|guid| Ok((guid, applied_store.file_data(guid)?)))
432 .collect::<std::result::Result<Vec<_>, crate::Error>>()?;
433 let written = crate::page::write::squash(
434 &index,
435 &edited,
436 &payloads,
437 &BTreeMap::new(),
438 Some(&unlocked),
439 )?;
419440 let store = crate::Store::parse(&written)?;
420441 UnlockedSection::open(&RevisionIndex::parse(&store)?, password, Limits::default())?;
421442 Ok(written)
crates/onestore/src/tree.rs+22-25
......@@ -1,9 +1,10 @@
11use crate::{
2 Error, ExGuid, RevisionIndex, Store,
2 Error, ExGuid,
3 active::{ActivePage, Changes},
34 create::{current_timestamps, properties, string},
4 document::{Document, Kind, Revision},
5 document::{Kind, Revision},
56 edit::{editable_parents, update_title},
6 write::{PropertyObject, fresh_guid, write_revision_on},
7 write::{PropertyObject, fresh_guid},
78};
89use serde::{Deserialize, Serialize};
910use std::{
......@@ -96,21 +97,17 @@ impl TreeEdit {
9697
9798 pub(crate) fn apply(&self, source: &[u8], space: ExGuid) -> Result<Vec<u8>, Error> {
9899 self.validate()?;
99 let store = Store::parse(source)?;
100 let index = RevisionIndex::parse(&store)?;
101 index.validate_current()?;
102 let mut document = Document::parse(&index)?;
103 let pages = document.pages_in(space)?;
100 crate::active::write(source, space, |active| self.changes(active))
101 }
102
103 pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> {
104 let pages = &active.pages;
104105 let [page] = pages.as_slice() else {
105106 return Err(invalid("Tree editing requires a single active page"));
106107 };
107 let mut view = document
108 .spaces
109 .remove(&space)
110 .and_then(crate::document::Space::into_active)
111 .unwrap();
112 let parents = editable_parents(&view, &pages, self.object)?;
113 let path = checked_path(&view, &parents, *page, self.object)?;
108 let mut view = active.view.clone();
109 let parents = active.editable_parents(self.object)?;
110 let path = checked_path(&view, parents, *page, self.object)?;
114111 let source_parent = *path
115112 .get(1)
116113 .ok_or_else(|| invalid("Select a paragraph or ordinary page outline"))?;
......@@ -132,7 +129,7 @@ impl TreeEdit {
132129 let mut changed_ids = BTreeSet::from([source_parent]);
133130 let mut affected = BTreeSet::from([self.object]);
134131 if let Placement::Move { parent, before } = self.placement {
135 if checked_path(&view, &parents, *page, parent)?.contains(&self.object) {
132 if checked_path(&view, parents, *page, parent)?.contains(&self.object) {
136133 return Err(invalid("A subtree cannot move inside itself"));
137134 }
138135 let destination = &view.nodes[&parent];
......@@ -143,7 +140,7 @@ impl TreeEdit {
143140 }
144141 let previous = destination.children.clone();
145142 if parent == source_parent && before == Some(self.object) {
146 return Ok(source.to_vec());
143 return Ok(Changes::new());
147144 }
148145 view.nodes
149146 .get_mut(&source_parent)
......@@ -156,7 +153,7 @@ impl TreeEdit {
156153 .unwrap_or(children.len());
157154 children.insert(position, self.object);
158155 if parent == source_parent && *children == previous {
159 return Ok(source.to_vec());
156 return Ok(Changes::new());
160157 }
161158 changed_ids.insert(parent);
162159 } else {
......@@ -187,7 +184,7 @@ impl TreeEdit {
187184 if node.extra[0].iter().any(|field| field.id == 0x08001d0c) {
188185 return Err(invalid("The emptied container cannot be deleted"));
189186 }
190 let path = checked_path(&view, &parents, *page, at)?;
187 let path = checked_path(&view, parents, *page, at)?;
191188 let parent = path[1];
192189 view.nodes
193190 .get_mut(&parent)
......@@ -256,7 +253,7 @@ impl TreeEdit {
256253 if !checked.insert(id) {
257254 continue;
258255 }
259 checked_path(&view, &parents, *page, id)?;
256 checked_path(&view, parents, *page, id)?;
260257 let node = &view.nodes[&id];
261258 if matches!(self.placement, Placement::Delete)
262259 && node.extra[0].iter().any(|field| field.id == 0x08001d0c)
......@@ -273,8 +270,8 @@ impl TreeEdit {
273270 .copied(),
274271 );
275272 }
276 let raw = index.resolve_active(space)?;
277 let active_parents = editable_parents(&view, &pages, *page)?;
273 let raw = &active.live.revision;
274 let active_parents = editable_parents(&view, pages, *page)?;
278275 let modified = current_timestamps()?.0.to_le_bytes();
279276 let mut changed = BTreeMap::new();
280277 let author_id = ExGuid {
......@@ -370,8 +367,8 @@ impl TreeEdit {
370367 object.set(&[(0x20001d79, &author), (0x14001d7a, &modified)])?;
371368 changed.insert(self.object, object);
372369 }
373 update_title(&store, &raw, view, &pages, &mut changed)?;
374 write_revision_on(&index, space, |_| Ok(changed))
370 update_title(active, view, &mut changed)?;
371 Ok(changed)
375372 }
376373}
377374
......@@ -417,7 +414,7 @@ fn checked_path(
417414mod tests {
418415 use super::*;
419416 use crate::write::write_revision;
420 use crate::{Insertion, PreparedEdit};
417 use crate::{Insertion, PreparedEdit, RevisionIndex, Store, document::Document};
421418
422419 #[test]
423420 fn a_retained_move_intent_can_reuse_its_immutable_author_after_another_move() {
crates/onestore/src/write.rs+549-233
......@@ -105,8 +105,8 @@ fn compact(id: ExGuid, table: &BTreeMap<u32, [u8; 16]>) -> Result<[u8; 4]> {
105105}
106106
107107/// The global id table entries a property object's references name.
108pub(crate) fn table_entries(bytes: &[u8]) -> Result<BTreeSet<u32>> {
109 let mut entries = BTreeSet::new();
108pub(crate) fn table_entries(bytes: &[u8]) -> Result<Vec<u32>> {
109 let mut entries = Vec::new();
110110 for property in PropertySets::parse(bytes)?.sets.iter().flatten() {
111111 if let Value::References { compact_ids, .. } = property.value {
112112 entries.extend(
......@@ -340,36 +340,42 @@ pub(crate) fn replace_objects(
340340 space: ExGuid,
341341 edits: &[ObjectEdit<'_>],
342342) -> Result<Vec<u8>> {
343 write_revision_on(index, space, |revision| {
344 let mut changed = BTreeMap::new();
345 for edit in edits {
346 if changed.contains_key(&edit.object) {
347 return Err(Error {
348 offset: 0,
349 message: "Duplicate object edit",
350 });
351 }
352 let object = revision.objects.get(&edit.object).ok_or(Error {
343 write_revision_on(index, space, |revision| patched(revision, edits))
344}
345
346/// Objects of `revision` with `edits` applied.
347pub(crate) fn patched(
348 revision: &crate::ResolvedRevision<'_>,
349 edits: &[ObjectEdit<'_>],
350) -> Result<BTreeMap<ExGuid, PropertyObject>> {
351 let mut changed = BTreeMap::new();
352 for edit in edits {
353 if changed.contains_key(&edit.object) {
354 return Err(Error {
353355 offset: 0,
354 message: "Object is absent from the active revision",
355 })?;
356 let ObjectData::Properties(blob) = object.data else {
357 return Err(Error {
358 offset: 0,
359 message: "Object does not contain editable properties",
360 });
361 };
362 changed.insert(
363 edit.object,
364 PropertyObject {
365 jcid: object.jcid,
366 bytes: patch_properties(blob, edit.updates, edit.inserts, &[])?,
367 global_ids: Arc::clone(&object.global_ids),
368 },
369 );
356 message: "Duplicate object edit",
357 });
370358 }
371 Ok(changed)
372 })
359 let object = revision.objects.get(&edit.object).ok_or(Error {
360 offset: 0,
361 message: "Object is absent from the active revision",
362 })?;
363 let ObjectData::Properties(blob) = object.data else {
364 return Err(Error {
365 offset: 0,
366 message: "Object does not contain editable properties",
367 });
368 };
369 changed.insert(
370 edit.object,
371 PropertyObject {
372 jcid: object.jcid,
373 bytes: patch_properties(blob, edit.updates, edit.inserts, &[])?,
374 global_ids: Arc::clone(&object.global_ids),
375 },
376 );
377 }
378 Ok(changed)
373379}
374380
375381/// The object type OneNote gives embedded picture payload declarations; embedded files
......@@ -377,10 +383,57 @@ pub(crate) fn replace_objects(
377383pub(crate) const FILE_DATA_JCID: u32 = 0x80039;
378384pub(crate) const EMBEDDED_FILE_JCID: u32 = 0x80036;
379385
386/// The reference and extension of a file-data declaration built by `PropertyObject::file`.
387fn file_declaration(bytes: &[u8]) -> Result<(&[u8], &[u8])> {
388 let malformed = || Error {
389 offset: 0,
390 message: "Malformed file-data declaration",
391 };
392 let mut rest = bytes;
393 let mut parts = Vec::new();
394 for _ in 0..2 {
395 let length = usize::try_from(u32::from_le_bytes(
396 rest.get(..4).ok_or_else(malformed)?.try_into().unwrap(),
397 ))
398 .map_err(|_| malformed())?;
399 let bytes = rest.get(4..4 + length).ok_or_else(malformed)?;
400 parts.push(bytes);
401 rest = &rest[4 + length..];
402 }
403 if !rest.is_empty() {
404 return Err(malformed());
405 }
406 Ok((parts[0], parts[1]))
407}
408
409/// An object of type `jcid` stored as `bytes`, as a revision declares it.
410pub(crate) fn declared(
411 jcid: u32,
412 bytes: &[u8],
413 global_ids: Arc<BTreeMap<u32, [u8; 16]>>,
414) -> Result<crate::Object<'_>> {
415 let data = if is_file_declaration(jcid) {
416 let (reference, extension) = file_declaration(bytes)?;
417 ObjectData::File {
418 reference,
419 extension,
420 }
421 } else {
422 ObjectData::Properties(bytes)
423 };
424 Ok(crate::Object {
425 jcid,
426 reference_count: 0,
427 data,
428 global_ids,
429 })
430}
431
380432fn is_file_declaration(jcid: u32) -> bool {
381433 jcid == FILE_DATA_JCID || jcid == EMBEDDED_FILE_JCID
382434}
383435
436#[derive(Clone)]
384437pub(crate) struct PropertyObject {
385438 pub jcid: u32,
386439 pub bytes: Vec<u8>,
......@@ -426,29 +479,6 @@ impl PropertyObject {
426479 })
427480 }
428481
429 /// The reference and extension of a file-data declaration built by `file`.
430 fn file_declaration(&self) -> Result<(&[u8], &[u8])> {
431 let malformed = || Error {
432 offset: 0,
433 message: "Malformed file-data declaration",
434 };
435 let mut rest = self.bytes.as_slice();
436 let mut parts = Vec::new();
437 for _ in 0..2 {
438 let length = usize::try_from(u32::from_le_bytes(
439 rest.get(..4).ok_or_else(malformed)?.try_into().unwrap(),
440 ))
441 .map_err(|_| malformed())?;
442 let bytes = rest.get(4..4 + length).ok_or_else(malformed)?;
443 parts.push(bytes);
444 rest = &rest[4 + length..];
445 }
446 if !rest.is_empty() {
447 return Err(malformed());
448 }
449 Ok((parts[0], parts[1]))
450 }
451
452482 pub fn from_object(object: &crate::Object<'_>) -> Result<Self> {
453483 let ObjectData::Properties(bytes) = object.data else {
454484 return Err(Error {
......@@ -735,6 +765,440 @@ pub(crate) trait Protection {
735765 fn seal_file(&self, clear: &[u8]) -> Vec<u8>;
736766}
737767
768/// The global-ID entries a group of objects declared together uses: each object's own
769/// and those its properties reference. A section's group stores only these.
770fn used_entries<'o>(
771 table: &BTreeMap<u32, [u8; 16]>,
772 objects: impl IntoIterator<Item = (ExGuid, &'o crate::Object<'o>)>,
773) -> Result<Vec<u32>> {
774 let mut used = Vec::new();
775 for (id, object) in objects {
776 used.push(u32::from_le_bytes(compact(id, table)?) >> 8);
777 if let ObjectData::Properties(bytes) = object.data {
778 used.extend(table_entries(bytes)?);
779 }
780 }
781 used.sort_unstable();
782 used.dedup();
783 Ok(used)
784}
785
786/// A space's revision as appending a revision and reading it back leaves it.
787pub(crate) struct LiveRevision<'a> {
788 pub revision: crate::ResolvedRevision<'a>,
789 /// Incoming references of each object reachable from the roots; a root counts once.
790 incoming: BTreeMap<ExGuid, u32>,
791 /// Reachable read-only property objects by type and content, which identical new
792 /// objects alias.
793 readonly: BTreeMap<u32, BTreeMap<&'a [u8], BTreeSet<ExGuid>>>,
794 /// Revisions in the dependency chain, counted to 512; zero before the first.
795 depth: usize,
796}
797
798impl<'a> LiveRevision<'a> {
799 pub(crate) fn new(revision: crate::ResolvedRevision<'a>, depth: usize) -> Result<Self> {
800 let incoming = if depth == 0 {
801 BTreeMap::new()
802 } else {
803 revision.checked_counts()?
804 };
805 let mut readonly = BTreeMap::new();
806 for id in incoming.keys() {
807 index(&mut readonly, *id, &revision.objects[id], true);
808 }
809 Ok(Self {
810 revision,
811 incoming,
812 readonly,
813 depth,
814 })
815 }
816
817 pub(crate) fn is_reachable(&self, id: ExGuid) -> bool {
818 self.incoming.contains_key(&id)
819 }
820
821 /// Validates replacements and drops what the revision would not store: new read-only
822 /// objects identical to reachable or earlier ones, whose references move to those, and
823 /// reachable objects that stay as they are.
824 pub(crate) fn prepare(
825 &self,
826 mut replacements: BTreeMap<ExGuid, PropertyObject>,
827 is_section: bool,
828 ) -> Result<BTreeMap<ExGuid, PropertyObject>> {
829 let revision = &self.revision;
830 for (id, replacement) in &replacements {
831 if let Some(object) = revision.objects.get(id) {
832 if object.jcid & 0x100000 != 0 {
833 return Err(Error {
834 offset: 0,
835 message: "Read-only object requires a new identity",
836 });
837 }
838 if replacement.jcid != object.jcid
839 || !matches!(object.data, ObjectData::Properties(_))
840 {
841 return Err(Error {
842 offset: 0,
843 message: "An existing object's type cannot be changed",
844 });
845 }
846 } else if !is_section && replacement.jcid != 0x20001 {
847 return Err(Error {
848 offset: 0,
849 message: "New objects in a table of contents are its entries",
850 });
851 }
852 if replacement.global_ids.keys().any(|i| *i > 0xffffff) {
853 return Err(Error {
854 offset: 0,
855 message: "Invalid property object declaration",
856 });
857 }
858 compact(*id, &replacement.global_ids)?;
859 if is_file_declaration(replacement.jcid) {
860 if !is_section {
861 return Err(Error {
862 offset: 0,
863 message: "File-data objects require a section file",
864 });
865 }
866 file_declaration(&replacement.bytes)?;
867 } else if replacement.jcid & 0x20000 == 0 {
868 return Err(Error {
869 offset: 0,
870 message: "Invalid property object declaration",
871 });
872 } else {
873 PropertySets::parse(&replacement.bytes)?;
874 }
875 }
876 // Native coalescing of duplicate readonly styles can leave dangling references.
877 let mut aliases = BTreeMap::new();
878 for (id, replacement) in &replacements {
879 if revision.objects.contains_key(id)
880 || replacement.jcid & 0x100000 == 0
881 || PropertySets::parse(&replacement.bytes)?
882 .sets
883 .iter()
884 .flatten()
885 .any(|p| matches!(p.value, Value::References { .. }))
886 {
887 continue;
888 }
889 let existing = self
890 .readonly
891 .get(&replacement.jcid)
892 .and_then(|contents| contents.get(replacement.bytes.as_slice()))
893 .and_then(|ids| ids.first().copied());
894 let existing = existing.or_else(|| {
895 replacements.range(..id).find_map(|(other, object)| {
896 (object.jcid == replacement.jcid && object.bytes == replacement.bytes)
897 .then_some(*aliases.get(other).unwrap_or(other))
898 })
899 });
900 if let Some(existing) = existing {
901 aliases.insert(*id, existing);
902 }
903 }
904 for id in aliases.keys() {
905 replacements.remove(id);
906 }
907 for object in replacements.values_mut() {
908 if is_file_declaration(object.jcid) {
909 continue;
910 }
911 // The compact IDs this object's table gives aliased identities.
912 let targets: Vec<(u32, ExGuid)> = aliases
913 .iter()
914 .filter(|(id, _)| id.n <= 0xff)
915 .filter_map(|(id, existing)| {
916 let (index, _) = object.global_ids.iter().find(|(_, g)| **g == id.guid)?;
917 Some(((index << 8) | id.n, *existing))
918 })
919 .collect();
920 if targets.is_empty() {
921 continue;
922 }
923 let mut remapped = Vec::new();
924 for property in PropertySets::parse(&object.bytes)?.sets.iter().flatten() {
925 if let Value::References {
926 stream: crate::IdStream::Objects,
927 compact_ids,
928 } = property.value
929 {
930 for bytes in compact_ids.chunks_exact(4) {
931 let raw = u32::from_le_bytes(bytes.try_into().unwrap());
932 if let Some((_, existing)) = targets.iter().find(|(id, _)| *id == raw) {
933 let offset = bytes.as_ptr().addr() - object.bytes.as_ptr().addr();
934 remapped.push((offset, *existing));
935 }
936 }
937 }
938 }
939 for (offset, id) in remapped {
940 let reference = object.reference(id)?;
941 object.bytes[offset..offset + 4].copy_from_slice(&reference);
942 }
943 }
944 // Detached objects must pass the final reachability check even when unchanged. Equal
945 // bytes are the same object when the table entries they name agree: stored tables
946 // keep only those.
947 let mut unchanged = Vec::new();
948 for (id, replacement) in &replacements {
949 if let Some(object) = revision.objects.get(id)
950 && self.is_reachable(*id)
951 && object.data == ObjectData::Properties(&replacement.bytes)
952 && (Arc::ptr_eq(&object.global_ids, &replacement.global_ids)
953 || table_entries(&replacement.bytes)?.iter().all(|entry| {
954 object.global_ids.get(entry) == replacement.global_ids.get(entry)
955 }))
956 {
957 unchanged.push(*id);
958 }
959 }
960 for id in unchanged {
961 replacements.remove(&id);
962 }
963 Ok(replacements)
964 }
965
966 /// Stores prepared replacements as the next revision, which checkpoints when the chain
967 /// would exceed 512 revisions.
968 pub(crate) fn commit(
969 &mut self,
970 replacements: Vec<(ExGuid, crate::Object<'a>)>,
971 ) -> Result<Commit> {
972 let mut changed = BTreeSet::new();
973 // Whether each object whose count may move was reachable before.
974 let mut touched = BTreeMap::new();
975 let (mut added, mut removed) = (Vec::new(), Vec::new());
976 for (id, object) in replacements {
977 changed.insert(id);
978 let reachable = self.is_reachable(id);
979 touched.insert(id, reachable);
980 if reachable {
981 moved_references(
982 &self.revision.objects[&id],
983 &object,
984 &mut removed,
985 &mut added,
986 )?;
987 }
988 self.revision.objects.insert(id, object);
989 }
990 let roots: BTreeSet<ExGuid> = self.revision.roots.values().copied().collect();
991 if roots.len() != self.revision.roots.len() {
992 return Err(Error {
993 offset: 0,
994 message: "Object is the root of multiple roles",
995 });
996 }
997 added.extend(
998 roots
999 .into_iter()
1000 .filter(|id| !self.incoming.contains_key(id)),
1001 );
1002 // Attaching before detaching keeps a moved subtree from being walked twice.
1003 while let Some(id) = added.pop() {
1004 let object = self.revision.objects.get(&id).ok_or(Error {
1005 offset: 0,
1006 message: "Reachable object has no declaration",
1007 })?;
1008 let count = self.incoming.entry(id).or_default();
1009 touched.entry(id).or_insert(*count > 0);
1010 *count = count.checked_add(1).ok_or(Error {
1011 offset: 0,
1012 message: "Object reference count overflows",
1013 })?;
1014 if *count == 1 {
1015 added.extend(object.references()?.objects);
1016 index(&mut self.readonly, id, object, true);
1017 }
1018 }
1019 while let Some(id) = removed.pop() {
1020 touched.entry(id).or_insert(true);
1021 let count = self.incoming.get_mut(&id).unwrap();
1022 *count -= 1;
1023 if *count == 0 {
1024 self.incoming.remove(&id);
1025 let object = &self.revision.objects[&id];
1026 removed.extend(object.references()?.objects);
1027 index(&mut self.readonly, id, object, false);
1028 }
1029 }
1030 for (&id, &reachable) in &touched {
1031 let count = self.incoming.get(&id).copied();
1032 if changed.contains(&id) && count.is_none() {
1033 return Err(Error {
1034 offset: 0,
1035 message: "Edited object is not reachable in the resulting revision",
1036 });
1037 }
1038 let object = self.revision.objects.get_mut(&id).unwrap();
1039 if (reachable || count.is_some()) && object.reference_count != count.unwrap_or(0) {
1040 object.reference_count = count.unwrap_or(0);
1041 changed.insert(id);
1042 }
1043 }
1044 // Native cold-open fails on long dependency chains; cap their depth at 512.
1045 let checkpoint = self.depth == 0 || self.depth >= 512;
1046 self.depth = if checkpoint { 1 } else { self.depth + 1 };
1047 Ok(Commit {
1048 checkpoint,
1049 changed,
1050 touched: touched.into_keys().collect(),
1051 })
1052 }
1053
1054 /// Gives the objects a revision declares the id tables reading it back yields: objects
1055 /// sharing a table are declared in one group, which keeps the entries they use.
1056 pub(crate) fn settle(&mut self, declared: impl IntoIterator<Item = ExGuid>) -> Result<()> {
1057 let mut groups = BTreeMap::<_, Vec<_>>::new();
1058 for id in declared {
1059 let table = Arc::clone(&self.revision.objects[&id].global_ids);
1060 groups.entry(table).or_default().push(id);
1061 }
1062 for (table, ids) in groups {
1063 let used = used_entries(
1064 &table,
1065 ids.iter().map(|id| (*id, &self.revision.objects[id])),
1066 )?;
1067 if used.iter().eq(table.keys()) {
1068 continue;
1069 }
1070 let kept: Arc<BTreeMap<_, _>> = Arc::new(
1071 table
1072 .iter()
1073 .filter(|(entry, _)| used.binary_search(entry).is_ok())
1074 .map(|(entry, guid)| (*entry, *guid))
1075 .collect(),
1076 );
1077 for id in ids {
1078 self.revision.objects.get_mut(&id).unwrap().global_ids = Arc::clone(&kept);
1079 }
1080 }
1081 Ok(())
1082 }
1083}
1084
1085/// Adds a reachable read-only property object to `readonly`, or removes an unreachable one.
1086fn index<'a>(
1087 readonly: &mut BTreeMap<u32, BTreeMap<&'a [u8], BTreeSet<ExGuid>>>,
1088 id: ExGuid,
1089 object: &crate::Object<'a>,
1090 reachable: bool,
1091) {
1092 let ObjectData::Properties(bytes) = object.data else {
1093 return;
1094 };
1095 if object.jcid & 0x100000 == 0 {
1096 return;
1097 }
1098 let ids = readonly
1099 .entry(object.jcid)
1100 .or_default()
1101 .entry(bytes)
1102 .or_default();
1103 if reachable {
1104 ids.insert(id);
1105 } else {
1106 ids.remove(&id);
1107 }
1108}
1109
1110/// Adds the object references `before` has and `after`, a later version of the object,
1111/// lacks to `removed`, and those it gains to `added`.
1112fn moved_references(
1113 before: &crate::Object<'_>,
1114 after: &crate::Object<'_>,
1115 removed: &mut Vec<ExGuid>,
1116 added: &mut Vec<ExGuid>,
1117) -> Result<()> {
1118 let compact_ids = |object: &crate::Object<'_>| -> Result<Vec<[u8; 4]>> {
1119 let ObjectData::Properties(bytes) = object.data else {
1120 return Ok(Vec::new());
1121 };
1122 let mut ids = Vec::new();
1123 for property in PropertySets::parse(bytes)?.sets.iter().flatten() {
1124 if let Value::References {
1125 stream: crate::IdStream::Objects,
1126 compact_ids,
1127 } = property.value
1128 {
1129 ids.extend(
1130 compact_ids
1131 .chunks_exact(4)
1132 .map(|id| <[u8; 4]>::try_from(id).unwrap()),
1133 );
1134 }
1135 }
1136 Ok(ids)
1137 };
1138 let decode = |ids: &[[u8; 4]], table| -> Result<Vec<ExGuid>> {
1139 let bytes = ids.concat();
1140 let mut cursor = crate::bytes::Cursor {
1141 bytes: &bytes,
1142 offset: 0,
1143 };
1144 std::iter::from_fn(|| (!cursor.bytes.is_empty()).then(|| cursor.compact(table))).collect()
1145 };
1146 // Equal compact IDs name the same objects where the later table keeps the earlier entries.
1147 let (old, new) = (&before.global_ids, &after.global_ids);
1148 let extended = Arc::ptr_eq(old, new) || {
1149 let mut later = new.iter();
1150 old.iter()
1151 .all(|entry| later.find(|later| later.0 >= entry.0) == Some(entry))
1152 };
1153 if extended {
1154 let (old_ids, new_ids) = (compact_ids(before)?, compact_ids(after)?);
1155 let (gone, gained) = differing(&old_ids, &new_ids);
1156 removed.extend(decode(gone, old)?);
1157 added.extend(decode(gained, new)?);
1158 } else {
1159 let (old_refs, new_refs) = (before.references()?.objects, after.references()?.objects);
1160 let (gone, gained) = differing(&old_refs, &new_refs);
1161 removed.extend_from_slice(gone);
1162 added.extend_from_slice(gained);
1163 }
1164 Ok(())
1165}
1166
1167/// The parts of two sequences between their common prefix and suffix: removing the first
1168/// from `before` and adding the second yields `after` as a multiset.
1169pub(crate) fn differing<'s, T: PartialEq>(before: &'s [T], after: &'s [T]) -> (&'s [T], &'s [T]) {
1170 let prefix = before.iter().zip(after).take_while(|(a, b)| a == b).count();
1171 let suffix = before[prefix..]
1172 .iter()
1173 .rev()
1174 .zip(after[prefix..].iter().rev())
1175 .take_while(|(a, b)| a == b)
1176 .count();
1177 (
1178 &before[prefix..before.len() - suffix],
1179 &after[prefix..after.len() - suffix],
1180 )
1181}
1182
1183/// A revision `LiveRevision::commit` stored.
1184pub(crate) struct Commit {
1185 /// Whether the revision declares every object, depending on none.
1186 pub checkpoint: bool,
1187 /// The objects it declares otherwise: the replacements and those whose counts moved.
1188 pub changed: BTreeSet<ExGuid>,
1189 /// Objects whose reachability or reference count may have moved.
1190 pub touched: BTreeSet<ExGuid>,
1191}
1192
1193/// Revisions in the dependency chain of `rid`, counted to 512.
1194pub(crate) fn chain_depth(index: &RevisionIndex<'_>, space: ExGuid, rid: ExGuid) -> usize {
1195 std::iter::successors(Some(rid), |id| {
1196 index.spaces[&space].revisions[id].dependency
1197 })
1198 .take(512)
1199 .count()
1200}
1201
7381202pub(crate) fn write_revision(
7391203 source: &[u8],
7401204 space: ExGuid,
......@@ -880,12 +1344,20 @@ pub(crate) fn write_revision_on(
8801344 Ok(output)
8811345}
8821346
883fn build_on(
1347#[cfg(test)]
1348thread_local! {
1349 /// Revisions `build_on` has built on this thread.
1350 pub(crate) static BUILDS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1351}
1352
1353pub(crate) fn build_on(
8841354 index: &RevisionIndex<'_>,
8851355 payloads: &[([u8; 16], &[u8])],
8861356 protection: Option<&dyn Protection>,
8871357 edit: impl FnOnce(&RevisionIndex<'_>) -> Result<BTreeMap<ExGuid, RevisionEdit>>,
8881358) -> Result<Vec<u8>> {
1359 #[cfg(test)]
1360 BUILDS.with(|builds| builds.set(builds.get() + 1));
8891361 let store = index.store;
8901362 let source = store.data;
8911363 let is_section = store.header.file_type == FileType::Section;
......@@ -933,7 +1405,7 @@ fn build_on(
9331405 for (space, change) in changes {
9341406 let new_space = matches!(change, RevisionEdit::Create { .. });
9351407 let current = (ExGuid::default(), 1_u32);
936 let (rid, label, mut revision, mut replacements) = match change {
1408 let (rid, label, revision, replacements) = match change {
9371409 RevisionEdit::Update(objects) => {
9381410 let rid = index.active(space)?;
9391411 (Some(rid), current, resolve(space, rid)?, objects)
......@@ -986,179 +1458,29 @@ fn build_on(
9861458 }
9871459 };
9881460 let existing: BTreeSet<ExGuid> = revision.objects.keys().copied().collect();
989 let reachable = if rid.is_some() {
990 revision.reachable()?
991 } else {
992 BTreeSet::new()
993 };
994 for (id, replacement) in &replacements {
995 if let Some(object) = revision.objects.get(id) {
996 if object.jcid & 0x100000 != 0 {
997 return Err(Error {
998 offset: 0,
999 message: "Read-only object requires a new identity",
1000 });
1001 }
1002 if replacement.jcid != object.jcid
1003 || !matches!(object.data, ObjectData::Properties(_))
1004 {
1005 return Err(Error {
1006 offset: 0,
1007 message: "An existing object's type cannot be changed",
1008 });
1009 }
1010 } else if !is_section && replacement.jcid != 0x20001 {
1011 return Err(Error {
1012 offset: 0,
1013 message: "New objects in a table of contents are its entries",
1014 });
1015 }
1016 if replacement.global_ids.keys().any(|i| *i > 0xffffff) {
1017 return Err(Error {
1018 offset: 0,
1019 message: "Invalid property object declaration",
1020 });
1021 }
1022 compact(*id, &replacement.global_ids)?;
1023 if is_file_declaration(replacement.jcid) {
1024 if !is_section {
1025 return Err(Error {
1026 offset: 0,
1027 message: "File-data objects require a section file",
1028 });
1029 }
1030 replacement.file_declaration()?;
1031 } else if replacement.jcid & 0x20000 == 0 {
1032 return Err(Error {
1033 offset: 0,
1034 message: "Invalid property object declaration",
1035 });
1036 } else {
1037 PropertySets::parse(&replacement.bytes)?;
1038 }
1039 }
1040 // Native coalescing of duplicate readonly styles can leave dangling references.
1041 let mut aliases = BTreeMap::new();
1042 for (id, replacement) in &replacements {
1043 if revision.objects.contains_key(id)
1044 || replacement.jcid & 0x100000 == 0
1045 || PropertySets::parse(&replacement.bytes)?
1046 .sets
1047 .iter()
1048 .flatten()
1049 .any(|p| matches!(p.value, Value::References { .. }))
1050 {
1051 continue;
1052 }
1053 let existing = revision.objects.iter().find_map(|(other, object)| {
1054 (reachable.contains(other)
1055 && object.jcid == replacement.jcid
1056 && object.data == ObjectData::Properties(&replacement.bytes))
1057 .then_some(*other)
1058 });
1059 let existing = existing.or_else(|| {
1060 replacements.range(..id).find_map(|(other, object)| {
1061 (object.jcid == replacement.jcid && object.bytes == replacement.bytes)
1062 .then_some(*aliases.get(other).unwrap_or(other))
1063 })
1064 });
1065 if let Some(existing) = existing {
1066 aliases.insert(*id, existing);
1067 }
1068 }
1069 for id in aliases.keys() {
1070 replacements.remove(id);
1071 }
1072 for object in replacements.values_mut() {
1073 if is_file_declaration(object.jcid) {
1074 continue;
1075 }
1076 let mut remapped = Vec::new();
1077 for property in PropertySets::parse(&object.bytes)?.sets.iter().flatten() {
1078 if let Value::References {
1079 stream: crate::IdStream::Objects,
1080 compact_ids,
1081 } = property.value
1082 {
1083 for bytes in compact_ids.chunks_exact(4) {
1084 let offset = bytes.as_ptr().addr() - object.bytes.as_ptr().addr();
1085 let id =
1086 crate::bytes::Cursor { bytes, offset }.compact(&object.global_ids)?;
1087 if let Some(existing) = aliases.get(&id) {
1088 remapped.push((offset, *existing));
1089 }
1090 }
1091 }
1092 }
1093 for (offset, id) in remapped {
1094 let reference = object.reference(id)?;
1095 object.bytes[offset..offset + 4].copy_from_slice(&reference);
1096 }
1097 }
1098 // Detached objects must pass the final reachability check even when unchanged. Equal
1099 // bytes are the same object when the table entries they name agree: stored tables
1100 // keep only those.
1101 let mut unchanged = Vec::new();
1102 for (id, replacement) in &replacements {
1103 if let Some(object) = revision.objects.get(id)
1104 && reachable.contains(id)
1105 && object.data == ObjectData::Properties(&replacement.bytes)
1106 && table_entries(&replacement.bytes)?
1107 .iter()
1108 .all(|entry| object.global_ids.get(entry) == replacement.global_ids.get(entry))
1109 {
1110 unchanged.push(*id);
1111 }
1112 }
1113 for id in unchanged {
1114 replacements.remove(&id);
1115 }
1461 let depth = rid.map_or(0, |rid| chain_depth(index, space, rid));
1462 let mut live = LiveRevision::new(revision, depth)?;
1463 let replacements = live.prepare(replacements, is_section)?;
11161464 if replacements.is_empty() {
11171465 continue;
11181466 }
1119 let mut changed: BTreeSet<_> = replacements.keys().copied().collect();
1120 for (id, replacement) in &replacements {
1121 let data = if is_file_declaration(replacement.jcid) {
1122 let (reference, extension) = replacement.file_declaration()?;
1123 ObjectData::File {
1124 reference,
1125 extension,
1126 }
1127 } else {
1128 ObjectData::Properties(&replacement.bytes)
1129 };
1130 revision.objects.insert(
1131 *id,
1132 crate::Object {
1133 jcid: replacement.jcid,
1134 reference_count: 0,
1135 data,
1136 global_ids: Arc::clone(&replacement.global_ids),
1137 },
1138 );
1139 }
1140 let incoming = revision.reference_counts()?;
1141 if replacements.keys().any(|id| !incoming.contains_key(id)) {
1142 return Err(Error {
1143 offset: 0,
1144 message: "Edited object is not reachable in the resulting revision",
1145 });
1146 }
1147 for (id, object) in &mut revision.objects {
1148 if reachable.contains(id) || incoming.contains_key(id) {
1149 let count = incoming.get(id).copied().unwrap_or(0);
1150 if object.reference_count != count {
1151 object.reference_count = count;
1152 changed.insert(*id);
1153 }
1154 }
1155 }
1156
1157 // Native cold-open fails on long dependency chains; cap their depth at 512.
1158 let checkpoint = rid.is_none()
1159 || std::iter::successors(rid, |id| index.spaces[&space].revisions[id].dependency)
1160 .nth(511)
1161 .is_some();
1467 let Commit {
1468 checkpoint,
1469 changed,
1470 ..
1471 } = live.commit(
1472 replacements
1473 .iter()
1474 .map(|(id, replacement)| {
1475 let global_ids = Arc::clone(&replacement.global_ids);
1476 Ok((
1477 *id,
1478 declared(replacement.jcid, &replacement.bytes, global_ids)?,
1479 ))
1480 })
1481 .collect::<Result<Vec<_>>>()?,
1482 )?;
1483 let revision = &live.revision;
11621484 let selected: Vec<_> = revision
11631485 .objects
11641486 .iter()
......@@ -1272,15 +1594,9 @@ fn build_on(
12721594 };
12731595 // A table read from a long-lived page names every session that edited it; the
12741596 // group stores the entries its objects use.
1275 let mut used = BTreeSet::new();
1276 for (id, object) in &objects {
1277 used.insert(u32::from_le_bytes(compact(*id, table)?) >> 8);
1278 if let ObjectData::Properties(bytes) = object.data {
1279 used.extend(table_entries(bytes)?);
1280 }
1281 }
1597 let used = used_entries(table, objects.iter().copied())?;
12821598 for (id, guid) in table {
1283 if is_section && !used.contains(id) {
1599 if is_section && used.binary_search(id).is_err() {
12841600 continue;
12851601 }
12861602 let mut entry = id.to_le_bytes().to_vec();
crates/snowbound/src/main.rs+27-16
......@@ -1323,24 +1323,21 @@ impl State {
13231323 return Ok(());
13241324 };
13251325 let after = self.view.editor.page()?;
1326 match session
1327 .section
1328 .save(session.space, &session.before, &after, "snowbound")?
1326 if after == session.before {
1327 return Ok(());
1328 }
1329 if let Some(entry) = session
1330 .pages
1331 .iter_mut()
1332 .find(|(space, ..)| *space == session.space)
13291333 {
1330 notebook::session::Save::Unchanged => {}
1331 notebook::session::Save::Queued(_) => {
1332 session.before = session.section.page(session.space)?;
1333 session.status = "Saving";
1334 if let Some(entry) = session
1335 .pages
1336 .iter_mut()
1337 .find(|(space, ..)| *space == session.space)
1338 {
1339 entry.1 = after.title;
1340 }
1341 }
1342 notebook::session::Save::Stale => self.reload()?,
1334 entry.1.clone_from(&after.title);
13431335 }
1336 // Queued saves chain: each one's `after` is the next one's `before`.
1337 let before = std::mem::replace(&mut session.before, after.clone());
1338 session
1339 .section
1340 .queue_save(session.space, before, after, "snowbound")?;
13441341 Ok(())
13451342 }
13461343
......@@ -1397,6 +1394,18 @@ impl State {
13971394 };
13981395 let shown = (session.status, session.conflict);
13991396 let mut refreshed = false;
1397 let mut stale = false;
1398 for (space, outcome) in session.section.saved() {
1399 match outcome {
1400 Ok(notebook::session::Save::Queued(_)) => session.status = "Saving",
1401 Ok(notebook::session::Save::Unchanged) => {}
1402 Ok(notebook::session::Save::Stale) => stale |= space == session.space,
1403 Err(error) => {
1404 eprintln!("Saving failed: {error}");
1405 session.status = "Not saving";
1406 }
1407 }
1408 }
14001409 for event in session.section.events() {
14011410 use notebook::session::Event;
14021411 session.status = match event {
......@@ -1436,6 +1445,8 @@ impl State {
14361445 } else if session.section.page(session.space)? != session.before {
14371446 self.reload()?;
14381447 }
1448 } else if stale {
1449 self.reload()?;
14391450 }
14401451 Ok(())
14411452 }