From 75e67fc117387e3301560b6a3ee21075f3487b71 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 12 Sep 2026 22:55:29 -0700 Subject: [PATCH] feat: notebook structure, change detection, link resolution, reviewed release of uncertain saves Table of contents editing (`onestore::TocEdit`, `PreparedEdit::table_of_contents`) adds, renames, colours, orders and removes section and group entries as one revision, and `notebook::session::Notebook` creates sections and groups, renames, colours, reorders and deletes into `OneNote_RecycleBin` the way OneNote does. Two writer rules came out of the cold reads: OneNote re-identifies any file whose header ancestor is not the parent TOC's identity or whose name CRC is not its file or folder name (`onestore::place_file` sets both on create, rename and delete), and a TOC revision manifest carries one global id table, or OneNote applies a revised entry's order to the wrong entry. `Notebook::refresh` now reports what another client changed, keyed by file identity (`Change::{Added, Removed, Moved, Reordered}`), keeping the last catalog when the notebook is unreachable. `page::link::parse_internal_link` and `Notebook::find_page` resolve stored links by identity across renamed, moved and stale sections. A stored attachment's shown name, source path and icon size edit in place. With the `protected` feature, `Notebook::unlock` reads a locked section's pages. An uncertain publication is released by review, never replay: `Replica::release_attempt` (`Section::release`) exports the branch to a new archive, then either continues from a reviewed page under the same id or abandons the branch, whose ids report `EditStatus::Archived { archive }` (cache schema 13). No receipt is ever written for the uncertain attempt. Cold reads: `corpus/notebook-edit/{structured,deleted}` (written order, colours, recycle bin, stable identities), `corpus/notebook-edit/native-reorder` (COM cannot reorder), `corpus/attachment-edit/renamed`. Assisted-by: claude-fable-5.1 --- corpus/attachment-edit/README.md | 12 + .../renamed/candidate/Open Notebook.onetoc2 | Bin 0 -> 1612 bytes .../renamed/candidate/synthetic.one | Bin 0 -> 21896 bytes .../renamed/cold/commands.jsonl | 3 + .../attachment-edit/renamed/cold/machine.json | 1 + .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3780 bytes .../renamed/cold/notebook/synthetic.one | Bin 0 -> 21896 bytes ...0b7c57eb3b6110a06aa2114e06d69a7.attachment | 1 + .../renamed/cold/read/environment.json | 7 + .../renamed/cold/read/hierarchy.xml | 2 + .../renamed/cold/read/page-000.xml | 8 + .../renamed/cold/read/payloads.json | 10 + corpus/attachment-edit/renamed/cold/run.json | 18 + .../renamed/cold/scripts/cold.ps1 | 27 ++ .../renamed/cold/scripts/read.ps1 | 142 ++++++ .../attachment-edit/renamed/cold/source.json | 14 + .../renamed/cold/teardown.json | 1 + corpus/notebook-edit/README.md | 40 ++ .../deleted/candidate/Kept/First.one | Bin 0 -> 3164 bytes .../candidate/Kept/Open Notebook.onetoc2 | Bin 0 -> 4120 bytes .../candidate/OneNote_RecycleBin/First.one | Bin 0 -> 3212 bytes .../OneNote_RecycleBin/Open Notebook.onetoc2 | Bin 0 -> 3824 bytes .../deleted/candidate/Open Notebook.onetoc2 | Bin 0 -> 6328 bytes .../deleted/candidate/Renamed.one | Bin 0 -> 5504 bytes .../notebook-edit/deleted/cold/commands.jsonl | 3 + .../notebook-edit/deleted/cold/machine.json | 1 + .../deleted/cold/notebook/Kept/First.one | Bin 0 -> 3164 bytes .../cold/notebook/Kept/Open Notebook.onetoc2 | Bin 0 -> 4120 bytes .../notebook/OneNote_RecycleBin/First.one | Bin 0 -> 3212 bytes .../OneNote_RecycleBin/Open Notebook.onetoc2 | Bin 0 -> 3824 bytes .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 7392 bytes .../deleted/cold/notebook/Renamed.one | Bin 0 -> 5504 bytes .../deleted/cold/read/environment.json | 7 + .../deleted/cold/read/hierarchy.xml | 2 + .../deleted/cold/read/page-000.xml | 2 + .../deleted/cold/read/page-001.xml | 2 + .../deleted/cold/read/page-002.xml | 2 + .../deleted/cold/read/payloads.json | 3 + corpus/notebook-edit/deleted/cold/run.json | 18 + .../deleted/cold/scripts/cold.ps1 | 27 ++ .../deleted/cold/scripts/read.ps1 | 142 ++++++ corpus/notebook-edit/deleted/cold/source.json | 38 ++ .../notebook-edit/deleted/cold/teardown.json | 1 + .../native-reorder/commands.jsonl | 4 + .../notebook-edit/native-reorder/machine.json | 1 + .../notebook-reordered/Group/Inner.one | Bin 0 -> 4016 bytes .../Group/Open Notebook.onetoc2 | Bin 0 -> 3752 bytes .../notebook-reordered/Open Notebook.onetoc2 | Bin 0 -> 4116 bytes .../notebook-reordered/Second.one | Bin 0 -> 4016 bytes .../notebook-reordered/Third.one | Bin 0 -> 4016 bytes .../notebook-reordered/links.one | Bin 0 -> 6128 bytes .../native-reorder/notebook/Group/Inner.one | Bin 0 -> 4016 bytes .../notebook/Group/Open Notebook.onetoc2 | Bin 0 -> 3752 bytes .../notebook/Open Notebook.onetoc2 | Bin 0 -> 4116 bytes .../native-reorder/notebook/Second.one | Bin 0 -> 4016 bytes .../native-reorder/notebook/Third.one | Bin 0 -> 4016 bytes .../native-reorder/notebook/links.one | Bin 0 -> 6128 bytes .../native-reorder/read/environment.json | 7 + .../native-reorder/read/hierarchy.xml | 2 + .../native-reorder/read/page-000.xml | 3 + .../native-reorder/read/payloads.json | 3 + .../native-reorder/reorder-00-opened.xml | 2 + .../native-reorder/reorder-01-update.xml | 1 + .../native-reorder/reorder-02-reordered.xml | 2 + .../native-reorder/reorder-03-reopened.xml | 2 + corpus/notebook-edit/native-reorder/run.json | 19 + .../native-reorder/scripts/author.ps1 | 50 ++ .../native-reorder/scripts/cold.ps1 | 27 ++ .../native-reorder/scripts/read.ps1 | 142 ++++++ .../notebook-edit/native-reorder/source.json | 38 ++ .../native-reorder/teardown.json | 1 + .../native-structure/commands.jsonl | 4 + .../native-structure/machine.json | 1 + .../notebook-renamed/Group/Inner.one | Bin 0 -> 4016 bytes .../Group/Open Notebook.onetoc2 | Bin 0 -> 3752 bytes .../notebook-renamed/Open Notebook.onetoc2 | Bin 0 -> 4116 bytes .../notebook-renamed/Second.one | Bin 0 -> 4016 bytes .../notebook-renamed/Third.one | Bin 0 -> 4016 bytes .../notebook-renamed/links.one | Bin 0 -> 6128 bytes .../native-structure/notebook/Group/Inner.one | Bin 0 -> 4016 bytes .../notebook/Group/Open Notebook.onetoc2 | Bin 0 -> 3752 bytes .../OneNote_DeletedPages.one | Bin 0 -> 4016 bytes .../OneNote_RecycleBin/Open Notebook.onetoc2 | Bin 0 -> 3856 bytes .../notebook/OneNote_RecycleBin/Third.one | Bin 0 -> 4016 bytes .../notebook/Open Notebook.onetoc2 | Bin 0 -> 5276 bytes .../native-structure/notebook/Second.one | Bin 0 -> 4016 bytes .../native-structure/notebook/links.one | Bin 0 -> 6128 bytes .../native-structure/read/environment.json | 7 + .../native-structure/read/hierarchy.xml | 2 + .../native-structure/read/page-000.xml | 3 + .../native-structure/read/payloads.json | 3 + .../notebook-edit/native-structure/run.json | 19 + .../native-structure/scripts/author.ps1 | 71 +++ .../native-structure/scripts/cold.ps1 | 27 ++ .../native-structure/scripts/read.ps1 | 142 ++++++ .../native-structure/source.json | 14 + .../native-structure/structure-00-opened.xml | 2 + .../native-structure/structure-01-created.xml | 2 + .../native-structure/structure-02-update.xml | 1 + .../native-structure/structure-03-renamed.xml | 2 + .../structure-03b-reopened.xml | 2 + .../native-structure/structure-04-deleted.xml | 2 + .../native-structure/teardown.json | 1 + .../structured/candidate/First.one | Bin 0 -> 3212 bytes .../structured/candidate/Kept/Inner.one | Bin 0 -> 3164 bytes .../candidate/Kept/Open Notebook.onetoc2 | Bin 0 -> 3824 bytes .../candidate/Open Notebook.onetoc2 | Bin 0 -> 5408 bytes .../structured/candidate/Renamed.one | Bin 0 -> 5504 bytes .../structured/cold/commands.jsonl | 3 + .../structured/cold/machine.json | 1 + .../structured/cold/notebook/First.one | Bin 0 -> 3212 bytes .../structured/cold/notebook/Kept/Inner.one | Bin 0 -> 3164 bytes .../cold/notebook/Kept/Open Notebook.onetoc2 | Bin 0 -> 3824 bytes .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 6472 bytes .../structured/cold/notebook/Renamed.one | Bin 0 -> 5504 bytes .../structured/cold/read/environment.json | 7 + .../structured/cold/read/hierarchy.xml | 2 + .../structured/cold/read/page-000.xml | 2 + .../structured/cold/read/page-001.xml | 2 + .../structured/cold/read/page-002.xml | 2 + .../structured/cold/read/payloads.json | 3 + corpus/notebook-edit/structured/cold/run.json | 18 + .../structured/cold/scripts/cold.ps1 | 27 ++ .../structured/cold/scripts/read.ps1 | 142 ++++++ .../notebook-edit/structured/cold/source.json | 32 ++ .../structured/cold/teardown.json | 1 + crates/notebook/README.md | 45 +- crates/notebook/examples/recovery_probe.rs | 1 + crates/notebook/src/lib.rs | 5 +- crates/notebook/src/schema.rs | 4 + crates/notebook/src/session.rs | 440 +++++++++++++++++- crates/notebook/src/sync.rs | 131 +++++- crates/notebook/tests/changes.rs | 78 ++++ crates/notebook/tests/links.rs | 78 ++++ crates/notebook/tests/protected.rs | 29 ++ crates/notebook/tests/session.rs | 97 ++++ crates/notebook/tests/structure.rs | 173 +++++++ crates/notebook/tests/sync.rs | 36 +- crates/notebook/tests/sync_formatting.rs | 7 +- crates/notebook/tests/sync_outline.rs | 10 +- crates/notebook/tests/sync_page.rs | 9 +- crates/notebook/tests/sync_pages.rs | 9 +- crates/notebook/tests/sync_paragraph.rs | 7 +- crates/notebook/tests/sync_worker.rs | 18 +- crates/onestore/README.md | 5 +- crates/onestore/src/commit.rs | 37 ++ crates/onestore/src/create.rs | 12 +- crates/onestore/src/lib.rs | 5 +- crates/onestore/src/page/link.rs | 43 ++ crates/onestore/src/page/mod.rs | 23 +- crates/onestore/src/page/write.rs | 82 +++- crates/onestore/src/store.rs | 10 +- crates/onestore/src/toc.rs | 226 +++++++++ crates/onestore/src/write.rs | 28 +- crates/onestore/tests/page_attachments.rs | 32 +- crates/onestore/tests/page_links.rs | 28 +- crates/onestore/tests/toc.rs | 108 +++++ tools/native/notebook-reorder.ps1 | 50 ++ tools/native/notebook-structure.ps1 | 71 +++ tools/test_attachment_edit.py | 9 + tools/test_notebook_edit.py | 65 +++ 161 files changed, 3316 insertions(+), 80 deletions(-) create mode 100644 corpus/attachment-edit/renamed/candidate/Open Notebook.onetoc2 create mode 100644 corpus/attachment-edit/renamed/candidate/synthetic.one create mode 100644 corpus/attachment-edit/renamed/cold/commands.jsonl create mode 100644 corpus/attachment-edit/renamed/cold/machine.json create mode 100644 corpus/attachment-edit/renamed/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/attachment-edit/renamed/cold/notebook/synthetic.one create mode 100644 corpus/attachment-edit/renamed/cold/read/af1af4b8987109ce1e335e538910fb3860b7c57eb3b6110a06aa2114e06d69a7.attachment create mode 100644 corpus/attachment-edit/renamed/cold/read/environment.json create mode 100644 corpus/attachment-edit/renamed/cold/read/hierarchy.xml create mode 100644 corpus/attachment-edit/renamed/cold/read/page-000.xml create mode 100644 corpus/attachment-edit/renamed/cold/read/payloads.json create mode 100644 corpus/attachment-edit/renamed/cold/run.json create mode 100644 corpus/attachment-edit/renamed/cold/scripts/cold.ps1 create mode 100644 corpus/attachment-edit/renamed/cold/scripts/read.ps1 create mode 100644 corpus/attachment-edit/renamed/cold/source.json create mode 100644 corpus/attachment-edit/renamed/cold/teardown.json create mode 100644 corpus/notebook-edit/README.md create mode 100644 corpus/notebook-edit/deleted/candidate/Kept/First.one create mode 100644 corpus/notebook-edit/deleted/candidate/Kept/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/deleted/candidate/OneNote_RecycleBin/First.one create mode 100644 corpus/notebook-edit/deleted/candidate/OneNote_RecycleBin/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/deleted/candidate/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/deleted/candidate/Renamed.one create mode 100644 corpus/notebook-edit/deleted/cold/commands.jsonl create mode 100644 corpus/notebook-edit/deleted/cold/machine.json create mode 100644 corpus/notebook-edit/deleted/cold/notebook/Kept/First.one create mode 100644 corpus/notebook-edit/deleted/cold/notebook/Kept/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/deleted/cold/notebook/OneNote_RecycleBin/First.one create mode 100644 corpus/notebook-edit/deleted/cold/notebook/OneNote_RecycleBin/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/deleted/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/deleted/cold/notebook/Renamed.one create mode 100644 corpus/notebook-edit/deleted/cold/read/environment.json create mode 100644 corpus/notebook-edit/deleted/cold/read/hierarchy.xml create mode 100644 corpus/notebook-edit/deleted/cold/read/page-000.xml create mode 100644 corpus/notebook-edit/deleted/cold/read/page-001.xml create mode 100644 corpus/notebook-edit/deleted/cold/read/page-002.xml create mode 100644 corpus/notebook-edit/deleted/cold/read/payloads.json create mode 100644 corpus/notebook-edit/deleted/cold/run.json create mode 100644 corpus/notebook-edit/deleted/cold/scripts/cold.ps1 create mode 100644 corpus/notebook-edit/deleted/cold/scripts/read.ps1 create mode 100644 corpus/notebook-edit/deleted/cold/source.json create mode 100644 corpus/notebook-edit/deleted/cold/teardown.json create mode 100644 corpus/notebook-edit/native-reorder/commands.jsonl create mode 100644 corpus/notebook-edit/native-reorder/machine.json create mode 100644 corpus/notebook-edit/native-reorder/notebook-reordered/Group/Inner.one create mode 100644 corpus/notebook-edit/native-reorder/notebook-reordered/Group/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/native-reorder/notebook-reordered/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/native-reorder/notebook-reordered/Second.one create mode 100644 corpus/notebook-edit/native-reorder/notebook-reordered/Third.one create mode 100644 corpus/notebook-edit/native-reorder/notebook-reordered/links.one create mode 100644 corpus/notebook-edit/native-reorder/notebook/Group/Inner.one create mode 100644 corpus/notebook-edit/native-reorder/notebook/Group/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/native-reorder/notebook/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/native-reorder/notebook/Second.one create mode 100644 corpus/notebook-edit/native-reorder/notebook/Third.one create mode 100644 corpus/notebook-edit/native-reorder/notebook/links.one create mode 100644 corpus/notebook-edit/native-reorder/read/environment.json create mode 100644 corpus/notebook-edit/native-reorder/read/hierarchy.xml create mode 100644 corpus/notebook-edit/native-reorder/read/page-000.xml create mode 100644 corpus/notebook-edit/native-reorder/read/payloads.json create mode 100644 corpus/notebook-edit/native-reorder/reorder-00-opened.xml create mode 100644 corpus/notebook-edit/native-reorder/reorder-01-update.xml create mode 100644 corpus/notebook-edit/native-reorder/reorder-02-reordered.xml create mode 100644 corpus/notebook-edit/native-reorder/reorder-03-reopened.xml create mode 100644 corpus/notebook-edit/native-reorder/run.json create mode 100644 corpus/notebook-edit/native-reorder/scripts/author.ps1 create mode 100644 corpus/notebook-edit/native-reorder/scripts/cold.ps1 create mode 100644 corpus/notebook-edit/native-reorder/scripts/read.ps1 create mode 100644 corpus/notebook-edit/native-reorder/source.json create mode 100644 corpus/notebook-edit/native-reorder/teardown.json create mode 100644 corpus/notebook-edit/native-structure/commands.jsonl create mode 100644 corpus/notebook-edit/native-structure/machine.json create mode 100644 corpus/notebook-edit/native-structure/notebook-renamed/Group/Inner.one create mode 100644 corpus/notebook-edit/native-structure/notebook-renamed/Group/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/native-structure/notebook-renamed/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/native-structure/notebook-renamed/Second.one create mode 100644 corpus/notebook-edit/native-structure/notebook-renamed/Third.one create mode 100644 corpus/notebook-edit/native-structure/notebook-renamed/links.one create mode 100644 corpus/notebook-edit/native-structure/notebook/Group/Inner.one create mode 100644 corpus/notebook-edit/native-structure/notebook/Group/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/OneNote_DeletedPages.one create mode 100644 corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/Third.one create mode 100644 corpus/notebook-edit/native-structure/notebook/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/native-structure/notebook/Second.one create mode 100644 corpus/notebook-edit/native-structure/notebook/links.one create mode 100644 corpus/notebook-edit/native-structure/read/environment.json create mode 100644 corpus/notebook-edit/native-structure/read/hierarchy.xml create mode 100644 corpus/notebook-edit/native-structure/read/page-000.xml create mode 100644 corpus/notebook-edit/native-structure/read/payloads.json create mode 100644 corpus/notebook-edit/native-structure/run.json create mode 100644 corpus/notebook-edit/native-structure/scripts/author.ps1 create mode 100644 corpus/notebook-edit/native-structure/scripts/cold.ps1 create mode 100644 corpus/notebook-edit/native-structure/scripts/read.ps1 create mode 100644 corpus/notebook-edit/native-structure/source.json create mode 100644 corpus/notebook-edit/native-structure/structure-00-opened.xml create mode 100644 corpus/notebook-edit/native-structure/structure-01-created.xml create mode 100644 corpus/notebook-edit/native-structure/structure-02-update.xml create mode 100644 corpus/notebook-edit/native-structure/structure-03-renamed.xml create mode 100644 corpus/notebook-edit/native-structure/structure-03b-reopened.xml create mode 100644 corpus/notebook-edit/native-structure/structure-04-deleted.xml create mode 100644 corpus/notebook-edit/native-structure/teardown.json create mode 100644 corpus/notebook-edit/structured/candidate/First.one create mode 100644 corpus/notebook-edit/structured/candidate/Kept/Inner.one create mode 100644 corpus/notebook-edit/structured/candidate/Kept/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/structured/candidate/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/structured/candidate/Renamed.one create mode 100644 corpus/notebook-edit/structured/cold/commands.jsonl create mode 100644 corpus/notebook-edit/structured/cold/machine.json create mode 100644 corpus/notebook-edit/structured/cold/notebook/First.one create mode 100644 corpus/notebook-edit/structured/cold/notebook/Kept/Inner.one create mode 100644 corpus/notebook-edit/structured/cold/notebook/Kept/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/structured/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/notebook-edit/structured/cold/notebook/Renamed.one create mode 100644 corpus/notebook-edit/structured/cold/read/environment.json create mode 100644 corpus/notebook-edit/structured/cold/read/hierarchy.xml create mode 100644 corpus/notebook-edit/structured/cold/read/page-000.xml create mode 100644 corpus/notebook-edit/structured/cold/read/page-001.xml create mode 100644 corpus/notebook-edit/structured/cold/read/page-002.xml create mode 100644 corpus/notebook-edit/structured/cold/read/payloads.json create mode 100644 corpus/notebook-edit/structured/cold/run.json create mode 100644 corpus/notebook-edit/structured/cold/scripts/cold.ps1 create mode 100644 corpus/notebook-edit/structured/cold/scripts/read.ps1 create mode 100644 corpus/notebook-edit/structured/cold/source.json create mode 100644 corpus/notebook-edit/structured/cold/teardown.json create mode 100644 crates/notebook/tests/changes.rs create mode 100644 crates/notebook/tests/links.rs create mode 100644 crates/notebook/tests/protected.rs create mode 100644 crates/notebook/tests/structure.rs create mode 100644 crates/onestore/src/toc.rs create mode 100644 crates/onestore/tests/toc.rs create mode 100644 tools/native/notebook-reorder.ps1 create mode 100644 tools/native/notebook-structure.ps1 create mode 100644 tools/test_notebook_edit.py diff --git a/corpus/attachment-edit/README.md b/corpus/attachment-edit/README.md index c6a769a2c5b96c0d89d5c8a0ac19476a575f437d..cb168e20325c7e9341e9e18f62d3776e6e12d523 100644 --- a/corpus/attachment-edit/README.md +++ b/corpus/attachment-edit/README.md @@ -21,3 +21,15 @@ without a VM. Regenerate with `ONESTORE_ATTACHMENT_EXPORT` set to a directory while running the test (it writes `plain/` and `icon/`), then cold-open each with `tools/native_runner.py OUTPUT/plain COLD --expected-pages 1 --collect-notebook`. + +## Renaming a stored attachment + +`renamed/candidate` is the writer's output for +`attachments_need_a_file_name_and_stored_ones_are_renamed_in_place` in +`crates/onestore/tests/page_attachments.rs`: the native attachment of +`corpus/native/cold-05-06-attachment` with its shown name and recorded source +path changed in place (properties `0x1c001c22`, `0x1c001d9c` and +`0x1c001d9d` on the attachment node), payload and preview untouched. +`renamed/cold` is its cold read: `preferredName` and `pathSource` carry the +new values. Regenerate with `ONESTORE_ATTACHMENT_RENAME_EXPORT` and cold-open +with `--expected-pages 1`. diff --git a/corpus/attachment-edit/renamed/candidate/Open Notebook.onetoc2 b/corpus/attachment-edit/renamed/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..96d7cf292d064465a5f0c577bfb2f67eec013527 GIT binary patch literal 1612 zcmZ3e|KIuM`!b(-j|5(YE0x9n%4gesZl9|Au1eRu{gq39F`xnayR!tOCw}!@dO>{R z+Y{?7_@H=3%whOXkJEF~}Ye4RYhMZS_-- G^#A}3+}jQS literal 0 HcmV?d00001 diff --git a/corpus/attachment-edit/renamed/candidate/synthetic.one b/corpus/attachment-edit/renamed/candidate/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..c0cc0a52087bcc02f3c098b72f5b72069dfd67a0 GIT binary patch literal 21896 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 literal 0 HcmV?d00001 diff --git a/corpus/attachment-edit/renamed/cold/commands.jsonl b/corpus/attachment-edit/renamed/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..e1f9f4e16cb3b0c0f8a00fafe9f0470de3bc08be --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/commands.jsonl @@ -0,0 +1,3 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-0A713CC8 -ExpectedPages 1", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/attachment-edit/renamed/cold/machine.json b/corpus/attachment-edit/renamed/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..8d1bab0ce7d6c1cf413b1ec8932880549d8fa5e4 --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-0a713cc8", "hostname": "ONE-M6-0A713CC8"} diff --git a/corpus/attachment-edit/renamed/cold/notebook/Open Notebook.onetoc2 b/corpus/attachment-edit/renamed/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..180ba6770401181374cac8454d062d1a295728e3 GIT binary patch literal 3780 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 literal 0 HcmV?d00001 diff --git a/corpus/attachment-edit/renamed/cold/read/af1af4b8987109ce1e335e538910fb3860b7c57eb3b6110a06aa2114e06d69a7.attachment b/corpus/attachment-edit/renamed/cold/read/af1af4b8987109ce1e335e538910fb3860b7c57eb3b6110a06aa2114e06d69a7.attachment new file mode 100644 index 0000000000000000000000000000000000000000..66b6151e91f90df6a61a19ef2f4447da278e07b2 --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/read/af1af4b8987109ce1e335e538910fb3860b7c57eb3b6110a06aa2114e06d69a7.attachment @@ -0,0 +1 @@ +Fictitious attachment for native corpus. \ No newline at end of file diff --git a/corpus/attachment-edit/renamed/cold/read/environment.json b/corpus/attachment-edit/renamed/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..0c16a3301cf381b1a9601bce5d23f4fc1d74fe4d --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-0A713CC8", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/attachment-edit/renamed/cold/read/hierarchy.xml b/corpus/attachment-edit/renamed/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..07947089e33a06eb8138a88efc4f064b7df9dcb8 --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/attachment-edit/renamed/cold/read/page-000.xml b/corpus/attachment-edit/renamed/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..d24863d517c8e1def63026be0bb4c21aa08fd864 --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/read/page-000.xml @@ -0,0 +1,8 @@ + +Fictitious: café, 東京, مرحبا]]>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9J1uoA +AAAASUVORK5CYII= diff --git a/corpus/attachment-edit/renamed/cold/read/payloads.json b/corpus/attachment-edit/renamed/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..ba0dea930c14a7ec44406df59e15ed3618073fbc --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/read/payloads.json @@ -0,0 +1,10 @@ +[ + { + "sha256": "af1af4b8987109ce1e335e538910fb3860b7c57eb3b6110a06aa2114e06d69a7", + "name": "renamed 🦀.txt", + "object": "{4E773A8B-F556-4F2B-AA89-7C345FD361D2}{11}{B0}", + "kind": "InsertedFile", + "page": "{C7383ECD-2D94-4CA8-AC10-BE6DDCFFE747}{1}{B0}", + "bytes": 43 + } +] \ No newline at end of file diff --git a/corpus/attachment-edit/renamed/cold/run.json b/corpus/attachment-edit/renamed/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..57488db26ebe5c285f3aa17e24a6b8c9f092f6ff --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/attachment-rename", + "expected_pages": 1, + "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/attachment-edit/renamed/cold/scripts/cold.ps1 b/corpus/attachment-edit/renamed/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/attachment-edit/renamed/cold/scripts/read.ps1 b/corpus/attachment-edit/renamed/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/attachment-edit/renamed/cold/source.json b/corpus/attachment-edit/renamed/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..45256bf6c7e5488a2f4c623994986571a369a437 --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1612, + "sha256": "e95a685134505ff745de97cb2795f289c23329966ed49eead8a2dd4f98b91571", + "mtime_ns": 1789278022646769677 + }, + { + "path": "synthetic.one", + "bytes": 21896, + "sha256": "ff9cba8456136873d2929bba7f2fa01931eb9aa69a1d5ebbaa61a669724e10e5", + "mtime_ns": 1789278022626746201 + } +] diff --git a/corpus/attachment-edit/renamed/cold/teardown.json b/corpus/attachment-edit/renamed/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/attachment-edit/renamed/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/notebook-edit/README.md b/corpus/notebook-edit/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f73cf66b688ad12b7ccc64f4927e9391b8793ccb --- /dev/null +++ b/corpus/notebook-edit/README.md @@ -0,0 +1,40 @@ +# Notebook structure + +`native-structure/` is OneNote 2010 building structure through the COM API on +a Rust-created notebook (`tools/native/notebook-structure.ps1`): it created +two sections and a section group with a section inside, renamed the notebook, +reopened it, and deleted a section; `structure-*.xml` are the hierarchy reads +after each step, `notebook-renamed/` the files before the delete. OneNote +keeps the documented table of contents (MS-ONE 2.2.14): one root object with +an entry array whose entries carry the file identity, order, filename and +colour (`0xffffffff` for sections, absent for groups); a section's colour +lives in its own metadata. Deleting moves the file into `OneNote_RecycleBin`, +a group with its own TOC that the root lists. OneNote also names every file +for its place in the header: `guidAncestor` is the parent TOC's file +identity and `crcName` the CRC of the section file name or the group folder +name. The Rust section `links.one`, created with a zero ancestor, was +re-identified on open and listed a second time under its new identity. + +`native-reorder/` is the COM API asked to move the last root section first +through `UpdateHierarchy` (`tools/native/notebook-reorder.ps1`): the +in-session read shows the new order, but after `SyncHierarchy`, close and +reopen the order is back and the TOC unchanged, so section order cannot be +authored through COM; the order rows below are proved by cold reads of Rust +candidates alone. Section display order is the entry's ordering number +ascending, sections before groups, which the owner's own notebook confirms +(array order there differs from the numbers, and OneNote lists by number). + +`structured/` is the notebook `crates/notebook/tests/structure.rs` builds +through `notebook::session::Notebook` (create sections and a group, rename +both, colour a section, order the root as group, renamed, first), placed the +same way; `deleted/` is the same notebook after deleting a section. Each +`cold/` is a fresh OneNote 2010 read: the hierarchy lists the entries in the +written order with the written colours, the recycle bin carries +`isRecycleBin`, and every file keeps the identity Rust wrote. +Every TOC revision the writer appends carries one global id table; with one +table per object group, as the section writer emits, OneNote applied a +reordered entry's value to the wrong entry. `tools/test_notebook_edit.py` +checks this without a VM. Regenerate with +`NOTEBOOK_STRUCTURE_EXPORT` and `NOTEBOOK_STRUCTURE_EXPORT_DELETED` set to +new directories while running the test, then cold-open each with +`tools/native_runner.py OUTPUT COLD --expected-pages -1 --collect-notebook`. diff --git a/corpus/notebook-edit/deleted/candidate/Kept/First.one b/corpus/notebook-edit/deleted/candidate/Kept/First.one new file mode 100644 index 0000000000000000000000000000000000000000..7c767bf992a6d9a522fb8556f13b14160842e9cf GIT binary patch literal 3164 zcmeHIUu;ul6hGZ&*KKulTi0#w78YpgL?9&~Baygu4H_JU1qjSZjKgf%!~o(Jab$2s zC6#H?1(`bHSinX81f;PK(;*o}%>pk?2vTDpnC!_=VX$ryR6M_a_uDSziVr@@Nlw3e z&+nXb&v*WHWYzlZAH=@!p9rnq7JZ`oLgG^X#*4pvT>W9wJM9C9ZZ}a1RbK9~m3AeT z^+$`k;=?Cmvl-X3RKLrFXOk3Yr<6-^#?H>XA3IksADlD#=ehy+#4T6pyuKWPHuGVz zisv zAD$00@PEw!&xVTeNhgm&N~1!>WeT}$ESc*K#T$DXR-7E4QL(JA_msN*SN~GqWs{sH zQqyUdlkA-f$$nZPdpX;H_Z%MKUl25*KIQurC(VKHNdA({E9XXi?=5=1V{O~f(0C3f2NI87h}+Ig!e3Z^1z{*QzJ#; z(d)l`3psR_Dyt&5hraXOP(Z{bB9lJu#G5{%_kySKQ1Wk25K zzCE9BIg!w=&$|3h(V7^dN50spwXClHCV6zEi|!2DP&f%tD)!lqJjyx-YGR zb}qA*+NhP9X)|r*H{p>Vm|$Pbr)APUT_I~e$7bglDP%5XR+yzflSP=pnn|q`T-*bgj`UmrElYt%v;I2SGajw;dQ{1znN#QgTYzWeIl zeJ}ZX+T5)Z_VNAr{(qU@!)DInuuoug_!GcTy&pL1g8DIusrcamqBM3Ym*7hWAyzDj?2gvHV>*J8W9vx z@9m`2<^is6p45JU52NqcSdO&d{|Q~p?Uu|q<`)|Q)qz}A0(s%xlpGLL29=9H<#*_P F{|z`&UkCsI literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/candidate/Kept/Open Notebook.onetoc2 b/corpus/notebook-edit/deleted/candidate/Kept/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..bd1cabf50275bdbe34b3579bba78a17f3e61dad5 GIT binary patch literal 4120 zcmZ3e|KIuM`!b(-j|5(YE0x8w)OQPB>kjtWt@H73_zt!23~0dq?koZ6iC;aJUJ#%7 z_Qbjy(m;7+3{(Ce2taIRm;jW9iNl1;tv}|9Z+PIfvH6}~h3pLZFAPgQ!Q?>n1Xc)7 z2S^z}*$EIKI4L0jXM!11XR{s6|9{?d-3%LVO@mkom>ZVyJi8F^?5x-Fp90Tz?*G0C zRo5tQGz3Ou2sAJy80E%HVn+1oR5Yti32ZcKa5QE&y z1jHbFKs3mWGbCeIe1Q597C1oRQ7{?;qaiSILZE?#VTvq+%uX2wk!&En4@iTG$gkIh zvaddz?$n#sT(x8IuQX7JAp*pn40%A9%233h$B++ZF)+YNHE_8CElNRk07wr6z{*Wf zc?1##mGy_;PEG%%KEbV_B~oUh|4mkqIKJ{9Vj4tn0jOw&mG}?`ITZkF2xws_0g?q812PGOL3ILF!b?iz IFoKnn00SylSpWb4 literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/candidate/OneNote_RecycleBin/First.one b/corpus/notebook-edit/deleted/candidate/OneNote_RecycleBin/First.one new file mode 100644 index 0000000000000000000000000000000000000000..65b5f1f600f5810df63e0cbca775851580602c07 GIT binary patch literal 3212 zcmeHIe@I<*6hF`BZ(sJ*r{ppmZKo?N33b%ZV0~e zk*>nb^+Qb2Af)Gn%z@j~z$n7X$cjmnL==xUmUAO;eLnBrt9NhL5%gy}aPEEge9rgt zJ>T;^=e)Ux_cnND`icjSV=-dJKJQW^BFbKNNN!{=!=`*;=Uk z-+151!2dM^=naMKW0ef2I4hm9+U1lKJ1oa@hX?mP-kA5^#3x5?Yl!Jk8h$U{mUq^o zcatbqIhrhvmaXC#b&4YuYpAdE&*4d-C*7C1E0QIR+*$p1bsm{^6}n?;_D<)#Ja`Sa zO#HPl?>$u7LvPB^eo+R?eyU$w_@Klymbxdpsj-Rt>+PbX0He=mXI}rht;17vy!n?! ztJnG~?euAVYD2EIxp7Bt_{5uc^p%FCVoh(IxFwjYFk_&R&@c)0$14OgHP8isbLVym zQcpy%Tmy3eFni%x8N{uIK`oqd6nsn3$d;qy@nbFed17dUII2YnjF$wIwUeTxN9`_~ zK6Pq`^6Wz=5?=@s7=~Bw3wy??s;S3MX}*7%+71ik+!N3)#sf0GUY=gN&;|;tV4ZXl?^4-U%MAjzj{4h zL0z(zSv~ih|HC&L)Roh^t)OM&!sR)#6vzRoky@z}msHCW@{Cl%s#h@C4Hc=Z!gsJ7 z0w;T;K3SYu&@JuXmC-XAnk)6-s*>lyDzfqp{){TC;S;{ZXLX7#7O^|fL-Eigs1vI5 zTf*VKd|)LT!9jQCcD|LEjouO^5e{oK4fXRfN8~-2EUp)!5r}<>k3UGLBw>wuuJzbI zZmpy&I>kRG0&Q0=BIG;*~XZvHvL+)?Q-`6#p*kwEwR1kXwGdG@Vs^7D&8K%tYRAlG z5F;hK8O96zck_v!6wW8J^Kw3ga}+|(sV~#{q|bM?n$h1lak*AftQxb~N1sa?H+{_Z z4@O*OYZ=P*?vaH^8B?yVq`=}KZY-X_dLf6|K6cn6p!`2$6{Fnz8E5};BJg%-*KdM( T!K@O-#hc;H`R*)EFmnF`a!6)E literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/candidate/OneNote_RecycleBin/Open Notebook.onetoc2 b/corpus/notebook-edit/deleted/candidate/OneNote_RecycleBin/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..9960294c46ef3dcf7aa082edaf66f88376cc1d6a GIT binary patch literal 3824 zcmZ3e|KIuM`!b(-j|5(YE0x8&tmFAMXNitq&kNS=!eNIWF`xnayR!tOCw}!@dO>{R z+Y{?Kf&ZhQNpnfd)o~Z!!!j5WgS!^8M?^s<0)3(4Y`0U}%^*xkA*qtjlRmhO9Qf=Ib0zLkyjL9XkbuD01`leZoGy7LxWxgLk9y3 z#1H`>#RbF=tspYefS~~P`GmdG0453 zatmY+hz7ZFhGguD4^ThC0tYBO3PwXMB$YdyDC}t>O&|}DF$YV$aior@XaJd34NOXX`inw z2h{-}W$48wtPa?~36ul^P|*r2@gWX!Dgf3H(85pxBnvbKWD*F2>I5K#t9%887D$=x Y{53H^Y7~ryz-R~z(-1%|13+E`0Q_nGLjV8( literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/candidate/Open Notebook.onetoc2 b/corpus/notebook-edit/deleted/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..f5f3c075ffed0e96eed2e99ee11517b704b4de48 GIT binary patch literal 6328 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 S`N1Cne+2$35zzMl`1B9kjmek* literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/cold/commands.jsonl b/corpus/notebook-edit/deleted/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..250d639f4fa7b6432ed08c3040cc5a6007f54902 --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/commands.jsonl @@ -0,0 +1,3 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-3574EFF5 -ExpectedPages -1", "exit": 0, "stdout": "Read 3 sections and 3 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/notebook-edit/deleted/cold/machine.json b/corpus/notebook-edit/deleted/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..b5dfe53f9cdcc6be1289b2ef5f323a3939ee0cfc --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-3574eff5", "hostname": "ONE-M6-3574EFF5"} diff --git a/corpus/notebook-edit/deleted/cold/notebook/Kept/First.one b/corpus/notebook-edit/deleted/cold/notebook/Kept/First.one new file mode 100644 index 0000000000000000000000000000000000000000..7c767bf992a6d9a522fb8556f13b14160842e9cf GIT binary patch literal 3164 zcmeHIUu;ul6hGZ&*KKulTi0#w78YpgL?9&~Baygu4H_JU1qjSZjKgf%!~o(Jab$2s zC6#H?1(`bHSinX81f;PK(;*o}%>pk?2vTDpnC!_=VX$ryR6M_a_uDSziVr@@Nlw3e z&+nXb&v*WHWYzlZAH=@!p9rnq7JZ`oLgG^X#*4pvT>W9wJM9C9ZZ}a1RbK9~m3AeT z^+$`k;=?Cmvl-X3RKLrFXOk3Yr<6-^#?H>XA3IksADlD#=ehy+#4T6pyuKWPHuGVz zisv zAD$00@PEw!&xVTeNhgm&N~1!>WeT}$ESc*K#T$DXR-7E4QL(JA_msN*SN~GqWs{sH zQqyUdlkA-f$$nZPdpX;H_Z%MKUl25*KIQurC(VKHNdA({E9XXi?=5=1V{O~f(0C3f2NI87h}+Ig!e3Z^1z{*QzJ#; z(d)l`3psR_Dyt&5hraXOP(Z{bB9lJu#G5{%_kySKQ1Wk25K zzCE9BIg!w=&$|3h(V7^dN50spwXClHCV6zEi|!2DP&f%tD)!lqJjyx-YGR zb}qA*+NhP9X)|r*H{p>Vm|$Pbr)APUT_I~e$7bglDP%5XR+yzflSP=pnn|q`T-*bgj`UmrElYt%v;I2SGajw;dQ{1znN#QgTYzWeIl zeJ}ZX+T5)Z_VNAr{(qU@!)DInuuoug_!GcTy&pL1g8DIusrcamqBM3Ym*7hWAyzDj?2gvHV>*J8W9vx z@9m`2<^is6p45JU52NqcSdO&d{|Q~p?Uu|q<`)|Q)qz}A0(s%xlpGLL29=9H<#*_P F{|z`&UkCsI literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/cold/notebook/Kept/Open Notebook.onetoc2 b/corpus/notebook-edit/deleted/cold/notebook/Kept/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..bd1cabf50275bdbe34b3579bba78a17f3e61dad5 GIT binary patch literal 4120 zcmZ3e|KIuM`!b(-j|5(YE0x8w)OQPB>kjtWt@H73_zt!23~0dq?koZ6iC;aJUJ#%7 z_Qbjy(m;7+3{(Ce2taIRm;jW9iNl1;tv}|9Z+PIfvH6}~h3pLZFAPgQ!Q?>n1Xc)7 z2S^z}*$EIKI4L0jXM!11XR{s6|9{?d-3%LVO@mkom>ZVyJi8F^?5x-Fp90Tz?*G0C zRo5tQGz3Ou2sAJy80E%HVn+1oR5Yti32ZcKa5QE&y z1jHbFKs3mWGbCeIe1Q597C1oRQ7{?;qaiSILZE?#VTvq+%uX2wk!&En4@iTG$gkIh zvaddz?$n#sT(x8IuQX7JAp*pn40%A9%233h$B++ZF)+YNHE_8CElNRk07wr6z{*Wf zc?1##mGy_;PEG%%KEbV_B~oUh|4mkqIKJ{9Vj4tn0jOw&mG}?`ITZkF2xws_0g?q812PGOL3ILF!b?iz IFoKnn00SylSpWb4 literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/cold/notebook/OneNote_RecycleBin/First.one b/corpus/notebook-edit/deleted/cold/notebook/OneNote_RecycleBin/First.one new file mode 100644 index 0000000000000000000000000000000000000000..65b5f1f600f5810df63e0cbca775851580602c07 GIT binary patch literal 3212 zcmeHIe@I<*6hF`BZ(sJ*r{ppmZKo?N33b%ZV0~e zk*>nb^+Qb2Af)Gn%z@j~z$n7X$cjmnL==xUmUAO;eLnBrt9NhL5%gy}aPEEge9rgt zJ>T;^=e)Ux_cnND`icjSV=-dJKJQW^BFbKNNN!{=!=`*;=Uk z-+151!2dM^=naMKW0ef2I4hm9+U1lKJ1oa@hX?mP-kA5^#3x5?Yl!Jk8h$U{mUq^o zcatbqIhrhvmaXC#b&4YuYpAdE&*4d-C*7C1E0QIR+*$p1bsm{^6}n?;_D<)#Ja`Sa zO#HPl?>$u7LvPB^eo+R?eyU$w_@Klymbxdpsj-Rt>+PbX0He=mXI}rht;17vy!n?! ztJnG~?euAVYD2EIxp7Bt_{5uc^p%FCVoh(IxFwjYFk_&R&@c)0$14OgHP8isbLVym zQcpy%Tmy3eFni%x8N{uIK`oqd6nsn3$d;qy@nbFed17dUII2YnjF$wIwUeTxN9`_~ zK6Pq`^6Wz=5?=@s7=~Bw3wy??s;S3MX}*7%+71ik+!N3)#sf0GUY=gN&;|;tV4ZXl?^4-U%MAjzj{4h zL0z(zSv~ih|HC&L)Roh^t)OM&!sR)#6vzRoky@z}msHCW@{Cl%s#h@C4Hc=Z!gsJ7 z0w;T;K3SYu&@JuXmC-XAnk)6-s*>lyDzfqp{){TC;S;{ZXLX7#7O^|fL-Eigs1vI5 zTf*VKd|)LT!9jQCcD|LEjouO^5e{oK4fXRfN8~-2EUp)!5r}<>k3UGLBw>wuuJzbI zZmpy&I>kRG0&Q0=BIG;*~XZvHvL+)?Q-`6#p*kwEwR1kXwGdG@Vs^7D&8K%tYRAlG z5F;hK8O96zck_v!6wW8J^Kw3ga}+|(sV~#{q|bM?n$h1lak*AftQxb~N1sa?H+{_Z z4@O*OYZ=P*?vaH^8B?yVq`=}KZY-X_dLf6|K6cn6p!`2$6{Fnz8E5};BJg%-*KdM( T!K@O-#hc;H`R*)EFmnF`a!6)E literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/cold/notebook/OneNote_RecycleBin/Open Notebook.onetoc2 b/corpus/notebook-edit/deleted/cold/notebook/OneNote_RecycleBin/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..9960294c46ef3dcf7aa082edaf66f88376cc1d6a GIT binary patch literal 3824 zcmZ3e|KIuM`!b(-j|5(YE0x8&tmFAMXNitq&kNS=!eNIWF`xnayR!tOCw}!@dO>{R z+Y{?Kf&ZhQNpnfd)o~Z!!!j5WgS!^8M?^s<0)3(4Y`0U}%^*xkA*qtjlRmhO9Qf=Ib0zLkyjL9XkbuD01`leZoGy7LxWxgLk9y3 z#1H`>#RbF=tspYefS~~P`GmdG0453 zatmY+hz7ZFhGguD4^ThC0tYBO3PwXMB$YdyDC}t>O&|}DF$YV$aior@XaJd34NOXX`inw z2h{-}W$48wtPa?~36ul^P|*r2@gWX!Dgf3H(85pxBnvbKWD*F2>I5K#t9%887D$=x Y{53H^Y7~ryz-R~z(-1%|13+E`0Q_nGLjV8( literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/cold/notebook/Open Notebook.onetoc2 b/corpus/notebook-edit/deleted/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..7547b2573857a848c268836f1800122ef540e851 GIT binary patch literal 7392 zcmeHL4@^~67(eO*>52LT4BUGzG$q{_VOgoI*Y*q`FTUf!m*i4*FN2~CfMB;xA*mP30b3E4>1)@KU-=rZoV4( z_Nm~_myT7RF_PZ%QJUW*Lj3_nAWB8965nFqeWfGo7DJT~UzrQ1qSi;Qr%-`$K#;VV z@b01dm5W#J4VxTOv!r`l>`!s0^+M(QPIh!vSl66Q*WR4HjCIWJ>7iZQKe1xC@@Tj4 zO1&QV&wD`9v69iu^r?vjt)jA?N&_wGTvBSQf1M+`_;O}q<*-qy14tG`PYipBy~;9} zgGI2HSvp%mGUX&PMy(`0y#}yu6B~yyfWi+} z*RPE^aPv~d#g?wd2?i!bWHBexp%-H|dgLX5Ll0LwbC?Gk{~ zNy7NL$8(R2Je;qrKY(N)e=d1g|G)!;1svz~RFSbB=)g(R z&L>5aCM7%e6-1X8BnC0BqT=GX;f-Cu9 zd!y;gKOyfcUqH{;Kgtcr88Gz7$)tqezH-LBm0@|Z)l^Z2oH4`BoKpnU_hbdxOr9}> zj^`8ZCYS+Zc$OfnH$XtxO)>@7u})wunw{C%nFoHf;J3{E@do(ug8%%wYY6xV!9TJ} z*8zU2;J^O4>;(8|!8dfxy#O8tVfC-?^2he|pjiI=<{@Ot20d%*MV)z7)4 zLA~zl01@5#{1M;fCs_}McimW68+L6F$sjk=O+A&R(TzQm-_sc*jRefrQw8`WV>jB% zbq|*(kD@|e7I+v6l2zhwS2`rJaPH{^{R7L^$HfCwBT`WX#hK_;i3Dd|$BCnU6Z+ zo!LV_TRDC4KuoOV8xnKU@WZ{_Up&tX7fc}oL|X6h5C1GCMJO3|cmBajdi#g=h6D*e zpA-o7`6ShC_NDrJ9lPw#-|L?7z~>|4v9I!=HLul{xW~V?d+`35(_p1mj7ZGsKEz{@ zxTvV(fmjs@T__pndD@|K3!UaZca4cC4=Sv+In0j|{;pdO$(IYO@``7J_ zQt0@(MAQWF4BZwYIEsl9e;y|iYH^ZnqE6>DJ?EP5w*rI1O3dAtT1?x4^F?&hUKh4>niw>i3XsyUh;at>w!CX!1Dkp GIPn+RFwPYK literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/cold/notebook/Renamed.one b/corpus/notebook-edit/deleted/cold/notebook/Renamed.one new file mode 100644 index 0000000000000000000000000000000000000000..31fba4276c85f0e413b6cb036992a8102c31d014 GIT binary patch literal 5504 zcmeHJeN2^Q6hBuGFCWtnDL&K4Vn(X_ghV@2opKJNoB*H{1Xuj-!dd7t+= zU(Y$`Ip;jj_~w@mHjIAc=;|)48Gg9=>!#%7lIl+zo;}e}67qVQl1e_1KGtH1aeK4d zhojt6gI%MmnXVs_eU<_LizGsuC0?TUufMq`>de)g?su+isAlA85K_+J@SGzzw>ZTi}7B^xW)iZu+Qs$*-no_wKr7#FFc=JFmIg11F$A-a<+O)4=o8m!6*c<^MrdhN=tvMfiVGVVUNJeE+9YA=DfP9w%K0evSK ztqj^gEY^q_yhilQEV&58URF)$aR6RZ?zxn@ixC6$(w_%_WU%ff#n)aa0K^WGatuDbV{1wnEwj=nI>xTXf_Qq(I`@f_mWH4B`lLVW~M5SZ8NY25rK9i z8Uli@YJ$1Scn+jm)`42sXD8bR z9x2TuTfx1QM<(r~YI4#W^d_(5{ldShd^FGIIr#t^g@v*AhYH8@^r2pQkFA%g*Jjzy@&zy*4QcT0FPj{bxt3CR6Al=M)(oW`!9>7guiWM z0a;iKZ-TX~;a|l>@bz~8m@4=bm-_7p63P*8E04AM`u$uZ7BB(tgCu?ZT9z<|A%h3( z4b@8)XhcXN6~gqyR*i_Bfqn1O6Gt^-5)*J}OC|AA54p@ZXNEW)I876AZQvjtON#vV zp&_s9{#&W7tJ3X>ZDX+uUQd#8T$!KSwcjCu+z zg?1wv4iKRd{pN1|%BLXujgi0TH>1I&$BI}w2Qy5swz zzdF`rXL-ukJb3KG!i(q;_j2F!yIqAi{w{cutFLb3z9#KLTf+Xh>&^}rZGU%nd46Nn z$A?~Y^#209(R&+sBHVS)Ydd>*ZRFLR?z#iLe;yZi7Bm_4UBnB0752d=>^a?5FRvY5 S`N1Cne+2$35zzMl`1B9kjmek* literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/deleted/cold/read/environment.json b/corpus/notebook-edit/deleted/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..7f5a3869aca0881ba74087b19fc76d989b96dd9f --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-3574EFF5", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/notebook-edit/deleted/cold/read/hierarchy.xml b/corpus/notebook-edit/deleted/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..11c04ae171ce950913442b9c554b0669c7ea2229 --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/deleted/cold/read/page-000.xml b/corpus/notebook-edit/deleted/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..a4a0c21aafb018bd9e7d8d975fbfd86a5060fafd --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/read/page-000.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/deleted/cold/read/page-001.xml b/corpus/notebook-edit/deleted/cold/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..fd6f5d67aa0aca95b864e297b61ac902b272d001 --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/read/page-001.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/deleted/cold/read/page-002.xml b/corpus/notebook-edit/deleted/cold/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..25998a0313e6f184bda856249a5d39e6470d903a --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/read/page-002.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/deleted/cold/read/payloads.json b/corpus/notebook-edit/deleted/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/notebook-edit/deleted/cold/run.json b/corpus/notebook-edit/deleted/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..e02a3e8b7cdff223ff00ea4337a5512be238a916 --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/structure3-deleted", + "expected_pages": -1, + "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/notebook-edit/deleted/cold/scripts/cold.ps1 b/corpus/notebook-edit/deleted/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/notebook-edit/deleted/cold/scripts/read.ps1 b/corpus/notebook-edit/deleted/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/notebook-edit/deleted/cold/source.json b/corpus/notebook-edit/deleted/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..b6176cbeb96c675874da6fb07b38478798a8a585 --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/source.json @@ -0,0 +1,38 @@ +[ + { + "path": "Kept/First.one", + "bytes": 3164, + "sha256": "61be453f86b7791923497293f74db57bc7f33b6e8d5fd38ad57631571475a1cf", + "mtime_ns": 1789279283845197267 + }, + { + "path": "Kept/Open Notebook.onetoc2", + "bytes": 4120, + "sha256": "7d734133a1019a00260296bfd143282a7411b667f68956874f7e7ffaa97518b8", + "mtime_ns": 1789279283861857142 + }, + { + "path": "OneNote_RecycleBin/First.one", + "bytes": 3212, + "sha256": "25d2946bbef7c546dc7391f848d43b7b7a4270cf8e0c35264e6939a1a083821c", + "mtime_ns": 1789279283804243010 + }, + { + "path": "OneNote_RecycleBin/Open Notebook.onetoc2", + "bytes": 3824, + "sha256": "8e3ad16bb0ed85c55577d0996a6a3f98df2537f82e3d1d48a35be40283cf4a40", + "mtime_ns": 1789279283820890719 + }, + { + "path": "Open Notebook.onetoc2", + "bytes": 6328, + "sha256": "0c10b7470bb74f2e17b3319707f2f422370fcbde05f09bd7c0e9dd088483c97e", + "mtime_ns": 1789279283837910881 + }, + { + "path": "Renamed.one", + "bytes": 5504, + "sha256": "9ee7bd23c1679ce5035c92adca0416730b149d11bedd1b4a7c181a0186df7007", + "mtime_ns": 1789279283726949659 + } +] diff --git a/corpus/notebook-edit/deleted/cold/teardown.json b/corpus/notebook-edit/deleted/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/notebook-edit/deleted/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/notebook-edit/native-reorder/commands.jsonl b/corpus/notebook-edit/native-reorder/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..21415205c388ac9c9609bd6d17bffb3e9e713ac6 --- /dev/null +++ b/corpus/notebook-edit/native-reorder/commands.jsonl @@ -0,0 +1,4 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\author.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-42A0DA0E", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-42A0DA0E -ExpectedPages -1 -UseCurrentCache", "exit": 0, "stdout": "Read 4 sections and 1 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/notebook-edit/native-reorder/machine.json b/corpus/notebook-edit/native-reorder/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..93855edd8d6c6a5596b870f0dad0465d440d3a09 --- /dev/null +++ b/corpus/notebook-edit/native-reorder/machine.json @@ -0,0 +1 @@ +{"name": "m6-42a0da0e", "hostname": "ONE-M6-42A0DA0E"} diff --git a/corpus/notebook-edit/native-reorder/notebook-reordered/Group/Inner.one b/corpus/notebook-edit/native-reorder/notebook-reordered/Group/Inner.one new file mode 100644 index 0000000000000000000000000000000000000000..ba9b977cddfbdba559da17d844cf9339f4d0983a GIT binary patch literal 4016 zcmaD76jR-EW4Z6TjlmTcG^brwQY~G6vwfq_BBi}kXK{8uU_b-*cV`JmPyFh+^n&=r zw;)KmcO1zyzQ)OdKXWOZn;2x%0Pqt>)XlCD6R`_pQW7%^)=(09L~R zRS7Z(W*Cr$sDP;1zz;JGLPx1DkYK%c&3W<{d;Rqy({(tZ(g!*N&lZ_x_|4ajF1mEQ zcQ&dnRNfZNh5X1ERd$p&8Un*31de?9{&izj*b)J#cQ|qwrZzCD2nuSg}Tm9Tq**4a@hS4DR%9rX+~Z1jK?s z3=#ujwyCzlAU;T32r32=zxy!J6vSr-VqqW#Rn;Kz=2wnkKx!0>hQMeD4Dt|w)&HRU z4FWtkDtQ4JhN(*!RUnFnDT%FogoO{NoC5)o0^E%txf2Zyky|cC_*%?x*`qw?!=}2z zOQ7-)8-R#!0JgR+F{;ekx1=Mi!^dIH(f(yZ$JZ|eNw%ag^dLJ3Y{;Y?CW28w7o>V0 zn)Jr?o%3wbs}VuQ8kyWcSy1VWO^ypF4l+yg1FJuP#x*c87|1Zlq{=Wz zfNBpbsSIHc%+BGq3>#c2FEnK%=Mvu!T1ON&Yj& literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-reorder/notebook-reordered/Group/Open Notebook.onetoc2 b/corpus/notebook-edit/native-reorder/notebook-reordered/Group/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..8b7a7eca827559a46b8bc3d21a19e7bd5da6c092 GIT binary patch literal 3752 zcmZ3e|KIuM`!b(-j|5(YE0x91QhvI0?)+_DtNFHX2{f<#&432%@6HmCp7_;s=>_qL zZ%?edAq|vA#xUjofdIs2feAoqm^e&W`@+rE6X`G9HY&?FZdmv*!^gSv3rGzJfYoq7 zRe}tH83v>wDj;fB@WD)j(9sve>s(LN`ZNgpuAlLA;uKD(bo`u8OKxn?_LzF;g3*_? zmlIKSq4Kt9F62kXsIsHH(GVCOA#mi&_pckP!j=d?y~B~iFtveEWy2~vmG!PM{!Q`M zzJ7cqT*nBMW=Uvhz$QLC0%Ft`qaiTNLjbwX^xkFMbXfFIH!RrJRg7_eDA*dKg+$QqG3v6D<5Iu11jf0K%@Y7BS`v1L&LtA-$nO*F85vHvoZMNF-JjAIf)G{ zsM^PF=X{qDBCjaY(7>Pq40@2#bd!c9r{&`v))X&&DX%xzQbVAjp$8~eQhGn;!D3VA z?f2y$ynXujBuJ~S2Se{^MlM(h3@oofA}1J`xqy<4kP^Hlg`o%8YalxqYBU*sMH)0T zK-4iX$N((?OW+}p4T04kKu0t{Z2u<1pn~Gt1{Q`XvJ5giWf(-Vf%HBg4XRy~R7;oN nY~Sd!NNMlXS)82@Kzc-g*pnd-2vZq~81xwO!7NBG58@yI-K_`A literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-reorder/notebook-reordered/Open Notebook.onetoc2 b/corpus/notebook-edit/native-reorder/notebook-reordered/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..491ef31bae9ed88e2453db2e4c6bbcbd248cd2e2 GIT binary patch literal 4116 zcmeHJeN2>f9RD307hQmd97WwJnP8kCU9d^7#({u<0us|TOG(dw8HwQ$6{{x`2X+le zU}^()4OzU57dzgf=?b!|wqlMN*rc~Xj9$P}&~nlD^W5{?!R;Z}f4TA9e$Vgsef@nu zzqjXWxo<9ZWU63u?GLKS?e`Sy3LP9Nwq;GkcdXVL+AZH_GZ%X7;z^aJ;!5JF!8&Jf@RVeaX`?jRrosTi%0c8 z#wxnD^y%6P){d3#yj4&c9=bLfk>{4!oacS;Wxu^EogUI7tIo1aY|<7>(xnx+=~sZ0 zv&t1r+pA^jebk?!9v5nb&rEB^2aTz{y-nL&{6DRdkxWhPS(d||WBXV>3upUjw3lS! zO=OI$I3;~#Y(~q1;A5wO%d=NHUfNdgEAZ+(#trXDAIQHqHKx5h>s{~3j#<%KGr_n_ zuW(i6Q-3Gants#GFqR|g#CWc~UB61rI2M#iA0pTAki)F==k$%Qjh%}#f2?V=-C58O zqGDWlAuC~Wcrlhn%?<$^JfKYzv;olaa%j+=&=X`~tfg4YD!f?;nEZa^p??35-VpIG zCqs}ICF7bPf~lfedA2YWADv|ULnpj-uJQa3jL5T`{Dk)r0Hk8e7nxHkKK2Qqkgppx z%!3{|HgvsOX@wF*xsse;04$mtzwbChu(sBY@I*1Y^4kV<9SzGn3Db$PafNxc{?6?n3T1uS->TL%%MK zGaG69mGcge;Q`LPkz{fuMe;aoWajw`ju}{yj{T{nus{-#k{{L5s@T+^h#7jjPu7mG zQt0$mW+@~H7u|j$K4~g9p~UieP)&7E^qOcp5_i%eoQUAqlao^&rB#N$l0y|yWj}R8 zhiv7`C{ZO@djOBs2Y3Wm3UEUNzTZWZWfTv_HOod!b#3}Agq1?2DmuOxg0IIS$1_dF z_|7Bao?)h3i?|m+05k}ReL)W$R86vt;?ziuEG0A|4r8^2ox7 z`%lO$Ua#BwbI->&Z!t9uJoEHz2rGqldUw4PGGw^smGSD~-M^>2ciMQ}J5aev3K`IV zpo0(YwYX;&-tr45K3bVjb$O*Ry~E;;6tGJ}gvC=y))3Ga`&dshg_6Ag{nYUe|I^R> zt0KOAxcC0RNKIEWoee%v;J+m<*Op)$;R9YF7S-<(!vX7S4@_-##}z;Ewu=&k?qy wcIJs6#KJt?7u58q=TEL4J5^KHmDKFhZQUC6;$MjGMJ<8y+s{lakH&WT2m6ydw*UYD literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-reorder/notebook-reordered/Second.one b/corpus/notebook-edit/native-reorder/notebook-reordered/Second.one new file mode 100644 index 0000000000000000000000000000000000000000..ff01d734a4719f80b339c3d449f371081d56f5c9 GIT binary patch literal 4016 zcmaD76jR-EW4Z6TjlmTcG^bteVLM#$UrW_{(f4DXc3XD{GN1wbyR!tOCw}!@dO>{R z+Y{?z>zQCzizAwTOt7U4o42d)CNYC_a`r&DcN4(J~w7g z$BaPR6^uY>mV|}|Y~sTsAVz&L8Un*S1d!`Y?_I`CheZ!{!}2{SgFC&ODGA~;0WqlJ z2Z@0&+f-X&5FaEi1Qi2`-+h>93gWW^u`m#Ws%nsU^DDVH^6 zp9e=JFCfD(bqS-&AomSr&cjwd!omks&VhhP0q#bS+=+&Uh+ET4I9APdYF&Q(qWgh4{!rCROe;OM>k}W9=J;)9M8!~B!iC`4a1t(W+ zU9tanz2D?VGrw8wx$n&llm(T}*yOl?;vfTM1R5HE6a@g;Ik5TzXj}smgMkc#OsWin z1gQ3SQTw`SZu~6&$wk|ya>f4I;P9h~-Ol+gA|M)=8Jc7nM7m8FME1!rh-d)yfbhg! z>wqLIyg~H|2y6kSM{L2P1oVvxP!6O9Jv20cay>w~j;^IQqRv0`*fO_sU(=`BeIO?+ oabf5^$H)Q-Fhfv~F@gk6FtUL{iJgHBD6oU#Z~_`d6@V?g0W=LqoB#j- literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-reorder/notebook-reordered/Third.one b/corpus/notebook-edit/native-reorder/notebook-reordered/Third.one new file mode 100644 index 0000000000000000000000000000000000000000..0fbb322cfd0d38596cc6b0d2c0d8bbfd39ff0c53 GIT binary patch literal 4016 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 literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-reorder/notebook/Group/Open Notebook.onetoc2 b/corpus/notebook-edit/native-reorder/notebook/Group/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..8b7a7eca827559a46b8bc3d21a19e7bd5da6c092 GIT binary patch literal 3752 zcmZ3e|KIuM`!b(-j|5(YE0x91QhvI0?)+_DtNFHX2{f<#&432%@6HmCp7_;s=>_qL zZ%?edAq|vA#xUjofdIs2feAoqm^e&W`@+rE6X`G9HY&?FZdmv*!^gSv3rGzJfYoq7 zRe}tH83v>wDj;fB@WD)j(9sve>s(LN`ZNgpuAlLA;uKD(bo`u8OKxn?_LzF;g3*_? zmlIKSq4Kt9F62kXsIsHH(GVCOA#mi&_pckP!j=d?y~B~iFtveEWy2~vmG!PM{!Q`M zzJ7cqT*nBMW=Uvhz$QLC0%Ft`qaiTNLjbwX^xkFMbXfFIH!RrJRg7_eDA*dKg+$QqG3v6D<5Iu11jf0K%@Y7BS`v1L&LtA-$nO*F85vHvoZMNF-JjAIf)G{ zsM^PF=X{qDBCjaY(7>Pq40@2#bd!c9r{&`v))X&&DX%xzQbVAjp$8~eQhGn;!D3VA z?f2y$ynXujBuJ~S2Se{^MlM(h3@oofA}1J`xqy<4kP^Hlg`o%8YalxqYBU*sMH)0T zK-4iX$N((?OW+}p4T04kKu0t{Z2u<1pn~Gt1{Q`XvJ5giWf(-Vf%HBg4XRy~R7;oN nY~Sd!NNMlXS)82@Kzc-g*pnd-2vZq~81xwO!7NBG58@yI-K_`A literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-reorder/notebook/Open Notebook.onetoc2 b/corpus/notebook-edit/native-reorder/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..491ef31bae9ed88e2453db2e4c6bbcbd248cd2e2 GIT binary patch literal 4116 zcmeHJeN2>f9RD307hQmd97WwJnP8kCU9d^7#({u<0us|TOG(dw8HwQ$6{{x`2X+le zU}^()4OzU57dzgf=?b!|wqlMN*rc~Xj9$P}&~nlD^W5{?!R;Z}f4TA9e$Vgsef@nu zzqjXWxo<9ZWU63u?GLKS?e`Sy3LP9Nwq;GkcdXVL+AZH_GZ%X7;z^aJ;!5JF!8&Jf@RVeaX`?jRrosTi%0c8 z#wxnD^y%6P){d3#yj4&c9=bLfk>{4!oacS;Wxu^EogUI7tIo1aY|<7>(xnx+=~sZ0 zv&t1r+pA^jebk?!9v5nb&rEB^2aTz{y-nL&{6DRdkxWhPS(d||WBXV>3upUjw3lS! zO=OI$I3;~#Y(~q1;A5wO%d=NHUfNdgEAZ+(#trXDAIQHqHKx5h>s{~3j#<%KGr_n_ zuW(i6Q-3Gants#GFqR|g#CWc~UB61rI2M#iA0pTAki)F==k$%Qjh%}#f2?V=-C58O zqGDWlAuC~Wcrlhn%?<$^JfKYzv;olaa%j+=&=X`~tfg4YD!f?;nEZa^p??35-VpIG zCqs}ICF7bPf~lfedA2YWADv|ULnpj-uJQa3jL5T`{Dk)r0Hk8e7nxHkKK2Qqkgppx z%!3{|HgvsOX@wF*xsse;04$mtzwbChu(sBY@I*1Y^4kV<9SzGn3Db$PafNxc{?6?n3T1uS->TL%%MK zGaG69mGcge;Q`LPkz{fuMe;aoWajw`ju}{yj{T{nus{-#k{{L5s@T+^h#7jjPu7mG zQt0$mW+@~H7u|j$K4~g9p~UieP)&7E^qOcp5_i%eoQUAqlao^&rB#N$l0y|yWj}R8 zhiv7`C{ZO@djOBs2Y3Wm3UEUNzTZWZWfTv_HOod!b#3}Agq1?2DmuOxg0IIS$1_dF z_|7Bao?)h3i?|m+05k}ReL)W$R86vt;?ziuEG0A|4r8^2ox7 z`%lO$Ua#BwbI->&Z!t9uJoEHz2rGqldUw4PGGw^smGSD~-M^>2ciMQ}J5aev3K`IV zpo0(YwYX;&-tr45K3bVjb$O*Ry~E;;6tGJ}gvC=y))3Ga`&dshg_6Ag{nYUe|I^R> zt0KOAxcC0RNKIEWoee%v;J+m<*Op)$;R9YF7S-<(!vX7S4@_-##}z;Ewu=&k?qy wcIJs6#KJt?7u58q=TEL4J5^KHmDKFhZQUC6;$MjGMJ<8y+s{lakH&WT2m6ydw*UYD literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-reorder/notebook/Second.one b/corpus/notebook-edit/native-reorder/notebook/Second.one new file mode 100644 index 0000000000000000000000000000000000000000..ff01d734a4719f80b339c3d449f371081d56f5c9 GIT binary patch literal 4016 zcmaD76jR-EW4Z6TjlmTcG^bteVLM#$UrW_{(f4DXc3XD{GN1wbyR!tOCw}!@dO>{R z+Y{?z>zQCzizAwTOt7U4o42d)CNYC_a`r&DcN4(J~w7g z$BaPR6^uY>mV|}|Y~sTsAVz&L8Un*S1d!`Y?_I`CheZ!{!}2{SgFC&ODGA~;0WqlJ z2Z@0&+f-X&5FaEi1Qi2`-+h>93gWW^u`m#Ws%nsU^DDVH^6 zp9e=JFCfD(bqS-&AomSr&cjwd!omks&VhhP0q#bS+=+&Uh+ET4I9APdYF&Q(qWgh4{!rCROe;OM>k}W9=J;)9M8!~B!iC`4a1t(W+ zU9tanz2D?VGrw8wx$n&llm(T}*yOl?;vfTM1R5HE6a@g;Ik5TzXj}smgMkc#OsWin z1gQ3SQTw`SZu~6&$wk|ya>f4I;P9h~-Ol+gA|M)=8Jc7nM7m8FME1!rh-d)yfbhg! z>wqLIyg~H|2y6kSM{L2P1oVvxP!6O9Jv20cay>w~j;^IQqRv0`*fO_sU(=`BeIO?+ oabf5^$H)Q-Fhfv~F@gk6FtUL{iJgHBD6oU#Z~_`d6@V?g0W=LqoB#j- literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-reorder/notebook/Third.one b/corpus/notebook-edit/native-reorder/notebook/Third.one new file mode 100644 index 0000000000000000000000000000000000000000..0fbb322cfd0d38596cc6b0d2c0d8bbfd39ff0c53 GIT binary patch literal 4016 zcmaD76jR-EW4Z6TjlmTcG^btm+j{5OJ@KpO(hK4f z-=0`^LklR6jA6?E0|AK50uzAJFmaf$_Jy0RC(>WIZB&+V+_3OrhEUk5V<0sk09L~R zRS7Z(W*Cr$sDP;1zz;JGLjR13*KBY3A-4?Zbg^`?4>6tfty)WjycAEd#=c@j( zoyMrTPk$nA*UoGO>3Z+m6$9-plW9 z$#q}%w}=ra&63d2fK7aO1jMK>MnhnjhX8V&>AlOi>9FXbZdkqtWpJl=GbKTMCLk6B zVvraJvrV-X2Ju1SLQpY~_}zzzrXW5$5DNn_sHz5uH@|WW15%@4Gz3ONV33CZto{e( zZxGYUDa!C zZRLTQ7g%r0vUwvD-tA<8^pxeYctF*KrIm00Iku#)F8pA@WmCjIIrep~qSnNZ1UKnsgH0eJpc97J^u9SXpYQg%*qWX+aJ|1q~ z+HzFeud;vwcbVI2J0+bNc=j`(A`-C0>?u%3x-=bW4Lb5uvt)Wu^5rwjL!$G1`dij* zJDy3mM_RUyx~Sq2A_?}i*oQPLgGh5!M;d>Yf>-RlDjC2M=FPrSedtB_UNSW9^WlXX zVm=tOv!PGU9Q!uD>6=@38cMP!?t(R_EQJKeJo|n1F_XqDTXG<=dS-Ps{QIg&@M9fp zMy#;}-Cw_Y?C!z#*UO78ZGjv%WyV_@uiq;*mge+b5jJjOsy~m}J(qGx0)}9=>B`)N zoUH35D1#-Xffc}=mjGq(24#Yz4C8NP&zVQsL0QhO2^a_BZ;Cqd^5U<1R;-lc=>Z{= zCWD05>xBc5zLJE2XR^{xCX9+);(8@uhuzRXStnXF|BCX1W-@P))>OqxrA*~yP_DQx zab>cPd$rRxyrHlfWCY%obVvx0U59WxjMsy$KyF+MrFhk0WHvd9Hg)1d(I2lw@M67+ z`fz`?$cEr#?cJN)zj8k|a$lmC(LLIo-xrPCexJb4Is7K}F?!iLkJR`4DM|z!Hx87|b6~H`^C8`EW?lCANwDwE9B%b!V$ZYr z%fS4w8F{3jWjp64zxFzE^Wc~?ytjHX_h#0CrFm4&Y;Ij3KY4Vh$Z_HMdETMC=Sc8o z@pBS8S^Xk+@SEvFhAd_mj-kmPUf@vZ&11y)e0hFPJ^4M*ks4|PYaSsIazQT&Dnf4B z=7V)0#*Yw=rz_9l%KkA1^%~7JnL+7Xm)ZPFgsM{$9(Y>6L_9bh;#~RuwMb*he(%Iz z_6-|>WIPET>~#{IqYfJ8S8Va%n54{LMNj5HTRaHt!V;zgW~|rMc4V%kjAI2_$IVt* zBPoGFn75m)QjHQbMKYt9-DTysWsE_R(!dH-Sw+0puqg)ydi~VzR70-2CFrC6p*{;> zoGBRxvL5A)fid#+>+Ptci4A47NhQY%x2J_K1c2G1L~Hy)UO>RF*0?G+Bk=35^gZEz z527PVwl9o*ewy)+tP@8!|B7RUjcwI_k zv6PPLC|{W?gqEGux_O|tZJyTs(%w93R=6p{UkVjQ2L*+hW)gG;@obSmZ*}s1r%Ga zW>%HbzF>vm*y23W=qcBm`O(fLz%vN(_Dh~D6qp;w6>En?SyFHK|JQi^*q3$B?7y6C zNe&seEUfY8nA6*8aeoD;_OhdVH2pw6y(oOj2re~M0lb1WWV_Lq|n=o+o?s| zUL5noW0&y^Cl3g5r%bjd{dwLrTIkgk9i%$jr$P}LgY=wACA`hk?noQLoeRxp1xfMoOE%( zaS4=tmAQG}Atn&&5pnIc_`)L#qDmdc_eil!KL3B$1O17$cq;TK{!6-xiA5Xpm#(=pYwVhe@G;vQ?&=YT()|K*BHSh7aKtc`Gb^D_dv}R- + diff --git a/corpus/notebook-edit/native-reorder/read/page-000.xml b/corpus/notebook-edit/native-reorder/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..09bbc41f9b5eb40793162004d7004c8b20bac55a --- /dev/null +++ b/corpus/notebook-edit/native-reorder/read/page-000.xml @@ -0,0 +1,3 @@ + +the Rust site]]> diff --git a/corpus/notebook-edit/native-reorder/read/payloads.json b/corpus/notebook-edit/native-reorder/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/notebook-edit/native-reorder/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/notebook-edit/native-reorder/reorder-00-opened.xml b/corpus/notebook-edit/native-reorder/reorder-00-opened.xml new file mode 100644 index 0000000000000000000000000000000000000000..536899dbd29217eeaa3d7064eeeb878b081248f2 --- /dev/null +++ b/corpus/notebook-edit/native-reorder/reorder-00-opened.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/native-reorder/reorder-01-update.xml b/corpus/notebook-edit/native-reorder/reorder-01-update.xml new file mode 100644 index 0000000000000000000000000000000000000000..3f97f191e282bee7f8224345c15f50a5ac45b9b3 --- /dev/null +++ b/corpus/notebook-edit/native-reorder/reorder-01-update.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/corpus/notebook-edit/native-reorder/reorder-02-reordered.xml b/corpus/notebook-edit/native-reorder/reorder-02-reordered.xml new file mode 100644 index 0000000000000000000000000000000000000000..c6e94bc84fd5026bdb3b36f8eac780ad7c8d30aa --- /dev/null +++ b/corpus/notebook-edit/native-reorder/reorder-02-reordered.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/native-reorder/reorder-03-reopened.xml b/corpus/notebook-edit/native-reorder/reorder-03-reopened.xml new file mode 100644 index 0000000000000000000000000000000000000000..e261b606fccf929e5292fc54e08312ae7124a7d5 --- /dev/null +++ b/corpus/notebook-edit/native-reorder/reorder-03-reopened.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/native-reorder/run.json b/corpus/notebook-edit/native-reorder/run.json new file mode 100644 index 0000000000000000000000000000000000000000..b06e8ae926ac3e811a3f7207e7b2837a7d1166df --- /dev/null +++ b/corpus/notebook-edit/native-reorder/run.json @@ -0,0 +1,19 @@ +{ + "notebook": "/Users/clo/dev/one/corpus/notebook-edit/native-structure/notebook-renamed", + "expected_pages": -1, + "author": "tools/native/notebook-reorder.ps1", + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "author.ps1": "61594f0aec7a19bf3c5b2bdc478a98aed9d59637f33f7fde074cdc4567716e03", + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/notebook-edit/native-reorder/scripts/author.ps1 b/corpus/notebook-edit/native-reorder/scripts/author.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..b672af93e4abe3eeeaa403c5ff4ed48c775238ac --- /dev/null +++ b/corpus/notebook-edit/native-reorder/scripts/author.ps1 @@ -0,0 +1,50 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +function Read-Tree { + $deadline = [DateTime]::UtcNow.AddSeconds(60) + do { + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + if (@($tree.SelectNodes('//*[local-name()="Section"]')).Count -ge 4) { return $hierarchy } + Start-Sleep -Milliseconds 500 + } while ([DateTime]::UtcNow -lt $deadline) + throw 'The notebook did not finish loading.' +} +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $hierarchy = Read-Tree + [IO.File]::WriteAllText((Join-Path $Root 'reorder-00-opened.xml'), $hierarchy, [Text.Encoding]::UTF8) + [xml]$tree = $hierarchy + $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable) + $manager.AddNamespace('one', $namespace) + $notebookNode = $tree.DocumentElement + $sections = @($notebookNode.SelectNodes('one:Section', $manager)) + if ($sections.Count -lt 3) { throw "Expected three root sections, found $($sections.Count)." } + $last = $sections[$sections.Count - 1] + [void]$notebookNode.RemoveChild($last) + [void]$notebookNode.InsertBefore($last, $sections[0]) + [IO.File]::WriteAllText((Join-Path $Root 'reorder-01-update.xml'), $tree.OuterXml, [Text.Encoding]::UTF8) + $app.UpdateHierarchy($tree.OuterXml, 1) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'reorder-02-reordered.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' + Copy-Item -Recurse (Join-Path $Root 'notebook') (Join-Path $Root 'notebook-reordered') + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $hierarchy = Read-Tree + [IO.File]::WriteAllText((Join-Path $Root 'reorder-03-reopened.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/corpus/notebook-edit/native-reorder/scripts/cold.ps1 b/corpus/notebook-edit/native-reorder/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/notebook-edit/native-reorder/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/notebook-edit/native-reorder/scripts/read.ps1 b/corpus/notebook-edit/native-reorder/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/notebook-edit/native-reorder/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/notebook-edit/native-reorder/source.json b/corpus/notebook-edit/native-reorder/source.json new file mode 100644 index 0000000000000000000000000000000000000000..06d1e5fafe5381cd296e513e70b0c7f59285f6ea --- /dev/null +++ b/corpus/notebook-edit/native-reorder/source.json @@ -0,0 +1,38 @@ +[ + { + "path": "Group/Inner.one", + "bytes": 4016, + "sha256": "7a482b9feafd8220bc9f8c60aca78b4710d64cdc233d7e4aff2d43214f30d26d", + "mtime_ns": 1789276348740792164 + }, + { + "path": "Group/Open Notebook.onetoc2", + "bytes": 3752, + "sha256": "5a81be6da6b559291303c74fede90e0d6f7f78b640c3bce9c1365edb0d99d1b4", + "mtime_ns": 1789276348740542872 + }, + { + "path": "Open Notebook.onetoc2", + "bytes": 4116, + "sha256": "bb8943cc70108ff95a393122f53fd06864db347aafe865f2cf4094ff202c3bbc", + "mtime_ns": 1789276348741057831 + }, + { + "path": "Second.one", + "bytes": 4016, + "sha256": "b1c37af371bb1d7b93950b6b73920d5080178156b7448f3aa30f45472f441ad5", + "mtime_ns": 1789276348741246832 + }, + { + "path": "Third.one", + "bytes": 4016, + "sha256": "6654509286c8c342844a6b716407cfb3dc1303aa8b9c7a0cfff606cc900aea33", + "mtime_ns": 1789276348739887203 + }, + { + "path": "links.one", + "bytes": 6128, + "sha256": "85d17a08e48b0fc0c6d49c4389c80c58910ff64ed2ffd464522523256d2db5dc", + "mtime_ns": 1789276348740094245 + } +] diff --git a/corpus/notebook-edit/native-reorder/teardown.json b/corpus/notebook-edit/native-reorder/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/notebook-edit/native-reorder/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/notebook-edit/native-structure/commands.jsonl b/corpus/notebook-edit/native-structure/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..9dafabf3699e65d78f3b303f159cb7f367ed0017 --- /dev/null +++ b/corpus/notebook-edit/native-structure/commands.jsonl @@ -0,0 +1,4 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\author.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-266B13AF", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-266B13AF -ExpectedPages -1 -UseCurrentCache", "exit": 0, "stdout": "Read 5 sections and 1 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/notebook-edit/native-structure/machine.json b/corpus/notebook-edit/native-structure/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..987ff337497fcaf05840f5e21dd78e6bea7b389a --- /dev/null +++ b/corpus/notebook-edit/native-structure/machine.json @@ -0,0 +1 @@ +{"name": "m6-266b13af", "hostname": "ONE-M6-266B13AF"} diff --git a/corpus/notebook-edit/native-structure/notebook-renamed/Group/Inner.one b/corpus/notebook-edit/native-structure/notebook-renamed/Group/Inner.one new file mode 100644 index 0000000000000000000000000000000000000000..ba9b977cddfbdba559da17d844cf9339f4d0983a GIT binary patch literal 4016 zcmaD76jR-EW4Z6TjlmTcG^brwQY~G6vwfq_BBi}kXK{8uU_b-*cV`JmPyFh+^n&=r zw;)KmcO1zyzQ)OdKXWOZn;2x%0Pqt>)XlCD6R`_pQW7%^)=(09L~R zRS7Z(W*Cr$sDP;1zz;JGLPx1DkYK%c&3W<{d;Rqy({(tZ(g!*N&lZ_x_|4ajF1mEQ zcQ&dnRNfZNh5X1ERd$p&8Un*31de?9{&izj*b)J#cQ|qwrZzCD2nuSg}Tm9Tq**4a@hS4DR%9rX+~Z1jK?s z3=#ujwyCzlAU;T32r32=zxy!J6vSr-VqqW#Rn;Kz=2wnkKx!0>hQMeD4Dt|w)&HRU z4FWtkDtQ4JhN(*!RUnFnDT%FogoO{NoC5)o0^E%txf2Zyky|cC_*%?x*`qw?!=}2z zOQ7-)8-R#!0JgR+F{;ekx1=Mi!^dIH(f(yZ$JZ|eNw%ag^dLJ3Y{;Y?CW28w7o>V0 zn)Jr?o%3wbs}VuQ8kyWcSy1VWO^ypF4l+yg1FJuP#x*c87|1Zlq{=Wz zfNBpbsSIHc%+BGq3>#c2FEnK%=Mvu!T1ON&Yj& literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-structure/notebook-renamed/Group/Open Notebook.onetoc2 b/corpus/notebook-edit/native-structure/notebook-renamed/Group/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..8b7a7eca827559a46b8bc3d21a19e7bd5da6c092 GIT binary patch literal 3752 zcmZ3e|KIuM`!b(-j|5(YE0x91QhvI0?)+_DtNFHX2{f<#&432%@6HmCp7_;s=>_qL zZ%?edAq|vA#xUjofdIs2feAoqm^e&W`@+rE6X`G9HY&?FZdmv*!^gSv3rGzJfYoq7 zRe}tH83v>wDj;fB@WD)j(9sve>s(LN`ZNgpuAlLA;uKD(bo`u8OKxn?_LzF;g3*_? zmlIKSq4Kt9F62kXsIsHH(GVCOA#mi&_pckP!j=d?y~B~iFtveEWy2~vmG!PM{!Q`M zzJ7cqT*nBMW=Uvhz$QLC0%Ft`qaiTNLjbwX^xkFMbXfFIH!RrJRg7_eDA*dKg+$QqG3v6D<5Iu11jf0K%@Y7BS`v1L&LtA-$nO*F85vHvoZMNF-JjAIf)G{ zsM^PF=X{qDBCjaY(7>Pq40@2#bd!c9r{&`v))X&&DX%xzQbVAjp$8~eQhGn;!D3VA z?f2y$ynXujBuJ~S2Se{^MlM(h3@oofA}1J`xqy<4kP^Hlg`o%8YalxqYBU*sMH)0T zK-4iX$N((?OW+}p4T04kKu0t{Z2u<1pn~Gt1{Q`XvJ5giWf(-Vf%HBg4XRy~R7;oN nY~Sd!NNMlXS)82@Kzc-g*pnd-2vZq~81xwO!7NBG58@yI-K_`A literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-structure/notebook-renamed/Open Notebook.onetoc2 b/corpus/notebook-edit/native-structure/notebook-renamed/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..491ef31bae9ed88e2453db2e4c6bbcbd248cd2e2 GIT binary patch literal 4116 zcmeHJeN2>f9RD307hQmd97WwJnP8kCU9d^7#({u<0us|TOG(dw8HwQ$6{{x`2X+le zU}^()4OzU57dzgf=?b!|wqlMN*rc~Xj9$P}&~nlD^W5{?!R;Z}f4TA9e$Vgsef@nu zzqjXWxo<9ZWU63u?GLKS?e`Sy3LP9Nwq;GkcdXVL+AZH_GZ%X7;z^aJ;!5JF!8&Jf@RVeaX`?jRrosTi%0c8 z#wxnD^y%6P){d3#yj4&c9=bLfk>{4!oacS;Wxu^EogUI7tIo1aY|<7>(xnx+=~sZ0 zv&t1r+pA^jebk?!9v5nb&rEB^2aTz{y-nL&{6DRdkxWhPS(d||WBXV>3upUjw3lS! zO=OI$I3;~#Y(~q1;A5wO%d=NHUfNdgEAZ+(#trXDAIQHqHKx5h>s{~3j#<%KGr_n_ zuW(i6Q-3Gants#GFqR|g#CWc~UB61rI2M#iA0pTAki)F==k$%Qjh%}#f2?V=-C58O zqGDWlAuC~Wcrlhn%?<$^JfKYzv;olaa%j+=&=X`~tfg4YD!f?;nEZa^p??35-VpIG zCqs}ICF7bPf~lfedA2YWADv|ULnpj-uJQa3jL5T`{Dk)r0Hk8e7nxHkKK2Qqkgppx z%!3{|HgvsOX@wF*xsse;04$mtzwbChu(sBY@I*1Y^4kV<9SzGn3Db$PafNxc{?6?n3T1uS->TL%%MK zGaG69mGcge;Q`LPkz{fuMe;aoWajw`ju}{yj{T{nus{-#k{{L5s@T+^h#7jjPu7mG zQt0$mW+@~H7u|j$K4~g9p~UieP)&7E^qOcp5_i%eoQUAqlao^&rB#N$l0y|yWj}R8 zhiv7`C{ZO@djOBs2Y3Wm3UEUNzTZWZWfTv_HOod!b#3}Agq1?2DmuOxg0IIS$1_dF z_|7Bao?)h3i?|m+05k}ReL)W$R86vt;?ziuEG0A|4r8^2ox7 z`%lO$Ua#BwbI->&Z!t9uJoEHz2rGqldUw4PGGw^smGSD~-M^>2ciMQ}J5aev3K`IV zpo0(YwYX;&-tr45K3bVjb$O*Ry~E;;6tGJ}gvC=y))3Ga`&dshg_6Ag{nYUe|I^R> zt0KOAxcC0RNKIEWoee%v;J+m<*Op)$;R9YF7S-<(!vX7S4@_-##}z;Ewu=&k?qy wcIJs6#KJt?7u58q=TEL4J5^KHmDKFhZQUC6;$MjGMJ<8y+s{lakH&WT2m6ydw*UYD literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-structure/notebook-renamed/Second.one b/corpus/notebook-edit/native-structure/notebook-renamed/Second.one new file mode 100644 index 0000000000000000000000000000000000000000..ff01d734a4719f80b339c3d449f371081d56f5c9 GIT binary patch literal 4016 zcmaD76jR-EW4Z6TjlmTcG^bteVLM#$UrW_{(f4DXc3XD{GN1wbyR!tOCw}!@dO>{R z+Y{?z>zQCzizAwTOt7U4o42d)CNYC_a`r&DcN4(J~w7g z$BaPR6^uY>mV|}|Y~sTsAVz&L8Un*S1d!`Y?_I`CheZ!{!}2{SgFC&ODGA~;0WqlJ z2Z@0&+f-X&5FaEi1Qi2`-+h>93gWW^u`m#Ws%nsU^DDVH^6 zp9e=JFCfD(bqS-&AomSr&cjwd!omks&VhhP0q#bS+=+&Uh+ET4I9APdYF&Q(qWgh4{!rCROe;OM>k}W9=J;)9M8!~B!iC`4a1t(W+ zU9tanz2D?VGrw8wx$n&llm(T}*yOl?;vfTM1R5HE6a@g;Ik5TzXj}smgMkc#OsWin z1gQ3SQTw`SZu~6&$wk|ya>f4I;P9h~-Ol+gA|M)=8Jc7nM7m8FME1!rh-d)yfbhg! z>wqLIyg~H|2y6kSM{L2P1oVvxP!6O9Jv20cay>w~j;^IQqRv0`*fO_sU(=`BeIO?+ oabf5^$H)Q-Fhfv~F@gk6FtUL{iJgHBD6oU#Z~_`d6@V?g0W=LqoB#j- literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-structure/notebook-renamed/Third.one b/corpus/notebook-edit/native-structure/notebook-renamed/Third.one new file mode 100644 index 0000000000000000000000000000000000000000..0fbb322cfd0d38596cc6b0d2c0d8bbfd39ff0c53 GIT binary patch literal 4016 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 literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-structure/notebook/Group/Open Notebook.onetoc2 b/corpus/notebook-edit/native-structure/notebook/Group/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..8b7a7eca827559a46b8bc3d21a19e7bd5da6c092 GIT binary patch literal 3752 zcmZ3e|KIuM`!b(-j|5(YE0x91QhvI0?)+_DtNFHX2{f<#&432%@6HmCp7_;s=>_qL zZ%?edAq|vA#xUjofdIs2feAoqm^e&W`@+rE6X`G9HY&?FZdmv*!^gSv3rGzJfYoq7 zRe}tH83v>wDj;fB@WD)j(9sve>s(LN`ZNgpuAlLA;uKD(bo`u8OKxn?_LzF;g3*_? zmlIKSq4Kt9F62kXsIsHH(GVCOA#mi&_pckP!j=d?y~B~iFtveEWy2~vmG!PM{!Q`M zzJ7cqT*nBMW=Uvhz$QLC0%Ft`qaiTNLjbwX^xkFMbXfFIH!RrJRg7_eDA*dKg+$QqG3v6D<5Iu11jf0K%@Y7BS`v1L&LtA-$nO*F85vHvoZMNF-JjAIf)G{ zsM^PF=X{qDBCjaY(7>Pq40@2#bd!c9r{&`v))X&&DX%xzQbVAjp$8~eQhGn;!D3VA z?f2y$ynXujBuJ~S2Se{^MlM(h3@oofA}1J`xqy<4kP^Hlg`o%8YalxqYBU*sMH)0T zK-4iX$N((?OW+}p4T04kKu0t{Z2u<1pn~Gt1{Q`XvJ5giWf(-Vf%HBg4XRy~R7;oN nY~Sd!NNMlXS)82@Kzc-g*pnd-2vZq~81xwO!7NBG58@yI-K_`A literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/OneNote_DeletedPages.one b/corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/OneNote_DeletedPages.one new file mode 100644 index 0000000000000000000000000000000000000000..0b35e4075f90bb3ef9acad76415f0cbcf3bd9d06 GIT binary patch literal 4016 zcmaD76jR-EW4Z6TjlmTcG^bszpBQ+~WkIcPgFw2F&*Rpm3~0dq?koZ6iC;aJUJ#%7 z_QbjyT0nVZ3{(Ce2taHWm;jW9iNk~s3ffOSb@Z9j{xj+iHmtJUt;1g52~q1a3W%Bw{4moXbU7ctWa85x{}sINUh~h$bKrzZ%WItyVG5t)(;BxiJ)6Jn zEUGS4-WJV;{Kyzpc9b_70>dK&j(qw4bz@c75&@`pIC2=KHZZD$Oy+GUc%SLAU_#;c zCA(d#8G+I)2@MU{#D_;fjQV0U1crGCAlI4RyNsI-iyrER<$F*DcX~Hd62xZ$VnHAV ziGeWNR9j&XA0#dW6$6RieVAwp;Fc5>PYLIyIE5|S(H3~*UU^D~?naQ@iH3#}FW38?Z&x_9Z{Jz=D=DNH zR32gj5b+Jb*48CP6~05E(Go{@`A=EewCVHgKguAJOlC4NMFMG7K`QG7J)+ z+T%eQ^Pw9O2VFM*kC>i%T}|3SJt#y0!)wCf=3DH8x^1&NDX>uXaMDUfN~`qw_g4z-0m@F^#db4vln|oPFUi? n(0h)N1r%V0pde!e37lYL1BDVh0~=6a2gTt8G>R$!TX+Kiok}*m literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/Open Notebook.onetoc2 b/corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..d5cd409141148e98332aa0f4404563392a4a5f56 GIT binary patch literal 3856 zcmZ3e|KIuM`!b(-j|5(YE0x6`6ttgu>gY44{b$r4Y*=Nxn*j~j-<>5OJ@KpO(hK4f z-=0`^LmDWLjA6?E0|AK50uzAJFmaf$_Jy0RC(>WIZB&+V+_3OrMpjDp43HWS0IT7E zsstGXGYm*WR6x`S@WV`l(3`H^zOY{PjmzS_%5VRe*eY>CrNi_??egn>IPG2c?c}MG zK|fG+q4Kt9F62kXsIsHH(GVCOA#mi&_pckP!j=d?y~B~iFtveErT(;@QACx7+k_un zf6_T0lrRFNSrQr=u!#?kfEe|~Xb24R5J0Xoy>}Tm9Tq**4a@hS41RekQvit11jK?s z3=#w3^v+qnAU;T32r32=w@JRY7{up+iXF)kX#_Gy!DtAKhQJUB0a*PH%HJTsgQJoc zkYSj*gi!^eXqb}N%12oEfXX=#5Glaj2$Fu$&@f4A)fcW;^Ii98&t;PDzg`Y1C$WJC z-yU+>Ip1Z3$SaC8G%%hfwSRoj-e$ebL_?sVp$8~;@{9e~j}_Zp z8;>;qGM=?S1Ef{ggQ536BQqC}$p|U0K_XWfxnN~7P#h$3f{~XStOiIhFt(&H^dNf{ zB*;){DR48=prHX`76XF}&|0troIutBt3QB}4J-^(WEo_3$}osz1L=K08dQtaPYgWg zvY^(tK_Feo=W**&kaLZI*q5OJ@KpO(hK4f z-=0`^LklR6jA6?E0|AK50uzAJFmaghK|%Yer;a{z+J8pt~6tmwPirwbT5^K3DaR z?KDQ!h05EaxsV?jqsor*MnhnDgusz6-@k6G3R@xo^$tf4!_)>wm5IIU*mj(*^Im>; zORoF6zeS8dX_kbB25jQPBOpe7F&YBHJOq&IOz&OBO@~Dfb;I&KD1$q_n<)w6GXb$6 z5QD@(m~E=9Fo+Kl7lMj`#P2>#GzIb5fmj%bK~*(Ky!n-57?2tTqaiRF0)spRVD&#J ze}e!Ij!IrYhGFUwMiq#nVM<~vA7SAGD(65zqyTp#NbW>KL$W{X#EV>?e74@_^5R*; zE)6OVu>pwq24HLJ5~E7oRFl;f32EiF~6YP`QP$w-C|Z3D|>8(9|1Wg%=6 z_12M0{1O?XQJj(fTJXcnG{#4&%DkCb#F#OVU~Qt^Z_Nf8C%s z+NY0kY3?Da;+Ox9DL4EPbd#}6Q78Hv`*!H(dNGdqrO|_Etb52Ytl`(>$oG2AL>W)} zRN5cfQW2o-L(9vdL3=TjAPapB`C>M)=#feMXN$I< z^8Vs|5&ujY2y&-n#wG}7sxY=MLkPutCmCPpgjzS&pC_CVx$Pr6p*{kDlyCVsF{S)t zod62?PNIf5cq7M(#&?TtP=Y8|lJgUQNt?v4JN6Jp+sZHTgBT~h$AWs)nXCKdNyid@ z>*#E2sE7B7_n0`B$PpGkeH_;uo@P%hd^VyjQ(fMa5c(zLQhP`h_0)Y!A}-3HXKbz) zhyB8FVBgrevRM_lIBL(qI|Hq<`M3N-Nl64C=sQdp3siBOk!)5kI=N=^6K7+&3)!je zS51#VzhDJt)YI}SoiRjLs8|2y9yr`%6P^fn;G~uC}IDv8+O& z>pavdv%;+ux=@o*3JKhWSr10XUCE9qFn{e=R^k^H5avKKnRIX`BG~^@|CPEurtsE+ z?M2$cUz(spw(=R2sFFrc0Uq0Q;1Nv8!Gs7rE-x%h+0_wME$cQMITpGeZlzGEwjCG` z!FN52deaR0=!V_BZo!6Zv$z&O05oukb-^1t$Z02zAoKrTQ&nymim%LjrC{bz(P9zV zHXa#`gNz(_7M&el79Sdi|Yg zbCk=ZkO2({I@sX57T4^;ho0(Pdy8XC*JtaK>&%mp0#>P$u(&B{Gz9d-I$B7kP?Gh; zyDrt&vfU$ZL&A>FZ>u-f+Yi8t6xz#Oz4uFeKgiHhn4f*z?dK$$HuB>aG}dBq*9ldL zM}t19qlm`0x}s~r{`8351J-Lha%61?JB%7C(qSA$AVMbAK79Ay)G#=xY#;;t9D)C# z=9wel=L@{+oa#@gEJI ziCxmr?13ZV0cy|FEp;t_Th&Y4TB+RfcUGLwIOj(1Fet<$cNprAcaKSI__q6lOYL!1 zP1E4R3r^T!hkHy4%b?rx>r5&tU(!b7lF8k%QTQFPsc4SeulO7>meiwpqO9JX=j6NY z^eCdGzCB0Fjx@$qPj9lV)aKuYe|Kszl;0+1U^&!x5EW?2csNN$W1@_Y#H0h*`5yxD zPI@dgoju*FYx`#2Woy-Qt;jKeS`z=ykVrA}XjRs*WPWY(X?-?OPsjgV&@dwBdmdI2 p$6O@2o{B`Ajye3UD3p}XzaMkFPuIgW1FjkP|IL8&FOhi9`v-XOr0oCz literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-structure/notebook/Second.one b/corpus/notebook-edit/native-structure/notebook/Second.one new file mode 100644 index 0000000000000000000000000000000000000000..ff01d734a4719f80b339c3d449f371081d56f5c9 GIT binary patch literal 4016 zcmaD76jR-EW4Z6TjlmTcG^bteVLM#$UrW_{(f4DXc3XD{GN1wbyR!tOCw}!@dO>{R z+Y{?z>zQCzizAwTOt7U4o42d)CNYC_a`r&DcN4(J~w7g z$BaPR6^uY>mV|}|Y~sTsAVz&L8Un*S1d!`Y?_I`CheZ!{!}2{SgFC&ODGA~;0WqlJ z2Z@0&+f-X&5FaEi1Qi2`-+h>93gWW^u`m#Ws%nsU^DDVH^6 zp9e=JFCfD(bqS-&AomSr&cjwd!omks&VhhP0q#bS+=+&Uh+ET4I9APdYF&Q(qWgh4{!rCROe;OM>k}W9=J;)9M8!~B!iC`4a1t(W+ zU9tanz2D?VGrw8wx$n&llm(T}*yOl?;vfTM1R5HE6a@g;Ik5TzXj}smgMkc#OsWin z1gQ3SQTw`SZu~6&$wk|ya>f4I;P9h~-Ol+gA|M)=8Jc7nM7m8FME1!rh-d)yfbhg! z>wqLIyg~H|2y6kSM{L2P1oVvxP!6O9Jv20cay>w~j;^IQqRv0`*fO_sU(=`BeIO?+ oabf5^$H)Q-Fhfv~F@gk6FtUL{iJgHBD6oU#Z~_`d6@V?g0W=LqoB#j- literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/native-structure/notebook/links.one b/corpus/notebook-edit/native-structure/notebook/links.one new file mode 100644 index 0000000000000000000000000000000000000000..c53bf910edd84ce5721c52e18ea5022d415c730c GIT binary patch literal 6128 zcmeHKeNa?Y6hF(CASf%Wxa6|#ASq-jO=u)4pkl0i$QYv#jFE^fqbcJ@0gm=mH2fG{ zzXeSLb(9=M1dFUpsV3A+QZlR@bF{Is7$HU7ag@$)-+QpY12y|YXE@B+_ue_@o_o*7 z{oTve3CZ(U)$EBmQ1W_aRlufm0e@)Ilde0AFOL1Ys`{(Lho}{WUd&hbUfW>YUDa!C zZRLTQ7g%r0vUwvD-tA<8^pxeYctF*KrIm00Iku#)F8pA@WmCjIIrep~qSnNZ1UKnsgH0eJpc97J^u9SXpYQg%*qWX+aJ|1q~ z+HzFeud;vwcbVI2J0+bNc=j`(A`-C0>?u%3x-=bW4Lb5uvt)Wu^5rwjL!$G1`dij* zJDy3mM_RUyx~Sq2A_?}i*oQPLgGh5!M;d>Yf>-RlDjC2M=FPrSedtB_UNSW9^WlXX zVm=tOv!PGU9Q!uD>6=@38cMP!?t(R_EQJKeJo|n1F_XqDTXG<=dS-Ps{QIg&@M9fp zMy#;}-Cw_Y?C!z#*UO78ZGjv%WyV_@uiq;*mge+b5jJjOsy~m}J(qGx0)}9=>B`)N zoUH35D1#-Xffc}=mjGq(24#Yz4C8NP&zVQsL0QhO2^a_BZ;Cqd^5U<1R;-lc=>Z{= zCWD05>xBc5zLJE2XR^{xCX9+);(8@uhuzRXStnXF|BCX1W-@P))>OqxrA*~yP_DQx zab>cPd$rRxyrHlfWCY%obVvx0U59WxjMsy$KyF+MrFhk0WHvd9Hg)1d(I2lw@M67+ z`fz`?$cEr#?cJN)zj8k|a$lmC(LLIo-xrPCexJb4Is7K}F?!iLkJR`4DM|z!Hx87|b6~H`^C8`EW?lCANwDwE9B%b!V$ZYr z%fS4w8F{3jWjp64zxFzE^Wc~?ytjHX_h#0CrFm4&Y;Ij3KY4Vh$Z_HMdETMC=Sc8o z@pBS8S^Xk+@SEvFhAd_mj-kmPUf@vZ&11y)e0hFPJ^4M*ks4|PYaSsIazQT&Dnf4B z=7V)0#*Yw=rz_9l%KkA1^%~7JnL+7Xm)ZPFgsM{$9(Y>6L_9bh;#~RuwMb*he(%Iz z_6-|>WIPET>~#{IqYfJ8S8Va%n54{LMNj5HTRaHt!V;zgW~|rMc4V%kjAI2_$IVt* zBPoGFn75m)QjHQbMKYt9-DTysWsE_R(!dH-Sw+0puqg)ydi~VzR70-2CFrC6p*{;> zoGBRxvL5A)fid#+>+Ptci4A47NhQY%x2J_K1c2G1L~Hy)UO>RF*0?G+Bk=35^gZEz z527PVwl9o*ewy)+tP@8!|B7RUjcwI_k zv6PPLC|{W?gqEGux_O|tZJyTs(%w93R=6p{UkVjQ2L*+hW)gG;@obSmZ*}s1r%Ga zW>%HbzF>vm*y23W=qcBm`O(fLz%vN(_Dh~D6qp;w6>En?SyFHK|JQi^*q3$B?7y6C zNe&seEUfY8nA6*8aeoD;_OhdVH2pw6y(oOj2re~M0lb1WWV_Lq|n=o+o?s| zUL5noW0&y^Cl3g5r%bjd{dwLrTIkgk9i%$jr$P}LgY=wACA`hk?noQLoeRxp1xfMoOE%( zaS4=tmAQG}Atn&&5pnIc_`)L#qDmdc_eil!KL3B$1O17$cq;TK{!6-xiA5Xpm#(=pYwVhe@G;vQ?&=YT()|K*BHSh7aKtc`Gb^D_dv}R- + diff --git a/corpus/notebook-edit/native-structure/read/page-000.xml b/corpus/notebook-edit/native-structure/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..aae586285f92029feab8401da5465764fba2406f --- /dev/null +++ b/corpus/notebook-edit/native-structure/read/page-000.xml @@ -0,0 +1,3 @@ + +the Rust site]]> diff --git a/corpus/notebook-edit/native-structure/read/payloads.json b/corpus/notebook-edit/native-structure/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/notebook-edit/native-structure/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/notebook-edit/native-structure/run.json b/corpus/notebook-edit/native-structure/run.json new file mode 100644 index 0000000000000000000000000000000000000000..6c3a356ea771b65ae32e22c7df0dcdbbc9e9988b --- /dev/null +++ b/corpus/notebook-edit/native-structure/run.json @@ -0,0 +1,19 @@ +{ + "notebook": "/Users/clo/dev/one/corpus/link-edit/candidate", + "expected_pages": -1, + "author": "tools/native/notebook-structure.ps1", + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "author.ps1": "1369d3e7ae7205370d11e9c6abc3fcf4dd9931e7c0da50b3b86f1e2945e1d4a6", + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/notebook-edit/native-structure/scripts/author.ps1 b/corpus/notebook-edit/native-structure/scripts/author.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..caa6737aea0e40f0695880dee51d166868e8ce3e --- /dev/null +++ b/corpus/notebook-edit/native-structure/scripts/author.ps1 @@ -0,0 +1,71 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$log = @() +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'structure-00-opened.xml'), $hierarchy, [Text.Encoding]::UTF8) + $second = '' + $app.OpenHierarchy('Second.one', $notebookId, [ref]$second, 3) + $third = '' + $app.OpenHierarchy('Third.one', $notebookId, [ref]$third, 3) + $group = '' + $app.OpenHierarchy('Group', $notebookId, [ref]$group, 2) + $inner = '' + $app.OpenHierarchy('Inner.one', $group, [ref]$inner, 3) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'structure-01-created.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable) + $manager.AddNamespace('one', $namespace) + $sections = @($tree.SelectNodes('//one:Section', $manager)) + foreach ($section in $sections) { + switch ($section.GetAttribute('name')) { + 'Second' { $section.SetAttribute('name', 'Renamed'); $section.SetAttribute('color', '#FFD75E') } + 'Third' { $section.SetAttribute('color', '#B7C997') } + } + } + [IO.File]::WriteAllText((Join-Path $Root 'structure-02-update.xml'), $tree.OuterXml, [Text.Encoding]::UTF8) + $app.UpdateHierarchy($tree.OuterXml, 1) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'structure-03-renamed.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' + Copy-Item -Recurse (Join-Path $Root 'notebook') (Join-Path $Root 'notebook-renamed') + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(60) + do { + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + if (@($tree.SelectNodes('//*[local-name()="Section"]')).Count -ge 4) { break } + Start-Sleep -Milliseconds 500 + } while ([DateTime]::UtcNow -lt $deadline) + $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable) + $manager.AddNamespace('one', $namespace) + [IO.File]::WriteAllText((Join-Path $Root 'structure-03b-reopened.xml'), $hierarchy, [Text.Encoding]::UTF8) + $doomed = @($tree.SelectNodes('//*[local-name()="Section"]') | Where-Object { $_.GetAttribute('name') -eq 'Third' }) + if ($doomed.Count -ne 1) { throw "Expected one section named Third, found $($doomed.Count)." } + $app.DeleteHierarchy($doomed[0].GetAttribute('ID'), [DateTime]::MinValue, $false) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'structure-04-deleted.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/corpus/notebook-edit/native-structure/scripts/cold.ps1 b/corpus/notebook-edit/native-structure/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/notebook-edit/native-structure/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/notebook-edit/native-structure/scripts/read.ps1 b/corpus/notebook-edit/native-structure/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/notebook-edit/native-structure/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/notebook-edit/native-structure/source.json b/corpus/notebook-edit/native-structure/source.json new file mode 100644 index 0000000000000000000000000000000000000000..14786084a474f1cc0a6f522bd49c8104ad3627f0 --- /dev/null +++ b/corpus/notebook-edit/native-structure/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1604, + "sha256": "6731c4214437fe1cb6aa347b0004542b7f7e4260f47773bf9ec07c4e894de47c", + "mtime_ns": 1789206821144296891 + }, + { + "path": "links.one", + "bytes": 6128, + "sha256": "0cfb356ed374fe919f73fb73fdbe5d66e0baa484d4119f70074d9f869d6db78a", + "mtime_ns": 1789206821116200533 + } +] diff --git a/corpus/notebook-edit/native-structure/structure-00-opened.xml b/corpus/notebook-edit/native-structure/structure-00-opened.xml new file mode 100644 index 0000000000000000000000000000000000000000..4e4c71ddc2d95b643864e00e4ca9b8442721ad31 --- /dev/null +++ b/corpus/notebook-edit/native-structure/structure-00-opened.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/native-structure/structure-01-created.xml b/corpus/notebook-edit/native-structure/structure-01-created.xml new file mode 100644 index 0000000000000000000000000000000000000000..d5aecd6a994608493b531c2376be05a83a289140 --- /dev/null +++ b/corpus/notebook-edit/native-structure/structure-01-created.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/native-structure/structure-02-update.xml b/corpus/notebook-edit/native-structure/structure-02-update.xml new file mode 100644 index 0000000000000000000000000000000000000000..0ba2ecae962b71c5557241f6dcce09f5f5c1b83c --- /dev/null +++ b/corpus/notebook-edit/native-structure/structure-02-update.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/corpus/notebook-edit/native-structure/structure-03-renamed.xml b/corpus/notebook-edit/native-structure/structure-03-renamed.xml new file mode 100644 index 0000000000000000000000000000000000000000..e6bbf68ba1b4aa95710683a7c7d85b9b51b3a468 --- /dev/null +++ b/corpus/notebook-edit/native-structure/structure-03-renamed.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/native-structure/structure-03b-reopened.xml b/corpus/notebook-edit/native-structure/structure-03b-reopened.xml new file mode 100644 index 0000000000000000000000000000000000000000..19831bb3f4ffdcffb15dd6cda847ff5c35e13e0e --- /dev/null +++ b/corpus/notebook-edit/native-structure/structure-03b-reopened.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/native-structure/structure-04-deleted.xml b/corpus/notebook-edit/native-structure/structure-04-deleted.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d58e35673332aebe601d62d209fbe6c3b78026c --- /dev/null +++ b/corpus/notebook-edit/native-structure/structure-04-deleted.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/native-structure/teardown.json b/corpus/notebook-edit/native-structure/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/notebook-edit/native-structure/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/notebook-edit/structured/candidate/First.one b/corpus/notebook-edit/structured/candidate/First.one new file mode 100644 index 0000000000000000000000000000000000000000..73c4e8efe60b799c1de48850f501516327e165bc GIT binary patch literal 3212 zcmeHIe@s<%6hDuL@PHRS@Bt$4$(y4@6Gv&qk_HSVgdocVEOQo7xN0$e6bZSjvGEEf zSPcnRF_J-Bb3Wt`!VMK^qtzM}Evm`fV*OEL$&ApoVV}>t_rmpgac%vXXFK=4dp_s; z`JV6jo^#&R<9q6wCwdA_`gYcit!w*ka%bs}_QC3{@BH!VqaR1cmr8(STg|+VX#4AY&8^dNA zyZZYHymB?WS2Zm>n>8QZVEH;}Fy_pL+<~0%^l#3TeyYdNvueWLo$^y|*TJuRm7^`W zy8rF>?F{^1Gl1Su*gjTCb&0FYB`ZBHNpiq)JZqqD@6+|!AB}u==eRk>$8^R2#{ zxMk$exmn+Vq7(F{^zRd;ulQ2U{M;vn&BMvNBOB@)$iK!RN)j;od~yEWZ(G}&^N%-+f2Ey1EzhmZvNqP|b=yXc-P=E^Ow&LQ0M4Cz zBuG6G!4eJ31iF0@}5#p>6B`{tRP*#tMk`l42 zc>L_yJmtm5PRAbEm8fH?$Kq49pK6uXb}gncfM%I$s1UCjml~PQ@%Qxe?>%V}4KqS^ zGmHsAI>g1}-txTB@S@mp7iQ;mWJ+uKGS%m@`ZDE1BuXrBizK2xZ^(uc;;-E+s9(Jv zucIzm%dDOUXMUd^3hK;k*_P9^ZtmI?S#o5*R7#api$^Nt8F@jT;=i#SVflJ!JkoOC46iPd{&p(qY=9kJrn~?fV!YM zzr_ak#RDrD2oAa$rBA>>!qiGrEW%vee15o4Fdw$y9O2-O054bZKC+STmyrKwk*odq=3cVU z?<@RYYQH};bmGp_X)}*B7FKPKUf;Hnd;gby?+sDOnSUqr0b{?P)4+WIVxXzUe)ojJ z0@(p&yb;Eg6$X%{VmE^4G>|if``%#$uWDc>0A6CG4u0N`0mOTUc%Zypi=da-bSbH^ z-{1A+tatgf&7EsA+;7miL~~9B&TB28GxPQHU-?73XZxq0@}2Mcuq^8!2b6IN1kNYs zg~It1I5){h;e09@nCy+duvNuNET8M$B$@srmFlVb!qN z3}U2YH^X>=|873flfwCAc3#dW8%H7JvidTePx^dUs~P=`6PIWu#i}uzee}7ianr|a z|6s&rww9sX>K<8$lwsw@N(w9<;>O|$tQT^a?c;zw49fpARx!%WpKUA!L8=CVW>1C!({+oW!uj>P-*zcieDG0Da{AqS ze&?KfzVojmE7$FKEB3MPSZLL@=p)_d5*P9}od5Z~nzx(wv=1D<(L^a!b+N}*+Lc(= zA1&&N4(9F2f2NJpa>k(J2=80?=m&>uPmC0W zN3UJ|8gl3?RaQoB41MkyG+&CYcI4((uuXp}1tq1=qdjee+ohWaU~I-Pcx4!R7~q&UJAZ`r}Ac2jeE1!TothHdJ%6cYo*pmHl{& z`x3p3?#UiFchsNt*fU{&d3b;Spc`?EPDQuElk7go@tq<*GpL4e(q%2{c-+f^% zv~!s~)JCn;Oq*y6zX^~0zy$kZJ}r~>=?Yo%IW{}bNFj46v%)O>nJmH#)=X-Z;5tvw zfs!ho9a2kJ{E)`xq+jF!ez!TPY&&xn$2#c`56L)6*+$%BCV#(RI+g#ekPWbi7`MsN zx`rQB6XC9R-;Xg6&--GIJDYvwupd|=zh3gwMrxt;oQoEIN0n%DehZRsVt#vH*?W2a zyU%-j+FY#@_VI)G{(qU@BWB?o`L;7x>gRVz1M3(lWOnQ4*Oti=rfO!yo1?d^(?D3F z6|8!&PXnInuuoug_!GhoOhi~1g8D5pzq-- zX9tukG-Y@~8GTSjzJ407J^5@R9Dgo!>cief&rU2H{Zyy!8{`FqeUrKe;SYp;V;xiL zOs6(^zO0E}-tybESrQW(Q@=uveN(T6$ZbPLmHnrEb7;k@5u=@gNa5WGLjjShf8RII z1YzG8-52{NmL+tqi^Fi=$bBukndccd*0q$;E`!c6)+Q60$LRYR9G8(TZ0=P@G$JUX z-q}g1%>!KDJgNNxA4cD?u^ef^{}Z~H+bx-K%r7kjtWt@H73_zt!23~0dq?koZ6iC;aJUJ#%7 z_Qbjy(m;7+3{(Ce2taHmm;jW9iNl1;tv}|9Z+PIfvH6}~h3pLZFAPgQ!Q?>n1Xc)7 z2S^z}*$EIKIQfAO&IB{GS>rG0CeLu%wUPC{Z1Y?`m>ZO0q9-rcG4@(=;^|B+hn4QA zx<+}UAuu9Cpn;L$n+$^r#P3JGeE+(!Dr|`$G$=$07#ei){vOSg;_~l)ng2-b`+-tM zsGRf*hK36qv75IfFL!Or{P3&p?2$<@Ib0zLkyjL9XkbuD01`leZoGy7LxWxgLk9y3 z#1H`>#RbF=tspYefS~~P`GmdG0453 zatmY+hz7ZFhGguD4^ThC0tYBO3PwXtxoIpt+02QsU5+C9qrvhLN0WAzAK(at%Kqi4Os7?ShXn~a3&R-J) Uq(;GL2#kinFbx6ZG63X702lcT>;M1& literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/structured/candidate/Open Notebook.onetoc2 b/corpus/notebook-edit/structured/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..0f0cfa53f1ab928351bb9ff5b1836d37a422da03 GIT binary patch literal 5408 zcmeHKe@sl!o6jj&Hx1U7S!goeZ+Hd%rDy z_x1FT&P~~U^_%+6c=qdy)_sTt?Qxnx)|%a54ym7P3KoYHZp6s}=5V|DG1Zf+R-tEN zOsFXurLo7l>zBkP*QNhD&sWrL)neFGK+~%2N3%Y^aJzKfKy=o~x58Q&fkzSn#X^+{ zY}FRjbWRe%L-aY@6l%RQW9;XLds{`@L$cn z6e~Z*(13$x;T=`ZUb}Jq_?A}=M@!xAA!WbIxjO$l@5*htNoy+#kG`Yzj=xOG-YY?8 z)XfxU)1lV}ie}gRsrj_=jJ4{YVJC%V-L`9nRale z+gi9pc>(=nx#VyL$rG96Et8C*@h12P+=JY((qDGI@>WK6mHSCgo8#Ntu-`3Tz)$o* zxdAx?fwp(#WdHqxKjXvAsq?!WwKd2Y%RJ;?D5-upE2v?Lj3Ip@p9q)`27(b;g1O!X z19LaqG+xI%!L{htd7yU(^aQEzdHMH2=ub)g>zh^Mp(jcGne~<+^hHwt@lwtA+zd+? zfiMCODFXVl*C<0&B1W9oK_Zr|<QRK==m1J2fhuqHE!v29ZqZQcz;*JJoij+lNkrgAq00B__pkPNj?3v zX!*X95B$|t6%(|xxbc{(@vZTU4m*N0<6&@Z!$kGt$y}+^f{sAJ(!J7~$hvm2hfnfw z0Z5bO92bp;xv7jkkAePIGs1=R25sQ_-5K~<=|}prPSjTq_9G-S ztl2CCegm#%_@SE_es{}baS{R-zl-ew2_fW>mzK+H)Z;U{eTe1AWK2AKVtnDL&K4Vn(X_ghV@2opKJNoB*H{1Xuj-!dd7t+= zU(Y$`Ip;jj_~w@mHjIAc=;|)48Gg9=>!#%7lIl+zo;}e}67qVQl1e_1KGtH1aeK4d zhojt6gI%MmnXVs_eU<_LizGsuC0?TUufMq`>de)g?su+isAlA85K_+J@SGzzw>ZTi}7B^xW)iZu+Qs$*-no_wKr7#FFc=JFmIg11F$A-a<+O)4=o8m!6*c<^MrdhN=tvMfiVGVVUNJeE+9YA=DfP9w%K0evSK ztqj^gEY^q_yhilQEV&58URF)$aR6RZ?zxn@ixC6$(w_%_WU%ff#n)aa0K^WGatuDbV{1wnEwj=nI>xTXf_Qq(I`@f_mWH4B`lLVW~M5SZ8NY25rK9i z8Uli@YJ$1Scn+jm)`42sXD8bR z9x2TuTfx1QM<(r~YI4#W^d_(5{ldShd^FGIIr#t^g@v*AhYH8@^r2pQkFA%g*Jjzy@&zy*4QcT0FPj{bxt3CR6Al=M)(oW`!9>7guiWM z0a;iKZ-TX~;a|l>@bz~8m@4=bm-_7p63P*8E04AM`u$uZ7BB(tgCu?ZT9z<|A%h3( z4b@8)XhcXN6~gqyR*i_Bfqn1O6Gt^-5)*J}OC|AA54p@ZXNEW)I876AZQvjtON#vV zp&_s9{#&W7tJ3X>ZDX+uUQd#8T$!KSwcjCu+z zg?1wv4iKRd{pN1|%BLXujgi0TH>1I&$BI}w2Qy5swz zzdF`rXL-ukJb3KG!i(q;_j2F!yIqAi{w{cutFLb3z9#KLTf+Xh>&^}rZGU%nd46Nn z$A?~Y^#209(R&+sBHVS)Ydd>*ZRFLR?z#iLe;yZi7Bm_4UBnB0752d=>^a?5FRvY5 S`N1Cne+2$35zzMl`1B9kjmek* literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/structured/cold/commands.jsonl b/corpus/notebook-edit/structured/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..97e2d1eb8ce74259020f9c18671dc67067b8f5da --- /dev/null +++ b/corpus/notebook-edit/structured/cold/commands.jsonl @@ -0,0 +1,3 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-A31627CC -ExpectedPages -1", "exit": 0, "stdout": "Read 3 sections and 3 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/notebook-edit/structured/cold/machine.json b/corpus/notebook-edit/structured/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..362a2d1d14429c44e98dbab0ef63ce8c0d9cfe2b --- /dev/null +++ b/corpus/notebook-edit/structured/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-a31627cc", "hostname": "ONE-M6-A31627CC"} diff --git a/corpus/notebook-edit/structured/cold/notebook/First.one b/corpus/notebook-edit/structured/cold/notebook/First.one new file mode 100644 index 0000000000000000000000000000000000000000..73c4e8efe60b799c1de48850f501516327e165bc GIT binary patch literal 3212 zcmeHIe@s<%6hDuL@PHRS@Bt$4$(y4@6Gv&qk_HSVgdocVEOQo7xN0$e6bZSjvGEEf zSPcnRF_J-Bb3Wt`!VMK^qtzM}Evm`fV*OEL$&ApoVV}>t_rmpgac%vXXFK=4dp_s; z`JV6jo^#&R<9q6wCwdA_`gYcit!w*ka%bs}_QC3{@BH!VqaR1cmr8(STg|+VX#4AY&8^dNA zyZZYHymB?WS2Zm>n>8QZVEH;}Fy_pL+<~0%^l#3TeyYdNvueWLo$^y|*TJuRm7^`W zy8rF>?F{^1Gl1Su*gjTCb&0FYB`ZBHNpiq)JZqqD@6+|!AB}u==eRk>$8^R2#{ zxMk$exmn+Vq7(F{^zRd;ulQ2U{M;vn&BMvNBOB@)$iK!RN)j;od~yEWZ(G}&^N%-+f2Ey1EzhmZvNqP|b=yXc-P=E^Ow&LQ0M4Cz zBuG6G!4eJ31iF0@}5#p>6B`{tRP*#tMk`l42 zc>L_yJmtm5PRAbEm8fH?$Kq49pK6uXb}gncfM%I$s1UCjml~PQ@%Qxe?>%V}4KqS^ zGmHsAI>g1}-txTB@S@mp7iQ;mWJ+uKGS%m@`ZDE1BuXrBizK2xZ^(uc;;-E+s9(Jv zucIzm%dDOUXMUd^3hK;k*_P9^ZtmI?S#o5*R7#api$^Nt8F@jT;=i#SVflJ!JkoOC46iPd{&p(qY=9kJrn~?fV!YM zzr_ak#RDrD2oAa$rBA>>!qiGrEW%vee15o4Fdw$y9O2-O054bZKC+STmyrKwk*odq=3cVU z?<@RYYQH};bmGp_X)}*B7FKPKUf;Hnd;gby?+sDOnSUqr0b{?P)4+WIVxXzUe)ojJ z0@(p&yb;Eg6$X%{VmE^4G>|if``%#$uWDc>0A6CG4u0N`0mOTUc%Zypi=da-bSbH^ z-{1A+tatgf&7EsA+;7miL~~9B&TB28GxPQHU-?73XZxq0@}2Mcuq^8!2b6IN1kNYs zg~It1I5){h;e09@nCy+duvNuNET8M$B$@srmFlVb!qN z3}U2YH^X>=|873flfwCAc3#dW8%H7JvidTePx^dUs~P=`6PIWu#i}uzee}7ianr|a z|6s&rww9sX>K<8$lwsw@N(w9<;>O|$tQT^a?c;zw49fpARx!%WpKUA!L8=CVW>1C!({+oW!uj>P-*zcieDG0Da{AqS ze&?KfzVojmE7$FKEB3MPSZLL@=p)_d5*P9}od5Z~nzx(wv=1D<(L^a!b+N}*+Lc(= zA1&&N4(9F2f2NJpa>k(J2=80?=m&>uPmC0W zN3UJ|8gl3?RaQoB41MkyG+&CYcI4((uuXp}1tq1=qdjee+ohWaU~I-Pcx4!R7~q&UJAZ`r}Ac2jeE1!TothHdJ%6cYo*pmHl{& z`x3p3?#UiFchsNt*fU{&d3b;Spc`?EPDQuElk7go@tq<*GpL4e(q%2{c-+f^% zv~!s~)JCn;Oq*y6zX^~0zy$kZJ}r~>=?Yo%IW{}bNFj46v%)O>nJmH#)=X-Z;5tvw zfs!ho9a2kJ{E)`xq+jF!ez!TPY&&xn$2#c`56L)6*+$%BCV#(RI+g#ekPWbi7`MsN zx`rQB6XC9R-;Xg6&--GIJDYvwupd|=zh3gwMrxt;oQoEIN0n%DehZRsVt#vH*?W2a zyU%-j+FY#@_VI)G{(qU@BWB?o`L;7x>gRVz1M3(lWOnQ4*Oti=rfO!yo1?d^(?D3F z6|8!&PXnInuuoug_!GhoOhi~1g8D5pzq-- zX9tukG-Y@~8GTSjzJ407J^5@R9Dgo!>cief&rU2H{Zyy!8{`FqeUrKe;SYp;V;xiL zOs6(^zO0E}-tybESrQW(Q@=uveN(T6$ZbPLmHnrEb7;k@5u=@gNa5WGLjjShf8RII z1YzG8-52{NmL+tqi^Fi=$bBukndccd*0q$;E`!c6)+Q60$LRYR9G8(TZ0=P@G$JUX z-q}g1%>!KDJgNNxA4cD?u^ef^{}Z~H+bx-K%r7kjtWt@H73_zt!23~0dq?koZ6iC;aJUJ#%7 z_Qbjy(m;7+3{(Ce2taHmm;jW9iNl1;tv}|9Z+PIfvH6}~h3pLZFAPgQ!Q?>n1Xc)7 z2S^z}*$EIKIQfAO&IB{GS>rG0CeLu%wUPC{Z1Y?`m>ZO0q9-rcG4@(=;^|B+hn4QA zx<+}UAuu9Cpn;L$n+$^r#P3JGeE+(!Dr|`$G$=$07#ei){vOSg;_~l)ng2-b`+-tM zsGRf*hK36qv75IfFL!Or{P3&p?2$<@Ib0zLkyjL9XkbuD01`leZoGy7LxWxgLk9y3 z#1H`>#RbF=tspYefS~~P`GmdG0453 zatmY+hz7ZFhGguD4^ThC0tYBO3PwXtxoIpt+02QsU5+C9qrvhLN0WAzAK(at%Kqi4Os7?ShXn~a3&R-J) Uq(;GL2#kinFbx6ZG63X702lcT>;M1& literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/structured/cold/notebook/Open Notebook.onetoc2 b/corpus/notebook-edit/structured/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..aca80d29b252adba4ae4fbd7b553da11023ed3d9 GIT binary patch literal 6472 zcmeHLe@s&VER)l=vm^fTwKf+iH~x9 zM**?~DJFx8vJev;;WS8FZ57eXfEL!B5Lj;jjZlJU=FXm zlJaKHyvm`^T6)9T?_JdH<1A=TR}G?8?Hcoh`pynd(S*W{Fn>i`##OtY($3}z<3M0p zJ0I6cdF9jnxt76#ls)_2&P~WH%?=Q*9eH$TZRnx&_s?B_VasSp`s5w@t+u5Nv(%#B z!>}3%{O1vnOjI2~mJclyx|QowT$_1P16etJ!5z+os!Ii#jWg%x_^>UMOA>9N=V&K6 zDVm<8e9C2;`X&-_FDZ~OJ~V2fMYu z*0G}G^xTij)cMbrJw(AAj5a_FW?n-TdE!y>=xmdTPH>%eyg6dLfC|1(@FOOmH@4{} z&(s?q>x5|_USBSF4r;{s*74od6+zs9sIe-VaWxPy5cnS;z+52nd>}Uzt_M5FBI)Oo zisj4g&ZF*xdbd3=|6%Dq^uL2k7H5zgIg@PG$tWssf{(yG$PLM^y=SX`OU@zBQ2k_9P{NcmhVaSx zM1~1rAQ(AIFxQ)5APQic%IlaXxE9@dUmq@o9wGF7>wXx6zF6p=T&oF!9xe2HwwgT9 zV}<_p#kwz{CkXwck#!fiX^4S^Br!-$q|DdII>@q^q!cM%UA;ANR$B^{`>HEIK&+o%>79 z^8$jE9DqpcllaJ=#q5aW!2TRQn8}U!@ZOMMk>5{V2<`jHuAA&j?e{u%+3mmAz1IVu zk64eRjVG+7eYVWz;P%n!ZHW&dO1(8=vn>C>dTbQmz1sD_T2%;NBnRjHw!_yde7gI5 z?ObI(#2Zsx$BFgeN#e!umH%tt9|`bns~-@zc;kp)mf?evtnDL&K4Vn(X_ghV@2opKJNoB*H{1Xuj-!dd7t+= zU(Y$`Ip;jj_~w@mHjIAc=;|)48Gg9=>!#%7lIl+zo;}e}67qVQl1e_1KGtH1aeK4d zhojt6gI%MmnXVs_eU<_LizGsuC0?TUufMq`>de)g?su+isAlA85K_+J@SGzzw>ZTi}7B^xW)iZu+Qs$*-no_wKr7#FFc=JFmIg11F$A-a<+O)4=o8m!6*c<^MrdhN=tvMfiVGVVUNJeE+9YA=DfP9w%K0evSK ztqj^gEY^q_yhilQEV&58URF)$aR6RZ?zxn@ixC6$(w_%_WU%ff#n)aa0K^WGatuDbV{1wnEwj=nI>xTXf_Qq(I`@f_mWH4B`lLVW~M5SZ8NY25rK9i z8Uli@YJ$1Scn+jm)`42sXD8bR z9x2TuTfx1QM<(r~YI4#W^d_(5{ldShd^FGIIr#t^g@v*AhYH8@^r2pQkFA%g*Jjzy@&zy*4QcT0FPj{bxt3CR6Al=M)(oW`!9>7guiWM z0a;iKZ-TX~;a|l>@bz~8m@4=bm-_7p63P*8E04AM`u$uZ7BB(tgCu?ZT9z<|A%h3( z4b@8)XhcXN6~gqyR*i_Bfqn1O6Gt^-5)*J}OC|AA54p@ZXNEW)I876AZQvjtON#vV zp&_s9{#&W7tJ3X>ZDX+uUQd#8T$!KSwcjCu+z zg?1wv4iKRd{pN1|%BLXujgi0TH>1I&$BI}w2Qy5swz zzdF`rXL-ukJb3KG!i(q;_j2F!yIqAi{w{cutFLb3z9#KLTf+Xh>&^}rZGU%nd46Nn z$A?~Y^#209(R&+sBHVS)Ydd>*ZRFLR?z#iLe;yZi7Bm_4UBnB0752d=>^a?5FRvY5 S`N1Cne+2$35zzMl`1B9kjmek* literal 0 HcmV?d00001 diff --git a/corpus/notebook-edit/structured/cold/read/environment.json b/corpus/notebook-edit/structured/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..b119e4fcd06ca1a80360de9c9c571427c3a3cf9a --- /dev/null +++ b/corpus/notebook-edit/structured/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-A31627CC", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/notebook-edit/structured/cold/read/hierarchy.xml b/corpus/notebook-edit/structured/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..37e4cd0165325041c867d2439844ea14c068758f --- /dev/null +++ b/corpus/notebook-edit/structured/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/structured/cold/read/page-000.xml b/corpus/notebook-edit/structured/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..52ece39370fe0bbef1d7fb32c6801cf76803a930 --- /dev/null +++ b/corpus/notebook-edit/structured/cold/read/page-000.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/structured/cold/read/page-001.xml b/corpus/notebook-edit/structured/cold/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..803fd8b035cd8815f744d0825401b690b29d9305 --- /dev/null +++ b/corpus/notebook-edit/structured/cold/read/page-001.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/structured/cold/read/page-002.xml b/corpus/notebook-edit/structured/cold/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..8c42c95e23447652243442f606cbc2614694a2b1 --- /dev/null +++ b/corpus/notebook-edit/structured/cold/read/page-002.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/notebook-edit/structured/cold/read/payloads.json b/corpus/notebook-edit/structured/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/notebook-edit/structured/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/notebook-edit/structured/cold/run.json b/corpus/notebook-edit/structured/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..efc1a27f8ac2fb96ec2c151500177de83d5852f8 --- /dev/null +++ b/corpus/notebook-edit/structured/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/structure3", + "expected_pages": -1, + "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/notebook-edit/structured/cold/scripts/cold.ps1 b/corpus/notebook-edit/structured/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/notebook-edit/structured/cold/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/notebook-edit/structured/cold/scripts/read.ps1 b/corpus/notebook-edit/structured/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/notebook-edit/structured/cold/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/notebook-edit/structured/cold/source.json b/corpus/notebook-edit/structured/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..b364775d9806efe99f98c992ccb7c2faf10f66d8 --- /dev/null +++ b/corpus/notebook-edit/structured/cold/source.json @@ -0,0 +1,32 @@ +[ + { + "path": "First.one", + "bytes": 3212, + "sha256": "d926b2f5502dd1bb2768928e44020497248299b9e6c1c8ddcec999117279c177", + "mtime_ns": 1789279283377754822 + }, + { + "path": "Kept/Inner.one", + "bytes": 3164, + "sha256": "33e43aef96619ba34e4499b605e21540ac25475542ae8817e37ed10382ac25ca", + "mtime_ns": 1789279283538323423 + }, + { + "path": "Kept/Open Notebook.onetoc2", + "bytes": 3824, + "sha256": "432b3413622bb8eabacc640ffc8c6ad80dee94cd45bed6154f5afbc2ae443965", + "mtime_ns": 1789279283654960588 + }, + { + "path": "Open Notebook.onetoc2", + "bytes": 5408, + "sha256": "e713f4d9cca21552e7fd650c54b6072fc2d8875e48b738b00887324f82331728", + "mtime_ns": 1789279283758856923 + }, + { + "path": "Renamed.one", + "bytes": 5504, + "sha256": "9ee7bd23c1679ce5035c92adca0416730b149d11bedd1b4a7c181a0186df7007", + "mtime_ns": 1789279283726949659 + } +] diff --git a/corpus/notebook-edit/structured/cold/teardown.json b/corpus/notebook-edit/structured/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/notebook-edit/structured/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/crates/notebook/README.md b/crates/notebook/README.md index 70ce70ea2b329dd4328c6264bea9cd80cb5160cc..8d854ac6801ad9acfc7a82075f3c5ed11cc84a35 100644 --- a/crates/notebook/README.md +++ b/crates/notebook/README.md @@ -160,6 +160,17 @@ in one local transaction. The supplied images must still match `snapshot()` and so another overlapping remote edit can produce a new conflict. Pending intents and uncertain attempts cannot be reviewed. +An attempt that stays `AwaitingConfirmation` is released by review, never by +replay: `release_attempt(id, local, remote, archive, after)` on the replica +(`release` on a session) exports the branch to a new archive, then retires +the oldest edit's attempt. `Some(page)` continues from a page reviewed +against the current remote image as a fresh intent under the same id, +keeping later edits; `None` abandons the local branch, whose ids report +`EditStatus::Archived { archive }` from then on, and the working image +returns to the remote image. Neither writes a receipt: the uncertain +publication may or may not have landed, and the archive is the record of +what was attempted. + Opening recognizes the application identity and the current schema version only; caches and archives written by other versions are refused unchanged. Until the application is usable end to end there are no migrations. @@ -295,7 +306,8 @@ Discovery returns ordered sections and nested groups with file identities and share-relative paths. Section display-name overrides remain distinct from file names. TOC references whose identities are absent from the directory remain inspectable; a cached filename never substitutes for an identity match. -Reserved `_onefiles` directories are excluded from section-group traversal. +Reserved `_onefiles` directories are excluded from section-group traversal; +OneNote's `OneNote_RecycleBin` is listed as the section group OneNote shows. Encrypted sections and valid storage with an unreadable document graph retain their identity as `Locked` or `Unreadable`, without being presented as empty pages. Malformed storage, failed reads and ambiguous identities reject the discovery. @@ -312,6 +324,37 @@ Missing files, permissions and size failures retain their I/O error kinds; an empty payload is a successful empty buffer. Embedded payloads remain available directly from the core document model. +## Notebook structure + +`Notebook::create_section`, `create_group`, `rename`, `set_section_color`, +`reorder` and `delete` change a notebook the way OneNote does: the table of +contents (`Open Notebook.onetoc2`, created when a folder has none) gains, +renames, reorders or loses entries; a section's colour lives in its own +metadata; a deleted section moves into `OneNote_RecycleBin`, a group with its +own TOC. Every created, renamed or moved file is placed with +`onestore::place_file`, which sets the header's ancestor to the parent TOC's +identity and the name CRC OneNote checks on open; a file without them is +re-identified and listed anew. These operate on the mounted directory; +the SMB transport has no create, rename or delete. + +`Notebook::find_page(url)` resolves a stored internal link to a section path +and page space by identity: the linked section first, then every readable +section, so a link follows its page when the section is renamed or moved and +when the page itself was moved to another section. Other URLs and unknown +targets are `None`. + +With the optional `protected` feature, `Notebook::unlock(path, password)` +reads the pages of a `Locked` section for display; nothing is cached or +written, and a wrong password is `Error::Protected(PasswordMismatch)`. + +`Notebook::refresh` rereads the directory on the caller's schedule and reports +what another client changed as `Change`s keyed by file identity: a renamed or +moved section is `Moved`, not removed and added, and a deleted section moves +into `OneNote_RecycleBin`; a folder whose surviving entries changed sequence +is `Reordered`. A failed read returns the error and keeps +the previous catalog, so an unreachable share never reads as an emptied +notebook. + ## SMB (feature `smb`) `notebook::smb` provides blocking SMB access for OneNote sections and diff --git a/crates/notebook/examples/recovery_probe.rs b/crates/notebook/examples/recovery_probe.rs index b69d3c3e351fbc46501a7b8d6b9fcf1b50861781..841d92c2aaa8a7b6dd47213c635bb2999d4f765e 100644 --- a/crates/notebook/examples/recovery_probe.rs +++ b/crates/notebook/examples/recovery_probe.rs @@ -91,6 +91,7 @@ fn report(cache: &Replica, root: &Path) -> Result<(), Box } Some(EditStatus::Published { revision }) => ("published", Some(revision.to_string())), Some(EditStatus::Conflict(_)) => ("conflict", None), + Some(EditStatus::Archived { archive }) => ("archived", Some(archive)), None => ("missing", None), }; println!( diff --git a/crates/notebook/src/lib.rs b/crates/notebook/src/lib.rs index 0e4cd47db025b66e4136562154dd7dfe011acbae..e35e6a7e4c8c52a22a195225f4bb0480bfec3ab6 100644 --- a/crates/notebook/src/lib.rs +++ b/crates/notebook/src/lib.rs @@ -51,12 +51,15 @@ pub enum Error { Discovery(#[from] discover::Error), #[error("External payload identity now refers to different bytes")] AssetChanged, + #[cfg(feature = "protected")] + #[error(transparent)] + Protected(#[from] onestore::protected::Error), } type Result = std::result::Result; const APPLICATION_ID: u32 = 0x4f4e454f; -const SCHEMA_VERSION: u32 = 12; +const SCHEMA_VERSION: u32 = 13; /// An edited page model together with the stored model it was edited from. /// `before` is the precondition reconciliation checks against the remote page. diff --git a/crates/notebook/src/schema.rs b/crates/notebook/src/schema.rs index ef12c0c62a5e7d1e06e4a0a08ef12a53857f3a06..4c163f268b3b3164cc6083a714c6729213884641 100644 --- a/crates/notebook/src/schema.rs +++ b/crates/notebook/src/schema.rs @@ -27,6 +27,10 @@ pub(crate) fn create(transaction: &Transaction<'_>) -> Result<()> { CREATE TABLE receipts ( edit_id INTEGER PRIMARY KEY CHECK(edit_id>0), revision TEXT NOT NULL + ) STRICT; + CREATE TABLE archived ( + edit_id INTEGER PRIMARY KEY CHECK(edit_id>0), + archive TEXT NOT NULL ) STRICT;", )?; transaction.execute_batch(CONFLICTS)?; diff --git a/crates/notebook/src/session.rs b/crates/notebook/src/session.rs index 9bc1b894349395de053b30128038e3487378717f..e117740c1587bbb0aa10d6531f7f7c23c5fc42df 100644 --- a/crates/notebook/src/session.rs +++ b/crates/notebook/src/session.rs @@ -8,7 +8,9 @@ use onestore::{ CommitError, ExGuid, PreparedEdit, RevisionIndex, Store, document::Document, page::Page, }; use std::{ + collections::BTreeMap, io, + io::Write, path::{Path, PathBuf}, sync::{ Arc, @@ -17,6 +19,52 @@ use std::{ time::Duration, }; +/// A difference between two catalog reads of a notebook, in catalog paths. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Change { + /// A section or group appeared, including one replacing a file under an existing path. + Added(String), + /// A section or group is no longer in the notebook. + Removed(String), + /// The same file now lives at another path: a rename or a move between groups. + Moved { from: String, to: String }, + /// A folder's surviving sections and groups changed order. + Reordered(String), +} + +/// Sections and groups are followed by file identity; a group without a TOC only by path. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum Key { + File([u8; 16]), + Folder(String), +} + +type Entries = (BTreeMap, BTreeMap>); + +fn entries(folder: &discover::Folder) -> Entries { + fn walk(folder: &discover::Folder, out: &mut Entries) { + let mut order = Vec::new(); + for section in &folder.sections { + let key = Key::File(section.file_id); + out.0.insert(key.clone(), section.path.clone()); + order.push(key); + } + for group in &folder.groups { + let key = match &group.toc { + Some(toc) => Key::File(toc.file_id), + None => Key::Folder(group.path.clone()), + }; + out.0.insert(key.clone(), group.path.clone()); + order.push(key); + walk(group, out); + } + out.1.insert(folder.path.clone(), order); + } + let mut out = Entries::default(); + walk(folder, &mut out); + out +} + /// A notebook directory and the cache directory holding its section replicas. pub struct Notebook { root: PathBuf, @@ -48,6 +96,365 @@ impl Notebook { &self.catalog } + /// Rereads the notebook directory and reports what changed since the last catalog, + /// keyed by file identity so a renamed or moved section stays the same section. A + /// failed read keeps the previous catalog: an unreachable notebook is not an empty one. + pub fn refresh(&mut self) -> Result> { + let catalog = discover::discover( + &mut discover::Local::open(&self.root)?, + discover::Limits { + entries: 100_000, + bytes_per_file: 256 * 1024 * 1024, + depth: 64, + }, + )?; + let (before, before_orders) = entries(&self.catalog); + let (after, after_orders) = entries(&catalog); + let mut changes = Vec::new(); + for key in before_orders.values().flatten() { + let from = &before[key]; + match after.get(key) { + None => changes.push(Change::Removed(from.clone())), + Some(to) if to != from => changes.push(Change::Moved { + from: from.clone(), + to: to.clone(), + }), + Some(_) => {} + } + } + for key in after_orders.values().flatten() { + if !before.contains_key(key) { + changes.push(Change::Added(after[key].clone())); + } + } + for (folder, order) in &after_orders { + let Some(previous) = before_orders.get(folder) else { + continue; + }; + let kept = |sequence: &[Key], other: &[Key]| -> Vec { + sequence + .iter() + .filter(|key| other.contains(key)) + .cloned() + .collect() + }; + if kept(previous, order) != kept(order, previous) { + changes.push(Change::Reordered(folder.clone())); + } + } + self.catalog = catalog; + Ok(changes) + } + + fn folder(&self, path: &str) -> Result<&discover::Folder> { + let mut folders = vec![&self.catalog]; + while let Some(folder) = folders.pop() { + if folder.path == path { + return Ok(folder); + } + folders.extend(&folder.groups); + } + Err(io::Error::from(io::ErrorKind::NotFound).into()) + } + + fn directory(&self, path: &str) -> Result { + let directory = if path.is_empty() { + self.root.clone() + } else { + self.root.join(path).canonicalize()? + }; + if !directory.starts_with(&self.root) || !directory.is_dir() { + return Err(io::Error::from(io::ErrorKind::PermissionDenied).into()); + } + Ok(directory) + } + + /// A folder's TOC path and file identity, creating the TOC when the folder has none + /// (OneNote names it `Open Notebook.onetoc2`). + fn toc(&self, folder: &str) -> Result<(PathBuf, [u8; 16])> { + let directory = self.directory(folder)?; + match &self.folder(folder)?.toc { + Some(toc) => Ok((directory.join(&toc.filename), toc.file_id)), + None => { + let path = directory.join("Open Notebook.onetoc2"); + let bytes = onestore::create_table_of_contents("Open Notebook.onetoc2", &[])?; + std::fs::File::create_new(&path)?.write_all(&bytes)?; + Ok((path, onestore::Store::parse(&bytes)?.header.file_id)) + } + } + } + + fn edit_toc(&self, folder: &str, edits: &[onestore::TocEdit]) -> Result<()> { + let (toc, _) = self.toc(folder)?; + let source = onestore::read_file(&toc)?; + PreparedEdit::table_of_contents(&source, edits)? + .commit_file(&toc) + .map_err(|error| error.error)?; + Ok(()) + } + + /// Creates `name.one` in `folder` with one empty page and lists it last in the folder's + /// TOC, as OneNote creates a section. Returns the new catalog path. + pub fn create_section(&mut self, folder: &str, name: &str, author: &str) -> Result { + let filename = format!("{name}.one"); + let directory = self.directory(folder)?; + let (_, ancestor) = self.toc(folder)?; + let bytes = onestore::create_section(&filename, "", author)?; + let path = directory.join(&filename); + std::fs::File::create_new(&path)?.write_all(&bytes)?; + onestore::place_file(&path, ancestor, &filename)?; + let identity = onestore::Store::parse(&bytes)?.header.file_id; + self.edit_toc( + folder, + &[onestore::TocEdit::Add { + filename: filename.clone(), + identity, + group: false, + }], + )?; + self.refresh()?; + Ok(catalog_path(folder, &filename)) + } + + /// Creates a section group: a folder with its own TOC, listed last in the parent's TOC. + pub fn create_group(&mut self, folder: &str, name: &str) -> Result { + let directory = self.directory(folder)?; + if !component(name) { + return Err(io::Error::from(io::ErrorKind::InvalidInput).into()); + } + let group = directory.join(name); + let (_, ancestor) = self.toc(folder)?; + std::fs::create_dir(&group)?; + let bytes = onestore::create_table_of_contents("Open Notebook.onetoc2", &[])?; + let path = group.join("Open Notebook.onetoc2"); + std::fs::File::create_new(&path)?.write_all(&bytes)?; + onestore::place_file(&path, ancestor, name)?; + let identity = onestore::Store::parse(&bytes)?.header.file_id; + self.edit_toc( + folder, + &[onestore::TocEdit::Add { + filename: name.to_owned(), + identity, + group: true, + }], + )?; + self.refresh()?; + Ok(catalog_path(folder, name)) + } + + /// Renames a section or section group: the file or folder on disk and its TOC entry. + pub fn rename(&mut self, path: &str, name: &str) -> Result { + let (folder, entry) = split(path); + let directory = self.directory(folder)?; + let (filename, identity) = self.entry(folder, entry)?; + let target = if filename.to_ascii_lowercase().ends_with(".one") { + format!("{name}.one") + } else { + name.to_owned() + }; + if !component(&target) || directory.join(&target).exists() { + return Err(io::Error::from(io::ErrorKind::AlreadyExists).into()); + } + std::fs::rename(directory.join(&filename), directory.join(&target))?; + let (_, ancestor) = self.toc(folder)?; + let placed = if target.ends_with(".one") { + directory.join(&target) + } else { + directory.join(&target).join("Open Notebook.onetoc2") + }; + onestore::place_file(placed, ancestor, &target)?; + self.edit_toc( + folder, + &[onestore::TocEdit::Rename { + identity, + filename: target.clone(), + }], + )?; + self.refresh()?; + Ok(catalog_path(folder, &target)) + } + + /// Sets a section's colour (COLORREF) in its own metadata, where OneNote keeps it. + pub fn set_section_color(&mut self, path: &str, color: Option) -> Result<()> { + let file = self.root.join(path).canonicalize()?; + if !file.starts_with(&self.root) { + return Err(io::Error::from(io::ErrorKind::PermissionDenied).into()); + } + let source = onestore::read_file(&file)?; + let store = onestore::Store::parse(&source)?; + let index = onestore::RevisionIndex::parse(&store)?; + let document = Document::parse(&index)?; + let revision = document.active(document.root)?; + let metadata = *revision + .roots + .get(&2) + .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?; + onestore::commit_file_property( + &file, + &source, + document.root, + metadata, + 0x14001cbe, + &color.unwrap_or(0xffff_ffff).to_le_bytes(), + ) + .map_err(|error| error.error)?; + self.refresh()?; + Ok(()) + } + + /// Orders a folder's sections and groups; entries left out follow in their current order. + pub fn reorder(&mut self, folder: &str, paths: &[&str]) -> Result<()> { + let mut identities = Vec::new(); + for path in paths { + let (parent, entry) = split(path); + if parent != folder { + return Err(io::Error::from(io::ErrorKind::InvalidInput).into()); + } + identities.push(self.entry(folder, entry)?.1); + } + self.edit_toc(folder, &[onestore::TocEdit::Order(identities)])?; + self.refresh().map(drop) + } + + /// Deletes a section the way OneNote does: the file moves into the notebook's + /// `OneNote_RecycleBin` folder, a section group with its own TOC that lists it (and that + /// the root TOC lists), and its own folder's TOC entry goes. + pub fn delete(&mut self, path: &str) -> Result<()> { + let (folder, entry) = split(path); + let directory = self.directory(folder)?; + let (filename, identity) = self.entry(folder, entry)?; + if !filename.to_ascii_lowercase().ends_with(".one") { + return Err(io::Error::from(io::ErrorKind::InvalidInput).into()); + } + let bin = self.root.join("OneNote_RecycleBin"); + let bin_toc = bin.join("Open Notebook.onetoc2"); + if !bin_toc.exists() { + std::fs::create_dir_all(&bin)?; + let bytes = onestore::create_table_of_contents("Open Notebook.onetoc2", &[])?; + std::fs::File::create_new(&bin_toc)?.write_all(&bytes)?; + onestore::place_file(&bin_toc, self.toc("")?.1, "OneNote_RecycleBin")?; + let bin_identity = onestore::Store::parse(&bytes)?.header.file_id; + self.edit_toc( + "", + &[onestore::TocEdit::Add { + filename: "OneNote_RecycleBin".into(), + identity: bin_identity, + group: true, + }], + )?; + } + let mut target = filename.clone(); + let mut attempt = 1; + while bin.join(&target).exists() { + attempt += 1; + let (stem, extension) = filename.rsplit_once('.').unwrap_or((&filename, "")); + target = format!("{stem} ({attempt}).{extension}"); + } + std::fs::rename(directory.join(&filename), bin.join(&target))?; + let source = onestore::read_file(&bin_toc)?; + onestore::place_file( + bin.join(&target), + onestore::Store::parse(&source)?.header.file_id, + &target, + )?; + PreparedEdit::table_of_contents( + &source, + &[onestore::TocEdit::Add { + filename: target, + identity, + group: false, + }], + )? + .commit_file(&bin_toc) + .map_err(|error| error.error)?; + self.edit_toc(folder, &[onestore::TocEdit::Remove { identity }])?; + self.refresh().map(drop) + } + + /// The stored filename and TOC identity of a folder's section or group. + fn entry(&self, folder: &str, name: &str) -> Result<(String, [u8; 16])> { + let parent = self.folder(folder)?; + if let Some(section) = parent + .sections + .iter() + .find(|section| split(§ion.path).1 == name) + { + return Ok((name.to_owned(), section.file_id)); + } + if let Some(group) = parent + .groups + .iter() + .find(|group| split(&group.path).1 == name) + { + let toc = group + .toc + .as_ref() + .ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?; + return Ok((name.to_owned(), toc.file_id)); + } + Err(io::Error::from(io::ErrorKind::NotFound).into()) + } + + /// The section path and page space a stored internal link opens, found by identity: + /// in the linked section first, then in every other readable section, so a link + /// follows its page across sections. `None` for other URLs and unknown targets. + pub fn find_page(&self, url: &str) -> Result)>> { + let Some(link) = onestore::page::link::parse_internal_link(url) else { + return Ok(None); + }; + let mut sections = Vec::new(); + let mut folders = vec![&self.catalog]; + while let Some(folder) = folders.pop() { + sections.extend(folder.sections.iter().filter(|section| { + matches!(section.state, discover::SectionState::Readable { .. }) + })); + folders.extend(&folder.groups); + } + sections.sort_by_key(|section| section.file_id != link.section); + let Some(page) = link.page else { + return Ok(sections + .first() + .filter(|section| section.file_id == link.section) + .map(|section| (section.path.clone(), None))); + }; + for section in sections { + let bytes = onestore::read_file(self.root.join(§ion.path))?; + let store = Store::parse(&bytes)?; + let index = RevisionIndex::parse(&store)?; + let document = Document::parse(&index)?; + for (space, _) in document.pages()? { + if Page::identity_of(document.active(space)?) == Some(page) { + return Ok(Some((section.path.clone(), Some(space)))); + } + } + } + Ok(None) + } + + /// The pages of a password-protected section, for reading: nothing is cached or + /// written, and the decoded buffers go when the pages have been built. + #[cfg(feature = "protected")] + pub fn unlock(&self, path: &str, password: &str) -> Result> { + let file = self.root.join(path).canonicalize()?; + if !file.starts_with(&self.root) { + return Err(io::Error::from(io::ErrorKind::PermissionDenied).into()); + } + let bytes = onestore::read_file(&file)?; + let store = Store::parse(&bytes)?; + let index = RevisionIndex::parse(&store)?; + let unlocked = onestore::protected::UnlockedSection::open( + &index, + password, + onestore::protected::Limits::default(), + )?; + let document = unlocked.document()?; + document + .pages()? + .into_iter() + .map(|(space, _)| Ok((space, Page::from_space(&document, space)?))) + .collect() + } + /// Opens a section by its catalog path. pub fn section(&self, path: &str, notify: impl Fn() + Send + 'static) -> Result
{ let mut folders = vec![&self.catalog]; @@ -65,6 +472,25 @@ impl Notebook { } } +fn component(name: &str) -> bool { + !name.is_empty() && !name.contains(['/', '\\', '\0']) && name != "." && name != ".." +} + +fn split(path: &str) -> (&str, &str) { + match path.rsplit_once('/') { + Some((folder, name)) => (folder, name), + None => ("", path), + } +} + +fn catalog_path(folder: &str, name: &str) -> String { + if folder.is_empty() { + name.to_owned() + } else { + format!("{folder}/{name}") + } +} + /// What happened on the synchronization thread since the last poll. #[derive(Debug)] pub enum Event { @@ -181,7 +607,7 @@ impl Section { Ok(None) => Event::Refreshed, Ok(Some((id, status))) => Event::Attempt { id: *id, - status: *status, + status: status.clone(), }, Err(Error::RemoteIo(error)) => Event::Unreachable(match error.raw_os_error() { Some(code) => io::Error::from_raw_os_error(code), @@ -308,6 +734,18 @@ impl Section { Ok(()) } + /// Retires an uncertain attempt after review, exporting the branch to `archive` + /// first: `Some(page)` continues from the reviewed page against `remote_page`, + /// `None` abandons the local branch. Neither claims the attempt was acknowledged. + pub fn release(&self, id: u64, archive: impl AsRef, after: Option<&Page>) -> Result<()> { + let local = self.replica.snapshot()?; + let remote = self.replica.remote_snapshot()?; + self.replica + .release_attempt(id, &local, &remote, archive.as_ref(), after)?; + self.wake(); + Ok(()) + } + /// Captures both images, the queue and its states in a read-only archive. pub fn export_recovery(&self, path: impl AsRef) -> Result<()> { self.replica.export_recovery(path) diff --git a/crates/notebook/src/sync.rs b/crates/notebook/src/sync.rs index 56b71f181a3cf4db24010ae7a15e3a8cbe09367b..5093fbe14797971599f6262ef65685d9bc65ca27 100644 --- a/crates/notebook/src/sync.rs +++ b/crates/notebook/src/sync.rs @@ -27,7 +27,7 @@ pub enum ConflictKind { ContentChanged = 3, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum EditStatus { Pending, /// Retained publication attempt; this revision alone may be insufficient to confirm it. @@ -39,6 +39,11 @@ pub enum EditStatus { Published { revision: ExGuid, }, + /// Retired unpublished by a reviewed release; the archive at this path holds the + /// intent, its attempt evidence and both images. + Archived { + archive: String, + }, } impl Replica { @@ -355,6 +360,120 @@ impl Replica { }) } + /// Retires the oldest edit's uncertain attempt after review. The branch is first + /// exported to `archive` (a new file), which is the record: no receipt is written. + /// `Some(after)` continues from the reviewed page as a fresh intent against the + /// current remote page, keeping the edit's id and its dependents; `None` abandons + /// the whole local branch, whose edits become `Archived`, and the working image + /// returns to the remote image. Both need the reviewed images to be current. + pub fn release_attempt( + &self, + id: u64, + local: &[u8], + remote: &[u8], + archive: &Path, + after: Option<&Page>, + ) -> Result<()> { + let owner = self.sync_owner()?; + let intent = self + .pending()? + .into_iter() + .next() + .filter(|intent| intent.id == id) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "Only the oldest edit can hold an attempt", + ) + })?; + let continued = match after { + Some(after) => { + let Operation::Page(edit) = intent.operation else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Select a page save to continue from", + ) + .into()); + }; + let before = page_of(remote, intent.space)?.ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "The page is no longer in the remote section", + ) + })?; + PreparedEdit::page(remote, intent.space, after, &edit.author)?; + Some(Operation::Page(PageIntent { + before, + after: after.clone(), + author: edit.author, + })) + } + None => None, + }; + let mut connection = self + .connection + .lock() + .map_err(|_| io::Error::other("Cache owner panicked"))?; + let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?; + let (base, working): (Vec, Vec) = + transaction.query_row("SELECT base, working FROM replica WHERE id=1", [], |row| { + Ok((row.get(0)?, row.get(1)?)) + })?; + if working != local || base != remote { + return Err(io::Error::new( + io::ErrorKind::ResourceBusy, + "The reviewed cache images changed", + ) + .into()); + } + let id = i64::try_from(id).map_err(io::Error::other)?; + let attempted: bool = transaction.query_row( + "SELECT EXISTS(SELECT 1 FROM attempt WHERE edit_id=?1)", + [id], + |row| row.get(0), + )?; + if !attempted { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "The edit has no uncertain attempt", + ) + .into()); + } + drop(transaction); + drop(connection); + self.export_recovery(archive)?; + let mut connection = self + .connection + .lock() + .map_err(|_| io::Error::other("Cache owner panicked"))?; + let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?; + transaction.execute("DELETE FROM attempt WHERE edit_id=?1", [id])?; + match continued { + Some(operation) => { + transaction.execute( + "UPDATE edits SET operation=?1 WHERE id=?2", + params![ + serde_json::to_string(&operation).map_err(io::Error::other)?, + id + ], + )?; + } + None => { + transaction.execute( + "INSERT INTO archived(edit_id, archive) SELECT id, ?1 FROM edits", + [archive.to_string_lossy().into_owned()], + )?; + transaction.execute("DELETE FROM edits", [])?; + transaction.execute("UPDATE replica SET working=base WHERE id=1", [])?; + } + } + transaction.commit()?; + drop(connection); + drop(owner); + self.wake_sync(); + Ok(()) + } + fn resolve_conflict( &self, id: u64, @@ -462,6 +581,16 @@ pub(crate) fn status(connection: &Connection, id: u64) -> Result(0), + ) + .optional()? + { + return Ok(Some(EditStatus::Archived { archive })); + } let record: Option<(Option, Option, String)> = connection.query_row( "SELECT attempt.revisions, conflicts.kind, edits.space FROM edits LEFT JOIN attempt ON attempt.edit_id=edits.id LEFT JOIN conflicts ON conflicts.edit_id=edits.id WHERE edits.id=?1", [id], |row| Ok((row.get(0)?,row.get(1)?, row.get(2)?))).optional()?; Ok(match record { diff --git a/crates/notebook/tests/changes.rs b/crates/notebook/tests/changes.rs new file mode 100644 index 0000000000000000000000000000000000000000..1131d39958c722393ffcedb30c3b651d8d264c9b --- /dev/null +++ b/crates/notebook/tests/changes.rs @@ -0,0 +1,78 @@ +use notebook::session::{Change, Notebook}; +use std::io::Write; + +/// One client watches a notebook while another changes it on disk; changes follow file +/// identity, and an unreachable notebook keeps the last catalog. +#[test] +fn external_structure_changes_are_reported_by_identity() { + let temporary = tempfile::tempdir().unwrap(); + let root = temporary.path().join("notebook"); + std::fs::create_dir(&root).unwrap(); + let first = onestore::create_section("First.one", "First page", "Author").unwrap(); + std::fs::File::create_new(root.join("First.one")) + .unwrap() + .write_all(&first) + .unwrap(); + let first_id = onestore::Store::parse(&first).unwrap().header.file_id; + std::fs::write( + root.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents("Open Notebook.onetoc2", &[("First.one", first_id)]) + .unwrap(), + ) + .unwrap(); + let mut watcher = Notebook::open(&root, temporary.path().join("watcher")).unwrap(); + let mut other = Notebook::open(&root, temporary.path().join("other")).unwrap(); + assert_eq!(watcher.refresh().unwrap(), []); + + other.create_section("", "Second", "Author").unwrap(); + other.create_group("", "Group").unwrap(); + other.create_section("Group", "Inner", "Author").unwrap(); + assert_eq!( + watcher.refresh().unwrap(), + [ + Change::Added("Second.one".into()), + Change::Added("Group".into()), + Change::Added("Group/Inner.one".into()), + ] + ); + + other.rename("Second.one", "Renamed").unwrap(); + assert_eq!( + watcher.refresh().unwrap(), + [Change::Moved { + from: "Second.one".into(), + to: "Renamed.one".into() + }] + ); + + other.reorder("", &["Renamed.one", "First.one"]).unwrap(); + assert_eq!(watcher.refresh().unwrap(), [Change::Reordered("".into())]); + + other.delete("First.one").unwrap(); + assert_eq!( + watcher.refresh().unwrap(), + [ + Change::Moved { + from: "First.one".into(), + to: "OneNote_RecycleBin/First.one".into() + }, + Change::Added("OneNote_RecycleBin".into()), + ] + ); + std::fs::remove_dir_all(root.join("OneNote_RecycleBin")).unwrap(); + assert_eq!( + watcher.refresh().unwrap(), + [ + Change::Removed("OneNote_RecycleBin".into()), + Change::Removed("OneNote_RecycleBin/First.one".into()), + ] + ); + + let away = temporary.path().join("away"); + std::fs::rename(&root, &away).unwrap(); + assert!(watcher.refresh().is_err()); + assert_eq!(watcher.catalog().sections.len(), 1); + std::fs::rename(&away, &root).unwrap(); + assert_eq!(watcher.refresh().unwrap(), []); + assert_eq!(watcher.refresh().unwrap(), []); +} diff --git a/crates/notebook/tests/links.rs b/crates/notebook/tests/links.rs new file mode 100644 index 0000000000000000000000000000000000000000..39307dc6bc5a259f238786ca8bec97d9dab4946c --- /dev/null +++ b/crates/notebook/tests/links.rs @@ -0,0 +1,78 @@ +use notebook::session::Notebook; +use onestore::page::link::{LinkTarget, internal_link}; +use std::io::Write; + +fn write_section(root: &std::path::Path, name: &str, text: &str) -> [u8; 16] { + let bytes = onestore::create_section(name, text, "Author").unwrap(); + std::fs::File::create_new(root.join(name)) + .unwrap() + .write_all(&bytes) + .unwrap(); + onestore::Store::parse(&bytes).unwrap().header.file_id +} + +/// Links resolve by identity: a page keeps its link when its section is renamed or moved, +/// and a link whose section identity is stale still finds the page in another section. +#[test] +fn internal_links_find_their_page_by_identity() { + let temporary = tempfile::tempdir().unwrap(); + let root = temporary.path().join("notebook"); + std::fs::create_dir(&root).unwrap(); + let first = write_section(&root, "First.one", "Target page"); + let second = write_section(&root, "Second.one", "Other page"); + std::fs::write( + root.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents( + "Open Notebook.onetoc2", + &[("First.one", first), ("Second.one", second)], + ) + .unwrap(), + ) + .unwrap(); + let mut notebook = Notebook::open(&root, temporary.path().join("cache")).unwrap(); + let section = notebook.section("First.one", || {}).unwrap(); + let (space, _) = section.pages().unwrap()[0]; + let page = section.page(space).unwrap(); + section.close().unwrap(); + let target = LinkTarget::Page { + identity: page.identity.unwrap(), + title: &page.title, + }; + let link = internal_link(first, "C:\\notebook\\First.one", target); + assert_eq!( + notebook.find_page(&link).unwrap(), + Some(("First.one".to_owned(), Some(space))) + ); + assert_eq!( + notebook + .find_page(&internal_link(first, "", LinkTarget::Section)) + .unwrap(), + Some(("First.one".to_owned(), None)) + ); + assert_eq!( + notebook.find_page("https://example.invalid/").unwrap(), + None + ); + + notebook.create_group("", "Group").unwrap(); + notebook.rename("First.one", "Renamed").unwrap(); + assert_eq!( + notebook.find_page(&link).unwrap(), + Some(("Renamed.one".to_owned(), Some(space))) + ); + + let stale = internal_link(second, "", target); + assert_eq!( + notebook.find_page(&stale).unwrap(), + Some(("Renamed.one".to_owned(), Some(space))) + ); + let unknown = internal_link( + second, + "", + LinkTarget::Page { + identity: [7; 16], + title: "Gone", + }, + ); + assert_eq!(notebook.find_page(&unknown).unwrap(), None); +} diff --git a/crates/notebook/tests/protected.rs b/crates/notebook/tests/protected.rs new file mode 100644 index 0000000000000000000000000000000000000000..9fe75e76a21ce67bce41efb8e85eb0d7eeaf9c9d --- /dev/null +++ b/crates/notebook/tests/protected.rs @@ -0,0 +1,29 @@ +#![cfg(feature = "protected")] + +use notebook::{discover::SectionState, session::Notebook}; +use std::path::Path; + +/// A locked section is discovered as such and reads with its password, page by page. +#[test] +fn a_locked_section_unlocks_for_reading() { + let root = Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../corpus/native-protected-boundaries" + )); + let manifest: serde_json::Value = + serde_json::from_slice(&std::fs::read(root.join("manifest.json")).unwrap()).unwrap(); + let password = manifest["password"].as_str().unwrap(); + let temporary = tempfile::tempdir().unwrap(); + let notebook = Notebook::open(root.join("notebook"), temporary.path()).unwrap(); + let section = ¬ebook.catalog().sections[0]; + assert!(matches!(section.state, SectionState::Locked)); + let pages = notebook.unlock(§ion.path, password).unwrap(); + assert_eq!(pages.len(), 11); + assert!(pages.iter().all(|(_, page)| !page.title.is_empty())); + assert!(matches!( + notebook.unlock(§ion.path, "wrong"), + Err(notebook::Error::Protected( + onestore::protected::Error::PasswordMismatch + )) + )); +} diff --git a/crates/notebook/tests/session.rs b/crates/notebook/tests/session.rs index 2d221c31d5c24564a079b187656bb8de04028e4a..260862ab46f589dc05b28140c92d11a8a8047cad 100644 --- a/crates/notebook/tests/session.rs +++ b/crates/notebook/tests/session.rs @@ -680,3 +680,100 @@ fn a_conflicting_save_is_reviewed_against_the_remote_page_and_archived_for_recov assert!(section.queue().unwrap().is_empty()); section.close().unwrap(); } + +#[path = "support/server.rs"] +mod server; + +/// An uncertain attempt survives a restart as `AwaitingConfirmation`; after exporting the +/// archive the user either continues from a reviewed page or abandons the branch. Neither +/// path records a receipt for the uncertain attempt. +#[test] +fn an_uncertain_attempt_is_released_after_restart_by_review() { + for continued in [true, false] { + let directory = tempfile::tempdir().unwrap(); + let file = directory.path().join("notes.one"); + let cache = directory.path().join("cache.sqlite"); + let source = onestore::create_section("notes.one", "Original", "Author").unwrap(); + std::fs::write(&file, &source).unwrap(); + let replica = notebook::Replica::create(&cache, &source).unwrap(); + let space = space_of(&source); + let page = model_ops::page_of(&source, space); + let text = first_text(&page); + let local = edited(&page, "Uncertain "); + let id = replica + .save(&source, space, &local, "Author") + .unwrap() + .unwrap(); + let mut faulty = server::Server::new(&source); + faulty.fault = server::Fault::UnknownBefore; + assert!(replica.sync_once(&mut faulty).is_err()); + assert!(matches!( + replica.status(id).unwrap(), + Some(EditStatus::AwaitingConfirmation { .. }) + )); + drop(replica); + + let notified = Arc::new(AtomicUsize::new(0)); + let counter = Arc::clone(¬ified); + let section = Section::resume(&file, notebook::Replica::open(&cache).unwrap(), move || { + counter.fetch_add(1, Ordering::SeqCst); + }) + .unwrap(); + wait( + §ion, + |event| matches!(event, Event::Attempt { id: n, status: EditStatus::AwaitingConfirmation { .. } } if *n == id), + ); + assert_eq!( + section + .queue() + .unwrap() + .iter() + .map(|edit| edit.id) + .collect::>(), + [id] + ); + assert_same(section.page(space).unwrap(), &local); + let archive = directory.path().join("review.sqlite"); + if continued { + let mut reviewed = section.remote_page(space).unwrap(); + model_ops::replace_text(&mut reviewed, text, 0..0, "Reviewed "); + section.release(id, &archive, Some(&reviewed)).unwrap(); + assert_eq!(section.status(id).unwrap(), Some(EditStatus::Pending)); + published(§ion, id); + assert_same(stored_page(&file, space), &reviewed); + } else { + section.release(id, &archive, None).unwrap(); + assert_eq!( + section.status(id).unwrap(), + Some(EditStatus::Archived { + archive: archive.to_string_lossy().into_owned() + }) + ); + assert_same(section.page(space).unwrap(), &page); + assert_eq!(stored_page(&file, space), page); + } + let recovery = Recovery::open(&archive).unwrap(); + assert_eq!(recovery.pending().unwrap().len(), 1); + assert!(matches!( + recovery.status(id).unwrap(), + Some(EditStatus::AwaitingConfirmation { .. }) + )); + assert!(section.queue().unwrap().is_empty()); + assert!( + section + .release(id, directory.path().join("again.sqlite"), None) + .is_err() + ); + section.close().unwrap(); + } +} + +fn space_of(source: &[u8]) -> ExGuid { + let store = onestore::Store::parse(source).unwrap(); + let index = onestore::RevisionIndex::parse(&store).unwrap(); + onestore::document::Document::parse(&index) + .unwrap() + .pages() + .unwrap()[0] + .0 +} diff --git a/crates/notebook/tests/structure.rs b/crates/notebook/tests/structure.rs new file mode 100644 index 0000000000000000000000000000000000000000..6274848d901a6ee49db5374bd193c0b9b0f6ae3d --- /dev/null +++ b/crates/notebook/tests/structure.rs @@ -0,0 +1,173 @@ +use notebook::session::Notebook; +use std::io::Write; + +/// Each file's header names its parent TOC and carries the name CRC OneNote wrote for the +/// same name in `corpus/notebook-edit/{structured,deleted}/cold`. +fn placement(root: &std::path::Path, expected: &[(&str, u32)]) { + let header = |path: std::path::PathBuf| { + onestore::Store::parse(&std::fs::read(path).unwrap()) + .unwrap() + .header + }; + for (path, crc) in expected { + let (folder, name) = path.rsplit_once('/').unwrap_or(("", path)); + let parent = header(root.join(folder).join("Open Notebook.onetoc2")).file_id; + let file = if name.ends_with(".one") { + root.join(path) + } else { + root.join(path).join("Open Notebook.onetoc2") + }; + let header = header(file); + assert_eq!((header.ancestor, header.name_crc), (parent, *crc), "{path}"); + } +} + +fn names(folder: ¬ebook::discover::Folder) -> (Vec, Vec) { + ( + folder.sections.iter().map(|s| s.path.clone()).collect(), + folder.groups.iter().map(|g| g.path.clone()).collect(), + ) +} + +/// `NOTEBOOK_STRUCTURE_EXPORT` names a new directory receiving the final notebook for a +/// cold reopen. +#[test] +fn sections_and_groups_are_created_renamed_coloured_ordered_and_deleted() { + let temporary = tempfile::tempdir().unwrap(); + let root = temporary.path().join("notebook"); + std::fs::create_dir(&root).unwrap(); + let first = onestore::create_section("First.one", "First page", "Author").unwrap(); + std::fs::File::create_new(root.join("First.one")) + .unwrap() + .write_all(&first) + .unwrap(); + let first_id = onestore::Store::parse(&first).unwrap().header.file_id; + std::fs::write( + root.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents("Open Notebook.onetoc2", &[("First.one", first_id)]) + .unwrap(), + ) + .unwrap(); + let toc = onestore::read_file(root.join("Open Notebook.onetoc2")).unwrap(); + let toc_id = onestore::Store::parse(&toc).unwrap().header.file_id; + onestore::place_file(root.join("First.one"), toc_id, "First.one").unwrap(); + let mut notebook = Notebook::open(&root, temporary.path().join("cache")).unwrap(); + assert_eq!( + notebook.create_section("", "Second", "Author").unwrap(), + "Second.one" + ); + assert_eq!(notebook.create_group("", "Archive").unwrap(), "Archive"); + assert_eq!( + notebook + .create_section("Archive", "Inner", "Author") + .unwrap(), + "Archive/Inner.one" + ); + assert_eq!( + names(notebook.catalog()), + ( + vec!["First.one".to_owned(), "Second.one".to_owned()], + vec!["Archive".to_owned()] + ) + ); + assert_eq!( + names(¬ebook.catalog().groups[0]), + (vec!["Archive/Inner.one".to_owned()], vec![]) + ); + assert_eq!( + notebook.rename("Second.one", "Renamed").unwrap(), + "Renamed.one" + ); + assert_eq!(notebook.rename("Archive", "Kept").unwrap(), "Kept"); + notebook + .set_section_color("Renamed.one", Some(0x5ed7ff)) + .unwrap(); + notebook.reorder("", &["Kept", "Renamed.one"]).unwrap(); + assert_eq!( + names(notebook.catalog()), + ( + vec!["Renamed.one".to_owned(), "First.one".to_owned()], + vec!["Kept".to_owned()] + ) + ); + let toc = notebook.catalog().toc.as_ref().unwrap(); + let orders: Vec<(Option, u32)> = toc + .unresolved + .iter() + .map(|entry| (entry.filename.clone(), entry.order)) + .collect(); + assert!(orders.is_empty(), "{orders:?}"); + let colour = onestore::read_file(root.join("Renamed.one")).unwrap(); + let store = onestore::Store::parse(&colour).unwrap(); + let index = onestore::RevisionIndex::parse(&store).unwrap(); + let document = onestore::document::Document::parse(&index).unwrap(); + let revision = document.active(document.root).unwrap(); + let onestore::document::Kind::SectionMetadata { color, .. } = + &revision.nodes[&revision.roots[&2]].kind + else { + panic!() + }; + assert_eq!(*color, Some(0x5ed7ff)); + placement( + &root, + &[ + ("First.one", 0x2e1b24f3), + ("Renamed.one", 0x6108912a), + ("Kept", 0xf2a400f4), + ("Kept/Inner.one", 0x83e261da), + ], + ); + if let Some(directory) = std::env::var_os("NOTEBOOK_STRUCTURE_EXPORT") { + copy_dir(&root, std::path::Path::new(&directory)); + } + notebook.delete("First.one").unwrap(); + assert_eq!( + names(notebook.catalog()), + ( + vec!["Renamed.one".to_owned()], + vec!["Kept".to_owned(), "OneNote_RecycleBin".to_owned()] + ) + ); + assert_eq!( + names(¬ebook.catalog().groups[1]), + (vec!["OneNote_RecycleBin/First.one".to_owned()], vec![]) + ); + assert!(root.join("OneNote_RecycleBin/First.one").exists()); + let bin = onestore::read_file(root.join("OneNote_RecycleBin/Open Notebook.onetoc2")).unwrap(); + assert!( + String::from_utf16_lossy( + &bin.chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect::>() + ) + .contains("First.one") + ); + assert!(notebook.rename("Kept/Inner.one", "First").is_ok()); + assert!(notebook.delete("Missing.one").is_err()); + placement( + &root, + &[ + ("Renamed.one", 0x6108912a), + ("Kept", 0xf2a400f4), + ("Kept/First.one", 0x2e1b24f3), + ("OneNote_RecycleBin", 0x986b646a), + ("OneNote_RecycleBin/First.one", 0x2e1b24f3), + ], + ); + if let Some(directory) = std::env::var_os("NOTEBOOK_STRUCTURE_EXPORT_DELETED") { + copy_dir(&root, std::path::Path::new(&directory)); + } +} + +fn copy_dir(from: &std::path::Path, to: &std::path::Path) { + std::fs::create_dir_all(to).unwrap(); + for entry in std::fs::read_dir(from).unwrap() { + let entry = entry.unwrap(); + let target = to.join(entry.file_name()); + if entry.file_type().unwrap().is_dir() { + copy_dir(&entry.path(), &target); + } else { + std::fs::copy(entry.path(), target).unwrap(); + } + } +} diff --git a/crates/notebook/tests/sync.rs b/crates/notebook/tests/sync.rs index 047b59de8873a97452f134398402d525a4724027..f2702fdfd81e39fc01c58932a45e2d5250ca3c1b 100644 --- a/crates/notebook/tests/sync.rs +++ b/crates/notebook/tests/sync.rs @@ -81,7 +81,7 @@ fn recovery_archive_preserves_the_queue_uncertainty_and_receipts_without_becomin assert_eq!(archive.snapshot().unwrap(), working); assert_eq!(archive.remote_snapshot().unwrap(), remote); assert_eq!(archive.pending().unwrap(), pending); - assert_eq!(archive.status(published).unwrap(), Some(receipt)); + assert_eq!(archive.status(published).unwrap(), Some(receipt.clone())); assert_eq!(archive.status(attempted).unwrap(), uncertain); for edit in &pending { assert_eq!( @@ -89,7 +89,7 @@ fn recovery_archive_preserves_the_queue_uncertainty_and_receipts_without_becomin cache.status(edit.id).unwrap() ); } - let EditStatus::Published { revision } = receipt else { + let EditStatus::Published { revision } = receipt.clone() else { panic!() }; assert_eq!(archive.receipts().unwrap(), [(published, revision)].into()); @@ -158,7 +158,7 @@ fn recovery_archive_retains_conflict_images_and_rejects_foreign_or_future_archiv let archive = Recovery::open(&path).unwrap(); assert_eq!(text(&archive.snapshot().unwrap()).2, "Local"); assert_eq!(archive.remote_snapshot().unwrap(), changed); - assert_eq!(archive.status(id).unwrap(), Some(outcome.1)); + assert_eq!(archive.status(id).unwrap(), Some(outcome.1.clone())); assert_eq!(archive.summary().unwrap().conflicts, 1); assert_eq!(archive.summary().unwrap().uncertain_edits, 0); drop(archive); @@ -173,7 +173,7 @@ fn recovery_archive_retains_conflict_images_and_rejects_foreign_or_future_archiv assert!(Recovery::open(&path).is_err()); assert_eq!(std::fs::read(&path).unwrap(), before); } - assert_eq!(cache.status(id).unwrap(), Some(outcome.1)); + assert_eq!(cache.status(id).unwrap(), Some(outcome.1.clone())); assert_eq!(text(&cache.snapshot().unwrap()).2, "Local"); } @@ -195,7 +195,7 @@ fn disjoint_remote_changes_merge_and_persist_the_remote_receipt() { assert!(cache.pending().unwrap().is_empty()); drop(cache); let cache = Replica::open(&path).unwrap(); - assert_eq!(cache.status(id).unwrap(), Some(outcome.1)); + assert_eq!(cache.status(id).unwrap(), Some(outcome.1.clone())); assert_eq!(cache.sync_once(&mut server).unwrap(), None); assert_eq!(server.publications, 1); assert_eq!(cache.status(id + 1).unwrap(), None); @@ -849,17 +849,17 @@ fn remote_restore_retains_historical_receipts_without_replaying_them() { assert_eq!(cache.sync_once(&mut server).unwrap(), None); assert_eq!(cache.snapshot().unwrap(), source); assert_eq!(cache.remote_snapshot().unwrap(), source); - assert_eq!(cache.status(published).unwrap(), Some(receipt)); + assert_eq!(cache.status(published).unwrap(), Some(receipt.clone())); assert_eq!(server.publications, 1); assert!(cache.pending().unwrap().is_empty()); let archive = notebook::Recovery::open(directory.path().join("published.sqlite")).unwrap(); assert_eq!(archive.snapshot().unwrap(), published_image); - assert_eq!(archive.status(published).unwrap(), Some(receipt)); + assert_eq!(archive.status(published).unwrap(), Some(receipt.clone())); assert_eq!(text(&archive.snapshot().unwrap()).2, "Published Original"); drop(cache); let cache = Replica::open(path).unwrap(); assert_eq!(text(&cache.snapshot().unwrap()).2, "Original"); - assert_eq!(cache.status(published).unwrap(), Some(receipt)); + assert_eq!(cache.status(published).unwrap(), Some(receipt.clone())); } #[test] @@ -892,10 +892,10 @@ fn remote_restore_rebases_unsent_work_but_never_replays_an_uncertain_attempt() { let attempts = server.publications; let cache = Replica::open(&path).unwrap(); let (_, status) = cache.sync_once(&mut server).unwrap().unwrap(); - assert_eq!(cache.status(published).unwrap(), Some(receipt)); + assert_eq!(cache.status(published).unwrap(), Some(receipt.clone())); if uncertain { assert!(matches!(status, EditStatus::AwaitingConfirmation { .. })); - assert_eq!(Some(status), prior_status); + assert_eq!(Some(status.clone()), prior_status); assert_eq!(server.publications, attempts); assert_eq!(server.visible, source); assert_eq!(cache.snapshot().unwrap(), local); @@ -913,8 +913,8 @@ fn remote_restore_rebases_unsent_work_but_never_replays_an_uncertain_attempt() { assert_eq!(archive.status(queued).unwrap(), prior_status); drop(cache); let cache = Replica::open(&path).unwrap(); - assert_eq!(cache.status(queued).unwrap(), Some(status)); - assert_eq!(cache.status(published).unwrap(), Some(receipt)); + assert_eq!(cache.status(queued).unwrap(), Some(status.clone())); + assert_eq!(cache.status(published).unwrap(), Some(receipt.clone())); } } @@ -934,7 +934,7 @@ fn restore_conflicts_keep_dependent_work_and_reject_stale_review() { let conflict = EditStatus::Conflict(ConflictKind::ContentChanged); assert_eq!( cache.sync_once(&mut server).unwrap(), - Some((head, conflict)) + Some((head, conflict.clone())) ); let end = u32::try_from(text(&cache.snapshot().unwrap()).2.encode_utf16().count()).unwrap(); let dependent = save(&cache, object, end..end, " Later").unwrap(); @@ -947,7 +947,7 @@ fn restore_conflicts_keep_dependent_work_and_reject_stale_review() { .unwrap(); drop(cache); let cache = Replica::open(&path).unwrap(); - assert_eq!(cache.status(head).unwrap(), Some(conflict)); + assert_eq!(cache.status(head).unwrap(), Some(conflict.clone())); assert_eq!(cache.pending().unwrap(), pending); assert_eq!(cache.snapshot().unwrap(), local); let reviewed = edited(&source, object, 0..0, "Revised "); @@ -956,7 +956,7 @@ fn restore_conflicts_keep_dependent_work_and_reject_stale_review() { server.durable.clone_from(&changed); assert_eq!( cache.sync_once(&mut server).unwrap(), - Some((head, conflict)) + Some((head, conflict.clone())) ); assert!( matches!(cache.review_page(head, &local, &source, &reviewed), @@ -975,7 +975,7 @@ fn restore_conflicts_keep_dependent_work_and_reject_stale_review() { Some((actual, EditStatus::Published { .. })) if actual == head)); assert_eq!( cache.sync_once(&mut server).unwrap(), - Some((dependent, conflict)) + Some((dependent, conflict.clone())) ); assert_eq!(server.publications, 2); let dependent_local = cache.snapshot().unwrap(); @@ -989,11 +989,11 @@ fn restore_conflicts_keep_dependent_work_and_reject_stale_review() { assert!(matches!(cache.sync_once(&mut server).unwrap(), Some((actual, EditStatus::Published { .. })) if actual == dependent)); assert_eq!(text(&server.durable).2, "Revised Original Later Remote"); - assert_eq!(cache.status(published).unwrap(), Some(receipt)); + assert_eq!(cache.status(published).unwrap(), Some(receipt.clone())); let archive = notebook::Recovery::open(directory.path().join("conflicted.sqlite")).unwrap(); assert_eq!(archive.snapshot().unwrap(), local); assert_eq!(archive.pending().unwrap(), pending); - assert_eq!(archive.status(head).unwrap(), Some(conflict)); + assert_eq!(archive.status(head).unwrap(), Some(conflict.clone())); } #[test] diff --git a/crates/notebook/tests/sync_formatting.rs b/crates/notebook/tests/sync_formatting.rs index a45e04dceafbdd0ca61bca9600abee508d190558..4396f27daef2375976ad5cb673f4f125fc0c4271 100644 --- a/crates/notebook/tests/sync_formatting.rs +++ b/crates/notebook/tests/sync_formatting.rs @@ -391,10 +391,13 @@ fn uncertain_formatting_attempts_keep_the_original_attempt_and_never_replay() { assert!(matches!(state, EditStatus::AwaitingConfirmation { .. })); drop(cache); let cache = Replica::open(&path).unwrap(); - assert_eq!(cache.status(id).unwrap(), Some(state)); + assert_eq!(cache.status(id).unwrap(), Some(state.clone())); if matches!(fault, Fault::UnknownBefore) { for _ in 0..3 { - assert_eq!(cache.sync_once(&mut server).unwrap(), Some((id, state))); + assert_eq!( + cache.sync_once(&mut server).unwrap(), + Some((id, state.clone())) + ); } assert_eq!(server.publications, 1); assert_eq!(server.durable, OUTLINES); diff --git a/crates/notebook/tests/sync_outline.rs b/crates/notebook/tests/sync_outline.rs index 210b6f1f3519787f1c5ea440cba70863cd3f9281..f83c7b95204621a9e4716416b987c6ce416a4354 100644 --- a/crates/notebook/tests/sync_outline.rs +++ b/crates/notebook/tests/sync_outline.rs @@ -410,11 +410,17 @@ fn an_uncertain_layout_attempt_confirms_by_revision_or_by_an_equal_remote_page() assert_eq!((server.publications, server.confirmations), (1, 1)); continue; } - assert_eq!(cache.sync_once(&mut server).unwrap(), Some((id, status))); + assert_eq!( + cache.sync_once(&mut server).unwrap(), + Some((id, status.clone())) + ); server.visible = remote_with(&source, space, |page| { resized(216.0, true)(outline_mut(page, outline)); }); - assert_eq!(cache.sync_once(&mut server).unwrap(), Some((id, status))); + assert_eq!( + cache.sync_once(&mut server).unwrap(), + Some((id, status.clone())) + ); assert_eq!(cache.snapshot().unwrap(), local); server.visible = remote_with(&source, space, |page| { resized(144.0, true)(outline_mut(page, outline)); diff --git a/crates/notebook/tests/sync_page.rs b/crates/notebook/tests/sync_page.rs index 5f157c4955f5b2d417b885dfe86c992ec713e390..93101f65f5f916dab60a1ef358379d73db35cdac 100644 --- a/crates/notebook/tests/sync_page.rs +++ b/crates/notebook/tests/sync_page.rs @@ -140,7 +140,7 @@ fn uncertain_page_publication_retains_both_revisions_and_never_replays() { cache.export_recovery(&archive).unwrap(); assert_eq!( Recovery::open(&archive).unwrap().status(id).unwrap(), - Some(attempted) + Some(attempted.clone()) ); drop(cache); let db = rusqlite::Connection::open(&path).unwrap(); @@ -163,7 +163,7 @@ fn uncertain_page_publication_retains_both_revisions_and_never_replays() { assert!(observed[..212] == server.durable[..212]); assert!(observed[252..] == server.durable[252..]); } else { - assert_eq!(result, (id, attempted)); + assert_eq!(result, (id, attempted.clone())); assert_eq!(cache.snapshot().unwrap(), local); } } @@ -219,7 +219,10 @@ fn surviving_page_revision_alone_does_not_confirm_section_publication() { .contains_key(&revisions[&page.space()]) ); let cache = Replica::open(&path).unwrap(); - assert_eq!(cache.sync_once(&mut server).unwrap(), Some((id, attempted))); + assert_eq!( + cache.sync_once(&mut server).unwrap(), + Some((id, attempted.clone())) + ); assert_eq!(server.confirmations, 0); assert_eq!(server.publications, 1); server.visible = complete; diff --git a/crates/notebook/tests/sync_pages.rs b/crates/notebook/tests/sync_pages.rs index 9110551c573e86f28ce274f524e89c45958c2de2..baffaa9274d7f4ea701e5d7ad69f131fdd12456d 100644 --- a/crates/notebook/tests/sync_pages.rs +++ b/crates/notebook/tests/sync_pages.rs @@ -302,7 +302,7 @@ fn uncertain_page_batches_survive_recovery_and_do_not_replay() { cache.export_recovery(&archive).unwrap(); let recovery = Recovery::open(&archive).unwrap(); assert_eq!(recovery.pending().unwrap(), queue); - assert_eq!(recovery.status(id).unwrap(), Some(attempted)); + assert_eq!(recovery.status(id).unwrap(), Some(attempted.clone())); assert_eq!(recovery.snapshot().unwrap(), local); assert!( cache @@ -315,7 +315,7 @@ fn uncertain_page_batches_survive_recovery_and_do_not_replay() { assert!(matches!(result.1, EditStatus::Published { .. })); assert_eq!(server.confirmations, 1); } else { - assert_eq!(result, (id, attempted)); + assert_eq!(result, (id, attempted.clone())); assert_eq!(server.confirmations, 0); } } @@ -366,7 +366,10 @@ fn an_unchanged_section_revision_cannot_confirm_a_changed_page() { let cache = Replica::open(&path).unwrap(); let complete = server.visible.clone(); server.visible = source; - assert_eq!(cache.sync_once(&mut server).unwrap(), Some((id, attempted))); + assert_eq!( + cache.sync_once(&mut server).unwrap(), + Some((id, attempted.clone())) + ); assert_eq!(server.confirmations, 0); server.visible = complete; assert!(matches!( diff --git a/crates/notebook/tests/sync_paragraph.rs b/crates/notebook/tests/sync_paragraph.rs index dc3e19bd3042cdd8128fd03f169c6a66f08e15cc..a8967ed404d913f808a78988aed7f3dc92ba877b 100644 --- a/crates/notebook/tests/sync_paragraph.rs +++ b/crates/notebook/tests/sync_paragraph.rs @@ -732,7 +732,7 @@ fn uncertain_splits_and_joins_keep_the_original_attempt_across_reopen() { let state = cache.status(id).unwrap().unwrap(); drop(cache); let cache = Replica::open(&path).unwrap(); - assert_eq!(cache.status(id).unwrap(), Some(state)); + assert_eq!(cache.status(id).unwrap(), Some(state.clone())); match fault { Fault::Before => { assert_eq!(state, EditStatus::Pending); @@ -748,7 +748,10 @@ fn uncertain_splits_and_joins_keep_the_original_attempt_across_reopen() { assert_eq!(cache.pending().unwrap(), pending); assert_eq!(cache.snapshot().unwrap(), local); for _ in 0..3 { - assert_eq!(cache.sync_once(&mut server).unwrap(), Some((id, state))); + assert_eq!( + cache.sync_once(&mut server).unwrap(), + Some((id, state.clone())) + ); } assert_eq!(server.publications, 1); assert_eq!(server.durable, BEFORE); diff --git a/crates/notebook/tests/sync_worker.rs b/crates/notebook/tests/sync_worker.rs index aa85f40166a8e544cb10f40910826ddb66a62b60..1dced50807904d233f60917b3e42c38a3d13d7bc 100644 --- a/crates/notebook/tests/sync_worker.rs +++ b/crates/notebook/tests/sync_worker.rs @@ -108,7 +108,7 @@ fn reconnects_after_connect_read_and_uncertain_publish_without_replaying() { }, move |result| { observed_tx - .send(result.as_ref().copied().map_err(|error| error.to_string())) + .send(result.as_ref().cloned().map_err(|error| error.to_string())) .unwrap(); }, ) @@ -163,7 +163,7 @@ fn local_saves_wake_an_idle_worker_and_publish_every_writer_marker() { Duration::from_secs(3600), move || Ok(shared.clone()), move |result| { - observed_tx.send(*result.as_ref().unwrap()).unwrap(); + observed_tx.send(result.as_ref().unwrap().clone()).unwrap(); if first { first = false; resume_rx.recv_timeout(Duration::from_secs(5)).unwrap(); @@ -305,7 +305,7 @@ fn dropping_during_publication_is_nonblocking_and_retains_ownership_until_recove Duration::from_secs(3600), move || Ok(shared.clone()), move |result| { - tx.send(*result.as_ref().unwrap()).unwrap(); + tx.send(result.as_ref().unwrap().clone()).unwrap(); }, ) .unwrap(); @@ -377,7 +377,7 @@ fn polling_preserves_conflicts_and_absent_uncertain_revisions_without_replay() { Duration::from_millis(10), move || Ok(shared.clone()), move |result| { - tx.send(result.as_ref().copied().map_err(|_| ())).unwrap(); + tx.send(result.as_ref().cloned().map_err(|_| ())).unwrap(); }, ) .unwrap(); @@ -401,7 +401,7 @@ fn polling_preserves_conflicts_and_absent_uncertain_revisions_without_replay() { if let Some(previous) = previous { assert_eq!(previous, status); } - previous = Some(status); + previous = Some(status.clone()); } assert!( started.elapsed() >= Duration::from_millis(30), @@ -508,7 +508,7 @@ fn invalid_intervals_and_callback_panics_leave_worker_ownership_recoverable() { Duration::from_secs(3600), move || Ok(shared.clone()), move |result| { - tx.send(*result.as_ref().unwrap()).unwrap(); + tx.send(result.as_ref().unwrap().clone()).unwrap(); }, ) .unwrap(); @@ -535,7 +535,7 @@ fn reviewed_conflict_wakes_the_worker_and_publishes_the_original_intent_once() { Duration::from_secs(3600), move || Ok(shared.clone()), move |result| { - tx.send(*result.as_ref().unwrap()).unwrap(); + tx.send(result.as_ref().unwrap().clone()).unwrap(); if first { first = false; resume_rx.recv_timeout(Duration::from_secs(5)).unwrap(); @@ -615,7 +615,7 @@ fn ordinary_read_and_unpublished_write_contention_reuse_the_connection() { Duration::from_millis(1), move || Ok(remote.take().expect("Contention caused a reconnect")), move |result| { - tx.send(result.as_ref().copied().map_err(|error| error.to_string())) + tx.send(result.as_ref().cloned().map_err(|error| error.to_string())) .unwrap(); }, ) @@ -678,7 +678,7 @@ fn publication_backoff_drains_local_wakes_without_waiting_for_the_idle_poll() { .unwrap(); tx.send((second, None)).unwrap(); } - Ok(Some((id, status))) => tx.send((*id, Some(*status))).unwrap(), + Ok(Some((id, status))) => tx.send((*id, Some(status.clone()))).unwrap(), Ok(None) => {} other => panic!("Unexpected worker result: {other:?}"), }, diff --git a/crates/onestore/README.md b/crates/onestore/README.md index bc267d205c498f34e9ea2a67ccec8b8f6cc6da13..59b3843b71b8bc68bb4870da4cef7bec8d013357 100644 --- a/crates/onestore/README.md +++ b/crates/onestore/README.md @@ -43,6 +43,7 @@ harness also accepts `--client-profile release`. | `Object::file_reference`, `Store::file_data` | Identify internal/external payloads and read internal payload bytes | | `document::Document`, `Revision::text_runs` | Interpret document objects and inherited text formatting while retaining unknown properties and revision identities | | `Document::active`, `Document::pages_in`, `Revision::parents`, `RevisionIndex::active` | Resolve the active revision, the pages of a space and parent links without repeating the lookups | +| `page::link::internal_link`, `page::link::parse_internal_link` | Build and read the `onenote:#…` URLs OneNote stores for links to sections, pages and paragraphs, by identity | | `page::Page`, `page::Paragraph`, `page::Ink`, `page::Math` | Build an editable page model (title, outlines, paragraphs with coalesced text spans, tables, images, attachments, ink drawings and handwriting decoded to stroke polylines in page points) with stored identities; equations parse from their linear text and run data into a tree that renders the MathML OneNote exports; content outside the model is retained as `Unsupported` | | `protected::UnlockedSection` (optional feature) | Own decoded buffers for explicit known-password inspection; clear those buffers on drop; derived document strings/exports remain caller-owned | | `create_section` | Create one page containing one plain-text paragraph and an author, including Unicode | @@ -50,6 +51,8 @@ harness also accepts `--client-profile release`. | `PageEdit`, `PreparedEdit::pages` | Publish explicitly selected page moves and indentation changes together, preserving page content and historical revisions | | `PreparedEdit::delete_pages_permanently` | Remove explicit pages and their section references atomically while retaining stored revisions | | `create_table_of_contents` | Create ordered section entries from filenames and file identities | +| `TocEdit`, `PreparedEdit::table_of_contents` | Add, rename, colour, order and remove a table of contents' section and group entries as one revision | +| `place_file` | Name a file for its notebook as OneNote does on adoption (parent TOC identity and name CRC in the header), so OneNote keeps its identity | | `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 | @@ -57,7 +60,7 @@ harness also accepts `--client-profile release`. | `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | | `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | -| `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks (external and, via `page::link`, to pages, paragraphs and sections), bullets and numbering, note tags, table rows, columns, cell shading and indents, nested tables, inserted pictures (in paragraphs or on the page) and attachments, picture position, size and description, ink drawings and handwriting (strokes added and erased), equations (built from `page::Math` trees), paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | +| `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks (external and, via `page::link`, to pages, paragraphs and sections), bullets and numbering, note tags, table rows, columns, cell shading and indents, nested tables, inserted pictures (in paragraphs or on the page) and attachments, picture position, size and description, a stored attachment's shown name, source path and icon size, ink drawings and handwriting (strokes added and erased), equations (built from `page::Math` trees), paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | | `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically | | `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 | diff --git a/crates/onestore/src/commit.rs b/crates/onestore/src/commit.rs index ad2abf55a7f16bff33ae5a85f753a617095c0f12..48e65c2cb419e6036caebd5331b64b23bc69f583 100644 --- a/crates/onestore/src/commit.rs +++ b/crates/onestore/src/commit.rs @@ -71,6 +71,31 @@ impl Drop for FileIo { } } +/// Places a file in its notebook the way OneNote does on adoption: the header's +/// `guidAncestor` becomes the parent table of contents' file identity and `crcName` the CRC +/// of `name` (a section's file name, a group's folder name). OneNote re-identifies a file +/// whose header disagrees with its location, which orphans its TOC entry. +#[cfg(any(unix, windows))] +pub fn place_file(path: impl AsRef, ancestor: [u8; 16], name: &str) -> io::Result<()> { + let mut io = FileIo::open(path, true)?; + let mut header = [0; 1024]; + let result = (|| { + if io.read_at(0, &mut header)? != header.len() { + return Err(io::Error::from(ErrorKind::UnexpectedEof)); + } + crate::Header::parse(&header) + .map_err(|error| io::Error::new(ErrorKind::InvalidData, error.message))?; + let placement = crate::create::placement(ancestor, name); + if io.write_at(128, &placement)? != placement.len() { + return Err(io::Error::from(ErrorKind::WriteZero)); + } + io.flush() + })(); + let released = io.release(); + result?; + released +} + /// Reads a snapshot under the same whole-file exclusion used for commits. /// Native writers can expose incomplete graphs to unlocked filesystem reads. #[cfg(any(unix, windows))] @@ -192,6 +217,18 @@ impl<'a> PreparedEdit<'a> { }) } + /// Applies table-of-contents edits (sections and section groups: add, rename, colour, + /// order, remove) as one revision of a `.onetoc2` file. + pub fn table_of_contents( + source: &'a [u8], + edits: &[crate::TocEdit], + ) -> Result { + Ok(Self { + source, + written: crate::toc::edit_table_of_contents(source, edits)?, + }) + } + /// Permanently removes explicitly selected pages from the section in one transaction. /// Subpages must be selected explicitly; a surviving first subpage becomes top-level. /// Creates no recycle-bin copies. Prior revisions remain stored; this is not secure erasure. diff --git a/crates/onestore/src/create.rs b/crates/onestore/src/create.rs index df06699aafcf8ed0767db5095557602542406a0c..062d894eadd3e0b641e36b8ef78318b2f4ac3927 100644 --- a/crates/onestore/src/create.rs +++ b/crates/onestore/src/create.rs @@ -101,6 +101,15 @@ struct NewSpace { objects: Vec, } +/// Header bytes 128..148: `guidAncestor` and `crcName`, which OneNote checks against a +/// file's location and name; a mismatch makes it re-identify the file. +pub(crate) fn placement(ancestor: [u8; 16], name: &str) -> [u8; 20] { + let mut bytes = [0; 20]; + bytes[..16].copy_from_slice(&ancestor); + bytes[16..].copy_from_slice(&(!crc(u32::MAX, &string(name), FileType::Section)).to_le_bytes()); + bytes +} + pub(crate) fn current_timestamps() -> Result<(u32, u64)> { let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -537,8 +546,7 @@ fn create(file_name: &str, file_type: FileType, spaces: Vec) -> Result output[at..at + 4].copy_from_slice(&u32::MAX.to_le_bytes()); } output[96..100].copy_from_slice(&1_u32.to_le_bytes()); - output[144..148] - .copy_from_slice(&(!crc(u32::MAX, &string(file_name), FileType::Section)).to_le_bytes()); + output[128..148].copy_from_slice(&placement([0; 16], file_name)); for (at, chunk) in hashed_chunk .map(|chunk| (148, chunk)) .into_iter() diff --git a/crates/onestore/src/lib.rs b/crates/onestore/src/lib.rs index 06a63f2e28bb773430ac9fd77976a81aaf077298..b40bd2dd500c157b342581fc669defb8ac4a6502 100644 --- a/crates/onestore/src/lib.rs +++ b/crates/onestore/src/lib.rs @@ -21,6 +21,7 @@ pub mod protected; mod revisions; mod snapshot; mod store; +mod toc; mod tree; mod write; @@ -30,7 +31,8 @@ pub use commit::{ }; #[cfg(any(unix, windows))] pub use commit::{ - commit_file_property, commit_file_text, confirm_file_snapshot, read_file, read_file_limited, + commit_file_property, commit_file_text, confirm_file_snapshot, place_file, read_file, + read_file_limited, }; pub use create::{create_section, create_table_of_contents}; pub use edit::replace_text; @@ -45,5 +47,6 @@ pub use properties::{IdStream, Property, PropertySets, Value}; pub use revisions::{ExGuid, ObjectSpace, Revision, RevisionIndex}; pub use snapshot::{read_snapshot, read_storage_snapshot}; pub use store::{Chunk, Error, FileType, Header, Node, NodeList, Reference, Store}; +pub use toc::TocEdit; pub use tree::TreeEdit; pub use write::replace_property_bytes; diff --git a/crates/onestore/src/page/link.rs b/crates/onestore/src/page/link.rs index c2a84e773fdbdd031e1701d869c3cc17e78eeb3f..e33e4b7b6057f0d3f71ca12d45048003d47530a4 100644 --- a/crates/onestore/src/page/link.rs +++ b/crates/onestore/src/page/link.rs @@ -62,6 +62,49 @@ pub fn internal_link(section: [u8; 16], base_path: &str, target: LinkTarget<'_>) url } +/// The identities a stored internal link names. OneNote resolves links by identity, so +/// the title and base path in the URL are hints only. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct InternalLink { + pub section: [u8; 16], + pub page: Option<[u8; 16]>, + pub object: Option, +} + +/// The identities in an `onenote:#…` link, or `None` for any other URL. +pub fn parse_internal_link(url: &str) -> Option { + let guid = |value: &str| { + format!("{value},0") + .parse::() + .ok() + .map(|id| id.guid) + }; + let parts: Vec<&str> = url.strip_prefix("onenote:#")?.split('&').collect(); + let value = |name: &str| { + parts.iter().find_map(|part| { + part.strip_prefix(name) + .and_then(|rest| rest.strip_prefix('=')) + }) + }; + let section = guid(value("section-id")?)?; + let page = match value("page-id") { + Some(id) => Some(guid(id)?), + None => None, + }; + let object = match parts.iter().position(|part| part.starts_with("object-id=")) { + Some(at) => Some(ExGuid { + guid: guid(&parts[at]["object-id=".len()..])?, + n: parts.get(at + 1)?.parse().ok()?, + }), + None => None, + }; + Some(InternalLink { + section, + page, + object, + }) +} + /// Percent-encodes a page title the way OneNote does in a link fragment. fn encoded(title: &str) -> String { let mut out = String::new(); diff --git a/crates/onestore/src/page/mod.rs b/crates/onestore/src/page/mod.rs index 642e28bea9cca331e8adafe3f23e96e5fb2b9e4e..ea7d612c5f944047551fad0f00ec8e6c38d6284d 100644 --- a/crates/onestore/src/page/mod.rs +++ b/crates/onestore/src/page/mod.rs @@ -331,6 +331,22 @@ impl Page { Self::from_revision(revision, id) } + /// The notebook-management identity of the page in an active page revision, which + /// internal links name. + pub fn identity_of(revision: &Revision<'_>) -> Option<[u8; 16]> { + revision + .roots + .get(&2) + .and_then(|id| revision.nodes.get(id)) + .filter(|node| matches!(node.kind, Kind::Metadata { .. })) + .and_then(|node| node.extra.first()) + .and_then(|fields| fields.iter().find(|field| field.id == 0x1c001c30)) + .and_then(|field| match field.value { + FieldValue::Bytes(bytes) => bytes.try_into().ok(), + _ => None, + }) + } + /// The single active page declared in one page object space. pub fn from_space(document: &Document<'_>, space: ExGuid) -> Result { let pages = document.pages_in(space)?; @@ -365,12 +381,7 @@ impl Page { .and_then(|node| node.extra.first()); let mut page = Self { title: page_title(revision, id).unwrap_or_default().to_owned(), - identity: metadata - .and_then(|fields| fields.iter().find(|field| field.id == 0x1c001c30)) - .and_then(|field| match field.value { - FieldValue::Bytes(bytes) => bytes.try_into().ok(), - _ => None, - }), + identity: Self::identity_of(revision), created: metadata .and_then(|fields| fields.iter().find(|field| field.id == 0x18001c65)) .map(|field| { diff --git a/crates/onestore/src/page/write.rs b/crates/onestore/src/page/write.rs index 8cb3217bf672b5da8e43c65ec37a6a45bb12d9b5..b6b037a8e48b82f8bc4d61040df7ea73fb636cef 100644 --- a/crates/onestore/src/page/write.rs +++ b/crates/onestore/src/page/write.rs @@ -1,7 +1,9 @@ //! Publishes an edited page model by lowering the difference from the stored page onto //! the typed writers, then squashing their transactions into one revision per space. -use super::{Image, Ink, Math, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table}; +use super::{ + Attachment, Image, Ink, Math, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table, +}; use crate::{ Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets, RevisionIndex, Store, TextAttribute, TreeEdit, Value, @@ -650,8 +652,8 @@ impl Lowering<'_> { } } (ParagraphContent::Attachment(a), ParagraphContent::Attachment(b)) => { - if a != b { - return Err(invalid("A stored attachment cannot be edited")); + if a.id != b.id { + return Err(invalid("Attachment identity cannot change")); } } _ => return Err(invalid("Paragraph content type cannot change")), @@ -904,21 +906,24 @@ impl Lowering<'_> { let ParagraphContent::Attachment(attachment) = ¶graph.content else { continue; }; - if let Some(previous) = old.paragraphs.get(paragraph_id) { - if previous.content != paragraph.content { - return Err(invalid("A stored attachment cannot be edited")); - } - continue; - } - let Some(bytes) = &attachment.bytes else { - return Err(invalid("A new attachment needs its payload")); - }; let name = attachment.filename.as_str(); if name.is_empty() || name.contains(['\0', '/', '\\']) { return Err(invalid( "An attachment needs a file name without path separators", )); } + if let Some(previous) = old.paragraphs.get(paragraph_id) { + let ParagraphContent::Attachment(stored) = &previous.content else { + return Err(invalid("Paragraph content type cannot change")); + }; + if stored != attachment { + self.edit_attachment(stored, attachment)?; + } + continue; + } + let Some(bytes) = &attachment.bytes else { + return Err(invalid("A new attachment needs its payload")); + }; let extension = name .rfind('.') .filter(|dot| *dot > 0) @@ -1314,6 +1319,59 @@ impl Lowering<'_> { /// Writes a moved, resized or described picture the way OneNote stores one: the /// position, the layout width and height with the user flag and the description on /// the picture object, leaving the intrinsic size alone. + /// A stored attachment keeps its payload and preview; its shown name, recorded source + /// path and icon size change in place, as OneNote's rename does. + fn edit_attachment( + &mut self, + stored: &Attachment, + attachment: &Attachment, + ) -> Result<(), Error> { + let mut values: Values = vec![( + 0x14001d7a, + crate::create::current_timestamps()? + .0 + .to_le_bytes() + .to_vec(), + )]; + let mut removed = Vec::new(); + if attachment.filename != stored.filename { + let name = crate::create::string(&attachment.filename); + values.push((0x1c001c22, name.clone())); + values.push((0x1c001d9c, name)); + } + if attachment.source_path != stored.source_path { + match &attachment.source_path { + Some(path) => values.push((0x1c001d9d, crate::create::string(path))), + None => removed.push(0x1c001d9d), + } + } + if attachment.size != stored.size { + match attachment.size { + Some([width, height]) => { + if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) { + return Err(invalid("Attachment icon size must be positive")); + } + values.push((0x140034cd, (width / 36.0).to_le_bytes().to_vec())); + values.push((0x140034ce, (height / 36.0).to_le_bytes().to_vec())); + } + None => removed.extend([0x140034cd, 0x140034ce]), + } + } + let (space, object) = (self.space, self.id(attachment.id)); + self.apply(|current| { + crate::write::write_revision(current, space, |raw| { + let mut node = PropertyObject::from_object(&raw.objects[&object])?; + node.remove(&removed)?; + let values: Vec<(u32, &[u8])> = values + .iter() + .map(|(id, bytes)| (*id, bytes.as_slice())) + .collect(); + node.set(&values)?; + Ok(BTreeMap::from([(object, node)])) + }) + }) + } + fn edit_image(&mut self, stored: &Image, image: &Image) -> Result<(), Error> { picture_fixed_fields(stored, image)?; let mut values: Values = vec![( diff --git a/crates/onestore/src/store.rs b/crates/onestore/src/store.rs index bcbdefbe89dfaf4fbabeb3a570005ff68764c62f..18c57e2aad6216fe68e30f290daf8353717d60f4 100644 --- a/crates/onestore/src/store.rs +++ b/crates/onestore/src/store.rs @@ -71,6 +71,10 @@ impl Chunk { pub struct Header { pub file_type: FileType, pub file_id: [u8; 16], + /// The parent table of contents' file identity; zero outside a notebook. + pub ancestor: [u8; 16], + /// CRC of the file name (a section's file name, a group's folder name). + pub name_crc: u32, pub transaction_count: u32, pub expected_length: u64, pub version_id: [u8; 16], @@ -166,7 +170,9 @@ impl Header { message: "Missing committed transaction", }); } - c.take(48)?; + c.take(28)?; + let ancestor = c.read()?; + let name_crc = u32::from_le_bytes(c.read()?); let hashed_chunks = c.chunk()?; let transaction_log = c.chunk()?; let root = c.chunk()?; @@ -179,6 +185,8 @@ impl Header { Ok(Self { file_type, file_id, + ancestor, + name_crc, transaction_count, expected_length, version_id, diff --git a/crates/onestore/src/toc.rs b/crates/onestore/src/toc.rs new file mode 100644 index 0000000000000000000000000000000000000000..68db7ee3ff4f7dfdcb6d6e08fc7346a4dbe1bdb7 --- /dev/null +++ b/crates/onestore/src/toc.rs @@ -0,0 +1,226 @@ +//! Edits to a notebook's table of contents: the documented `jcidPersistablePropertyContainerForTOC` +//! root lists section and section-group entries, each carrying a file identity, an ordering +//! number, a filename and (for sections) a colour. + +use crate::{ + Error, ExGuid, PropertySets, Store, Value, + document::{Document, Kind}, + revisions::RevisionIndex, + write::{PropertyObject, fresh_guid, write_revision}, +}; +use std::collections::BTreeMap; + +fn invalid(message: &'static str) -> Error { + Error { offset: 0, message } +} + +/// One change to a table of contents, addressed by the entry's file identity. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TocEdit { + /// A section (`name.one`) or, with `group`, a section-group folder appended last. + Add { + filename: String, + identity: [u8; 16], + group: bool, + }, + Rename { + identity: [u8; 16], + filename: String, + }, + /// Section colour as COLORREF; `None` restores OneNote's "undefined" 0xffffffff. + Color { + identity: [u8; 16], + color: Option, + }, + /// Every entry, in the wanted order; entries left out keep their relative order after these. + Order(Vec<[u8; 16]>), + Remove { + identity: [u8; 16], + }, +} + +fn component(name: &str) -> bool { + !name.is_empty() && !name.contains(['/', '\\', '\0']) && name != "." && name != ".." +} + +/// The table of contents with `edits` applied in order. +type PropertyChange = (u32, Option>); + +pub(crate) fn edit_table_of_contents(source: &[u8], edits: &[TocEdit]) -> Result, Error> { + let store = Store::parse(source)?; + if store.header.file_type != crate::FileType::TableOfContents { + return Err(invalid("Choose a table-of-contents file")); + } + let index = RevisionIndex::parse(&store)?; + let document = Document::parse(&index)?; + let space = document.root; + let revision = document.active(space)?; + let root = *revision + .roots + .get(&1) + .ok_or_else(|| invalid("Missing notebook TOC root"))?; + let Some(Kind::Toc { entries, .. }) = revision.nodes.get(&root).map(|node| &node.kind) else { + return Err(invalid("Missing notebook TOC root")); + }; + // Entries in stored order with their identities and filenames. + let mut listed: Vec<(ExGuid, [u8; 16], String)> = Vec::new(); + for id in entries { + let Some(Kind::Toc { + filename, + identity: Some(identity), + .. + }) = revision.nodes.get(id).map(|node| &node.kind) + else { + return Err(invalid("Incomplete notebook TOC reference")); + }; + listed.push((*id, *identity, filename.clone().unwrap_or_default())); + } + let mut created: BTreeMap = BTreeMap::new(); + // Property values to set (or remove, `None`) on each existing entry. + let mut changes: BTreeMap> = BTreeMap::new(); + for edit in edits { + let position = |identity: &[u8; 16]| { + listed + .iter() + .position(|(_, known, _)| known == identity) + .ok_or_else(|| invalid("The table of contents has no entry with that identity")) + }; + match edit { + TocEdit::Add { + filename, + identity, + group, + } => { + if !component(filename) + || (!group && !filename.to_ascii_lowercase().ends_with(".one")) + || *identity == [0; 16] + { + return Err(invalid( + "A TOC entry needs a section filename or group folder name and an identity", + )); + } + if listed.iter().any(|(_, known, name)| { + known == identity || name.eq_ignore_ascii_case(filename) + }) { + return Err(invalid( + "The table of contents already lists that section or group", + )); + } + let id = ExGuid { + guid: fresh_guid()?, + n: 10, + }; + let mut values: Vec<(u32, Vec)> = vec![ + (0x1c001d94, identity.to_vec()), + (0x14001cb9, 0u32.to_le_bytes().to_vec()), + (0x1c001d6b, crate::create::string(filename)), + ]; + if !group { + values.push((0x14001cbe, vec![0xff; 4])); + } + let mut object = PropertyObject { + jcid: 0x20001, + bytes: crate::create::properties(&values)?, + global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])), + }; + object.reference(id)?; + created.insert(id, object); + listed.push((id, *identity, filename.clone())); + } + TocEdit::Rename { identity, filename } => { + if !component(filename) { + return Err(invalid( + "A TOC entry needs a filename without path separators", + )); + } + let at = position(identity)?; + if listed + .iter() + .enumerate() + .any(|(i, (_, _, name))| i != at && name.eq_ignore_ascii_case(filename)) + { + return Err(invalid("The table of contents already lists that name")); + } + let id = listed[at].0; + listed[at].2 = filename.clone(); + changes + .entry(id) + .or_default() + .push((0x1c001d6b, Some(crate::create::string(filename)))); + } + TocEdit::Color { identity, color } => { + let id = listed[position(identity)?].0; + changes.entry(id).or_default().push(( + 0x14001cbe, + Some(color.unwrap_or(0xffff_ffff).to_le_bytes().to_vec()), + )); + } + TocEdit::Order(wanted) => { + let mut ordered: Vec<(ExGuid, [u8; 16], String)> = Vec::new(); + for identity in wanted { + let at = position(identity)?; + if ordered.iter().any(|(_, known, _)| known == identity) { + return Err(invalid("An entry is ordered twice")); + } + ordered.push(listed[at].clone()); + } + let rest: Vec<_> = listed + .iter() + .filter(|(_, known, _)| !wanted.contains(known)) + .cloned() + .collect(); + ordered.extend(rest); + listed = ordered; + } + TocEdit::Remove { identity } => { + let at = position(identity)?; + let (id, _, _) = listed.remove(at); + changes.remove(&id); + created.remove(&id); + } + } + } + let listed = listed; + write_revision(source, space, |raw| { + let mut changed = BTreeMap::new(); + let mut root_object = PropertyObject::from_object(&raw.objects[&root])?; + let mut references = Vec::new(); + for (order, (id, _, _)) in listed.iter().enumerate() { + let mut object = match created.remove(id) { + Some(object) => object, + None => PropertyObject::from_object(&raw.objects[id])?, + }; + let stored_order = PropertySets::parse(&object.bytes)?.sets[0] + .iter() + .find(|p| p.id == 0x14001cb9) + .and_then(|p| match p.value { + Value::Bytes(b) => b.try_into().ok().map(u32::from_le_bytes), + _ => None, + }); + let mut updates: Vec<(u32, Vec)> = Vec::new(); + if stored_order != Some(order as u32 + 1) { + updates.push((0x14001cb9, (order as u32 + 1).to_le_bytes().to_vec())); + } + for (property, value) in changes.get(id).into_iter().flatten() { + match value { + Some(bytes) => updates.push((*property, bytes.clone())), + None => object.remove(&[*property])?, + } + } + let updates: Vec<(u32, &[u8])> = updates + .iter() + .map(|(id, bytes)| (*id, bytes.as_slice())) + .collect(); + if !updates.is_empty() { + object.set(&updates)?; + } + references.extend(root_object.reference(*id)?); + if !updates.is_empty() || created.contains_key(id) || !raw.objects.contains_key(id) { + changed.insert(*id, object); + } + } + root_object.set(&[(0x24001cf6, &references)])?; + changed.insert(root, root_object); + Ok(changed) + }) +} diff --git a/crates/onestore/src/write.rs b/crates/onestore/src/write.rs index a63774c575c95c9d6995134c4c40ffea970897cd..e9266a1735575b317ae1e04a743f6a30bc30add8 100644 --- a/crates/onestore/src/write.rs +++ b/crates/onestore/src/write.rs @@ -838,6 +838,7 @@ pub(crate) fn write_revisions_with_payloads( ) } }; + let existing: BTreeSet = revision.objects.keys().copied().collect(); let reachable = if rid.is_some() { revision.reachable()? } else { @@ -859,10 +860,10 @@ pub(crate) fn write_revisions_with_payloads( message: "An existing object's type cannot be changed", }); } - } else if !is_section { + } else if !is_section && replacement.jcid != 0x20001 { return Err(Error { offset: 0, - message: "New objects require a section file", + message: "New objects in a table of contents are its entries", }); } if replacement.global_ids.keys().any(|i| *i > 0xffffff) { @@ -1006,10 +1007,16 @@ pub(crate) fn write_revisions_with_payloads( .iter() .filter(|(id, _)| checkpoint || changed.contains(id)) .collect(); - let toc_table = if checkpoint && !is_section { - if selected.iter().any(|(_, object)| { - object.jcid != 0x20001 || !matches!(object.data, ObjectData::Properties(_)) - }) { + // A table-of-contents manifest has one global id table (sections group objects, + // each group with its own table); OneNote resolves every node against it. + let toc_table = if is_section { + None + } else { + if checkpoint + && selected.iter().any(|(_, object)| { + object.jcid != 0x20001 || !matches!(object.data, ObjectData::Properties(_)) + }) + { return Err(Error { offset: 0, message: "TOC checkpoint requires table-of-contents property objects", @@ -1032,8 +1039,6 @@ pub(crate) fn write_revisions_with_payloads( .map(|(i, guid)| (u32::try_from(i).unwrap(), guid)) .collect::>(), ) - } else { - None }; let mut groups = BTreeMap::<_, Vec<_>>::new(); for (id, object) in selected { @@ -1144,10 +1149,13 @@ pub(crate) fn write_revisions_with_payloads( length: u64::try_from(bytes.len()).unwrap(), } }; + // A table-of-contents object is declared when the revision is a + // checkpoint or the object is new, and revised otherwise. + let declared = checkpoint || !existing.contains(&id); if is_section { declaration.extend_from_slice(&object.jcid.to_le_bytes()); declaration.push(flags); - } else if checkpoint { + } else if declared { let body = 1_u64 | (u64::from(flags & 1) << 16); declaration.extend_from_slice(&body.to_le_bytes()[..6]); } else { @@ -1161,7 +1169,7 @@ pub(crate) fn write_revisions_with_payloads( group.push(node( if is_section { if readonly { 0xc5 } else { 0xa5 } - } else if checkpoint { + } else if declared { 0x2e } else { 0x42 diff --git a/crates/onestore/tests/page_attachments.rs b/crates/onestore/tests/page_attachments.rs index 5feeaa00824a57094e105c775e36467ed1be38dd..89e0d1f3cfdb1bb6394075688ce6e9233df656e4 100644 --- a/crates/onestore/tests/page_attachments.rs +++ b/crates/onestore/tests/page_attachments.rs @@ -169,7 +169,7 @@ fn an_attachment_inserted_on_a_fresh_page_reads_back_and_can_be_removed() { } #[test] -fn attachments_need_a_file_name_and_stored_ones_stay_fixed() { +fn attachments_need_a_file_name_and_stored_ones_are_renamed_in_place() { let source = onestore::create_section("files.one", "Text", "Author").unwrap(); let (space, before) = first_page(&source); let mut after = before.clone(); @@ -192,12 +192,38 @@ fn attachments_need_a_file_name_and_stored_ones_stay_fixed() { if let PageObject::Outline(outline) = object { for paragraph in &mut outline.paragraphs { if let ParagraphContent::Attachment(attachment) = &mut paragraph.content { - attachment.filename = "renamed.txt".into(); + attachment.filename = "renamed \u{1f980}.txt".into(); + attachment.source_path = Some("C:\\inputs\\renamed \u{1f980}.txt".into()); found += 1; } } } } assert_eq!(found, 1); - assert!(PreparedEdit::page(NATIVE, space, &renamed, AUTHOR).is_err()); + let written = PreparedEdit::page(NATIVE, space, &renamed, AUTHOR).unwrap(); + let stored = page_in(written.as_bytes(), space); + let (before, after) = (attachments(&native)[0], attachments(&stored)[0]); + assert_eq!(after.filename, "renamed \u{1f980}.txt"); + assert_eq!( + after.source_path.as_deref(), + Some("C:\\inputs\\renamed \u{1f980}.txt") + ); + assert_eq!((after.id, after.size), (before.id, before.size)); + assert_eq!(after.bytes, before.bytes); + assert_eq!(after.preview, before.preview); + if let Some(directory) = std::env::var_os("ONESTORE_ATTACHMENT_RENAME_EXPORT") { + let directory = std::path::PathBuf::from(directory); + std::fs::create_dir_all(&directory).unwrap(); + std::fs::write(directory.join("synthetic.one"), written.as_bytes()).unwrap(); + let file_id = Store::parse(written.as_bytes()).unwrap().header.file_id; + std::fs::write( + directory.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents( + "Open Notebook.onetoc2", + &[("synthetic.one", file_id)], + ) + .unwrap(), + ) + .unwrap(); + } } diff --git a/crates/onestore/tests/page_links.rs b/crates/onestore/tests/page_links.rs index a0c3be26bfd9c6f0e66005ea13fd66c8d50358af..e315f23c148917354be9a7414cda46504b95938b 100644 --- a/crates/onestore/tests/page_links.rs +++ b/crates/onestore/tests/page_links.rs @@ -3,7 +3,7 @@ use onestore::{ document::Document, page::{ Page, PageObject, PageParagraph, Paragraph, - link::{LinkTarget, internal_link}, + link::{InternalLink, LinkTarget, internal_link, parse_internal_link}, text::Edit, }, }; @@ -258,6 +258,32 @@ fn internal_links_match_what_onenote_stores() { ), "onenote:#A%20b%2Fc§ion-id={00000000-0000-0000-0000-000000000000}&page-id={00000000-0000-0000-0000-000000000000}&end&base-path=p" ); + let identity = target.identity.unwrap(); + assert_eq!( + stored[1..] + .iter() + .map(|url| parse_internal_link(url)) + .collect::>(), + [ + Some(InternalLink { + section, + page: Some(identity), + object: None + }), + Some(InternalLink { + section, + page: Some(identity), + object: Some(paragraph) + }), + Some(InternalLink { + section, + page: None, + object: None + }), + ] + ); + assert_eq!(parse_internal_link(&stored[0]), None); + assert_eq!(parse_internal_link("onenote:#x&page-id={0}"), None); } /// `ONESTORE_INTERNAL_LINK_EXPORT` names a new directory receiving the candidate for a cold diff --git a/crates/onestore/tests/toc.rs b/crates/onestore/tests/toc.rs new file mode 100644 index 0000000000000000000000000000000000000000..7a71d89380e88a3faf41b338025b848435ece205 --- /dev/null +++ b/crates/onestore/tests/toc.rs @@ -0,0 +1,108 @@ +use onestore::{PreparedEdit, RevisionIndex, Store, TocEdit, document::Document}; + +fn entries(bytes: &[u8]) -> Vec<(String, [u8; 16], u32, Option)> { + let store = Store::parse(bytes).unwrap(); + assert!(store.checksum_mismatches.is_empty()); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let revision = document.active(document.root).unwrap(); + let root = &revision.nodes[&revision.roots[&1]]; + let onestore::document::Kind::Toc { entries, .. } = &root.kind else { + panic!() + }; + entries + .iter() + .map(|id| match &revision.nodes[id].kind { + onestore::document::Kind::Toc { + filename, + identity, + order, + color, + .. + } => ( + filename.clone().unwrap(), + identity.unwrap(), + order.unwrap(), + *color, + ), + _ => panic!(), + }) + .collect() +} + +#[test] +fn sections_and_groups_are_added_renamed_coloured_ordered_and_removed() { + let a = [1; 16]; + let b = [2; 16]; + let toc = onestore::create_table_of_contents( + "Open Notebook.onetoc2", + &[("Alpha.one", a), ("Beta.one", b)], + ) + .unwrap(); + let c = [3; 16]; + let group = [4; 16]; + let written = PreparedEdit::table_of_contents( + &toc, + &[ + TocEdit::Add { + filename: "Gamma.one".into(), + identity: c, + group: false, + }, + TocEdit::Add { + filename: "Archive".into(), + identity: group, + group: true, + }, + ], + ) + .unwrap(); + assert_eq!( + entries(written.as_bytes()), + [ + ("Alpha.one".to_owned(), a, 1, Some(0xffff_ffff)), + ("Beta.one".to_owned(), b, 2, Some(0xffff_ffff)), + ("Gamma.one".to_owned(), c, 3, Some(0xffff_ffff)), + ("Archive".to_owned(), group, 4, None), + ] + ); + let again = PreparedEdit::table_of_contents( + written.as_bytes(), + &[ + TocEdit::Rename { + identity: b, + filename: "Renamed.one".into(), + }, + TocEdit::Color { + identity: c, + color: Some(0x00d7ff), + }, + TocEdit::Order(vec![group, c]), + TocEdit::Remove { identity: a }, + ], + ) + .unwrap(); + assert_eq!( + entries(again.as_bytes()), + [ + ("Archive".to_owned(), group, 1, None), + ("Gamma.one".to_owned(), c, 2, Some(0x00d7ff)), + ("Renamed.one".to_owned(), b, 3, Some(0xffff_ffff)), + ] + ); + assert!( + PreparedEdit::table_of_contents( + again.as_bytes(), + &[TocEdit::Add { + filename: "renamed.one".into(), + identity: [9; 16], + group: false + }] + ) + .is_err() + ); + assert!( + PreparedEdit::table_of_contents(again.as_bytes(), &[TocEdit::Remove { identity: a }]) + .is_err() + ); +} diff --git a/tools/native/notebook-reorder.ps1 b/tools/native/notebook-reorder.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..b672af93e4abe3eeeaa403c5ff4ed48c775238ac --- /dev/null +++ b/tools/native/notebook-reorder.ps1 @@ -0,0 +1,50 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +function Read-Tree { + $deadline = [DateTime]::UtcNow.AddSeconds(60) + do { + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + if (@($tree.SelectNodes('//*[local-name()="Section"]')).Count -ge 4) { return $hierarchy } + Start-Sleep -Milliseconds 500 + } while ([DateTime]::UtcNow -lt $deadline) + throw 'The notebook did not finish loading.' +} +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $hierarchy = Read-Tree + [IO.File]::WriteAllText((Join-Path $Root 'reorder-00-opened.xml'), $hierarchy, [Text.Encoding]::UTF8) + [xml]$tree = $hierarchy + $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable) + $manager.AddNamespace('one', $namespace) + $notebookNode = $tree.DocumentElement + $sections = @($notebookNode.SelectNodes('one:Section', $manager)) + if ($sections.Count -lt 3) { throw "Expected three root sections, found $($sections.Count)." } + $last = $sections[$sections.Count - 1] + [void]$notebookNode.RemoveChild($last) + [void]$notebookNode.InsertBefore($last, $sections[0]) + [IO.File]::WriteAllText((Join-Path $Root 'reorder-01-update.xml'), $tree.OuterXml, [Text.Encoding]::UTF8) + $app.UpdateHierarchy($tree.OuterXml, 1) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'reorder-02-reordered.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' + Copy-Item -Recurse (Join-Path $Root 'notebook') (Join-Path $Root 'notebook-reordered') + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $hierarchy = Read-Tree + [IO.File]::WriteAllText((Join-Path $Root 'reorder-03-reopened.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/tools/native/notebook-structure.ps1 b/tools/native/notebook-structure.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..caa6737aea0e40f0695880dee51d166868e8ce3e --- /dev/null +++ b/tools/native/notebook-structure.ps1 @@ -0,0 +1,71 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$log = @() +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'structure-00-opened.xml'), $hierarchy, [Text.Encoding]::UTF8) + $second = '' + $app.OpenHierarchy('Second.one', $notebookId, [ref]$second, 3) + $third = '' + $app.OpenHierarchy('Third.one', $notebookId, [ref]$third, 3) + $group = '' + $app.OpenHierarchy('Group', $notebookId, [ref]$group, 2) + $inner = '' + $app.OpenHierarchy('Inner.one', $group, [ref]$inner, 3) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'structure-01-created.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable) + $manager.AddNamespace('one', $namespace) + $sections = @($tree.SelectNodes('//one:Section', $manager)) + foreach ($section in $sections) { + switch ($section.GetAttribute('name')) { + 'Second' { $section.SetAttribute('name', 'Renamed'); $section.SetAttribute('color', '#FFD75E') } + 'Third' { $section.SetAttribute('color', '#B7C997') } + } + } + [IO.File]::WriteAllText((Join-Path $Root 'structure-02-update.xml'), $tree.OuterXml, [Text.Encoding]::UTF8) + $app.UpdateHierarchy($tree.OuterXml, 1) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'structure-03-renamed.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' + Copy-Item -Recurse (Join-Path $Root 'notebook') (Join-Path $Root 'notebook-renamed') + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(60) + do { + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + if (@($tree.SelectNodes('//*[local-name()="Section"]')).Count -ge 4) { break } + Start-Sleep -Milliseconds 500 + } while ([DateTime]::UtcNow -lt $deadline) + $manager = New-Object Xml.XmlNamespaceManager($tree.NameTable) + $manager.AddNamespace('one', $namespace) + [IO.File]::WriteAllText((Join-Path $Root 'structure-03b-reopened.xml'), $hierarchy, [Text.Encoding]::UTF8) + $doomed = @($tree.SelectNodes('//*[local-name()="Section"]') | Where-Object { $_.GetAttribute('name') -eq 'Third' }) + if ($doomed.Count -ne 1) { throw "Expected one section named Third, found $($doomed.Count)." } + $app.DeleteHierarchy($doomed[0].GetAttribute('ID'), [DateTime]::MinValue, $false) + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $Root 'structure-04-deleted.xml'), $hierarchy, [Text.Encoding]::UTF8) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/tools/test_attachment_edit.py b/tools/test_attachment_edit.py index e257c7611eb8941b6a056d4a47ae6e5576e4cec1..7544f904d9a732ec56178326991abe3b077c1ad8 100644 --- a/tools/test_attachment_edit.py +++ b/tools/test_attachment_edit.py @@ -31,6 +31,15 @@ class AttachmentEditTest(unittest.TestCase): def test_a_rust_inserted_attachment_with_an_icon_preview_renders_natively(self): self.check('icon') + def test_a_stored_attachment_renamed_by_rust_shows_its_new_name_and_source(self): + with TemporaryDirectory() as temporary: + read = Path(temporary) / 'read' + shutil.copytree(FIXTURE / 'renamed/cold/read', read) + compare(FIXTURE / 'renamed/candidate', read) + page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / 'renamed/cold/read').glob('page-*.xml'))) + inserted, = page.iter('{%s}InsertedFile' % ns['one']) + self.assertEqual((inserted.get('preferredName'), inserted.get('pathSource')), ('renamed 🦀.txt', 'C:\\inputs\\renamed 🦀.txt')) + if __name__ == '__main__': unittest.main() diff --git a/tools/test_notebook_edit.py b/tools/test_notebook_edit.py new file mode 100644 index 0000000000000000000000000000000000000000..911f96ce81aacf3c6098115519d981392fae640b --- /dev/null +++ b/tools/test_notebook_edit.py @@ -0,0 +1,65 @@ +from pathlib import Path +import runpy +import shutil +from tempfile import TemporaryDirectory +import unittest +import uuid +import xml.etree.ElementTree as ET + +from native_xml import ns + +ROOT = Path(__file__).resolve().parent.parent +FIXTURE = ROOT / 'corpus/notebook-edit' +compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] + + +def file_identity(path): + return uuid.UUID(bytes_le=path.read_bytes()[16:32]) + + +def entries(node): + return [(child.tag.rsplit('}', 1)[-1], child.get('name'), child.get('color'), entries(child)) + for child in node if child.tag.rsplit('}', 1)[-1] in ('Section', 'SectionGroup')] + + +class NotebookEditTest(unittest.TestCase): + def check(self, row, expected): + with TemporaryDirectory() as temporary: + read = Path(temporary) / 'read' + shutil.copytree(FIXTURE / row / 'cold/read', read) + compare(FIXTURE / row / 'candidate', read) + hierarchy = ET.parse(FIXTURE / row / 'cold/read/hierarchy.xml').getroot() + self.assertEqual(entries(hierarchy), expected) + for candidate in sorted((FIXTURE / row / 'candidate').rglob('*.one*')): + relative = candidate.relative_to(FIXTURE / row / 'candidate') + self.assertEqual(file_identity(FIXTURE / row / 'cold/notebook' / relative), file_identity(candidate), str(relative)) + + def test_rust_sections_and_groups_open_natively_in_the_written_order(self): + self.check('structured', [ + ('Section', 'Renamed', '#FFD75E', []), + ('Section', 'First', '#8AA8E4', []), + ('SectionGroup', 'Kept', None, [('Section', 'Inner', '#8AA8E4', [])]), + ]) + + def test_a_rust_deleted_section_sits_in_the_native_recycle_bin(self): + self.check('deleted', [ + ('Section', 'Renamed', '#FFD75E', []), + ('SectionGroup', 'Kept', None, [('Section', 'First', '#8AA8E4', [])]), + ('SectionGroup', 'OneNote_RecycleBin', None, [('Section', 'First', '#8AA8E4', [])]), + ]) + hierarchy = ET.parse(FIXTURE / 'deleted/cold/read/hierarchy.xml').getroot() + bin = hierarchy.find("one:SectionGroup[@name='OneNote_RecycleBin']", ns) + self.assertEqual(bin.get('isRecycleBin'), 'true') + self.assertEqual(bin.find('one:Section', ns).get('isInRecycleBin'), 'true') + + def test_onenote_re_identifies_files_whose_header_disagrees_with_their_place(self): + # The native session lists a Rust section created with a zero ancestor twice: once + # under the identity Rust wrote and once under the identity OneNote assigned. + toc = (FIXTURE / 'native-structure/notebook/Open Notebook.onetoc2').read_bytes() + links = FIXTURE / 'native-structure/notebook/links.one' + self.assertEqual(uuid.UUID(bytes_le=links.read_bytes()[128:144]), file_identity(FIXTURE / 'native-structure/notebook/Open Notebook.onetoc2')) + self.assertIn(file_identity(links).bytes_le, toc) + + +if __name__ == '__main__': + unittest.main() -- 2.54.0