1//! Referenced MS-ONE document revisions. Coordinates are points; text offsets are UTF-16 units.
2//!
3//! These are inspection views of one section or table-of-contents file. Mutating
4//! their public fields does not edit the notebook; use the crate's commit functions.
5
6use crate::{
7 Error, ExGuid, FileDataReference, IdStream, Object, ObjectData, Property, PropertySets,
8 RevisionIndex, Value, bytes::Cursor,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::{BTreeMap, BTreeSet};
12
13type Result<T> = std::result::Result<T, Error>;
14
15fn no_ciphertext<'a>() -> &'a [u8] {
16 &[]
17}
18
19fn invalid(message: &'static str) -> Error {
20 Error { offset: 0, message }
21}
22
23#[derive(Debug, Serialize)]
24/// Borrows source payloads; retain the input snapshot and store while using this view.
25pub struct Document<'a> {
26 pub file_id: [u8; 16],
27 pub root: ExGuid,
28 pub spaces: BTreeMap<ExGuid, Space<'a>>,
29}
30
31#[derive(Debug, Serialize)]
32pub struct Space<'a> {
33 pub contexts: BTreeMap<ExGuid, ExGuid>,
34 pub revisions: BTreeMap<ExGuid, Revision<'a>>,
35}
36
37impl<'a> Space<'a> {
38 /// The revision of the default context, which is the current content of the space.
39 pub fn active(&self) -> Option<&Revision<'a>> {
40 self.revisions.get(self.contexts.get(&ExGuid::default())?)
41 }
42}
43
44#[derive(Debug, Clone, Serialize)]
45pub struct Revision<'a> {
46 pub roots: BTreeMap<u32, ExGuid>,
47 pub nodes: BTreeMap<ExGuid, Element<'a>>,
48}
49
50#[derive(Debug, Clone, Serialize)]
51pub struct Element<'a> {
52 pub jcid: u32,
53 pub children: Vec<ExGuid>,
54 pub content: Vec<ExGuid>,
55 pub structure: Vec<ExGuid>,
56 pub spaces: Vec<ExGuid>,
57 /// Relative indentation level of child outline elements.
58 pub child_level: Option<u8>,
59 pub layout: Layout,
60 pub format: Format,
61 /// Time32 seconds since 1980-01-01 UTC.
62 pub created: Option<u32>,
63 /// Time32 seconds since 1980-01-01 UTC.
64 pub modified: Option<u32>,
65 pub original_author: Option<ExGuid>,
66 pub latest_author: Option<ExGuid>,
67 pub media_ids: Vec<[u8; 16]>,
68 pub media_time_ms: Option<u32>,
69 pub tags: Vec<Tag>,
70 pub kind: Kind<'a>,
71 /// Uninterpreted fields retain their nested-set indices and decoded reference identities.
72 pub extra: Vec<Vec<Field<'a>>>,
73}
74
75#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
76pub struct Layout {
77 pub x: Option<f32>,
78 pub y: Option<f32>,
79 pub max_width: Option<f32>,
80 /// An explicit user width when true; false or absent leaves an automatic layout hint.
81 pub width_set_by_user: Option<bool>,
82 pub max_height: Option<f32>,
83 pub reserved_width: Option<f32>,
84}
85
86macro_rules! format_fields {
87 ($($field:ident: $value:ty),* $(,)?) => {
88 #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
89 pub struct Format { $(pub $field: Option<$value>),* }
90
91 impl Format {
92 /// Explicit false values override inherited true values.
93 pub fn inherit(&self, parent: &Self) -> Self {
94 Self { $($field: self.$field.as_ref().or(parent.$field.as_ref()).cloned()),* }
95 }
96
97 /// The values `self` sets that `parent` does not already give.
98 pub fn over(&self, parent: &Self) -> Self {
99 Self {
100 $($field: self.$field.as_ref().filter(|value| parent.$field.as_ref() != Some(*value)).cloned()),*
101 }
102 }
103 }
104 };
105}
106
107format_fields! {
108 bold: bool,
109 italic: bool,
110 underline: bool,
111 strike: bool,
112 superscript: bool,
113 subscript: bool,
114 hidden: bool,
115 hyperlink: bool,
116 hyperlink_label: bool,
117 math: bool,
118 embedded_object: bool,
119 font: String,
120 font_size: f32,
121 color: u32,
122 highlight: u32,
123 language: u32,
124 alignment: u8,
125 rtl: bool,
126 space_before: f32,
127 space_after: f32,
128 line_spacing: f32,
129 list_spacing: f32,
130 math_object: MathObject,
131}
132
133/// The inline math object a run belongs to, from its text-run data: `kind` follows the
134/// `OBJECTTYPE` values of the Windows text object model (31 superscript, 16 fraction, …),
135/// `arguments` counts the object's arguments on its opening run, and `symbols` are the
136/// characters the object builds with (a bracket pair, an n-ary operator, `^` or `/`).
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138pub struct MathObject {
139 pub kind: u32,
140 pub arguments: Option<u32>,
141 /// Column count of a matrix or equation array.
142 pub columns: Option<u8>,
143 pub symbols: Vec<char>,
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct TextRun {
148 pub start: u32,
149 pub end: u32,
150 pub format: Option<ExGuid>,
151 /// Index in the text element's extra arena for preserved TextRunData properties.
152 pub extra_set: Option<usize>,
153}
154
155#[derive(Debug, Serialize)]
156pub struct ResolvedTextRun<'a> {
157 pub text: &'a str,
158 pub format: Format,
159 pub link: Option<&'a str>,
160}
161
162impl Revision<'_> {
163 /// Resolves inherited formatting without discarding hidden fields or their source text.
164 pub fn text_runs(&self, id: ExGuid) -> Result<Vec<ResolvedTextRun<'_>>> {
165 let node = self
166 .nodes
167 .get(&id)
168 .ok_or_else(|| invalid("Missing text object"))?;
169 let Kind::RichText {
170 text,
171 runs,
172 paragraph_style,
173 ..
174 } = &node.kind
175 else {
176 return Err(invalid("Expected a rich-text object"));
177 };
178 let style = |id: ExGuid| -> Result<&Format> {
179 let node = self
180 .nodes
181 .get(&id)
182 .ok_or_else(|| invalid("Missing text style"))?;
183 if !matches!(node.kind, Kind::Style { .. }) {
184 return Err(invalid("Text formatting does not reference a style"));
185 }
186 Ok(&node.format)
187 };
188 let default = Format::default();
189 let parent = paragraph_style.map(style).transpose()?.unwrap_or(&default);
190 let base = node.format.inherit(parent);
191 let mut characters = text.char_indices();
192 let (mut units, mut byte) = (0, 0);
193 let mut target = None;
194 let mut resolved = Vec::with_capacity(runs.len());
195 for run in runs {
196 if run.start != units || run.end < units {
197 return Err(invalid("Text runs do not partition the text"));
198 }
199 let start = byte;
200 while units < run.end {
201 let (offset, character) = characters
202 .next()
203 .ok_or_else(|| invalid("Text run exceeds text"))?;
204 units = units
205 .checked_add(character.len_utf16() as u32)
206 .ok_or_else(|| invalid("Text exceeds UTF-16 offset range"))?;
207 byte = offset + character.len_utf8();
208 }
209 if units != run.end {
210 return Err(invalid("Text-run boundary splits a surrogate pair"));
211 }
212 let fragment = &text[start..byte];
213 let mut format = run
214 .format
215 .map(style)
216 .transpose()?
217 .unwrap_or(&default)
218 .inherit(&base);
219 if let Some(set) = run.extra_set.and_then(|index| node.extra.get(index)) {
220 let scalar = |id: u32| {
221 set.iter().find_map(|field| match field.value {
222 FieldValue::Bytes(bytes) if field.id == id => Some(bytes),
223 _ => None,
224 })
225 };
226 if let Some(kind) = scalar(0x1400344f) {
227 format.math_object = Some(MathObject {
228 kind: u32::from_le_bytes(
229 kind.try_into()
230 .map_err(|_| invalid("Math object type has an invalid length"))?,
231 ),
232 arguments: scalar(0x14003450)
233 .map(|b| {
234 b.try_into().map(u32::from_le_bytes).map_err(|_| {
235 invalid("Math argument count has an invalid length")
236 })
237 })
238 .transpose()?,
239 columns: scalar(0x0c003451).and_then(|b| b.first().copied()),
240 symbols: [0x10003453, 0x10003454, 0x10003455]
241 .into_iter()
242 .filter_map(scalar)
243 .map(|b| {
244 b.try_into()
245 .ok()
246 .map(u16::from_le_bytes)
247 .and_then(|unit| char::from_u32(u32::from(unit)))
248 .ok_or_else(|| invalid("Math symbol is not a character"))
249 })
250 .collect::<Result<_>>()?,
251 });
252 }
253 }
254 let mut link = None;
255 if format.hyperlink != Some(true) {
256 target = None;
257 } else if format.hidden == Some(true) {
258 // OneNote's native field encoding is retained even when its syntax is unrecognized.
259 target = fragment
260 .strip_prefix("\u{fddf}HYPERLINK \"")
261 .and_then(|s| s.strip_suffix('"'));
262 } else if format.hyperlink_label == Some(true) {
263 link = target;
264 } else {
265 link = Some(fragment);
266 }
267 resolved.push(ResolvedTextRun {
268 text: fragment,
269 format,
270 link,
271 });
272 }
273 if byte != text.len() {
274 return Err(invalid("Text runs do not cover the text"));
275 }
276 Ok(resolved)
277 }
278}
279
280impl<'a> Revision<'a> {
281 /// The elements reachable in `revision` of `space`, and the (space, context) pairs they
282 /// reference.
283 pub(crate) fn parse(
284 space: ExGuid,
285 revision: &crate::ResolvedRevision<'a>,
286 file_type: crate::FileType,
287 file_data: &mut impl FnMut([u8; 16]) -> Result<&'a [u8]>,
288 ) -> Result<(Self, Vec<(ExGuid, ExGuid)>)> {
289 let mut reachable = BTreeSet::new();
290 let mut objects: Vec<_> = revision.roots.values().copied().collect();
291 let mut encrypted = false;
292 while let Some(oid) = objects.pop() {
293 if !reachable.insert(oid) {
294 continue;
295 }
296 let object = revision
297 .objects
298 .get(&oid)
299 .ok_or_else(|| invalid("Document object has no declaration"))?;
300 if matches!(object.data, ObjectData::Encrypted(_)) {
301 encrypted = true;
302 } else {
303 objects.extend(object.references()?.objects);
304 }
305 }
306 if !encrypted {
307 revision.reachable()?;
308 }
309 let mut referenced = Vec::new();
310 let mut nodes = BTreeMap::new();
311 for oid in reachable {
312 let object = &revision.objects[&oid];
313 if !matches!(object.data, ObjectData::Encrypted(_)) {
314 let refs = object.references()?;
315 referenced.extend(
316 refs.object_spaces
317 .into_iter()
318 .map(|sid| (sid, ExGuid::default())),
319 );
320 referenced.extend(refs.contexts.into_iter().map(|context| (space, context)));
321 }
322 nodes.insert(oid, Element::parse_with(object, file_type, file_data)?);
323 }
324 for node in nodes.values() {
325 if let Kind::RichText {
326 runs,
327 paragraph_style,
328 ..
329 } = &node.kind
330 {
331 for target in paragraph_style
332 .iter()
333 .chain(runs.iter().filter_map(|r| r.format.as_ref()))
334 {
335 if !matches!(nodes.get(target).map(|n| &n.kind), Some(Kind::Style { .. })) {
336 return Err(invalid("Text formatting does not reference a style"));
337 }
338 }
339 }
340 let mut tag_types = BTreeSet::new();
341 for tag in &node.tags {
342 let action_type = if tag.status & 4 != 0 {
343 tag.action_type
344 } else {
345 match tag
346 .definition
347 .and_then(|id| nodes.get(&id))
348 .map(|n| &n.kind)
349 {
350 Some(Kind::TagDefinition { action_type, .. }) => *action_type,
351 _ => {
352 return Err(invalid("Note tag does not reference a tag definition"));
353 }
354 }
355 }
356 .ok_or_else(|| invalid("Note tag has no action type"))?;
357 if !tag_types.insert(action_type) {
358 return Err(invalid("Repeated note tag action type"));
359 }
360 }
361 }
362 Ok((
363 Self {
364 roots: revision.roots.clone(),
365 nodes,
366 },
367 referenced,
368 ))
369 }
370}
371
372impl Revision<'_> {
373 /// Maps every element reachable from `roots` through children, content and structure
374 /// references to its referencing parents, in visiting order.
375 pub fn parents(&self, roots: &[ExGuid]) -> Result<BTreeMap<ExGuid, Vec<ExGuid>>> {
376 let mut pending: Vec<_> = roots.to_vec();
377 let mut seen = BTreeSet::new();
378 let mut parents = BTreeMap::<_, Vec<_>>::new();
379 while let Some(id) = pending.pop() {
380 if !seen.insert(id) {
381 continue;
382 }
383 let element = self
384 .nodes
385 .get(&id)
386 .ok_or_else(|| invalid("Page content is unavailable"))?;
387 for child in element
388 .children
389 .iter()
390 .chain(&element.content)
391 .chain(&element.structure)
392 {
393 parents.entry(*child).or_default().push(id);
394 pending.push(*child);
395 }
396 }
397 Ok(parents)
398 }
399}
400
401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
402/// Tag dates count seconds since 1980-01-01 UTC; status retains the ActionItemStatus bits.
403/// A task tag (status bit 4) has no definition and carries its own icon and property status.
404pub struct Tag {
405 pub definition: Option<ExGuid>,
406 pub action_type: Option<u16>,
407 pub shape: Option<u16>,
408 pub property_status: Option<u32>,
409 pub status: u16,
410 pub created: Option<u32>,
411 pub completed: Option<u32>,
412 pub start: Option<u32>,
413 pub due: Option<u32>,
414 pub task_id: Option<[u8; 16]>,
415 /// Index in the element's extra-property arena for uninterpreted tag fields.
416 pub extra_set: usize,
417}
418
419#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
420#[serde(tag = "type")]
421pub enum Kind<'a> {
422 Section {
423 default_template: Option<ExGuid>,
424 },
425 PageSeries,
426 Manifest {
427 history: Option<ExGuid>,
428 },
429 VersionHistory,
430 VersionProxy {
431 context: ExGuid,
432 modified_filetime: Option<u64>,
433 },
434 VersionHistoryMetadata,
435 /// Native default-page template metadata, outside the published MS-ONE object list.
436 TemplateMetadata {
437 name: Option<String>,
438 },
439 Page {
440 alternate_title: Option<String>,
441 level: Option<u32>,
442 width: Option<f32>,
443 height: Option<f32>,
444 margin_origin_x: Option<f32>,
445 margin_origin_y: Option<f32>,
446 rtl: Option<bool>,
447 /// Every audio and video recording on the page (AudioRecordingGuids).
448 recordings: Vec<[u8; 16]>,
449 },
450 Metadata {
451 title: Option<String>,
452 level: Option<u32>,
453 },
454 ConflictMetadata {
455 title: Option<String>,
456 level: Option<u32>,
457 author: Option<String>,
458 },
459 RevisionMetadata {
460 modified_filetime: Option<u64>,
461 },
462 SectionMetadata {
463 name: Option<String>,
464 color: Option<u32>,
465 },
466 Title,
467 Outline {
468 indents: Vec<f32>,
469 },
470 Paragraph {
471 lists: Vec<ExGuid>,
472 paragraph_style: Option<ExGuid>,
473 /// Saved state: 0 expanded, 1 collapsed, otherwise opaque; native UI overrides can stay cache-local.
474 collapse_state: Option<u8>,
475 },
476 OutlineGroup,
477 /// An ink drawing or handwriting container: strokes hang off `data`, nested containers off
478 /// the element's content list. Scaling multiplies stroke coordinates. A drawn shape adds
479 /// its kind (11 a line, 12 a closed shape) and geometry: a line's ends, or a closed
480 /// shape's transform and anchors, as `page::InkShape` reads them.
481 Ink {
482 data: Option<ExGuid>,
483 scale_x: Option<f32>,
484 scale_y: Option<f32>,
485 shape_kind: Option<u8>,
486 line: Option<Vec<u8>>,
487 anchors: Option<Vec<u8>>,
488 },
489 InkData {
490 strokes: Vec<ExGuid>,
491 bounds: Option<[i32; 4]>,
492 },
493 /// One stroke: `path` holds ISF multi-byte first differences, one block per dimension of
494 /// the style's dimension table, in HIMETRIC (1/2540 inch) page coordinates.
495 InkStroke {
496 path: Vec<u8>,
497 style: Option<ExGuid>,
498 bias: Option<u8>,
499 language: Option<u16>,
500 identity: Option<[u8; 16]>,
501 index: Option<u32>,
502 },
503 /// Drawing attributes shared by strokes; `dimensions` holds 32-byte entries of GUID, lower
504 /// and upper limit, and resolution.
505 InkStyle {
506 dimensions: Vec<u8>,
507 width: Option<f32>,
508 height: Option<f32>,
509 color: Option<u32>,
510 transparency: Option<u8>,
511 pen_tip: Option<u8>,
512 raster_operation: Option<u8>,
513 antialiased: Option<bool>,
514 fit_to_curve: Option<bool>,
515 ignore_pressure: Option<bool>,
516 },
517 RichText {
518 text: String,
519 runs: Vec<TextRun>,
520 paragraph_style: Option<ExGuid>,
521 boilerplate: bool,
522 },
523 Style {
524 name: Option<String>,
525 /// NextStyle: the style Enter gives the paragraph after this one's end.
526 #[serde(default, skip_serializing_if = "Option::is_none")]
527 next: Option<String>,
528 },
529 List {
530 font: Option<String>,
531 format: Option<String>,
532 restart: Option<u32>,
533 bullet: Option<u16>,
534 },
535 /// Column arrays and row children use visual left-to-right order, including on RTL pages.
536 Table {
537 rows: Option<u32>,
538 columns: Option<u32>,
539 /// Stored widths in points; unlocked columns may fit their content in native layout.
540 widths: Vec<f32>,
541 locked: Vec<bool>,
542 borders: Option<bool>,
543 },
544 Row,
545 Cell {
546 shading: Option<u32>,
547 indents: Vec<f32>,
548 },
549 Image {
550 container: Option<ExGuid>,
551 filename: Option<String>,
552 alt: Option<String>,
553 picture_width: Option<f32>,
554 picture_height: Option<f32>,
555 background: Option<bool>,
556 printout: Option<bool>,
557 link: Option<String>,
558 },
559 Attachment {
560 container: Option<ExGuid>,
561 preview: Option<ExGuid>,
562 filename: Option<String>,
563 source_path: Option<String>,
564 recording_id: Option<[u8; 16]>,
565 recording_type: Option<u32>,
566 /// AudioRecordingDuration, in milliseconds.
567 recording_duration: Option<u32>,
568 /// Displayed icon width and height in points.
569 icon_width: Option<f32>,
570 icon_height: Option<f32>,
571 },
572 File {
573 reference: FileDataReference,
574 extension: String,
575 #[serde(skip)]
576 payload: Option<&'a [u8]>,
577 },
578 Author {
579 name: Option<String>,
580 },
581 TagDefinition {
582 label: Option<String>,
583 action_type: Option<u16>,
584 shape: Option<u16>,
585 color: Option<u32>,
586 highlight: Option<u32>,
587 },
588 Toc {
589 entries: Vec<ExGuid>,
590 filename: Option<String>,
591 identity: Option<[u8; 16]>,
592 order: Option<u32>,
593 color: Option<u32>,
594 },
595 Encrypted {
596 #[serde(serialize_with = "hex", skip_deserializing, default = "no_ciphertext")]
597 ciphertext: &'a [u8],
598 },
599 Unknown,
600}
601
602#[derive(Debug, Clone, PartialEq, Serialize)]
603pub struct Field<'a> {
604 pub id: u32,
605 pub value: FieldValue<'a>,
606}
607
608#[derive(Debug, Clone, PartialEq, Serialize)]
609pub enum FieldValue<'a> {
610 NoData,
611 Bytes(#[serde(serialize_with = "hex")] &'a [u8]),
612 Objects(Vec<ExGuid>),
613 Spaces(Vec<ExGuid>),
614 Contexts(Vec<ExGuid>),
615 Sets(std::ops::Range<usize>),
616}
617
618fn hex<S: serde::Serializer>(bytes: &&[u8], serializer: S) -> std::result::Result<S::Ok, S::Error> {
619 use std::fmt::Write;
620 let mut text = String::with_capacity(bytes.len() * 2);
621 for byte in *bytes {
622 write!(text, "{byte:02x}").unwrap();
623 }
624 serializer.serialize_str(&text)
625}
626
627fn unicode(bytes: &[u8]) -> Result<String> {
628 if !bytes.len().is_multiple_of(2) {
629 return Err(invalid("Odd UTF-16 document string length"));
630 }
631 let units: Vec<_> = bytes
632 .chunks_exact(2)
633 .map(|x| u16::from_le_bytes(x.try_into().unwrap()))
634 .collect();
635 String::from_utf16(units.strip_suffix(&[0]).unwrap_or(&units))
636 .map_err(|_| invalid("Invalid UTF-16 document string"))
637}
638
639struct Fields<'a, 'o> {
640 sets: Vec<Vec<Property<'a>>>,
641 ids: &'o BTreeMap<u32, [u8; 16]>,
642}
643
644impl<'a, 'o> Fields<'a, 'o> {
645 fn new(object: &'o Object<'a>) -> Result<Self> {
646 let ObjectData::Properties(bytes) = object.data else {
647 return Err(invalid("Expected document properties"));
648 };
649 let sets = PropertySets::parse(bytes)?.sets;
650 for set in &sets {
651 let mut seen = BTreeSet::new();
652 if set.iter().any(|p| !seen.insert(p.id & 0x7fffffff)) {
653 return Err(invalid("Repeated document property"));
654 }
655 }
656 Ok(Self {
657 sets,
658 ids: &object.global_ids,
659 })
660 }
661
662 fn take(&mut self, id: u32) -> Option<Property<'a>> {
663 let index = self.sets[0].iter().position(|p| p.id & 0x7fffffff == id)?;
664 Some(self.sets[0].remove(index))
665 }
666
667 fn bytes(&mut self, id: u32) -> Result<Option<&'a [u8]>> {
668 self.take(id)
669 .map(|p| match p.value {
670 Value::Bytes(bytes) => Ok(bytes),
671 _ => Err(invalid("Document property requires bytes")),
672 })
673 .transpose()
674 }
675
676 fn fixed<const N: usize>(&mut self, id: u32) -> Result<Option<[u8; N]>> {
677 self.bytes(id)?
678 .map(|b| {
679 b.try_into()
680 .map_err(|_| invalid("Document scalar has an invalid length"))
681 })
682 .transpose()
683 }
684
685 fn u32(&mut self, id: u32) -> Result<Option<u32>> {
686 Ok(self.fixed(id)?.map(u32::from_le_bytes))
687 }
688 fn guids(&mut self, id: u32) -> Result<Vec<[u8; 16]>> {
689 let bytes = self.bytes(id)?.unwrap_or_default();
690 if !bytes.len().is_multiple_of(16) {
691 return Err(invalid("Media identifiers have an invalid length"));
692 }
693 Ok(bytes
694 .chunks_exact(16)
695 .map(|id| id.try_into().unwrap())
696 .collect())
697 }
698 fn u16(&mut self, id: u32) -> Result<Option<u16>> {
699 Ok(self.fixed(id)?.map(u16::from_le_bytes))
700 }
701 fn u8(&mut self, id: u32) -> Result<Option<u8>> {
702 Ok(self.fixed::<1>(id)?.map(|b| b[0]))
703 }
704 fn float(&mut self, id: u32, scale: f32) -> Result<Option<f32>> {
705 self.fixed(id)?
706 .map(|b| finite(f32::from_le_bytes(b) * scale))
707 .transpose()
708 }
709 fn text(&mut self, id: u32) -> Result<Option<String>> {
710 self.bytes(id)?.map(unicode).transpose()
711 }
712 fn boolean(&mut self, id: u32) -> Result<Option<bool>> {
713 self.take(id)
714 .map(|p| match p.value {
715 Value::NoData => Ok(p.id & 0x80000000 != 0),
716 _ => Err(invalid("Document flag requires a Boolean")),
717 })
718 .transpose()
719 }
720 fn refs(&mut self, id: u32, expected: IdStream) -> Result<Vec<ExGuid>> {
721 self.take(id)
722 .map(|p| match p.value {
723 Value::References {
724 stream,
725 compact_ids,
726 } if stream == expected => references(compact_ids, self.ids),
727 _ => Err(invalid("Document reference uses the wrong stream")),
728 })
729 .transpose()
730 .map(Option::unwrap_or_default)
731 }
732 fn one(&mut self, id: u32) -> Result<Option<ExGuid>> {
733 let refs = self.refs(id, IdStream::Objects)?;
734 if refs.len() > 1 {
735 return Err(invalid("Document scalar has multiple references"));
736 }
737 Ok(refs.first().copied())
738 }
739 fn remainder(self) -> Result<Vec<Vec<Field<'a>>>> {
740 self.sets
741 .into_iter()
742 .map(|set| {
743 set.into_iter()
744 .map(|p| {
745 let value = match p.value {
746 Value::NoData => FieldValue::NoData,
747 Value::Bytes(b) => FieldValue::Bytes(b),
748 Value::Sets(r) => FieldValue::Sets(r),
749 Value::References {
750 stream,
751 compact_ids,
752 } => {
753 let refs = references(compact_ids, self.ids)?;
754 match stream {
755 IdStream::Objects => FieldValue::Objects(refs),
756 IdStream::ObjectSpaces => FieldValue::Spaces(refs),
757 IdStream::Contexts => FieldValue::Contexts(refs),
758 }
759 }
760 };
761 Ok(Field { id: p.id, value })
762 })
763 .collect()
764 })
765 .collect()
766 }
767}
768
769fn references(bytes: &[u8], ids: &BTreeMap<u32, [u8; 16]>) -> Result<Vec<ExGuid>> {
770 // A long list, such as a large outline's children, reads a dense table as an array.
771 if bytes.len() >= 256
772 && bytes.len().is_multiple_of(4)
773 && let Some((last, _)) = ids.last_key_value()
774 && *last as usize + 1 == ids.len()
775 {
776 let table: Vec<[u8; 16]> = ids.values().copied().collect();
777 return bytes
778 .chunks_exact(4)
779 .enumerate()
780 .map(|(i, bytes)| {
781 let raw = u32::from_le_bytes(bytes.try_into().unwrap());
782 let guid = table.get((raw >> 8) as usize).ok_or(Error {
783 offset: i * 4,
784 message: "Compact ID refers to a missing global ID",
785 })?;
786 Ok(ExGuid {
787 guid: *guid,
788 n: raw & 0xff,
789 })
790 })
791 .collect();
792 }
793 let mut cursor = Cursor { bytes, offset: 0 };
794 let mut result = Vec::new();
795 while !cursor.bytes.is_empty() {
796 result.push(cursor.compact(ids)?);
797 }
798 Ok(result)
799}
800
801fn finite(value: f32) -> Result<f32> {
802 if value.is_finite() {
803 Ok(value)
804 } else {
805 Err(invalid("Non-finite document measurement"))
806 }
807}
808
809fn measurements(bytes: Option<&[u8]>, header: usize) -> Result<Vec<f32>> {
810 let Some(bytes) = bytes else {
811 return Ok(Vec::new());
812 };
813 if bytes.len() < header || bytes.len() != header + usize::from(bytes[0]) * 4 {
814 return Err(invalid("Document measurement array has an invalid length"));
815 }
816 bytes[header..]
817 .chunks_exact(4)
818 .map(|b| finite(f32::from_le_bytes(b.try_into().unwrap()) * 36.0))
819 .collect()
820}
821
822impl<'a> Document<'a> {
823 /// Active (object space, page) identities in section order, excluding history and conflicts.
824 /// Encrypted sections return no visible pages; table-of-contents files return an error.
825 /// The active revision of one object space.
826 pub fn active(&self, space: ExGuid) -> Result<&Revision<'a>> {
827 self.spaces
828 .get(&space)
829 .and_then(Space::active)
830 .ok_or_else(|| invalid("Object space has no active revision"))
831 }
832
833 /// Active page objects declared in one page space, in section order.
834 pub fn pages_in(&self, space: ExGuid) -> Result<Vec<ExGuid>> {
835 Ok(self
836 .pages()?
837 .into_iter()
838 .filter_map(|(sid, page)| (sid == space).then_some(page))
839 .collect())
840 }
841
842 pub fn pages(&self) -> Result<Vec<(ExGuid, ExGuid)>> {
843 let active = |sid| self.active(sid);
844 let root = active(self.root)?;
845 let section = root
846 .roots
847 .get(&1)
848 .and_then(|id| root.nodes.get(id))
849 .ok_or_else(|| invalid("Section root is unavailable"))?;
850 if matches!(section.kind, Kind::Encrypted { .. }) {
851 return Ok(Vec::new());
852 }
853 if !matches!(section.kind, Kind::Section { .. }) {
854 return Err(invalid("Expected a section document"));
855 }
856 let mut pages = Vec::new();
857 let mut seen = BTreeSet::new();
858 for id in &section.children {
859 let series = root
860 .nodes
861 .get(id)
862 .ok_or_else(|| invalid("Page series is unavailable"))?;
863 if !matches!(series.kind, Kind::PageSeries) {
864 return Err(invalid("Section child is not a page series"));
865 }
866 for sid in &series.spaces {
867 let revision = active(*sid)?;
868 let manifest = revision
869 .roots
870 .get(&1)
871 .and_then(|id| revision.nodes.get(id))
872 .ok_or_else(|| invalid("Page manifest is unavailable"))?;
873 if !matches!(manifest.kind, Kind::Manifest { .. }) {
874 return Err(invalid("Page space has no manifest"));
875 }
876 for page in &manifest.content {
877 if !matches!(
878 revision.nodes.get(page).map(|n| &n.kind),
879 Some(Kind::Page { .. })
880 ) || !seen.insert((*sid, *page))
881 {
882 return Err(invalid(
883 "Section page is missing, duplicated or has the wrong type",
884 ));
885 }
886 pages.push((*sid, *page));
887 }
888 }
889 }
890 Ok(pages)
891 }
892
893 /// Rejects checksum damage and follows referenced contexts, retaining encrypted payloads opaquely.
894 pub fn parse(index: &RevisionIndex<'a>) -> Result<Self> {
895 Self::parse_with(
896 index,
897 |space, revision| index.resolve(space, revision),
898 |id| index.store.file_data(id),
899 )
900 }
901
902 pub(crate) fn parse_with(
903 index: &RevisionIndex<'a>,
904 mut resolve: impl FnMut(ExGuid, ExGuid) -> Result<crate::ResolvedRevision<'a>>,
905 mut file_data: impl FnMut([u8; 16]) -> Result<&'a [u8]>,
906 ) -> Result<Self> {
907 if !index.store.checksum_mismatches.is_empty() {
908 return Err(invalid("Document transaction checksum mismatch"));
909 }
910 let mut spaces: BTreeMap<ExGuid, Space<'a>> = BTreeMap::new();
911 let mut pending = vec![(index.root, ExGuid::default())];
912 while let Some((id, context)) = pending.pop() {
913 let space = spaces.entry(id).or_insert_with(|| Space {
914 contexts: BTreeMap::new(),
915 revisions: BTreeMap::new(),
916 });
917 if space.contexts.contains_key(&context) {
918 continue;
919 }
920 let rid = index
921 .spaces
922 .get(&id)
923 .and_then(|s| s.labels.get(&(context, 1)));
924 let rid = match rid {
925 Some(rid) => *rid,
926 // OneNote's copy of a page version names a history it never wrote.
927 None if context == crate::section::HISTORY => continue,
928 None => return Err(invalid("Document context has no revision")),
929 };
930 space.contexts.insert(context, rid);
931 if space.revisions.contains_key(&rid) {
932 continue;
933 }
934 let (revision, referenced) = Revision::parse(
935 id,
936 &resolve(id, rid)?,
937 index.store.header.file_type,
938 &mut file_data,
939 )?;
940 pending.extend(referenced);
941 space.revisions.insert(rid, revision);
942 }
943 for space in spaces.values() {
944 for node in space.revisions.values().flat_map(|r| r.nodes.values()) {
945 let (context, history) = match &node.kind {
946 Kind::Manifest {
947 history: Some(context),
948 } => (*context, true),
949 Kind::VersionProxy { context, .. } => (*context, false),
950 _ => continue,
951 };
952 let Some(rid) = space.contexts.get(&context) else {
953 continue;
954 };
955 let revision = &space.revisions[rid];
956 let root = revision.roots.get(&1).and_then(|id| revision.nodes.get(id));
957 if history {
958 if !matches!(root.map(|n| &n.kind), Some(Kind::VersionHistory)) {
959 return Err(invalid(
960 "History context does not resolve to version history",
961 ));
962 }
963 } else if !matches!(root.map(|n| &n.kind), Some(Kind::Manifest { .. }))
964 || space.contexts.get(&ExGuid::default()) == Some(rid)
965 {
966 return Err(invalid(
967 "Version context does not resolve to a historical page",
968 ));
969 }
970 }
971 }
972 Ok(Self {
973 file_id: index.store.header.file_id,
974 root: index.root,
975 spaces,
976 })
977 }
978}
979
980impl<'a> Element<'a> {
981 pub(crate) fn parse_with(
982 object: &Object<'a>,
983 file_type: crate::FileType,
984 file_data: &mut impl FnMut([u8; 16]) -> Result<&'a [u8]>,
985 ) -> Result<Self> {
986 let empty = |kind| Self {
987 jcid: object.jcid,
988 children: vec![],
989 content: vec![],
990 structure: vec![],
991 spaces: vec![],
992 child_level: None,
993 layout: Layout::default(),
994 format: Format::default(),
995 created: None,
996 modified: None,
997 original_author: None,
998 latest_author: None,
999 media_ids: vec![],
1000 media_time_ms: None,
1001 tags: vec![],
1002 kind,
1003 extra: vec![],
1004 };
1005 if let ObjectData::Encrypted(ciphertext) = object.data {
1006 return Ok(empty(Kind::Encrypted { ciphertext }));
1007 }
1008 if let ObjectData::File { extension, .. } = object.data {
1009 let reference = object
1010 .file_reference()?
1011 .ok_or_else(|| invalid("File object has no reference"))?;
1012 let payload = if let FileDataReference::Internal(guid) = &reference {
1013 Some(file_data(*guid)?)
1014 } else {
1015 None
1016 };
1017 return Ok(empty(Kind::File {
1018 reference,
1019 extension: unicode(extension)?,
1020 payload,
1021 }));
1022 }
1023 let mut f = Fields::new(object)?;
1024 let children = f.refs(0x24001c20, IdStream::Objects)?;
1025 let content = f.refs(0x24001c1f, IdStream::Objects)?;
1026 let structure = f.refs(0x24001d5f, IdStream::Objects)?;
1027 let spaces = f.refs(0x2c001d63, IdStream::ObjectSpaces)?;
1028 let child_level = f.u8(0x0c001c03)?;
1029 if child_level.is_some_and(|level| !(1..=31).contains(&level)) {
1030 return Err(invalid("Child indentation level is out of range"));
1031 }
1032 let layout = Layout {
1033 x: f.float(0x14001c14, 36.0)?,
1034 y: f.float(0x14001c15, 36.0)?,
1035 max_width: f.float(0x14001c1b, 36.0)?,
1036 width_set_by_user: f.boolean(0x08001cbd)?,
1037 max_height: f.float(0x14001c1c, 36.0)?,
1038 reserved_width: f.float(0x14001cdb, 36.0)?,
1039 };
1040 let format = Format {
1041 bold: f.boolean(0x08001c04)?,
1042 italic: f.boolean(0x08001c05)?,
1043 underline: f.boolean(0x08001c06)?,
1044 strike: f.boolean(0x08001c07)?,
1045 superscript: f.boolean(0x08001c08)?,
1046 subscript: f.boolean(0x08001c09)?,
1047 hidden: f.boolean(0x08001e16)?,
1048 hyperlink: f.boolean(0x08001e14)?,
1049 hyperlink_label: f.boolean(0x08001e19)?,
1050 math: f.boolean(0x08003401)?,
1051 embedded_object: f.boolean(0x08001e22)?,
1052 font: f.text(0x1c001c0a)?,
1053 font_size: f.u16(0x10001c0b)?.map(|v| f32::from(v) / 2.0),
1054 color: f.u32(0x14001c0c)?,
1055 highlight: f.u32(0x14001c0d)?,
1056 language: f.u32(0x14001c3b)?,
1057 alignment: f.u8(0x0c003477)?,
1058 rtl: f.boolean(0x08003476)?,
1059 space_before: f.float(0x1400342e, 36.0)?,
1060 space_after: f.float(0x1400342f, 36.0)?,
1061 line_spacing: f.float(0x14003430, 36.0)?,
1062 list_spacing: f.float(0x14001ccb, 36.0)?,
1063 math_object: None,
1064 };
1065 let mut tags = Vec::new();
1066 if let Some(property) = f.take(0x40003489) {
1067 let Value::Sets(range) = property.value else {
1068 return Err(invalid("Note tags require property sets"));
1069 };
1070 if range.len() > 9 {
1071 return Err(invalid("An element has more than nine note tags"));
1072 }
1073 for extra_set in range {
1074 let mut tag = Fields {
1075 sets: vec![std::mem::take(&mut f.sets[extra_set])],
1076 ids: f.ids,
1077 };
1078 tags.push(Tag {
1079 definition: tag.one(0x20003488)?,
1080 action_type: tag.u16(0x10003463)?,
1081 shape: tag.u16(0x10003464)?,
1082 property_status: tag.u32(0x14003467)?,
1083 status: tag
1084 .u16(0x10003470)?
1085 .ok_or_else(|| invalid("Note tag has no status"))?,
1086 created: tag.u32(0x1400346e)?,
1087 completed: tag.u32(0x1400346f)?,
1088 start: tag.u32(0x1400346a)?,
1089 due: tag.u32(0x1400346b)?,
1090 task_id: tag.fixed(0x1c003469)?,
1091 extra_set,
1092 });
1093 f.sets[extra_set] = tag.sets.remove(0);
1094 }
1095 }
1096 let kind = match object.jcid {
1097 0x60007 => {
1098 let templates = f.refs(0x2c001d62, IdStream::ObjectSpaces)?;
1099 if templates.len() > 1 {
1100 return Err(invalid("Section has multiple default page templates"));
1101 }
1102 Kind::Section {
1103 default_template: templates.first().copied(),
1104 }
1105 }
1106 0x60008 => Kind::PageSeries,
1107 0x60037 => {
1108 let contexts = f.refs(0x3400347b, IdStream::Contexts)?;
1109 if contexts.len() > 1 {
1110 return Err(invalid("Page manifest has multiple history contexts"));
1111 }
1112 Kind::Manifest {
1113 history: contexts.first().copied(),
1114 }
1115 }
1116 0x6003c => Kind::VersionHistory,
1117 0x6003d => {
1118 let contexts = f.refs(0x3400347b, IdStream::Contexts)?;
1119 let [context] = contexts.as_slice() else {
1120 return Err(invalid("Version proxy requires one context"));
1121 };
1122 Kind::VersionProxy {
1123 context: *context,
1124 modified_filetime: f.fixed(0x18001d77)?.map(u64::from_le_bytes),
1125 }
1126 }
1127 0x20046 => Kind::VersionHistoryMetadata,
1128 0x2003e => Kind::TemplateMetadata {
1129 name: f.text(0x1c001ca5)?,
1130 },
1131 0x6000b => Kind::Page {
1132 alternate_title: f.text(0x1c001d3c)?,
1133 level: f.u32(0x14001dff)?,
1134 width: f.float(0x14001c01, 36.0)?,
1135 height: f.float(0x14001c02, 36.0)?,
1136 margin_origin_x: f.float(0x14001d0f, 36.0)?,
1137 margin_origin_y: f.float(0x14001d10, 36.0)?,
1138 rtl: f.boolean(0x08001c92)?,
1139 recordings: f.guids(0x1c001ca3)?,
1140 },
1141 0x20030 => Kind::Metadata {
1142 title: f.text(0x1c001cf3)?,
1143 level: f.u32(0x14001dff)?,
1144 },
1145 0x20038 => Kind::ConflictMetadata {
1146 title: f.text(0x1c001cf3)?,
1147 level: f.u32(0x14001dff)?,
1148 author: f.text(0x1c001d9e)?,
1149 },
1150 0x60014 => Kind::Ink {
1151 data: f.one(0x20003415)?,
1152 scale_x: f.float(0x14001c46, 1.0)?,
1153 scale_y: f.float(0x14001c47, 1.0)?,
1154 shape_kind: f.bytes(0x0c001d4f)?.and_then(|b| b.first().copied()),
1155 line: f.bytes(0x1c001dac)?.map(<[u8]>::to_vec),
1156 anchors: f.bytes(0x1c001daa)?.map(<[u8]>::to_vec),
1157 },
1158 0x2003b => Kind::InkData {
1159 strokes: f.refs(0x24003416, IdStream::Objects)?,
1160 bounds: f.fixed::<16>(0x1c003418)?.map(|b| {
1161 std::array::from_fn(|i| {
1162 i32::from_le_bytes(b[i * 4..i * 4 + 4].try_into().unwrap())
1163 })
1164 }),
1165 },
1166 0x20047 => Kind::InkStroke {
1167 path: f.bytes(0x1c00340b)?.unwrap_or_default().to_vec(),
1168 style: f.one(0x20003409)?,
1169 bias: f.u8(0x0c00341c)?,
1170 language: f.u16(0x1000341b)?,
1171 identity: f.fixed(0x1c00341a)?,
1172 index: f.u32(0x14003419)?,
1173 },
1174 0x120048 => Kind::InkStyle {
1175 dimensions: f.bytes(0x1c00340a)?.unwrap_or_default().to_vec(),
1176 width: f.float(0x1400340d, 1.0)?,
1177 height: f.float(0x1400340c, 1.0)?,
1178 color: f.u32(0x1400340f)?,
1179 transparency: f.u8(0x0c003414)?,
1180 pen_tip: f.u8(0x0c003412)?,
1181 raster_operation: f.u8(0x0c003413)?,
1182 antialiased: f.boolean(0x0800340e)?,
1183 fit_to_curve: f.boolean(0x08003410)?,
1184 ignore_pressure: f.boolean(0x08003411)?,
1185 },
1186 0x20031 => Kind::SectionMetadata {
1187 name: f.text(0x1c00349b)?,
1188 color: f.u32(0x14001cbe)?,
1189 },
1190 0x20044 => Kind::RevisionMetadata {
1191 modified_filetime: f.fixed(0x18001d77)?.map(u64::from_le_bytes),
1192 },
1193 0x6002c => Kind::Title,
1194 0x6000c => Kind::Outline {
1195 indents: measurements(f.bytes(0x1c001c12)?, 4)?,
1196 },
1197 0x6000d => Kind::Paragraph {
1198 lists: f.refs(0x24001c26, IdStream::Objects)?,
1199 paragraph_style: f.one(0x2000342c)?,
1200 collapse_state: f.u8(0x0c001c11)?,
1201 },
1202 0x60019 => Kind::OutlineGroup,
1203 0x6000e => {
1204 let utf16 = f.bytes(0x1c001c22)?;
1205 let ascii = f.bytes(0x1c003498)?;
1206 let text = match utf16 {
1207 Some(b) => unicode(b)?,
1208 None => ascii
1209 .unwrap_or_default()
1210 .iter()
1211 .map(|b| char::from(*b))
1212 .collect(),
1213 };
1214 let length = u32::try_from(text.encode_utf16().count())
1215 .map_err(|_| invalid("Text exceeds UTF-16 offset range"))?;
1216 let mut bounds = vec![0];
1217 if let Some(bytes) = f.bytes(0x1c001e12)? {
1218 if !bytes.len().is_multiple_of(4) {
1219 return Err(invalid("Text-run index has an invalid length"));
1220 }
1221 bounds.extend(
1222 bytes
1223 .chunks_exact(4)
1224 .map(|b| u32::from_le_bytes(b.try_into().unwrap())),
1225 );
1226 }
1227 bounds.push(length);
1228 if bounds.windows(2).any(|b| b[0] > b[1]) {
1229 return Err(invalid("Text-run indices exceed text or are out of order"));
1230 }
1231 let mut boundaries = bounds.iter().copied().peekable();
1232 let mut position = 0;
1233 for character in text.chars() {
1234 while boundaries.peek() == Some(&position) {
1235 boundaries.next();
1236 }
1237 position += character.len_utf16() as u32;
1238 if boundaries.peek().is_some_and(|next| *next < position) {
1239 return Err(invalid("Text-run boundary splits a surrogate pair"));
1240 }
1241 }
1242 let formats = f.refs(0x24001e13, IdStream::Objects)?;
1243 if !formats.is_empty() && formats.len() != bounds.len() - 1 {
1244 return Err(invalid("Text-run indices and formats disagree"));
1245 }
1246 let data = f
1247 .take(0x40003499)
1248 .map(|property| {
1249 let Value::Sets(range) = property.value else {
1250 return Err(invalid("Text-run data requires property sets"));
1251 };
1252 if range.len() != bounds.len() - 1 {
1253 return Err(invalid("Text-run indices and data disagree"));
1254 }
1255 Ok(range)
1256 })
1257 .transpose()?;
1258 let runs = bounds
1259 .windows(2)
1260 .enumerate()
1261 .map(|(i, b)| TextRun {
1262 start: b[0],
1263 end: b[1],
1264 format: formats.get(i).copied(),
1265 extra_set: data.as_ref().map(|range| range.start + i),
1266 })
1267 .collect();
1268 let boilerplate = f.boolean(0x08001c87)?.unwrap_or(false)
1269 | f.boolean(0x08001c88)?.unwrap_or(false)
1270 | f.boolean(0x08001cb5)?.unwrap_or(false);
1271 Kind::RichText {
1272 text,
1273 runs,
1274 paragraph_style: f.one(0x2000342c)?,
1275 boilerplate,
1276 }
1277 }
1278 0x12004d => Kind::Style {
1279 name: f.text(0x1c00345a)?,
1280 next: f.text(0x1c00348a)?,
1281 },
1282 0x60012 => {
1283 let format = f
1284 .bytes(0x1c001c1a)?
1285 .map(|bytes| {
1286 if bytes.len() < 2
1287 || bytes.len()
1288 != 2 + usize::from(u16::from_le_bytes(
1289 bytes[..2].try_into().unwrap(),
1290 )) * 2
1291 {
1292 return Err(invalid("List format has an invalid length"));
1293 }
1294 let units = bytes[2..]
1295 .chunks_exact(2)
1296 .map(|b| u16::from_le_bytes(b.try_into().unwrap()))
1297 .collect::<Vec<_>>();
1298 String::from_utf16(&units)
1299 .map_err(|_| invalid("Invalid UTF-16 list format"))
1300 })
1301 .transpose()?;
1302 Kind::List {
1303 font: f.text(0x1c001c52)?,
1304 format,
1305 restart: f.u32(0x14001cb7)?,
1306 bullet: f.u16(0x10001d0e)?,
1307 }
1308 }
1309 0x60022 => {
1310 let columns = f.u32(0x14001d58)?;
1311 let widths = measurements(f.bytes(0x1c001d66)?, 1)?;
1312 let mut locked = Vec::new();
1313 if let Some(bytes) = f.bytes(0x1c001d7d)? {
1314 let count = usize::from(
1315 *bytes
1316 .first()
1317 .ok_or_else(|| invalid("Empty table column locks"))?,
1318 );
1319 if bytes.len() != 1 + count.div_ceil(8) || columns != Some(count as u32) {
1320 return Err(invalid("Table column locks disagree with column count"));
1321 }
1322 locked.extend((0..count).map(|i| bytes[1 + i / 8] & (1 << (i % 8)) != 0));
1323 }
1324 if !widths.is_empty() && columns != Some(widths.len() as u32) {
1325 return Err(invalid("Table widths disagree with column count"));
1326 }
1327 Kind::Table {
1328 rows: f.u32(0x14001d57)?,
1329 columns,
1330 widths,
1331 locked,
1332 borders: f.boolean(0x08001d5e)?,
1333 }
1334 }
1335 0x60023 => Kind::Row,
1336 0x60024 => Kind::Cell {
1337 shading: f.u32(0x14001e26)?,
1338 indents: measurements(f.bytes(0x1c001c12)?, 4)?,
1339 },
1340 0x60011 => Kind::Image {
1341 container: f.one(0x20001c3f)?,
1342 filename: f.text(0x1c001dd7)?,
1343 alt: f.text(0x1c001e58)?,
1344 picture_width: f.float(0x140034cd, 36.0)?,
1345 picture_height: f.float(0x140034ce, 36.0)?,
1346 background: f.boolean(0x08001d13)?,
1347 printout: f.boolean(0x08001d85)?,
1348 link: f.text(0x1c001e20)?,
1349 },
1350 0x60035 => Kind::Attachment {
1351 container: f.one(0x20001d9b)?,
1352 preview: f.one(0x20001c3f)?,
1353 filename: f.text(0x1c001d9c)?,
1354 source_path: f.text(0x1c001d9d)?,
1355 recording_id: f.fixed(0x1c001c97)?,
1356 recording_type: f.u32(0x14001d24)?,
1357 recording_duration: f.u32(0x14001cfd)?,
1358 icon_width: f.float(0x140034cd, 36.0)?,
1359 icon_height: f.float(0x140034ce, 36.0)?,
1360 },
1361 0x120001 => Kind::Author {
1362 name: f.text(0x1c001d75)?,
1363 },
1364 0x120043 => Kind::TagDefinition {
1365 label: f.text(0x1c003468)?,
1366 action_type: f.u16(0x10003463)?,
1367 shape: f.u16(0x10003464)?,
1368 color: f.u32(0x14003466)?,
1369 highlight: f.u32(0x14003465)?,
1370 },
1371 0x20001 if file_type == crate::FileType::TableOfContents => Kind::Toc {
1372 entries: f.refs(0x24001cf6, IdStream::Objects)?,
1373 filename: f.text(0x1c001d6b)?,
1374 identity: f.fixed(0x1c001d94)?,
1375 order: f.u32(0x14001cb9)?,
1376 color: f.u32(0x14001cbe)?,
1377 },
1378 _ => Kind::Unknown,
1379 };
1380 let media_ids = f.guids(0x1c001c98)?;
1381 Ok(Self {
1382 jcid: object.jcid,
1383 children,
1384 content,
1385 structure,
1386 spaces,
1387 child_level,
1388 layout,
1389 format,
1390 created: f.u32(0x14001d09)?,
1391 modified: f.u32(0x14001d7a)?,
1392 original_author: f.one(0x20001d78)?,
1393 latest_author: f.one(0x20001d79)?,
1394 media_ids,
1395 media_time_ms: f.u32(0x14001c99)?,
1396 tags,
1397 kind,
1398 extra: f.remainder()?,
1399 })
1400 }
1401}