| author | |
| committer | |
| log | e0b4334a77134228b5f773ce7cb9227fc2dd027d |
| tree | f4ba24b7af1affa009b2dc066a076ae37687589d |
| parent | 710848e64a97302d4f0df26f496f6d1d3de7e809 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
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-520 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` |
| 68 | means the stored page no longer matches `before` because the section changed | 68 | means the stored page no longer matches `before` because the section changed |
| 69 | underneath the editor, so the page must be reloaded before saving again. | 69 | underneath 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 | ||
| 71 | thread 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 | ||
| 73 | one's `after` continues it, needing no reload in between, and a run of such | ||
| 74 | saves waiting together is written once. | ||
| 70 | `events()` drains what the synchronization thread reported since the last | 75 | `events()` drains what the synchronization thread reported since the last |
| 71 | poll: refreshes, attempt outcomes and unreachable files; `notify` runs on that | 76 | poll: refreshes, attempt outcomes and unreachable files; `notify` runs |
| 72 | thread whenever an event is available so the application can wake its event | 77 | whenever an event or save outcome is available so the application can wake its |
| 73 | loop. `close()` stops publication. | 78 | event loop. `close()` finishes queued saves and stops publication. |
| 74 | 79 | ||
| 75 | `Event::Unreachable` retains an `io::Error`: callers can distinguish permission | 80 | `Event::Unreachable` retains an `io::Error`: callers can distinguish permission |
| 76 | denial, missing targets, timeouts, and connection failures through `kind()` without | 81 | denial, 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 | |||
| 4 | use notebook::session::Section; | ||
| 5 | use onestore::{ | ||
| 6 | document::{Format, Layout}, | ||
| 7 | page::{ | ||
| 8 | Outline, Page, PageObject, PageParagraph, Paragraph, ParagraphContent, TextObject, | ||
| 9 | text::{Edit, new_id}, | ||
| 10 | }, | ||
| 11 | }; | ||
| 12 | use std::time::{Duration, Instant}; | ||
| 13 | |||
| 14 | fn 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 | } |
| 350 | 363 | ||
| 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 | }; |
| 11 | use std::{ | 11 | use 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 | }; |
| 22 | 23 | ||
| ... | @@ -751,12 +752,23 @@ pub struct QueuedEdit { | ... | @@ -751,12 +752,23 @@ pub struct QueuedEdit { |
| 751 | pub status: EditStatus, | 752 | pub status: EditStatus, |
| 752 | } | 753 | } |
| 753 | 754 | ||
| 754 | /// A section file with its replica and background publication. | 755 | /// A section file with its replica, background saves and background publication. |
| 755 | pub struct Section { | 756 | pub 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. | ||
| 767 | struct PageSave { | ||
| 768 | space: ExGuid, | ||
| 769 | before: Page, | ||
| 770 | after: Page, | ||
| 771 | author: String, | ||
| 760 | } | 772 | } |
| 761 | 773 | ||
| 762 | impl Section { | 774 | impl 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 | } |
| 860 | 895 | ||
| ... | @@ -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 page | 986 | /// 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 | } |
| 969 | 1007 | ||
| 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 | } |
| 1045 | 1083 | ||
| 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 retains | 1102 | /// 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 | } |
| 1067 | 1117 | ||
| 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`. | ||
| 1122 | fn 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. | ||
| 1160 | fn 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. |
| 1069 | struct FileRemote(PathBuf); | 1208 | struct FileRemote(PathBuf); |
| 1070 | 1209 |
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 | } |
| 234 | 234 | ||
| 235 | #[test] | ||
| 236 | fn 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] |
| 236 | fn a_notebook_directory_lists_its_sections_and_opens_them() { | 316 | fn 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 |
| 9 | 9 | ||
| 10 | [dependencies] | 10 | [dependencies] |
| 11 | base64 = "0.22.1" | 11 | base64 = "0.22.1" |
| 12 | bumpalo = "3.20.3" | ||
| 12 | serde_json = "1.0.151" | 13 | serde_json = "1.0.151" |
| 13 | md5 = "0.8.1" | 14 | md5 = "0.8.1" |
| 14 | getrandom = "0.4.3" | 15 | getrandom = "0.4.3" |
crates/onestore/src/active.rs created+374| ... | @@ -0,0 +1,374 @@ | ||
| 1 | use crate::{ | ||
| 2 | Error, ExGuid, Object, RevisionIndex, Store, | ||
| 3 | document::{Document, Element, Revision, Space}, | ||
| 4 | write::{Commit, LiveRevision, PropertyObject, chain_depth, declared, differing}, | ||
| 5 | }; | ||
| 6 | use bumpalo::Bump; | ||
| 7 | use std::collections::{BTreeMap, BTreeSet}; | ||
| 8 | |||
| 9 | type Result<T> = std::result::Result<T, Error>; | ||
| 10 | |||
| 11 | /// Objects a writer stores in one revision of a page space. | ||
| 12 | pub(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. | ||
| 16 | pub(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 | |||
| 30 | impl<'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 | |||
| 218 | fn 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 | |||
| 226 | fn 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. | ||
| 233 | pub(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)] | ||
| 248 | mod 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 | } |
| 48 | 48 | ||
| 49 | #[derive(Debug, Serialize)] | 49 | #[derive(Debug, Clone, Serialize)] |
| 50 | pub struct Revision<'a> { | 50 | pub 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 | } |
| 54 | 54 | ||
| 55 | #[derive(Debug, Serialize)] | 55 | #[derive(Debug, Clone, Serialize)] |
| 56 | pub struct Element<'a> { | 56 | pub 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 | } |
| 492 | 492 | ||
| 493 | #[derive(Debug, PartialEq, Serialize)] | 493 | #[derive(Debug, Clone, PartialEq, Serialize)] |
| 494 | pub struct Field<'a> { | 494 | pub struct Field<'a> { |
| 495 | pub id: u32, | 495 | pub id: u32, |
| 496 | pub value: FieldValue<'a>, | 496 | pub value: FieldValue<'a>, |
| 497 | } | 497 | } |
| 498 | 498 | ||
| 499 | #[derive(Debug, PartialEq, Serialize)] | 499 | #[derive(Debug, Clone, PartialEq, Serialize)] |
| 500 | pub enum FieldValue<'a> { | 500 | pub 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 | } |
| 649 | 649 | ||
| 650 | fn references(bytes: &[u8], ids: &BTreeMap<u32, [u8; 16]>) -> Result<Vec<ExGuid>> { | 650 | fn 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 | } |
| 907 | 930 | ||
| 908 | impl<'a> Element<'a> { | 931 | impl<'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 @@ |
| 1 | use crate::{ | 1 | use 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 | }; |
| 7 | use std::{collections::BTreeMap, ops::Range, sync::Arc}; | 8 | use 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. |
| 51 | pub(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 = document | 63 | 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 = revision | 64 | let node = revision |
| 61 | .nodes | 65 | .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 | } |
| 301 | 305 | ||
| 302 | pub(crate) fn editable_parents( | 306 | pub(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. | ||
| 317 | pub(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 | } |
| 328 | 343 | ||
| 329 | pub(crate) fn page_title( | 344 | pub(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. | ||
| 360 | pub(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<_> = revision | 370 | if !parents.contains_key(&id) && !pages.contains(&id) { |
| 338 | .nodes | 371 | 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.kind | 435 | } = &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 | } |
| 426 | 459 | ||
| 460 | /// Updates the page title for `changed` objects made on `view`, a copy of the page's view. | ||
| 427 | pub(crate) fn update_title( | 461 | pub(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 @@ |
| 1 | use crate::{ | 1 | use 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 | }; |
| 8 | use serde::{Deserialize, Serialize}; | 8 | use serde::{Deserialize, Serialize}; |
| 9 | use std::{ | 9 | use 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. | ||
| 107 | pub(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<_> = values | 198 | 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 | *id | 206 | *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 | id | 225 | 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 | } |
| 257 | 263 | ||
| 258 | pub(crate) fn touch_ancestors( | 264 | pub(crate) fn touch_ancestors( |
crates/onestore/src/insertion.rs+66-79| ... | @@ -1,9 +1,9 @@ | ... | @@ -1,9 +1,9 @@ |
| 1 | use crate::{ | 1 | use 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 | }; |
| 8 | use serde::{Deserialize, Serialize}; | 8 | use serde::{Deserialize, Serialize}; |
| 9 | use std::{ | 9 | use std::{ |
| ... | @@ -191,20 +191,15 @@ impl Insertion { | ... | @@ -191,20 +191,15 @@ impl Insertion { |
| 191 | 191 | ||
| 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 = document | 201 | let view = &active.view; |
| 203 | .spaces | 202 | 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.nodes | 369 | 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 | .children | 372 | 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 = raw | 412 | if let Some((page, automatic, title)) = title { |
| 426 | .roots | 413 | 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")] |
| 3 | 3 | ||
| 4 | mod active; | ||
| 4 | mod bytes; | 5 | mod bytes; |
| 5 | mod commit; | 6 | mod commit; |
| 6 | mod create; | 7 | mod create; |
crates/onestore/src/objects.rs+6-1| ... | @@ -82,6 +82,11 @@ impl Object<'_> { | ... | @@ -82,6 +82,11 @@ impl Object<'_> { |
| 82 | 82 | ||
| 83 | impl ResolvedRevision<'_> { | 83 | impl 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 | } |
| 96 | 101 | ||
| 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 @@ |
| 1 | use crate::{ | 1 | use 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 | }; |
| 8 | use serde::{Deserialize, Serialize}; | 9 | use serde::{Deserialize, Serialize}; |
| 9 | use std::collections::{BTreeMap, BTreeSet}; | 10 | use 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 = document | 66 | let view = &active.view; |
| 66 | .spaces | 67 | 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 | &values | 108 | &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 | } |
| 138 | 134 | ||
| ... | @@ -140,6 +136,7 @@ impl OutlineEdit { | ... | @@ -140,6 +136,7 @@ impl OutlineEdit { |
| 140 | mod tests { | 136 | mod 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}; | ||
| 143 | 140 | ||
| 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 onto | 1 | //! 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. | ||
| 3 | 4 | ||
| 4 | use super::{ | 5 | use 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::{ |
| 7 | use crate::{ | 8 | use 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 | }; |
| 15 | use bumpalo::Bump; | ||
| 13 | use std::{ | 16 | use 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 | } |
| 375 | 376 | ||
| 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 | } |
| 462 | 463 | ||
| 463 | struct Lowering<'a> { | 464 | struct 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 | } |
| 475 | 479 | ||
| 476 | impl Lowering<'_> { | 480 | impl<'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 | } |
| 490 | 494 | ||
| 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 | } |
| 499 | 515 | ||
| 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 | } |
| 509 | 522 | ||
| 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 | } |
| 1249 | 1249 | ||
| ... | @@ -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 = document | 1273 | .get(&container) |
| 1274 | .spaces | 1274 | .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 | } |
| 1312 | 1306 | ||
| ... | @@ -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 | } |
| 1392 | 1380 | ||
| ... | @@ -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])> = values | 1430 | 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 | } |
| 1451 | 1438 | ||
| ... | @@ -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])> = values | 1461 | 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 | } |
| 1483 | 1469 | ||
| ... | @@ -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])> = values | 1600 | .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_values | 1754 | 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 | id | 1946 | } |
| 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 | &values | 2133 | } |
| 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 | &values | 2492 | &values |
| 2493 | .iter() | ||
| 2494 | .map(|(id, bytes)| (*id, bytes.as_slice())) | ||
| 2495 | .collect::<Vec<_>>(), | ||
| 2496 | )?; | ||
| 2497 | if let Some((_, alignment)) = values.iter().find(|(id, _)| *id == 0x0c003477) { | ||
| 2498 | for property in [0x14001c3e, 0x14001c84] { | ||
| 2499 | let fields = PropertySets::parse(&target.bytes)?; | ||
| 2500 | let previous = fields.sets[0] | ||
| 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) => bytes | 2512 | 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 | .outlines | 2614 | .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 keeps | 2618 | // 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.indents | 2622 | && 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 | } |
| 2928 | 2867 | ||
| 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 the | 2869 | pub(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. | ||
| 2932 | pub(crate) fn squash( | 2882 | pub(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 | store | 2894 | let payloads: Vec<_> = payloads |
| 2944 | .lists | 2895 | .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 entries | 2925 | 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 | } |
| 3055 | 2977 | ||
| ... | @@ -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)] | ||
| 3003 | mod 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 @@ |
| 1 | use crate::{ | 1 | use 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 | }; |
| 8 | use serde::{Deserialize, Serialize}; | 9 | use serde::{Deserialize, Serialize}; |
| 9 | use std::{ | 10 | use 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 = document | 91 | let view = &active.view; |
| 92 | .spaces | 92 | 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] = parents | 93 | 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 | } |
| 258 | 254 | ||
| ... | @@ -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 = document | 413 | let view = &active.view; |
| 418 | .spaces | 414 | 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 | } |
| 688 | 680 |
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 @@ |
| 1 | use crate::{ | 1 | use 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 | }; |
| 8 | use serde::{Deserialize, Serialize}; | 9 | use serde::{Deserialize, Serialize}; |
| 9 | use std::{ | 10 | use std::{ |
| ... | @@ -96,21 +97,17 @@ impl TreeEdit { | ... | @@ -96,21 +97,17 @@ impl TreeEdit { |
| 96 | 97 | ||
| 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 = document | 108 | let mut view = active.view.clone(); |
| 108 | .spaces | 109 | 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 = *path | 111 | 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.nodes | 145 | 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.nodes | 189 | 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 | } |
| 377 | 374 | ||
| ... | @@ -417,7 +414,7 @@ fn checked_path( | ... | @@ -417,7 +414,7 @@ fn checked_path( |
| 417 | mod tests { | 414 | mod 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}; |
| 421 | 418 | ||
| 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 | } |
| 106 | 106 | ||
| 107 | /// The global id table entries a property object's references name. | 107 | /// The global id table entries a property object's references name. |
| 108 | pub(crate) fn table_entries(bytes: &[u8]) -> Result<BTreeSet<u32>> { | 108 | pub(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 { | 347 | pub(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 | } |
| 374 | 380 | ||
| 375 | /// The object type OneNote gives embedded picture payload declarations; embedded files | 381 | /// 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( |
| 377 | pub(crate) const FILE_DATA_JCID: u32 = 0x80039; | 383 | pub(crate) const FILE_DATA_JCID: u32 = 0x80039; |
| 378 | pub(crate) const EMBEDDED_FILE_JCID: u32 = 0x80036; | 384 | pub(crate) const EMBEDDED_FILE_JCID: u32 = 0x80036; |
| 379 | 385 | ||
| 386 | /// The reference and extension of a file-data declaration built by `PropertyObject::file`. | ||
| 387 | fn 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. | ||
| 410 | pub(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 | |||
| 380 | fn is_file_declaration(jcid: u32) -> bool { | 432 | fn is_file_declaration(jcid: u32) -> bool { |
| 381 | jcid == FILE_DATA_JCID || jcid == EMBEDDED_FILE_JCID | 433 | jcid == FILE_DATA_JCID || jcid == EMBEDDED_FILE_JCID |
| 382 | } | 434 | } |
| 383 | 435 | ||
| 436 | #[derive(Clone)] | ||
| 384 | pub(crate) struct PropertyObject { | 437 | pub(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 | } |
| 428 | 481 | ||
| 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 | } |
| 737 | 767 | ||
| 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. | ||
| 770 | fn 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. | ||
| 787 | pub(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 | |||
| 798 | impl<'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. | ||
| 1086 | fn 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`. | ||
| 1112 | fn 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. | ||
| 1169 | pub(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. | ||
| 1184 | pub(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. | ||
| 1194 | pub(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 | |||
| 738 | pub(crate) fn write_revision( | 1202 | pub(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 | } |
| 882 | 1346 | ||
| 883 | fn build_on( | 1347 | #[cfg(test)] |
| 1348 | thread_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 | |||
| 1353 | pub(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<_> = revision | 1484 | let selected: Vec<_> = revision |
| 1163 | .objects | 1485 | .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; the | 1595 | // 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 session | 1326 | if after == session.before { |
| 1327 | .section | 1327 | 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 | } |
| 1346 | 1343 | ||
| ... | @@ -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 | } |