1use super::patch_properties;
2use crate::{PropertySets, Value, create::properties};
3
4mod multi;
5
6#[test]
7fn copying_reference_arrays_remaps_every_entry_in_each_stream() {
8 use super::PropertyObject;
9 use std::{collections::BTreeMap, sync::Arc};
10 for kind in [9_u32, 11, 13] {
11 for count in [0_u32, 1, 2, 3, 4, 8, 33] {
12 for nested in [false, true] {
13 let property = (kind << 26) | 1;
14 let references: Vec<_> = (0..count)
15 .flat_map(|i| (((i % 3) << 8) | i).to_le_bytes())
16 .collect();
17 let mut bytes = properties(&[(property, references)]).unwrap();
18 let selected = if nested {
19 let parsed = PropertySets::parse(&bytes).unwrap();
20 let offset = parsed.root_ids.as_ptr().addr() - bytes.as_ptr().addr() - 2;
21 bytes.truncate(bytes.len() - parsed.padding.len());
22 bytes.splice(offset..offset, [1, 0, 2, 0, 0, 0x44]);
23 bytes.resize(bytes.len().next_multiple_of(8), 0);
24 0x44000002
25 } else {
26 property
27 };
28 let source = PropertyObject {
29 jcid: 0x6000e,
30 bytes,
31 global_ids: Arc::new(BTreeMap::from([
32 (0, [1; 16]),
33 (1, [2; 16]),
34 (2, [3; 16]),
35 ])),
36 };
37 let mut target = PropertyObject {
38 jcid: 0x6000e,
39 bytes: properties(&[(0x14000003, 47_u32.to_le_bytes().to_vec())]).unwrap(),
40 global_ids: Arc::new(BTreeMap::from([
41 (0, [3; 16]),
42 (1, [9; 16]),
43 (2, [1; 16]),
44 ])),
45 };
46 target.copy_property(&source, selected).unwrap();
47 let parsed = PropertySets::parse(&target.bytes).unwrap();
48 assert_eq!(parsed.sets[0][0].value, Value::Bytes(&47_u32.to_le_bytes()));
49 let copied = parsed
50 .sets
51 .iter()
52 .flatten()
53 .find(|p| p.id == property)
54 .unwrap();
55 let Value::References { compact_ids, .. } = copied.value else {
56 panic!()
57 };
58 assert_eq!(compact_ids.len(), count as usize * 4);
59 for (i, compact) in compact_ids.chunks_exact(4).enumerate() {
60 let compact = u32::from_le_bytes(compact.try_into().unwrap());
61 assert_eq!(compact & 255, i as u32);
62 assert_eq!(target.global_ids[&(compact >> 8)], [(i % 3 + 1) as u8; 16]);
63 }
64 let before = target.bytes.clone();
65 target.copy_property(&source, selected).unwrap();
66 assert_eq!(target.bytes, before);
67 }
68 }
69 }
70}
71
72#[test]
73fn document_insertions_and_formatting_respect_readonly_ancestors() {
74 use super::{PropertyObject, write_revision};
75 use crate::{
76 ExGuid, RevisionIndex, Store, TextAttribute,
77 document::{Format, Layout},
78 op::{
79 PageOp,
80 tests::{edited, text_paragraph},
81 },
82 page::{Outline, PageObject, text::new_id},
83 };
84 use std::collections::BTreeMap;
85 let source = crate::create_section("readonly.one", "Original", "Author").unwrap();
86 let store = Store::parse(&source).unwrap();
87 let index = RevisionIndex::parse(&store).unwrap();
88 let (sid, page, outline, paragraph) = index
89 .spaces
90 .iter()
91 .find_map(|(sid, s)| {
92 let raw = index
93 .resolve(*sid, s.labels[&(ExGuid::default(), 1)])
94 .unwrap();
95 let by_type = |jcid| {
96 raw.objects
97 .iter()
98 .find_map(|(id, o)| (o.jcid == jcid).then_some(*id))
99 };
100 Some((
101 *sid,
102 by_type(0x6000b)?,
103 by_type(0x6000c)?,
104 by_type(0x6000d)?,
105 ))
106 })
107 .unwrap();
108 let right = text_paragraph("Right", Format::default());
109 let right_text = right.text().unwrap().id;
110 let insert = |container, paragraph| PageOp::Insert {
111 container,
112 before: None,
113 paragraphs: vec![paragraph],
114 };
115 let expanded = edited(&source, sid, vec![insert(outline, right.clone())]).unwrap();
116 let source = expanded.as_slice();
117 let raw = index
118 .resolve(sid, index.spaces[&sid].labels[&(ExGuid::default(), 1)])
119 .unwrap();
120 let text = raw
121 .objects
122 .iter()
123 .find_map(|(id, object)| (object.jcid == 0x6000e).then_some(*id))
124 .unwrap();
125 for blocked in [page, outline, paragraph] {
126 let protected = write_revision(source, sid, |raw| {
127 let mut object = PropertyObject::from_object(&raw.objects[&blocked])?;
128 object.set(&[(0x88001cde, &[])])?;
129 Ok(BTreeMap::from([(blocked, object)]))
130 })
131 .unwrap();
132 let refused = |op| edited(&protected, sid, vec![op]).is_err();
133 assert!(refused(insert(
134 paragraph,
135 text_paragraph("Nested", Format::default())
136 )));
137 assert!(refused(PageOp::Split {
138 text,
139 at: 1,
140 paragraph: new_id().unwrap(),
141 right: new_id().unwrap(),
142 lists: Vec::new(),
143 }));
144 assert!(refused(PageOp::Join {
145 left: text,
146 right: right_text,
147 }));
148 assert!(refused(PageOp::Format {
149 text,
150 range: 1..3,
151 set: vec![TextAttribute::Bold(true)],
152 clear: Vec::new(),
153 }));
154 if blocked == page {
155 assert!(refused(PageOp::Add {
156 object: PageObject::Outline(Outline {
157 id: new_id().unwrap(),
158 title: false,
159 min_width: None,
160 layout: Layout {
161 x: Some(36.0),
162 y: Some(36.0),
163 ..Default::default()
164 },
165 indents: Vec::new(),
166 paragraphs: vec![text_paragraph("Outline", Format::default())],
167 unsupported: Vec::new(),
168 }),
169 before: None,
170 }));
171 }
172 }
173 let protected = write_revision(source, sid, |raw| {
174 let mut object = PropertyObject::from_object(&raw.objects[&right.id])?;
175 object.set(&[(0x88001cde, &[])])?;
176 Ok(BTreeMap::from([(right.id, object)]))
177 })
178 .unwrap();
179 let join = PageOp::Join {
180 left: text,
181 right: right_text,
182 };
183 assert!(edited(&protected, sid, vec![join]).is_err());
184}
185
186fn add_paragraph(source: &[u8], number: u32) -> Vec<u8> {
187 use super::{PropertyObject, compact, write_revision};
188 use crate::{ExGuid, ObjectData, RevisionIndex, Store};
189 use std::{collections::BTreeMap, sync::Arc};
190 let store = Store::parse(source).unwrap();
191 let index = RevisionIndex::parse(&store).unwrap();
192 let space = index
193 .spaces
194 .iter()
195 .find_map(|(sid, s)| {
196 let revision = index
197 .resolve(*sid, s.labels[&(ExGuid::default(), 1)])
198 .unwrap();
199 revision
200 .objects
201 .values()
202 .any(|o| o.jcid == 0x6000c)
203 .then_some(*sid)
204 })
205 .unwrap();
206 write_revision(source, space, |revision| {
207 let (&outline_id, outline) = revision
208 .objects
209 .iter()
210 .find(|(_, o)| o.jcid == 0x6000c)
211 .unwrap();
212 let (&author, _) = revision
213 .objects
214 .iter()
215 .find(|(_, o)| o.jcid == 0x120001)
216 .unwrap();
217 let (&style, _) = revision
218 .objects
219 .iter()
220 .find(|(_, o)| o.jcid == 0x12004d)
221 .unwrap();
222 let mut guid = [0x69; 16];
223 guid[..4].copy_from_slice(&number.to_le_bytes());
224 let paragraph = ExGuid { guid, n: 1 };
225 let text = ExGuid { guid, n: 2 };
226 let mut table = (*outline.global_ids).clone();
227 for guid in [guid, author.guid, style.guid] {
228 if !table.values().any(|previous| *previous == guid) {
229 table.insert(table.last_key_value().map_or(0, |(i, _)| i + 1), guid);
230 }
231 }
232 let table = Arc::new(table);
233 let ObjectData::Properties(blob) = outline.data else {
234 unreachable!()
235 };
236 let parsed = PropertySets::parse(blob).unwrap();
237 let Value::References { compact_ids, .. } = parsed.sets[0]
238 .iter()
239 .find(|p| p.id == 0x24001c20)
240 .unwrap()
241 .value
242 else {
243 unreachable!()
244 };
245 let mut children = compact_ids.to_vec();
246 children.extend_from_slice(&compact(paragraph, &table).unwrap());
247 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
248 let mut changed = BTreeMap::new();
249 changed.insert(
250 outline_id,
251 PropertyObject {
252 jcid: outline.jcid,
253 bytes: patch_properties(
254 blob,
255 &[(0x24001c20, &children), (0x14001d7a, &modified)],
256 &[],
257 &[],
258 )?,
259 global_ids: Arc::clone(&table),
260 },
261 );
262 changed.insert(
263 paragraph,
264 PropertyObject {
265 jcid: 0x6000d,
266 bytes: properties(&[
267 (0x14001d7a, modified.to_vec()),
268 (0x14001d09, modified.to_vec()),
269 (0x0c001c03, vec![1]),
270 (0x24001c1f, compact(text, &table)?.to_vec()),
271 (0x20001d78, compact(author, &table)?.to_vec()),
272 (0x20001d79, compact(author, &table)?.to_vec()),
273 ])?,
274 global_ids: Arc::clone(&table),
275 },
276 );
277 changed.insert(
278 text,
279 PropertyObject {
280 jcid: 0x6000e,
281 bytes: properties(&[
282 (0x14001d7a, modified.to_vec()),
283 (
284 0x1c001c22,
285 format!("Paragraph {number}\0")
286 .encode_utf16()
287 .flat_map(u16::to_le_bytes)
288 .collect(),
289 ),
290 (0x24001e13, compact(style, &table)?.to_vec()),
291 ])?,
292 global_ids: table,
293 },
294 );
295 Ok(changed)
296 })
297 .unwrap()
298}
299
300fn restyle(source: &[u8]) -> Vec<u8> {
301 use super::{PropertyObject, compact, write_revision};
302 use crate::{ExGuid, ObjectData, RevisionIndex, Store};
303 use std::{collections::BTreeMap, sync::Arc};
304 let store = Store::parse(source).unwrap();
305 let index = RevisionIndex::parse(&store).unwrap();
306 let space = index
307 .spaces
308 .iter()
309 .find_map(|(sid, s)| {
310 let revision = index
311 .resolve(*sid, s.labels[&(ExGuid::default(), 1)])
312 .unwrap();
313 revision
314 .objects
315 .values()
316 .any(|o| o.jcid == 0x6000c)
317 .then_some(*sid)
318 })
319 .unwrap();
320 let style = ExGuid {
321 guid: [0x74; 16],
322 n: 1,
323 };
324 write_revision(source, space, |revision| {
325 let previous = revision
326 .objects
327 .values()
328 .find(|o| o.jcid == 0x12004d)
329 .unwrap();
330 let ObjectData::Properties(blob) = previous.data else {
331 unreachable!()
332 };
333 let mut table = (*previous.global_ids).clone();
334 table.insert(table.last_key_value().unwrap().0 + 1, style.guid);
335 let mut changed = BTreeMap::from([(
336 style,
337 PropertyObject {
338 jcid: previous.jcid,
339 bytes: patch_properties(blob, &[], &[(0x88001c04, &[])], &[])?,
340 global_ids: Arc::new(table),
341 },
342 )]);
343 for (id, text) in revision.objects.iter().filter(|(_, o)| o.jcid == 0x6000e) {
344 let ObjectData::Properties(blob) = text.data else {
345 unreachable!()
346 };
347 let mut table = (*text.global_ids).clone();
348 table.insert(table.last_key_value().unwrap().0 + 1, style.guid);
349 changed.insert(
350 *id,
351 PropertyObject {
352 jcid: text.jcid,
353 bytes: patch_properties(
354 blob,
355 &[(0x24001e13, &compact(style, &table)?)],
356 &[],
357 &[],
358 )?,
359 global_ids: Arc::new(table),
360 },
361 );
362 }
363 Ok(changed)
364 })
365 .unwrap()
366}
367
368#[test]
369fn replaced_readonly_styles_retain_history_with_zero_current_references() {
370 use crate::{ExGuid, RevisionIndex, Store};
371 let source = add_paragraph(
372 &crate::create_section("style.one", "Original", "Author").unwrap(),
373 1,
374 );
375 let changed = restyle(&source);
376 let store = Store::parse(&changed).unwrap();
377 let index = RevisionIndex::parse(&store).unwrap();
378 index.validate_current().unwrap();
379 let mut styles = Vec::new();
380 for (sid, s) in &index.spaces {
381 let revision = index
382 .resolve(*sid, s.labels[&(ExGuid::default(), 1)])
383 .unwrap();
384 for (id, object) in &revision.objects {
385 if object.jcid == 0x12004d {
386 styles.push((object.reference_count, id.guid));
387 }
388 }
389 }
390 styles.sort();
391 assert_eq!(styles.len(), 2);
392 assert_eq!(styles[0].0, 0);
393 assert_eq!(styles[1], (2, [0x74; 16]));
394 let document = crate::document::Document::parse(&index).unwrap();
395 let mut count = 0;
396 for space in document.spaces.values() {
397 for revision in space.revisions.values() {
398 for (id, node) in &revision.nodes {
399 if matches!(node.kind, crate::document::Kind::RichText { .. }) {
400 for run in revision.text_runs(*id).unwrap() {
401 assert_eq!(run.format.bold, Some(true));
402 }
403 count += 1;
404 }
405 }
406 }
407 }
408 assert_eq!(count, 2);
409}
410
411#[test]
412#[ignore = "Writes cold-native candidates to ONESTORE_GROWTH_OUTPUT"]
413fn export_native_growth_candidates() {
414 let output = std::path::PathBuf::from(std::env::var_os("ONESTORE_GROWTH_OUTPUT").unwrap());
415 std::fs::create_dir(&output).unwrap();
416 let mut source = crate::create_section("growth.one", "Original", "Author").unwrap();
417 for number in 1..=24 {
418 source = add_paragraph(&source, number);
419 }
420 std::fs::write(output.join("growth.one"), &source).unwrap();
421 std::fs::write(output.join("restyled.one"), restyle(&source)).unwrap();
422}
423
424#[test]
425fn atomic_graph_growth_preserves_history_and_counts_shared_readonly_objects() {
426 use crate::{ExGuid, RevisionIndex, Store};
427 let initial = crate::create_section("growth.one", "Original", "Author").unwrap();
428 let mut source = initial.clone();
429 for number in 1..=24 {
430 source = add_paragraph(&source, number);
431 let store = Store::parse(&source).unwrap();
432 let index = RevisionIndex::parse(&store).unwrap();
433 index.validate_current().unwrap();
434 assert!(store.checksum_mismatches.is_empty());
435 assert_eq!(store.header.transaction_count, number + 1);
436 let mut text_count = 0;
437 for (sid, s) in &index.spaces {
438 let revision = index
439 .resolve(*sid, s.labels[&(ExGuid::default(), 1)])
440 .unwrap();
441 let reachable = revision.reachable().unwrap();
442 for id in reachable {
443 let object = &revision.objects[&id];
444 match object.jcid {
445 0x6000e => text_count += 1,
446 0x120001 => assert_eq!(object.reference_count, (number + 1) * 2),
447 0x12004d => assert_eq!(object.reference_count, number + 1),
448 _ => {}
449 }
450 }
451 }
452 assert_eq!(text_count, number + 1);
453 }
454 let initial_store = Store::parse(&initial).unwrap();
455 let initial_index = RevisionIndex::parse(&initial_store).unwrap();
456 let store = Store::parse(&source).unwrap();
457 let index = RevisionIndex::parse(&store).unwrap();
458 for (sid, s) in &initial_index.spaces {
459 let rid = s.labels[&(ExGuid::default(), 1)];
460 let before = initial_index.resolve(*sid, rid).unwrap();
461 let after = index.resolve(*sid, rid).unwrap();
462 assert_eq!(before.roots, after.roots);
463 for (id, object) in &before.objects {
464 assert_eq!(object.data, after.objects[id].data);
465 assert_eq!(object.reference_count, after.objects[id].reference_count);
466 }
467 }
468}
469
470#[test]
471fn changed_graphs_reject_cycles_dangling_and_unreachable_additions() {
472 use super::{PropertyObject, compact, write_revision};
473 use crate::{ExGuid, ObjectData, RevisionIndex, Store};
474 use std::{collections::BTreeMap, sync::Arc};
475 let source = crate::create_section("invalid.one", "Original", "Author").unwrap();
476 let store = Store::parse(&source).unwrap();
477 let index = RevisionIndex::parse(&store).unwrap();
478 for (sid, s) in &index.spaces {
479 let revision = index
480 .resolve(*sid, s.labels[&(ExGuid::default(), 1)])
481 .unwrap();
482 let Some((&oid, outline)) = revision.objects.iter().find(|(_, o)| o.jcid == 0x6000c) else {
483 continue;
484 };
485 let ObjectData::Properties(blob) = outline.data else {
486 unreachable!()
487 };
488 for target in [
489 oid,
490 ExGuid {
491 guid: oid.guid,
492 n: 255,
493 },
494 ] {
495 assert!(
496 write_revision(&source, *sid, |_| Ok(BTreeMap::from([(
497 oid,
498 PropertyObject {
499 jcid: outline.jcid,
500 bytes: patch_properties(
501 blob,
502 &[(0x24001c20, &compact(target, &outline.global_ids)?)],
503 &[],
504 &[],
505 )?,
506 global_ids: Arc::clone(&outline.global_ids),
507 }
508 )])))
509 .is_err()
510 );
511 }
512 let orphan = ExGuid {
513 guid: oid.guid,
514 n: 255,
515 };
516 assert!(
517 write_revision(&source, *sid, |_| Ok(BTreeMap::from([(
518 orphan,
519 PropertyObject {
520 jcid: 0x6000e,
521 bytes: properties(&[])?,
522 global_ids: Arc::clone(&outline.global_ids),
523 }
524 )])))
525 .is_err()
526 );
527 }
528}
529
530#[test]
531fn property_splices_match_independently_encoded_flat_sets() {
532 let mut seed = 0x749391acb66327d5_u64;
533 let mut next = || {
534 seed ^= seed << 13;
535 seed ^= seed >> 7;
536 seed ^= seed << 17;
537 seed
538 };
539 for case in 0..20_000 {
540 let mut original = Vec::new();
541 let mut expected = Vec::new();
542 let mut updates = Vec::new();
543 let mut inserts = Vec::new();
544 for index in 0..next() % 30 {
545 let kind = 2 + (next() % 8) as u32;
546 let mut id = (kind << 26) | index as u32;
547 let length = |random: u64| match kind {
548 2 => 0,
549 3..=6 => 1 << (kind - 3),
550 7 => (random % 20) as usize,
551 8 => 4,
552 9 => (random % 5) as usize * 4,
553 _ => unreachable!(),
554 };
555 let value = (0..length(next()))
556 .map(|_| next() as u8)
557 .collect::<Vec<_>>();
558 if kind == 2 && next() & 1 != 0 {
559 id |= 0x80000000;
560 }
561 original.push((id, value.clone()));
562 if next() & 1 != 0 {
563 if kind == 2 {
564 id ^= 0x80000000;
565 }
566 let value = (0..length(next()))
567 .map(|_| next() as u8)
568 .collect::<Vec<_>>();
569 expected.push((id, value.clone()));
570 updates.push((id, value));
571 } else {
572 expected.push((id, value));
573 }
574 }
575 for index in 0..next() % 6 {
576 let kind = [2, 7, 8, 9][(next() % 4) as usize];
577 let mut id = (kind << 26) | (100 + index as u32);
578 if kind == 2 && next() & 1 != 0 {
579 id |= 0x80000000;
580 }
581 let length = match kind {
582 2 => 0,
583 7 => next() as usize % 10,
584 8 => 4,
585 9 => (next() as usize % 4) * 4,
586 _ => unreachable!(),
587 };
588 let value = (0..length).map(|_| next() as u8).collect::<Vec<_>>();
589 inserts.push((id, value.clone()));
590 expected.push((id, value));
591 }
592 updates.reverse();
593 let updates: Vec<_> = updates
594 .iter()
595 .map(|(id, value)| (*id, value.as_slice()))
596 .collect();
597 let inserts: Vec<_> = inserts
598 .iter()
599 .map(|(id, value)| (*id, value.as_slice()))
600 .collect();
601 let original = properties(&original).unwrap();
602 let actual = patch_properties(&original, &updates, &inserts, &[]).unwrap();
603 assert_eq!(actual, properties(&expected).unwrap(), "case {case}");
604 }
605}
606
607#[test]
608fn nested_fields_and_other_reference_streams_remain_byte_exact() {
609 let mut original = 0x40000003_u32.to_le_bytes().to_vec();
610 original.extend_from_slice(&[1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]);
611 original.extend_from_slice(&0x40000001_u32.to_le_bytes());
612 original.extend_from_slice(&[4, 0, 0, 0]);
613 original.extend_from_slice(&1_u32.to_le_bytes());
614 original.extend_from_slice(&[5, 0, 0, 0]);
615 original.extend_from_slice(&4_u16.to_le_bytes());
616 for id in [0x24000001_u32, 0x40000002, 0x24000003, 0x1c000004] {
617 original.extend_from_slice(&id.to_le_bytes());
618 }
619 original.extend_from_slice(&1_u32.to_le_bytes());
620 let nested_start = original.len();
621 original.extend_from_slice(&1_u32.to_le_bytes());
622 original.extend_from_slice(&0x44003456_u32.to_le_bytes());
623 original.extend_from_slice(&4_u16.to_le_bytes());
624 for id in [0x20000001_u32, 0x28000002, 0x30000003, 0x1c000004] {
625 original.extend_from_slice(&id.to_le_bytes());
626 }
627 original.extend_from_slice(&3_u32.to_le_bytes());
628 original.extend_from_slice(&[91, 92, 93]);
629 let nested_end = original.len();
630 original.extend_from_slice(&1_u32.to_le_bytes());
631 original.extend_from_slice(&2_u32.to_le_bytes());
632 original.extend_from_slice(&[94, 95]);
633 original.resize(original.len().next_multiple_of(8), 0);
634 let changed = patch_properties(
635 &original,
636 &[
637 (0x24000003, &[]),
638 (0x1c000004, &[96, 97, 98, 99]),
639 (0x24000001, &[6, 0, 0, 0, 7, 0, 0, 0]),
640 ],
641 &[(0x24000005, &[8, 0, 0, 0]), (0x88000006, &[])],
642 &[],
643 )
644 .unwrap();
645 let parsed = PropertySets::parse(&changed).unwrap();
646 let previous = PropertySets::parse(&original).unwrap();
647 assert_eq!(parsed.sets[1], previous.sets[1]);
648 let nested = &original[nested_start..nested_end];
649 assert_eq!(
650 changed
651 .windows(nested.len())
652 .filter(|bytes| *bytes == nested)
653 .count(),
654 1
655 );
656 assert_eq!(
657 &changed[4..20],
658 &[6, 0, 0, 0, 7, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0]
659 );
660 assert_eq!(&changed[20..36], &original[16..32]);
661 assert_eq!(parsed.sets[0][3].value, Value::Bytes(&[96, 97, 98, 99]));
662
663 for removed in 0..16 {
664 let ids: Vec<_> = previous.sets[0]
665 .iter()
666 .enumerate()
667 .filter_map(|(i, property)| (removed & (1 << i) != 0).then_some(property.id))
668 .collect();
669 let mut object = super::PropertyObject {
670 jcid: 0x6000e,
671 bytes: original.clone(),
672 global_ids: Default::default(),
673 };
674 object.remove(&ids).unwrap();
675 let parsed = PropertySets::parse(&object.bytes).unwrap();
676 assert!(
677 parsed.sets[0]
678 .iter()
679 .eq(previous.sets[0].iter().filter(|p| !ids.contains(&p.id)))
680 );
681 if removed & 2 == 0 {
682 assert_eq!(parsed.sets[1], previous.sets[1]);
683 assert_eq!(
684 object
685 .bytes
686 .windows(nested.len())
687 .filter(|bytes| *bytes == nested)
688 .count(),
689 1
690 );
691 } else {
692 assert_eq!(parsed.sets.len(), 1);
693 }
694 if removed == 0 {
695 assert_eq!(object.bytes, original);
696 }
697 let before = object.bytes.clone();
698 object.remove(&ids).unwrap();
699 assert_eq!(object.bytes, before);
700 }
701 let source = super::PropertyObject {
702 jcid: 0x6000e,
703 bytes: original.clone(),
704 global_ids: std::sync::Arc::new(std::collections::BTreeMap::from([(0, [1; 16])])),
705 };
706 let mut target = super::PropertyObject {
707 jcid: 0x6000e,
708 bytes: properties(&[(0x20000001, vec![6, 0, 0, 0])]).unwrap(),
709 global_ids: std::sync::Arc::new(std::collections::BTreeMap::from([(0, [9; 16])])),
710 };
711 target.copy_property(&source, 0x40000002).unwrap();
712 let copied = PropertySets::parse(&target.bytes).unwrap();
713 assert_eq!(target.global_ids[&0], [9; 16]);
714 assert_eq!(target.global_ids[&1], [1; 16]);
715 assert_eq!(
716 copied.sets[0][0].value,
717 Value::References {
718 stream: crate::IdStream::Objects,
719 compact_ids: &[6, 0, 0, 0],
720 }
721 );
722 for (i, n) in [2, 4, 5].into_iter().enumerate() {
723 let Value::References { compact_ids, .. } = copied.sets[1][i].value else {
724 panic!()
725 };
726 assert_eq!(compact_ids, &[n, 1, 0, 0]);
727 }
728 assert_eq!(copied.sets[1][3], previous.sets[1][3]);
729 assert_eq!(
730 target
731 .bytes
732 .windows(nested.len())
733 .filter(|b| *b == nested)
734 .count(),
735 1
736 );
737 let before = target.bytes.clone();
738 target.copy_property(&source, 0x40000002).unwrap();
739 assert_eq!(target.bytes, before);
740 target.copy_property(&source, 0x20000001).unwrap();
741 assert_eq!(PropertySets::parse(&target.bytes).unwrap().sets[0].len(), 1);
742 let before = target.bytes.clone();
743 let ids = std::sync::Arc::clone(&target.global_ids);
744 let invalid = super::PropertyObject {
745 global_ids: Default::default(),
746 ..source
747 };
748 assert!(target.copy_property(&invalid, 0x40000002).is_err());
749 assert_eq!(target.bytes, before);
750 assert_eq!(target.global_ids, ids);
751}
752
753#[test]
754fn deep_property_splices_do_not_use_the_call_stack() {
755 let mut bytes = 0x80000000_u32.to_le_bytes().to_vec();
756 for _ in 0..100_000 {
757 bytes.extend_from_slice(&1_u16.to_le_bytes());
758 bytes.extend_from_slice(&0x44000001_u32.to_le_bytes());
759 }
760 bytes.extend_from_slice(&0_u16.to_le_bytes());
761 let changed = patch_properties(&bytes, &[], &[(0x88000002, &[])], &[]).unwrap();
762 let parsed = PropertySets::parse(&changed).unwrap();
763 assert_eq!(parsed.sets.len(), 100_001);
764 assert_eq!(parsed.sets[0].len(), 2);
765 assert_eq!(
766 &changed[14..changed.len() - parsed.padding.len()],
767 &bytes[10..]
768 );
769 let mut object = super::PropertyObject {
770 jcid: 0x6000e,
771 bytes: changed,
772 global_ids: Default::default(),
773 };
774 object.remove(&[0x08000002]).unwrap();
775 let parsed = PropertySets::parse(&object.bytes).unwrap();
776 assert_eq!(parsed.sets.len(), 100_001);
777 assert_eq!(&object.bytes[..bytes.len()], &bytes);
778 let mut copied = super::PropertyObject {
779 jcid: object.jcid,
780 bytes: properties(&[]).unwrap(),
781 global_ids: Default::default(),
782 };
783 copied.copy_property(&object, 0x44000001).unwrap();
784 assert_eq!(copied.bytes, object.bytes);
785 object.remove(&[0x44000001]).unwrap();
786 let parsed = PropertySets::parse(&object.bytes).unwrap();
787 assert_eq!(parsed.sets.len(), 1);
788 assert!(parsed.sets[0].is_empty());
789}
790
791#[test]
792fn invalid_property_splices_are_rejected() {
793 let bytes = properties(&[(0x08000001, vec![]), (0x24000002, vec![])]).unwrap();
794 for (updates, inserts) in [
795 (vec![(0x88000001, &[][..]), (0x08000001, &[])], vec![]),
796 (vec![(0x14000003, &[0; 4][..])], vec![]),
797 (vec![(0x24000002, &[0; 3][..])], vec![]),
798 (vec![(0x88000001, &[1][..])], vec![]),
799 (vec![], vec![(0x88000001, &[][..])]),
800 (vec![], vec![(0x20000003, &[][..])]),
801 ] {
802 assert!(patch_properties(&bytes, &updates, &inserts, &[]).is_err());
803 }
804}
805
806#[test]
807fn character_formatting_preserves_inheritance_and_associated_data() {
808 use super::{PropertyObject, write_revision};
809 use crate::{
810 ExGuid, RevisionIndex, Store, TextAttribute,
811 document::{Document, Kind},
812 op::{PageOp, tests::edited},
813 };
814 use std::collections::BTreeMap;
815 let source = crate::create_section("inherited.one", "abcdef", "Author").unwrap();
816 let store = Store::parse(&source).unwrap();
817 let index = RevisionIndex::parse(&store).unwrap();
818 let (sid, text) = index
819 .spaces
820 .iter()
821 .find_map(|(sid, s)| {
822 let raw = index
823 .resolve(*sid, s.labels[&(ExGuid::default(), 1)])
824 .unwrap();
825 raw.objects
826 .iter()
827 .find_map(|(id, o)| (o.jcid == 0x6000e).then_some((*sid, *id)))
828 })
829 .unwrap();
830 for variant in 0..10 {
831 let fixture = write_revision(&source, sid, |raw| {
832 let mut target = PropertyObject::from_object(&raw.objects[&text])?;
833 target.bytes = properties(&[
834 (0x1c001c22, crate::create::string("abcdef")),
835 (0x14001d7a, vec![0; 4]),
836 (0x1c001c0a, crate::create::string("Georgia")),
837 (0x88001c05, Vec::new()),
838 ])?;
839 match variant {
840 1 | 4 | 5 | 6 => {
841 let flag = match variant {
842 1 => 0x88001e16,
843 4 => 0x88001e14,
844 5 => 0x88003401,
845 _ => 0x88001e22,
846 };
847 target.set(&[(flag, &[])])?;
848 }
849 2 => {
850 let author = raw
851 .objects
852 .iter()
853 .find_map(|(id, o)| (o.jcid == 0x120001).then_some(*id))
854 .unwrap();
855 let reference = target.reference(author)?;
856 target.set(&[(0x24003458, &reference)])?;
857 }
858 3 | 7 | 8 | 9 => {
859 if variant != 3 {
860 let ends = if variant == 9 { [2_u32, 2] } else { [2, 4] };
861 target.set(&[(
862 0x1c001e12,
863 &ends
864 .into_iter()
865 .flat_map(u32::to_le_bytes)
866 .collect::<Vec<_>>(),
867 )])?;
868 }
869 let parsed = PropertySets::parse(&target.bytes)?;
870 let at = parsed.root_ids.as_ptr().addr() - target.bytes.as_ptr().addr();
871 let count = parsed.sets[0].len();
872 let end = target.bytes.len() - parsed.padding.len();
873 let mut bytes = target.bytes[..end].to_vec();
874 bytes[at - 2..at].copy_from_slice(&((count + 1) as u16).to_le_bytes());
875 bytes.splice(at + count * 4..at + count * 4, 0x40003499_u32.to_le_bytes());
876 let runs: u32 = if variant == 3 { 1 } else { 3 };
877 bytes.extend_from_slice(&runs.to_le_bytes());
878 bytes.extend_from_slice(&0x44001234_u32.to_le_bytes());
879 for run in 0..runs {
880 let populated = (variant == 3 || variant == 8) && run == runs - 1;
881 bytes.extend_from_slice(&u16::from(populated).to_le_bytes());
882 if populated {
883 bytes.extend_from_slice(&0x14001234_u32.to_le_bytes());
884 bytes.extend_from_slice(&0xdeadbeef_u32.to_le_bytes());
885 }
886 }
887 bytes.resize(bytes.len().next_multiple_of(8), 0);
888 target.bytes = bytes;
889 }
890 _ => {}
891 }
892 Ok(BTreeMap::from([(text, target)]))
893 })
894 .unwrap();
895 let text_edit = |range, with: &str| {
896 let with = with.to_owned();
897 edited(&fixture, sid, vec![PageOp::Text { text, range, with }])
898 };
899 let bold = |range| {
900 edited(
901 &fixture,
902 sid,
903 vec![PageOp::Format {
904 text,
905 range,
906 set: vec![TextAttribute::Bold(true)],
907 clear: Vec::new(),
908 }],
909 )
910 };
911 if variant == 9 {
912 assert!(text_edit(0..0, "x").is_err());
913 continue;
914 }
915 if variant >= 7 {
916 let edit = text_edit(1..3, "longer").unwrap();
917 let [before, after] = [&fixture, &edit].map(|bytes| {
918 let store = Store::parse(bytes).unwrap();
919 let index = RevisionIndex::parse(&store).unwrap();
920 let document = Document::parse(&index).unwrap();
921 let space = &document.spaces[&sid];
922 let view = space.active().unwrap();
923 let Kind::RichText { runs, .. } = &view.nodes[&text].kind else {
924 panic!()
925 };
926 assert_eq!(runs.len(), 3);
927 format!("{:?}", view.nodes[&text].extra)
928 });
929 assert_eq!(before, after);
930 assert!(text_edit(0..4, "x").is_err());
931 assert_eq!(text_edit(3..5, "x").is_ok(), variant == 7);
932 continue;
933 }
934 // Hidden and hyperlink runs are ordinary text with flags; associated data,
935 // equations and embedded objects are not.
936 assert_eq!(text_edit(1..5, "x").is_ok(), matches!(variant, 0 | 1 | 4));
937 let edit = bold(0..6);
938 if matches!(variant, 2 | 5 | 6) {
939 assert!(edit.is_err());
940 continue;
941 }
942 let edit = edit.unwrap();
943 let store = Store::parse(&edit).unwrap();
944 let index = RevisionIndex::parse(&store).unwrap();
945 let doc = Document::parse(&index).unwrap();
946 let s = &doc.spaces[&sid];
947 let view = s.active().unwrap();
948 let runs = view.text_runs(text).unwrap();
949 assert_eq!(runs.len(), 1);
950 assert_eq!(runs[0].format.font.as_deref(), Some("Georgia"));
951 assert_eq!(runs[0].format.italic, Some(true));
952 assert_eq!(runs[0].format.bold, Some(true));
953 if variant == 3 {
954 let Kind::RichText { runs, .. } = &view.nodes[&text].kind else {
955 panic!()
956 };
957 let data = &view.nodes[&text].extra[runs[0].extra_set.unwrap()];
958 assert_eq!(data.len(), 1);
959 assert_eq!(data[0].id, 0x14001234);
960 assert!(bold(1..3).is_err());
961 let previous = Store::parse(&fixture).unwrap();
962 let previous = RevisionIndex::parse(&previous).unwrap();
963 let previous_doc = Document::parse(&previous).unwrap();
964 let s = &previous_doc.spaces[&sid];
965 let previous_view = s.active().unwrap();
966 assert_eq!(
967 format!("{:?}", view.nodes[&text].extra),
968 format!("{:?}", previous_view.nodes[&text].extra)
969 );
970 }
971 }
972}