1//! Page versions, the earlier states of a page OneNote 2010 keeps (MS-ONE 2.1.17, 2.1.18).
2//! A version is an earlier revision of the page's own object space, current under a context
3//! of its own. The page's version history, the space's revision current under `HISTORY`,
4//! holds a proxy for each (`jcidVersionProxy`), newest first. `corpus/page-versions` records
5//! what OneNote 2010 writes.
6
7use super::{Frozen, Open, Section, SpaceState, changed, one_page};
8use crate::{
9 Error, ExGuid, FileType, Object, ResolvedRevision, RevisionIndex, Store,
10 active::{ActivePage, manifest_pages},
11 create::{author_properties, current_timestamps, properties},
12 document::{Element, Kind, Revision},
13 op::{Failure, OpError},
14 page::Page,
15 pages::set_references,
16 write::{LiveRevision, PropertyObject, chain_depth, compact, declared},
17};
18use std::{
19 collections::{BTreeMap, BTreeSet},
20 sync::Arc,
21};
22
23type Result<T> = std::result::Result<T, Error>;
24
25/// The context of a page's version history, {7111497F-1B6B-4209-9491-C98B04CF4C5A},1.
26pub(crate) const HISTORY: ExGuid = ExGuid {
27 guid: [
28 0x7f, 0x49, 0x11, 0x71, 0x6b, 0x1b, 0x09, 0x42, 0x94, 0x91, 0xc9, 0x8b, 0x04, 0xcf, 0x4c,
29 0x5a,
30 ],
31 n: 1,
32};
33
34/// A version's context is its revision's identity with these bits flipped, and its proxy's
35/// identity the context's with `PROXY` flipped, as OneNote 2010 derives both.
36const CONTEXT: [u8; 16] = [
37 0x7c, 0xfe, 0x52, 0x9d, 0xbc, 0x8f, 0x95, 0x44, 0x93, 0x00, 0x12, 0x9b, 0x57, 0x97, 0xa9, 0xea,
38];
39const PROXY: [u8; 16] = [
40 0xca, 0x0b, 0x44, 0x88, 0x8c, 0x11, 0x81, 0x0e, 0x32, 0xbf, 0x83, 0xdb, 0x8c, 0xb8, 0xb6, 0xb5,
41];
42
43fn flipped(mut id: ExGuid, salt: [u8; 16]) -> ExGuid {
44 for (byte, salt) in id.guid.iter_mut().zip(salt) {
45 *byte ^= salt;
46 }
47 id
48}
49
50/// An earlier state of a page, as the page list names it.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct PageVersion {
53 /// The context its revision is current under, which names it.
54 pub context: ExGuid,
55 /// FILETIME of its last change (`LastModifiedTimeStamp`).
56 pub modified: Option<u64>,
57 /// Who last changed it (`AuthorMostRecent`).
58 pub author: Option<String>,
59}
60
61fn element<'o>(object: &Object<'o>) -> Result<Element<'o>> {
62 Element::parse_with(object, FileType::Section, &mut |_| {
63 Err(Error {
64 offset: 0,
65 message: "Version metadata holds no payload",
66 })
67 })
68}
69
70/// When `revision`'s page was last changed and by whom, as its revision metadata says.
71fn last_change(revision: &ResolvedRevision<'_>) -> Result<(Option<u64>, Option<ExGuid>)> {
72 let Some(metadata) = revision
73 .roots
74 .get(&4)
75 .and_then(|id| revision.objects.get(id))
76 else {
77 return Ok((None, None));
78 };
79 let metadata = element(metadata)?;
80 let Kind::RevisionMetadata { modified_filetime } = metadata.kind else {
81 return Ok((None, None));
82 };
83 Ok((modified_filetime, metadata.latest_author))
84}
85
86impl<'a> Section<'a> {
87 /// The versions of each page that has them, in section order, each page's newest first as
88 /// OneNote 2010 lists them; read from the version histories alone.
89 pub fn versions(&mut self) -> Result<Vec<(ExGuid, Vec<PageVersion>)>> {
90 let mut listed = Vec::new();
91 for (page, ..) in self.pages()? {
92 let versions: Vec<PageVersion> = self
93 .listed(page)?
94 .into_iter()
95 .map(|(_, version)| version)
96 .collect();
97 if !versions.is_empty() {
98 listed.push((page, versions));
99 }
100 }
101 Ok(listed)
102 }
103
104 /// Page `space` as its version `context` holds it. Reads the sealed image: O(section).
105 pub fn version(&self, space: ExGuid, context: ExGuid) -> Result<Page> {
106 if !self
107 .listed(space)?
108 .iter()
109 .any(|(_, version)| version.context == context)
110 {
111 return Err(Error {
112 offset: 0,
113 message: "The page has no such version",
114 });
115 }
116 let stored = &self.spaces[&space];
117 if let Some(frozen) = stored
118 .frozen
119 .iter()
120 .find(|frozen| flipped(frozen.rid, CONTEXT) == context)
121 {
122 let files = self.files();
123 let pending = &self.payloads;
124 let (view, _) = Revision::parse(
125 space,
126 &frozen.live.revision,
127 FileType::Section,
128 &mut |guid| match pending.iter().find(|(id, _)| *id == guid) {
129 Some((_, payload)) => Ok(*payload),
130 None => files(guid),
131 },
132 )?;
133 return one_page(&view, &manifest_pages(&view));
134 }
135 let image = self.image();
136 let store = Store::parse(&image)?;
137 let rid = self.version_revision(&RevisionIndex::parse(&store)?, space, context)?;
138 self.page_at(space, rid)
139 }
140
141 /// Page `space` as its stored revision `revision` holds it, whichever context that
142 /// revision is current under, if any. Reads the sealed image: O(section).
143 pub fn page_at(&self, space: ExGuid, revision: ExGuid) -> Result<Page> {
144 let image = self.image();
145 let store = Store::parse(&image)?;
146 let resolved = RevisionIndex::parse(&store)?.resolve(space, revision)?;
147 let resolved = self.resident(&image, resolved)?;
148 let (view, _) = Revision::parse(space, &resolved, FileType::Section, &mut |guid| {
149 let stored = store.file_data(guid)?;
150 match &self.unlocked {
151 Some(unlocked) => unlocked.file(&self.arena.0, stored),
152 None => Ok(stored),
153 }
154 })?;
155 one_page(&view, &manifest_pages(&view))
156 }
157
158 /// Page `space`'s versions, newest first, with their proxies.
159 fn listed(&self, space: ExGuid) -> Result<Vec<(ExGuid, PageVersion)>> {
160 let Some(history) = self
161 .spaces
162 .get(&space)
163 .and_then(|space| space.history.as_ref())
164 else {
165 return Ok(Vec::new());
166 };
167 let lookup = |id: &ExGuid| -> Result<Option<Object<'_>>> {
168 match history.pending.get(id) {
169 Some(object) => {
170 declared(object.jcid, &object.bytes, Arc::clone(&object.global_ids)).map(Some)
171 }
172 None => Ok(history.revision.objects.get(id).cloned()),
173 }
174 };
175 let Some(root) = history.revision.roots.get(&1) else {
176 return Ok(Vec::new());
177 };
178 let Some(root) = lookup(root)? else {
179 return Ok(Vec::new());
180 };
181 let mut versions = Vec::new();
182 for proxy in element(&root)?.children {
183 let Some(object) = lookup(&proxy)? else {
184 return Err(Error {
185 offset: 0,
186 message: "A version proxy is unavailable",
187 });
188 };
189 let node = element(&object)?;
190 let Kind::VersionProxy {
191 context,
192 modified_filetime,
193 } = node.kind
194 else {
195 return Err(Error {
196 offset: 0,
197 message: "A version history lists another kind of object",
198 });
199 };
200 let author = match node
201 .latest_author
202 .as_ref()
203 .map(lookup)
204 .transpose()?
205 .flatten()
206 {
207 Some(author) => match element(&author)?.kind {
208 Kind::Author { name } => name,
209 _ => None,
210 },
211 None => None,
212 };
213 versions.push((
214 proxy,
215 PageVersion {
216 context,
217 modified: modified_filetime,
218 author,
219 },
220 ));
221 }
222 Ok(versions)
223 }
224
225 /// The revision version `context` of `space` is, in `index` of the sealed image.
226 fn version_revision(
227 &self,
228 index: &RevisionIndex<'_>,
229 space: ExGuid,
230 context: ExGuid,
231 ) -> Result<ExGuid> {
232 let labelled = self.spaces[&space]
233 .labels
234 .iter()
235 .find(|(_, labelled)| *labelled == context)
236 .map(|(rid, _)| *rid);
237 labelled
238 .or_else(|| index.spaces.get(&space)?.labels.get(&(context, 1)).copied())
239 .ok_or(Error {
240 offset: 0,
241 message: "A version's revision is not stored",
242 })
243 }
244
245 /// Version `context` of `space` as a revision of the section's own bytes, with its
246 /// identity and chain depth. Reads the sealed image: O(section).
247 fn historic(
248 &self,
249 space: ExGuid,
250 context: ExGuid,
251 ) -> Result<(ExGuid, ResolvedRevision<'a>, usize)> {
252 if self.spaces[&space]
253 .frozen
254 .iter()
255 .any(|frozen| flipped(frozen.rid, CONTEXT) == context)
256 {
257 return Err(Error {
258 offset: 0,
259 message: "This version is still saving. Restore it in a moment.",
260 });
261 }
262 let image = self.image();
263 let store = Store::parse(&image)?;
264 let index = RevisionIndex::parse(&store)?;
265 let rid = self.version_revision(&index, space, context)?;
266 let revision = self.resident(&image, index.resolve(space, rid)?)?;
267 Ok((rid, revision, chain_depth(&index, space, rid)))
268 }
269
270 /// `resolved`, read from `image`, the section's sealed image, with its bytes in the
271 /// section's segments, which outlive the image; a protected section's decoded.
272 fn resident(
273 &self,
274 image: &[u8],
275 resolved: ResolvedRevision<'_>,
276 ) -> Result<ResolvedRevision<'a>> {
277 let resident = |part: &[u8]| -> Result<&'a [u8]> {
278 let offset = (part.as_ptr().addr() - image.as_ptr().addr()) as u64;
279 self.segments
280 .iter()
281 .rfind(|(start, _)| *start <= offset)
282 .and_then(|(start, segment)| {
283 let at = (offset - start) as usize;
284 segment.get(at..at + part.len())
285 })
286 .filter(|bytes| *bytes == part)
287 .ok_or(Error {
288 offset: offset as usize,
289 message: "A version's bytes lie outside the section",
290 })
291 };
292 super::bind(self.arena, self.unlocked.as_ref(), resolved, resident)
293 }
294
295 /// Restores version `context` of page `space` as OneNote 2010's Restore Version does: the
296 /// page as it stands becomes the newest version, and the page's next revision builds on
297 /// the version's, its revision metadata naming `author` now. New objects take identities
298 /// from `guid`.
299 pub(crate) fn restore(
300 &mut self,
301 author: &str,
302 space: ExGuid,
303 context: ExGuid,
304 guid: [u8; 16],
305 ) -> std::result::Result<(), Failure> {
306 if !self
307 .listed(space)?
308 .iter()
309 .any(|(_, version)| version.context == context)
310 {
311 return Err(OpError::TargetUnavailable(context).into());
312 }
313 let (from, revision, depth) = self.historic(space, context)?;
314 let (now, filetime) = current_timestamps()?;
315 let named = |n| ExGuid { guid, n };
316 self.remember(space);
317 let files = self.files();
318 let arena = &self.arena.0;
319 self.editable(space)?;
320 let stored = self.spaces.get_mut(&space).unwrap();
321 let SpaceState::Open(open) = &mut stored.state else {
322 unreachable!()
323 };
324 // The page as it stands: the stored revision, or its unsaved state as one of its own.
325 let (current, modified, who) = if open.pending.is_empty() && stored.restored.is_none() {
326 let rid = stored.rid.ok_or(Error {
327 offset: 0,
328 message: "A page never saved has no versions",
329 })?;
330 let (modified, who) = last_change(&open.file.revision)?;
331 let who = who
332 .and_then(|id| open.file.revision.objects.get(&id))
333 .cloned();
334 (rid, modified, who)
335 } else {
336 let mut live = open.file.clone();
337 let revised = changed(arena, &open.page.live, &open.pending, &mut live, true)?
338 .expect("a forced revision is written");
339 let rid = named(1);
340 stored.frozen.push(Frozen {
341 rid,
342 previous: stored.restored.or(stored.rid),
343 live,
344 revised,
345 });
346 (rid, None, None)
347 };
348 let label = flipped(current, CONTEXT);
349 stored.labels.push((current, label));
350 // Its proxy, newest in the history, names when and by whom it was last changed.
351 let proxy = flipped(label, PROXY);
352 let proxy_author = named(3);
353 let author_object = match &who {
354 Some(object) => PropertyObject::from_object(object)?,
355 None => PropertyObject {
356 jcid: 0x120001,
357 bytes: properties(&author_properties(author))?,
358 global_ids: Arc::new(BTreeMap::new()),
359 },
360 };
361 let author_object = PropertyObject {
362 global_ids: Arc::new(BTreeMap::from([(0, proxy_author.guid)])),
363 ..author_object
364 };
365 let table = BTreeMap::from([(0, proxy.guid), (1, proxy_author.guid), (2, label.guid)]);
366 let proxy_object = PropertyObject {
367 jcid: 0x6003d,
368 bytes: properties(&[
369 (0x14001d09, now.to_le_bytes().to_vec()),
370 (
371 0x18001d77,
372 modified.unwrap_or(filetime).to_le_bytes().to_vec(),
373 ),
374 (0x20001d79, compact(proxy_author, &table)?.to_vec()),
375 (0x3400347b, compact(label, &table)?.to_vec()),
376 ])?,
377 global_ids: Arc::new(table),
378 };
379 let listed: Vec<ExGuid> = self
380 .listed(space)?
381 .into_iter()
382 .map(|(proxy, _)| proxy)
383 .collect();
384 let stored = self.spaces.get_mut(&space).unwrap();
385 let history = stored.history.as_mut().ok_or(Error {
386 offset: 0,
387 message: "The page has no version history",
388 })?;
389 let root = *history.revision.roots.get(&1).ok_or(Error {
390 offset: 0,
391 message: "The version history has no content",
392 })?;
393 let mut root_object = match history.pending.get(&root) {
394 Some(object) => object.clone(),
395 None => PropertyObject::from_object(&history.revision.objects[&root])?,
396 };
397 let children: Vec<ExGuid> = std::iter::once(proxy).chain(listed).collect();
398 set_references(&mut root_object, 0x24001c20, &children)?;
399 history.pending.extend([
400 (root, root_object),
401 (proxy, proxy_object),
402 (proxy_author, author_object),
403 ]);
404 // The page's next revision builds on the version's.
405 let file = LiveRevision::new(revision, depth)?;
406 let page = ActivePage::open(space, file.clone(), FileType::Section, files)?;
407 stored.state = SpaceState::Open(Box::new(Open {
408 file,
409 page,
410 pending: BTreeSet::new(),
411 }));
412 stored.restored = Some(from);
413 // Its revision metadata names who restored it, and when.
414 let active = self.active(space)?;
415 let revision = &active.live.revision;
416 let mut changes = BTreeMap::new();
417 if let Some(id) = revision.roots.get(&4)
418 && revision.objects[id].jcid == 0x20044
419 {
420 let restorer = named(2);
421 let mut metadata = PropertyObject::from_object(&revision.objects[id])?;
422 let reference = metadata.reference(restorer)?;
423 metadata.set(&[
424 (0x18001d77, &filetime.to_le_bytes()),
425 (0x20001d79, &reference),
426 ])?;
427 changes.insert(*id, metadata);
428 changes.insert(
429 restorer,
430 PropertyObject {
431 jcid: 0x120001,
432 bytes: properties(&author_properties(author))?,
433 global_ids: Arc::new(BTreeMap::from([(0, restorer.guid)])),
434 },
435 );
436 }
437 self.apply_changes(space, &[], changes)?;
438 Ok(())
439 }
440
441 /// Deletes versions `contexts` of page `space` as OneNote 2010's Delete Version does: its
442 /// history stops listing them; their revisions stay stored. True when none is left.
443 pub(crate) fn unlist(
444 &mut self,
445 space: ExGuid,
446 contexts: &[ExGuid],
447 ) -> std::result::Result<bool, Failure> {
448 let listed = self.listed(space)?;
449 if let Some(context) = contexts.iter().find(|context| {
450 !listed
451 .iter()
452 .any(|(_, version)| version.context == **context)
453 }) {
454 return Err(OpError::TargetUnavailable(*context).into());
455 }
456 let kept: Vec<ExGuid> = listed
457 .iter()
458 .filter(|(_, version)| !contexts.contains(&version.context))
459 .map(|(proxy, _)| *proxy)
460 .collect();
461 self.remember(space);
462 let history = self
463 .spaces
464 .get_mut(&space)
465 .and_then(|space| space.history.as_mut())
466 .ok_or(Error {
467 offset: 0,
468 message: "The page has no version history",
469 })?;
470 let root = *history.revision.roots.get(&1).ok_or(Error {
471 offset: 0,
472 message: "The version history has no content",
473 })?;
474 let mut object = match history.pending.get(&root) {
475 Some(object) => object.clone(),
476 None => PropertyObject::from_object(&history.revision.objects[&root])?,
477 };
478 if kept.is_empty() {
479 object.remove(&[0x24001c20])?;
480 } else {
481 set_references(&mut object, 0x24001c20, &kept)?;
482 }
483 history.pending.insert(root, object);
484 Ok(kept.is_empty())
485 }
486}