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
Binary files /dev/null and b/corpus/attachment-edit/renamed/candidate/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/attachment-edit/renamed/candidate/synthetic.one differ
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
Binary files /dev/null and b/corpus/attachment-edit/renamed/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/attachment-edit/renamed/cold/notebook/synthetic.one b/corpus/attachment-edit/renamed/cold/notebook/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..f36451aa996243bb0cba272c4e45a7be8187c24c
Binary files /dev/null and b/corpus/attachment-edit/renamed/cold/notebook/synthetic.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/candidate/Kept/First.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/candidate/Kept/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/candidate/OneNote_RecycleBin/First.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/candidate/OneNote_RecycleBin/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/candidate/Open Notebook.onetoc2 differ
diff --git a/corpus/notebook-edit/deleted/candidate/Renamed.one b/corpus/notebook-edit/deleted/candidate/Renamed.one
new file mode 100644
index 0000000000000000000000000000000000000000..31fba4276c85f0e413b6cb036992a8102c31d014
Binary files /dev/null and b/corpus/notebook-edit/deleted/candidate/Renamed.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/cold/notebook/Kept/First.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/cold/notebook/Kept/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/cold/notebook/OneNote_RecycleBin/First.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/cold/notebook/OneNote_RecycleBin/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/cold/notebook/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/deleted/cold/notebook/Renamed.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook-reordered/Group/Inner.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook-reordered/Group/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook-reordered/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook-reordered/Second.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook-reordered/Third.one differ
diff --git a/corpus/notebook-edit/native-reorder/notebook-reordered/links.one b/corpus/notebook-edit/native-reorder/notebook-reordered/links.one
new file mode 100644
index 0000000000000000000000000000000000000000..c53bf910edd84ce5721c52e18ea5022d415c730c
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook-reordered/links.one differ
diff --git a/corpus/notebook-edit/native-reorder/notebook/Group/Inner.one b/corpus/notebook-edit/native-reorder/notebook/Group/Inner.one
new file mode 100644
index 0000000000000000000000000000000000000000..ba9b977cddfbdba559da17d844cf9339f4d0983a
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook/Group/Inner.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook/Group/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook/Second.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook/Third.one differ
diff --git a/corpus/notebook-edit/native-reorder/notebook/links.one b/corpus/notebook-edit/native-reorder/notebook/links.one
new file mode 100644
index 0000000000000000000000000000000000000000..c53bf910edd84ce5721c52e18ea5022d415c730c
Binary files /dev/null and b/corpus/notebook-edit/native-reorder/notebook/links.one differ
diff --git a/corpus/notebook-edit/native-reorder/read/environment.json b/corpus/notebook-edit/native-reorder/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..c45b314aec57ef1a2274499d9d6f4b9395296b1b
--- /dev/null
+++ b/corpus/notebook-edit/native-reorder/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-42A0DA0E",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/notebook-edit/native-reorder/read/hierarchy.xml b/corpus/notebook-edit/native-reorder/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7600b2ee2751be04ef7af8bf4080fc28ebbf11f9
--- /dev/null
+++ b/corpus/notebook-edit/native-reorder/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook-renamed/Group/Inner.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook-renamed/Group/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook-renamed/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook-renamed/Second.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook-renamed/Third.one differ
diff --git a/corpus/notebook-edit/native-structure/notebook-renamed/links.one b/corpus/notebook-edit/native-structure/notebook-renamed/links.one
new file mode 100644
index 0000000000000000000000000000000000000000..c53bf910edd84ce5721c52e18ea5022d415c730c
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook-renamed/links.one differ
diff --git a/corpus/notebook-edit/native-structure/notebook/Group/Inner.one b/corpus/notebook-edit/native-structure/notebook/Group/Inner.one
new file mode 100644
index 0000000000000000000000000000000000000000..ba9b977cddfbdba559da17d844cf9339f4d0983a
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook/Group/Inner.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook/Group/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/OneNote_DeletedPages.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/Open Notebook.onetoc2 differ
diff --git a/corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/Third.one b/corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/Third.one
new file mode 100644
index 0000000000000000000000000000000000000000..b1b6224c9ebacb66370578d83d9d275e0616fbb4
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook/OneNote_RecycleBin/Third.one differ
diff --git a/corpus/notebook-edit/native-structure/notebook/Open Notebook.onetoc2 b/corpus/notebook-edit/native-structure/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..88430cfcbf5f58d02bffc29907e5e23c55cc44ad
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook/Second.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/native-structure/notebook/links.one differ
diff --git a/corpus/notebook-edit/native-structure/read/environment.json b/corpus/notebook-edit/native-structure/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..3a6eeb9e7081153938f3dde5e315a1d4722edc94
--- /dev/null
+++ b/corpus/notebook-edit/native-structure/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-266B13AF",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/notebook-edit/native-structure/read/hierarchy.xml b/corpus/notebook-edit/native-structure/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e57b3c07b38e4cf2fef1c55fb358abba994eee5c
--- /dev/null
+++ b/corpus/notebook-edit/native-structure/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
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
Binary files /dev/null and b/corpus/notebook-edit/structured/candidate/First.one differ
diff --git a/corpus/notebook-edit/structured/candidate/Kept/Inner.one b/corpus/notebook-edit/structured/candidate/Kept/Inner.one
new file mode 100644
index 0000000000000000000000000000000000000000..52c134aeb17662b915978ff668e8fa5aed1a79b9
Binary files /dev/null and b/corpus/notebook-edit/structured/candidate/Kept/Inner.one differ
diff --git a/corpus/notebook-edit/structured/candidate/Kept/Open Notebook.onetoc2 b/corpus/notebook-edit/structured/candidate/Kept/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..5991fddfa49d5235a429f41fee6aefa7ae655195
Binary files /dev/null and b/corpus/notebook-edit/structured/candidate/Kept/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/structured/candidate/Open Notebook.onetoc2 differ
diff --git a/corpus/notebook-edit/structured/candidate/Renamed.one b/corpus/notebook-edit/structured/candidate/Renamed.one
new file mode 100644
index 0000000000000000000000000000000000000000..31fba4276c85f0e413b6cb036992a8102c31d014
Binary files /dev/null and b/corpus/notebook-edit/structured/candidate/Renamed.one differ
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
Binary files /dev/null and b/corpus/notebook-edit/structured/cold/notebook/First.one differ
diff --git a/corpus/notebook-edit/structured/cold/notebook/Kept/Inner.one b/corpus/notebook-edit/structured/cold/notebook/Kept/Inner.one
new file mode 100644
index 0000000000000000000000000000000000000000..52c134aeb17662b915978ff668e8fa5aed1a79b9
Binary files /dev/null and b/corpus/notebook-edit/structured/cold/notebook/Kept/Inner.one differ
diff --git a/corpus/notebook-edit/structured/cold/notebook/Kept/Open Notebook.onetoc2 b/corpus/notebook-edit/structured/cold/notebook/Kept/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..5991fddfa49d5235a429f41fee6aefa7ae655195
Binary files /dev/null and b/corpus/notebook-edit/structured/cold/notebook/Kept/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/notebook-edit/structured/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/notebook-edit/structured/cold/notebook/Renamed.one b/corpus/notebook-edit/structured/cold/notebook/Renamed.one
new file mode 100644
index 0000000000000000000000000000000000000000..31fba4276c85f0e413b6cb036992a8102c31d014
Binary files /dev/null and b/corpus/notebook-edit/structured/cold/notebook/Renamed.one differ
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