| author | |
| committer | |
| log | e9d0ff1435aa931d38788bf676968c992e370104 |
| tree | deba8d70fb04411dcddb63f32d46d6253cc8f31a |
| parent | de3ecb8964829cbf26e3abd78486bc59405b2ac4 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
Account for the revision metadata author and FILETIME while keeping untouched properties, object identities and historical revisions exact. Build checkpoint fault fixtures from the actual dependency depth, and verify first attribution checkpoints once with same-second precision retained.
Assisted-by: gpt-6.1-sol7 files changed, 316 insertions(+), 27 deletions(-)
crates/onestore/src/op/tests.rs+18-6| ... | ... | @@ -2766,7 +2766,7 @@ fn a_page_date_stores_what_onenote_stores() { |
| 2766 | 2766 | .apply( |
| 2767 | 2767 | "Author", |
| 2768 | 2768 | &Edit { |
| 2769 | at: AT, | |
| 2769 | at: AT + 12345, | |
| 2770 | 2770 | ops: vec![Op::Page { space: kept, op }], |
| 2771 | 2771 | }, |
| 2772 | 2772 | ) |
| ... | ... | @@ -2777,12 +2777,24 @@ fn a_page_date_stores_what_onenote_stores() { |
| 2777 | 2777 | assert_eq!(stored.created, Some(DATED_AT)); |
| 2778 | 2778 | assert_eq!(date_fields(&stored), fields); |
| 2779 | 2779 | assert_eq!(stored, predicted); |
| 2780 | assert_eq!(changed_jcids(&image, kept), native); | |
| 2780 | let mut expected = native; | |
| 2781 | expected.extend([0x20044, 0x120001]); | |
| 2782 | expected.sort(); | |
| 2783 | assert_eq!(changed_jcids(&image, kept), expected); | |
| 2781 | 2784 | let store = Store::parse(&image).unwrap(); |
| 2782 | RevisionIndex::parse(&store) | |
| 2783 | .unwrap() | |
| 2784 | .validate_current() | |
| 2785 | .unwrap(); | |
| 2785 | let index = RevisionIndex::parse(&store).unwrap(); | |
| 2786 | index.validate_current().unwrap(); | |
| 2787 | let document = Document::parse(&index).unwrap(); | |
| 2788 | let revision = document.active(kept).unwrap(); | |
| 2789 | let metadata = &revision.nodes[&revision.roots[&4]]; | |
| 2790 | assert!(matches!( | |
| 2791 | metadata.kind, | |
| 2792 | Kind::RevisionMetadata { modified_filetime: Some(at) } if at == AT + 12345 | |
| 2793 | )); | |
| 2794 | assert!(matches!( | |
| 2795 | &revision.nodes[&metadata.latest_author.unwrap()].kind, | |
| 2796 | Kind::Author { name } if name.as_deref() == Some("Author") | |
| 2797 | )); | |
| 2786 | 2798 | |
| 2787 | 2799 | // The whole-page lowering reaches the same page through the same op. |
| 2788 | 2800 | let mut after = before.clone(); |
crates/onestore/tests/edit.rs+30-5| ... | ... | @@ -2,6 +2,8 @@ |
| 2 | 2 | mod checkpoint; |
| 3 | 3 | #[path = "support/disk.rs"] |
| 4 | 4 | mod disk; |
| 5 | #[path = "support/metadata.rs"] | |
| 6 | mod metadata; | |
| 5 | 7 | #[path = "support/ops.rs"] |
| 6 | 8 | mod ops; |
| 7 | 9 | #[path = "support/trace.rs"] |
| ... | ... | @@ -321,10 +323,24 @@ fn assert_other_objects_preserved( |
| 321 | 323 | .find(|field| field.id == 0x14001d7a) |
| 322 | 324 | .unwrap() |
| 323 | 325 | .value; |
| 326 | let filetime = metadata::check(before, after, "Author"); | |
| 327 | let author = metadata::author(&after.objects[&after.roots[&4]]); | |
| 328 | assert_eq!( | |
| 329 | *modified, | |
| 330 | onestore::Value::Bytes( | |
| 331 | &u32::try_from(filetime / 10_000_000 - 11_960_006_400) | |
| 332 | .unwrap() | |
| 333 | .to_le_bytes() | |
| 334 | ) | |
| 335 | ); | |
| 324 | 336 | let reachable = before.reachable().unwrap(); |
| 325 | assert_eq!(before.objects.len(), after.objects.len()); | |
| 337 | assert!( | |
| 338 | after.objects.keys().all(|id| { | |
| 339 | before.objects.contains_key(id) || *id == after.roots[&4] || *id == author | |
| 340 | }) | |
| 341 | ); | |
| 326 | 342 | for (id, object) in &before.objects { |
| 327 | if *id == edited { | |
| 343 | if *id == edited || Some(id) == before.roots.get(&4) { | |
| 328 | 344 | continue; |
| 329 | 345 | } |
| 330 | 346 | let current = &after.objects[id]; |
| ... | ... | @@ -334,7 +350,8 @@ fn assert_other_objects_preserved( |
| 334 | 350 | current |
| 335 | 351 | .global_ids |
| 336 | 352 | .iter() |
| 337 | .all(|(entry, guid)| object.global_ids.get(entry) == Some(guid)) | |
| 353 | .all(|(entry, guid)| object.global_ids.get(entry) == Some(guid) | |
| 354 | || *guid == author.guid) | |
| 338 | 355 | ); |
| 339 | 356 | let mut pending = vec![*id]; |
| 340 | 357 | let mut visited = std::collections::BTreeSet::new(); |
| ... | ... | @@ -349,6 +366,10 @@ fn assert_other_objects_preserved( |
| 349 | 366 | if ancestor { |
| 350 | 367 | allowed.push(0x14001d7a); |
| 351 | 368 | } |
| 369 | if ancestor && object.jcid == 0x6000d { | |
| 370 | allowed.push(0x20001d79); | |
| 371 | assert_eq!(metadata::author(current), author); | |
| 372 | } | |
| 352 | 373 | if ancestor && object.jcid == 0x6000b { |
| 353 | 374 | allowed.push(0x1c001d3c); |
| 354 | 375 | } |
| ... | ... | @@ -368,7 +389,11 @@ fn assert_other_objects_preserved( |
| 368 | 389 | if ancestor { |
| 369 | 390 | let old_time = old.sets[0].iter().find(|field| field.id == 0x14001d7a); |
| 370 | 391 | let new_time = new.sets[0].iter().find(|field| field.id == 0x14001d7a); |
| 371 | assert_eq!(old_time.is_some(), new_time.is_some()); | |
| 392 | if object.jcid == 0x6000d { | |
| 393 | assert!(new_time.is_some()); | |
| 394 | } else { | |
| 395 | assert_eq!(old_time.is_some(), new_time.is_some()); | |
| 396 | } | |
| 372 | 397 | if let Some(time) = new_time { |
| 373 | 398 | assert_eq!(&time.value, modified); |
| 374 | 399 | } |
| ... | ... | @@ -413,10 +438,10 @@ fn text_splices_preserve_formats_objects_and_history_in_one_transaction() { |
| 413 | 438 | index.spaces[space_id].labels[&(ExGuid::default(), 1)], |
| 414 | 439 | ) |
| 415 | 440 | .unwrap(); |
| 416 | assert_eq!(previous.objects.len(), current.objects.len()); | |
| 417 | 441 | if *space_id == sid { |
| 418 | 442 | assert_other_objects_preserved(&previous, &current, oid); |
| 419 | 443 | } else { |
| 444 | assert_eq!(previous.objects.len(), current.objects.len()); | |
| 420 | 445 | for (id, object) in previous.objects { |
| 421 | 446 | assert_eq!(object.data, current.objects[&id].data); |
| 422 | 447 | } |
crates/onestore/tests/insertion.rs+60-1| ... | ... | @@ -4,6 +4,8 @@ mod checkpoint; |
| 4 | 4 | mod current; |
| 5 | 5 | #[path = "support/disk.rs"] |
| 6 | 6 | mod disk; |
| 7 | #[path = "support/metadata.rs"] | |
| 8 | mod metadata; | |
| 7 | 9 | #[path = "support/ops.rs"] |
| 8 | 10 | mod ops; |
| 9 | 11 | #[path = "support/sweep.rs"] |
| ... | ... | @@ -415,6 +417,60 @@ fn paragraph_and_outline_insertions_publish_metadata_and_references_together() { |
| 415 | 417 | } |
| 416 | 418 | } |
| 417 | 419 | |
| 420 | #[test] | |
| 421 | fn first_revision_metadata_checkpoints_once_and_keeps_history() { | |
| 422 | let source = onestore::create_section("metadata.one", "Original", "Same author").unwrap(); | |
| 423 | let (sid, _, _, _, text) = targets(&source); | |
| 424 | let store = Store::parse(&source).unwrap(); | |
| 425 | let index = RevisionIndex::parse(&store).unwrap(); | |
| 426 | let original = index.resolve_active(sid).unwrap(); | |
| 427 | assert!(!original.roots.contains_key(&4)); | |
| 428 | let arena = onestore::Arena::default(); | |
| 429 | let mut section = onestore::Section::open(&arena, source.clone()).unwrap(); | |
| 430 | let at = 134_000_000_000_012_345; | |
| 431 | let mut previous = None; | |
| 432 | for at in [at, at + 1] { | |
| 433 | section | |
| 434 | .apply( | |
| 435 | "Same author", | |
| 436 | &Edit { | |
| 437 | at, | |
| 438 | ops: vec![Op::Page { | |
| 439 | space: sid, | |
| 440 | op: PageOp::Text { | |
| 441 | text, | |
| 442 | range: 0..0, | |
| 443 | with: "x".into(), | |
| 444 | }, | |
| 445 | }], | |
| 446 | }, | |
| 447 | ) | |
| 448 | .unwrap(); | |
| 449 | section.seal().unwrap().unwrap(); | |
| 450 | let image = section.image(); | |
| 451 | let current_store = Store::parse(&image).unwrap(); | |
| 452 | let current = RevisionIndex::parse(&current_store).unwrap(); | |
| 453 | current.validate_current().unwrap(); | |
| 454 | let rid = current.active(sid).unwrap(); | |
| 455 | assert_eq!(current.spaces[&sid].revisions[&rid].dependency, previous); | |
| 456 | let revision = current.resolve_active(sid).unwrap(); | |
| 457 | assert_eq!(metadata::check(&original, &revision, "Same author"), at); | |
| 458 | assert!( | |
| 459 | revision | |
| 460 | .objects | |
| 461 | .keys() | |
| 462 | .all(|id| original.objects.contains_key(id) || *id == revision.roots[&4]) | |
| 463 | ); | |
| 464 | for old in index.spaces[&sid].revisions.keys() { | |
| 465 | assert_eq!( | |
| 466 | format!("{:?}", index.resolve(sid, *old).unwrap()), | |
| 467 | format!("{:?}", current.resolve(sid, *old).unwrap()) | |
| 468 | ); | |
| 469 | } | |
| 470 | previous = Some(rid); | |
| 471 | } | |
| 472 | } | |
| 473 | ||
| 418 | 474 | #[test] |
| 419 | 475 | fn repeated_insertions_and_formatting_share_immutable_objects() { |
| 420 | 476 | let mut source = onestore::create_section("shared.one", "Original", "Same author").unwrap(); |
| ... | ... | @@ -468,7 +524,10 @@ fn repeated_insertions_and_formatting_share_immutable_objects() { |
| 468 | 524 | counts.push((object.jcid, object.reference_count)); |
| 469 | 525 | } |
| 470 | 526 | counts.sort(); |
| 471 | assert_eq!(counts, [(0x120001, 26), (0x12004d, 12), (0x12004d, 25)]); | |
| 527 | metadata::check(&raw, &raw, "Same author"); | |
| 528 | let author = metadata::author(&raw.objects[&raw.roots[&4]]); | |
| 529 | assert_eq!(raw.objects[&author].jcid, 0x120001); | |
| 530 | assert_eq!(counts, [(0x120001, 27), (0x12004d, 12), (0x12004d, 25)]); | |
| 472 | 531 | let document = Document::parse(&index).unwrap(); |
| 473 | 532 | let space = &document.spaces[&sid]; |
| 474 | 533 | let view = &space.revisions[&space.contexts[&ExGuid::default()]]; |
crates/onestore/tests/page_movement.rs+56-14| ... | ... | @@ -11,6 +11,8 @@ use onestore::{ |
| 11 | 11 | mod current; |
| 12 | 12 | #[path = "support/disk.rs"] |
| 13 | 13 | mod disk; |
| 14 | #[path = "support/metadata.rs"] | |
| 15 | mod metadata; | |
| 14 | 16 | #[path = "support/sweep.rs"] |
| 15 | 17 | mod sweep; |
| 16 | 18 | |
| ... | ... | @@ -91,14 +93,33 @@ fn verify(before: &[u8], after: &[u8], native: &[u8]) { |
| 91 | 93 | let new = indexes[1] |
| 92 | 94 | .resolve(*sid, indexes[1].spaces[sid].labels[&(ExGuid::default(), 1)]) |
| 93 | 95 | .unwrap(); |
| 94 | assert_eq!(old.roots, new.roots); | |
| 95 | assert_eq!( | |
| 96 | old.objects.keys().collect::<Vec<_>>(), | |
| 97 | new.objects.keys().collect::<Vec<_>>() | |
| 98 | ); | |
| 96 | let changed = old.objects[&old.roots[&2]].data != new.objects[&new.roots[&2]].data; | |
| 97 | let mut authors = std::collections::BTreeSet::new(); | |
| 98 | if changed { | |
| 99 | metadata::check(&old, &new, "Author"); | |
| 100 | authors.insert(metadata::author(&new.objects[&new.roots[&4]])); | |
| 101 | if let Some(id) = old.roots.get(&4) { | |
| 102 | authors.extend(old.objects[id].references().unwrap().objects); | |
| 103 | } | |
| 104 | assert!(new.objects.keys().all(|id| { | |
| 105 | old.objects.contains_key(id) || *id == new.roots[&4] || authors.contains(id) | |
| 106 | })); | |
| 107 | } else { | |
| 108 | assert_eq!(old.roots, new.roots); | |
| 109 | assert_eq!( | |
| 110 | old.objects.keys().collect::<Vec<_>>(), | |
| 111 | new.objects.keys().collect::<Vec<_>>() | |
| 112 | ); | |
| 113 | } | |
| 99 | 114 | for (id, object) in &old.objects { |
| 100 | if *id != old.roots[&2] { | |
| 101 | assert_eq!(format!("{object:?}"), format!("{:?}", new.objects[id])); | |
| 115 | if *id != old.roots[&2] && !(changed && Some(id) == old.roots.get(&4)) { | |
| 116 | let current = &new.objects[id]; | |
| 117 | assert_eq!(object.data, current.data); | |
| 118 | assert_eq!(object.jcid, current.jcid); | |
| 119 | assert_eq!(object.global_ids, current.global_ids); | |
| 120 | if !authors.contains(id) { | |
| 121 | assert_eq!(object.reference_count, current.reference_count); | |
| 122 | } | |
| 102 | 123 | } |
| 103 | 124 | } |
| 104 | 125 | } |
| ... | ... | @@ -383,13 +404,34 @@ fn metadata_copy_order_does_not_change_page_identity() { |
| 383 | 404 | let stores = [edited.as_bytes(), expected.as_slice()].map(|b| Store::parse(b).unwrap()); |
| 384 | 405 | let indexes = stores.each_ref().map(|s| RevisionIndex::parse(s).unwrap()); |
| 385 | 406 | let documents = indexes.each_ref().map(|i| Document::parse(i).unwrap()); |
| 386 | for sid in documents[0].spaces.keys() { | |
| 387 | let nodes = documents.each_ref().map(|d| { | |
| 388 | let space = &d.spaces[sid]; | |
| 389 | serde_json::to_value(&space.revisions[&space.contexts[&ExGuid::default()]].nodes) | |
| 390 | .unwrap() | |
| 391 | }); | |
| 392 | assert_eq!(nodes[0], nodes[1], "{sid}"); | |
| 407 | for space_id in documents[0].spaces.keys() { | |
| 408 | let views = documents | |
| 409 | .each_ref() | |
| 410 | .map(|d| d.spaces[space_id].active().unwrap()); | |
| 411 | let mut nodes = views.map(|view| view.nodes.clone()); | |
| 412 | if *space_id == sid { | |
| 413 | let raw = indexes.each_ref().map(|index| { | |
| 414 | index | |
| 415 | .resolve( | |
| 416 | *space_id, | |
| 417 | index.spaces[space_id].labels[&(ExGuid::default(), 1)], | |
| 418 | ) | |
| 419 | .unwrap() | |
| 420 | }); | |
| 421 | metadata::check(&raw[1], &raw[0], "Author"); | |
| 422 | let author = metadata::author(&raw[0].objects[&raw[0].roots[&4]]); | |
| 423 | if !nodes[1].contains_key(&author) { | |
| 424 | nodes[0].remove(&author).unwrap(); | |
| 425 | } | |
| 426 | for (nodes, view) in nodes.iter_mut().zip(views) { | |
| 427 | nodes.remove(&view.roots[&4]).unwrap(); | |
| 428 | } | |
| 429 | } | |
| 430 | assert_eq!( | |
| 431 | serde_json::to_value(&nodes[0]).unwrap(), | |
| 432 | serde_json::to_value(&nodes[1]).unwrap(), | |
| 433 | "{space_id}" | |
| 434 | ); | |
| 393 | 435 | } |
| 394 | 436 | } |
| 395 | 437 |
crates/onestore/tests/paragraph.rs+28| ... | ... | @@ -12,6 +12,8 @@ mod checkpoint; |
| 12 | 12 | mod current; |
| 13 | 13 | #[path = "support/disk.rs"] |
| 14 | 14 | mod disk; |
| 15 | #[path = "support/metadata.rs"] | |
| 16 | mod metadata; | |
| 15 | 17 | #[path = "support/ops.rs"] |
| 16 | 18 | mod ops; |
| 17 | 19 | |
| ... | ... | @@ -220,6 +222,22 @@ fn joins_match_native_graphs_tags_and_inherited_character_styles() { |
| 220 | 222 | .unwrap(); |
| 221 | 223 | let current_rid = current_document.spaces[sid].contexts[&ExGuid::default()]; |
| 222 | 224 | let new_raw = current_index.resolve(*sid, current_rid).unwrap(); |
| 225 | let modified = metadata::check(&old_raw, &new_raw, "Author"); | |
| 226 | let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]); | |
| 227 | changed.extend(old_raw.roots.get(&4)); | |
| 228 | for id in &changed { | |
| 229 | if current | |
| 230 | .nodes | |
| 231 | .get(id) | |
| 232 | .is_some_and(|node| matches!(node.kind, Kind::Paragraph { .. })) | |
| 233 | { | |
| 234 | assert_eq!(metadata::author(&new_raw.objects[id]), author); | |
| 235 | assert_eq!( | |
| 236 | current.nodes[id].modified.map(u64::from), | |
| 237 | Some(modified / 10_000_000 - 11_960_006_400) | |
| 238 | ); | |
| 239 | } | |
| 240 | } | |
| 223 | 241 | for (id, object) in old_raw.objects { |
| 224 | 242 | if !changed.contains(&id) { |
| 225 | 243 | assert_eq!( |
| ... | ... | @@ -409,6 +427,16 @@ fn splits_partition_native_paragraphs_at_every_scalar_boundary() { |
| 409 | 427 | let new_raw = current_index |
| 410 | 428 | .resolve(*sid, current_space.contexts[&ExGuid::default()]) |
| 411 | 429 | .unwrap(); |
| 430 | let modified = metadata::check(&old_raw, &new_raw, "Author"); | |
| 431 | let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]); | |
| 432 | changed.extend(old_raw.roots.get(&4)); | |
| 433 | for id in [paragraph, new_paragraph] { | |
| 434 | assert_eq!(metadata::author(&new_raw.objects[&id]), author); | |
| 435 | assert_eq!( | |
| 436 | after.nodes[&id].modified.map(u64::from), | |
| 437 | Some(modified / 10_000_000 - 11_960_006_400) | |
| 438 | ); | |
| 439 | } | |
| 412 | 440 | for (id, object) in old_raw.objects { |
| 413 | 441 | if !changed.contains(&id) { |
| 414 | 442 | assert_eq!(object.data, new_raw.objects[&id].data, "untouched {id}"); |
crates/onestore/tests/support/checkpoint.rs+5-1| ... | ... | @@ -26,7 +26,11 @@ pub fn pending(source: &[u8], space: ExGuid, text: ExGuid) -> Vec<u8> { |
| 26 | 26 | with: with.into(), |
| 27 | 27 | }, |
| 28 | 28 | }; |
| 29 | for at in 0..512 - depth(source) as u64 { | |
| 29 | for at in 0..=512 { | |
| 30 | let image = section.image(); | |
| 31 | if depth(&image) == 512 { | |
| 32 | return image; | |
| 33 | } | |
| 30 | 34 | let ops = vec![retouch(0..0, "x"), retouch(0..1, "")]; |
| 31 | 35 | let at = 134_000_000_000_000_000 + at * 10_000_000; |
| 32 | 36 | section.apply("Author", &Edit { at, ops }).unwrap(); |
crates/onestore/tests/support/metadata.rs created+119| ... | ... | @@ -0,0 +1,119 @@ |
| 1 | use onestore::{ExGuid, IdStream, Object, ObjectData, PropertySets, ResolvedRevision, Value}; | |
| 2 | ||
| 3 | pub fn author(object: &Object<'_>) -> ExGuid { | |
| 4 | let ObjectData::Properties(bytes) = object.data else { | |
| 5 | panic!("AuthorMostRecent needs a property object") | |
| 6 | }; | |
| 7 | let fields = PropertySets::parse(bytes).unwrap(); | |
| 8 | let Value::References { | |
| 9 | stream: IdStream::Objects, | |
| 10 | compact_ids, | |
| 11 | } = fields.sets[0] | |
| 12 | .iter() | |
| 13 | .find(|field| field.id == 0x20001d79) | |
| 14 | .unwrap() | |
| 15 | .value | |
| 16 | else { | |
| 17 | panic!("AuthorMostRecent needs one object reference") | |
| 18 | }; | |
| 19 | let compact = u32::from_le_bytes(compact_ids.try_into().unwrap()); | |
| 20 | ExGuid { | |
| 21 | guid: object.global_ids[&(compact >> 8)], | |
| 22 | n: compact & 255, | |
| 23 | } | |
| 24 | } | |
| 25 | ||
| 26 | /// Checks the revision context's author/time while preserving every other metadata field. | |
| 27 | pub fn check(before: &ResolvedRevision<'_>, after: &ResolvedRevision<'_>, name: &str) -> u64 { | |
| 28 | let roots = |revision: &ResolvedRevision<'_>| { | |
| 29 | revision | |
| 30 | .roots | |
| 31 | .iter() | |
| 32 | .filter(|(kind, _)| **kind != 4) | |
| 33 | .map(|(kind, id)| (*kind, *id)) | |
| 34 | .collect::<Vec<_>>() | |
| 35 | }; | |
| 36 | assert_eq!(roots(before), roots(after)); | |
| 37 | let id = after.roots[&4]; | |
| 38 | let metadata = &after.objects[&id]; | |
| 39 | assert_eq!(metadata.jcid, 0x20044); | |
| 40 | let ObjectData::Properties(bytes) = metadata.data else { | |
| 41 | panic!() | |
| 42 | }; | |
| 43 | let fields = PropertySets::parse(bytes).unwrap(); | |
| 44 | let Value::Bytes(time) = fields.sets[0] | |
| 45 | .iter() | |
| 46 | .find(|field| field.id == 0x18001d77) | |
| 47 | .unwrap() | |
| 48 | .value | |
| 49 | else { | |
| 50 | panic!() | |
| 51 | }; | |
| 52 | let filetime = u64::from_le_bytes(time.try_into().unwrap()); | |
| 53 | assert!(filetime >= 119_600_064_000_000_000); | |
| 54 | let author_id = author(metadata); | |
| 55 | let named = &after.objects[&author_id]; | |
| 56 | assert_eq!(named.jcid, 0x120001); | |
| 57 | let ObjectData::Properties(bytes) = named.data else { | |
| 58 | panic!() | |
| 59 | }; | |
| 60 | let named_fields = PropertySets::parse(bytes).unwrap(); | |
| 61 | let Value::Bytes(stored) = named_fields.sets[0] | |
| 62 | .iter() | |
| 63 | .find(|field| field.id == 0x1c001d75) | |
| 64 | .unwrap() | |
| 65 | .value | |
| 66 | else { | |
| 67 | panic!() | |
| 68 | }; | |
| 69 | let expected: Vec<_> = name | |
| 70 | .encode_utf16() | |
| 71 | .chain([0]) | |
| 72 | .flat_map(u16::to_le_bytes) | |
| 73 | .collect(); | |
| 74 | assert_eq!(stored, expected); | |
| 75 | if let Some(old) = before.objects.get(&author_id) { | |
| 76 | assert_eq!(old.data, named.data); | |
| 77 | } | |
| 78 | if let Some(old_id) = before.roots.get(&4) { | |
| 79 | assert_eq!(*old_id, id); | |
| 80 | let old = &before.objects[old_id]; | |
| 81 | assert_eq!(old.jcid, metadata.jcid); | |
| 82 | assert!(metadata.global_ids.iter().all(|(entry, guid)| { | |
| 83 | old.global_ids.get(entry) == Some(guid) || *guid == author_id.guid | |
| 84 | })); | |
| 85 | let ObjectData::Properties(old_bytes) = old.data else { | |
| 86 | panic!() | |
| 87 | }; | |
| 88 | let mut old_fields = PropertySets::parse(old_bytes).unwrap(); | |
| 89 | let mut new = fields; | |
| 90 | for fields in [&mut old_fields, &mut new] { | |
| 91 | fields.sets[0].retain(|field| ![0x18001d77, 0x20001d79].contains(&field.id)); | |
| 92 | } | |
| 93 | assert_eq!(old_fields.sets, new.sets); | |
| 94 | for field in old_fields.sets.iter().flatten() { | |
| 95 | if let Value::References { compact_ids, .. } = field.value { | |
| 96 | let ids = compact_ids.chunks_exact(4); | |
| 97 | assert!(ids.remainder().is_empty()); | |
| 98 | for compact in ids { | |
| 99 | let entry = u32::from_le_bytes(compact.try_into().unwrap()) >> 8; | |
| 100 | assert_eq!( | |
| 101 | metadata.global_ids.get(&entry), | |
| 102 | Some(&old.global_ids[&entry]) | |
| 103 | ); | |
| 104 | } | |
| 105 | } | |
| 106 | } | |
| 107 | } else { | |
| 108 | assert!(!before.objects.contains_key(&id)); | |
| 109 | assert_eq!(fields.sets.len(), 1); | |
| 110 | assert_eq!( | |
| 111 | fields.sets[0] | |
| 112 | .iter() | |
| 113 | .map(|field| field.id) | |
| 114 | .collect::<Vec<_>>(), | |
| 115 | [0x18001d77, 0x20001d79] | |
| 116 | ); | |
| 117 | } | |
| 118 | filetime | |
| 119 | } |