diff --git a/corpus/paragraph-edit/README.md b/corpus/paragraph-edit/README.md index 14333fa67ce761a4c93017134f400497e767249c..a79e9aa4b4ff5b1ccd19c5624b398bc504a4e85d 100644 --- a/corpus/paragraph-edit/README.md +++ b/corpus/paragraph-edit/README.md @@ -26,6 +26,14 @@ notebook before completing inspection. Cold-open the split notebook for the join pass. Captured COM IDs belong to their originating sessions; discover fresh IDs when regenerating the corpus. -`tools/test_paragraph_edit.py` verifies these native controls without a VM. They -establish application behavior; they do not advertise a library split/join API. +`rust-split` retains twelve `ParagraphSplit` outputs, their cold native captures, +and a second native session typing into four empty boundary paragraphs. The +hyperlink case stays unchanged because this operation rejects fields. The Rust +test `export_native_paragraph_splits` generates candidate notebooks; its output +directory comes from `ONESTORE_PARAGRAPH_OUTPUT`. Native character/style comparisons +use both the keyboard-generated controls and the Rust-written notebooks. Empty +typing checks extend the preexisting empty run in an independent expected model. + +`tools/test_paragraph_edit.py` verifies these controls without a VM. The native +join captures establish behavior for subsequent join implementation. Identical captured files link to one canonical copy within this corpus. diff --git a/corpus/paragraph-edit/rust-split/candidate/synthetic.one b/corpus/paragraph-edit/rust-split/candidate/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..b531feed7f641011262d6bc9eafaad31bdb863e9 Binary files /dev/null and b/corpus/paragraph-edit/rust-split/candidate/synthetic.one differ diff --git a/corpus/paragraph-edit/rust-split/manifest.json b/corpus/paragraph-edit/rust-split/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..22e5a0ebff3be8f6c8d1969b8ce4e45311969948 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/manifest.json @@ -0,0 +1,352 @@ +{ + "cases": [ + { + "case": "Split middle", + "intent": { + "author": "Rust split author", + "created": 1473324660, + "guid": [ + 228, + 60, + 65, + 246, + 194, + 244, + 184, + 76, + 170, + 5, + 19, + 130, + 32, + 122, + 148, + 241 + ], + "offset": 2, + "text": "{DFE950EF-6BEB-0F5B-2403-7B74F1AE5002},44" + }, + "new_paragraph": "{F6413CE4-F4C2-4CB8-AA05-1382207A94F1},1", + "new_text": "{F6413CE4-F4C2-4CB8-AA05-1382207A94F1},2" + }, + { + "case": "Split style boundary", + "intent": { + "author": "Rust split author", + "created": 1473324660, + "guid": [ + 163, + 91, + 197, + 73, + 195, + 225, + 53, + 68, + 140, + 152, + 83, + 168, + 172, + 124, + 226, + 213 + ], + "offset": 4, + "text": "{5FB484E2-1E3E-0A9D-1989-271A3EBBEC42},44" + }, + "new_paragraph": "{49C55BA3-E1C3-4435-8C98-53A8AC7CE2D5},1", + "new_text": "{49C55BA3-E1C3-4435-8C98-53A8AC7CE2D5},2" + }, + { + "case": "Split start", + "intent": { + "author": "Rust split author", + "created": 1473324660, + "guid": [ + 135, + 16, + 214, + 248, + 73, + 35, + 2, + 69, + 148, + 213, + 125, + 28, + 43, + 124, + 81, + 35 + ], + "offset": 0, + "text": "{012ED94B-43B3-079A-3050-B1CBE25C1C09},44" + }, + "new_paragraph": "{F8D61087-2349-4502-94D5-7D1C2B7C5123},1", + "new_text": "{F8D61087-2349-4502-94D5-7D1C2B7C5123},2" + }, + { + "case": "Split end", + "intent": { + "author": "Rust split author", + "created": 1473324660, + "guid": [ + 37, + 220, + 149, + 138, + 85, + 3, + 106, + 69, + 168, + 172, + 49, + 82, + 247, + 99, + 160, + 139 + ], + "offset": 22, + "text": "{DAF723A3-C2A3-0798-2EAB-34613166A794},44" + }, + "new_paragraph": "{8A95DC25-0355-456A-A8AC-3152F763A08B},1", + "new_text": "{8A95DC25-0355-456A-A8AC-3152F763A08B},2" + }, + { + "case": "Split empty", + "intent": { + "author": "Rust split author", + "created": 1473324660, + "guid": [ + 232, + 204, + 91, + 132, + 180, + 27, + 239, + 79, + 158, + 115, + 213, + 165, + 40, + 35, + 189, + 93 + ], + "offset": 0, + "text": "{0B467F31-D287-08CD-09F4-A6FBB2F5418F},45" + }, + "new_paragraph": "{845BCCE8-1BB4-4FEF-9E73-D5A52823BD5D},1", + "new_text": "{845BCCE8-1BB4-4FEF-9E73-D5A52823BD5D},2" + }, + { + "case": "Split parent", + "intent": { + "author": "Rust split author", + "created": 1473324660, + "guid": [ + 54, + 186, + 114, + 171, + 44, + 189, + 95, + 71, + 135, + 190, + 71, + 95, + 23, + 246, + 37, + 157 + ], + "offset": 2, + "text": "{3860EA13-D444-0598-35D6-CF96769C6A21},44" + }, + "new_paragraph": "{AB72BA36-BD2C-475F-87BE-475F17F6259D},1", + "new_text": "{AB72BA36-BD2C-475F-87BE-475F17F6259D},2" + }, + { + "case": "Split child", + "intent": { + "author": "Rust split author", + "created": 1473324660, + "guid": [ + 160, + 247, + 235, + 152, + 30, + 98, + 166, + 77, + 157, + 204, + 141, + 45, + 114, + 151, + 225, + 195 + ], + "offset": 2, + "text": "{B587CBC6-0BCC-0A23-16F6-E1B0B3C5D061},49" + }, + "new_paragraph": "{98EBF7A0-621E-4DA6-9DCC-8D2D7297E1C3},1", + "new_text": "{98EBF7A0-621E-4DA6-9DCC-8D2D7297E1C3},2" + }, + { + "case": "Split bullet", + "intent": { + "author": "Rust split author", + "created": 1473324660, + "guid": [ + 53, + 245, + 214, + 76, + 159, + 56, + 134, + 76, + 178, + 90, + 131, + 182, + 74, + 114, + 31, + 24 + ], + "offset": 2, + "text": "{8C65E427-477C-02ED-1807-2F10E277019D},44" + }, + "new_paragraph": "{4CD6F535-389F-4C86-B25A-83B64A721F18},1", + "new_text": "{4CD6F535-389F-4C86-B25A-83B64A721F18},2" + }, + { + "case": "Split number", + "intent": { + "author": "Rust split author", + "created": 1473324661, + "guid": [ + 196, + 133, + 251, + 251, + 193, + 175, + 8, + 65, + 175, + 180, + 126, + 16, + 31, + 185, + 237, + 66 + ], + "offset": 2, + "text": "{D4ABBE4F-2165-0122-1F9F-ACE3FE92C703},44" + }, + "new_paragraph": "{FBFB85C4-AFC1-4108-AFB4-7E101FB9ED42},1", + "new_text": "{FBFB85C4-AFC1-4108-AFB4-7E101FB9ED42},2" + }, + { + "case": "Split tag", + "intent": { + "author": "Rust split author", + "created": 1473324661, + "guid": [ + 40, + 122, + 110, + 77, + 185, + 255, + 122, + 69, + 128, + 15, + 14, + 166, + 54, + 156, + 154, + 91 + ], + "offset": 2, + "text": "{37AA5877-2980-0227-169F-8B1E8EE0795A},44" + }, + "new_paragraph": "{4D6E7A28-FFB9-457A-800F-0EA6369C9A5B},1", + "new_text": "{4D6E7A28-FFB9-457A-800F-0EA6369C9A5B},2" + }, + { + "case": "Split cell", + "intent": { + "author": "Rust split author", + "created": 1473324661, + "guid": [ + 150, + 222, + 89, + 134, + 80, + 167, + 167, + 67, + 179, + 23, + 93, + 176, + 27, + 184, + 119, + 185 + ], + "offset": 2, + "text": "{F0832BC9-1578-0A96-08AF-99344644FFB8},50" + }, + "new_paragraph": "{8659DE96-A750-43A7-B317-5DB01BB877B9},1", + "new_text": "{8659DE96-A750-43A7-B317-5DB01BB877B9},2" + }, + { + "case": "Split soft break", + "intent": { + "author": "Rust split author", + "created": 1473324661, + "guid": [ + 199, + 62, + 94, + 81, + 29, + 157, + 236, + 77, + 159, + 34, + 22, + 137, + 163, + 57, + 215, + 79 + ], + "offset": 2, + "text": "{88189BA1-2F19-0282-05ED-A0B83153F143},44" + }, + "new_paragraph": "{515E3EC7-9D1D-4DEC-9F22-1689A339D74F},1", + "new_text": "{515E3EC7-9D1D-4DEC-9F22-1689A339D74F},2" + } + ] +} diff --git a/corpus/paragraph-edit/rust-split/native/notebook/Open Notebook.onetoc2 b/corpus/paragraph-edit/rust-split/native/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..d098f9c144e2d763a97f3afb08dd64fbf40ca2ce Binary files /dev/null and b/corpus/paragraph-edit/rust-split/native/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/paragraph-edit/rust-split/native/notebook/synthetic.one b/corpus/paragraph-edit/rust-split/native/notebook/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..d94fcd2d93f08baf60705f2f6efaf3382771ac60 Binary files /dev/null and b/corpus/paragraph-edit/rust-split/native/notebook/synthetic.one differ diff --git a/corpus/paragraph-edit/rust-split/native/read/environment.json b/corpus/paragraph-edit/rust-split/native/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..811feab20c52431eac39f4fd8a9941c2792dab1e --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-AA441F91", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/paragraph-edit/rust-split/native/read/hierarchy.xml b/corpus/paragraph-edit/rust-split/native/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..1a1fa721e275b1ad6dba521965ab3465b036cb4b --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/paragraph-edit/rust-split/native/read/page-000.xml b/corpus/paragraph-edit/rust-split/native/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..a9668c8465aaa3402ee6ed0d137a7e9392c836dd --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-000.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-001.xml b/corpus/paragraph-edit/rust-split/native/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..f17994e11b3e8151253fe9d0fb30a61ea082985c --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-001.xml @@ -0,0 +1,3 @@ + +Link label tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-002.xml b/corpus/paragraph-edit/rust-split/native/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..0bf5f878baccf05bb7b1018097bb1c70a34ac26e --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-002.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-003.xml b/corpus/paragraph-edit/rust-split/native/read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..1edfd62909024b699557b53a4160cf85a18f90f2 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-003.xml @@ -0,0 +1,5 @@ + +Bold italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-004.xml b/corpus/paragraph-edit/rust-split/native/read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..2d11ba8bb4ef29fc5d176c5965c193c71c8586c1 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-004.xml @@ -0,0 +1,20 @@ + +Bold 🦀 italic é color 東京
+End
]]>
Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/paragraph-edit/rust-split/native/read/page-005.xml b/corpus/paragraph-edit/rust-split/native/read/page-005.xml new file mode 100644 index 0000000000000000000000000000000000000000..842263609e2e64c135bb2a723336f4f123605a7a --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-005.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-006.xml b/corpus/paragraph-edit/rust-split/native/read/page-006.xml new file mode 100644 index 0000000000000000000000000000000000000000..44cff92fdb875ec70dca04e86c13cac1202bf029 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-006.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-007.xml b/corpus/paragraph-edit/rust-split/native/read/page-007.xml new file mode 100644 index 0000000000000000000000000000000000000000..d0478450f77cd743ee35fa7b24a8226a1c58da3f --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-007.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-008.xml b/corpus/paragraph-edit/rust-split/native/read/page-008.xml new file mode 100644 index 0000000000000000000000000000000000000000..2404f24838a0e02484aefc92fa9fcc210be4df32 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-008.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/paragraph-edit/rust-split/native/read/page-009.xml b/corpus/paragraph-edit/rust-split/native/read/page-009.xml new file mode 100644 index 0000000000000000000000000000000000000000..03a8e82cd85b0c700a3eabe7361f012e8281f8a1 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-009.xml @@ -0,0 +1,5 @@ + +Bold]]> italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-010.xml b/corpus/paragraph-edit/rust-split/native/read/page-010.xml new file mode 100644 index 0000000000000000000000000000000000000000..f53de11177ec872120eb26097e1bbe56479b0e33 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-010.xml @@ -0,0 +1,3 @@ + + +soft break]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-011.xml b/corpus/paragraph-edit/rust-split/native/read/page-011.xml new file mode 100644 index 0000000000000000000000000000000000000000..b4669863a60eda90fcea80955dce12ca279fb445 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-011.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-012.xml b/corpus/paragraph-edit/rust-split/native/read/page-012.xml new file mode 100644 index 0000000000000000000000000000000000000000..60c49da4442fb8967b0338bb261888c5d42f6cdb --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-012.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/page-013.xml b/corpus/paragraph-edit/rust-split/native/read/page-013.xml new file mode 100644 index 0000000000000000000000000000000000000000..e5b3cb0a2c43e8e62a5d55b50f90a9841e5f625a --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/page-013.xml @@ -0,0 +1,5 @@ + +Bold italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/native/read/payloads.json b/corpus/paragraph-edit/rust-split/native/read/payloads.json new file mode 120000 index 0000000000000000000000000000000000000000..3cf45dc1acb611d7f764e0340d73e87192f81407 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/read/payloads.json @@ -0,0 +1 @@ +../../../before/read/payloads.json \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/native/run.json b/corpus/paragraph-edit/rust-split/native/run.json new file mode 100644 index 0000000000000000000000000000000000000000..75cde082d61b20c444453d6e556ac188337f6644 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/Users/clo/dev/one/evidence/m10/paragraph-rust-split-01/candidate", + "expected_pages": 14, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/paragraph-edit/rust-split/native/source.json b/corpus/paragraph-edit/rust-split/native/source.json new file mode 100644 index 0000000000000000000000000000000000000000..8c94d98a7932d0c052fdab6a0838b23b034786b3 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/source.json @@ -0,0 +1,8 @@ +[ + { + "path": "synthetic.one", + "bytes": 110224, + "sha256": "c07cf1d31c658534b7cd3c6f77dd48dd52a1b9be4ef037b3d016b24336a4c3f4", + "mtime_ns": 1788857461138239881 + } +] diff --git a/corpus/paragraph-edit/rust-split/native/teardown.json b/corpus/paragraph-edit/rust-split/native/teardown.json new file mode 120000 index 0000000000000000000000000000000000000000..b0d2ee26322df655cbbf9e42f6c30b72a4d110a5 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/native/teardown.json @@ -0,0 +1 @@ +../../joined/teardown.json \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/environment.json b/corpus/paragraph-edit/rust-split/typed/before-read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..85febdc32b4c62e1097a8a3e8093d4a8a867ece6 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-02D586B5", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/hierarchy.xml b/corpus/paragraph-edit/rust-split/typed/before-read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..d06f27e3d2490f6ba635ca49f278b8e75251af56 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-000.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..1f527e736cdfc381990e9305fa39195e5aeee6e6 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-000.xml @@ -0,0 +1,5 @@ + +Bold italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-001.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..ee57a8f384bdb5c38bcf79afa057bb436b2c6334 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-001.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-002.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..19fdd7bc8f33e638e160766c0d86c187e88aa070 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-002.xml @@ -0,0 +1,3 @@ + +Link label tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-003.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..ecafe255e8a8c3b04adcade2b7a0556a6720c734 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-003.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-004.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..9e064f0a16b64644a69d3776f3ed051dce651d62 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-004.xml @@ -0,0 +1,20 @@ + +Bold 🦀 italic é color 東京
+End
]]>
Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-005.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-005.xml new file mode 100644 index 0000000000000000000000000000000000000000..5243351b67b5be8d005d15c516caa5bc0faefc7c --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-005.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-006.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-006.xml new file mode 100644 index 0000000000000000000000000000000000000000..4ef49e6e614a744a497f4e378556c0889ad34627 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-006.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-007.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-007.xml new file mode 100644 index 0000000000000000000000000000000000000000..377b10aa1567c2c493d94c393b4f2ec75e698665 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-007.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-008.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-008.xml new file mode 100644 index 0000000000000000000000000000000000000000..99aa56a3aa5af936111fb180369103a8ffcf4b0e --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-008.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-009.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-009.xml new file mode 100644 index 0000000000000000000000000000000000000000..997b60af44673400616150639a200ba051b04221 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-009.xml @@ -0,0 +1,5 @@ + +Bold]]> italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-010.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-010.xml new file mode 100644 index 0000000000000000000000000000000000000000..526b4eabee04951b37345a30b8271320d99c1137 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-010.xml @@ -0,0 +1,3 @@ + + +soft break]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-011.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-011.xml new file mode 100644 index 0000000000000000000000000000000000000000..d0479a4a7ba278852ec7bd0f477b32558f531b28 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-011.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-012.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-012.xml new file mode 100644 index 0000000000000000000000000000000000000000..6971a83b841b68f68153e456bc9f364da7ee1f8d --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-012.xml @@ -0,0 +1,6 @@ + +Bo]]>ld italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/page-013.xml b/corpus/paragraph-edit/rust-split/typed/before-read/page-013.xml new file mode 100644 index 0000000000000000000000000000000000000000..59689f1af558b09919de7f39bf4cd5c7fef83341 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/page-013.xml @@ -0,0 +1,5 @@ + +Bold italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/before-read/payloads.json b/corpus/paragraph-edit/rust-split/typed/before-read/payloads.json new file mode 120000 index 0000000000000000000000000000000000000000..3cf45dc1acb611d7f764e0340d73e87192f81407 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/before-read/payloads.json @@ -0,0 +1 @@ +../../../before/read/payloads.json \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/notebook/Open Notebook.onetoc2 b/corpus/paragraph-edit/rust-split/typed/notebook/Open Notebook.onetoc2 new file mode 120000 index 0000000000000000000000000000000000000000..a36d6b55613d6521df68617616a6bc584ff3bf48 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/notebook/Open Notebook.onetoc2 @@ -0,0 +1 @@ +../../native/notebook/Open Notebook.onetoc2 \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/notebook/synthetic.one b/corpus/paragraph-edit/rust-split/typed/notebook/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..a76c576ffb1abf83c1ac548f076b71077274db13 Binary files /dev/null and b/corpus/paragraph-edit/rust-split/typed/notebook/synthetic.one differ diff --git a/corpus/paragraph-edit/rust-split/typed/read/environment.json b/corpus/paragraph-edit/rust-split/typed/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..5ca67f6fb1d036ac95773ceb0f63fa4054269c43 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-02D586B5", + "cold": false, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/paragraph-edit/rust-split/typed/read/hierarchy.xml b/corpus/paragraph-edit/rust-split/typed/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..128219ee630ac2ac04188f22df8e05fe06a732ea --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-000.xml b/corpus/paragraph-edit/rust-split/typed/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..3917e00aded6a5e2fdd79d873ddf1d9b2354e5f9 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-000.xml @@ -0,0 +1,5 @@ + +Bold italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-001.xml b/corpus/paragraph-edit/rust-split/typed/read/page-001.xml new file mode 120000 index 0000000000000000000000000000000000000000..145194458110015f127fb3a46bc43b26504912ca --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-001.xml @@ -0,0 +1 @@ +../before-read/page-001.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-002.xml b/corpus/paragraph-edit/rust-split/typed/read/page-002.xml new file mode 120000 index 0000000000000000000000000000000000000000..7abfb30faff0251ae0ef47e7ecd825b352db3681 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-002.xml @@ -0,0 +1 @@ +../before-read/page-002.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-003.xml b/corpus/paragraph-edit/rust-split/typed/read/page-003.xml new file mode 120000 index 0000000000000000000000000000000000000000..39c1498839527129786dde8c7a2ea388309c5e98 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-003.xml @@ -0,0 +1 @@ +../before-read/page-003.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-004.xml b/corpus/paragraph-edit/rust-split/typed/read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..e0e354a605e0c94cc1b063cc0aec6977d7141c09 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-004.xml @@ -0,0 +1,20 @@ + +Bold 🦀 italic é color 東京
+End
]]>
Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-005.xml b/corpus/paragraph-edit/rust-split/typed/read/page-005.xml new file mode 120000 index 0000000000000000000000000000000000000000..28293914b5e97a92909e2dd758ae7ec90d626069 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-005.xml @@ -0,0 +1 @@ +../before-read/page-005.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-006.xml b/corpus/paragraph-edit/rust-split/typed/read/page-006.xml new file mode 120000 index 0000000000000000000000000000000000000000..d238f4bffd7ce07f0077616b2fd91132f21cecf5 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-006.xml @@ -0,0 +1 @@ +../before-read/page-006.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-007.xml b/corpus/paragraph-edit/rust-split/typed/read/page-007.xml new file mode 120000 index 0000000000000000000000000000000000000000..8ca327c46e02e25ad46fd0cd632c0246b69b2858 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-007.xml @@ -0,0 +1 @@ +../before-read/page-007.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-008.xml b/corpus/paragraph-edit/rust-split/typed/read/page-008.xml new file mode 100644 index 0000000000000000000000000000000000000000..cc6661a5df25bff662c77431973d37e3248542f3 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-008.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-009.xml b/corpus/paragraph-edit/rust-split/typed/read/page-009.xml new file mode 120000 index 0000000000000000000000000000000000000000..d7a70e4fc176f81797798e161c6ffaac7dd4f066 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-009.xml @@ -0,0 +1 @@ +../before-read/page-009.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-010.xml b/corpus/paragraph-edit/rust-split/typed/read/page-010.xml new file mode 120000 index 0000000000000000000000000000000000000000..037ed30fb57e17b3e19d3f9af87d476c2ff02e77 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-010.xml @@ -0,0 +1 @@ +../before-read/page-010.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-011.xml b/corpus/paragraph-edit/rust-split/typed/read/page-011.xml new file mode 120000 index 0000000000000000000000000000000000000000..5d6e25667baca93299dffa8ee65a1f002997b4b0 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-011.xml @@ -0,0 +1 @@ +../before-read/page-011.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-012.xml b/corpus/paragraph-edit/rust-split/typed/read/page-012.xml new file mode 120000 index 0000000000000000000000000000000000000000..9c9cfd64e953f4b49f8c3868dea5d3b4ba24dfc2 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-012.xml @@ -0,0 +1 @@ +../before-read/page-012.xml \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/read/page-013.xml b/corpus/paragraph-edit/rust-split/typed/read/page-013.xml new file mode 100644 index 0000000000000000000000000000000000000000..364cc0e312597602131d3875b21b9be6068f1c6d --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/page-013.xml @@ -0,0 +1,5 @@ + +Bold italic 🦀 é tail]]> diff --git a/corpus/paragraph-edit/rust-split/typed/read/payloads.json b/corpus/paragraph-edit/rust-split/typed/read/payloads.json new file mode 120000 index 0000000000000000000000000000000000000000..3cf45dc1acb611d7f764e0340d73e87192f81407 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/read/payloads.json @@ -0,0 +1 @@ +../../../before/read/payloads.json \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/run.json b/corpus/paragraph-edit/rust-split/typed/run.json new file mode 100644 index 0000000000000000000000000000000000000000..57b54ecb5b215fd335e95ac18d3ff92d09e84f99 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/Users/clo/dev/one/evidence/m10/paragraph-rust-split-01/native/notebook", + "expected_pages": 14, + "author": null, + "author_timeout_seconds": 600, + "inspect": true, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/paragraph-edit/rust-split/typed/source.json b/corpus/paragraph-edit/rust-split/typed/source.json new file mode 100644 index 0000000000000000000000000000000000000000..50ad111d8faa56e60244e599c7a1a972e49ddcb3 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 4800, + "sha256": "7c258d318aaa6089989a7beb427a820beb99ebd7833f2a76ecbbb3713df07dbb", + "mtime_ns": 1788857651415033964 + }, + { + "path": "synthetic.one", + "bytes": 110224, + "sha256": "29a64584c4d7ac72958595769854a3df199156cb89541df354d9e9f484450712", + "mtime_ns": 1788857651415524756 + } +] diff --git a/corpus/paragraph-edit/rust-split/typed/teardown.json b/corpus/paragraph-edit/rust-split/typed/teardown.json new file mode 120000 index 0000000000000000000000000000000000000000..b0d2ee26322df655cbbf9e42f6c30b72a4d110a5 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/teardown.json @@ -0,0 +1 @@ +../../joined/teardown.json \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/ui/selections.json b/corpus/paragraph-edit/rust-split/typed/ui/selections.json new file mode 100644 index 0000000000000000000000000000000000000000..604e7b9f345a4899dc0fc581056828633618a509 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/ui/selections.json @@ -0,0 +1,30 @@ +[ + { + "case": "Split start", + "index": 0, + "page": "{058E24C9-6521-4913-9088-94646869E7EC}{1}{B0}", + "object": "{7DB914B1-2E9B-4E51-B78C-87B90E14A385}{40}{B0}", + "text": "Typed " + }, + { + "case": "Split end", + "index": 1, + "page": "{EEE8E5F3-7A14-4AA5-B06A-56D4BAC7C8AF}{1}{B0}", + "object": "{F60211DF-6E7D-0CA1-2F70-07201B2B1F07}{1}{B0}", + "text": "Typed " + }, + { + "case": "Split empty", + "index": 0, + "page": "{984F0936-3DA3-4D19-B112-F3CA259A4148}{1}{B0}", + "object": "{77D1B2CB-BFAF-4106-8E28-90895EBDFE03}{40}{B0}", + "text": "Typed " + }, + { + "case": "Split empty", + "index": 1, + "page": "{984F0936-3DA3-4D19-B112-F3CA259A4148}{1}{B0}", + "object": "{F8CC0112-769C-0624-19AF-E3D7C46B02D1}{1}{B0}", + "text": "Typed " + } +] \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-empty-0.ahk b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-0.ahk new file mode 100644 index 0000000000000000000000000000000000000000..cd7e3139b8b371dbfccc8e6df83d5240bbadee8c --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-0.ahk @@ -0,0 +1,23 @@ +#Requires AutoHotkey v2.0 +OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1))) +dm := Buffer(220, 0) +NumPut("UShort", 220, dm, 68) +if !DllCall("EnumDisplaySettingsW", "Ptr", 0, "UInt", 0xFFFFFFFF, "Ptr", dm) + throw Error("Cannot inspect display mode") +NumPut("UInt", NumGet(dm, 72, "UInt") | 0x180000, dm, 72) +NumPut("UInt", 1280, dm, 172) +NumPut("UInt", 720, dm, 176) +if DllCall("ChangeDisplaySettingsW", "Ptr", dm, "UInt", 0, "Int") != 0 + throw Error("Display mode rejected") +app := ComObject("OneNote.Application") +app.NavigateTo("{984F0936-3DA3-4D19-B112-F3CA259A4148}{1}{B0}", "{77D1B2CB-BFAF-4106-8E28-90895EBDFE03}{40}{B0}", false) +hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10) +if !hwnd + throw Error("OneNote window did not appear") +WinMaximize(hwnd) +WinActivate(hwnd) +if !WinWaitActive(hwnd,, 10) + throw Error("OneNote window did not become active") +Sleep 300 +Send "{Left}" +SendText "Typed " diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-empty-0.json b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-0.json new file mode 120000 index 0000000000000000000000000000000000000000..e05935d2e8052dbcef4ce8e5a4c0f7cc6af8ce3c --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-0.json @@ -0,0 +1 @@ +../../../joined/ui/split-empty.json \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-empty-0.png b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-0.png new file mode 100644 index 0000000000000000000000000000000000000000..94c12d501890f4c760c0db9b34c087eba3420ed6 Binary files /dev/null and b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-0.png differ diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-empty-1.ahk b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-1.ahk new file mode 100644 index 0000000000000000000000000000000000000000..e0e075d2a05a22c9ba696c2412b39d8eac82d19c --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-1.ahk @@ -0,0 +1,23 @@ +#Requires AutoHotkey v2.0 +OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1))) +dm := Buffer(220, 0) +NumPut("UShort", 220, dm, 68) +if !DllCall("EnumDisplaySettingsW", "Ptr", 0, "UInt", 0xFFFFFFFF, "Ptr", dm) + throw Error("Cannot inspect display mode") +NumPut("UInt", NumGet(dm, 72, "UInt") | 0x180000, dm, 72) +NumPut("UInt", 1280, dm, 172) +NumPut("UInt", 720, dm, 176) +if DllCall("ChangeDisplaySettingsW", "Ptr", dm, "UInt", 0, "Int") != 0 + throw Error("Display mode rejected") +app := ComObject("OneNote.Application") +app.NavigateTo("{984F0936-3DA3-4D19-B112-F3CA259A4148}{1}{B0}", "{F8CC0112-769C-0624-19AF-E3D7C46B02D1}{1}{B0}", false) +hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10) +if !hwnd + throw Error("OneNote window did not appear") +WinMaximize(hwnd) +WinActivate(hwnd) +if !WinWaitActive(hwnd,, 10) + throw Error("OneNote window did not become active") +Sleep 300 +Send "{Left}" +SendText "Typed " diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-empty-1.json b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-1.json new file mode 120000 index 0000000000000000000000000000000000000000..e05935d2e8052dbcef4ce8e5a4c0f7cc6af8ce3c --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-1.json @@ -0,0 +1 @@ +../../../joined/ui/split-empty.json \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-empty-1.png b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-1.png new file mode 100644 index 0000000000000000000000000000000000000000..a0b933ccf1c78cbe39065f091bb8a541e5553524 Binary files /dev/null and b/corpus/paragraph-edit/rust-split/typed/ui/split-empty-1.png differ diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-end-1.ahk b/corpus/paragraph-edit/rust-split/typed/ui/split-end-1.ahk new file mode 100644 index 0000000000000000000000000000000000000000..c2961c0d7722b32680ba53bc822a8d2641d6e031 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/ui/split-end-1.ahk @@ -0,0 +1,23 @@ +#Requires AutoHotkey v2.0 +OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1))) +dm := Buffer(220, 0) +NumPut("UShort", 220, dm, 68) +if !DllCall("EnumDisplaySettingsW", "Ptr", 0, "UInt", 0xFFFFFFFF, "Ptr", dm) + throw Error("Cannot inspect display mode") +NumPut("UInt", NumGet(dm, 72, "UInt") | 0x180000, dm, 72) +NumPut("UInt", 1280, dm, 172) +NumPut("UInt", 720, dm, 176) +if DllCall("ChangeDisplaySettingsW", "Ptr", dm, "UInt", 0, "Int") != 0 + throw Error("Display mode rejected") +app := ComObject("OneNote.Application") +app.NavigateTo("{EEE8E5F3-7A14-4AA5-B06A-56D4BAC7C8AF}{1}{B0}", "{F60211DF-6E7D-0CA1-2F70-07201B2B1F07}{1}{B0}", false) +hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10) +if !hwnd + throw Error("OneNote window did not appear") +WinMaximize(hwnd) +WinActivate(hwnd) +if !WinWaitActive(hwnd,, 10) + throw Error("OneNote window did not become active") +Sleep 300 +Send "{Left}" +SendText "Typed " diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-end-1.json b/corpus/paragraph-edit/rust-split/typed/ui/split-end-1.json new file mode 120000 index 0000000000000000000000000000000000000000..09fdbf0e38d0228a3e637bd7e9644b9d366549f9 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/ui/split-end-1.json @@ -0,0 +1 @@ +../../../joined/ui/split-end.json \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-end-1.png b/corpus/paragraph-edit/rust-split/typed/ui/split-end-1.png new file mode 100644 index 0000000000000000000000000000000000000000..c38f66a9dbee53b087821b60b7498a42661dc1b0 Binary files /dev/null and b/corpus/paragraph-edit/rust-split/typed/ui/split-end-1.png differ diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-start-0.ahk b/corpus/paragraph-edit/rust-split/typed/ui/split-start-0.ahk new file mode 100644 index 0000000000000000000000000000000000000000..c374ff646a887af04b9830fc8bacc7c28a776ae7 --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/ui/split-start-0.ahk @@ -0,0 +1,23 @@ +#Requires AutoHotkey v2.0 +OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1))) +dm := Buffer(220, 0) +NumPut("UShort", 220, dm, 68) +if !DllCall("EnumDisplaySettingsW", "Ptr", 0, "UInt", 0xFFFFFFFF, "Ptr", dm) + throw Error("Cannot inspect display mode") +NumPut("UInt", NumGet(dm, 72, "UInt") | 0x180000, dm, 72) +NumPut("UInt", 1280, dm, 172) +NumPut("UInt", 720, dm, 176) +if DllCall("ChangeDisplaySettingsW", "Ptr", dm, "UInt", 0, "Int") != 0 + throw Error("Display mode rejected") +app := ComObject("OneNote.Application") +app.NavigateTo("{058E24C9-6521-4913-9088-94646869E7EC}{1}{B0}", "{7DB914B1-2E9B-4E51-B78C-87B90E14A385}{40}{B0}", false) +hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10) +if !hwnd + throw Error("OneNote window did not appear") +WinMaximize(hwnd) +WinActivate(hwnd) +if !WinWaitActive(hwnd,, 10) + throw Error("OneNote window did not become active") +Sleep 300 +Send "{Left}" +SendText "Typed " diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-start-0.json b/corpus/paragraph-edit/rust-split/typed/ui/split-start-0.json new file mode 120000 index 0000000000000000000000000000000000000000..3d98eda4565545331e305ce936e9c787448aa0dc --- /dev/null +++ b/corpus/paragraph-edit/rust-split/typed/ui/split-start-0.json @@ -0,0 +1 @@ +../../../joined/ui/split-start.json \ No newline at end of file diff --git a/corpus/paragraph-edit/rust-split/typed/ui/split-start-0.png b/corpus/paragraph-edit/rust-split/typed/ui/split-start-0.png new file mode 100644 index 0000000000000000000000000000000000000000..576e945b2a98eed8e208d7e7d847d6251430f33d Binary files /dev/null and b/corpus/paragraph-edit/rust-split/typed/ui/split-start-0.png differ diff --git a/crates/onestore/README.md b/crates/onestore/README.md index 2e63064f53df82ca64ab15fb2a70604d78bd5ce6..e029c43cadc0257e54ea518ef6d1e30f6632f27c 100644 --- a/crates/onestore/README.md +++ b/crates/onestore/README.md @@ -49,6 +49,7 @@ harness also accepts `--client-profile release`. | `replace_property_bytes` | Append one scalar-property revision; preserve prior revisions and unrelated property values and references | | `replace_text`, `commit_text`, `commit_file_text` | Replace a UTF-16 range across ordinary text runs; publish text, run boundaries and modification time together | | `Insertion`, `PreparedEdit::insert` | Insert paragraphs into editable containers or positioned outlines into a page, retaining intent identities across rebases | +| `ParagraphSplit`, `PreparedEdit::split` | Split ordinary text at a UTF-16 scalar boundary, retaining the original left identities and moving children to the right | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | | `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter | | `read_file` | Read a snapshot under whole-file exclusion | @@ -77,6 +78,13 @@ accepts one `0..0` span for subsequent typing. Retain the `Insertion` value for value creates different object identities. Duplicate insertion identities require reconciliation. Formatting accepts explicit attributes, preserves inherited values, and gives retired immutable styles zero current references while retaining history. +Paragraph splits preserve character formatting, retain tags on the left, and clone +mutable list objects without restarting numbering. The new right paragraph/text +identities belong to the retained `ParagraphSplit` intent. Its publication includes +the complete child graph and title metadata; repeating an existing identity requires +reconciliation. Title containers, generated fields, recording-linked text and +associated run metadata are rejected before I/O. Native split controls and subsequent +typing checks reside in [the paragraph corpus](../../corpus/paragraph-edit/README.md). Generated fields, protected targets and unsupported run-data boundary changes are rejected before publication. Local caches expose text, insertion and formatting edits; the [document-writer acceptance](../../evidence/MILESTONE9.md#document-writer-and-offline-acceptance) @@ -215,6 +223,7 @@ subsets of unflushed bytes and is shared with the stateful commit fuzzer. ```sh cargo +nightly fuzz run revisions -- -max_total_time=120 -max_len=262144 -rss_limit_mb=2048 cargo +nightly fuzz run commit -- -max_total_time=300 -max_len=4096 -rss_limit_mb=2048 +cargo +nightly fuzz run paragraph -- -max_total_time=120 -max_len=160 -rss_limit_mb=2048 ``` Fuzz targets cover storage, properties, revisions, scalar edits, creation, and diff --git a/crates/onestore/src/commit.rs b/crates/onestore/src/commit.rs index ccd041f819c516faa7c76dfc7740d4e69ef687cc..dbc621e4e78b394a7cf6df0fd6cf7a56099b682c 100644 --- a/crates/onestore/src/commit.rs +++ b/crates/onestore/src/commit.rs @@ -183,6 +183,19 @@ pub struct PreparedEdit<'a> { } impl<'a> PreparedEdit<'a> { + /// Splits a paragraph and updates its children, lists, tags and title metadata atomically. + /// Fields and associated run metadata are rejected before I/O. + pub fn split( + source: &'a [u8], + space: ExGuid, + split: &crate::ParagraphSplit, + ) -> Result { + Ok(Self { + source, + written: split.apply(source, space)?, + }) + } + /// Prepares an insertion and its dependent metadata in one revision, without I/O. pub fn insert( source: &'a [u8], diff --git a/crates/onestore/src/lib.rs b/crates/onestore/src/lib.rs index 70eb0e618e32bc1e3f9eea14ea1ad90ba1bbd717..9eac9aa570e7f35e36f497f9f2d791d773d38af8 100644 --- a/crates/onestore/src/lib.rs +++ b/crates/onestore/src/lib.rs @@ -11,6 +11,7 @@ mod flush; mod formatting; mod insertion; mod objects; +mod paragraph; mod properties; #[cfg(feature = "protected")] pub mod protected; @@ -31,6 +32,7 @@ pub use files::FileDataReference; pub use formatting::TextAttribute; pub use insertion::Insertion; pub use objects::{Object, ObjectData, ObjectReferences, ResolvedRevision}; +pub use paragraph::ParagraphSplit; pub use properties::{IdStream, Property, PropertySets, Value}; pub use revisions::{ExGuid, ObjectSpace, Revision, RevisionIndex}; pub use snapshot::{read_snapshot, read_storage_snapshot}; diff --git a/crates/onestore/src/paragraph.rs b/crates/onestore/src/paragraph.rs new file mode 100644 index 0000000000000000000000000000000000000000..0d96074ac3ecbddf7b3f14376fb6d2c37914d625 --- /dev/null +++ b/crates/onestore/src/paragraph.rs @@ -0,0 +1,383 @@ +use crate::{ + Error, ExGuid, Object, ObjectData, PropertySets, RevisionIndex, Store, + create::{current_timestamps, properties, string}, + document::{Document, Element, Kind}, + edit::{editable_parents, page_title}, + write::{PropertyObject, fresh_guid, write_revision}, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{BTreeMap, BTreeSet}, + ops::Range, + sync::Arc, +}; + +fn invalid(message: &'static str) -> Error { + Error { offset: 0, message } +} + +/// Splits ordinary paragraph text while retaining the new objects' identities across retries. +/// The original paragraph/text remain on the left; nested children move to the right. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ParagraphSplit { + guid: [u8; 16], + text: ExGuid, + offset: u32, + author: String, + created: u32, +} + +impl ParagraphSplit { + /// The offset is measured in UTF-16 code units and must lie between Unicode scalars. + pub fn new(text: ExGuid, offset: u32, author: &str) -> Result { + if text.guid == [0; 16] || author.contains('\0') { + return Err(invalid( + "Choose paragraph text and an author name without NUL", + )); + } + Ok(Self { + guid: fresh_guid()?, + text, + offset, + author: author.to_owned(), + created: current_timestamps()?.0, + }) + } + + /// Identity of the new right paragraph. + pub fn object(&self) -> ExGuid { + ExGuid { + guid: self.guid, + n: 1, + } + } + + /// Identity of the new right paragraph's text. + pub fn text_object(&self) -> ExGuid { + ExGuid { + guid: self.guid, + n: 2, + } + } + + pub(crate) fn apply(&self, source: &[u8], space: ExGuid) -> Result, Error> { + if self.guid == [0; 16] || self.text.guid == [0; 16] || self.author.contains('\0') { + return Err(invalid( + "Choose paragraph text and an author name without NUL", + )); + } + let store = Store::parse(source)?; + let index = RevisionIndex::parse(&store)?; + index.validate_current()?; + let mut document = Document::parse(&index)?; + let pages: Vec<_> = document + .pages()? + .into_iter() + .filter_map(|(sid, page)| (sid == space).then_some(page)) + .collect(); + let [page] = pages.as_slice() else { + return Err(invalid("Splitting requires a single active page")); + }; + let semantic = document + .spaces + .remove(&space) + .ok_or_else(|| invalid("The active page is unavailable"))?; + let rid = semantic.contexts[&ExGuid::default()]; + let mut view = semantic + .revisions + .into_iter() + .find_map(|(id, view)| (id == rid).then_some(view)) + .unwrap(); + let parents = editable_parents(&view, &pages, self.text)?; + let [paragraph] = parents + .get(&self.text) + .map(Vec::as_slice) + .unwrap_or_default() + else { + return Err(invalid("Select text belonging to one paragraph")); + }; + let [parent] = parents + .get(paragraph) + .map(Vec::as_slice) + .unwrap_or_default() + else { + return Err(invalid("Select a paragraph with one parent")); + }; + let left = &view.nodes[paragraph]; + let Kind::Paragraph { lists, .. } = &left.kind else { + return Err(invalid("Select ordinary paragraph text")); + }; + if left.content != [self.text] + || !matches!( + view.nodes[parent].kind, + Kind::Outline { .. } + | Kind::OutlineGroup + | Kind::Paragraph { .. } + | Kind::Cell { .. } + ) + { + return Err(invalid( + "Select ordinary paragraph text inside an outline or table cell", + )); + } + let mut ancestors = BTreeSet::new(); + let mut pending = vec![*paragraph]; + while let Some(id) = pending.pop() { + if !ancestors.insert(id) { + continue; + } + if matches!(view.nodes[&id].kind, Kind::Title) { + return Err(invalid( + "Title containers cannot be split into ordinary paragraphs", + )); + } + pending.extend(parents.get(&id).into_iter().flatten().copied()); + } + let node = &view.nodes[&self.text]; + let Kind::RichText { + text, + runs, + boilerplate, + .. + } = &node.kind + else { + return Err(invalid("Select ordinary paragraph text")); + }; + if *boilerplate || !node.media_ids.is_empty() || node.media_time_ms.is_some() { + return Err(invalid( + "Generated or recording-linked text cannot be split", + )); + } + for run in view.text_runs(self.text)? { + if [ + run.format.hidden, + run.format.hyperlink, + run.format.math, + run.format.embedded_object, + ] + .contains(&Some(true)) + || run.text.contains(['\u{fffc}', '\u{fddf}']) + { + return Err(invalid( + "This paragraph contains a field or embedded object that cannot be split", + )); + } + } + let raw = index.resolve(space, rid)?; + let ObjectData::Properties(blob) = raw.objects[&self.text].data else { + unreachable!() + }; + if PropertySets::parse(blob)?.sets[0] + .iter() + .any(|p| matches!(p.id, 0x40003499 | 0x24003458)) + { + return Err(invalid( + "This paragraph contains run metadata that cannot be split", + )); + } + let length = u32::try_from(text.encode_utf16().count()) + .map_err(|_| invalid("Paragraph exceeds the UTF-16 offset range"))?; + if self.offset > length || lists.len() > 251 { + return Err(invalid( + "Choose a position within the paragraph and at most 251 list levels", + )); + } + let author_id = ExGuid { + guid: self.guid, + n: 3, + }; + let normal_id = ExGuid { + guid: self.guid, + n: 4, + }; + let typing = runs.last().and_then(|run| run.format).unwrap_or(normal_id); + let modified = current_timestamps()?.0.to_le_bytes(); + let mut changed = BTreeMap::new(); + if (self.offset == 0 && !text.is_empty()) || runs.iter().any(|run| run.format.is_none()) { + changed.insert( + normal_id, + PropertyObject { + jcid: 0x12004d, + bytes: properties(&[])?, + global_ids: Arc::new(BTreeMap::from([(0, self.guid)])), + }, + ); + } + let mut prefix = fragment( + &raw.objects[&self.text], + node, + 0..self.offset, + if text.is_empty() { typing } else { normal_id }, + )?; + let mut suffix = fragment(&raw.objects[&self.text], node, self.offset..length, typing)?; + prefix.set(&[(0x14001d7a, &modified)])?; + suffix.remove(&[0x40003489])?; + suffix.set(&[(0x14001d7a, &modified)])?; + suffix.reference(self.text_object())?; + changed.insert(self.text, prefix); + changed.insert(self.text_object(), suffix); + changed.insert( + author_id, + PropertyObject { + jcid: 0x120001, + bytes: properties(&[(0x1c001d75, string(&self.author))])?, + global_ids: Arc::new(BTreeMap::from([(0, self.guid)])), + }, + ); + let mut right = PropertyObject::from_object(&raw.objects[paragraph])?; + let content = right.reference(self.text_object())?; + let author = right.reference(author_id)?; + right.set(&[ + (0x24001c1f, &content), + (0x14001d09, &self.created.to_le_bytes()), + (0x20001d78, &author), + (0x20001d79, &author), + (0x14001d7a, &modified), + ])?; + if !lists.is_empty() { + let mut references = Vec::new(); + for (i, old) in lists.iter().enumerate() { + if !matches!(view.nodes[old].kind, Kind::List { .. }) { + return Err(invalid("The paragraph list is unavailable")); + } + let id = ExGuid { + guid: self.guid, + n: 5 + u32::try_from(i).unwrap(), + }; + let mut list = PropertyObject::from_object(&raw.objects[old])?; + list.remove(&[0x14001cb7])?; + list.reference(id)?; + references.extend_from_slice(&right.reference(id)?); + changed.insert(id, list); + } + right.set(&[(0x24001c26, &references)])?; + } + changed.insert(self.object(), right); + let mut original = PropertyObject::from_object(&raw.objects[paragraph])?; + original.remove(&[0x24001c20])?; + let author = original.reference(author_id)?; + original.set(&[(0x20001d79, &author), (0x14001d7a, &modified)])?; + changed.insert(*paragraph, original); + let mut parent_object = PropertyObject::from_object(&raw.objects[parent])?; + let mut children = Vec::new(); + for child in &view.nodes[parent].children { + children.extend_from_slice(&parent_object.reference(*child)?); + if child == paragraph { + children.extend_from_slice(&parent_object.reference(self.object())?); + } + } + parent_object.set(&[(0x24001c20, &children)])?; + changed.insert(*parent, parent_object); + if changed + .keys() + .any(|id| id.guid == self.guid && raw.objects.contains_key(id)) + { + return Err(invalid( + "A split identity already exists; reconcile the existing edit", + )); + } + for id in ancestors { + let object = match changed.entry(id) { + std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(), + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(PropertyObject::from_object(&raw.objects[&id])?) + } + }; + object.set(&[(0x14001d7a, &modified)])?; + } + for (id, object) in &changed { + view.nodes.insert( + *id, + Element::parse( + &Object { + jcid: object.jcid, + reference_count: 0, + data: ObjectData::Properties(&object.bytes), + global_ids: Arc::clone(&object.global_ids), + }, + &store, + )?, + ); + } + let title = page_title(&view, &pages, None)?; + drop(view); + if let Some((_, automatic, title)) = title { + let metadata = raw + .roots + .get(&2) + .ok_or_else(|| invalid("Page title metadata is unavailable"))?; + if raw.objects[metadata].jcid != 0x20030 { + return Err(invalid("Page title metadata is unavailable")); + } + let title = string(&title); + let mut object = PropertyObject::from_object(&raw.objects[metadata])?; + object.set(&[(0x1c001cf3, &title)])?; + changed.insert(*metadata, object); + changed + .get_mut(page) + .unwrap() + .set(&[(0x1c001d3c, if automatic { &title } else { &[0, 0] })])?; + } + write_revision(source, space, |_| Ok(changed)) + } +} + +fn fragment( + raw: &Object<'_>, + node: &Element<'_>, + range: Range, + empty: ExGuid, +) -> Result { + let Kind::RichText { text, runs, .. } = &node.kind else { + return Err(invalid("Select ordinary paragraph text")); + }; + let units: Vec<_> = text.encode_utf16().collect(); + let start = usize::try_from(range.start) + .map_err(|_| invalid("Choose a position within the paragraph"))?; + let end = usize::try_from(range.end) + .map_err(|_| invalid("Choose a position within the paragraph"))?; + let slice = units + .get(start..end) + .ok_or_else(|| invalid("Choose a position within the paragraph"))?; + let text = String::from_utf16(slice) + .map_err(|_| invalid("Choose a position between Unicode scalars"))?; + let mut segments = Vec::new(); + for run in runs { + if run.start < range.end && range.start < run.end { + segments.push(( + run.end.min(range.end) - range.start, + run.format.unwrap_or(empty), + )); + } + } + if segments.is_empty() { + segments.push((0, empty)); + } + if !range.is_empty() && end == units.len() { + let last = runs.last().unwrap(); + if last.start == last.end { + segments.push((range.end - range.start, last.format.unwrap_or(empty))); + } + } + if segments[..segments.len() - 1] + .windows(2) + .any(|pair| pair[0].0 >= pair[1].0) + { + return Err(invalid("Text-run boundaries must be strictly increasing")); + } + let mut object = PropertyObject::from_object(raw)?; + let mut ends = Vec::new(); + let mut references = Vec::new(); + for (end, id) in segments { + ends.extend_from_slice(&end.to_le_bytes()); + references.extend_from_slice(&object.reference(id)?); + } + ends.truncate(ends.len() - 4); + object.set(&[ + (0x1c001c22, &string(&text)), + (0x1c001e12, &ends), + (0x24001e13, &references), + ])?; + Ok(object) +} diff --git a/crates/onestore/src/write.rs b/crates/onestore/src/write.rs index 19d078326fd9404b759eb1b861369da66a22a77d..581a72460722c325de61b9e9bbef0b5e00bcf1a3 100644 --- a/crates/onestore/src/write.rs +++ b/crates/onestore/src/write.rs @@ -120,6 +120,15 @@ fn field_length(property: &crate::Property<'_>, set_lengths: &[usize]) -> usize } } +fn property_set_lengths(properties: &PropertySets<'_>) -> Vec { + let mut lengths = vec![0; properties.sets.len()]; + for (i, set) in properties.sets.iter().enumerate().rev() { + lengths[i] = + 2 + set.len() * 4 + set.iter().map(|p| field_length(p, &lengths)).sum::(); + } + lengths +} + fn patch_properties( blob: &[u8], updates: &[(u32, &[u8])], @@ -130,15 +139,7 @@ fn patch_properties( let ids = properties.root_ids.as_ptr().addr() - blob.as_ptr().addr(); let ids_end = ids + properties.root_ids.len(); let body_end = blob.len() - properties.padding.len(); - let mut set_lengths = vec![0; properties.sets.len()]; - for (i, set) in properties.sets.iter().enumerate().rev() { - set_lengths[i] = 2 - + set.len() * 4 - + set - .iter() - .map(|p| field_length(p, &set_lengths)) - .sum::(); - } + let set_lengths = property_set_lengths(&properties); let mut offsets = Vec::with_capacity(root.len()); let mut offset = ids_end; for property in root { @@ -379,6 +380,79 @@ impl PropertyObject { Ok(()) } + pub fn remove(&mut self, ids: &[u32]) -> Result<()> { + let properties = PropertySets::parse(&self.bytes)?; + let removed = |id: u32| { + ids.iter() + .any(|wanted| id & 0x7fffffff == wanted & 0x7fffffff) + }; + if !properties.sets[0].iter().any(|p| removed(p.id)) { + return Ok(()); + } + let lengths = property_set_lengths(&properties); + let mut offset = properties.root_ids.as_ptr().addr() - self.bytes.as_ptr().addr() + + properties.root_ids.len(); + let mut retained_ids = Vec::new(); + let mut fields = Vec::new(); + let mut references: [Vec; 3] = std::array::from_fn(|_| Vec::new()); + for property in &properties.sets[0] { + let end = offset + field_length(property, &lengths); + if !removed(property.id) { + retained_ids.extend_from_slice(&property.id.to_le_bytes()); + fields.extend_from_slice(&self.bytes[offset..end]); + let mut pending = vec![property]; + while let Some(field) = pending.pop() { + match &field.value { + Value::References { + stream, + compact_ids, + } => { + let index = match stream { + crate::IdStream::Objects => 0, + crate::IdStream::ObjectSpaces => 1, + crate::IdStream::Contexts => 2, + }; + references[index].extend_from_slice(compact_ids); + } + Value::Sets(children) => { + for child in children.clone().rev() { + pending.extend(properties.sets[child].iter().rev()); + } + } + _ => {} + } + } + } + offset = end; + } + let mut cursor = crate::bytes::Cursor { + bytes: &self.bytes, + offset: 0, + }; + let streams = crate::properties::reference_streams(&mut cursor)?; + let mut bytes = Vec::new(); + for (stream, retained) in streams.iter().zip(&references) { + if stream.offset == 0 { + continue; + } + let header = u32::from_le_bytes( + self.bytes[stream.offset - 4..stream.offset] + .try_into() + .unwrap(), + ); + let count = u32::try_from(retained.len() / 4).unwrap(); + bytes.extend_from_slice(&((header & 0xff000000) | count).to_le_bytes()); + bytes.extend_from_slice(retained); + } + bytes.extend_from_slice(&u16::try_from(retained_ids.len() / 4).unwrap().to_le_bytes()); + bytes.extend_from_slice(&retained_ids); + bytes.extend_from_slice(&fields); + bytes.resize(bytes.len().next_multiple_of(8), 0); + PropertySets::parse(&bytes)?; + self.bytes = bytes; + Ok(()) + } + pub fn reference(&mut self, id: ExGuid) -> Result<[u8; 4]> { if !self.global_ids.values().any(|guid| *guid == id.guid) { let mut index = 0; diff --git a/crates/onestore/src/write/tests.rs b/crates/onestore/src/write/tests.rs index 845064f1c0da2afbadca12938a8496554f36eb78..5e996875596247be4c62f01f55f8f8c6c619c30a 100644 --- a/crates/onestore/src/write/tests.rs +++ b/crates/onestore/src/write/tests.rs @@ -46,6 +46,8 @@ fn document_insertions_and_formatting_respect_readonly_ancestors() { .iter() .find_map(|(id, object)| (object.jcid == 0x6000e).then_some(*id)) .unwrap(); + let split = crate::ParagraphSplit::new(text, 1, "Author").unwrap(); + assert!(PreparedEdit::split(&protected, sid, &split).is_err()); assert!( PreparedEdit::format( &protected, @@ -531,6 +533,45 @@ fn nested_fields_and_other_reference_streams_remain_byte_exact() { ); assert_eq!(&changed[20..36], &original[16..32]); assert_eq!(parsed.sets[0][3].value, Value::Bytes(&[96, 97, 98, 99])); + + for removed in 0..16 { + let ids: Vec<_> = previous.sets[0] + .iter() + .enumerate() + .filter_map(|(i, property)| (removed & (1 << i) != 0).then_some(property.id)) + .collect(); + let mut object = super::PropertyObject { + jcid: 0x6000e, + bytes: original.clone(), + global_ids: Default::default(), + }; + object.remove(&ids).unwrap(); + let parsed = PropertySets::parse(&object.bytes).unwrap(); + assert!( + parsed.sets[0] + .iter() + .eq(previous.sets[0].iter().filter(|p| !ids.contains(&p.id))) + ); + if removed & 2 == 0 { + assert_eq!(parsed.sets[1], previous.sets[1]); + assert_eq!( + object + .bytes + .windows(nested.len()) + .filter(|bytes| *bytes == nested) + .count(), + 1 + ); + } else { + assert_eq!(parsed.sets.len(), 1); + } + if removed == 0 { + assert_eq!(object.bytes, original); + } + let before = object.bytes.clone(); + object.remove(&ids).unwrap(); + assert_eq!(object.bytes, before); + } } #[test] @@ -549,6 +590,19 @@ fn deep_property_splices_do_not_use_the_call_stack() { &changed[14..changed.len() - parsed.padding.len()], &bytes[10..] ); + let mut object = super::PropertyObject { + jcid: 0x6000e, + bytes: changed, + global_ids: Default::default(), + }; + object.remove(&[0x08000002]).unwrap(); + let parsed = PropertySets::parse(&object.bytes).unwrap(); + assert_eq!(parsed.sets.len(), 100_001); + assert_eq!(&object.bytes[..bytes.len()], &bytes); + object.remove(&[0x44000001]).unwrap(); + let parsed = PropertySets::parse(&object.bytes).unwrap(); + assert_eq!(parsed.sets.len(), 1); + assert!(parsed.sets[0].is_empty()); } #[test] diff --git a/crates/onestore/tests/paragraph.rs b/crates/onestore/tests/paragraph.rs new file mode 100644 index 0000000000000000000000000000000000000000..4767022d053c4b677a6cd49151b564757c906bd9 --- /dev/null +++ b/crates/onestore/tests/paragraph.rs @@ -0,0 +1,343 @@ +use onestore::{ + ExGuid, ParagraphSplit, PreparedEdit, RevisionIndex, Store, + document::{Document, Kind, Revision}, +}; +use serde_json::Value; + +#[path = "support/checkpoint.rs"] +mod checkpoint; +#[path = "support/current.rs"] +mod current; +#[path = "support/disk.rs"] +mod disk; + +const SOURCE: &[u8] = + include_bytes!("../../../corpus/paragraph-edit/before/notebook/synthetic.one"); + +fn characters(view: &Revision<'_>, id: ExGuid) -> Vec<(char, Value)> { + view.text_runs(id) + .unwrap() + .into_iter() + .flat_map(|run| { + let format = serde_json::to_value(run.format).unwrap(); + run.text.chars().map(move |c| (c, format.clone())) + }) + .collect() +} + +#[test] +fn splits_partition_native_paragraphs_at_every_scalar_boundary() { + let manifest: Value = + serde_json::from_str(include_str!("../../../corpus/paragraph-edit/manifest.json")).unwrap(); + let store = Store::parse(SOURCE).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + for case in manifest["cases"].as_array().unwrap() { + let text: ExGuid = serde_json::from_value(case["original_text"].clone()).unwrap(); + let paragraph: ExGuid = serde_json::from_value(case["original_paragraph"].clone()).unwrap(); + let (sid, space) = document + .spaces + .iter() + .find(|(_, space)| { + space.revisions[&space.contexts[&ExGuid::default()]] + .nodes + .contains_key(&text) + }) + .unwrap(); + let before = &space.revisions[&space.contexts[&ExGuid::default()]]; + let (parent, parent_node) = before + .nodes + .iter() + .find(|(_, node)| node.children.contains(¶graph)) + .unwrap(); + let expected = characters(before, text); + let offsets: Vec = std::iter::once(0) + .chain(expected.iter().scan(0, |offset, (c, _)| { + *offset += u32::try_from(c.len_utf16()).unwrap(); + Some(*offset) + })) + .collect(); + for (position, offset) in offsets.iter().enumerate() { + let intent = ParagraphSplit::new(text, *offset, "Split author").unwrap(); + let restored = serde_json::from_value(serde_json::to_value(&intent).unwrap()).unwrap(); + assert_eq!(intent, restored); + let edited = PreparedEdit::split(SOURCE, *sid, &restored); + if case["case"] == "Split before hyperlink" { + assert!(edited.is_err()); + continue; + } + let edited = + edited.unwrap_or_else(|error| panic!("{} at {offset}: {error}", case["case"])); + let current_store = Store::parse(edited.as_bytes()).unwrap(); + assert_eq!( + current_store.header.transaction_count, + store.header.transaction_count + 1 + ); + let current_index = RevisionIndex::parse(¤t_store).unwrap(); + current_index.validate_current().unwrap(); + let current_document = Document::parse(¤t_index).unwrap(); + let current_space = ¤t_document.spaces[sid]; + let after = ¤t_space.revisions[¤t_space.contexts[&ExGuid::default()]]; + let mut changed = std::collections::BTreeSet::from([text, before.roots[&2]]); + let mut pending = vec![paragraph]; + while let Some(id) = pending.pop() { + if !changed.insert(id) { + continue; + } + pending.extend(before.nodes.iter().filter_map(|(parent, node)| { + node.children + .iter() + .chain(&node.content) + .chain(&node.structure) + .any(|child| *child == id) + .then_some(*parent) + })); + } + let old_raw = index + .resolve(*sid, space.contexts[&ExGuid::default()]) + .unwrap(); + let new_raw = current_index + .resolve(*sid, current_space.contexts[&ExGuid::default()]) + .unwrap(); + for (id, object) in old_raw.objects { + if !changed.contains(&id) { + assert_eq!(object.data, new_raw.objects[&id].data, "untouched {id}"); + } + } + assert_eq!( + characters(after, text), + expected[..position], + "{} prefix at {offset}", + case["case"] + ); + assert_eq!( + characters(after, intent.text_object()), + expected[position..], + "{} suffix at {offset}", + case["case"] + ); + let mut children = parent_node.children.clone(); + children.insert( + children.iter().position(|id| *id == paragraph).unwrap() + 1, + intent.object(), + ); + assert_eq!(after.nodes[parent].children, children); + assert_eq!(after.nodes[¶graph].content, [text]); + assert!(after.nodes[¶graph].children.is_empty()); + assert_eq!( + after.nodes[&intent.object()].content, + [intent.text_object()] + ); + assert_eq!( + after.nodes[&intent.object()].children, + before.nodes[¶graph].children + ); + assert_eq!( + serde_json::to_value(&after.nodes[&text].tags).unwrap(), + serde_json::to_value(&before.nodes[&text].tags).unwrap() + ); + assert!(after.nodes[&intent.text_object()].tags.is_empty()); + let Kind::Paragraph { + lists: old_lists, .. + } = &before.nodes[¶graph].kind + else { + panic!() + }; + let Kind::Paragraph { lists, .. } = &after.nodes[&intent.object()].kind else { + panic!() + }; + assert_eq!(old_lists.len(), lists.len()); + for (old, new) in old_lists.iter().zip(lists) { + assert_ne!(old, new); + let mut old = serde_json::to_value(&before.nodes[old]).unwrap(); + old["kind"]["restart"] = Value::Null; + assert_eq!(serde_json::to_value(&after.nodes[new]).unwrap(), old); + } + for (space_id, old_space) in &index.spaces { + for revision in old_space.revisions.keys() { + let old = index.resolve(*space_id, *revision).unwrap(); + let retained = current_index.resolve(*space_id, *revision).unwrap(); + assert_eq!(old.roots, retained.roots); + for (id, object) in old.objects { + assert_eq!(object.data, retained.objects[&id].data); + } + } + } + assert!(PreparedEdit::split(edited.as_bytes(), *sid, &intent).is_err()); + } + for offset in 0..=*offsets.last().unwrap() + 1 { + if offsets.contains(&offset) { + continue; + } + let intent = ParagraphSplit::new(text, offset, "Author").unwrap(); + assert!(PreparedEdit::split(SOURCE, *sid, &intent).is_err()); + } + } +} + +#[test] +fn invalid_split_identities_and_title_targets_are_rejected() { + assert!(ParagraphSplit::new(ExGuid::default(), 0, "Author").is_err()); + let manifest: Value = + serde_json::from_str(include_str!("../../../corpus/paragraph-edit/manifest.json")).unwrap(); + let text: ExGuid = + serde_json::from_value(manifest["cases"][0]["original_text"].clone()).unwrap(); + assert!(ParagraphSplit::new(text, 0, "a\0b").is_err()); + let store = Store::parse(SOURCE).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let sid = *document + .spaces + .iter() + .find(|(_, space)| { + space.revisions[&space.contexts[&ExGuid::default()]] + .nodes + .contains_key(&text) + }) + .unwrap() + .0; + let intent = ParagraphSplit::new(text, 1, "Author").unwrap(); + for (field, value) in [ + ("guid", serde_json::to_value([0_u8; 16]).unwrap()), + ("guid", serde_json::to_value(text.guid).unwrap()), + ("text", serde_json::to_value(ExGuid::default()).unwrap()), + ("author", serde_json::json!("a\0b")), + ("offset", serde_json::json!(u32::MAX)), + ] { + let mut encoded = serde_json::to_value(&intent).unwrap(); + encoded[field] = value; + let forged = serde_json::from_value(encoded).unwrap(); + assert!(PreparedEdit::split(SOURCE, sid, &forged).is_err()); + } + let mut titles = 0; + for (sid, _) in document.pages().unwrap() { + let space = &document.spaces[&sid]; + let view = &space.revisions[&space.contexts[&ExGuid::default()]]; + let mut pending: Vec<_> = view + .nodes + .iter() + .filter_map(|(id, n)| matches!(n.kind, Kind::Title).then_some(*id)) + .collect(); + let mut seen = std::collections::BTreeSet::new(); + while let Some(id) = pending.pop() { + if !seen.insert(id) { + continue; + } + let node = &view.nodes[&id]; + pending.extend(node.children.iter().chain(&node.content).copied()); + if matches!(node.kind, Kind::RichText { .. }) { + let intent = ParagraphSplit::new(id, 0, "Author").unwrap(); + assert!(PreparedEdit::split(SOURCE, sid, &intent).is_err()); + titles += 1; + } + } + } + assert!(titles >= 14); +} + +#[test] +#[ignore = "exports paragraph splits for independent native validation"] +fn export_native_paragraph_splits() { + use std::{fs, path::PathBuf}; + let output = PathBuf::from(std::env::var_os("ONESTORE_PARAGRAPH_OUTPUT").unwrap()); + assert!(output.is_absolute()); + fs::create_dir(&output).unwrap(); + let manifest: Value = + serde_json::from_str(include_str!("../../../corpus/paragraph-edit/manifest.json")).unwrap(); + let mut source = SOURCE.to_vec(); + let mut written = Vec::new(); + for case in manifest["cases"].as_array().unwrap() { + if case["case"] == "Split before hyperlink" { + continue; + } + let text: ExGuid = serde_json::from_value(case["original_text"].clone()).unwrap(); + let store = Store::parse(&source).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let sid = *document + .spaces + .iter() + .find(|(_, space)| { + space.revisions[&space.contexts[&ExGuid::default()]] + .nodes + .contains_key(&text) + }) + .unwrap() + .0; + let intent = ParagraphSplit::new( + text, + serde_json::from_value(case["offset_utf16"].clone()).unwrap(), + "Rust split author", + ) + .unwrap(); + let edited = PreparedEdit::split(&source, sid, &intent).unwrap(); + written.push(serde_json::json!({"case": case["case"], "intent": intent, + "new_paragraph": intent.object(), "new_text": intent.text_object()})); + source = edited.as_bytes().to_vec(); + } + let candidate = output.join("candidate"); + fs::create_dir(&candidate).unwrap(); + fs::write(candidate.join("synthetic.one"), source).unwrap(); + fs::write( + output.join("manifest.json"), + serde_json::to_vec_pretty(&serde_json::json!({"cases": written})).unwrap(), + ) + .unwrap(); +} + +#[test] +fn interrupted_splits_publish_a_complete_graph_or_retain_the_original() { + let original = onestore::create_section("split.one", "Original", "Author").unwrap(); + let store = Store::parse(&original).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let (sid, page) = document.pages().unwrap()[0]; + let space = &document.spaces[&sid]; + let view = &space.revisions[&space.contexts[&ExGuid::default()]]; + let outline = view.nodes[&page] + .children + .iter() + .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. })) + .unwrap(); + let paragraph = view.nodes[outline].children[0]; + let insertion = onestore::Insertion::paragraph(paragraph, None, "a🦀b", "Author") + .unwrap() + .with_formatting(1..3, &[onestore::TextAttribute::Bold(true)]) + .unwrap(); + let inserted = PreparedEdit::insert(&original, sid, &insertion).unwrap(); + let source = inserted.as_bytes(); + let checkpoint = checkpoint::pending(source, sid, insertion.text_object(), 0x14001d7a); + for source in [source, &checkpoint] { + let intent = ParagraphSplit::new(insertion.text_object(), 1, "Author").unwrap(); + let edit = PreparedEdit::split(source, sid, &intent).unwrap(); + let before = current::current(source); + let after = current::current(edit.as_bytes()); + for write_limit in [17, 4096] { + let disk = |fail_at| disk::Disk { + visible: source.to_vec(), + durable: source.to_vec(), + operation: 0, + fail_at, + write_limit, + random: 347, + }; + let mut successful = disk(None); + edit.commit(&mut successful).unwrap(); + assert_eq!(successful.durable, edit.as_bytes()); + for at in 1..=successful.operation { + let mut interrupted = disk(Some(at)); + let error = edit.commit(&mut interrupted).unwrap_err(); + let recovered = current::current(&interrupted.durable); + assert!( + recovered == before || recovered == after, + "interruption {at}" + ); + match error.state { + onestore::CommitState::NotCommitted => assert_eq!(recovered, before), + onestore::CommitState::Committed => assert_eq!(recovered, after), + onestore::CommitState::Unknown => {} + } + } + } + } +} diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index edaf899d52a4762dbc9615ddefbf58c2baed74ef..3c0a6bb007fa7d2349e20ebf0db8b3539a536c65 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -84,3 +84,10 @@ path = "fuzz_targets/insert.rs" test = false doc = false bench = false + +[[bin]] +name = "paragraph" +path = "fuzz_targets/paragraph.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/paragraph.rs b/fuzz/fuzz_targets/paragraph.rs new file mode 100644 index 0000000000000000000000000000000000000000..fcc5b46163d65f926a61b277abe432623a0410df --- /dev/null +++ b/fuzz/fuzz_targets/paragraph.rs @@ -0,0 +1,144 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use onestore::{ + CommitState, ExGuid, Insertion, ParagraphSplit, PreparedEdit, RevisionIndex, Store, + TextAttribute as A, + document::{Document, Kind}, +}; +use std::sync::LazyLock; + +#[path = "../../crates/onestore/tests/support/current.rs"] +mod current; +#[path = "../../crates/onestore/tests/support/disk.rs"] +mod disk; + +static SOURCE: LazyLock<(Vec, ExGuid, ExGuid)> = LazyLock::new(|| { + let source = onestore::create_section("paragraph.one", "Original", "Author").unwrap(); + let store = Store::parse(&source).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let (sid, page) = document.pages().unwrap()[0]; + let space = &document.spaces[&sid]; + let view = &space.revisions[&space.contexts[&ExGuid::default()]]; + let outline = *view.nodes[&page] + .children + .iter() + .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. })) + .unwrap(); + let parent = view.nodes[&outline].children[0]; + let intent = Insertion::paragraph(parent, None, "a🦀 e\u{301} 東京\rEnd", "Author") + .unwrap() + .with_formatting(0..4, &[A::Bold(true)]) + .unwrap() + .with_formatting(4..7, &[A::Italic(true), A::Color(Some([12, 34, 56]))]) + .unwrap(); + let edited = PreparedEdit::insert(&source, sid, &intent).unwrap(); + (edited.as_bytes().to_vec(), sid, outline) +}); + +fuzz_target!(|input: &[u8]| { + let (source, sid, outline) = &*SOURCE; + if let Ok(intent) = serde_json::from_slice::(input) + && let Ok(edit) = PreparedEdit::split(source, *sid, &intent) + { + current::current(edit.as_bytes()); + } + let mut persisted = source.clone(); + let mut caches = std::array::from_fn::<_, 12, _>(|_| source.clone()); + for step in input.chunks_exact(8).take(20) { + let actor = usize::from(step[0]) % caches.len(); + if step[1] % 3 == 0 { + caches[actor].clone_from(&persisted); + } + let source = &caches[actor]; + let store = Store::parse(source).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let space = &document.spaces[sid]; + let view = &space.revisions[&space.contexts[&ExGuid::default()]]; + let mut pending = vec![*outline]; + let mut paragraphs = Vec::new(); + while let Some(id) = pending.pop() { + let node = &view.nodes[&id]; + pending.extend(node.children.iter().copied()); + if matches!(node.kind, Kind::Paragraph { .. }) { + paragraphs.push(id); + } + } + let paragraph = paragraphs[usize::from(step[2]) % paragraphs.len()]; + let text = view.nodes[¶graph].content[0]; + let characters: Vec<_> = view + .text_runs(text) + .unwrap() + .into_iter() + .flat_map(|run| { + let style = serde_json::to_value(run.format).unwrap(); + run.text.chars().map(move |c| (c, style.clone())) + }) + .collect(); + let offsets: Vec = std::iter::once(0) + .chain(characters.iter().scan(0, |n, (c, _)| { + *n += u32::try_from(c.len_utf16()).unwrap(); + Some(*n) + })) + .collect(); + let offset = u32::from(step[3]) % (offsets.last().unwrap() + 2); + let intent = ParagraphSplit::new(text, offset, "Paragraph fuzz").unwrap(); + let intent = serde_json::from_value(serde_json::to_value(intent).unwrap()).unwrap(); + let edit = PreparedEdit::split(source, *sid, &intent); + let Some(position) = offsets.iter().position(|n| *n == offset) else { + assert!(edit.is_err()); + continue; + }; + let edit = edit.unwrap(); + let after_store = Store::parse(edit.as_bytes()).unwrap(); + let after_index = RevisionIndex::parse(&after_store).unwrap(); + let after_document = Document::parse(&after_index).unwrap(); + let space = &after_document.spaces[sid]; + let after_view = &space.revisions[&space.contexts[&ExGuid::default()]]; + for (id, expected) in [ + (text, &characters[..position]), + (intent.text_object(), &characters[position..]), + ] { + let actual: Vec<_> = after_view + .text_runs(id) + .unwrap() + .into_iter() + .flat_map(|run| { + let style = serde_json::to_value(run.format).unwrap(); + run.text.chars().map(move |c| (c, style.clone())) + }) + .collect(); + assert_eq!(actual, expected); + } + assert!(after_view.nodes[¶graph].children.is_empty()); + assert_eq!( + after_view.nodes[&intent.object()].children, + view.nodes[¶graph].children + ); + let before = current::current(&persisted); + let after = current::current(edit.as_bytes()); + let mut disk = disk::Disk { + visible: persisted.clone(), + durable: persisted.clone(), + operation: 0, + fail_at: (step[4] != 0).then_some(usize::from(u16::from_le_bytes([step[4], step[5]]))), + write_limit: if step[6] & 1 == 0 { 17 } else { 4096 }, + random: u64::from(step[7]) + 1, + }; + let result = edit.commit(&mut disk); + let observed = current::current(&disk.durable); + match result { + Ok(()) => assert_eq!(observed, after), + Err(error) => { + assert!(observed == before || observed == after); + match error.state { + CommitState::NotCommitted => assert_eq!(observed, before), + CommitState::Committed => assert_eq!(observed, after), + CommitState::Unknown => {} + } + } + } + persisted = disk.durable; + } +}); diff --git a/tools/test_paragraph_edit.py b/tools/test_paragraph_edit.py index 04e36e9ca80be005699aa8ec842c10225bf4f286..e14d5c3ae8d591f8d8469a6dd4cee120ea85e337 100644 --- a/tools/test_paragraph_edit.py +++ b/tools/test_paragraph_edit.py @@ -8,7 +8,7 @@ import unittest import xml.etree.ElementTree as ET from document_model import EXPORTER, ordered_pages, walk -from native_format import native_characters +from native_format import compare_formats, native_characters from native_xml import ns ROOT = Path(__file__).resolve().parent.parent @@ -17,6 +17,65 @@ compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] class ParagraphEditTest(unittest.TestCase): + def test_rust_splits_match_native_controls_and_preserve_empty_typing_styles(self): + fixture = FIXTURE / 'rust-split' + manifest = json.loads((fixture / 'manifest.json').read_text()) + with TemporaryDirectory() as temporary: + for source, capture in [('candidate', 'native'), ('native/notebook', 'native'), + ('typed/notebook', 'typed')]: + native = Path(temporary) / source.replace('/', '-') / 'read' + shutil.copytree(fixture / capture / 'read', native) + compare(fixture / source, native) + subprocess.run([EXPORTER, fixture / 'candidate/synthetic.one', Path(temporary) / 'model'], check=True) + model = json.loads((Path(temporary) / 'model/document.json').read_text()) + subprocess.run([EXPORTER, fixture / 'native/notebook/synthetic.one', Path(temporary) / 'saved'], check=True) + saved = json.loads((Path(temporary) / 'saved/document.json').read_text()) + views = {r['nodes'][r['roots']['2']]['kind']['title']: (r, page) + for _, _, r, page in ordered_pages(model)} + saved_views = {r['nodes'][r['roots']['2']]['kind']['title']: (r, page) + for _, _, r, page in ordered_pages(saved)} + text_nodes = {} + for name, (revision, page) in views.items(): + current, current_page = saved_views[name] + self.assertEqual(page, current_page) + text_nodes[name] = [] + for outline in revision['nodes'][page]['children']: + if revision['nodes'][outline]['kind']['type'] != 'Outline': continue + for oid, node in walk(revision, outline): + self.assertEqual(current['nodes'][oid]['children'], node['children']) + self.assertEqual(current['nodes'][oid]['content'], node['content']) + if node['kind']['type'] == 'RichText': text_nodes[name].append(node) + captures = {} + for phase, folder in [('native', fixture / 'native/read'), ('typed', fixture / 'typed/read'), + ('control', FIXTURE / 'cold-split/read')]: + captures[phase] = {} + for path in folder.glob('page-*.xml'): + page = ET.parse(path).getroot() + captures[phase][page.get('name')] = native_characters(page, page.findall('one:Outline', ns)) + self.assertEqual(len(captures[phase]), 14) + cases = [c for c in manifest['cases'] if 'intent' in c] + self.assertEqual(len(cases), 12) + for case in cases: + name = case['case'] + with self.subTest(case=name): + for a, b in zip(captures['native'][name], captures['control'][name], strict=True): + for (c, left), (d, right) in zip(a, b, strict=True): + self.assertEqual(c, d) + for key in left.keys() | right.keys(): + default = 'automatic' if key in ('color', 'highlight') else False + self.assertEqual(left.get(key, default), right.get(key, default)) + selections = json.loads((fixture / 'typed/ui/selections.json').read_text()) + self.assertEqual(len(selections), 4) + for selection in selections: + node = text_nodes[selection['case']][selection['index']] + self.assertEqual(node['kind']['text'], '') + self.assertEqual(len(node['kind']['runs']), 1) + node['kind']['text'] = selection['text'] + node['kind']['runs'][0]['end'] = len(selection['text'].encode('utf-16-le')) // 2 + for name, (revision, page) in views.items(): + _, differences = compare_formats(revision, text_nodes[name], captures['typed'][name]) + self.assertEqual(differences, [], name) + def test_native_split_join_graphs_styles_and_identity_boundaries(self): manifest = json.loads((FIXTURE / 'manifest.json').read_text()) models, native = {}, {}