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,...@@ -67,10 +67,15 @@ author)` queues the edited model: `Save::Queued(id)` is durable locally,
67`Save::Unchanged` means the model equals the stored page, and `Save::Stale`67`Save::Unchanged` means the model equals the stored page, and `Save::Stale`
68means the stored page no longer matches `before` because the section changed68means the stored page no longer matches `before` because the section changed
69underneath the editor, so the page must be reloaded before saving again.69underneath 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.
70`events()` drains what the synchronization thread reported since the last75`events()` drains what the synchronization thread reported since the last
71poll: refreshes, attempt outcomes and unreachable files; `notify` runs on that76poll: refreshes, attempt outcomes and unreachable files; `notify` runs
72thread whenever an event is available so the application can wake its event77whenever an event or save outcome is available so the application can wake its
73loop. `close()` stops publication.78event loop. `close()` finishes queued saves and stops publication.
7479
75`Event::Unreachable` retains an `io::Error`: callers can distinguish permission80`Event::Unreachable` retains an `io::Error`: callers can distinguish permission
76denial, missing targets, timeouts, and connection failures through `kind()` without81denial, 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 {...@@ -274,9 +274,20 @@ impl Replica {
274 after: &Page,274 after: &Page,
275 author: &str,275 author: &str,
276 ) -> Result<Option<u64>> {276 ) -> 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>)> {
277 let prepared = PreparedEdit::page(source, space, after, author)?;288 let prepared = PreparedEdit::page(source, space, after, author)?;
278 if prepared.as_bytes() == source {289 if prepared.as_bytes() == source {
279 return Ok(None);290 return Ok((None, source.to_vec()));
280 }291 }
281 let before = page_of(source, space)?.ok_or_else(|| {292 let before = page_of(source, space)?.ok_or_else(|| {
282 io::Error::new(293 io::Error::new(
...@@ -332,11 +343,12 @@ impl Replica {...@@ -332,11 +343,12 @@ impl Replica {
332 transaction.commit()?;343 transaction.commit()?;
333 drop(connection);344 drop(connection);
334 self.wake_sync();345 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()));
336 }348 }
337 }349 }
338 }350 }
339 self.record(351 let id = self.record(
340 source,352 source,
341 space,353 space,
342 Operation::Page(PageIntent {354 Operation::Page(PageIntent {
...@@ -345,7 +357,8 @@ impl Replica {...@@ -345,7 +357,8 @@ impl Replica {
345 author: author.to_owned(),357 author: author.to_owned(),
346 }),358 }),
347 &prepared,359 &prepared,
348 )360 )?;
361 Ok((id, prepared.as_bytes().to_vec()))
349 }362 }
350363
351 /// Queues a new page and its section entry with stable identities for dependent edits.364 /// 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::{...@@ -9,14 +9,15 @@ use onestore::{
9 page::Page,9 page::Page,
10};10};
11use std::{11use std::{
12 collections::BTreeMap,12 collections::{BTreeMap, VecDeque},
13 io,13 io,
14 io::Write,14 io::Write,
15 path::{Path, PathBuf},15 path::{Path, PathBuf},
16 sync::{16 sync::{
17 Arc,17 Arc, Mutex,
18 mpsc::{self, Receiver},18 mpsc::{self, Receiver, Sender},
19 },19 },
20 thread::{self, JoinHandle},
20 time::Duration,21 time::Duration,
21};22};
2223
...@@ -751,12 +752,23 @@ pub struct QueuedEdit {...@@ -751,12 +752,23 @@ pub struct QueuedEdit {
751 pub status: EditStatus,752 pub status: EditStatus,
752}753}
753754
754/// A section file with its replica and background publication.755/// A section file with its replica, background saves and background publication.
755pub struct Section {756pub struct Section {
756 file: PathBuf,757 file: PathBuf,
757 replica: Arc<Replica>,758 replica: Arc<Replica>,
758 worker: Option<SyncWorker>,759 worker: Option<SyncWorker>,
759 events: Receiver<Event>,760 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,
760}772}
761773
762impl Section {774impl Section {
...@@ -830,6 +842,26 @@ impl Section {...@@ -830,6 +842,26 @@ impl Section {
830 ) -> Result<Self> {842 ) -> Result<Self> {
831 let replica = Arc::new(replica);843 let replica = Arc::new(replica);
832 let (sender, events) = mpsc::channel();844 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 };
833 let worker = replica.start_sync(Duration::from_secs(2), connect, move |result| {865 let worker = replica.start_sync(Duration::from_secs(2), connect, move |result| {
834 let event = match result {866 let event = match result {
835 Ok(None) => Event::Refreshed,867 Ok(None) => Event::Refreshed,
...@@ -847,7 +879,7 @@ impl Section {...@@ -847,7 +879,7 @@ impl Section {
847 Err(error) => Event::Failed(error.to_string()),879 Err(error) => Event::Failed(error.to_string()),
848 };880 };
849 if sender.send(event).is_ok() {881 if sender.send(event).is_ok() {
850 notify();882 signal();
851 }883 }
852 })?;884 })?;
853 Ok(Self {885 Ok(Self {
...@@ -855,6 +887,9 @@ impl Section {...@@ -855,6 +887,9 @@ impl Section {
855 replica,887 replica,
856 worker: Some(worker),888 worker: Some(worker),
857 events,889 events,
890 saves: Some(saves),
891 saved,
892 saver: Some(saver),
858 })893 })
859 }894 }
860895
...@@ -951,20 +986,23 @@ impl Section {...@@ -951,20 +986,23 @@ impl Section {
951 /// Saves an edited page. `before` is the model the edit started from; a stored page986 /// Saves an edited page. `before` is the model the edit started from; a stored page
952 /// that differs from it means the section changed underneath the editor.987 /// that differs from it means the section changed underneath the editor.
953 pub fn save(&self, space: ExGuid, before: &Page, after: &Page, author: &str) -> Result<Save> {988 pub fn save(&self, space: ExGuid, before: &Page, after: &Page, author: &str) -> Result<Save> {
954 loop {989 Ok(save(&self.replica, space, before, None, after, author)?.0)
955 let snapshot = self.replica.snapshot()?;990 }
956 let store = Store::parse(&snapshot)?;991
957 let index = RevisionIndex::parse(&store)?;992 /// `save` on the section's save thread, returning at once; `saved` reports the outcome.
958 if Page::from_space(&Document::parse(&index)?, space)? != *before {993 /// Saves queue in order, and one whose `before` is the previous save's `after` continues
959 return Ok(Save::Stale);994 /// it: pass each save's `after` as the next one's `before`.
960 }995 pub fn queue_save(&self, space: ExGuid, before: Page, after: Page, author: &str) -> Result<()> {
961 match self.replica.save(&snapshot, space, after, author) {996 let save = PageSave {
962 Ok(Some(id)) => return Ok(Save::Queued(id)),997 space,
963 Ok(None) => return Ok(Save::Unchanged),998 before,
964 Err(Error::Io(error)) if error.kind() == io::ErrorKind::ResourceBusy => {}999 after,
965 Err(error) => return Err(error),1000 author: author.to_owned(),
966 }1001 };
967 }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())
968 }1006 }
9691007
970 pub fn status(&self, id: u64) -> Result<Option<EditStatus>> {1008 pub fn status(&self, id: u64) -> Result<Option<EditStatus>> {
...@@ -1043,6 +1081,12 @@ impl Section {...@@ -1043,6 +1081,12 @@ impl Section {
1043 self.events.try_iter().collect()1081 self.events.try_iter().collect()
1044 }1082 }
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
1046 /// Requests a synchronization attempt now.1090 /// Requests a synchronization attempt now.
1047 pub fn wake(&self) {1091 pub fn wake(&self) {
1048 if let Some(worker) = &self.worker {1092 if let Some(worker) = &self.worker {
...@@ -1058,6 +1102,12 @@ impl Section {...@@ -1058,6 +1102,12 @@ impl Section {
1058 /// Dropping instead requests cancellation without waiting; the worker retains1102 /// Dropping instead requests cancellation without waiting; the worker retains
1059 /// cache ownership until that operation finishes. Remote calls must be bounded.1103 /// cache ownership until that operation finishes. Remote calls must be bounded.
1060 pub fn close(mut self) -> Result<()> {1104 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 }
1061 match self.worker.take() {1111 match self.worker.take() {
1062 Some(worker) => worker.stop(),1112 Some(worker) => worker.stop(),
1063 None => Ok(()),1113 None => Ok(()),
...@@ -1065,6 +1115,95 @@ impl Section {...@@ -1065,6 +1115,95 @@ impl Section {
1065 }1115 }
1066}1116}
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
1068/// The section file itself as the publication target, under OneNote-compatible exclusion.1207/// The section file itself as the publication target, under OneNote-compatible exclusion.
1069struct FileRemote(PathBuf);1208struct 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()...@@ -232,6 +232,86 @@ fn an_external_change_refreshes_the_page_and_stales_a_save_from_the_old_model()
232 section.close().unwrap();232 section.close().unwrap();
233}233}
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
235#[test]315#[test]
236fn a_notebook_directory_lists_its_sections_and_opens_them() {316fn a_notebook_directory_lists_its_sections_and_opens_them() {
237 let directory = tempfile::tempdir().unwrap();317 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...@@ -9,6 +9,7 @@ protected = ["dep:aes", "dep:cbc", "dep:roxmltree", "dep:sha1", "dep:subtle", "d
99
10[dependencies]10[dependencies]
11base64 = "0.22.1"11base64 = "0.22.1"
12bumpalo = "3.20.3"
12serde_json = "1.0.151"13serde_json = "1.0.151"
13md5 = "0.8.1"14md5 = "0.8.1"
14getrandom = "0.4.3"15getrandom = "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> {...@@ -46,13 +46,13 @@ impl<'a> Space<'a> {
46 }46 }
47}47}
4848
49#[derive(Debug, Serialize)]49#[derive(Debug, Clone, Serialize)]
50pub struct Revision<'a> {50pub struct Revision<'a> {
51 pub roots: BTreeMap<u32, ExGuid>,51 pub roots: BTreeMap<u32, ExGuid>,
52 pub nodes: BTreeMap<ExGuid, Element<'a>>,52 pub nodes: BTreeMap<ExGuid, Element<'a>>,
53}53}
5454
55#[derive(Debug, Serialize)]55#[derive(Debug, Clone, Serialize)]
56pub struct Element<'a> {56pub struct Element<'a> {
57 pub jcid: u32,57 pub jcid: u32,
58 pub children: Vec<ExGuid>,58 pub children: Vec<ExGuid>,
...@@ -490,13 +490,13 @@ pub enum Kind<'a> {...@@ -490,13 +490,13 @@ pub enum Kind<'a> {
490 Unknown,490 Unknown,
491}491}
492492
493#[derive(Debug, PartialEq, Serialize)]493#[derive(Debug, Clone, PartialEq, Serialize)]
494pub struct Field<'a> {494pub struct Field<'a> {
495 pub id: u32,495 pub id: u32,
496 pub value: FieldValue<'a>,496 pub value: FieldValue<'a>,
497}497}
498498
499#[derive(Debug, PartialEq, Serialize)]499#[derive(Debug, Clone, PartialEq, Serialize)]
500pub enum FieldValue<'a> {500pub enum FieldValue<'a> {
501 NoData,501 NoData,
502 Bytes(#[serde(serialize_with = "hex")] &'a [u8]),502 Bytes(#[serde(serialize_with = "hex")] &'a [u8]),
...@@ -648,6 +648,29 @@ impl<'a, 'o> Fields<'a, 'o> {...@@ -648,6 +648,29 @@ impl<'a, 'o> Fields<'a, 'o> {
648}648}
649649
650fn references(bytes: &[u8], ids: &BTreeMap<u32, [u8; 16]>) -> Result<Vec<ExGuid>> {650fn 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 }
651 let mut cursor = Cursor { bytes, offset: 0 };674 let mut cursor = Cursor { bytes, offset: 0 };
652 let mut result = Vec::new();675 let mut result = Vec::new();
653 while !cursor.bytes.is_empty() {676 while !cursor.bytes.is_empty() {
...@@ -906,11 +929,7 @@ impl<'a> Document<'a> {...@@ -906,11 +929,7 @@ impl<'a> Document<'a> {
906}929}
907930
908impl<'a> Element<'a> {931impl<'a> Element<'a> {
909 pub(crate) fn parse(object: &Object<'a>, store: &Store<'a>) -> Result<Self> {932 pub(crate) fn parse_with(
910 Self::parse_with(object, store, &mut |id| store.file_data(id))
911 }
912
913 fn parse_with(
914 object: &Object<'a>,933 object: &Object<'a>,
915 store: &Store<'a>,934 store: &Store<'a>,
916 file_data: &mut impl FnMut([u8; 16]) -> Result<&'a [u8]>,935 file_data: &mut impl FnMut([u8; 16]) -> Result<&'a [u8]>,
crates/onestore/src/edit.rs+87-57
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1use crate::{1use crate::{
2 Error, ExGuid, Object, ObjectData, PropertySets, RevisionIndex, Store,2 Error, ExGuid, Object, ObjectData, PropertySets,
3 active::{ActivePage, Changes},
3 create::string,4 create::string,
4 document::{Document, Element, Kind},5 document::{Element, Kind},
5 write::PropertyObject,6 write::PropertyObject,
6};7};
7use std::{collections::BTreeMap, ops::Range, sync::Arc};8use std::{collections::BTreeMap, ops::Range, sync::Arc};
...@@ -35,28 +36,31 @@ pub fn replace_text(...@@ -35,28 +36,31 @@ pub fn replace_text(
35 range: Range<u32>,36 range: Range<u32>,
36 replacement: &str,37 replacement: &str,
37) -> Result<Vec<u8>, Error> {38) -> Result<Vec<u8>, Error> {
38 let invalid = |message| Error { offset: 0, message };
39 if range.start > range.end || replacement.contains(['\0', '\n', '\r', '\u{fffc}']) {39 if range.start > range.end || replacement.contains(['\0', '\n', '\r', '\u{fffc}']) {
40 return Err(invalid(40 return Err(Error {
41 "Use a valid text range and ordinary paragraph text",41 offset: 0,
42 ));42 message: "Use a valid text range and ordinary paragraph text",
43 });
43 }44 }
44 let store = Store::parse(source)?;45 crate::active::write(source, space, |active| {
45 let index = RevisionIndex::parse(&store)?;46 text_changes(active, object, range, replacement)
46 index.validate_current()?;47 })
47 let rid = index.active(space)?;48}
48 let raw = index.resolve(space, rid)?;49
49 if !raw.reachable()?.contains(&object) {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) {
50 return Err(invalid("Object is not reachable in the active revision"));60 return Err(invalid("Object is not reachable in the active revision"));
51 }61 }
52 let document = Document::parse(&index)?;62 let revision = &active.view;
53 let revision = document63 let parents = active.editable_parents(object)?;
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)?;
60 let node = revision64 let node = revision
61 .nodes65 .nodes
62 .get(&object)66 .get(&object)
...@@ -129,7 +133,7 @@ pub fn replace_text(...@@ -129,7 +133,7 @@ pub fn replace_text(
129 ));133 ));
130 };134 };
131 if &text[start..end] == replacement {135 if &text[start..end] == replacement {
132 return Ok(source.to_vec());136 return Ok(Changes::new());
133 }137 }
134 let added = u32::try_from(replacement.encode_utf16().count())138 let added = u32::try_from(replacement.encode_utf16().count())
135 .map_err(|_| invalid("Replacement text exceeds the UTF-16 offset range"))?;139 .map_err(|_| invalid("Replacement text exceeds the UTF-16 offset range"))?;
...@@ -235,9 +239,9 @@ pub fn replace_text(...@@ -235,9 +239,9 @@ pub fn replace_text(
235 pending.extend(parents.get(&id).into_iter().flatten().copied());239 pending.extend(parents.get(&id).into_iter().flatten().copied());
236 }240 }
237 let Some((page, automatic, title_text)) =241 let Some((page, automatic, title_text)) =
238 page_title(revision, &pages, Some((object, &changed)))?242 active.title(&BTreeMap::new(), Some((object, &changed)))?
239 else {243 else {
240 return crate::write::replace_objects(&index, space, &edits);244 return crate::write::patched(raw, &edits);
241 };245 };
242 let Kind::Page {246 let Kind::Page {
243 alternate_title, ..247 alternate_title, ..
...@@ -296,7 +300,7 @@ pub fn replace_text(...@@ -296,7 +300,7 @@ pub fn replace_text(
296 &[]300 &[]
297 },301 },
298 });302 });
299 crate::write::replace_objects(&index, space, &edits)303 crate::write::patched(raw, &edits)
300}304}
301305
302pub(crate) fn editable_parents(306pub(crate) fn editable_parents(
...@@ -304,8 +308,19 @@ pub(crate) fn editable_parents(...@@ -304,8 +308,19 @@ pub(crate) fn editable_parents(
304 pages: &[ExGuid],308 pages: &[ExGuid],
305 object: ExGuid,309 object: ExGuid,
306) -> Result<std::collections::BTreeMap<ExGuid, Vec<ExGuid>>, Error> {310) -> Result<std::collections::BTreeMap<ExGuid, Vec<ExGuid>>, Error> {
307 let invalid = |message| Error { offset: 0, message };
308 let parents = revision.parents(pages)?;311 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 };
309 if !parents.contains_key(&object) && !pages.contains(&object) {324 if !parents.contains_key(&object) && !pages.contains(&object) {
310 return Err(invalid("Select content on an active editable page"));325 return Err(invalid("Select content on an active editable page"));
311 }326 }
...@@ -323,22 +338,40 @@ pub(crate) fn editable_parents(...@@ -323,22 +338,40 @@ pub(crate) fn editable_parents(
323 }338 }
324 pending.extend(parents.get(&id).into_iter().flatten().copied());339 pending.extend(parents.get(&id).into_iter().flatten().copied());
325 }340 }
326 Ok(parents)341 Ok(())
327}342}
328343
329pub(crate) fn page_title(344pub(crate) fn page_title(
330 revision: &crate::document::Revision<'_>,345 revision: &crate::document::Revision<'_>,
331 pages: &[ExGuid],346 pages: &[ExGuid],
332 text_update: Option<(ExGuid, &str)>,347 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)>,
333) -> Result<Option<(ExGuid, bool, String)>, Error> {366) -> Result<Option<(ExGuid, bool, String)>, Error> {
334 let invalid = |message| Error { offset: 0, message };367 let invalid = |message| Error { offset: 0, message };
335 let parents = revision.parents(pages)?;368 let titles: Vec<_> = candidates
336 let seen: std::collections::BTreeSet<_> = parents.keys().chain(pages).copied().collect();369 .filter_map(|id| {
337 let titles: Vec<_> = revision370 if !parents.contains_key(&id) && !pages.contains(&id) {
338 .nodes371 return None;
339 .iter()372 }
340 .filter_map(|(id, node)| {373 let node = node(id)?;
341 if !seen.contains(id) || !node.extra[0].iter().any(|field| field.id == 0x88001cb4) {374 if !node.extra[0].iter().any(|field| field.id == 0x88001cb4) {
342 return None;375 return None;
343 }376 }
344 match &node.kind {377 match &node.kind {
...@@ -346,7 +379,7 @@ pub(crate) fn page_title(...@@ -346,7 +379,7 @@ pub(crate) fn page_title(
346 text,379 text,
347 boilerplate: false,380 boilerplate: false,
348 ..381 ..
349 } => Some((*id, text.as_str())),382 } => Some((id, text.as_str())),
350 _ => None,383 _ => None,
351 }384 }
352 })385 })
...@@ -369,15 +402,15 @@ pub(crate) fn page_title(...@@ -369,15 +402,15 @@ pub(crate) fn page_title(
369 let [page] = pages else {402 let [page] = pages else {
370 return Err(invalid("Title editing requires a single active page"));403 return Err(invalid("Title editing requires a single active page"));
371 };404 };
372 let Kind::Page { rtl, .. } = &revision.nodes[page].kind else {405 let Kind::Page { rtl, .. } = &node(*page).unwrap().kind else {
373 unreachable!()406 unreachable!()
374 };407 };
375 let mut title_text = title_line(title_text);408 let mut title_text = title_line(title_text);
376 if automatic {409 if automatic {
377 let mut roots = revision.nodes[page].children.clone();410 let mut roots = node(*page).unwrap().children.clone();
378 roots.sort_by(|a, b| {411 roots.sort_by(|a, b| {
379 let a = &revision.nodes[a].layout;412 let a = &node(*a).unwrap().layout;
380 let b = &revision.nodes[b].layout;413 let b = &node(*b).unwrap().layout;
381 a.y.unwrap_or(0.0)414 a.y.unwrap_or(0.0)
382 .total_cmp(&b.y.unwrap_or(0.0))415 .total_cmp(&b.y.unwrap_or(0.0))
383 .then_with(|| {416 .then_with(|| {
...@@ -394,12 +427,12 @@ pub(crate) fn page_title(...@@ -394,12 +427,12 @@ pub(crate) fn page_title(
394 if !seen.insert(id) {427 if !seen.insert(id) {
395 continue;428 continue;
396 }429 }
397 let node = &revision.nodes[&id];430 let element = node(id).unwrap();
398 if let Kind::RichText {431 if let Kind::RichText {
399 text,432 text,
400 boilerplate: false,433 boilerplate: false,
401 ..434 ..
402 } = &node.kind435 } = &element.kind
403 {436 {
404 title_text = automatic_title(437 title_text = automatic_title(
405 if let Some((_, changed)) = text_update.filter(|(object, _)| *object == id) {438 if let Some((_, changed)) = text_update.filter(|(object, _)| *object == id) {
...@@ -412,40 +445,37 @@ pub(crate) fn page_title(...@@ -412,40 +445,37 @@ pub(crate) fn page_title(
412 break;445 break;
413 }446 }
414 }447 }
415 if *rtl == Some(true) && matches!(node.kind, Kind::Row) {448 if *rtl == Some(true) && matches!(element.kind, Kind::Row) {
416 pending.extend(node.children.iter().copied());449 pending.extend(element.children.iter().copied());
417 } else {450 } else {
418 pending.extend(node.children.iter().rev().copied());451 pending.extend(element.children.iter().rev().copied());
419 }452 }
420 pending.extend(node.content.iter().rev().copied());453 pending.extend(element.content.iter().rev().copied());
421 pending.extend(node.structure.iter().rev().copied());454 pending.extend(element.structure.iter().rev().copied());
422 }455 }
423 }456 }
424 Ok(Some((*page, automatic, title_text.to_owned())))457 Ok(Some((*page, automatic, title_text.to_owned())))
425}458}
426459
460/// Updates the page title for `changed` objects made on `view`, a copy of the page's view.
427pub(crate) fn update_title(461pub(crate) fn update_title(
428 store: &Store<'_>,462 active: &ActivePage<'_>,
429 raw: &crate::ResolvedRevision<'_>,
430 view: crate::document::Revision<'_>,463 view: crate::document::Revision<'_>,
431 pages: &[ExGuid],
432 changed: &mut BTreeMap<ExGuid, PropertyObject>,464 changed: &mut BTreeMap<ExGuid, PropertyObject>,
433) -> Result<(), Error> {465) -> Result<(), Error> {
434 let invalid = |message| Error { offset: 0, message };466 let invalid = |message| Error { offset: 0, message };
467 let (raw, pages) = (&active.live.revision, &active.pages);
435 // Shorten the moved view's lifetime to the changed property buffers.468 // Shorten the moved view's lifetime to the changed property buffers.
436 let mut view = view;469 let mut view = view;
437 for (id, object) in changed.iter() {470 for (id, object) in changed.iter() {
438 view.nodes.insert(471 view.nodes.insert(
439 *id,472 *id,
440 Element::parse(473 active.element(&Object {
441 &Object {474 jcid: object.jcid,
442 jcid: object.jcid,475 reference_count: 0,
443 reference_count: 0,476 data: ObjectData::Properties(&object.bytes),
444 data: ObjectData::Properties(&object.bytes),477 global_ids: Arc::clone(&object.global_ids),
445 global_ids: Arc::clone(&object.global_ids),478 })?,
446 },
447 store,
448 )?,
449 );479 );
450 }480 }
451 let title = page_title(&view, pages, None)?;481 let title = page_title(&view, pages, None)?;
crates/onestore/src/formatting.rs+95-89
...@@ -1,9 +1,9 @@...@@ -1,9 +1,9 @@
1use crate::{1use crate::{
2 Error, ExGuid, PropertySets, RevisionIndex, Store,2 Error, ExGuid, PropertySets,
3 active::{ActivePage, Changes},
3 create::{current_timestamps, properties, string},4 create::{current_timestamps, properties, string},
4 document::{Document, Kind},5 document::Kind,
5 edit::editable_parents,6 write::{PropertyObject, fresh_guid},
6 write::{PropertyObject, fresh_guid, write_revision_on},
7};7};
8use serde::{Deserialize, Serialize};8use serde::{Deserialize, Serialize};
9use std::{9use std::{
...@@ -98,19 +98,26 @@ pub(crate) fn format_text(...@@ -98,19 +98,26 @@ pub(crate) fn format_text(
98 range: Range<u32>,98 range: Range<u32>,
99 attributes: &[TextAttribute],99 attributes: &[TextAttribute],
100) -> Result<Vec<u8>, Error> {100) -> 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> {
101 if attributes.is_empty() || range.start > range.end {113 if attributes.is_empty() || range.start > range.end {
102 return Err(invalid(114 return Err(invalid(
103 "Select a text range and at least one formatting attribute",115 "Select a text range and at least one formatting attribute",
104 ));116 ));
105 }117 }
106 let values = attribute_values(attributes)?;118 let values = attribute_values(attributes)?;
107 let store = Store::parse(source)?;119 let view = &active.view;
108 let index = RevisionIndex::parse(&store)?;120 let parents = active.editable_parents(object)?;
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)?;
114 let node = &view.nodes[&object];121 let node = &view.nodes[&object];
115 let Kind::RichText {122 let Kind::RichText {
116 text,123 text,
...@@ -172,87 +179,86 @@ pub(crate) fn format_text(...@@ -172,87 +179,86 @@ pub(crate) fn format_text(
172 }179 }
173 }180 }
174 let modified = current_timestamps()?.0.to_le_bytes();181 let modified = current_timestamps()?.0.to_le_bytes();
175 write_revision_on(&index, space, |raw| {182 let raw = &active.live.revision;
176 let mut target = PropertyObject::from_object(&raw.objects[&object])?;183 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
177 let fields = PropertySets::parse(&target.bytes)?;184 let fields = PropertySets::parse(&target.bytes)?;
178 if fields.sets[0].iter().any(|p| p.id == 0x24003458) {185 if fields.sets[0].iter().any(|p| p.id == 0x24003458) {
179 return Err(invalid("This text object contains associated run objects"));186 return Err(invalid("This text object contains associated run objects"));
180 }187 }
181 if segments.len() != runs.len() && fields.sets[0].iter().any(|p| p.id == 0x40003499) {188 if segments.len() != runs.len() && fields.sets[0].iter().any(|p| p.id == 0x40003499) {
182 return Err(invalid(189 return Err(invalid(
183 "Formatting boundaries cannot split preserved run data",190 "Formatting boundaries cannot split preserved run data",
184 ));191 ));
185 }192 }
186 let mut styles = BTreeMap::new();193 let mut styles = BTreeMap::new();
187 let mut changed = BTreeMap::new();194 let mut changed = BTreeMap::new();
188 let mut references = Vec::new();195 let mut references = Vec::new();
189 let mut ends = Vec::new();196 let mut ends = Vec::new();
190 let mut updated = false;197 let mut updated = false;
191 let changes: Vec<_> = values198 let changes: Vec<_> = values
192 .iter()199 .iter()
193 .map(|(id, value)| (*id, value.as_slice()))200 .map(|(id, value)| (*id, value.as_slice()))
194 .collect();201 .collect();
195 for &(i, end, selected) in &segments {202 for &(i, end, selected) in &segments {
196 let previous = runs[i].format;203 let previous = runs[i].format;
197 let key = (previous, selected);204 let key = (previous, selected);
198 let id = if let Some(id) = styles.get(&key) {205 let id = if let Some(id) = styles.get(&key) {
199 *id206 *id
200 } else if let Some(id) = previous.filter(|_| !selected) {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);
201 id225 id
202 } else {226 } else {
203 let mut style = match previous {227 if !PropertySets::parse(&style.bytes)?.sets[0]
204 Some(id) => PropertyObject::from_object(&raw.objects[&id])?,228 .iter()
205 None => PropertyObject {229 .any(|p| p.id == 0x14001c3b)
206 jcid: 0x12004d,230 {
207 bytes: properties(&[])?,231 style.set(&[(
208 global_ids: Arc::new(BTreeMap::new()),232 0x14001c3b,
209 },233 &resolved[i].format.language.unwrap_or(0x409).to_le_bytes(),
210 };234 )])?;
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
237 }235 }
238 };236 let id = ExGuid {
239 updated |= selected && previous != Some(id);237 guid: fresh_guid()?,
240 references.extend_from_slice(&target.reference(id)?);238 n: 1,
241 ends.extend_from_slice(&end.to_le_bytes());239 };
242 }240 style.reference(id)?;
243 if !updated {241 changed.insert(id, style);
244 return Ok(BTreeMap::new());242 styles.insert(key, id);
245 }243 id
246 ends.truncate(ends.len() - 4);244 }
247 target.set(&[245 };
248 (0x24001e13, &references),246 updated |= selected && previous != Some(id);
249 (0x1c001e12, &ends),247 references.extend_from_slice(&target.reference(id)?);
250 (0x14001d7a, &modified),248 ends.extend_from_slice(&end.to_le_bytes());
251 ])?;249 }
252 changed.insert(object, target);250 if !updated {
253 touch_ancestors(raw, &parents, object, &modified, &mut changed)?;251 return Ok(BTreeMap::new());
254 Ok(changed)252 }
255 })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)
256}262}
257263
258pub(crate) fn touch_ancestors(264pub(crate) fn touch_ancestors(
crates/onestore/src/insertion.rs+66-79
...@@ -1,9 +1,9 @@...@@ -1,9 +1,9 @@
1use crate::{1use crate::{
2 Error, ExGuid, Object, ObjectData, RevisionIndex, Store,2 Error, ExGuid, Object, ObjectData,
3 active::{ActivePage, Changes},
3 create::{current_timestamps, default_text_style, properties, string},4 create::{current_timestamps, default_text_style, properties, string},
4 document::{Document, Element, Kind},5 document::Kind,
5 edit::{editable_parents, page_title},6 write::{PropertyObject, fresh_guid},
6 write::{PropertyObject, fresh_guid, write_revision_on},
7};7};
8use serde::{Deserialize, Serialize};8use serde::{Deserialize, Serialize};
9use std::{9use std::{
...@@ -191,20 +191,15 @@ impl Insertion {...@@ -191,20 +191,15 @@ impl Insertion {
191191
192 pub(crate) fn apply(&self, source: &[u8], space: ExGuid) -> Result<Vec<u8>, Error> {192 pub(crate) fn apply(&self, source: &[u8], space: ExGuid) -> Result<Vec<u8>, Error> {
193 self.validate()?;193 self.validate()?;
194 let store = Store::parse(source)?;194 crate::active::write(source, space, |active| self.changes(active))
195 let index = RevisionIndex::parse(&store)?;195 }
196 index.validate_current()?;196
197 let mut document = Document::parse(&index)?;197 pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> {
198 let pages = document.pages_in(space)?;198 let [page] = active.pages.as_slice() else {
199 let [page] = pages.as_slice() else {
200 return Err(invalid("Insertion requires a single active page"));199 return Err(invalid("Insertion requires a single active page"));
201 };200 };
202 let mut view = document201 let view = &active.view;
203 .spaces202 let parents = active.editable_parents(self.parent)?;
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)?;
208 let parent = &view.nodes[&self.parent];203 let parent = &view.nodes[&self.parent];
209 let position = match self.placement {204 let position = match self.placement {
210 Placement::Paragraph { before } => {205 Placement::Paragraph { before } => {
...@@ -363,82 +358,74 @@ impl Insertion {...@@ -363,82 +358,74 @@ impl Insertion {
363 },358 },
364 );359 );
365 }360 }
366 let raw = index.resolve_active(space)?;361 let raw = &active.live.revision;
367 if new.keys().any(|id| raw.objects.contains_key(id)) {362 if new.keys().any(|id| raw.objects.contains_key(id)) {
368 return Err(invalid(363 return Err(invalid(
369 "An insertion identity is already present; reconcile the existing edit",364 "An insertion identity is already present; reconcile the existing edit",
370 ));365 ));
371 }366 }
372 // Drop the semantic view before moving the property bytes it borrows.367 // Drop the overlay before moving the property bytes it borrows.
373 let title = {368 let title = {
374 view.nodes369 let mut overlay = BTreeMap::new();
375 .get_mut(&self.parent)370 let mut parent = view.nodes[&self.parent].clone();
376 .unwrap()371 parent.children.insert(position, self.object());
377 .children372 overlay.insert(self.parent, parent);
378 .insert(position, self.object());
379 for (id, object) in &new {373 for (id, object) in &new {
380 view.nodes.insert(374 overlay.insert(
381 *id,375 *id,
382 Element::parse(376 active.element(&Object {
383 &Object {377 jcid: object.jcid,
384 jcid: object.jcid,378 reference_count: 0,
385 reference_count: 0,379 data: ObjectData::Properties(&object.bytes),
386 data: ObjectData::Properties(&object.bytes),380 global_ids: Arc::clone(&object.global_ids),
387 global_ids: Arc::clone(&object.global_ids),381 })?,
388 },
389 &store,
390 )?,
391 );382 );
392 }383 }
393 let title = page_title(&view, &pages, None)?;384 active.title(&overlay, None)?
394 drop(view);
395 title
396 };385 };
397 write_revision_on(&index, space, |raw| {386 let mut changed = new;
398 let mut changed = new;387 for id in &ancestors {
399 for id in &ancestors {388 let mut object = PropertyObject::from_object(&raw.objects[id])?;
400 let mut object = PropertyObject::from_object(&raw.objects[id])?;389 object.set(&[(0x14001d7a, &modified)])?;
401 object.set(&[(0x14001d7a, &modified)])?;390 changed.insert(*id, object);
402 changed.insert(*id, object);391 }
403 }392 let parent = changed.get_mut(&self.parent).unwrap();
404 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 { .. }) {
405 let properties = crate::PropertySets::parse(&parent.bytes)?;407 let properties = crate::PropertySets::parse(&parent.bytes)?;
406 let existing = properties.sets[0].iter().find(|p| p.id == 0x24001c20);408 if !properties.sets[0].iter().any(|p| p.id == 0x0c001c03) {
407 let mut ids = match existing.map(|p| &p.value) {409 parent.set(&[(0x0c001c03, &[1])])?;
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 }
423 }410 }
424 if let Some((page, automatic, title)) = title {411 }
425 let metadata = raw412 if let Some((page, automatic, title)) = title {
426 .roots413 let metadata = raw
427 .get(&2)414 .roots
428 .ok_or_else(|| invalid("Page title metadata is unavailable"))?;415 .get(&2)
429 if raw.objects[metadata].jcid != 0x20030 {416 .ok_or_else(|| invalid("Page title metadata is unavailable"))?;
430 return Err(invalid("Page title metadata is unavailable"));417 if raw.objects[metadata].jcid != 0x20030 {
431 }418 return Err(invalid("Page title metadata is unavailable"));
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] })])?;
440 }419 }
441 Ok(changed)420 let title = string(&title);
442 })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)
443 }430 }
444}431}
crates/onestore/src/lib.rs+1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1#![forbid(unsafe_code)]1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]2#![doc = include_str!("../README.md")]
33
4mod active;
4mod bytes;5mod bytes;
5mod commit;6mod commit;
6mod create;7mod create;
crates/onestore/src/objects.rs+6-1
...@@ -82,6 +82,11 @@ impl Object<'_> {...@@ -82,6 +82,11 @@ impl Object<'_> {
8282
83impl ResolvedRevision<'_> {83impl ResolvedRevision<'_> {
84 pub fn reachable(&self) -> Result<BTreeSet<ExGuid>> {84 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>> {
85 let incoming = self.reference_counts()?;90 let incoming = self.reference_counts()?;
86 for (id, count) in &incoming {91 for (id, count) in &incoming {
87 if self.objects[id].reference_count != *count {92 if self.objects[id].reference_count != *count {
...@@ -91,7 +96,7 @@ impl ResolvedRevision<'_> {...@@ -91,7 +96,7 @@ impl ResolvedRevision<'_> {
91 });96 });
92 }97 }
93 }98 }
94 Ok(incoming.into_keys().collect())99 Ok(incoming)
95 }100 }
96101
97 pub(crate) fn reference_counts(&self) -> Result<BTreeMap<ExGuid, u32>> {102 pub(crate) fn reference_counts(&self) -> Result<BTreeMap<ExGuid, u32>> {
crates/onestore/src/outline.rs+39-42
...@@ -1,9 +1,10 @@...@@ -1,9 +1,10 @@
1use crate::{1use crate::{
2 Error, ExGuid, RevisionIndex, Store,2 Error, ExGuid,
3 active::{ActivePage, Changes},
3 create::current_timestamps,4 create::current_timestamps,
4 document::{Document, Kind},5 document::Kind,
5 edit::{editable_parents, update_title},6 edit::update_title,
6 write::{PropertyObject, write_revision_on},7 write::PropertyObject,
7};8};
8use serde::{Deserialize, Serialize};9use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, BTreeSet};10use std::collections::{BTreeMap, BTreeSet};
...@@ -30,6 +31,10 @@ impl OutlineEdit {...@@ -30,6 +31,10 @@ impl OutlineEdit {
30 space: ExGuid,31 space: ExGuid,
31 object: ExGuid,32 object: ExGuid,
32 ) -> Result<Vec<u8>, Error> {33 ) -> 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> {
33 let invalid = |message| Error { offset: 0, message };38 let invalid = |message| Error { offset: 0, message };
34 let values = match self {39 let values = match self {
35 Self::Position { x, y } => {40 Self::Position { x, y } => {
...@@ -54,20 +59,12 @@ impl OutlineEdit {...@@ -54,20 +59,12 @@ impl OutlineEdit {
54 }59 }
55 Self::Collapsed(value) => vec![(0x0c001c11, vec![u8::from(value)])],60 Self::Collapsed(value) => vec![(0x0c001c11, vec![u8::from(value)])],
56 };61 };
57 let store = Store::parse(source)?;62 let pages = &active.pages;
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 [page] = pages.as_slice() else {63 let [page] = pages.as_slice() else {
63 return Err(invalid("Outline editing requires a single active page"));64 return Err(invalid("Outline editing requires a single active page"));
64 };65 };
65 let view = document66 let view = &active.view;
66 .spaces67 let parents = active.editable_parents(object)?;
67 .remove(&space)
68 .and_then(crate::document::Space::into_active)
69 .unwrap();
70 let parents = editable_parents(&view, &pages, object)?;
71 let node = &view.nodes[&object];68 let node = &view.nodes[&object];
72 match self {69 match self {
73 Self::Collapsed(_) => {70 Self::Collapsed(_) => {
...@@ -105,34 +102,33 @@ impl OutlineEdit {...@@ -105,34 +102,33 @@ impl OutlineEdit {
105 pending.extend(parents.get(&id).into_iter().flatten().copied());102 pending.extend(parents.get(&id).into_iter().flatten().copied());
106 }103 }
107 let modified = current_timestamps()?.0.to_le_bytes();104 let modified = current_timestamps()?.0.to_le_bytes();
108 write_revision_on(&index, space, |raw| {105 let raw = &active.live.revision;
109 let mut target = PropertyObject::from_object(&raw.objects[&object])?;106 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
110 target.set(107 target.set(
111 &values108 &values
112 .iter()109 .iter()
113 .map(|(id, data)| (*id, data.as_slice()))110 .map(|(id, data)| (*id, data.as_slice()))
114 .collect::<Vec<_>>(),111 .collect::<Vec<_>>(),
115 )?;112 )?;
116 if matches!(self, Self::Width { .. }) {113 if matches!(self, Self::Width { .. }) {
117 target.remove(&[0x14001cdb])?;114 target.remove(&[0x14001cdb])?;
118 }115 }
119 if raw.objects[&object].data == crate::ObjectData::Properties(&target.bytes) {116 if raw.objects[&object].data == crate::ObjectData::Properties(&target.bytes) {
120 return Ok(BTreeMap::new());117 return Ok(BTreeMap::new());
121 }118 }
122 target.set(&[(0x14001d7a, &modified)])?;119 target.set(&[(0x14001d7a, &modified)])?;
123 let mut changed = BTreeMap::from([(object, target)]);120 let mut changed = BTreeMap::from([(object, target)]);
124 for id in ancestors {121 for id in ancestors {
125 if id != object {122 if id != object {
126 let mut ancestor = PropertyObject::from_object(&raw.objects[&id])?;123 let mut ancestor = PropertyObject::from_object(&raw.objects[&id])?;
127 ancestor.set(&[(0x14001d7a, &modified)])?;124 ancestor.set(&[(0x14001d7a, &modified)])?;
128 changed.insert(id, ancestor);125 changed.insert(id, ancestor);
129 }
130 }
131 if matches!(self, Self::Position { .. }) {
132 update_title(&store, raw, view, &pages, &mut changed)?;
133 }126 }
134 Ok(changed)127 }
135 })128 if matches!(self, Self::Position { .. }) {
129 update_title(active, view.clone(), &mut changed)?;
130 }
131 Ok(changed)
136 }132 }
137}133}
138134
...@@ -140,6 +136,7 @@ impl OutlineEdit {...@@ -140,6 +136,7 @@ impl OutlineEdit {
140mod tests {136mod tests {
141 use super::*;137 use super::*;
142 use crate::write::write_revision;138 use crate::write::write_revision;
139 use crate::{RevisionIndex, Store, document::Document};
143140
144 #[test]141 #[test]
145 fn protection_on_the_target_or_ancestor_prevents_layout_edits() {142 fn protection_on_the_target_or_ancestor_prevents_layout_edits() {
crates/onestore/src/page/write.rs+572-572
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1//! Publishes an edited page model by lowering the difference from the stored page onto1//! 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
4use super::{5use super::{
5 Attachment, Image, Ink, Math, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table,6 Attachment, Image, Ink, Math, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table,
...@@ -7,9 +8,11 @@ use super::{...@@ -7,9 +8,11 @@ use super::{
7use crate::{8use crate::{
8 Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets,9 Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets,
9 RevisionIndex, Store, TextAttribute, TreeEdit, Value,10 RevisionIndex, Store, TextAttribute, TreeEdit, Value,
10 document::{Document, Format, Kind, Tag},11 active::{ActivePage, Changes},
12 document::{Format, Kind, Tag},
11 write::{PropertyObject, RevisionEdit},13 write::{PropertyObject, RevisionEdit},
12};14};
15use bumpalo::Bump;
13use std::{16use std::{
14 collections::{BTreeMap, BTreeSet},17 collections::{BTreeMap, BTreeSet},
15 ops::Range,18 ops::Range,
...@@ -339,38 +342,36 @@ pub(crate) fn write_page(...@@ -339,38 +342,36 @@ pub(crate) fn write_page(
339 if author.contains('\0') {342 if author.contains('\0') {
340 return Err(invalid("Choose an author name without NUL"));343 return Err(invalid("Choose an author name without NUL"));
341 }344 }
342 // The parsed source is released before the writers parse their own images.345 let store = Store::parse(source)?;
343 let (page, before, existing) = {346 let index = RevisionIndex::parse(&store)?;
344 let store = Store::parse(source)?;347 let arena = Bump::new();
345 let index = RevisionIndex::parse(&store)?;348 let active = ActivePage::parse(&index, space)?;
346 index.validate_current()?;349 let [page] = active.pages[..] else {
347 let document = Document::parse(&index)?;350 return Err(invalid("Choose an object space containing one active page"));
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)
359 };351 };
352 let before = Page::from_revision(&active.view, page)?;
360 let mut lowering = Lowering {353 let mut lowering = Lowering {
361 image: source.to_vec(),354 active,
355 arena: &arena,
362 current: Some(before.clone()),356 current: Some(before.clone()),
363 space,357 edited: false,
364 page,358 page,
365 author,359 author,
366 alias: BTreeMap::new(),360 alias: BTreeMap::new(),
367 built: BTreeSet::new(),361 built: BTreeSet::new(),
368 };362 };
369 lowering.run(&before, after, &existing)?;363 lowering.run(&before, after)?;
370 if lowering.image == source {364 if !lowering.edited {
371 return Ok(lowering.image);365 return Ok(source.to_vec());
372 }366 }
373 squash(source, &lowering.image, &lowering.alias, None)367 let active = &lowering.active;
368 squash(
369 &index,
370 &[(space, &active.live.revision)],
371 &active.payloads,
372 &lowering.alias,
373 None,
374 )
374}375}
375376
376/// Direct children of every container, in model order, plus lookups by identity.377/// Direct children of every container, in model order, plus lookups by identity.
...@@ -461,10 +462,13 @@ impl<'a> View<'a> {...@@ -461,10 +462,13 @@ impl<'a> View<'a> {
461}462}
462463
463struct Lowering<'a> {464struct Lowering<'a> {
464 image: Vec<u8>,465 active: ActivePage<'a>,
465 /// The page as `image` stores it, until a writer changes the image.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.
466 current: Option<Page>,469 current: Option<Page>,
467 space: ExGuid,470 /// Whether a writer stored anything.
471 edited: bool,
468 page: ExGuid,472 page: ExGuid,
469 author: &'a str,473 author: &'a str,
470 /// Model identities of new objects mapped to the identities the typed writers allocated.474 /// Model identities of new objects mapped to the identities the typed writers allocated.
...@@ -473,7 +477,7 @@ struct Lowering<'a> {...@@ -473,7 +477,7 @@ struct Lowering<'a> {
473 built: BTreeSet<ExGuid>,477 built: BTreeSet<ExGuid>,
474}478}
475479
476impl Lowering<'_> {480impl<'a> Lowering<'a> {
477 fn id(&self, model: ExGuid) -> ExGuid {481 fn id(&self, model: ExGuid) -> ExGuid {
478 self.alias.get(&model).copied().unwrap_or(model)482 self.alias.get(&model).copied().unwrap_or(model)
479 }483 }
...@@ -488,31 +492,35 @@ impl Lowering<'_> {...@@ -488,31 +492,35 @@ impl Lowering<'_> {
488 Ok(written)492 Ok(written)
489 }493 }
490494
491 fn apply(&mut self, edit: impl FnOnce(&[u8]) -> Result<Vec<u8>, Error>) -> Result<(), Error> {495 /// Stores the next revision a typed writer computes from the page as stored so far.
492 let image = edit(&self.image)?;496 fn write(
493 if image != self.image {497 &mut self,
494 self.image = image;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)? {
495 self.current = None;510 self.current = None;
511 self.edited = true;
496 }512 }
497 Ok(())513 Ok(())
498 }514 }
499515
500 fn current(&mut self) -> Result<Page, Error> {516 fn current(&mut self) -> Result<Page, Error> {
501 if self.current.is_none() {517 if self.current.is_none() {
502 let store = Store::parse(&self.image)?;518 self.current = Some(Page::from_revision(&self.active.view, self.page)?);
503 let index = RevisionIndex::parse(&store)?;
504 let document = Document::parse(&index)?;
505 self.current = Some(Page::from_space(&document, self.space)?);
506 }519 }
507 Ok(self.current.clone().unwrap())520 Ok(self.current.clone().unwrap())
508 }521 }
509522
510 fn run(523 fn run(&mut self, before: &Page, after: &Page) -> Result<(), Error> {
511 &mut self,
512 before: &Page,
513 after: &Page,
514 existing: &BTreeSet<ExGuid>,
515 ) -> Result<(), Error> {
516 if after.created != before.created || after.margin_origin != before.margin_origin {524 if after.created != before.created || after.margin_origin != before.margin_origin {
517 return Err(invalid("Page creation time and margins cannot be edited"));525 return Err(invalid("Page creation time and margins cannot be edited"));
518 }526 }
...@@ -540,7 +548,7 @@ impl Lowering<'_> {...@@ -540,7 +548,7 @@ impl Lowering<'_> {
540 for id in new.outlines.keys().chain(new.paragraphs.keys()) {548 for id in new.outlines.keys().chain(new.paragraphs.keys()) {
541 if !old.outlines.contains_key(id)549 if !old.outlines.contains_key(id)
542 && !old.paragraphs.contains_key(id)550 && !old.paragraphs.contains_key(id)
543 && existing.contains(id)551 && self.active.live.revision.objects.contains_key(id)
544 {552 {
545 return Err(invalid(553 return Err(invalid(
546 "A new model identity already exists in the section",554 "A new model identity already exists in the section",
...@@ -730,8 +738,7 @@ impl Lowering<'_> {...@@ -730,8 +738,7 @@ impl Lowering<'_> {
730 }738 }
731 };739 };
732 let insertion = Insertion::outline(self.page, x, y, text, self.author)?;740 let insertion = Insertion::outline(self.page, x, y, text, self.author)?;
733 let space = self.space;741 self.write(|active| insertion.changes(active))?;
734 self.apply(|image| insertion.apply(image, space))?;
735 let outline_id = insertion.object();742 let outline_id = insertion.object();
736 self.alias.insert(*id, outline_id);743 self.alias.insert(*id, outline_id);
737 self.alias.insert(744 self.alias.insert(
...@@ -809,8 +816,7 @@ impl Lowering<'_> {...@@ -809,8 +816,7 @@ impl Lowering<'_> {
809 continue;816 continue;
810 }817 }
811 let split = ParagraphSplit::new(previous.id, offset, self.author)?;818 let split = ParagraphSplit::new(previous.id, offset, self.author)?;
812 let space = self.space;819 self.write(|active| split.changes(active))?;
813 self.apply(|image| split.apply(image, space))?;
814 self.alias.insert(right, split.object());820 self.alias.insert(right, split.object());
815 self.alias.insert(next.id, split.text_object());821 self.alias.insert(next.id, split.text_object());
816 let list = placed.get_mut(container).unwrap();822 let list = placed.get_mut(container).unwrap();
...@@ -855,8 +861,7 @@ impl Lowering<'_> {...@@ -855,8 +861,7 @@ impl Lowering<'_> {
855 ));861 ));
856 }862 }
857 let join = ParagraphJoin::new(previous.id, removed.id, self.author)?;863 let join = ParagraphJoin::new(previous.id, removed.id, self.author)?;
858 let space = self.space;864 self.write(|active| join.changes(active))?;
859 self.apply(|image| join.apply(image, space))?;
860 consumed.insert(right);865 consumed.insert(right);
861 placed.get_mut(container).unwrap().retain(|id| *id != right);866 placed.get_mut(container).unwrap().retain(|id| *id != right);
862 placed.remove(&right);867 placed.remove(&right);
...@@ -905,8 +910,7 @@ impl Lowering<'_> {...@@ -905,8 +910,7 @@ impl Lowering<'_> {
905 next.map(|n| self.id(n)),910 next.map(|n| self.id(n)),
906 self.author,911 self.author,
907 )?;912 )?;
908 let space = self.space;913 self.write(|active| edit.changes(active))?;
909 self.apply(|image| edit.apply(image, space))?;
910 }914 }
911 next = Some(*id);915 next = Some(*id);
912 }916 }
...@@ -1037,7 +1041,6 @@ impl Lowering<'_> {...@@ -1037,7 +1041,6 @@ impl Lowering<'_> {
1037 values.push((0x1c001d9d, crate::create::string(path)));1041 values.push((0x1c001d9d, crate::create::string(path)));
1038 }1042 }
1039 let holder = self.id(*paragraph_id);1043 let holder = self.id(*paragraph_id);
1040 let space = self.space;
1041 let mut payloads: Vec<([u8; 16], &[u8])> = vec![(payload_guid, bytes)];1044 let mut payloads: Vec<([u8; 16], &[u8])> = vec![(payload_guid, bytes)];
1042 let mut preview = None;1045 let mut preview = None;
1043 if let Some(icon) = &attachment.preview {1046 if let Some(icon) = &attachment.preview {
...@@ -1054,35 +1057,34 @@ impl Lowering<'_> {...@@ -1054,35 +1057,34 @@ impl Lowering<'_> {
1054 payload_reference(guid),1057 payload_reference(guid),
1055 ));1058 ));
1056 }1059 }
1057 self.apply(|current| {1060 self.write_with(&payloads, |active| {
1058 crate::write::write_revision_with_payloads(current, space, &payloads, |raw| {1061 let raw = &active.live.revision;
1059 let mut changed = BTreeMap::new();1062 let mut changed = BTreeMap::new();
1060 let mut file = PropertyObject::file(file_id, &reference, extension)?;1063 let mut file = PropertyObject::file(file_id, &reference, extension)?;
1061 file.jcid = 0x80036;1064 file.jcid = 0x80036;
1062 changed.insert(file_id, file);1065 changed.insert(file_id, file);
1063 let mut node = PropertyObject {1066 let mut node = PropertyObject {
1064 jcid: 0x60035,1067 jcid: 0x60035,
1065 bytes: crate::create::properties(&values)?,1068 bytes: crate::create::properties(&values)?,
1066 global_ids: std::sync::Arc::new(BTreeMap::from([(0, attachment_id.guid)])),1069 global_ids: std::sync::Arc::new(BTreeMap::from([(0, attachment_id.guid)])),
1067 };1070 };
1068 node.reference(attachment_id)?;1071 node.reference(attachment_id)?;
1069 let container = node.reference(file_id)?;1072 let container = node.reference(file_id)?;
1070 node.set(&[(0x20001d9b, &container)])?;1073 node.set(&[(0x20001d9b, &container)])?;
1071 if let Some((icon_id, icon_reference)) = &preview {1074 if let Some((icon_id, icon_reference)) = &preview {
1072 changed.insert(1075 changed.insert(
1073 *icon_id,1076 *icon_id,
1074 PropertyObject::file(*icon_id, icon_reference, ".png")?,1077 PropertyObject::file(*icon_id, icon_reference, ".png")?,
1075 );1078 );
1076 let icon = node.reference(*icon_id)?;1079 let icon = node.reference(*icon_id)?;
1077 node.set(&[(0x20001c3f, &icon)])?;1080 node.set(&[(0x20001c3f, &icon)])?;
1078 }1081 }
1079 changed.insert(attachment_id, node);1082 changed.insert(attachment_id, node);
1080 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;1083 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1081 let content = object.reference(attachment_id)?;1084 let content = object.reference(attachment_id)?;
1082 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;1085 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;
1083 changed.insert(holder, object);1086 changed.insert(holder, object);
1084 Ok(changed)1087 Ok(changed)
1085 })
1086 })?;1088 })?;
1087 }1089 }
1088 Ok(())1090 Ok(())
...@@ -1201,49 +1203,47 @@ impl Lowering<'_> {...@@ -1201,49 +1203,47 @@ impl Lowering<'_> {
1201 }1203 }
1202 let (modified, filetime) = crate::create::current_timestamps()?;1204 let (modified, filetime) = crate::create::current_timestamps()?;
1203 let modified = modified.to_le_bytes();1205 let modified = modified.to_le_bytes();
1204 let (space, page) = (self.space, self.page);1206 let page = self.page;
1205 self.apply(|current| {1207 self.write(|active| {
1206 crate::write::write_revision(current, space, |raw| {1208 let raw = &active.live.revision;
1207 let mut changed = BTreeMap::new();1209 let mut changed = BTreeMap::new();
1208 let mut data_object = PropertyObject {1210 let mut data_object = PropertyObject {
1209 jcid: 0x2003b,1211 jcid: 0x2003b,
1210 bytes: crate::create::properties(&[])?,1212 bytes: crate::create::properties(&[])?,
1211 global_ids: std::sync::Arc::new(BTreeMap::from([(0, data.guid)])),1213 global_ids: std::sync::Arc::new(BTreeMap::from([(0, data.guid)])),
1212 };1214 };
1213 data_object.reference(data)?;1215 data_object.reference(data)?;
1214 let references =1216 let references = write_strokes(&mut changed, &mut data_object, &strokes, 0, filetime)?;
1215 write_strokes(&mut changed, &mut data_object, &strokes, 0, filetime)?;1217 data_object.set(&[(0x24003416, &references)])?;
1216 data_object.set(&[(0x24003416, &references)])?;1218 changed.insert(data, data_object);
1217 changed.insert(data, data_object);1219 let mut object = PropertyObject {
1218 let mut object = PropertyObject {1220 jcid: 0x60014,
1219 jcid: 0x60014,1221 bytes: crate::create::properties(&[
1220 bytes: crate::create::properties(&[1222 (0x14001d7a, modified.to_vec()),
1221 (0x14001d7a, modified.to_vec()),1223 (0x14001d4e, 1u32.to_le_bytes().to_vec()),
1222 (0x14001d4e, 1u32.to_le_bytes().to_vec()),1224 ])?,
1223 ])?,1225 global_ids: std::sync::Arc::new(BTreeMap::from([(0, container.guid)])),
1224 global_ids: std::sync::Arc::new(BTreeMap::from([(0, container.guid)])),1226 };
1225 };1227 object.reference(container)?;
1226 object.reference(container)?;1228 let data_reference = object.reference(data)?;
1227 let data_reference = object.reference(data)?;1229 object.set(&[(0x20003415, &data_reference)])?;
1228 object.set(&[(0x20003415, &data_reference)])?;1230 changed.insert(container, object);
1229 changed.insert(container, object);1231 match holder {
1230 match holder {1232 Some(holder) => {
1231 Some(holder) => {1233 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1232 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;1234 let content = object.reference(container)?;
1233 let content = object.reference(container)?;1235 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;
1234 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;1236 changed.insert(holder, object);
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 }
1244 }1237 }
1245 Ok(changed)1238 None => {
1246 })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)
1247 })1247 })
1248 }1248 }
12491249
...@@ -1266,47 +1266,41 @@ impl Lowering<'_> {...@@ -1266,47 +1266,41 @@ impl Lowering<'_> {
1266 }1266 }
1267 }1267 }
1268 let container = self.id(ink.id);1268 let container = self.id(ink.id);
1269 let data = {1269 let data = match self
1270 let store = Store::parse(&self.image)?;1270 .active
1271 let index = RevisionIndex::parse(&store)?;1271 .view
1272 let document = Document::parse(&index)?;1272 .nodes
1273 let revision = document1273 .get(&container)
1274 .spaces1274 .map(|node| &node.kind)
1275 .get(&self.space)1275 {
1276 .and_then(crate::document::Space::active)1276 Some(crate::document::Kind::Ink {
1277 .ok_or_else(|| invalid("The page space is unavailable"))?;1277 data: Some(data), ..
1278 match revision.nodes.get(&container).map(|node| &node.kind) {1278 }) => *data,
1279 Some(crate::document::Kind::Ink {1279 _ => return Err(invalid("Stored ink has no stroke data to rewrite")),
1280 data: Some(data), ..
1281 }) => *data,
1282 _ => return Err(invalid("Stored ink has no stroke data to rewrite")),
1283 }
1284 };1280 };
1285 let (modified, filetime) = crate::create::current_timestamps()?;1281 let (modified, filetime) = crate::create::current_timestamps()?;
1286 let modified = modified.to_le_bytes();1282 let modified = modified.to_le_bytes();
1287 let space = self.space;1283 self.write(|active| {
1288 self.apply(|current| {1284 let raw = &active.live.revision;
1289 crate::write::write_revision(current, space, |raw| {1285 let mut changed = BTreeMap::new();
1290 let mut changed = BTreeMap::new();1286 let mut data_object = PropertyObject::from_object(&raw.objects[&data])?;
1291 let mut data_object = PropertyObject::from_object(&raw.objects[&data])?;1287 let mut references = Vec::new();
1292 let mut references = Vec::new();1288 for id in &kept {
1293 for id in &kept {1289 references.extend(data_object.reference(*id)?);
1294 references.extend(data_object.reference(*id)?);1290 }
1295 }1291 references.extend(write_strokes(
1296 references.extend(write_strokes(1292 &mut changed,
1297 &mut changed,1293 &mut data_object,
1298 &mut data_object,1294 &added,
1299 &added,1295 kept.len(),
1300 kept.len(),1296 filetime,
1301 filetime,1297 )?);
1302 )?);1298 data_object.set(&[(0x24003416, &references)])?;
1303 data_object.set(&[(0x24003416, &references)])?;1299 changed.insert(data, data_object);
1304 changed.insert(data, data_object);1300 let mut object = PropertyObject::from_object(&raw.objects[&container])?;
1305 let mut object = PropertyObject::from_object(&raw.objects[&container])?;1301 object.set(&[(0x14001d7a, &modified)])?;
1306 object.set(&[(0x14001d7a, &modified)])?;1302 changed.insert(container, object);
1307 changed.insert(container, object);1303 Ok(changed)
1308 Ok(changed)
1309 })
1310 })1304 })
1311 }1305 }
13121306
...@@ -1349,44 +1343,38 @@ impl Lowering<'_> {...@@ -1349,44 +1343,38 @@ impl Lowering<'_> {
1349 values.push((0x08001d13 | (1 << 31), Vec::new()));1343 values.push((0x08001d13 | (1 << 31), Vec::new()));
1350 }1344 }
1351 values.push((0x08001d85, Vec::new()));1345 values.push((0x08001d85, Vec::new()));
1352 let (space, page) = (self.space, self.page);1346 let page = self.page;
1353 let payload: &[u8] = bytes;1347 let payload: &[u8] = bytes;
1354 self.apply(|current| {1348 self.write_with(&[(payload_guid, payload)], |active| {
1355 crate::write::write_revision_with_payloads(1349 let raw = &active.live.revision;
1356 current,1350 let mut changed = BTreeMap::new();
1357 space,1351 let file = PropertyObject::file(file_id, &reference, extension)?;
1358 &[(payload_guid, payload)],1352 changed.insert(file_id, file);
1359 |raw| {1353 let mut picture = PropertyObject {
1360 let mut changed = BTreeMap::new();1354 jcid: 0x60011,
1361 let file = PropertyObject::file(file_id, &reference, extension)?;1355 bytes: crate::create::properties(&values)?,
1362 changed.insert(file_id, file);1356 global_ids: std::sync::Arc::new(BTreeMap::from([(0, image_id.guid)])),
1363 let mut picture = PropertyObject {1357 };
1364 jcid: 0x60011,1358 picture.reference(image_id)?;
1365 bytes: crate::create::properties(&values)?,1359 let container = picture.reference(file_id)?;
1366 global_ids: std::sync::Arc::new(BTreeMap::from([(0, image_id.guid)])),1360 picture.set(&[(0x20001c3f, &container)])?;
1367 };1361 changed.insert(image_id, picture);
1368 picture.reference(image_id)?;1362 match holder {
1369 let container = picture.reference(file_id)?;1363 Some(holder) => {
1370 picture.set(&[(0x20001c3f, &container)])?;1364 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;
1371 changed.insert(image_id, picture);1365 let content = object.reference(image_id)?;
1372 match holder {1366 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;
1373 Some(holder) => {1367 changed.insert(holder, object);
1374 let mut object = PropertyObject::from_object(&raw.objects[&holder])?;1368 }
1375 let content = object.reference(image_id)?;1369 None => {
1376 object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?;1370 let mut object = PropertyObject::from_object(&raw.objects[&page])?;
1377 changed.insert(holder, object);1371 let mut children = page_children(&object)?;
1378 }1372 children.extend(object.reference(image_id)?);
1379 None => {1373 object.set(&[(0x24001c20, &children), (0x14001d7a, &modified)])?;
1380 let mut object = PropertyObject::from_object(&raw.objects[&page])?;1374 changed.insert(page, object);
1381 let mut children = page_children(&object)?;1375 }
1382 children.extend(object.reference(image_id)?);1376 }
1383 object.set(&[(0x24001c20, &children), (0x14001d7a, &modified)])?;1377 Ok(changed)
1384 changed.insert(page, object);
1385 }
1386 }
1387 Ok(changed)
1388 },
1389 )
1390 })1378 })
1391 }1379 }
13921380
...@@ -1434,18 +1422,17 @@ impl Lowering<'_> {...@@ -1434,18 +1422,17 @@ impl Lowering<'_> {
1434 None => removed.extend([0x140034cd, 0x140034ce]),1422 None => removed.extend([0x140034cd, 0x140034ce]),
1435 }1423 }
1436 }1424 }
1437 let (space, object) = (self.space, self.id(attachment.id));1425 let object = self.id(attachment.id);
1438 self.apply(|current| {1426 self.write(|active| {
1439 crate::write::write_revision(current, space, |raw| {1427 let raw = &active.live.revision;
1440 let mut node = PropertyObject::from_object(&raw.objects[&object])?;1428 let mut node = PropertyObject::from_object(&raw.objects[&object])?;
1441 node.remove(&removed)?;1429 node.remove(&removed)?;
1442 let values: Vec<(u32, &[u8])> = values1430 let values: Vec<(u32, &[u8])> = values
1443 .iter()1431 .iter()
1444 .map(|(id, bytes)| (*id, bytes.as_slice()))1432 .map(|(id, bytes)| (*id, bytes.as_slice()))
1445 .collect();1433 .collect();
1446 node.set(&values)?;1434 node.set(&values)?;
1447 Ok(BTreeMap::from([(object, node)]))1435 Ok(BTreeMap::from([(object, node)]))
1448 })
1449 })1436 })
1450 }1437 }
14511438
...@@ -1466,18 +1453,17 @@ impl Lowering<'_> {...@@ -1466,18 +1453,17 @@ impl Lowering<'_> {
1466 None => removed.push(0x1c001e58),1453 None => removed.push(0x1c001e58),
1467 }1454 }
1468 }1455 }
1469 let (space, object) = (self.space, self.id(image.id));1456 let object = self.id(image.id);
1470 self.apply(|current| {1457 self.write(|active| {
1471 crate::write::write_revision(current, space, |raw| {1458 let raw = &active.live.revision;
1472 let mut picture = PropertyObject::from_object(&raw.objects[&object])?;1459 let mut picture = PropertyObject::from_object(&raw.objects[&object])?;
1473 picture.remove(&removed)?;1460 picture.remove(&removed)?;
1474 let values: Vec<(u32, &[u8])> = values1461 let values: Vec<(u32, &[u8])> = values
1475 .iter()1462 .iter()
1476 .map(|(id, bytes)| (*id, bytes.as_slice()))1463 .map(|(id, bytes)| (*id, bytes.as_slice()))
1477 .collect();1464 .collect();
1478 picture.set(&values)?;1465 picture.set(&values)?;
1479 Ok(BTreeMap::from([(object, picture)]))1466 Ok(BTreeMap::from([(object, picture)]))
1480 })
1481 })1467 })
1482 }1468 }
14831469
...@@ -1540,8 +1526,7 @@ impl Lowering<'_> {...@@ -1540,8 +1526,7 @@ impl Lowering<'_> {
1540 };1526 };
1541 let insertion =1527 let insertion =
1542 Insertion::paragraph(self.id(*container), anchor, text, self.author)?;1528 Insertion::paragraph(self.id(*container), anchor, text, self.author)?;
1543 let space = self.space;1529 self.write(|active| insertion.changes(active))?;
1544 self.apply(|image| insertion.apply(image, space))?;
1545 self.alias.insert(*id, insertion.object());1530 self.alias.insert(*id, insertion.object());
1546 if let Some(text_id) = text_id {1531 if let Some(text_id) = text_id {
1547 self.alias.insert(text_id, insertion.text_object());1532 self.alias.insert(text_id, insertion.text_object());
...@@ -1549,8 +1534,7 @@ impl Lowering<'_> {...@@ -1549,8 +1534,7 @@ impl Lowering<'_> {
1549 } else if !kept.contains(id) {1534 } else if !kept.contains(id) {
1550 let edit =1535 let edit =
1551 TreeEdit::move_to(self.id(*id), self.id(*container), anchor, self.author)?;1536 TreeEdit::move_to(self.id(*id), self.id(*container), anchor, self.author)?;
1552 let space = self.space;1537 self.write(|active| edit.changes(active))?;
1553 self.apply(|image| edit.apply(image, space))?;
1554 }1538 }
1555 next = Some(*id);1539 next = Some(*id);
1556 }1540 }
...@@ -1607,19 +1591,17 @@ impl Lowering<'_> {...@@ -1607,19 +1591,17 @@ impl Lowering<'_> {
1607 values.push((0x1c001c12, measurement_bytes(&cell.indents, 4)?));1591 values.push((0x1c001c12, measurement_bytes(&cell.indents, 4)?));
1608 }1592 }
1609 }1593 }
1610 let (space, object) = (self.space, self.id(cell.id));1594 let object = self.id(cell.id);
1611 self.apply(|current| {1595 self.write(|active| {
1612 crate::write::write_revision(current, space, |raw| {1596 let raw = &active.live.revision;
1613 let mut object_properties =1597 let mut object_properties = PropertyObject::from_object(&raw.objects[&object])?;
1614 PropertyObject::from_object(&raw.objects[&object])?;1598 object_properties.remove(&removed)?;
1615 object_properties.remove(&removed)?;1599 let values: Vec<(u32, &[u8])> = values
1616 let values: Vec<(u32, &[u8])> = values1600 .iter()
1617 .iter()1601 .map(|(id, bytes)| (*id, bytes.as_slice()))
1618 .map(|(id, bytes)| (*id, bytes.as_slice()))1602 .collect();
1619 .collect();1603 object_properties.set(&values)?;
1620 object_properties.set(&values)?;1604 Ok(BTreeMap::from([(object, object_properties)]))
1621 Ok(BTreeMap::from([(object, object_properties)]))
1622 })
1623 })?;1605 })?;
1624 }1606 }
1625 }1607 }
...@@ -1755,56 +1737,54 @@ impl Lowering<'_> {...@@ -1755,56 +1737,54 @@ impl Lowering<'_> {
1755 }1737 }
1756 let holder = self.id(*paragraph_id);1738 let holder = self.id(*paragraph_id);
1757 let is_new = previous.is_none();1739 let is_new = previous.is_none();
1758 let space = self.space;1740 self.write(|active| {
1759 self.apply(|image| {1741 let raw = &active.live.revision;
1760 crate::write::write_revision(image, space, |raw| {1742 let mut changed: BTreeMap<ExGuid, PropertyObject> = BTreeMap::new();
1761 let mut changed: BTreeMap<ExGuid, PropertyObject> = BTreeMap::new();1743 for (id, jcid, values) in &created {
1762 for (id, jcid, values) in &created {1744 let mut node = PropertyObject {
1763 let mut node = PropertyObject {1745 jcid: *jcid,
1764 jcid: *jcid,1746 bytes: crate::create::properties(values)?,
1765 bytes: crate::create::properties(values)?,1747 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
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])?,
1787 };1748 };
1788 for (row_id, _) in &rows {1749 node.reference(*id)?;
1789 row_refs.extend_from_slice(&table_object.reference(*row_id)?);1750 changed.insert(*id, node);
1790 }1751 }
1791 table_object.remove(&[0x1c001d7d, 0x08001d5e])?;1752 let mut row_refs = Vec::new();
1792 table_object.set(1753 for (row_id, cells) in &rows {
1793 &table_values1754 let mut row = match changed.remove(row_id) {
1794 .iter()1755 Some(row) => row,
1795 .map(|(id, bytes)| (*id, bytes.as_slice()))1756 None => PropertyObject::from_object(&raw.objects[row_id])?,
1796 .collect::<Vec<_>>(),1757 };
1797 )?;1758 let mut cell_refs = Vec::new();
1798 table_object.set(&[(0x24001c20, &row_refs)])?;1759 for cell in cells {
1799 changed.insert(table_id, table_object);1760 cell_refs.extend_from_slice(&row.reference(*cell)?);
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);
1805 }1761 }
1806 Ok(changed)1762 row.set(&[(0x24001c20, &cell_refs), (0x14001d7a, &modified)])?;
1807 })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)
1808 })?;1788 })?;
1809 }1789 }
1810 Ok(())1790 Ok(())
...@@ -1846,8 +1826,7 @@ impl Lowering<'_> {...@@ -1846,8 +1826,7 @@ impl Lowering<'_> {
1846 continue;1826 continue;
1847 }1827 }
1848 let edit = TreeEdit::delete(*id, self.author)?;1828 let edit = TreeEdit::delete(*id, self.author)?;
1849 let space = self.space;1829 self.write(|active| edit.changes(active))?;
1850 self.apply(|image| edit.apply(image, space))?;
1851 }1830 }
1852 let kept: BTreeSet<ExGuid> = page_images(new.page)1831 let kept: BTreeSet<ExGuid> = page_images(new.page)
1853 .map(|(id, _)| id)1832 .map(|(id, _)| id)
...@@ -1859,8 +1838,7 @@ impl Lowering<'_> {...@@ -1859,8 +1838,7 @@ impl Lowering<'_> {
1859 {1838 {
1860 if !kept.contains(&id) {1839 if !kept.contains(&id) {
1861 let edit = TreeEdit::delete(id, self.author)?;1840 let edit = TreeEdit::delete(id, self.author)?;
1862 let space = self.space;1841 self.write(|active| edit.changes(active))?;
1863 self.apply(|image| edit.apply(image, space))?;
1864 }1842 }
1865 }1843 }
1866 for id in old.outlines.keys() {1844 for id in old.outlines.keys() {
...@@ -1869,8 +1847,7 @@ impl Lowering<'_> {...@@ -1869,8 +1847,7 @@ impl Lowering<'_> {
1869 return Err(invalid("Title outlines cannot be removed"));1847 return Err(invalid("Title outlines cannot be removed"));
1870 }1848 }
1871 let edit = TreeEdit::delete(*id, self.author)?;1849 let edit = TreeEdit::delete(*id, self.author)?;
1872 let space = self.space;1850 self.write(|active| edit.changes(active))?;
1873 self.apply(|image| edit.apply(image, space))?;
1874 }1851 }
1875 }1852 }
1876 Ok(())1853 Ok(())
...@@ -1943,56 +1920,54 @@ impl Lowering<'_> {...@@ -1943,56 +1920,54 @@ impl Lowering<'_> {
1943 ends.truncate(ends.len() - 4);1920 ends.truncate(ends.len() - 4);
1944 let has_objects = sets.iter().any(|set| !set.is_empty());1921 let has_objects = sets.iter().any(|set| !set.is_empty());
1945 let modified = crate::create::current_timestamps()?.0.to_le_bytes();1922 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
1946 let space = self.space;1923 self.write(|active| {
1947 self.apply(|current| {1924 let raw = &active.live.revision;
1948 crate::write::write_revision(current, space, |raw| {1925 let mut changed = BTreeMap::new();
1949 let mut changed = BTreeMap::new();1926 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
1950 let mut target = PropertyObject::from_object(&raw.objects[&object])?;1927 let mut references = Vec::new();
1951 let mut references = Vec::new();1928 let mut created: Vec<(Values, ExGuid)> = Vec::new();
1952 let mut created: Vec<(Values, ExGuid)> = Vec::new();1929 for values in &styles {
1953 for values in &styles {1930 let id = match created.iter().find(|(known, _)| known == values) {
1954 let id = match created.iter().find(|(known, _)| known == values) {1931 Some((_, id)) => *id,
1955 Some((_, id)) => *id,1932 None => {
1956 None => {1933 let id = ExGuid {
1957 let id = ExGuid {1934 guid: crate::write::fresh_guid()?,
1958 guid: crate::write::fresh_guid()?,1935 n: 1,
1959 n: 1,1936 };
1960 };1937 let mut style = PropertyObject {
1961 let mut style = PropertyObject {1938 jcid: 0x12004d,
1962 jcid: 0x12004d,1939 bytes: crate::create::properties(values)?,
1963 bytes: crate::create::properties(values)?,1940 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
1964 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),1941 };
1965 };1942 style.reference(id)?;
1966 style.reference(id)?;1943 changed.insert(id, style);
1967 changed.insert(id, style);1944 created.push((values.clone(), id));
1968 created.push((values.clone(), id));1945 id
1969 id1946 }
1970 }1947 };
1971 };1948 references.extend(target.reference(id)?);
1972 references.extend(target.reference(id)?);1949 }
1973 }1950 target.remove(&[0x1c003498, 0x40003499])?;
1974 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.
1975 target.set(&[1963 target.set(&[
1976 (0x1c001c22, &encoded),1964 (0x10001cfe, &0x7f_u16.to_le_bytes()),
1977 (0x1c001e12, &ends),1965 (0x14001c3e, &1u32.to_le_bytes()),
1978 (0x24001e13, &references),1966 (0x14001c84, &1u32.to_le_bytes()),
1979 (0x14001d7a, &modified),
1980 ])?;1967 ])?;
1981 if has_objects {1968 }
1982 target.set_sets(0x40003499, 0x44000811, &sets)?;1969 changed.insert(object, target);
1983 }1970 Ok(changed)
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 })
1996 })?;1971 })?;
1997 }1972 }
1998 Ok(())1973 Ok(())
...@@ -2016,8 +1991,8 @@ impl Lowering<'_> {...@@ -2016,8 +1991,8 @@ impl Lowering<'_> {
2016 continue;1991 continue;
2017 }1992 }
2018 let (range, replacement) = text_edit(stored.text.text(), text.text.text())?;1993 let (range, replacement) = text_edit(stored.text.text(), text.text.text())?;
2019 let (space, object) = (self.space, stored.id);1994 let object = stored.id;
2020 self.apply(|image| crate::replace_text(image, space, object, range, &replacement))?;1995 self.write(|active| crate::edit::text_changes(active, object, range, &replacement))?;
2021 }1996 }
2022 Ok(())1997 Ok(())
2023 }1998 }
...@@ -2132,70 +2107,56 @@ impl Lowering<'_> {...@@ -2132,70 +2107,56 @@ impl Lowering<'_> {
2132 }2107 }
2133 nodes.push((node_id, values));2108 nodes.push((node_id, values));
2134 }2109 }
2135 let (space, object) = (self.space, self.id(*id));2110 let object = self.id(*id);
2136 self.apply(|image| {2111 self.write(|active| {
2137 let store = Store::parse(image)?;2112 let parents = active.editable_parents(object)?;
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 )?;
2145 let modified = crate::create::current_timestamps()?.0.to_le_bytes();2113 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
2146 crate::write::write_revision(image, space, |raw| {2114 let raw = &active.live.revision;
2147 let mut changed = BTreeMap::new();2115 let mut changed = BTreeMap::new();
2148 let mut target = PropertyObject::from_object(&raw.objects[&object])?;2116 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2149 let mut references = Vec::new();2117 let mut references = Vec::new();
2150 for (list, values) in &nodes {2118 for (list, values) in &nodes {
2151 let mut node = match raw.objects.get(list) {2119 let mut node = match raw.objects.get(list) {
2152 Some(existing) => {2120 Some(existing) => {
2153 if existing.jcid != 0x60012 {2121 if existing.jcid != 0x60012 {
2154 return Err(invalid(2122 return Err(invalid(
2155 "A list definition identity belongs to another object",2123 "A list definition identity belongs to another object",
2156 ));2124 ));
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
2165 }2125 }
2166 None => PropertyObject {2126 let mut node = PropertyObject::from_object(existing)?;
2167 jcid: 0x60012,2127 node.remove(&[
2168 bytes: crate::create::properties(&[])?,2128 0x1c001c1a, 0x1c001c52, 0x14001cb7, 0x10001d0e, 0x0c001cc0,
2169 global_ids: std::sync::Arc::new(BTreeMap::from([(0, list.guid)])),2129 0x1c001c0a, 0x10001c0b, 0x14001c0c, 0x14001c3b, 0x08001c04,
2170 },2130 0x08001c05,
2171 };2131 ])?;
2172 node.set(2132 node
2173 &values2133 }
2174 .iter()2134 None => PropertyObject {
2175 .map(|(id, bytes)| (*id, bytes.as_slice()))2135 jcid: 0x60012,
2176 .collect::<Vec<_>>(),2136 bytes: crate::create::properties(&[])?,
2177 )?;2137 global_ids: std::sync::Arc::new(BTreeMap::from([(0, list.guid)])),
2178 node.set(&[(0x14001d7a, &modified)])?;2138 },
2179 node.reference(*list)?;2139 };
2180 references.extend_from_slice(&target.reference(*list)?);2140 node.set(
2181 changed.insert(*list, node);2141 &values
2182 }2142 .iter()
2183 if references.is_empty() {2143 .map(|(id, bytes)| (*id, bytes.as_slice()))
2184 target.remove(&[0x24001c26])?;2144 .collect::<Vec<_>>(),
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,
2196 )?;2145 )?;
2197 Ok(changed)2146 node.set(&[(0x14001d7a, &modified)])?;
2198 })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)
2199 })?;2160 })?;
2200 }2161 }
2201 Ok(())2162 Ok(())
...@@ -2330,51 +2291,42 @@ impl Lowering<'_> {...@@ -2330,51 +2291,42 @@ impl Lowering<'_> {
2330 }2291 }
2331 sets.push((index, fields));2292 sets.push((index, fields));
2332 }2293 }
2333 let space = self.space;2294 self.write(|active| {
2334 self.apply(|image| {2295 let parents = active.editable_parents(object)?;
2335 let store = Store::parse(image)?;2296 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
2336 let index = RevisionIndex::parse(&store)?;2297 let raw = &active.live.revision;
2337 let document = Document::parse(&index)?;2298 let mut changed = BTreeMap::new();
2338 let parents = crate::edit::editable_parents(2299 for (id, values) in &definitions {
2339 document.active(space)?,2300 let Some(values) = values else {
2340 &document.pages_in(space)?,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,
2341 object,2325 object,
2326 &modified,
2327 &mut changed,
2342 )?;2328 )?;
2343 let modified = crate::create::current_timestamps()?.0.to_le_bytes();2329 Ok(changed)
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 })
2378 })?;2330 })?;
2379 }2331 }
2380 }2332 }
...@@ -2444,25 +2396,24 @@ impl Lowering<'_> {...@@ -2444,25 +2396,24 @@ impl Lowering<'_> {
2444 }2396 }
2445 Some(values)2397 Some(values)
2446 };2398 };
2447 let (space, object) = (self.space, self.id(text.id));2399 let object = self.id(text.id);
2448 self.apply(|image| {2400 self.write(|active| {
2449 crate::write::write_revision(image, space, |raw| {2401 let raw = &active.live.revision;
2450 let mut changed = BTreeMap::new();2402 let mut changed = BTreeMap::new();
2451 if let Some(values) = &values {2403 if let Some(values) = &values {
2452 let mut node = PropertyObject {2404 let mut node = PropertyObject {
2453 jcid: 0x12004d,2405 jcid: 0x12004d,
2454 bytes: crate::create::properties(values)?,2406 bytes: crate::create::properties(values)?,
2455 global_ids: std::sync::Arc::new(BTreeMap::from([(0, style_id.guid)])),2407 global_ids: std::sync::Arc::new(BTreeMap::from([(0, style_id.guid)])),
2456 };2408 };
2457 node.reference(style_id)?;2409 node.reference(style_id)?;
2458 changed.insert(style_id, node);2410 changed.insert(style_id, node);
2459 }2411 }
2460 let mut target = PropertyObject::from_object(&raw.objects[&object])?;2412 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2461 let reference = target.reference(style_id)?;2413 let reference = target.reference(style_id)?;
2462 target.set(&[(0x2000342c, &reference)])?;2414 target.set(&[(0x2000342c, &reference)])?;
2463 changed.insert(object, target);2415 changed.insert(object, target);
2464 Ok(changed)2416 Ok(changed)
2465 })
2466 })?;2417 })?;
2467 }2418 }
2468 Ok(())2419 Ok(())
...@@ -2531,55 +2482,41 @@ impl Lowering<'_> {...@@ -2531,55 +2482,41 @@ impl Lowering<'_> {
2531 "Generated title fields cannot be formatted as ordinary text",2482 "Generated title fields cannot be formatted as ordinary text",
2532 ));2483 ));
2533 }2484 }
2534 let (space, object) = (self.space, stored.id);2485 let object = stored.id;
2535 self.apply(|image| {2486 self.write(|active| {
2536 let store = Store::parse(image)?;2487 let parents = active.editable_parents(object)?;
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 )?;
2544 let modified = crate::create::current_timestamps()?.0.to_le_bytes();2488 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
2545 crate::write::write_revision(image, space, |raw| {2489 let raw = &active.live.revision;
2546 let mut target = PropertyObject::from_object(&raw.objects[&object])?;2490 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
2547 target.set(2491 target.set(
2548 &values2492 &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]
2549 .iter()2501 .iter()
2550 .map(|(id, bytes)| (*id, bytes.as_slice()))2502 .find(|field| field.id == property)
2551 .collect::<Vec<_>>(),2503 .map(|field| match field.value {
2552 )?;2504 Value::Bytes(bytes) => bytes
2553 if let Some((_, alignment)) = values.iter().find(|(id, _)| *id == 0x0c003477) {2505 .try_into()
2554 for property in [0x14001c3e, 0x14001c84] {2506 .map(u32::from_le_bytes)
2555 let fields = PropertySets::parse(&target.bytes)?;2507 .map_err(|_| invalid("Invalid paragraph layout alignment")),
2556 let previous = fields.sets[0]2508 _ => Err(invalid("Invalid paragraph layout alignment")),
2557 .iter()2509 })
2558 .find(|field| field.id == property)2510 .transpose()?
2559 .map(|field| match field.value {2511 .unwrap_or(0);
2560 Value::Bytes(bytes) => bytes2512 let value = (previous & !7) | (u32::from(alignment[0]) + 1);
2561 .try_into()2513 target.set(&[(property, &value.to_le_bytes())])?;
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 }
2571 }2514 }
2572 target.set(&[(0x14001d7a, &modified)])?;2515 }
2573 let mut changed = BTreeMap::from([(object, target)]);2516 target.set(&[(0x14001d7a, &modified)])?;
2574 crate::formatting::touch_ancestors(2517 let mut changed = BTreeMap::from([(object, target)]);
2575 raw,2518 crate::formatting::touch_ancestors(raw, parents, object, &modified, &mut changed)?;
2576 &parents,2519 Ok(changed)
2577 object,
2578 &modified,
2579 &mut changed,
2580 )?;
2581 Ok(changed)
2582 })
2583 })?;2520 })?;
2584 }2521 }
2585 Ok(())2522 Ok(())
...@@ -2646,9 +2583,9 @@ impl Lowering<'_> {...@@ -2646,9 +2583,9 @@ impl Lowering<'_> {
2646 if attributes.is_empty() {2583 if attributes.is_empty() {
2647 continue;2584 continue;
2648 }2585 }
2649 let (space, object) = (self.space, stored.id);2586 let object = stored.id;
2650 self.apply(|image| {2587 self.write(|active| {
2651 crate::formatting::format_text(image, space, object, range, &attributes)2588 crate::formatting::format_changes(active, object, range, &attributes)
2652 })?;2589 })?;
2653 }2590 }
2654 }2591 }
...@@ -2664,9 +2601,9 @@ impl Lowering<'_> {...@@ -2664,9 +2601,9 @@ impl Lowering<'_> {
2664 .get(&self.id(*id))2601 .get(&self.id(*id))
2665 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?;2602 .ok_or_else(|| invalid("A paragraph is missing after text edits"))?;
2666 if stored.collapsed != paragraph.collapsed {2603 if stored.collapsed != paragraph.collapsed {
2667 let (space, object) = (self.space, self.id(*id));2604 let object = self.id(*id);
2668 let edit = OutlineEdit::Collapsed(paragraph.collapsed);2605 let edit = OutlineEdit::Collapsed(paragraph.collapsed);
2669 self.apply(|image| edit.apply(image, space, object))?;2606 self.write(|active| edit.changes(active, object))?;
2670 }2607 }
2671 }2608 }
2672 for (id, outline) in &new.outlines {2609 for (id, outline) in &new.outlines {
...@@ -2677,7 +2614,7 @@ impl Lowering<'_> {...@@ -2677,7 +2614,7 @@ impl Lowering<'_> {
2677 .outlines2614 .outlines
2678 .get(&self.id(*id))2615 .get(&self.id(*id))
2679 .ok_or_else(|| invalid("An outline is missing after text edits"))?;2616 .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);
2681 // A new outline takes the model's indentation table (a copied outline keeps2618 // A new outline takes the model's indentation table (a copied outline keeps
2682 // its levels' offsets); a stored table stays as it is.2619 // its levels' offsets); a stored table stays as it is.
2683 if !old.outlines.contains_key(id)2620 if !old.outlines.contains_key(id)
...@@ -2685,12 +2622,11 @@ impl Lowering<'_> {...@@ -2685,12 +2622,11 @@ impl Lowering<'_> {
2685 && outline.indents != stored.indents2622 && outline.indents != stored.indents
2686 {2623 {
2687 let indents = measurement_bytes(&outline.indents, 4)?;2624 let indents = measurement_bytes(&outline.indents, 4)?;
2688 self.apply(|current| {2625 self.write(|active| {
2689 crate::write::write_revision(current, space, |raw| {2626 let raw = &active.live.revision;
2690 let mut node = PropertyObject::from_object(&raw.objects[&object])?;2627 let mut node = PropertyObject::from_object(&raw.objects[&object])?;
2691 node.set(&[(0x1c001c12, &indents)])?;2628 node.set(&[(0x1c001c12, &indents)])?;
2692 Ok(BTreeMap::from([(object, node)]))2629 Ok(BTreeMap::from([(object, node)]))
2693 })
2694 })?;2630 })?;
2695 }2631 }
2696 if (outline.layout.x, outline.layout.y) != (stored.layout.x, stored.layout.y) {2632 if (outline.layout.x, outline.layout.y) != (stored.layout.x, stored.layout.y) {
...@@ -2698,7 +2634,7 @@ impl Lowering<'_> {...@@ -2698,7 +2634,7 @@ impl Lowering<'_> {
2698 return Err(invalid("An outline position needs both coordinates"));2634 return Err(invalid("An outline position needs both coordinates"));
2699 };2635 };
2700 let edit = OutlineEdit::Position { x, y };2636 let edit = OutlineEdit::Position { x, y };
2701 self.apply(|image| edit.apply(image, space, object))?;2637 self.write(|active| edit.changes(active, object))?;
2702 }2638 }
2703 if (outline.layout.max_width, outline.layout.width_set_by_user)2639 if (outline.layout.max_width, outline.layout.width_set_by_user)
2704 != (stored.layout.max_width, stored.layout.width_set_by_user)2640 != (stored.layout.max_width, stored.layout.width_set_by_user)
...@@ -2711,7 +2647,7 @@ impl Lowering<'_> {...@@ -2711,7 +2647,7 @@ impl Lowering<'_> {
2711 points,2647 points,
2712 user_set: outline.layout.width_set_by_user == Some(true),2648 user_set: outline.layout.width_set_by_user == Some(true),
2713 };2649 };
2714 self.apply(|image| edit.apply(image, space, object))?;2650 self.write(|active| edit.changes(active, object))?;
2715 }2651 }
2716 }2652 }
2717 Ok(())2653 Ok(())
...@@ -2755,6 +2691,9 @@ fn kept_set(...@@ -2755,6 +2691,9 @@ fn kept_set(
2755 "Images and unsupported objects cannot be reordered",2691 "Images and unsupported objects cannot be reordered",
2756 ));2692 ));
2757 }2693 }
2694 if sequence.windows(2).all(|pair| pair[0].1 < pair[1].1) {
2695 return Ok(sequence.into_iter().map(|(id, _)| id).collect());
2696 }
2758 // Longest increasing subsequence over `after` positions, forced through immovable items.2697 // Longest increasing subsequence over `after` positions, forced through immovable items.
2759 let n = sequence.len();2698 let n = sequence.len();
2760 let mut best = vec![1usize; n];2699 let mut best = vec![1usize; n];
...@@ -2926,12 +2865,24 @@ fn attributes(current: &Format, target: &Format, fresh: bool) -> Result<Vec<Text...@@ -2926,12 +2865,24 @@ fn attributes(current: &Format, target: &Format, fresh: bool) -> Result<Vec<Text
2926 Ok(out)2865 Ok(out)
2927}2866}
29282867
2929/// Rewrites every revision the typed writers appended as one transaction on `source`,2868/// Payload identities the file-data store of `store` declares, in order.
2930/// renaming writer-allocated identities to the model's. A protected `source` takes the2869pub(crate) fn declared_payloads(store: &Store<'_>) -> Vec<[u8; 16]> {
2931/// revisions its plaintext twin gained.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.
2932pub(crate) fn squash(2882pub(crate) fn squash(
2933 source: &[u8],2883 source: &RevisionIndex<'_>,
2934 applied: &[u8],2884 edited: &[(ExGuid, &crate::ResolvedRevision<'_>)],
2885 payloads: &[([u8; 16], &[u8])],
2935 alias: &BTreeMap<ExGuid, ExGuid>,2886 alias: &BTreeMap<ExGuid, ExGuid>,
2936 protection: Option<&dyn crate::write::Protection>,2887 protection: Option<&dyn crate::write::Protection>,
2937) -> Result<Vec<u8>, Error> {2888) -> Result<Vec<u8>, Error> {
...@@ -2939,44 +2890,19 @@ pub(crate) fn squash(...@@ -2939,44 +2890,19 @@ pub(crate) fn squash(
2939 .iter()2890 .iter()
2940 .map(|(model, image)| (*image, *model))2891 .map(|(model, image)| (*image, *model))
2941 .collect();2892 .collect();
2942 let declared = |store: &Store<'_>| -> Vec<[u8; 16]> {2893 let existing = declared_payloads(source.store);
2943 store2894 let payloads: Vec<_> = payloads
2944 .lists2895 .iter()
2945 .values()2896 .filter(|(guid, _)| !existing.contains(guid))
2946 .flat_map(|list| &list.nodes)2897 .copied()
2947 .filter(|node| node.id == 0x94)2898 .collect();
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 }
2961 let edit = |index: &RevisionIndex<'_>| {2899 let edit = |index: &RevisionIndex<'_>| {
2962 let mut changes = BTreeMap::new();2900 let mut changes = BTreeMap::new();
2963 for sid in applied_index.spaces.keys() {2901 for (sid, after) in edited {
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 }
2975 let before = match protection {2902 let before = match protection {
2976 Some(protection) => protection.resolve(*sid, index.active(*sid)?)?,2903 Some(protection) => protection.resolve(*sid, index.active(*sid)?)?,
2977 None => index.resolve_active(*sid)?,2904 None => index.resolve_active(*sid)?,
2978 };2905 };
2979 let after = applied_index.resolve(*sid, after_rid)?;
2980 if before.roots != after.roots {2906 if before.roots != after.roots {
2981 return Err(invalid("Page edits cannot change revision roots"));2907 return Err(invalid("Page edits cannot change revision roots"));
2982 }2908 }
...@@ -2994,7 +2920,7 @@ pub(crate) fn squash(...@@ -2994,7 +2920,7 @@ pub(crate) fn squash(
2994 {2920 {
2995 let entries = match object.data {2921 let entries = match object.data {
2996 ObjectData::Properties(bytes) => crate::write::table_entries(bytes)?,2922 ObjectData::Properties(bytes) => crate::write::table_entries(bytes)?,
2997 _ => BTreeSet::new(),2923 _ => Vec::new(),
2998 };2924 };
2999 if entries2925 if entries
3000 .iter()2926 .iter()
...@@ -3044,12 +2970,8 @@ pub(crate) fn squash(...@@ -3044,12 +2970,8 @@ pub(crate) fn squash(
3044 }2970 }
3045 Ok(changes)2971 Ok(changes)
3046 };2972 };
3047 let validate = protection.is_none();2973 let output = crate::write::build_on(source, &payloads, protection, edit)?;
3048 let output = crate::write::build(source, &payloads, protection, validate, edit)?;2974 crate::write::check(&output, protection.is_none())?;
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)?;
3053 Ok(output)2975 Ok(output)
3054}2976}
30552977
...@@ -3076,3 +2998,81 @@ fn remap(object: &mut PropertyObject, rename: &BTreeMap<ExGuid, ExGuid>) -> Resu...@@ -3076,3 +2998,81 @@ fn remap(object: &mut PropertyObject, rename: &BTreeMap<ExGuid, ExGuid>) -> Resu
3076 }2998 }
3077 Ok(())2999 Ok(())
3078}3000}
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 @@...@@ -1,9 +1,10 @@
1use crate::{1use crate::{
2 Error, ExGuid, Object, ObjectData, PropertySets, RevisionIndex, Store,2 Error, ExGuid, Object, ObjectData, PropertySets,
3 active::{ActivePage, Changes},
3 create::{current_timestamps, properties, string},4 create::{current_timestamps, properties, string},
4 document::{Document, Element, Kind},5 document::{Element, Kind},
5 edit::{editable_parents, update_title},6 edit::update_title,
6 write::{PropertyObject, fresh_guid, write_revision_on},7 write::{PropertyObject, fresh_guid},
7};8};
8use serde::{Deserialize, Serialize};9use serde::{Deserialize, Serialize};
9use std::{10use std::{
...@@ -80,20 +81,15 @@ impl ParagraphSplit {...@@ -80,20 +81,15 @@ impl ParagraphSplit {
80 "Choose paragraph text and an author name without NUL",81 "Choose paragraph text and an author name without NUL",
81 ));82 ));
82 }83 }
83 let store = Store::parse(source)?;84 crate::active::write(source, space, |active| self.changes(active))
84 let index = RevisionIndex::parse(&store)?;85 }
85 index.validate_current()?;86
86 let mut document = Document::parse(&index)?;87 pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> {
87 let pages = document.pages_in(space)?;88 let [_] = active.pages.as_slice() else {
88 let [_] = pages.as_slice() else {
89 return Err(invalid("Splitting requires a single active page"));89 return Err(invalid("Splitting requires a single active page"));
90 };90 };
91 let view = document91 let view = &active.view;
92 .spaces92 let parents = active.editable_parents(self.text)?;
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)?;
97 let [paragraph] = parents93 let [paragraph] = parents
98 .get(&self.text)94 .get(&self.text)
99 .map(Vec::as_slice)95 .map(Vec::as_slice)
...@@ -138,8 +134,8 @@ impl ParagraphSplit {...@@ -138,8 +134,8 @@ impl ParagraphSplit {
138 }134 }
139 pending.extend(parents.get(&id).into_iter().flatten().copied());135 pending.extend(parents.get(&id).into_iter().flatten().copied());
140 }136 }
141 let raw = index.resolve_active(space)?;137 let raw = &active.live.revision;
142 let (text, runs) = ordinary_text(&view, &raw, self.text)?;138 let (text, runs) = ordinary_text(view, raw, self.text)?;
143 let node = &view.nodes[&self.text];139 let node = &view.nodes[&self.text];
144 let length = u32::try_from(text.encode_utf16().count())140 let length = u32::try_from(text.encode_utf16().count())
145 .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?;141 .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?;
...@@ -251,8 +247,8 @@ impl ParagraphSplit {...@@ -251,8 +247,8 @@ impl ParagraphSplit {
251 };247 };
252 object.set(&[(0x14001d7a, &modified)])?;248 object.set(&[(0x14001d7a, &modified)])?;
253 }249 }
254 update_title(&store, &raw, view, &pages, &mut changed)?;250 update_title(active, view.clone(), &mut changed)?;
255 write_revision_on(&index, space, |_| Ok(changed))251 Ok(changed)
256 }252 }
257}253}
258254
...@@ -406,21 +402,17 @@ impl ParagraphJoin {...@@ -406,21 +402,17 @@ impl ParagraphJoin {
406 "Choose two distinct text objects and an author name without NUL",402 "Choose two distinct text objects and an author name without NUL",
407 ));403 ));
408 }404 }
409 let store = Store::parse(source)?;405 crate::active::write(source, space, |active| self.changes(active))
410 let index = RevisionIndex::parse(&store)?;406 }
411 index.validate_current()?;407
412 let mut document = Document::parse(&index)?;408 pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> {
413 let pages = document.pages_in(space)?;409 let pages = &active.pages;
414 if pages.len() != 1 {410 if pages.len() != 1 {
415 return Err(invalid("Joining requires a single active page"));411 return Err(invalid("Joining requires a single active page"));
416 }412 }
417 let view = document413 let view = &active.view;
418 .spaces414 let parents = active.editable_parents(self.left)?;
419 .remove(&space)415 active.editable_parents(self.right)?;
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)?;
424 let parent = |id| -> Result<ExGuid, Error> {416 let parent = |id| -> Result<ExGuid, Error> {
425 let [parent] = parents.get(&id).map(Vec::as_slice).unwrap_or_default() else {417 let [parent] = parents.get(&id).map(Vec::as_slice).unwrap_or_default() else {
426 return Err(invalid("Select text with a unique editable parent"));418 return Err(invalid("Select text with a unique editable parent"));
...@@ -482,9 +474,9 @@ impl ParagraphJoin {...@@ -482,9 +474,9 @@ impl ParagraphJoin {
482 "Joining these child indentation levels requires a hierarchy edit",474 "Joining these child indentation levels requires a hierarchy edit",
483 ));475 ));
484 }476 }
485 let raw = index.resolve_active(space)?;477 let raw = &active.live.revision;
486 let (a, a_runs) = ordinary_text(&view, &raw, self.left)?;478 let (a, a_runs) = ordinary_text(view, raw, self.left)?;
487 let (b, b_runs) = ordinary_text(&view, &raw, self.right)?;479 let (b, b_runs) = ordinary_text(view, raw, self.right)?;
488 let length = u32::try_from(a.encode_utf16().count())480 let length = u32::try_from(a.encode_utf16().count())
489 .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?;481 .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?;
490 let right_length = u32::try_from(b.encode_utf16().count())482 let right_length = u32::try_from(b.encode_utf16().count())
...@@ -531,8 +523,8 @@ impl ParagraphJoin {...@@ -531,8 +523,8 @@ impl ParagraphJoin {
531 references.extend_from_slice(&target.reference(id)?);523 references.extend_from_slice(&target.reference(id)?);
532 ends.extend_from_slice(&run.end.to_le_bytes());524 ends.extend_from_slice(&run.end.to_le_bytes());
533 }525 }
534 let left_base = character_properties(&view, &raw, self.left)?;526 let left_base = character_properties(view, raw, self.left)?;
535 let right_base = character_properties(&view, &raw, self.right)?;527 let right_base = character_properties(view, raw, self.right)?;
536 for (i, run) in b_runs.iter().enumerate() {528 for (i, run) in b_runs.iter().enumerate() {
537 if run.start == run.end && i != b_runs.len() - 1 {529 if run.start == run.end && i != b_runs.len() - 1 {
538 continue;530 continue;
...@@ -681,8 +673,8 @@ impl ParagraphJoin {...@@ -681,8 +673,8 @@ impl ParagraphJoin {
681 object.set(&[(0x14001d7a, &modified)])?;673 object.set(&[(0x14001d7a, &modified)])?;
682 pending.extend(parents.get(&id).into_iter().flatten().copied());674 pending.extend(parents.get(&id).into_iter().flatten().copied());
683 }675 }
684 update_title(&store, &raw, view, &pages, &mut changed)?;676 update_title(active, view.clone(), &mut changed)?;
685 write_revision_on(&index, space, |_| Ok(changed))677 Ok(changed)
686 }678 }
687}679}
688680
crates/onestore/src/protected/mod.rs+28-7
...@@ -407,15 +407,36 @@ pub(crate) fn write_page(...@@ -407,15 +407,36 @@ pub(crate) fn write_page(
407 }407 }
408 let twin = unlocked.twin()?;408 let twin = unlocked.twin()?;
409 let applied = Zeroizing::new(crate::page::write::write_page(&twin, space, page, author)?);409 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.410 let applied_store = crate::Store::parse(&applied)?;
411 let spaces = |image: &[u8]| -> Result<Vec<ExGuid>> {411 let applied_index = RevisionIndex::parse(&applied_store)?;
412 let store = crate::Store::parse(image)?;412 let twin_store = crate::Store::parse(&twin)?;
413 Ok(RevisionIndex::parse(&store)?.spaces.into_keys().collect())413 if RevisionIndex::parse(&twin_store)?
414 };414 .spaces
415 if spaces(&twin)? != spaces(&applied)? {415 .keys()
416 .ne(applied_index.spaces.keys())
417 {
416 return Err(invalid("Page edits cannot create object spaces"));418 return Err(invalid("Page edits cannot create object spaces"));
417 }419 }
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 )?;
419 let store = crate::Store::parse(&written)?;440 let store = crate::Store::parse(&written)?;
420 UnlockedSection::open(&RevisionIndex::parse(&store)?, password, Limits::default())?;441 UnlockedSection::open(&RevisionIndex::parse(&store)?, password, Limits::default())?;
421 Ok(written)442 Ok(written)
crates/onestore/src/tree.rs+22-25
...@@ -1,9 +1,10 @@...@@ -1,9 +1,10 @@
1use crate::{1use crate::{
2 Error, ExGuid, RevisionIndex, Store,2 Error, ExGuid,
3 active::{ActivePage, Changes},
3 create::{current_timestamps, properties, string},4 create::{current_timestamps, properties, string},
4 document::{Document, Kind, Revision},5 document::{Kind, Revision},
5 edit::{editable_parents, update_title},6 edit::{editable_parents, update_title},
6 write::{PropertyObject, fresh_guid, write_revision_on},7 write::{PropertyObject, fresh_guid},
7};8};
8use serde::{Deserialize, Serialize};9use serde::{Deserialize, Serialize};
9use std::{10use std::{
...@@ -96,21 +97,17 @@ impl TreeEdit {...@@ -96,21 +97,17 @@ impl TreeEdit {
9697
97 pub(crate) fn apply(&self, source: &[u8], space: ExGuid) -> Result<Vec<u8>, Error> {98 pub(crate) fn apply(&self, source: &[u8], space: ExGuid) -> Result<Vec<u8>, Error> {
98 self.validate()?;99 self.validate()?;
99 let store = Store::parse(source)?;100 crate::active::write(source, space, |active| self.changes(active))
100 let index = RevisionIndex::parse(&store)?;101 }
101 index.validate_current()?;102
102 let mut document = Document::parse(&index)?;103 pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> {
103 let pages = document.pages_in(space)?;104 let pages = &active.pages;
104 let [page] = pages.as_slice() else {105 let [page] = pages.as_slice() else {
105 return Err(invalid("Tree editing requires a single active page"));106 return Err(invalid("Tree editing requires a single active page"));
106 };107 };
107 let mut view = document108 let mut view = active.view.clone();
108 .spaces109 let parents = active.editable_parents(self.object)?;
109 .remove(&space)110 let path = checked_path(&view, parents, *page, self.object)?;
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)?;
114 let source_parent = *path111 let source_parent = *path
115 .get(1)112 .get(1)
116 .ok_or_else(|| invalid("Select a paragraph or ordinary page outline"))?;113 .ok_or_else(|| invalid("Select a paragraph or ordinary page outline"))?;
...@@ -132,7 +129,7 @@ impl TreeEdit {...@@ -132,7 +129,7 @@ impl TreeEdit {
132 let mut changed_ids = BTreeSet::from([source_parent]);129 let mut changed_ids = BTreeSet::from([source_parent]);
133 let mut affected = BTreeSet::from([self.object]);130 let mut affected = BTreeSet::from([self.object]);
134 if let Placement::Move { parent, before } = self.placement {131 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) {
136 return Err(invalid("A subtree cannot move inside itself"));133 return Err(invalid("A subtree cannot move inside itself"));
137 }134 }
138 let destination = &view.nodes[&parent];135 let destination = &view.nodes[&parent];
...@@ -143,7 +140,7 @@ impl TreeEdit {...@@ -143,7 +140,7 @@ impl TreeEdit {
143 }140 }
144 let previous = destination.children.clone();141 let previous = destination.children.clone();
145 if parent == source_parent && before == Some(self.object) {142 if parent == source_parent && before == Some(self.object) {
146 return Ok(source.to_vec());143 return Ok(Changes::new());
147 }144 }
148 view.nodes145 view.nodes
149 .get_mut(&source_parent)146 .get_mut(&source_parent)
...@@ -156,7 +153,7 @@ impl TreeEdit {...@@ -156,7 +153,7 @@ impl TreeEdit {
156 .unwrap_or(children.len());153 .unwrap_or(children.len());
157 children.insert(position, self.object);154 children.insert(position, self.object);
158 if parent == source_parent && *children == previous {155 if parent == source_parent && *children == previous {
159 return Ok(source.to_vec());156 return Ok(Changes::new());
160 }157 }
161 changed_ids.insert(parent);158 changed_ids.insert(parent);
162 } else {159 } else {
...@@ -187,7 +184,7 @@ impl TreeEdit {...@@ -187,7 +184,7 @@ impl TreeEdit {
187 if node.extra[0].iter().any(|field| field.id == 0x08001d0c) {184 if node.extra[0].iter().any(|field| field.id == 0x08001d0c) {
188 return Err(invalid("The emptied container cannot be deleted"));185 return Err(invalid("The emptied container cannot be deleted"));
189 }186 }
190 let path = checked_path(&view, &parents, *page, at)?;187 let path = checked_path(&view, parents, *page, at)?;
191 let parent = path[1];188 let parent = path[1];
192 view.nodes189 view.nodes
193 .get_mut(&parent)190 .get_mut(&parent)
...@@ -256,7 +253,7 @@ impl TreeEdit {...@@ -256,7 +253,7 @@ impl TreeEdit {
256 if !checked.insert(id) {253 if !checked.insert(id) {
257 continue;254 continue;
258 }255 }
259 checked_path(&view, &parents, *page, id)?;256 checked_path(&view, parents, *page, id)?;
260 let node = &view.nodes[&id];257 let node = &view.nodes[&id];
261 if matches!(self.placement, Placement::Delete)258 if matches!(self.placement, Placement::Delete)
262 && node.extra[0].iter().any(|field| field.id == 0x08001d0c)259 && node.extra[0].iter().any(|field| field.id == 0x08001d0c)
...@@ -273,8 +270,8 @@ impl TreeEdit {...@@ -273,8 +270,8 @@ impl TreeEdit {
273 .copied(),270 .copied(),
274 );271 );
275 }272 }
276 let raw = index.resolve_active(space)?;273 let raw = &active.live.revision;
277 let active_parents = editable_parents(&view, &pages, *page)?;274 let active_parents = editable_parents(&view, pages, *page)?;
278 let modified = current_timestamps()?.0.to_le_bytes();275 let modified = current_timestamps()?.0.to_le_bytes();
279 let mut changed = BTreeMap::new();276 let mut changed = BTreeMap::new();
280 let author_id = ExGuid {277 let author_id = ExGuid {
...@@ -370,8 +367,8 @@ impl TreeEdit {...@@ -370,8 +367,8 @@ impl TreeEdit {
370 object.set(&[(0x20001d79, &author), (0x14001d7a, &modified)])?;367 object.set(&[(0x20001d79, &author), (0x14001d7a, &modified)])?;
371 changed.insert(self.object, object);368 changed.insert(self.object, object);
372 }369 }
373 update_title(&store, &raw, view, &pages, &mut changed)?;370 update_title(active, view, &mut changed)?;
374 write_revision_on(&index, space, |_| Ok(changed))371 Ok(changed)
375 }372 }
376}373}
377374
...@@ -417,7 +414,7 @@ fn checked_path(...@@ -417,7 +414,7 @@ fn checked_path(
417mod tests {414mod tests {
418 use super::*;415 use super::*;
419 use crate::write::write_revision;416 use crate::write::write_revision;
420 use crate::{Insertion, PreparedEdit};417 use crate::{Insertion, PreparedEdit, RevisionIndex, Store, document::Document};
421418
422 #[test]419 #[test]
423 fn a_retained_move_intent_can_reuse_its_immutable_author_after_another_move() {420 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]> {...@@ -105,8 +105,8 @@ fn compact(id: ExGuid, table: &BTreeMap<u32, [u8; 16]>) -> Result<[u8; 4]> {
105}105}
106106
107/// The global id table entries a property object's references name.107/// The global id table entries a property object's references name.
108pub(crate) fn table_entries(bytes: &[u8]) -> Result<BTreeSet<u32>> {108pub(crate) fn table_entries(bytes: &[u8]) -> Result<Vec<u32>> {
109 let mut entries = BTreeSet::new();109 let mut entries = Vec::new();
110 for property in PropertySets::parse(bytes)?.sets.iter().flatten() {110 for property in PropertySets::parse(bytes)?.sets.iter().flatten() {
111 if let Value::References { compact_ids, .. } = property.value {111 if let Value::References { compact_ids, .. } = property.value {
112 entries.extend(112 entries.extend(
...@@ -340,36 +340,42 @@ pub(crate) fn replace_objects(...@@ -340,36 +340,42 @@ pub(crate) fn replace_objects(
340 space: ExGuid,340 space: ExGuid,
341 edits: &[ObjectEdit<'_>],341 edits: &[ObjectEdit<'_>],
342) -> Result<Vec<u8>> {342) -> Result<Vec<u8>> {
343 write_revision_on(index, space, |revision| {343 write_revision_on(index, space, |revision| patched(revision, edits))
344 let mut changed = BTreeMap::new();344}
345 for edit in edits {345
346 if changed.contains_key(&edit.object) {346/// Objects of `revision` with `edits` applied.
347 return Err(Error {347pub(crate) fn patched(
348 offset: 0,348 revision: &crate::ResolvedRevision<'_>,
349 message: "Duplicate object edit",349 edits: &[ObjectEdit<'_>],
350 });350) -> Result<BTreeMap<ExGuid, PropertyObject>> {
351 }351 let mut changed = BTreeMap::new();
352 let object = revision.objects.get(&edit.object).ok_or(Error {352 for edit in edits {
353 if changed.contains_key(&edit.object) {
354 return Err(Error {
353 offset: 0,355 offset: 0,
354 message: "Object is absent from the active revision",356 message: "Duplicate object edit",
355 })?;357 });
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 );
370 }358 }
371 Ok(changed)359 let object = revision.objects.get(&edit.object).ok_or(Error {
372 })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)
373}379}
374380
375/// The object type OneNote gives embedded picture payload declarations; embedded files381/// The object type OneNote gives embedded picture payload declarations; embedded files
...@@ -377,10 +383,57 @@ pub(crate) fn replace_objects(...@@ -377,10 +383,57 @@ pub(crate) fn replace_objects(
377pub(crate) const FILE_DATA_JCID: u32 = 0x80039;383pub(crate) const FILE_DATA_JCID: u32 = 0x80039;
378pub(crate) const EMBEDDED_FILE_JCID: u32 = 0x80036;384pub(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
380fn is_file_declaration(jcid: u32) -> bool {432fn is_file_declaration(jcid: u32) -> bool {
381 jcid == FILE_DATA_JCID || jcid == EMBEDDED_FILE_JCID433 jcid == FILE_DATA_JCID || jcid == EMBEDDED_FILE_JCID
382}434}
383435
436#[derive(Clone)]
384pub(crate) struct PropertyObject {437pub(crate) struct PropertyObject {
385 pub jcid: u32,438 pub jcid: u32,
386 pub bytes: Vec<u8>,439 pub bytes: Vec<u8>,
...@@ -426,29 +479,6 @@ impl PropertyObject {...@@ -426,29 +479,6 @@ impl PropertyObject {
426 })479 })
427 }480 }
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
452 pub fn from_object(object: &crate::Object<'_>) -> Result<Self> {482 pub fn from_object(object: &crate::Object<'_>) -> Result<Self> {
453 let ObjectData::Properties(bytes) = object.data else {483 let ObjectData::Properties(bytes) = object.data else {
454 return Err(Error {484 return Err(Error {
...@@ -735,6 +765,440 @@ pub(crate) trait Protection {...@@ -735,6 +765,440 @@ pub(crate) trait Protection {
735 fn seal_file(&self, clear: &[u8]) -> Vec<u8>;765 fn seal_file(&self, clear: &[u8]) -> Vec<u8>;
736}766}
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
738pub(crate) fn write_revision(1202pub(crate) fn write_revision(
739 source: &[u8],1203 source: &[u8],
740 space: ExGuid,1204 space: ExGuid,
...@@ -880,12 +1344,20 @@ pub(crate) fn write_revision_on(...@@ -880,12 +1344,20 @@ pub(crate) fn write_revision_on(
880 Ok(output)1344 Ok(output)
881}1345}
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(
884 index: &RevisionIndex<'_>,1354 index: &RevisionIndex<'_>,
885 payloads: &[([u8; 16], &[u8])],1355 payloads: &[([u8; 16], &[u8])],
886 protection: Option<&dyn Protection>,1356 protection: Option<&dyn Protection>,
887 edit: impl FnOnce(&RevisionIndex<'_>) -> Result<BTreeMap<ExGuid, RevisionEdit>>,1357 edit: impl FnOnce(&RevisionIndex<'_>) -> Result<BTreeMap<ExGuid, RevisionEdit>>,
888) -> Result<Vec<u8>> {1358) -> Result<Vec<u8>> {
1359 #[cfg(test)]
1360 BUILDS.with(|builds| builds.set(builds.get() + 1));
889 let store = index.store;1361 let store = index.store;
890 let source = store.data;1362 let source = store.data;
891 let is_section = store.header.file_type == FileType::Section;1363 let is_section = store.header.file_type == FileType::Section;
...@@ -933,7 +1405,7 @@ fn build_on(...@@ -933,7 +1405,7 @@ fn build_on(
933 for (space, change) in changes {1405 for (space, change) in changes {
934 let new_space = matches!(change, RevisionEdit::Create { .. });1406 let new_space = matches!(change, RevisionEdit::Create { .. });
935 let current = (ExGuid::default(), 1_u32);1407 let current = (ExGuid::default(), 1_u32);
936 let (rid, label, mut revision, mut replacements) = match change {1408 let (rid, label, revision, replacements) = match change {
937 RevisionEdit::Update(objects) => {1409 RevisionEdit::Update(objects) => {
938 let rid = index.active(space)?;1410 let rid = index.active(space)?;
939 (Some(rid), current, resolve(space, rid)?, objects)1411 (Some(rid), current, resolve(space, rid)?, objects)
...@@ -986,179 +1458,29 @@ fn build_on(...@@ -986,179 +1458,29 @@ fn build_on(
986 }1458 }
987 };1459 };
988 let existing: BTreeSet<ExGuid> = revision.objects.keys().copied().collect();1460 let existing: BTreeSet<ExGuid> = revision.objects.keys().copied().collect();
989 let reachable = if rid.is_some() {1461 let depth = rid.map_or(0, |rid| chain_depth(index, space, rid));
990 revision.reachable()?1462 let mut live = LiveRevision::new(revision, depth)?;
991 } else {1463 let replacements = live.prepare(replacements, is_section)?;
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 }
1116 if replacements.is_empty() {1464 if replacements.is_empty() {
1117 continue;1465 continue;
1118 }1466 }
1119 let mut changed: BTreeSet<_> = replacements.keys().copied().collect();1467 let Commit {
1120 for (id, replacement) in &replacements {1468 checkpoint,
1121 let data = if is_file_declaration(replacement.jcid) {1469 changed,
1122 let (reference, extension) = replacement.file_declaration()?;1470 ..
1123 ObjectData::File {1471 } = live.commit(
1124 reference,1472 replacements
1125 extension,1473 .iter()
1126 }1474 .map(|(id, replacement)| {
1127 } else {1475 let global_ids = Arc::clone(&replacement.global_ids);
1128 ObjectData::Properties(&replacement.bytes)1476 Ok((
1129 };1477 *id,
1130 revision.objects.insert(1478 declared(replacement.jcid, &replacement.bytes, global_ids)?,
1131 *id,1479 ))
1132 crate::Object {1480 })
1133 jcid: replacement.jcid,1481 .collect::<Result<Vec<_>>>()?,
1134 reference_count: 0,1482 )?;
1135 data,1483 let revision = &live.revision;
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();
1162 let selected: Vec<_> = revision1484 let selected: Vec<_> = revision
1163 .objects1485 .objects
1164 .iter()1486 .iter()
...@@ -1272,15 +1594,9 @@ fn build_on(...@@ -1272,15 +1594,9 @@ fn build_on(
1272 };1594 };
1273 // A table read from a long-lived page names every session that edited it; the1595 // A table read from a long-lived page names every session that edited it; the
1274 // group stores the entries its objects use.1596 // group stores the entries its objects use.
1275 let mut used = BTreeSet::new();1597 let used = used_entries(table, objects.iter().copied())?;
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 }
1282 for (id, guid) in table {1598 for (id, guid) in table {
1283 if is_section && !used.contains(id) {1599 if is_section && used.binary_search(id).is_err() {
1284 continue;1600 continue;
1285 }1601 }
1286 let mut entry = id.to_le_bytes().to_vec();1602 let mut entry = id.to_le_bytes().to_vec();
crates/snowbound/src/main.rs+27-16
...@@ -1323,24 +1323,21 @@ impl State {...@@ -1323,24 +1323,21 @@ impl State {
1323 return Ok(());1323 return Ok(());
1324 };1324 };
1325 let after = self.view.editor.page()?;1325 let after = self.view.editor.page()?;
1326 match session1326 if after == session.before {
1327 .section1327 return Ok(());
1328 .save(session.space, &session.before, &after, "snowbound")?1328 }
1329 if let Some(entry) = session
1330 .pages
1331 .iter_mut()
1332 .find(|(space, ..)| *space == session.space)
1329 {1333 {
1330 notebook::session::Save::Unchanged => {}1334 entry.1.clone_from(&after.title);
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()?,
1343 }1335 }
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")?;
1344 Ok(())1341 Ok(())
1345 }1342 }
13461343
...@@ -1397,6 +1394,18 @@ impl State {...@@ -1397,6 +1394,18 @@ impl State {
1397 };1394 };
1398 let shown = (session.status, session.conflict);1395 let shown = (session.status, session.conflict);
1399 let mut refreshed = false;1396 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 }
1400 for event in session.section.events() {1409 for event in session.section.events() {
1401 use notebook::session::Event;1410 use notebook::session::Event;
1402 session.status = match event {1411 session.status = match event {
...@@ -1436,6 +1445,8 @@ impl State {...@@ -1436,6 +1445,8 @@ impl State {
1436 } else if session.section.page(session.space)? != session.before {1445 } else if session.section.page(session.space)? != session.before {
1437 self.reload()?;1446 self.reload()?;
1438 }1447 }
1448 } else if stale {
1449 self.reload()?;
1439 }1450 }
1440 Ok(())1451 Ok(())
1441 }1452 }