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