1use crate::{Error, FileType, Node, Reference, Store, bytes::Cursor};
2use std::{collections::BTreeMap, fmt};
3
4type Result<T> = std::result::Result<T, Error>;
5
6#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct ExGuid {
8 /// GUID bytes in Microsoft's mixed-endian order.
9 pub guid: [u8; 16],
10 pub n: u32,
11}
12
13impl 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
47impl 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
69impl 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
78impl<'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
87impl 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
103impl<'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)]
138pub struct Revision<'a> {
139 pub dependency: Option<ExGuid>,
140 pub encrypted: bool,
141 pub nodes: &'a [Node<'a>],
142}
143
144#[derive(Debug)]
145pub struct ObjectSpace<'a> {
146 pub revisions: BTreeMap<ExGuid, Revision<'a>>,
147 pub labels: BTreeMap<(ExGuid, u32), ExGuid>,
148}
149
150#[derive(Debug)]
151pub struct RevisionIndex<'a> {
152 pub root: ExGuid,
153 pub spaces: BTreeMap<ExGuid, ObjectSpace<'a>>,
154 pub(crate) store: &'a Store<'a>,
155}
156
157impl<'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
413impl<'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}