| 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 | |
| 7 | use super::{Frozen, Open, Section, SpaceState, changed, one_page}; |
| 8 | use 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 | }; |
| 18 | use std::{ |
| 19 | collections::{BTreeMap, BTreeSet}, |
| 20 | sync::Arc, |
| 21 | }; |
| 22 | |
| 23 | type Result<T> = std::result::Result<T, Error>; |
| 24 | |
| 25 | /// The context of a page's version history, {7111497F-1B6B-4209-9491-C98B04CF4C5A},1. |
| 26 | pub(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. |
| 36 | const CONTEXT: [u8; 16] = [ |
| 37 | 0x7c, 0xfe, 0x52, 0x9d, 0xbc, 0x8f, 0x95, 0x44, 0x93, 0x00, 0x12, 0x9b, 0x57, 0x97, 0xa9, 0xea, |
| 38 | ]; |
| 39 | const PROXY: [u8; 16] = [ |
| 40 | 0xca, 0x0b, 0x44, 0x88, 0x8c, 0x11, 0x81, 0x0e, 0x32, 0xbf, 0x83, 0xdb, 0x8c, 0xb8, 0xb6, 0xb5, |
| 41 | ]; |
| 42 | |
| 43 | fn 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)] |
| 52 | pub 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 | |
| 61 | fn 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. |
| 71 | fn 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 | |
| 86 | impl<'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 | } |