| 1 | use super::patch_properties; |
| 2 | use crate::{PropertySets, Value, create::properties}; |
| 3 | |
| 4 | mod multi; |
| 5 | |
| 6 | #[test] |
| 7 | fn 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] |
| 73 | fn 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 | |
| 186 | fn 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 | |
| 300 | fn 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] |
| 369 | fn 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"] |
| 413 | fn 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] |
| 425 | fn 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] |
| 471 | fn 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] |
| 531 | fn 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] |
| 608 | fn 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] |
| 754 | fn 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] |
| 792 | fn 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] |
| 807 | fn 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 | } |