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 .as_chunks::<2>()
633 .0
634 .iter()
635 .map(|x| u16::from_le_bytes(*x))
636 .collect();
637 String::from_utf16(units.strip_suffix(&[0]).unwrap_or(&units))
638 .map_err(|_| invalid("Invalid UTF-16 document string"))
639}
640
641struct Fields<'a, 'o> {
642 sets: Vec<Vec<Property<'a>>>,
643 ids: &'o BTreeMap<u32, [u8; 16]>,
644}
645
646impl<'a, 'o> Fields<'a, 'o> {
647 fn new(object: &'o Object<'a>) -> Result<Self> {
648 let ObjectData::Properties(bytes) = object.data else {
649 return Err(invalid("Expected document properties"));
650 };
651 let sets = PropertySets::parse(bytes)?.sets;
652 for set in &sets {
653 let mut seen = BTreeSet::new();
654 if set.iter().any(|p| !seen.insert(p.id & 0x7fffffff)) {
655 return Err(invalid("Repeated document property"));
656 }
657 }
658 Ok(Self {
659 sets,
660 ids: &object.global_ids,
661 })
662 }
663
664 fn take(&mut self, id: u32) -> Option<Property<'a>> {
665 let index = self.sets[0].iter().position(|p| p.id & 0x7fffffff == id)?;
666 Some(self.sets[0].remove(index))
667 }
668
669 fn bytes(&mut self, id: u32) -> Result<Option<&'a [u8]>> {
670 self.take(id)
671 .map(|p| match p.value {
672 Value::Bytes(bytes) => Ok(bytes),
673 _ => Err(invalid("Document property requires bytes")),
674 })
675 .transpose()
676 }
677
678 fn fixed<const N: usize>(&mut self, id: u32) -> Result<Option<[u8; N]>> {
679 self.bytes(id)?
680 .map(|b| {
681 b.try_into()
682 .map_err(|_| invalid("Document scalar has an invalid length"))
683 })
684 .transpose()
685 }
686
687 fn u32(&mut self, id: u32) -> Result<Option<u32>> {
688 Ok(self.fixed(id)?.map(u32::from_le_bytes))
689 }
690 fn guids(&mut self, id: u32) -> Result<Vec<[u8; 16]>> {
691 let bytes = self.bytes(id)?.unwrap_or_default();
692 if !bytes.len().is_multiple_of(16) {
693 return Err(invalid("Media identifiers have an invalid length"));
694 }
695 Ok(bytes.as_chunks::<16>().0.to_vec())
696 }
697 fn u16(&mut self, id: u32) -> Result<Option<u16>> {
698 Ok(self.fixed(id)?.map(u16::from_le_bytes))
699 }
700 fn u8(&mut self, id: u32) -> Result<Option<u8>> {
701 Ok(self.fixed::<1>(id)?.map(|b| b[0]))
702 }
703 fn float(&mut self, id: u32, scale: f32) -> Result<Option<f32>> {
704 self.fixed(id)?
705 .map(|b| finite(f32::from_le_bytes(b) * scale))
706 .transpose()
707 }
708 fn text(&mut self, id: u32) -> Result<Option<String>> {
709 self.bytes(id)?.map(unicode).transpose()
710 }
711 fn boolean(&mut self, id: u32) -> Result<Option<bool>> {
712 self.take(id)
713 .map(|p| match p.value {
714 Value::NoData => Ok(p.id & 0x80000000 != 0),
715 _ => Err(invalid("Document flag requires a Boolean")),
716 })
717 .transpose()
718 }
719 fn refs(&mut self, id: u32, expected: IdStream) -> Result<Vec<ExGuid>> {
720 self.take(id)
721 .map(|p| match p.value {
722 Value::References {
723 stream,
724 compact_ids,
725 } if stream == expected => references(compact_ids, self.ids),
726 _ => Err(invalid("Document reference uses the wrong stream")),
727 })
728 .transpose()
729 .map(Option::unwrap_or_default)
730 }
731 fn one(&mut self, id: u32) -> Result<Option<ExGuid>> {
732 let refs = self.refs(id, IdStream::Objects)?;
733 if refs.len() > 1 {
734 return Err(invalid("Document scalar has multiple references"));
735 }
736 Ok(refs.first().copied())
737 }
738 fn remainder(self) -> Result<Vec<Vec<Field<'a>>>> {
739 self.sets
740 .into_iter()
741 .map(|set| {
742 set.into_iter()
743 .map(|p| {
744 let value = match p.value {
745 Value::NoData => FieldValue::NoData,
746 Value::Bytes(b) => FieldValue::Bytes(b),
747 Value::Sets(r) => FieldValue::Sets(r),
748 Value::References {
749 stream,
750 compact_ids,
751 } => {
752 let refs = references(compact_ids, self.ids)?;
753 match stream {
754 IdStream::Objects => FieldValue::Objects(refs),
755 IdStream::ObjectSpaces => FieldValue::Spaces(refs),
756 IdStream::Contexts => FieldValue::Contexts(refs),
757 }
758 }
759 };
760 Ok(Field { id: p.id, value })
761 })
762 .collect()
763 })
764 .collect()
765 }
766}
767
768fn references(bytes: &[u8], ids: &BTreeMap<u32, [u8; 16]>) -> Result<Vec<ExGuid>> {
769 // A long list, such as a large outline's children, reads a dense table as an array.
770 if bytes.len() >= 256
771 && bytes.len().is_multiple_of(4)
772 && let Some((last, _)) = ids.last_key_value()
773 && *last as usize + 1 == ids.len()
774 {
775 let table: Vec<[u8; 16]> = ids.values().copied().collect();
776 return bytes
777 .as_chunks::<4>()
778 .0
779 .iter()
780 .enumerate()
781 .map(|(i, bytes)| {
782 let raw = u32::from_le_bytes(*bytes);
783 let guid = table.get((raw >> 8) as usize).ok_or(Error {
784 offset: i * 4,
785 message: "Compact ID refers to a missing global ID",
786 })?;
787 Ok(ExGuid {
788 guid: *guid,
789 n: raw & 0xff,
790 })
791 })
792 .collect();
793 }
794 let mut cursor = Cursor { bytes, offset: 0 };
795 let mut result = Vec::new();
796 while !cursor.bytes.is_empty() {
797 result.push(cursor.compact(ids)?);
798 }
799 Ok(result)
800}
801
802fn finite(value: f32) -> Result<f32> {
803 if value.is_finite() {
804 Ok(value)
805 } else {
806 Err(invalid("Non-finite document measurement"))
807 }
808}
809
810fn measurements(bytes: Option<&[u8]>, header: usize) -> Result<Vec<f32>> {
811 let Some(bytes) = bytes else {
812 return Ok(Vec::new());
813 };
814 if bytes.len() < header || bytes.len() != header + usize::from(bytes[0]) * 4 {
815 return Err(invalid("Document measurement array has an invalid length"));
816 }
817 bytes[header..]
818 .as_chunks::<4>()
819 .0
820 .iter()
821 .map(|b| finite(f32::from_le_bytes(*b) * 36.0))
822 .collect()
823}
824
825impl<'a> Document<'a> {
826 /// Active (object space, page) identities in section order, excluding history and conflicts.
827 /// Encrypted sections return no visible pages; table-of-contents files return an error.
828 /// The active revision of one object space.
829 pub fn active(&self, space: ExGuid) -> Result<&Revision<'a>> {
830 self.spaces
831 .get(&space)
832 .and_then(Space::active)
833 .ok_or_else(|| invalid("Object space has no active revision"))
834 }
835
836 /// Active page objects declared in one page space, in section order.
837 pub fn pages_in(&self, space: ExGuid) -> Result<Vec<ExGuid>> {
838 Ok(self
839 .pages()?
840 .into_iter()
841 .filter_map(|(sid, page)| (sid == space).then_some(page))
842 .collect())
843 }
844
845 pub fn pages(&self) -> Result<Vec<(ExGuid, ExGuid)>> {
846 let active = |sid| self.active(sid);
847 let root = active(self.root)?;
848 let section = root
849 .roots
850 .get(&1)
851 .and_then(|id| root.nodes.get(id))
852 .ok_or_else(|| invalid("Section root is unavailable"))?;
853 if matches!(section.kind, Kind::Encrypted { .. }) {
854 return Ok(Vec::new());
855 }
856 if !matches!(section.kind, Kind::Section { .. }) {
857 return Err(invalid("Expected a section document"));
858 }
859 let mut pages = Vec::new();
860 let mut seen = BTreeSet::new();
861 for id in &section.children {
862 let series = root
863 .nodes
864 .get(id)
865 .ok_or_else(|| invalid("Page series is unavailable"))?;
866 if !matches!(series.kind, Kind::PageSeries) {
867 return Err(invalid("Section child is not a page series"));
868 }
869 for sid in &series.spaces {
870 let revision = active(*sid)?;
871 let manifest = revision
872 .roots
873 .get(&1)
874 .and_then(|id| revision.nodes.get(id))
875 .ok_or_else(|| invalid("Page manifest is unavailable"))?;
876 if !matches!(manifest.kind, Kind::Manifest { .. }) {
877 return Err(invalid("Page space has no manifest"));
878 }
879 for page in &manifest.content {
880 if !matches!(
881 revision.nodes.get(page).map(|n| &n.kind),
882 Some(Kind::Page { .. })
883 ) || !seen.insert((*sid, *page))
884 {
885 return Err(invalid(
886 "Section page is missing, duplicated or has the wrong type",
887 ));
888 }
889 pages.push((*sid, *page));
890 }
891 }
892 }
893 Ok(pages)
894 }
895
896 /// Rejects checksum damage and follows referenced contexts, retaining encrypted payloads opaquely.
897 pub fn parse(index: &RevisionIndex<'a>) -> Result<Self> {
898 Self::parse_with(
899 index,
900 |space, revision| index.resolve(space, revision),
901 |id| index.store.file_data(id),
902 )
903 }
904
905 pub(crate) fn parse_with(
906 index: &RevisionIndex<'a>,
907 mut resolve: impl FnMut(ExGuid, ExGuid) -> Result<crate::ResolvedRevision<'a>>,
908 mut file_data: impl FnMut([u8; 16]) -> Result<&'a [u8]>,
909 ) -> Result<Self> {
910 if !index.store.checksum_mismatches.is_empty() {
911 return Err(invalid("Document transaction checksum mismatch"));
912 }
913 let mut spaces: BTreeMap<ExGuid, Space<'a>> = BTreeMap::new();
914 let mut pending = vec![(index.root, ExGuid::default())];
915 while let Some((id, context)) = pending.pop() {
916 let space = spaces.entry(id).or_insert_with(|| Space {
917 contexts: BTreeMap::new(),
918 revisions: BTreeMap::new(),
919 });
920 if space.contexts.contains_key(&context) {
921 continue;
922 }
923 let rid = index
924 .spaces
925 .get(&id)
926 .and_then(|s| s.labels.get(&(context, 1)));
927 let rid = match rid {
928 Some(rid) => *rid,
929 // OneNote's copy of a page version names a history it never wrote.
930 None if context == crate::section::HISTORY => continue,
931 None => return Err(invalid("Document context has no revision")),
932 };
933 space.contexts.insert(context, rid);
934 if space.revisions.contains_key(&rid) {
935 continue;
936 }
937 let (revision, referenced) = Revision::parse(
938 id,
939 &resolve(id, rid)?,
940 index.store.header.file_type,
941 &mut file_data,
942 )?;
943 pending.extend(referenced);
944 space.revisions.insert(rid, revision);
945 }
946 for space in spaces.values() {
947 for node in space.revisions.values().flat_map(|r| r.nodes.values()) {
948 let (context, history) = match &node.kind {
949 Kind::Manifest {
950 history: Some(context),
951 } => (*context, true),
952 Kind::VersionProxy { context, .. } => (*context, false),
953 _ => continue,
954 };
955 let Some(rid) = space.contexts.get(&context) else {
956 continue;
957 };
958 let revision = &space.revisions[rid];
959 let root = revision.roots.get(&1).and_then(|id| revision.nodes.get(id));
960 if history {
961 if !matches!(root.map(|n| &n.kind), Some(Kind::VersionHistory)) {
962 return Err(invalid(
963 "History context does not resolve to version history",
964 ));
965 }
966 } else if !matches!(root.map(|n| &n.kind), Some(Kind::Manifest { .. }))
967 || space.contexts.get(&ExGuid::default()) == Some(rid)
968 {
969 return Err(invalid(
970 "Version context does not resolve to a historical page",
971 ));
972 }
973 }
974 }
975 Ok(Self {
976 file_id: index.store.header.file_id,
977 root: index.root,
978 spaces,
979 })
980 }
981}
982
983impl<'a> Element<'a> {
984 pub(crate) fn parse_with(
985 object: &Object<'a>,
986 file_type: crate::FileType,
987 file_data: &mut impl FnMut([u8; 16]) -> Result<&'a [u8]>,
988 ) -> Result<Self> {
989 let empty = |kind| Self {
990 jcid: object.jcid,
991 children: vec![],
992 content: vec![],
993 structure: vec![],
994 spaces: vec![],
995 child_level: None,
996 layout: Layout::default(),
997 format: Format::default(),
998 created: None,
999 modified: None,
1000 original_author: None,
1001 latest_author: None,
1002 media_ids: vec![],
1003 media_time_ms: None,
1004 tags: vec![],
1005 kind,
1006 extra: vec![],
1007 };
1008 if let ObjectData::Encrypted(ciphertext) = object.data {
1009 return Ok(empty(Kind::Encrypted { ciphertext }));
1010 }
1011 if let ObjectData::File { extension, .. } = object.data {
1012 let reference = object
1013 .file_reference()?
1014 .ok_or_else(|| invalid("File object has no reference"))?;
1015 let payload = if let FileDataReference::Internal(guid) = &reference {
1016 Some(file_data(*guid)?)
1017 } else {
1018 None
1019 };
1020 return Ok(empty(Kind::File {
1021 reference,
1022 extension: unicode(extension)?,
1023 payload,
1024 }));
1025 }
1026 let mut f = Fields::new(object)?;
1027 let children = f.refs(0x24001c20, IdStream::Objects)?;
1028 let content = f.refs(0x24001c1f, IdStream::Objects)?;
1029 let structure = f.refs(0x24001d5f, IdStream::Objects)?;
1030 let spaces = f.refs(0x2c001d63, IdStream::ObjectSpaces)?;
1031 let child_level = f.u8(0x0c001c03)?;
1032 if child_level.is_some_and(|level| !(1..=31).contains(&level)) {
1033 return Err(invalid("Child indentation level is out of range"));
1034 }
1035 let layout = Layout {
1036 x: f.float(0x14001c14, 36.0)?,
1037 y: f.float(0x14001c15, 36.0)?,
1038 max_width: f.float(0x14001c1b, 36.0)?,
1039 width_set_by_user: f.boolean(0x08001cbd)?,
1040 max_height: f.float(0x14001c1c, 36.0)?,
1041 reserved_width: f.float(0x14001cdb, 36.0)?,
1042 };
1043 let format = Format {
1044 bold: f.boolean(0x08001c04)?,
1045 italic: f.boolean(0x08001c05)?,
1046 underline: f.boolean(0x08001c06)?,
1047 strike: f.boolean(0x08001c07)?,
1048 superscript: f.boolean(0x08001c08)?,
1049 subscript: f.boolean(0x08001c09)?,
1050 hidden: f.boolean(0x08001e16)?,
1051 hyperlink: f.boolean(0x08001e14)?,
1052 hyperlink_label: f.boolean(0x08001e19)?,
1053 math: f.boolean(0x08003401)?,
1054 embedded_object: f.boolean(0x08001e22)?,
1055 font: f.text(0x1c001c0a)?,
1056 font_size: f.u16(0x10001c0b)?.map(|v| f32::from(v) / 2.0),
1057 color: f.u32(0x14001c0c)?,
1058 highlight: f.u32(0x14001c0d)?,
1059 language: f.u32(0x14001c3b)?,
1060 alignment: f.u8(0x0c003477)?,
1061 rtl: f.boolean(0x08003476)?,
1062 space_before: f.float(0x1400342e, 36.0)?,
1063 space_after: f.float(0x1400342f, 36.0)?,
1064 line_spacing: f.float(0x14003430, 36.0)?,
1065 list_spacing: f.float(0x14001ccb, 36.0)?,
1066 math_object: None,
1067 };
1068 let mut tags = Vec::new();
1069 if let Some(property) = f.take(0x40003489) {
1070 let Value::Sets(range) = property.value else {
1071 return Err(invalid("Note tags require property sets"));
1072 };
1073 if range.len() > 9 {
1074 return Err(invalid("An element has more than nine note tags"));
1075 }
1076 for extra_set in range {
1077 let mut tag = Fields {
1078 sets: vec![std::mem::take(&mut f.sets[extra_set])],
1079 ids: f.ids,
1080 };
1081 tags.push(Tag {
1082 definition: tag.one(0x20003488)?,
1083 action_type: tag.u16(0x10003463)?,
1084 shape: tag.u16(0x10003464)?,
1085 property_status: tag.u32(0x14003467)?,
1086 status: tag
1087 .u16(0x10003470)?
1088 .ok_or_else(|| invalid("Note tag has no status"))?,
1089 created: tag.u32(0x1400346e)?,
1090 completed: tag.u32(0x1400346f)?,
1091 start: tag.u32(0x1400346a)?,
1092 due: tag.u32(0x1400346b)?,
1093 task_id: tag.fixed(0x1c003469)?,
1094 extra_set,
1095 });
1096 f.sets[extra_set] = tag.sets.remove(0);
1097 }
1098 }
1099 let kind = match object.jcid {
1100 0x60007 => {
1101 let templates = f.refs(0x2c001d62, IdStream::ObjectSpaces)?;
1102 if templates.len() > 1 {
1103 return Err(invalid("Section has multiple default page templates"));
1104 }
1105 Kind::Section {
1106 default_template: templates.first().copied(),
1107 }
1108 }
1109 0x60008 => Kind::PageSeries,
1110 0x60037 => {
1111 let contexts = f.refs(0x3400347b, IdStream::Contexts)?;
1112 if contexts.len() > 1 {
1113 return Err(invalid("Page manifest has multiple history contexts"));
1114 }
1115 Kind::Manifest {
1116 history: contexts.first().copied(),
1117 }
1118 }
1119 0x6003c => Kind::VersionHistory,
1120 0x6003d => {
1121 let contexts = f.refs(0x3400347b, IdStream::Contexts)?;
1122 let [context] = contexts.as_slice() else {
1123 return Err(invalid("Version proxy requires one context"));
1124 };
1125 Kind::VersionProxy {
1126 context: *context,
1127 modified_filetime: f.fixed(0x18001d77)?.map(u64::from_le_bytes),
1128 }
1129 }
1130 0x20046 => Kind::VersionHistoryMetadata,
1131 0x2003e => Kind::TemplateMetadata {
1132 name: f.text(0x1c001ca5)?,
1133 },
1134 0x6000b => Kind::Page {
1135 alternate_title: f.text(0x1c001d3c)?,
1136 level: f.u32(0x14001dff)?,
1137 width: f.float(0x14001c01, 36.0)?,
1138 height: f.float(0x14001c02, 36.0)?,
1139 margin_origin_x: f.float(0x14001d0f, 36.0)?,
1140 margin_origin_y: f.float(0x14001d10, 36.0)?,
1141 rtl: f.boolean(0x08001c92)?,
1142 recordings: f.guids(0x1c001ca3)?,
1143 },
1144 0x20030 => Kind::Metadata {
1145 title: f.text(0x1c001cf3)?,
1146 level: f.u32(0x14001dff)?,
1147 },
1148 0x20038 => Kind::ConflictMetadata {
1149 title: f.text(0x1c001cf3)?,
1150 level: f.u32(0x14001dff)?,
1151 author: f.text(0x1c001d9e)?,
1152 },
1153 0x60014 => Kind::Ink {
1154 data: f.one(0x20003415)?,
1155 scale_x: f.float(0x14001c46, 1.0)?,
1156 scale_y: f.float(0x14001c47, 1.0)?,
1157 shape_kind: f.bytes(0x0c001d4f)?.and_then(|b| b.first().copied()),
1158 line: f.bytes(0x1c001dac)?.map(<[u8]>::to_vec),
1159 anchors: f.bytes(0x1c001daa)?.map(<[u8]>::to_vec),
1160 },
1161 0x2003b => Kind::InkData {
1162 strokes: f.refs(0x24003416, IdStream::Objects)?,
1163 bounds: f.fixed::<16>(0x1c003418)?.map(|b| {
1164 std::array::from_fn(|i| {
1165 i32::from_le_bytes(b[i * 4..i * 4 + 4].try_into().unwrap())
1166 })
1167 }),
1168 },
1169 0x20047 => Kind::InkStroke {
1170 path: f.bytes(0x1c00340b)?.unwrap_or_default().to_vec(),
1171 style: f.one(0x20003409)?,
1172 bias: f.u8(0x0c00341c)?,
1173 language: f.u16(0x1000341b)?,
1174 identity: f.fixed(0x1c00341a)?,
1175 index: f.u32(0x14003419)?,
1176 },
1177 0x120048 => Kind::InkStyle {
1178 dimensions: f.bytes(0x1c00340a)?.unwrap_or_default().to_vec(),
1179 width: f.float(0x1400340d, 1.0)?,
1180 height: f.float(0x1400340c, 1.0)?,
1181 color: f.u32(0x1400340f)?,
1182 transparency: f.u8(0x0c003414)?,
1183 pen_tip: f.u8(0x0c003412)?,
1184 raster_operation: f.u8(0x0c003413)?,
1185 antialiased: f.boolean(0x0800340e)?,
1186 fit_to_curve: f.boolean(0x08003410)?,
1187 ignore_pressure: f.boolean(0x08003411)?,
1188 },
1189 0x20031 => Kind::SectionMetadata {
1190 name: f.text(0x1c00349b)?,
1191 color: f.u32(0x14001cbe)?,
1192 },
1193 0x20044 => Kind::RevisionMetadata {
1194 modified_filetime: f.fixed(0x18001d77)?.map(u64::from_le_bytes),
1195 },
1196 0x6002c => Kind::Title,
1197 0x6000c => Kind::Outline {
1198 indents: measurements(f.bytes(0x1c001c12)?, 4)?,
1199 },
1200 0x6000d => Kind::Paragraph {
1201 lists: f.refs(0x24001c26, IdStream::Objects)?,
1202 paragraph_style: f.one(0x2000342c)?,
1203 collapse_state: f.u8(0x0c001c11)?,
1204 },
1205 0x60019 => Kind::OutlineGroup,
1206 0x6000e => {
1207 let utf16 = f.bytes(0x1c001c22)?;
1208 let ascii = f.bytes(0x1c003498)?;
1209 let text = match utf16 {
1210 Some(b) => unicode(b)?,
1211 None => ascii
1212 .unwrap_or_default()
1213 .iter()
1214 .map(|b| char::from(*b))
1215 .collect(),
1216 };
1217 let length = u32::try_from(text.encode_utf16().count())
1218 .map_err(|_| invalid("Text exceeds UTF-16 offset range"))?;
1219 let mut bounds = vec![0];
1220 if let Some(bytes) = f.bytes(0x1c001e12)? {
1221 if !bytes.len().is_multiple_of(4) {
1222 return Err(invalid("Text-run index has an invalid length"));
1223 }
1224 bounds.extend(
1225 bytes
1226 .as_chunks::<4>()
1227 .0
1228 .iter()
1229 .map(|b| u32::from_le_bytes(*b)),
1230 );
1231 }
1232 bounds.push(length);
1233 if bounds.windows(2).any(|b| b[0] > b[1]) {
1234 return Err(invalid("Text-run indices exceed text or are out of order"));
1235 }
1236 let mut boundaries = bounds.iter().copied().peekable();
1237 let mut position = 0;
1238 for character in text.chars() {
1239 while boundaries.peek() == Some(&position) {
1240 boundaries.next();
1241 }
1242 position += character.len_utf16() as u32;
1243 if boundaries.peek().is_some_and(|next| *next < position) {
1244 return Err(invalid("Text-run boundary splits a surrogate pair"));
1245 }
1246 }
1247 let formats = f.refs(0x24001e13, IdStream::Objects)?;
1248 if !formats.is_empty() && formats.len() != bounds.len() - 1 {
1249 return Err(invalid("Text-run indices and formats disagree"));
1250 }
1251 let data = f
1252 .take(0x40003499)
1253 .map(|property| {
1254 let Value::Sets(range) = property.value else {
1255 return Err(invalid("Text-run data requires property sets"));
1256 };
1257 if range.len() != bounds.len() - 1 {
1258 return Err(invalid("Text-run indices and data disagree"));
1259 }
1260 Ok(range)
1261 })
1262 .transpose()?;
1263 let runs = bounds
1264 .windows(2)
1265 .enumerate()
1266 .map(|(i, b)| TextRun {
1267 start: b[0],
1268 end: b[1],
1269 format: formats.get(i).copied(),
1270 extra_set: data.as_ref().map(|range| range.start + i),
1271 })
1272 .collect();
1273 let boilerplate = f.boolean(0x08001c87)?.unwrap_or(false)
1274 | f.boolean(0x08001c88)?.unwrap_or(false)
1275 | f.boolean(0x08001cb5)?.unwrap_or(false);
1276 Kind::RichText {
1277 text,
1278 runs,
1279 paragraph_style: f.one(0x2000342c)?,
1280 boilerplate,
1281 }
1282 }
1283 0x12004d => Kind::Style {
1284 name: f.text(0x1c00345a)?,
1285 next: f.text(0x1c00348a)?,
1286 },
1287 0x60012 => {
1288 let format = f
1289 .bytes(0x1c001c1a)?
1290 .map(|bytes| {
1291 if bytes.len() < 2
1292 || bytes.len()
1293 != 2 + usize::from(u16::from_le_bytes(
1294 bytes[..2].try_into().unwrap(),
1295 )) * 2
1296 {
1297 return Err(invalid("List format has an invalid length"));
1298 }
1299 let units = bytes[2..]
1300 .as_chunks::<2>()
1301 .0
1302 .iter()
1303 .map(|b| u16::from_le_bytes(*b))
1304 .collect::<Vec<_>>();
1305 String::from_utf16(&units)
1306 .map_err(|_| invalid("Invalid UTF-16 list format"))
1307 })
1308 .transpose()?;
1309 Kind::List {
1310 font: f.text(0x1c001c52)?,
1311 format,
1312 restart: f.u32(0x14001cb7)?,
1313 bullet: f.u16(0x10001d0e)?,
1314 }
1315 }
1316 0x60022 => {
1317 let columns = f.u32(0x14001d58)?;
1318 let widths = measurements(f.bytes(0x1c001d66)?, 1)?;
1319 let mut locked = Vec::new();
1320 if let Some(bytes) = f.bytes(0x1c001d7d)? {
1321 let count = usize::from(
1322 *bytes
1323 .first()
1324 .ok_or_else(|| invalid("Empty table column locks"))?,
1325 );
1326 if bytes.len() != 1 + count.div_ceil(8) || columns != Some(count as u32) {
1327 return Err(invalid("Table column locks disagree with column count"));
1328 }
1329 locked.extend((0..count).map(|i| bytes[1 + i / 8] & (1 << (i % 8)) != 0));
1330 }
1331 if !widths.is_empty() && columns != Some(widths.len() as u32) {
1332 return Err(invalid("Table widths disagree with column count"));
1333 }
1334 Kind::Table {
1335 rows: f.u32(0x14001d57)?,
1336 columns,
1337 widths,
1338 locked,
1339 borders: f.boolean(0x08001d5e)?,
1340 }
1341 }
1342 0x60023 => Kind::Row,
1343 0x60024 => Kind::Cell {
1344 shading: f.u32(0x14001e26)?,
1345 indents: measurements(f.bytes(0x1c001c12)?, 4)?,
1346 },
1347 0x60011 => Kind::Image {
1348 container: f.one(0x20001c3f)?,
1349 filename: f.text(0x1c001dd7)?,
1350 alt: f.text(0x1c001e58)?,
1351 picture_width: f.float(0x140034cd, 36.0)?,
1352 picture_height: f.float(0x140034ce, 36.0)?,
1353 background: f.boolean(0x08001d13)?,
1354 printout: f.boolean(0x08001d85)?,
1355 link: f.text(0x1c001e20)?,
1356 },
1357 0x60035 => Kind::Attachment {
1358 container: f.one(0x20001d9b)?,
1359 preview: f.one(0x20001c3f)?,
1360 filename: f.text(0x1c001d9c)?,
1361 source_path: f.text(0x1c001d9d)?,
1362 recording_id: f.fixed(0x1c001c97)?,
1363 recording_type: f.u32(0x14001d24)?,
1364 recording_duration: f.u32(0x14001cfd)?,
1365 icon_width: f.float(0x140034cd, 36.0)?,
1366 icon_height: f.float(0x140034ce, 36.0)?,
1367 },
1368 0x120001 => Kind::Author {
1369 name: f.text(0x1c001d75)?,
1370 },
1371 0x120043 => Kind::TagDefinition {
1372 label: f.text(0x1c003468)?,
1373 action_type: f.u16(0x10003463)?,
1374 shape: f.u16(0x10003464)?,
1375 color: f.u32(0x14003466)?,
1376 highlight: f.u32(0x14003465)?,
1377 },
1378 0x20001 if file_type == crate::FileType::TableOfContents => Kind::Toc {
1379 entries: f.refs(0x24001cf6, IdStream::Objects)?,
1380 filename: f.text(0x1c001d6b)?,
1381 identity: f.fixed(0x1c001d94)?,
1382 order: f.u32(0x14001cb9)?,
1383 color: f.u32(0x14001cbe)?,
1384 },
1385 _ => Kind::Unknown,
1386 };
1387 let media_ids = f.guids(0x1c001c98)?;
1388 Ok(Self {
1389 jcid: object.jcid,
1390 children,
1391 content,
1392 structure,
1393 spaces,
1394 child_level,
1395 layout,
1396 format,
1397 created: f.u32(0x14001d09)?,
1398 modified: f.u32(0x14001d7a)?,
1399 original_author: f.one(0x20001d78)?,
1400 latest_author: f.one(0x20001d79)?,
1401 media_ids,
1402 media_time_ms: f.u32(0x14001c99)?,
1403 tags,
1404 kind,
1405 extra: f.remainder()?,
1406 })
1407 }
1408}