| 1 | use crate::{Error, FileType, Node, Reference, Store, bytes::Cursor}; |
| 2 | use std::{collections::BTreeMap, fmt}; |
| 3 | |
| 4 | type Result<T> = std::result::Result<T, Error>; |
| 5 | |
| 6 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
| 7 | pub struct ExGuid { |
| 8 | /// GUID bytes in Microsoft's mixed-endian order. |
| 9 | pub guid: [u8; 16], |
| 10 | pub n: u32, |
| 11 | } |
| 12 | |
| 13 | impl std::str::FromStr for ExGuid { |
| 14 | type Err = Error; |
| 15 | |
| 16 | /// Parses the display form, accepting either hexadecimal letter case. |
| 17 | fn from_str(value: &str) -> Result<Self> { |
| 18 | let invalid = || Error { |
| 19 | offset: 0, |
| 20 | message: "Invalid extended GUID", |
| 21 | }; |
| 22 | let (guid_text, extension) = value.split_once(',').ok_or_else(invalid)?; |
| 23 | if guid_text.len() != 38 || !guid_text.is_ascii() || !(40..=49).contains(&value.len()) { |
| 24 | return Err(invalid()); |
| 25 | } |
| 26 | let mut guid = [0; 16]; |
| 27 | for (byte, at) in guid |
| 28 | .iter_mut() |
| 29 | .zip([1, 3, 5, 7, 10, 12, 15, 17, 20, 22, 25, 27, 29, 31, 33, 35]) |
| 30 | { |
| 31 | *byte = u8::from_str_radix(&guid_text[at..at + 2], 16).map_err(|_| invalid())?; |
| 32 | } |
| 33 | guid[..4].reverse(); |
| 34 | guid[4..6].reverse(); |
| 35 | guid[6..8].reverse(); |
| 36 | let id = Self { |
| 37 | guid, |
| 38 | n: extension.parse().map_err(|_| invalid())?, |
| 39 | }; |
| 40 | if (id.guid == [0; 16] && id.n != 0) || !id.to_string().eq_ignore_ascii_case(value) { |
| 41 | return Err(invalid()); |
| 42 | } |
| 43 | Ok(id) |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | impl fmt::Display for ExGuid { |
| 48 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 49 | let g = &self.guid; |
| 50 | write!( |
| 51 | f, |
| 52 | "{{{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}}},{}", |
| 53 | u32::from_le_bytes(g[..4].try_into().unwrap()), |
| 54 | u16::from_le_bytes(g[4..6].try_into().unwrap()), |
| 55 | u16::from_le_bytes(g[6..8].try_into().unwrap()), |
| 56 | g[8], |
| 57 | g[9], |
| 58 | g[10], |
| 59 | g[11], |
| 60 | g[12], |
| 61 | g[13], |
| 62 | g[14], |
| 63 | g[15], |
| 64 | self.n |
| 65 | ) |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | impl serde::Serialize for ExGuid { |
| 70 | fn serialize<S: serde::Serializer>( |
| 71 | &self, |
| 72 | serializer: S, |
| 73 | ) -> std::result::Result<S::Ok, S::Error> { |
| 74 | serializer.collect_str(self) |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | impl<'de> serde::Deserialize<'de> for ExGuid { |
| 79 | fn deserialize<D: serde::Deserializer<'de>>( |
| 80 | deserializer: D, |
| 81 | ) -> std::result::Result<Self, D::Error> { |
| 82 | let value = <String as serde::Deserialize>::deserialize(deserializer)?; |
| 83 | value.parse().map_err(serde::de::Error::custom) |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | impl Cursor<'_> { |
| 88 | pub(crate) fn exguid(&mut self) -> Result<ExGuid> { |
| 89 | let id = ExGuid { |
| 90 | guid: self.read()?, |
| 91 | n: u32::from_le_bytes(self.read()?), |
| 92 | }; |
| 93 | if id.guid == [0; 16] && id.n != 0 { |
| 94 | return Err(Error { |
| 95 | offset: self.offset - 20, |
| 96 | message: "Zero GUID has a nonzero extension", |
| 97 | }); |
| 98 | } |
| 99 | Ok(id) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | impl<'a> Node<'a> { |
| 104 | pub(crate) fn fields(&self, store: &Store<'a>) -> Cursor<'a> { |
| 105 | Cursor { |
| 106 | bytes: self.payload, |
| 107 | offset: self |
| 108 | .payload |
| 109 | .as_ptr() |
| 110 | .addr() |
| 111 | .checked_sub(store.data.as_ptr().addr()) |
| 112 | .filter(|offset| *offset <= store.data.len()) |
| 113 | .unwrap_or(self.offset), |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | /// Whether the reference is fcrZero or fcrNil, as OneNote leaves one whose target its |
| 118 | /// garbage collection freed. |
| 119 | pub(crate) fn freed(&self) -> bool { |
| 120 | matches!( |
| 121 | self.reference, |
| 122 | Some(Reference::Data(chunk) | Reference::NodeList(chunk)) if chunk.absent() |
| 123 | ) |
| 124 | } |
| 125 | |
| 126 | pub(crate) fn referenced_list<'s>(&self, store: &'s Store<'a>) -> Result<&'s [Node<'a>]> { |
| 127 | let Some(Reference::NodeList(chunk)) = self.reference else { |
| 128 | return Err(Error { |
| 129 | offset: self.offset, |
| 130 | message: "Node requires a file-node list reference", |
| 131 | }); |
| 132 | }; |
| 133 | Ok(&store.list(chunk)?.nodes) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | #[derive(Debug)] |
| 138 | pub struct Revision<'a> { |
| 139 | pub dependency: Option<ExGuid>, |
| 140 | pub encrypted: bool, |
| 141 | pub nodes: &'a [Node<'a>], |
| 142 | } |
| 143 | |
| 144 | #[derive(Debug)] |
| 145 | pub struct ObjectSpace<'a> { |
| 146 | pub revisions: BTreeMap<ExGuid, Revision<'a>>, |
| 147 | pub labels: BTreeMap<(ExGuid, u32), ExGuid>, |
| 148 | } |
| 149 | |
| 150 | #[derive(Debug)] |
| 151 | pub struct RevisionIndex<'a> { |
| 152 | pub root: ExGuid, |
| 153 | pub spaces: BTreeMap<ExGuid, ObjectSpace<'a>>, |
| 154 | pub(crate) store: &'a Store<'a>, |
| 155 | } |
| 156 | |
| 157 | impl<'a> RevisionIndex<'a> { |
| 158 | pub fn parse(store: &'a Store<'a>) -> Result<Self> { |
| 159 | let mut root = None; |
| 160 | let mut spaces = BTreeMap::new(); |
| 161 | for node in &store.list(store.header.root)?.nodes { |
| 162 | match node.id { |
| 163 | 4 => { |
| 164 | let id = node.fields(store).exguid()?; |
| 165 | if !spaces.contains_key(&id) { |
| 166 | return Err(Error { |
| 167 | offset: node.offset, |
| 168 | message: "Root object space is referenced before its declaration", |
| 169 | }); |
| 170 | } |
| 171 | if root.replace(id).is_some() { |
| 172 | return Err(Error { |
| 173 | offset: node.offset, |
| 174 | message: "Multiple root object spaces", |
| 175 | }); |
| 176 | } |
| 177 | } |
| 178 | 8 if node.freed() => {} |
| 179 | 8 => { |
| 180 | let id = node.fields(store).exguid()?; |
| 181 | let manifest = node.referenced_list(store)?; |
| 182 | let Some(first) = manifest.first() else { |
| 183 | return Err(Error { |
| 184 | offset: node.offset, |
| 185 | message: "Empty object-space manifest", |
| 186 | }); |
| 187 | }; |
| 188 | if id == ExGuid::default() |
| 189 | || first.id != 0xc |
| 190 | || first.fields(store).exguid()? != id |
| 191 | { |
| 192 | return Err(Error { |
| 193 | offset: first.offset, |
| 194 | message: "Object-space identity does not match its manifest", |
| 195 | }); |
| 196 | } |
| 197 | let last = manifest.iter().rfind(|node| node.id == 0x10).ok_or(Error { |
| 198 | offset: first.offset, |
| 199 | message: "Object space has no revision manifest list", |
| 200 | })?; |
| 201 | let nodes = last.referenced_list(store)?; |
| 202 | let Some(first) = nodes.first() else { |
| 203 | return Err(Error { |
| 204 | offset: last.offset, |
| 205 | message: "Empty revision manifest list", |
| 206 | }); |
| 207 | }; |
| 208 | if first.id != 0x14 || first.fields(store).exguid()? != id { |
| 209 | return Err(Error { |
| 210 | offset: first.offset, |
| 211 | message: "Revision list belongs to a different object space", |
| 212 | }); |
| 213 | } |
| 214 | let mut revisions: BTreeMap<ExGuid, Revision<'a>> = BTreeMap::new(); |
| 215 | let mut labels = BTreeMap::new(); |
| 216 | let mut encryption_data = None; |
| 217 | let mut position = 1; |
| 218 | while position < nodes.len() { |
| 219 | let node = &nodes[position]; |
| 220 | let mut c = node.fields(store); |
| 221 | match node.id { |
| 222 | 0x1b | 0x1e | 0x1f => { |
| 223 | if (node.id == 0x1b) |
| 224 | != (store.header.file_type == FileType::TableOfContents) |
| 225 | { |
| 226 | return Err(Error { |
| 227 | offset: node.offset, |
| 228 | message: "Revision encoding does not match the file type", |
| 229 | }); |
| 230 | } |
| 231 | let rid = c.exguid()?; |
| 232 | let dependency = c.exguid()?; |
| 233 | if node.id == 0x1b { |
| 234 | c.take(8)?; |
| 235 | } |
| 236 | let role = u32::from_le_bytes(c.read()?); |
| 237 | let encoding = u16::from_le_bytes(c.read()?); |
| 238 | let context = if node.id == 0x1f { |
| 239 | c.exguid()? |
| 240 | } else { |
| 241 | ExGuid::default() |
| 242 | }; |
| 243 | if rid == ExGuid::default() |
| 244 | || role > 0xffff |
| 245 | || !matches!(encoding, 0 | 2) |
| 246 | || (node.id == 0x1b && encoding != 0) |
| 247 | { |
| 248 | return Err(Error { |
| 249 | offset: node.offset, |
| 250 | message: "Invalid revision identity, role, or encoding", |
| 251 | }); |
| 252 | } |
| 253 | let dependency = if dependency == ExGuid::default() { |
| 254 | None |
| 255 | } else { |
| 256 | let previous = revisions.get(&dependency).ok_or(Error { |
| 257 | offset: node.offset, |
| 258 | message: "Revision dependency is not an earlier revision", |
| 259 | })?; |
| 260 | if previous.encrypted != (encoding == 2) { |
| 261 | return Err(Error { |
| 262 | offset: node.offset, |
| 263 | message: "Revision changes its dependency's encryption mode", |
| 264 | }); |
| 265 | } |
| 266 | Some(dependency) |
| 267 | }; |
| 268 | let start = position + 1; |
| 269 | position = start; |
| 270 | while position < nodes.len() && nodes[position].id != 0x1c { |
| 271 | if matches!( |
| 272 | nodes[position].id, |
| 273 | 0x1b | 0x1e | 0x1f | 0x5c | 0x5d |
| 274 | ) { |
| 275 | return Err(Error { |
| 276 | offset: nodes[position].offset, |
| 277 | message: "Unterminated revision manifest", |
| 278 | }); |
| 279 | } |
| 280 | position += 1; |
| 281 | } |
| 282 | if position == nodes.len() { |
| 283 | return Err(Error { |
| 284 | offset: node.offset, |
| 285 | message: "Unterminated revision manifest", |
| 286 | }); |
| 287 | } |
| 288 | let body = &nodes[start..position]; |
| 289 | for (position, item) in body.iter().enumerate() { |
| 290 | if item.id == 0xb0 |
| 291 | && body.get(position + 1).is_none_or(|next| next.id != 0x84) |
| 292 | { |
| 293 | return Err(Error { |
| 294 | offset: item.offset, |
| 295 | message: "Object group lacks its dependency overrides", |
| 296 | }); |
| 297 | } |
| 298 | } |
| 299 | // A protected revision names its key unless it inherits the |
| 300 | // key of the revision it depends on. |
| 301 | let keyed = body.first().is_some_and(|node| node.id == 0x7c); |
| 302 | if keyed && encoding != 2 |
| 303 | || !keyed && encoding == 2 && dependency.is_none() |
| 304 | { |
| 305 | return Err(Error { |
| 306 | offset: node.offset, |
| 307 | message: "Revision encryption key does not match its encoding", |
| 308 | }); |
| 309 | } |
| 310 | if revisions |
| 311 | .values() |
| 312 | .next() |
| 313 | .is_some_and(|previous| previous.encrypted != (encoding == 2)) |
| 314 | { |
| 315 | return Err(Error { |
| 316 | offset: node.offset, |
| 317 | message: "Object space mixes encrypted and unencrypted revisions", |
| 318 | }); |
| 319 | } |
| 320 | if keyed { |
| 321 | let key = &body[0]; |
| 322 | let Some(Reference::Data(chunk)) = key.reference else { |
| 323 | return Err(Error { |
| 324 | offset: key.offset, |
| 325 | message: "Encryption key lacks a data reference", |
| 326 | }); |
| 327 | }; |
| 328 | let data = store.encryption_key(chunk)?; |
| 329 | if encryption_data |
| 330 | .replace(data) |
| 331 | .is_some_and(|previous| previous != data) |
| 332 | { |
| 333 | return Err(Error { |
| 334 | offset: key.offset, |
| 335 | message: "Object space changes its encryption key", |
| 336 | }); |
| 337 | } |
| 338 | } |
| 339 | if revisions |
| 340 | .insert( |
| 341 | rid, |
| 342 | Revision { |
| 343 | dependency, |
| 344 | encrypted: encoding == 2, |
| 345 | nodes: body, |
| 346 | }, |
| 347 | ) |
| 348 | .is_some() |
| 349 | { |
| 350 | return Err(Error { |
| 351 | offset: node.offset, |
| 352 | message: "Duplicate revision identity", |
| 353 | }); |
| 354 | } |
| 355 | labels.insert((context, role), rid); |
| 356 | } |
| 357 | 0x5c | 0x5d => { |
| 358 | let rid = c.exguid()?; |
| 359 | let role = u32::from_le_bytes(c.read()?); |
| 360 | let context = if node.id == 0x5d { |
| 361 | c.exguid()? |
| 362 | } else { |
| 363 | ExGuid::default() |
| 364 | }; |
| 365 | if !revisions.contains_key(&rid) || role > 0xffff { |
| 366 | return Err(Error { |
| 367 | offset: node.offset, |
| 368 | message: "Revision label has an invalid target or role", |
| 369 | }); |
| 370 | } |
| 371 | labels.insert((context, role), rid); |
| 372 | } |
| 373 | _ => { |
| 374 | return Err(Error { |
| 375 | offset: node.offset, |
| 376 | message: "Unexpected node between revision manifests", |
| 377 | }); |
| 378 | } |
| 379 | } |
| 380 | position += 1; |
| 381 | } |
| 382 | if spaces |
| 383 | .insert(id, ObjectSpace { revisions, labels }) |
| 384 | .is_some() |
| 385 | { |
| 386 | return Err(Error { |
| 387 | offset: node.offset, |
| 388 | message: "Duplicate object-space identity", |
| 389 | }); |
| 390 | } |
| 391 | } |
| 392 | 0x90 => {} |
| 393 | _ => { |
| 394 | return Err(Error { |
| 395 | offset: node.offset, |
| 396 | message: "Unexpected node in the root file-node list", |
| 397 | }); |
| 398 | } |
| 399 | } |
| 400 | } |
| 401 | let root = root.filter(|id| spaces.contains_key(id)).ok_or(Error { |
| 402 | offset: 172, |
| 403 | message: "Root object space is not declared", |
| 404 | })?; |
| 405 | Ok(Self { |
| 406 | root, |
| 407 | spaces, |
| 408 | store, |
| 409 | }) |
| 410 | } |
| 411 | } |
| 412 | |
| 413 | impl<'a> RevisionIndex<'a> { |
| 414 | /// The revision labelled current in the default context of one object space. |
| 415 | pub fn active(&self, space: ExGuid) -> Result<ExGuid> { |
| 416 | self.spaces |
| 417 | .get(&space) |
| 418 | .and_then(|space| space.labels.get(&(ExGuid::default(), 1))) |
| 419 | .copied() |
| 420 | .ok_or(Error { |
| 421 | offset: 0, |
| 422 | message: "Object space has no active default revision", |
| 423 | }) |
| 424 | } |
| 425 | |
| 426 | pub fn resolve_active(&self, space: ExGuid) -> Result<crate::ResolvedRevision<'a>> { |
| 427 | self.resolve(space, self.active(space)?) |
| 428 | } |
| 429 | } |