1use crate::{
2 Error, ExGuid, Object, ObjectData, PropertySets,
3 active::{ActivePage, Changes},
4 create::{current_timestamps, properties, string},
5 document::{Element, Kind},
6 edit::update_title,
7 write::{PropertyObject, fresh_guid},
8};
9use std::{
10 collections::{BTreeMap, BTreeSet},
11 ops::Range,
12 sync::Arc,
13};
14
15fn invalid(message: &'static str) -> Error {
16 Error { offset: 0, message }
17}
18
19/// Splits ordinary paragraph text. The original paragraph/text remain on the left; nested
20/// children move to the right.
21#[derive(Debug, Clone, PartialEq)]
22pub(crate) struct ParagraphSplit {
23 guid: [u8; 16],
24 text: ExGuid,
25 offset: u32,
26 author: String,
27 created: u32,
28}
29
30impl ParagraphSplit {
31 /// The offset is measured in UTF-16 code units and must lie between Unicode scalars.
32 pub(crate) fn new(text: ExGuid, offset: u32, author: &str) -> Result<Self, Error> {
33 if text.guid == [0; 16] || author.contains('\0') {
34 return Err(invalid(
35 "Choose paragraph text and an author name without NUL",
36 ));
37 }
38 Ok(Self {
39 guid: fresh_guid()?,
40 text,
41 offset,
42 author: author.to_owned(),
43 created: current_timestamps()?.0,
44 })
45 }
46
47 /// `changes` creating the right paragraph `paragraph` with text `right`, and a copy of
48 /// the left paragraph's list nodes under `lists`, in order.
49 pub(crate) fn changes_as(
50 &self,
51 active: &ActivePage<'_>,
52 paragraph_id: ExGuid,
53 right_text: ExGuid,
54 list_ids: &[ExGuid],
55 ) -> Result<Changes, Error> {
56 let [_] = active.pages.as_slice() else {
57 return Err(invalid("Splitting requires a single active page"));
58 };
59 let view = &active.view;
60 let parents = active.editable_parents(self.text)?;
61 let [paragraph] = parents
62 .get(&self.text)
63 .map(Vec::as_slice)
64 .unwrap_or_default()
65 else {
66 return Err(invalid("Select text belonging to one paragraph"));
67 };
68 let [parent] = parents
69 .get(paragraph)
70 .map(Vec::as_slice)
71 .unwrap_or_default()
72 else {
73 return Err(invalid("Select a paragraph with one parent"));
74 };
75 let left = &view.nodes[paragraph];
76 let Kind::Paragraph { lists, .. } = &left.kind else {
77 return Err(invalid("Select ordinary paragraph text"));
78 };
79 if left.content != [self.text]
80 || !matches!(
81 view.nodes[parent].kind,
82 Kind::Outline { .. }
83 | Kind::OutlineGroup
84 | Kind::Paragraph { .. }
85 | Kind::Cell { .. }
86 )
87 {
88 return Err(invalid(
89 "Select ordinary paragraph text inside an outline or table cell",
90 ));
91 }
92 let mut ancestors = BTreeSet::new();
93 let mut pending = vec![*paragraph];
94 while let Some(id) = pending.pop() {
95 if !ancestors.insert(id) {
96 continue;
97 }
98 if matches!(view.nodes[&id].kind, Kind::Title) {
99 return Err(invalid(
100 "Title containers cannot be split into ordinary paragraphs",
101 ));
102 }
103 pending.extend(parents.get(&id).into_iter().flatten().copied());
104 }
105 let raw = &active.live.revision;
106 let (text, runs) = ordinary_text(view, raw, self.text)?;
107 let resolved = view.text_runs(self.text)?;
108 let link = |at: usize| {
109 resolved
110 .get(at)
111 .is_some_and(|run| run.format.hyperlink == Some(true))
112 };
113 if let Some(after) = runs
114 .iter()
115 .position(|run| run.start < self.offset && self.offset < run.end)
116 {
117 if link(after) {
118 return Err(invalid("A split cannot divide a hyperlink"));
119 }
120 } else if let Some(after) = runs
121 .iter()
122 .position(|run| run.start == self.offset && run.end > run.start && self.offset > 0)
123 && link(after - 1)
124 && link(after)
125 && !resolved[after].text.starts_with('\u{fddf}')
126 {
127 return Err(invalid("A split cannot divide a hyperlink"));
128 }
129 let node = &view.nodes[&self.text];
130 let length = u32::try_from(text.encode_utf16().count())
131 .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?;
132 if self.offset > length || lists.len() > 251 || lists.len() > list_ids.len() {
133 return Err(invalid(
134 "Choose a position within the paragraph and at most 251 list levels",
135 ));
136 }
137 let author_id = ExGuid {
138 guid: self.guid,
139 n: 3,
140 };
141 let normal_id = ExGuid {
142 guid: self.guid,
143 n: 4,
144 };
145 // The text's last characters' style, past a final run an edit emptied.
146 let typing = runs
147 .iter()
148 .rev()
149 .find(|run| run.end > run.start)
150 .or(runs.last())
151 .and_then(|run| run.format)
152 .unwrap_or(normal_id);
153 // Enter at the start leaves the emptied upper paragraph the first run's style, as
154 // OneNote does (`evidence/structural-edits/xml/c2s-*`).
155 let leading = runs.first().and_then(|run| run.format);
156 let modified = current_timestamps()?.0.to_le_bytes();
157 let mut changed = BTreeMap::new();
158 if (self.offset == 0 && !text.is_empty() && leading.is_none())
159 || runs.iter().any(|run| run.format.is_none())
160 {
161 changed.insert(
162 normal_id,
163 PropertyObject {
164 jcid: 0x12004d,
165 bytes: properties(&[])?,
166 global_ids: Arc::new(BTreeMap::from([(0, self.guid)])),
167 },
168 );
169 }
170 let mut prefix = fragment(
171 &raw.objects[&self.text],
172 node,
173 0..self.offset,
174 if text.is_empty() {
175 typing
176 } else if self.offset == 0 {
177 leading.unwrap_or(normal_id)
178 } else {
179 normal_id
180 },
181 )?;
182 let mut suffix = fragment(&raw.objects[&self.text], node, self.offset..length, typing)?;
183 prefix.set(&[(0x14001d7a, &modified)])?;
184 // Note tags and the recording link stay with the original text.
185 suffix.remove(&[0x40003489, 0x1c001c98, 0x14001c99])?;
186 suffix.set(&[(0x14001d7a, &modified)])?;
187 suffix.reference(right_text)?;
188 changed.insert(self.text, prefix);
189 changed.insert(right_text, suffix);
190 changed.insert(
191 author_id,
192 PropertyObject {
193 jcid: 0x120001,
194 bytes: properties(&crate::create::author_properties(&self.author))?,
195 global_ids: Arc::new(BTreeMap::from([(0, self.guid)])),
196 },
197 );
198 let mut right = PropertyObject::from_object(&raw.objects[paragraph])?;
199 right.reference(paragraph_id)?;
200 let content = right.reference(right_text)?;
201 let author = right.reference(author_id)?;
202 right.set(&[
203 (0x24001c1f, &content),
204 (0x14001d09, &self.created.to_le_bytes()),
205 (0x20001d78, &author),
206 (0x20001d79, &author),
207 (0x14001d7a, &modified),
208 ])?;
209 if !lists.is_empty() {
210 let mut references = Vec::new();
211 for (i, old) in lists.iter().enumerate() {
212 if !matches!(view.nodes[old].kind, Kind::List { .. }) {
213 return Err(invalid("The paragraph list is unavailable"));
214 }
215 let id = list_ids[i];
216 let mut list = PropertyObject::from_object(&raw.objects[old])?;
217 list.remove(&[0x14001cb7])?;
218 list.reference(id)?;
219 references.extend_from_slice(&right.reference(id)?);
220 changed.insert(id, list);
221 }
222 right.set(&[(0x24001c26, &references)])?;
223 }
224 changed.insert(paragraph_id, right);
225 let mut original = PropertyObject::from_object(&raw.objects[paragraph])?;
226 original.remove(&[0x24001c20])?;
227 let author = original.reference(author_id)?;
228 original.set(&[(0x20001d79, &author), (0x14001d7a, &modified)])?;
229 changed.insert(*paragraph, original);
230 let mut parent_object = PropertyObject::from_object(&raw.objects[parent])?;
231 let mut children = Vec::new();
232 for child in &view.nodes[parent].children {
233 children.extend_from_slice(&parent_object.reference(*child)?);
234 if child == paragraph {
235 children.extend_from_slice(&parent_object.reference(paragraph_id)?);
236 }
237 }
238 parent_object.set(&[(0x24001c20, &children)])?;
239 changed.insert(*parent, parent_object);
240 for id in ancestors {
241 let object = match changed.entry(id) {
242 std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(),
243 std::collections::btree_map::Entry::Vacant(entry) => {
244 entry.insert(PropertyObject::from_object(&raw.objects[&id])?)
245 }
246 };
247 object.set(&[(0x14001d7a, &modified)])?;
248 }
249 update_title(active, view.clone(), &mut changed)?;
250 Ok(changed)
251 }
252}
253
254fn fragment(
255 raw: &Object<'_>,
256 node: &Element<'_>,
257 range: Range<u32>,
258 empty: ExGuid,
259) -> Result<PropertyObject, Error> {
260 let Kind::RichText { text, runs, .. } = &node.kind else {
261 return Err(invalid("Select ordinary paragraph text"));
262 };
263 let units: Vec<_> = text.encode_utf16().collect();
264 let start = usize::try_from(range.start)
265 .map_err(|_| invalid("Choose a position within the paragraph"))?;
266 let end = usize::try_from(range.end)
267 .map_err(|_| invalid("Choose a position within the paragraph"))?;
268 let slice = units
269 .get(start..end)
270 .ok_or_else(|| invalid("Choose a position within the paragraph"))?;
271 let text = String::from_utf16(slice)
272 .map_err(|_| invalid("Choose a position between Unicode scalars"))?;
273 let mut segments = Vec::new();
274 for run in runs {
275 if run.start < range.end && range.start < run.end {
276 segments.push((
277 run.end.min(range.end) - range.start,
278 run.format.unwrap_or(empty),
279 ));
280 }
281 }
282 if segments.is_empty() {
283 segments.push((0, empty));
284 }
285 if !range.is_empty() && end == units.len() {
286 let last = runs.last().unwrap();
287 if last.start == last.end {
288 segments.push((range.end - range.start, last.format.unwrap_or(empty)));
289 }
290 }
291 if segments[..segments.len() - 1]
292 .windows(2)
293 .any(|pair| pair[0].0 >= pair[1].0)
294 {
295 return Err(invalid("Text-run boundaries must be strictly increasing"));
296 }
297 let mut object = PropertyObject::from_object(raw)?;
298 let mut ends = Vec::new();
299 let mut references = Vec::new();
300 for (end, id) in segments {
301 ends.extend_from_slice(&end.to_le_bytes());
302 references.extend_from_slice(&object.reference(id)?);
303 }
304 ends.truncate(ends.len() - 4);
305 object.set(&[
306 (0x1c001c22, &string(&text)),
307 (0x1c001e12, &ends),
308 (0x24001e13, &references),
309 ])?;
310 Ok(object)
311}
312
313/// A text object's text and runs, unless it holds what a split or join cannot divide or
314/// merge: generated title text, equations, embedded objects or per-run data. Hyperlink
315/// fields are ordinary runs; a recording link belongs to the text object.
316fn ordinary_text<'a>(
317 view: &'a crate::document::Revision<'_>,
318 raw: &crate::ResolvedRevision<'_>,
319 id: ExGuid,
320) -> Result<(&'a str, &'a [crate::document::TextRun]), Error> {
321 let node = &view.nodes[&id];
322 let Kind::RichText {
323 text,
324 runs,
325 boilerplate,
326 ..
327 } = &node.kind
328 else {
329 return Err(invalid("Select ordinary paragraph text"));
330 };
331 if *boilerplate {
332 return Err(invalid("Generated text cannot be split or joined"));
333 }
334 for run in view.text_runs(id)? {
335 if [run.format.math, run.format.embedded_object].contains(&Some(true))
336 || run.text.contains('\u{fffc}')
337 {
338 return Err(invalid(
339 "This paragraph contains an equation or embedded object that cannot be split or joined",
340 ));
341 }
342 }
343 let ObjectData::Properties(blob) = raw.objects[&id].data else {
344 unreachable!()
345 };
346 if PropertySets::parse(blob)?.sets[0]
347 .iter()
348 .any(|p| matches!(p.id, 0x40003499 | 0x24003458))
349 {
350 return Err(invalid(
351 "This paragraph contains run metadata that cannot be split or joined",
352 ));
353 }
354 Ok((text, runs))
355}
356
357/// Joins adjacent ordinary text in one outline or table cell.
358/// The left paragraph survives. Empty left text adopts the right text identity;
359/// otherwise the left text survives. The left paragraph's tags are retained.
360/// Right-side tags are removed from active text, including when the left text is empty.
361#[derive(Debug, Clone, PartialEq)]
362pub(crate) struct ParagraphJoin {
363 left: ExGuid,
364 right: ExGuid,
365 author: String,
366}
367
368impl ParagraphJoin {
369 /// Select the preceding leaf paragraph's text and the following paragraph's text.
370 pub(crate) fn new(left: ExGuid, right: ExGuid, author: &str) -> Result<Self, Error> {
371 if left == right || left.guid == [0; 16] || right.guid == [0; 16] || author.contains('\0') {
372 return Err(invalid(
373 "Choose two distinct text objects and an author name without NUL",
374 ));
375 }
376 Ok(Self {
377 left,
378 right,
379 author: author.to_owned(),
380 })
381 }
382
383 pub(crate) fn changes(&self, active: &ActivePage<'_>) -> Result<Changes, Error> {
384 let pages = &active.pages;
385 if pages.len() != 1 {
386 return Err(invalid("Joining requires a single active page"));
387 }
388 let view = &active.view;
389 let parents = active.editable_parents(self.left)?;
390 active.editable_parents(self.right)?;
391 let parent = |id| -> Result<ExGuid, Error> {
392 let [parent] = parents.get(&id).map(Vec::as_slice).unwrap_or_default() else {
393 return Err(invalid("Select text with a unique editable parent"));
394 };
395 Ok(*parent)
396 };
397 let left = parent(self.left)?;
398 let right = parent(self.right)?;
399 for (paragraph, text) in [(left, self.left), (right, self.right)] {
400 if !matches!(view.nodes[&paragraph].kind, Kind::Paragraph { .. })
401 || view.nodes[&paragraph].content != [text]
402 {
403 return Err(invalid("Select ordinary paragraph text"));
404 }
405 let mut at = paragraph;
406 while at != pages[0] {
407 if matches!(view.nodes[&at].kind, Kind::Title) {
408 return Err(invalid(
409 "Title containers cannot be joined with ordinary paragraphs",
410 ));
411 }
412 at = parent(at)?;
413 }
414 }
415 if !view.nodes[&left].children.is_empty() {
416 return Err(invalid("Select the preceding paragraph's last descendant"));
417 }
418 let right_parent = parent(right)?;
419 if !matches!(
420 view.nodes[&right_parent].kind,
421 Kind::Outline { .. } | Kind::OutlineGroup | Kind::Paragraph { .. } | Kind::Cell { .. }
422 ) {
423 return Err(invalid("Select paragraphs in one outline or table cell"));
424 }
425 let mut stem = left;
426 while parent(stem)? != right_parent {
427 let previous = stem;
428 stem = parent(stem)?;
429 if !matches!(
430 view.nodes[&stem].kind,
431 Kind::Paragraph { .. } | Kind::OutlineGroup
432 ) || view.nodes[&stem].children.last() != Some(&previous)
433 {
434 return Err(invalid(
435 "Select adjacent text within one outline or table cell",
436 ));
437 }
438 }
439 let siblings = &view.nodes[&right_parent].children;
440 if !siblings.windows(2).any(|pair| pair == [stem, right]) {
441 return Err(invalid("Select adjacent paragraph text in document order"));
442 }
443 let children = &view.nodes[&right].children;
444 if !children.is_empty()
445 && !view.nodes[&stem].children.is_empty()
446 && view.nodes[&stem].child_level != view.nodes[&right].child_level
447 {
448 return Err(invalid(
449 "Joining these child indentation levels requires a hierarchy edit",
450 ));
451 }
452 let raw = &active.live.revision;
453 let (a, a_runs) = ordinary_text(view, raw, self.left)?;
454 let (b, b_runs) = ordinary_text(view, raw, self.right)?;
455 let length = u32::try_from(a.encode_utf16().count())
456 .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?;
457 let right_length = u32::try_from(b.encode_utf16().count())
458 .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?;
459 length
460 .checked_add(right_length)
461 .ok_or_else(|| invalid("Joined text exceeds the UTF-16 offset range"))?;
462 let modified = current_timestamps()?.0.to_le_bytes();
463 let mut changed = BTreeMap::new();
464 let (survivor, mut text) = if a.is_empty() {
465 let mut target = PropertyObject::from_object(&raw.objects[&self.right])?;
466 target.copy_property(
467 &PropertyObject::from_object(&raw.objects[&self.left])?,
468 0x40003489,
469 )?;
470 (self.right, target)
471 } else {
472 let mut target = PropertyObject::from_object(&raw.objects[&self.left])?;
473 let mut styles = BTreeMap::new();
474 let mut ends = Vec::new();
475 let mut references = Vec::new();
476 let mut empty_style = None;
477 for run in a_runs.iter().filter(|run| run.start < run.end) {
478 let id = if let Some(id) = run.format {
479 id
480 } else if let Some(id) = empty_style {
481 id
482 } else {
483 let id = ExGuid {
484 guid: fresh_guid()?,
485 n: 1,
486 };
487 changed.insert(
488 id,
489 PropertyObject {
490 jcid: 0x12004d,
491 bytes: properties(&[])?,
492 global_ids: Arc::new(BTreeMap::from([(0, id.guid)])),
493 },
494 );
495 empty_style = Some(id);
496 id
497 };
498 references.extend_from_slice(&target.reference(id)?);
499 ends.extend_from_slice(&run.end.to_le_bytes());
500 }
501 let left_base = character_properties(view, raw, self.left)?;
502 let right_base = character_properties(view, raw, self.right)?;
503 for (i, run) in b_runs.iter().enumerate() {
504 if run.start == run.end && i != b_runs.len() - 1 {
505 continue;
506 }
507 let id = if let Some(id) = styles.get(&run.format) {
508 *id
509 } else {
510 let mut style = if let Some(id) = run.format {
511 PropertyObject::from_object(&raw.objects[&id])?
512 } else {
513 PropertyObject {
514 jcid: 0x12004d,
515 bytes: properties(&[])?,
516 global_ids: Arc::new(BTreeMap::new()),
517 }
518 };
519 if left_base != right_base {
520 let mut values = right_base.clone();
521 for property in &PropertySets::parse(&style.bytes)?.sets[0] {
522 let key = property.id & 0x7fffffff;
523 if is_character_property(key) {
524 let value = match property.value {
525 crate::Value::NoData => &[][..],
526 crate::Value::Bytes(bytes) => bytes,
527 _ => unreachable!(),
528 };
529 values.insert(key, (property.id, value.to_vec()));
530 }
531 }
532 for key in left_base.keys() {
533 if values.contains_key(key) {
534 continue;
535 }
536 let value = match key >> 26 & 31 {
537 2 => Vec::new(),
538 _ if matches!(*key, 0x14001c0c | 0x14001c0d) => {
539 0xff000000_u32.to_le_bytes().to_vec()
540 }
541 _ => {
542 return Err(invalid(
543 "The right paragraph's implicit font or language cannot be preserved under the left style",
544 ));
545 }
546 };
547 values.insert(*key, (*key, value));
548 }
549 style.set(
550 &values
551 .values()
552 .map(|(id, value)| (*id, value.as_slice()))
553 .collect::<Vec<_>>(),
554 )?;
555 }
556 let id = if let Some(id) = run
557 .format
558 .filter(|id| raw.objects[id].data == ObjectData::Properties(&style.bytes))
559 {
560 id
561 } else {
562 let id = ExGuid {
563 guid: fresh_guid()?,
564 n: 1,
565 };
566 style.reference(id)?;
567 changed.insert(id, style);
568 id
569 };
570 styles.insert(run.format, id);
571 id
572 };
573 references.extend_from_slice(&target.reference(id)?);
574 ends.extend_from_slice(&(length + run.end).to_le_bytes());
575 }
576 ends.truncate(ends.len() - 4);
577 if ends
578 .chunks_exact(4)
579 .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
580 .collect::<Vec<_>>()
581 .windows(2)
582 .any(|pair| pair[0] >= pair[1])
583 {
584 return Err(invalid("Text-run boundaries must be strictly increasing"));
585 }
586 target.set(&[
587 (0x1c001c22, &string(&format!("{a}{b}"))),
588 (0x1c001e12, &ends),
589 (0x24001e13, &references),
590 ])?;
591 (self.left, target)
592 };
593 text.set(&[(0x14001d7a, &modified)])?;
594 changed.insert(survivor, text);
595 let author = ExGuid {
596 guid: fresh_guid()?,
597 n: 1,
598 };
599 changed.insert(
600 author,
601 PropertyObject {
602 jcid: 0x120001,
603 bytes: properties(&crate::create::author_properties(&self.author))?,
604 global_ids: Arc::new(BTreeMap::from([(0, author.guid)])),
605 },
606 );
607 let mut left_object = PropertyObject::from_object(&raw.objects[&left])?;
608 let content = left_object.reference(survivor)?;
609 let author = left_object.reference(author)?;
610 left_object.set(&[(0x24001c1f, &content), (0x20001d79, &author)])?;
611 changed.insert(left, left_object);
612 if !children.is_empty() {
613 let target = match changed.entry(stem) {
614 std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(),
615 std::collections::btree_map::Entry::Vacant(entry) => {
616 entry.insert(PropertyObject::from_object(&raw.objects[&stem])?)
617 }
618 };
619 let mut references = Vec::new();
620 for id in view.nodes[&stem].children.iter().chain(children) {
621 references.extend_from_slice(&target.reference(*id)?);
622 }
623 target.set(&[(0x24001c20, &references)])?;
624 target.copy_property(
625 &PropertyObject::from_object(&raw.objects[&right])?,
626 0x0c001c03,
627 )?;
628 }
629 let mut container = PropertyObject::from_object(&raw.objects[&right_parent])?;
630 let mut references = Vec::new();
631 for id in siblings.iter().filter(|id| **id != right) {
632 references.extend_from_slice(&container.reference(*id)?);
633 }
634 container.set(&[(0x24001c20, &references)])?;
635 changed.insert(right_parent, container);
636 let mut pending = vec![left, right_parent];
637 let mut ancestors = BTreeSet::new();
638 while let Some(id) = pending.pop() {
639 if !ancestors.insert(id) {
640 continue;
641 }
642 let object = match changed.entry(id) {
643 std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(),
644 std::collections::btree_map::Entry::Vacant(entry) => {
645 entry.insert(PropertyObject::from_object(&raw.objects[&id])?)
646 }
647 };
648 object.set(&[(0x14001d7a, &modified)])?;
649 pending.extend(parents.get(&id).into_iter().flatten().copied());
650 }
651 update_title(active, view.clone(), &mut changed)?;
652 Ok(changed)
653 }
654}
655
656fn is_character_property(id: u32) -> bool {
657 matches!(
658 id,
659 0x08001c04
660 ..=0x08001c09
661 | 0x08001e16
662 | 0x08001e14
663 | 0x08001e19
664 | 0x08003401
665 | 0x08001e22
666 | 0x08003476
667 | 0x1c001c0a
668 | 0x10001c0b
669 | 0x14001c0c
670 | 0x14001c0d
671 | 0x14001c3b
672 )
673}
674
675fn character_properties(
676 view: &crate::document::Revision<'_>,
677 raw: &crate::ResolvedRevision<'_>,
678 text: ExGuid,
679) -> Result<BTreeMap<u32, (u32, Vec<u8>)>, Error> {
680 let Kind::RichText {
681 paragraph_style, ..
682 } = view.nodes[&text].kind
683 else {
684 unreachable!()
685 };
686 let mut values = BTreeMap::new();
687 for id in paragraph_style.into_iter().chain([text]) {
688 let ObjectData::Properties(bytes) = raw.objects[&id].data else {
689 unreachable!()
690 };
691 for property in &PropertySets::parse(bytes)?.sets[0] {
692 let key = property.id & 0x7fffffff;
693 if is_character_property(key) {
694 let value = match property.value {
695 crate::Value::NoData => &[][..],
696 crate::Value::Bytes(bytes) => bytes,
697 _ => unreachable!(),
698 };
699 values.insert(key, (property.id, value.to_vec()));
700 }
701 }
702 }
703 Ok(values)
704}