diff --git a/.gitignore b/.gitignore
index b10b426374d528986f027a4f06c106c8df415b02..a394d270c02acc5b4765500f48d5bac581bdab39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,6 @@
/corpus/private/
/corpus/recovered/
/evidence/
-/.DS_Store
__pycache__/
/fuzz/target/
/fuzz/corpus/
diff --git a/corpus/page-lifecycle/removal/README.md b/corpus/page-lifecycle/removal/README.md
index 2d227802946bcfc09ffb98f479ff08fe9fd28edf..05edb05202ecc5222108986c5e689f8eda4eef92 100644
--- a/corpus/page-lifecycle/removal/README.md
+++ b/corpus/page-lifecycle/removal/README.md
@@ -50,3 +50,15 @@ failure. Decode the input hex into a new writable corpus and set
the platform path separator, then use `cargo +nightly fuzz run document CORPUS --
-seed=1993 -max_total_time=120 -timeout=30 -runs=20000`. The target also retains its
built-in synthetic sources. This is bounded reader testing of these inputs.
+
+`rust-followup` takes four Rust `delete_pages_permanently` outputs (`parent`, `all`,
+`features`, `leading-parent`) and lets OneNote 2010 create a titled page with a bold
+body in each section through COM (`tools/native/page-removal-followup.ps1`), then
+reopens the natively saved section through a fresh cache. The Rust test
+`native_pages_created_after_rust_removal_reopen_cold` requires every surviving page
+to keep its exact active revision through both native phases, the new page to carry
+the native title and body, and the cold reopen to change no object space.
+`tools/test_page_removal.py` compares both captures' native XML with the Rust model
+and finds the native title exactly once in each hierarchy. Regenerate the candidates
+with `ONESTORE_PAGE_REMOVAL_OUTPUT` on the removal reference test, then run the
+runner with `--author` into `followup` and cold into `followup-cold`.
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/candidate/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/all/candidate/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..c62907c99edfd451551a13ca4a9765a6863b4ecb
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/all/candidate/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/candidate/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/all/candidate/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..dd1d819eba785a38d9e40b7e5756d54aabd53423
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/all/candidate/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/commands.jsonl b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..272e436dc9993dbab4ce33e98a66201cf9087ad9
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-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-9D41F146 -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/page-lifecycle/removal/rust-followup/all/followup-cold/format-differences.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/format-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/format-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/geometry-differences.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/geometry-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/geometry-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/machine.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..6dd00423f2118997f76bdfed137f38a6dc89d92f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-9d41f146", "hostname": "ONE-M6-9D41F146"}
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..109ebb490bd6bfa8b8c8355a1d156ccb7af0c005
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..dd1d819eba785a38d9e40b7e5756d54aabd53423
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/pdf-format-checks.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/pdf-format-checks.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/pdf-format-checks.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/environment.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..78ee0f9728a367e6c834591068a71a285d72b4ff
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-9D41F146",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/hierarchy.xml b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..aba3be8c57f9508052f551d5e9dd9b2e1dc1b25f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/page-000.xml b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..136e228354ab47498675e05b15e035c6839cae71
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/page-000.xml
@@ -0,0 +1,3 @@
+
+Native body after Rust removal.]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/payloads.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/run.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..471b5efd98a69d8a966cc1c208ed2283f0f0bf1e
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/page-removal/run/all/followup/notebook",
+ "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/page-lifecycle/removal/rust-followup/all/followup-cold/scripts/cold.ps1 b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-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/page-lifecycle/removal/rust-followup/all/followup-cold/scripts/read.ps1 b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-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/page-lifecycle/removal/rust-followup/all/followup-cold/source.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..ff4ba59576491f27c378f85c1eb592fdbf70ff3f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Lifecycle.one",
+ "bytes": 71048,
+ "sha256": "36a33c1d7f8a13d93f6bcb0ea9431d5524a1f43aa8a8e121dc2c735e076cc029",
+ "mtime_ns": 1789112222791725210
+ },
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "0f15237414704e35cc85965841e1388fb91d2150e1eb2f41981256649e72cd40",
+ "mtime_ns": 1789112222791871294
+ }
+]
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/table-autofit.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/table-autofit.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/table-autofit.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/teardown.json b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup-cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/commands.jsonl b/corpus/page-lifecycle/removal/rust-followup/all/followup/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..b57045b0e11b4803fa86e12ba75d117d4eddbc2e
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/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-4C5FF100", "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-4C5FF100 -ExpectedPages 1 -UseCurrentCache", "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/page-lifecycle/removal/rust-followup/all/followup/format-differences.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/format-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/format-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/geometry-differences.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/geometry-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/geometry-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/machine.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..03ed100faa8e91a2d71f82f8d82209e0045873c2
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/machine.json
@@ -0,0 +1 @@
+{"name": "m6-4c5ff100", "hostname": "ONE-M6-4C5FF100"}
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/native-title.txt b/corpus/page-lifecycle/removal/rust-followup/all/followup/native-title.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1d6cb6d0262b9c45259962e524e8b806eb94dbac
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/native-title.txt
@@ -0,0 +1 @@
+Native after Rust removal 🦋 é
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/all/followup/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..109ebb490bd6bfa8b8c8355a1d156ccb7af0c005
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/all/followup/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/all/followup/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..dd1d819eba785a38d9e40b7e5756d54aabd53423
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/all/followup/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/pdf-format-checks.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/pdf-format-checks.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/pdf-format-checks.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/read/environment.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..a2b2b563f3da92fe04b0823e72b0c40257bede24
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-4C5FF100",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/read/hierarchy.xml b/corpus/page-lifecycle/removal/rust-followup/all/followup/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..24038339d3ff7079016d482c48f9eaa85755cf34
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/read/page-000.xml b/corpus/page-lifecycle/removal/rust-followup/all/followup/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e665510f38610111b74fa8aa21c92d5a18366c3d
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/read/page-000.xml
@@ -0,0 +1,3 @@
+
+Native body after Rust removal.]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/read/payloads.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/run.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..f789eb2ef4cee0c78af9a9e0eb2a69b68d3fb20f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/run.json
@@ -0,0 +1,19 @@
+{
+ "notebook": "/private/tmp/page-removal/run/all/candidate/notebook",
+ "expected_pages": 1,
+ "author": "tools/native/page-removal-followup.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": "6e6bd74f0460457118d436345350c38594c52bd9199fb4a1e31efec3b3e954fd",
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/scripts/author.ps1 b/corpus/page-lifecycle/removal/rust-followup/all/followup/scripts/author.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..1393a1ff21e7ae5c383cf8babf1f4f21d5f0c14b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/scripts/author.ps1
@@ -0,0 +1,27 @@
+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
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('Lifecycle.one', $notebook, [ref]$section, 0)
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 2)
+ $title = 'Native after Rust removal ' + [char]::ConvertFromUtf32(0x1F98B) + ' e' + [char]0x0301
+ $xml = 'Native body after Rust removal.]]>'
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+ $app.SyncHierarchy($notebook)
+ [IO.File]::WriteAllText("$Root\native-title.txt", $title, [Text.Encoding]::UTF8)
+} finally {
+ try { if ($notebook) { $app.CloseNotebook($notebook, $false) } }
+ finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 120 -ErrorAction SilentlyContinue
+ }
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/scripts/cold.ps1 b/corpus/page-lifecycle/removal/rust-followup/all/followup/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/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/page-lifecycle/removal/rust-followup/all/followup/scripts/read.ps1 b/corpus/page-lifecycle/removal/rust-followup/all/followup/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/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/page-lifecycle/removal/rust-followup/all/followup/source.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..0ed7dfc21854677822642582c2c64f8d014cc0b0
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Lifecycle.one",
+ "bytes": 64904,
+ "sha256": "645169c4f76dedd0deb0c928a08339479763ae607c21b7f604c21d823eee53eb",
+ "mtime_ns": 1789112028773633383
+ },
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "0f15237414704e35cc85965841e1388fb91d2150e1eb2f41981256649e72cd40",
+ "mtime_ns": 1789112028775905222
+ }
+]
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/table-autofit.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/table-autofit.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/table-autofit.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/all/followup/teardown.json b/corpus/page-lifecycle/removal/rust-followup/all/followup/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/all/followup/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/candidate/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/features/candidate/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..d6167b04713bf4ddbe1f66b0889f651b8ad9d633
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/features/candidate/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/candidate/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/features/candidate/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..18f015f65a9419ea2a74835decb84d7b76760183
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/features/candidate/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/commands.jsonl b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..6a92af073bea2e379f714ff7df6fa5b79e6348e5
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-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-53651AB4 -ExpectedPages 7", "exit": 0, "stdout": "Read 1 sections and 7 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/page-lifecycle/removal/rust-followup/features/followup-cold/format-differences.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/format-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/format-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/geometry-differences.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/geometry-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/geometry-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/machine.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..8e1e3e547bb11369fba508bd8d88ea5b40481e2b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-53651ab4", "hostname": "ONE-M6-53651AB4"}
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..2161fccbbe9e8d4960d8a2da837179db6c4b2ca6
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..18f015f65a9419ea2a74835decb84d7b76760183
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/pdf-format-checks.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/pdf-format-checks.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/pdf-format-checks.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/environment.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..b6daba10ca2c77f5587014c0ae5b59a421228499
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-53651AB4",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/hierarchy.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..673ca207f9911f1bb9008ce68c7556b900ab7651
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-000.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8b11ce85b2864cff5b5a794ce2172eac2d244105
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-000.xml
@@ -0,0 +1,21 @@
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a
+HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABAAGADASIA
+AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA
+AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3
+ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm
+p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA
+AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx
+BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK
+U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3
+uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDwhYfa
+plh9qsrD7VMsPtUqRFOqVVh9qmWH2qysPtUyw+1UpHbTqlZYfaplh9qsrD7VMsPtVqR206pVWGpl
+h9qsrDUyw+1UpHbTqlZYalWH2q0sNSrD7VSkdtOqVlh9qmWH2qysPtUyw+1UpHdTqnHrD7VMsPtV
+lYfaplh9q81SPyqnVKqw+1TLD7VZWH2qZYfaqUjtp1SssPtUyw+1WVh9qmWH2qlI7adUqrDUyw+1
+WVhqZYfarUjtp1SssNSrD7VaWGpVh9qpSO2nVKyw+1TLD7VZWH2qZYfaqUjup1Tj1h9qmWH2qysP
+tUyw+1eapH5VTqlVYfaplh9qsrD7VMsPtVKR206pWWH2qZYfarKw+1TLD7VSkdtOqVVhqZYfarKw
+1MsPtVqR206pWWGpVh9qtLDUqw+1UpHbTqlZYfaplh9qsrD7VMsPtVKR206px6w+1TLD7VZWH2qZ
+YfavNUj8rp1SqsPtUyw+1WVh9qmWH2qlI7adUrLD7VMsPtVlYfaplh9qpSO2nVKqw1MsPtVlYamW
+H2q1I7adUrLDUqw+1WlhqVYfaqUjtp1SssPtUyw+1WVh9qmWH2qlI7adU//Z
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-001.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fd0654815ec2be17f896932b46ad35321185ebd3
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-001.xml
@@ -0,0 +1,6 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec
+CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI
+E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp
+oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT
++wHXVRp4qDc0HQAAAABJRU5ErkJggg==
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-002.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..20ef6c4713ec013609fa2fefad6c2a04c0bb7b7e
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-002.xml
@@ -0,0 +1,3 @@
+
+Native body after Rust removal.]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-003.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e701da2ba007b444287e936f43fb9a1768622fc3
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-003.xml
@@ -0,0 +1,6 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec
+CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI
+E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp
+oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT
++wHXVRp4qDc0HQAAAABJRU5ErkJggg==
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-004.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..12e1b328e3a710166b1e7580cad6c44bddca1e4d
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-004.xml
@@ -0,0 +1,46 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAMAAADS6oI8AAADAFBMVEW4t5mrt5O4qpWrqo6euo2S
+uoeZs4ieqoiSqoK4nJCrnIq4kIyrkIaen4SSn36ZmH+ekH+SkHmGuoGGs395t3pounJos3CGqnx5
+qnVtqm9iqmqGnHd5nHGGkHN5kG1on2lomGdokGS4hIirhIK4d4Srd32eh3ySh3aZgHeed3eSd3G4
+a4Cra3m4YHyrYHaea3OSa22eYG+SYGmGh3CGgG55hGloh2FogF+Gd2t5d2Rtd15id1mGa2d5a2CG
+YGN5YF1ta1pia1VoYFRWumlWs2dJt2I4ulo4s1hWqmRJql09qlcyqlImulEaukshs0wKukMKs0Em
+qkwaqkYPqkAEqjtWnF9JnFlWkFtJkFU4n1E4mE84kEwmn0gan0IhmEMmkEMakD0KnzoKmDgKkDVW
+h1hWgFZJhFE4h0k4gEdWd1NJd0w9d0Yyd0FWa09Ja0hWYEtJYEU9a0Iyaz04YDwmh0AahzohgDsK
+hzIKgDAmdzsadzUPdy8EdyomazcaazEmYDMaYC0PaysEayYKYCW4VHirVHK4R3SrR22eV2ySV2aZ
+UGeeR2eSR2G4O3CrO2m4MGyrMGaeO2OSO12eMF+SMFmGV2CGUF55VFloV1FoUE+GR1t5R1RtR05i
+R0mGO1d5O1CGMFN5ME1tO0piO0VoMES4JGirJGK4GGSrGF6eJ1ySJ1aZIFeeGFeSGFG4DmGrDlq4
+A12rA1eeDlSSDk6eA1CSA0qGJE95JEmGGEt5GEVoJ0FoID9oGDyGDkh5DkGGA0R5Az5tDjtiDjZo
+AzVWVEdJVEE4Vzk4UDdWR0NJRzw9RzYyRzFWOz9JOzhWMDtJMDU9OzIyOy04MCwmVzAaVyohUCsK
+VyIKUCAmRysaRyUPRx8ERxomOycaOyEmMCMaMB0POxsEOxYKMBVWJDdJJDFWGDNJGC04Jyk4ICc4
+GCRWDjBJDilWAyxJAyY9DiMyDh44Ax0mJyAaJxohIBsmGBsaGBUKJxIKIBAKGA0mDhgaDhImAxQa
+Aw4PDgwEDgcKAwZYdbPqAAAAAWJLR0QAiAUdSAAAAAxjbVBQSkNtcDA3MTIAAAADSABzvAAABjJJ
+REFUWEedkImbTgUcha9ChZl2RUKR0TKTtGiRsZUSEpI0tKm0GoW0WSOjMmNpL82oFG1apJJltNDC
+KKRNURRZytZqRr9zzl2+79779Ty+9w943+ccZ7fPLvGv+N1js9jk8r7HfDFPzCRvktfIDOGEA64/
+rYD8oUCFVyhPGuAHtqRYUJq0IHFAOFBh7C4Hu/4B//4NfgO/bwGbN4JNm34FC8D7pWD+XDBv3hww
+8w3w5uvgtVfBjBmvAAf+CvrL6f+HfgXo30L/RurdAP2l9M+lfs4c+t+g/3X6X6U+JiB/egH54wJ/
+xQ74bUNkAR9a8F5kQeKAaOAvUr5T7CB20Aay5WexTkBPSt8Rb4uXyUvkBfK8cCqkNxL9O3ZIbyQH
+1klvxATkDwVC/p3btcAPrE8RmJ0U0IAX4wJ/+mDBdoLAeg8E1q5da/f8BGZ7IDBr1iy75y0DeqIB
+U6dOtXueM5wgsM1QYKvhB0wOaE8MmBzAbkyfPl2BadOmmd2A3Qv8QRBAAXoGfiHRwLskEkDB9OGA
+5H5gm/Rbt8oeDcgeDtgAg/qUAQ7YliJQVoZ7lhjJgZIS/FNcHAkUFeGfwsJC50efH8T34guPMgE5
+eNajRBSLSWQiKSImB5GA608rIH8osMZbsDppgB9YmmLBlKQFiQPCgTXGj6vBD6vA99+Bz8EXS0HZ
+YrBkyWfgGfDsFFDyNCgungwmTQATx4OicaCw8EHgwL+G/tX0r6JfAfqX0r+YejdA/xT6n6Z+8mT6
+J9A/nv5x1McE5E8vIH/KQGRAzAI+9Ex0QeKAUOBb8g34mnxFPiWfgI/FIvIUeRI8IR4nD5D7yX1i
+NHECfzRAvxuQXwH63YD8MQH5owEN0II9DWiAuyAxsHJl8NCKFb5/4cLIAg547LHIAj00dmx8YCXh
+QysEBywkuOhDY9GiDwD9ABc9YjwsqCcYMMoYPfpe4HwrvbHckH+ZIb0Bu0G9Ib0BexAoKCiQf8yY
+MdCPGkW9BaD+kiCAAvQMfESigUeJH3gIWAAF04cDkvuB5dIvWyZ7NCB7EKBegQLqUwY4YHmKQH4+
+7ulnJAdycxP8iYGcHPyTnZ3tjPQZJoaK2zzyBeSgtUeuaCGakpNIDjE5iARcf1oB+UOBEd6C4UkD
+/ED/FAtaJi1IHBAOjDBGDgfDhoCh94BbwW39Qf4toF+/m0Er0LolyD0btGjRHDRtAk46EeScALKz
+jwcO/CPoH07/EPoVoL8//bdQ7wbob0n/2dQ3b05/E/pPpP8E6mMC8qcXkD9lIDIgZgEfahVdkDgg
+FLib3AXuJHeQm8iN4AZxPTmLnAnOEKeT48ix5BjRkDiBPxqg3w3IrwD9bkD+mID80YAGaMGeBjTA
+XZAYGDw4eGjQIN/ft29kAQc0axZZoIcaN44PDCZ8aJDggL4EF11rXCPoB7joVOMUQT3BgAZGw4ZH
+A+du6Y2BhvwDDOkN2IPANdIbsAeBrKws+Rs1agR9gwbUWwDq2wkCKEDPwHXED/Qhpj6N+IGTgQVQ
+MH04ILkfGCj9gAGyBwHp+/SRPQhQr0AW9SkDHDAwRaBHj/hA3boJ/sRAjRr4p3r16s7VPleIy8Vl
+Hj3EJS5HedQVR4oDyQGkBjE5iARcf1oB+VMF3AG9Qwt6plhQP2mBBuwfF7jKB4XeBI2eHgh0797d
+HroY1PdAoE6dOnbPEQb0RAOqVatm9+xnOEGgl6FAnuEHTA5oTwyYHMBuZGZmKpCRkWF2A3YvcCVB
+AAXoGbiURAP1SCSAgunDAcn9QC/p8/JkjwZkDwdsgEF9ONDN83flgkigffuki2p7/lq1Ei/6v0A3
+48quoFcXkHcRuABc2h6cL84jtUG9WuBwcRjJ3Bdk7AP2FnsRB/5u9Helvwv9CtDvBuRXgH43IP9h
+9O9LvxuQPxqQP72A/DGBzp2DAZ06+f527SILOKBmzcgCDahaNT7QmfChToID2hFcdI7RVtAPcNEh
+xsGCeoIBlQx7h3ST3uhoyN/BkN6APQi0ld6APQhUqVJF/sqVK0NfqZL0jgP1hQQBFKBn4FziB9oQ
+Ux9K/MBBwAIomD4ckNwPdJS+QwfZg4D0bdrIHgSoV6AK9SkDHNAxzQD9sYH/AHAD8Cry1R2MAAAA
+AElFTkSuQmCC
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-005.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e9d9556d56d5adceb3610718267d535cf5b6f34d
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-005.xml
@@ -0,0 +1,6 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec
+CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI
+E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp
+oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT
++wHXVRp4qDc0HQAAAABJRU5ErkJggg==
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-006.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..06085d84fb78cce2f2ca1b9794139e3714ee687f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/page-006.xml
@@ -0,0 +1,9 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAO
+xAAADsQBlSsOGwAAARlJREFUeF7t2VFHQ2EcBvD3PWetVqvVam1ERESMiIiIiL7/J5p9gD1dvF51
+8eNcn4uf53n2P1ZLKUOpnkMCtYx0kkAt00F8gkAtsxFQBJpPAEWgxRGgCLScAopAq2NAEWhzAigC
+3c0ARaD7U0AR6OEMUAR6nAOKQE/ngCLQ8wWgCLRdAIpAL5eAItDrFaAI9LYEFIHerwFFoI8bQBHo
+cwUoAn3dAopA32tAEehnAygKjPu/xjyHBAY0WQDQL+0BBKhtYSVIgiSo6w+xiqmYiqnYX34J2SAb
+ZINskA1qa4EESZAEdW1B15e7g9xBbf2VIAmSICPtDmprgQRJkAR1bUHXl7uD3EFt/ZUgCZIgI+0O
+amuBBEnQP07QDo1OW207QPoWAAAAAElFTkSuQmCC
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/payloads.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/run.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..444c0b4fcd2140ff113f36c4ac5cf6ebfd49c19f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/page-removal/run/features/followup/notebook",
+ "expected_pages": 7,
+ "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/page-lifecycle/removal/rust-followup/features/followup-cold/scripts/cold.ps1 b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-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/page-lifecycle/removal/rust-followup/features/followup-cold/scripts/read.ps1 b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-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/page-lifecycle/removal/rust-followup/features/followup-cold/source.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..5c7d34c2abdbf0c8d7eee89aaa6659484b11fd6e
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Lifecycle.one",
+ "bytes": 139520,
+ "sha256": "4c51e1d89b121d74fc15945b3d4e83698956d274bfa092f21a12a817f3acd95a",
+ "mtime_ns": 1789112618636859068
+ },
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "f9e4761726171bdc72e946e6742603dd225f2e154298987f36f628b6d8741e78",
+ "mtime_ns": 1789112618637055068
+ }
+]
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/table-autofit.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/table-autofit.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/table-autofit.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/teardown.json b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup-cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/commands.jsonl b/corpus/page-lifecycle/removal/rust-followup/features/followup/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..8890440e62c8df2f58d706068cca42d10f800418
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/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-49B2CF78", "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-49B2CF78 -ExpectedPages 7 -UseCurrentCache", "exit": 0, "stdout": "Read 1 sections and 7 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/page-lifecycle/removal/rust-followup/features/followup/format-differences.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/format-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/format-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/geometry-differences.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/geometry-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/geometry-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/machine.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..a418249529b52569a1b8b65cbbf1ad53f97ba9c1
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/machine.json
@@ -0,0 +1 @@
+{"name": "m6-49b2cf78", "hostname": "ONE-M6-49B2CF78"}
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/native-title.txt b/corpus/page-lifecycle/removal/rust-followup/features/followup/native-title.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1d6cb6d0262b9c45259962e524e8b806eb94dbac
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/native-title.txt
@@ -0,0 +1 @@
+Native after Rust removal 🦋 é
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/features/followup/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..466222a5eec81265694fb371a7f3971a7c738814
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/features/followup/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/features/followup/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..18f015f65a9419ea2a74835decb84d7b76760183
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/features/followup/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/pdf-format-checks.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/pdf-format-checks.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/pdf-format-checks.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/environment.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..42f3ba3791b3c908136ed484017f14e6b71c78d5
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-49B2CF78",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/hierarchy.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7ea818ed49dfbad766702b7b502ab2e6e5d87fdd
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-000.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..992e8b870ffa1581d640dcc8baf4b3b9491c0d71
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-000.xml
@@ -0,0 +1,3 @@
+
+Native body after Rust removal.]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-001.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e589833ec0f679592a096846ed427e76e1b9dc89
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-001.xml
@@ -0,0 +1,6 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec
+CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI
+E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp
+oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT
++wHXVRp4qDc0HQAAAABJRU5ErkJggg==
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-002.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..94285d475fff149e2215da58eb159e42f50edae4
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-002.xml
@@ -0,0 +1,21 @@
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a
+HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABAAGADASIA
+AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA
+AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3
+ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm
+p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA
+AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx
+BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK
+U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3
+uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDwhYfa
+plh9qsrD7VMsPtUqRFOqVVh9qmWH2qysPtUyw+1UpHbTqlZYfaplh9qsrD7VMsPtVqR206pVWGpl
+h9qsrDUyw+1UpHbTqlZYalWH2q0sNSrD7VSkdtOqVlh9qmWH2qysPtUyw+1UpHdTqnHrD7VMsPtV
+lYfaplh9q81SPyqnVKqw+1TLD7VZWH2qZYfaqUjtp1SssPtUyw+1WVh9qmWH2qlI7adUqrDUyw+1
+WVhqZYfarUjtp1SssNSrD7VaWGpVh9qpSO2nVKyw+1TLD7VZWH2qZYfaqUjup1Tj1h9qmWH2qysP
+tUyw+1eapH5VTqlVYfaplh9qsrD7VMsPtVKR206pWWH2qZYfarKw+1TLD7VSkdtOqVVhqZYfarKw
+1MsPtVqR206pWWGpVh9qtLDUqw+1UpHbTqlZYfaplh9qsrD7VMsPtVKR206px6w+1TLD7VZWH2qZ
+YfavNUj8rp1SqsPtUyw+1WVh9qmWH2qlI7adUrLD7VMsPtVlYfaplh9qpSO2nVKqw1MsPtVlYamW
+H2q1I7adUrLDUqw+1WlhqVYfaqUjtp1SssPtUyw+1WVh9qmWH2qlI7adU//Z
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-003.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..54c0fb93aa867e9698a344396dbd4309290b46af
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-003.xml
@@ -0,0 +1,46 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAMAAADS6oI8AAADAFBMVEW4t5mrt5O4qpWrqo6euo2S
+uoeZs4ieqoiSqoK4nJCrnIq4kIyrkIaen4SSn36ZmH+ekH+SkHmGuoGGs395t3pounJos3CGqnx5
+qnVtqm9iqmqGnHd5nHGGkHN5kG1on2lomGdokGS4hIirhIK4d4Srd32eh3ySh3aZgHeed3eSd3G4
+a4Cra3m4YHyrYHaea3OSa22eYG+SYGmGh3CGgG55hGloh2FogF+Gd2t5d2Rtd15id1mGa2d5a2CG
+YGN5YF1ta1pia1VoYFRWumlWs2dJt2I4ulo4s1hWqmRJql09qlcyqlImulEaukshs0wKukMKs0Em
+qkwaqkYPqkAEqjtWnF9JnFlWkFtJkFU4n1E4mE84kEwmn0gan0IhmEMmkEMakD0KnzoKmDgKkDVW
+h1hWgFZJhFE4h0k4gEdWd1NJd0w9d0Yyd0FWa09Ja0hWYEtJYEU9a0Iyaz04YDwmh0AahzohgDsK
+hzIKgDAmdzsadzUPdy8EdyomazcaazEmYDMaYC0PaysEayYKYCW4VHirVHK4R3SrR22eV2ySV2aZ
+UGeeR2eSR2G4O3CrO2m4MGyrMGaeO2OSO12eMF+SMFmGV2CGUF55VFloV1FoUE+GR1t5R1RtR05i
+R0mGO1d5O1CGMFN5ME1tO0piO0VoMES4JGirJGK4GGSrGF6eJ1ySJ1aZIFeeGFeSGFG4DmGrDlq4
+A12rA1eeDlSSDk6eA1CSA0qGJE95JEmGGEt5GEVoJ0FoID9oGDyGDkh5DkGGA0R5Az5tDjtiDjZo
+AzVWVEdJVEE4Vzk4UDdWR0NJRzw9RzYyRzFWOz9JOzhWMDtJMDU9OzIyOy04MCwmVzAaVyohUCsK
+VyIKUCAmRysaRyUPRx8ERxomOycaOyEmMCMaMB0POxsEOxYKMBVWJDdJJDFWGDNJGC04Jyk4ICc4
+GCRWDjBJDilWAyxJAyY9DiMyDh44Ax0mJyAaJxohIBsmGBsaGBUKJxIKIBAKGA0mDhgaDhImAxQa
+Aw4PDgwEDgcKAwZYdbPqAAAAAWJLR0QAiAUdSAAAAAxjbVBQSkNtcDA3MTIAAAADSABzvAAABjJJ
+REFUWEedkImbTgUcha9ChZl2RUKR0TKTtGiRsZUSEpI0tKm0GoW0WSOjMmNpL82oFG1apJJltNDC
+KKRNURRZytZqRr9zzl2+79779Ty+9w943+ccZ7fPLvGv+N1js9jk8r7HfDFPzCRvktfIDOGEA64/
+rYD8oUCFVyhPGuAHtqRYUJq0IHFAOFBh7C4Hu/4B//4NfgO/bwGbN4JNm34FC8D7pWD+XDBv3hww
+8w3w5uvgtVfBjBmvAAf+CvrL6f+HfgXo30L/RurdAP2l9M+lfs4c+t+g/3X6X6U+JiB/egH54wJ/
+xQ74bUNkAR9a8F5kQeKAaOAvUr5T7CB20Aay5WexTkBPSt8Rb4uXyUvkBfK8cCqkNxL9O3ZIbyQH
+1klvxATkDwVC/p3btcAPrE8RmJ0U0IAX4wJ/+mDBdoLAeg8E1q5da/f8BGZ7IDBr1iy75y0DeqIB
+U6dOtXueM5wgsM1QYKvhB0wOaE8MmBzAbkyfPl2BadOmmd2A3Qv8QRBAAXoGfiHRwLskEkDB9OGA
+5H5gm/Rbt8oeDcgeDtgAg/qUAQ7YliJQVoZ7lhjJgZIS/FNcHAkUFeGfwsJC50efH8T34guPMgE5
+eNajRBSLSWQiKSImB5GA608rIH8osMZbsDppgB9YmmLBlKQFiQPCgTXGj6vBD6vA99+Bz8EXS0HZ
+YrBkyWfgGfDsFFDyNCgungwmTQATx4OicaCw8EHgwL+G/tX0r6JfAfqX0r+YejdA/xT6n6Z+8mT6
+J9A/nv5x1McE5E8vIH/KQGRAzAI+9Ex0QeKAUOBb8g34mnxFPiWfgI/FIvIUeRI8IR4nD5D7yX1i
+NHECfzRAvxuQXwH63YD8MQH5owEN0II9DWiAuyAxsHJl8NCKFb5/4cLIAg547LHIAj00dmx8YCXh
+QysEBywkuOhDY9GiDwD9ABc9YjwsqCcYMMoYPfpe4HwrvbHckH+ZIb0Bu0G9Ib0BexAoKCiQf8yY
+MdCPGkW9BaD+kiCAAvQMfESigUeJH3gIWAAF04cDkvuB5dIvWyZ7NCB7EKBegQLqUwY4YHmKQH4+
+7ulnJAdycxP8iYGcHPyTnZ3tjPQZJoaK2zzyBeSgtUeuaCGakpNIDjE5iARcf1oB+UOBEd6C4UkD
+/ED/FAtaJi1IHBAOjDBGDgfDhoCh94BbwW39Qf4toF+/m0Er0LolyD0btGjRHDRtAk46EeScALKz
+jwcO/CPoH07/EPoVoL8//bdQ7wbob0n/2dQ3b05/E/pPpP8E6mMC8qcXkD9lIDIgZgEfahVdkDgg
+FLib3AXuJHeQm8iN4AZxPTmLnAnOEKeT48ix5BjRkDiBPxqg3w3IrwD9bkD+mID80YAGaMGeBjTA
+XZAYGDw4eGjQIN/ft29kAQc0axZZoIcaN44PDCZ8aJDggL4EF11rXCPoB7joVOMUQT3BgAZGw4ZH
+A+du6Y2BhvwDDOkN2IPANdIbsAeBrKws+Rs1agR9gwbUWwDq2wkCKEDPwHXED/Qhpj6N+IGTgQVQ
+MH04ILkfGCj9gAGyBwHp+/SRPQhQr0AW9SkDHDAwRaBHj/hA3boJ/sRAjRr4p3r16s7VPleIy8Vl
+Hj3EJS5HedQVR4oDyQGkBjE5iARcf1oB+VMF3AG9Qwt6plhQP2mBBuwfF7jKB4XeBI2eHgh0797d
+HroY1PdAoE6dOnbPEQb0RAOqVatm9+xnOEGgl6FAnuEHTA5oTwyYHMBuZGZmKpCRkWF2A3YvcCVB
+AAXoGbiURAP1SCSAgunDAcn9QC/p8/JkjwZkDwdsgEF9ONDN83flgkigffuki2p7/lq1Ei/6v0A3
+48quoFcXkHcRuABc2h6cL84jtUG9WuBwcRjJ3Bdk7AP2FnsRB/5u9Helvwv9CtDvBuRXgH43IP9h
+9O9LvxuQPxqQP72A/DGBzp2DAZ06+f527SILOKBmzcgCDahaNT7QmfChToID2hFcdI7RVtAPcNEh
+xsGCeoIBlQx7h3ST3uhoyN/BkN6APQi0ld6APQhUqVJF/sqVK0NfqZL0jgP1hQQBFKBn4FziB9oQ
+Ux9K/MBBwAIomD4ckNwPdJS+QwfZg4D0bdrIHgSoV6AK9SkDHNAxzQD9sYH/AHAD8Cry1R2MAAAA
+AElFTkSuQmCC
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-004.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dea54bb07c51e60e69758160afd7553f31677024
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-004.xml
@@ -0,0 +1,6 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec
+CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI
+E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp
+oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT
++wHXVRp4qDc0HQAAAABJRU5ErkJggg==
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-005.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0b86a1b45fe6fcb3f02072ca13dd5fab56963444
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-005.xml
@@ -0,0 +1,9 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAO
+xAAADsQBlSsOGwAAARlJREFUeF7t2VFHQ2EcBvD3PWetVqvVam1ERESMiIiIiL7/J5p9gD1dvF51
+8eNcn4uf53n2P1ZLKUOpnkMCtYx0kkAt00F8gkAtsxFQBJpPAEWgxRGgCLScAopAq2NAEWhzAigC
+3c0ARaD7U0AR6OEMUAR6nAOKQE/ngCLQ8wWgCLRdAIpAL5eAItDrFaAI9LYEFIHerwFFoI8bQBHo
+cwUoAn3dAopA32tAEehnAygKjPu/xjyHBAY0WQDQL+0BBKhtYSVIgiSo6w+xiqmYiqnYX34J2SAb
+ZINskA1qa4EESZAEdW1B15e7g9xBbf2VIAmSICPtDmprgQRJkAR1bUHXl7uD3EFt/ZUgCZIgI+0O
+amuBBEnQP07QDo1OW207QPoWAAAAAElFTkSuQmCC
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-006.xml b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5d49dbcfd78d74202ce870dc239f35f73a3b3ead
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/page-006.xml
@@ -0,0 +1,6 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec
+CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI
+E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp
+oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT
++wHXVRp4qDc0HQAAAABJRU5ErkJggg==
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/read/payloads.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/run.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..6edbf0a12183a6055fa32fa6ebb441519fbf8730
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/run.json
@@ -0,0 +1,19 @@
+{
+ "notebook": "/private/tmp/page-removal/run/features/candidate/notebook",
+ "expected_pages": 7,
+ "author": "tools/native/page-removal-followup.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": "6e6bd74f0460457118d436345350c38594c52bd9199fb4a1e31efec3b3e954fd",
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/scripts/author.ps1 b/corpus/page-lifecycle/removal/rust-followup/features/followup/scripts/author.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..1393a1ff21e7ae5c383cf8babf1f4f21d5f0c14b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/scripts/author.ps1
@@ -0,0 +1,27 @@
+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
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('Lifecycle.one', $notebook, [ref]$section, 0)
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 2)
+ $title = 'Native after Rust removal ' + [char]::ConvertFromUtf32(0x1F98B) + ' e' + [char]0x0301
+ $xml = 'Native body after Rust removal.]]>'
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+ $app.SyncHierarchy($notebook)
+ [IO.File]::WriteAllText("$Root\native-title.txt", $title, [Text.Encoding]::UTF8)
+} finally {
+ try { if ($notebook) { $app.CloseNotebook($notebook, $false) } }
+ finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 120 -ErrorAction SilentlyContinue
+ }
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/scripts/cold.ps1 b/corpus/page-lifecycle/removal/rust-followup/features/followup/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/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/page-lifecycle/removal/rust-followup/features/followup/scripts/read.ps1 b/corpus/page-lifecycle/removal/rust-followup/features/followup/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/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/page-lifecycle/removal/rust-followup/features/followup/source.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..89f2898869d302da880f24f0500622da42562f67
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Lifecycle.one",
+ "bytes": 132280,
+ "sha256": "266aaf86a29b942255349dc84ec8a8d9691c965aeafd16122326a97b3acb10ee",
+ "mtime_ns": 1789112426557088257
+ },
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "f9e4761726171bdc72e946e6742603dd225f2e154298987f36f628b6d8741e78",
+ "mtime_ns": 1789112426559091346
+ }
+]
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/table-autofit.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/table-autofit.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/table-autofit.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/features/followup/teardown.json b/corpus/page-lifecycle/removal/rust-followup/features/followup/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/features/followup/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/candidate/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/leading-parent/candidate/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..7fe07784b693ae51a17260afb50bafe4fb3cd360
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/leading-parent/candidate/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/candidate/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/leading-parent/candidate/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..1dae1b4365a16cf77afdc426b8d194a6094c4959
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/leading-parent/candidate/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/commands.jsonl b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..2c37f99ded86c68753a7e4fd783ddceb880b5b18
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-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-063C5792 -ExpectedPages 9", "exit": 0, "stdout": "Read 1 sections and 9 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/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/format-differences.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/format-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/format-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/geometry-differences.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/geometry-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/geometry-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/machine.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..f9216d1602f5fc6bd0625b4fbddb91c00fa88941
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-063c5792", "hostname": "ONE-M6-063C5792"}
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..3383072968b32d792c5b00be44ef5b8ccf23ad20
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..1dae1b4365a16cf77afdc426b8d194a6094c4959
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/pdf-format-checks.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/pdf-format-checks.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/pdf-format-checks.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/environment.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..9e05ec1c8423079d671ca080c52a44995664b654
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-063C5792",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/hierarchy.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ca97e331eff8d1687518046c7439a79b14044cdb
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-000.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f8d107f1f3e620f09f5233ce36544bf3ac0cd5f5
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-001.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0950640f2a72186a7cba084de16c4612a2f2e9d7
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-002.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4d3714d0012ec9fac7c078bf3db94d3fb639b8e8
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-003.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..403bd15be0b0f58a4a0872191af6d4ccaea2a32d
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-004.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f44adc13639a3e0949b32574a8efe968d37bd0a1
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-005.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..53ce86660fa9b562bbbf7daffd38c9f636e70e4a
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-005.xml
@@ -0,0 +1,3 @@
+
+Native body after Rust removal.]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-006.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a0542bdf4b035130ff2a99225b955b406646aa14
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-007.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..998c43fbf34ac4edb0070c408a5fd2bd6a1c4854
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-008.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..abcef66e0197b993a57ffc1724b5635520b07a6e
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/payloads.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/run.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..60af9620b50621dc4b9b5b726ce9de6a5726ab87
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/page-removal/run/leading-parent/followup/notebook",
+ "expected_pages": 9,
+ "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/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/scripts/cold.ps1 b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-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/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/scripts/read.ps1 b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-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/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/source.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..5bd43afecbfca65159103620a696807254fefebb
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Lifecycle.one",
+ "bytes": 83640,
+ "sha256": "07d44900cbb3e03c7ccb27fb256afee1601fb2473cfa927b1c3e20aa662c3dd4",
+ "mtime_ns": 1789113084012096660
+ },
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "99b99375297e299d0dabe97e4c156a1a2249fd0521edebae64ac3221a5955f2c",
+ "mtime_ns": 1789113084012236119
+ }
+]
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/table-autofit.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/table-autofit.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/table-autofit.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/teardown.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup-cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/commands.jsonl b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..ff4e17a7646b5df9c9be14fa7db35d795876606d
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/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-9A847CAB", "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-9A847CAB -ExpectedPages 9 -UseCurrentCache", "exit": 0, "stdout": "Read 1 sections and 9 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/page-lifecycle/removal/rust-followup/leading-parent/followup/format-differences.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/format-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/format-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/geometry-differences.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/geometry-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/geometry-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/machine.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..322696b28ec0103274fb9099155a6771a0411448
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/machine.json
@@ -0,0 +1 @@
+{"name": "m6-9a847cab", "hostname": "ONE-M6-9A847CAB"}
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/native-title.txt b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/native-title.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1d6cb6d0262b9c45259962e524e8b806eb94dbac
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/native-title.txt
@@ -0,0 +1 @@
+Native after Rust removal 🦋 é
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..3383072968b32d792c5b00be44ef5b8ccf23ad20
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..1dae1b4365a16cf77afdc426b8d194a6094c4959
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/pdf-format-checks.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/pdf-format-checks.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/pdf-format-checks.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/environment.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..9ad51a471da74087fbd8037174d956f3cde710cc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-9A847CAB",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/hierarchy.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f2321523859e1724c2f36b7a94dc577fcb1d8aaa
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-000.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9298dcbbbe758bda0abc748b4e7d56792817039b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-001.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e81fb3389e0c5c07d2f6df7a31842b4e69c08635
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-002.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..caba52d63cf1396da7822afe5c2c48a67315504f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-003.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1633d9ae816a1833019e3b2c0374d309d3a6513e
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-004.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8ed396aba33ba7970da318b21644c30d6e7d46b5
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-004.xml
@@ -0,0 +1,3 @@
+
+Native body after Rust removal.]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-005.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..91f894c8b52afa812c523e188f9860f9706fda70
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-006.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..42c2d85c414b62f40014dd72e418ec1d8011f783
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-007.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..03a4e117fdea11277906ed5c1f3f42f53da98d69
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body parent 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-008.xml b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1657d126f1c92d6106cb941aced314c954b53396
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/payloads.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/run.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..a1fc8f5f18ca882d551ca0ca57f1ce4734ce47bb
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/run.json
@@ -0,0 +1,19 @@
+{
+ "notebook": "/private/tmp/page-removal/run/leading-parent/candidate/notebook",
+ "expected_pages": 9,
+ "author": "tools/native/page-removal-followup.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": "6e6bd74f0460457118d436345350c38594c52bd9199fb4a1e31efec3b3e954fd",
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/scripts/author.ps1 b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/scripts/author.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..1393a1ff21e7ae5c383cf8babf1f4f21d5f0c14b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/scripts/author.ps1
@@ -0,0 +1,27 @@
+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
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('Lifecycle.one', $notebook, [ref]$section, 0)
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 2)
+ $title = 'Native after Rust removal ' + [char]::ConvertFromUtf32(0x1F98B) + ' e' + [char]0x0301
+ $xml = 'Native body after Rust removal.]]>'
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+ $app.SyncHierarchy($notebook)
+ [IO.File]::WriteAllText("$Root\native-title.txt", $title, [Text.Encoding]::UTF8)
+} finally {
+ try { if ($notebook) { $app.CloseNotebook($notebook, $false) } }
+ finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 120 -ErrorAction SilentlyContinue
+ }
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/scripts/cold.ps1 b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/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/page-lifecycle/removal/rust-followup/leading-parent/followup/scripts/read.ps1 b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/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/page-lifecycle/removal/rust-followup/leading-parent/followup/source.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..a0b4ba0e793cce014c04a95e8ce91e8f014cc727
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Lifecycle.one",
+ "bytes": 76440,
+ "sha256": "4aabf26fa4960cf6d99d76c1f76906ef1023f04bf01b65ea07464ac87449ca3e",
+ "mtime_ns": 1789112626756437119
+ },
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "99b99375297e299d0dabe97e4c156a1a2249fd0521edebae64ac3221a5955f2c",
+ "mtime_ns": 1789112626758941541
+ }
+]
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/table-autofit.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/table-autofit.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/table-autofit.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/teardown.json b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/leading-parent/followup/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/candidate/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/parent/candidate/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..c24e3d33dbe8b0e1520ab626de0b76d9eb50c593
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/parent/candidate/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/candidate/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/parent/candidate/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..4b21b2a4655ba5798579711f620169ba8703d230
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/parent/candidate/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/commands.jsonl b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..7f52bf47fb5b15fba4107471fd31e7833e6a7d3d
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-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-E51EA1B2 -ExpectedPages 9", "exit": 0, "stdout": "Read 1 sections and 9 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/page-lifecycle/removal/rust-followup/parent/followup-cold/format-differences.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/format-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/format-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/geometry-differences.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/geometry-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/geometry-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/machine.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..570b75fc34d5a631e3c86e82c943e4752ec4989b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-e51ea1b2", "hostname": "ONE-M6-E51EA1B2"}
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..77e89bb5d74fc2fae18da69eda267b9907f1c4e4
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..4b21b2a4655ba5798579711f620169ba8703d230
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/pdf-format-checks.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/pdf-format-checks.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/pdf-format-checks.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/environment.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..cbb69571638aff46bf12999c3411309d83ae9257
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-E51EA1B2",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/hierarchy.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a2ce47acea2d9fc6f9b840f589a0b82d9485c838
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-000.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..182a7c665bbc1f9cbb52306bc7a5606e1a99d9d4
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-001.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..33caceac601c6ef1edcf48ede908fd804fd7ea24
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-001.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-002.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..153373a906d9ed1d550cf7634aaebc6c1a6755aa
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-002.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-003.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..68b3002e8923753a7c3deb251aedec8f4eb2efd2
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-004.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4b1bbbb5232fb2226e633ad50d3005f9e7160b93
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-004.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-005.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..17d7e6acffb6e3b7679021840d2f448d1ab30f36
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-006.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0c942bdf032eef7ec552066297a4470d0aa2641d
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-006.xml
@@ -0,0 +1,3 @@
+
+Native body after Rust removal.]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-007.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..416598cb23bbe1a18ac8d6a3e40d0f8d808af126
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-008.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c8d7cd9b92cf1a8e409ae6471fd120d35072ea49
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/payloads.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/run.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..9c0f9a413a9bada6bc6e413cb34951453ae421df
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/page-removal/run/parent/followup/notebook",
+ "expected_pages": 9,
+ "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/page-lifecycle/removal/rust-followup/parent/followup-cold/scripts/cold.ps1 b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-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/page-lifecycle/removal/rust-followup/parent/followup-cold/scripts/read.ps1 b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-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/page-lifecycle/removal/rust-followup/parent/followup-cold/source.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..8c4bcdeee919f316a3182ceaec6f30c23a4aaca3
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Lifecycle.one",
+ "bytes": 62856,
+ "sha256": "e1c9e25eb5bb28c90ecd2f9bd627497489382e485c47efbc38e719880e7c0f3d",
+ "mtime_ns": 1789111825549418917
+ },
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "a0530a11d189eaf6c64273f6997ca3a2e953ab911798993420b872794d1d3cbc",
+ "mtime_ns": 1789111825549583834
+ }
+]
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/table-autofit.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/table-autofit.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/table-autofit.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/teardown.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup-cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/commands.jsonl b/corpus/page-lifecycle/removal/rust-followup/parent/followup/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..9a38c1d06cd642b4bb6812561346d086e909ce23
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/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-E0700600", "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-E0700600 -ExpectedPages 9 -UseCurrentCache", "exit": 0, "stdout": "Read 1 sections and 9 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/page-lifecycle/removal/rust-followup/parent/followup/format-differences.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/format-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/format-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/geometry-differences.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/geometry-differences.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/geometry-differences.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/machine.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..58ddf44e426c7ff6d659dc2e09f098b4ea62e98f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/machine.json
@@ -0,0 +1 @@
+{"name": "m6-e0700600", "hostname": "ONE-M6-E0700600"}
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/native-title.txt b/corpus/page-lifecycle/removal/rust-followup/parent/followup/native-title.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1d6cb6d0262b9c45259962e524e8b806eb94dbac
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/native-title.txt
@@ -0,0 +1 @@
+Native after Rust removal 🦋 é
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/notebook/Lifecycle.one b/corpus/page-lifecycle/removal/rust-followup/parent/followup/notebook/Lifecycle.one
new file mode 100644
index 0000000000000000000000000000000000000000..49437dba925e8a3e684cfcccacaf6a6c279bf373
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/parent/followup/notebook/Lifecycle.one differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/notebook/Open Notebook.onetoc2 b/corpus/page-lifecycle/removal/rust-followup/parent/followup/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..4b21b2a4655ba5798579711f620169ba8703d230
Binary files /dev/null and b/corpus/page-lifecycle/removal/rust-followup/parent/followup/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/pdf-format-checks.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/pdf-format-checks.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/pdf-format-checks.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/environment.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..33936a4a44f62cca5f2695411462577b5aba822c
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-E0700600",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/hierarchy.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..052cb1f195ad1e755e5610ccf627aead83a3ea6d
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-000.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c70617f7dc55405d0d76d844475dcd38aed4291e
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-000.xml
@@ -0,0 +1,4 @@
+
+Body child 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-001.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5ca7645eb6045823acb2ce8b818ac9b4202572d6
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-001.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-002.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dd99c619f8ebe470b08df5eb0e0b06f6f8fba370
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-002.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-003.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..42dc5a8822f99a3854c4c0fd1d06912dde0d857e
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-003.xml
@@ -0,0 +1,4 @@
+
+Body grandchild 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-004.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..617f8cadd8c53f8b51f2ee687c5e995ecc2a6582
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-005.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..21034b0c616812c48c64dc059595ab966257011b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-005.xml
@@ -0,0 +1,3 @@
+
+Native body after Rust removal.]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-006.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3dc9610328c1dac6527ec48cc49b2e092f7167b9
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Body duplicate 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-007.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..aaac10673bbd220fb5104ad677b951e826e61500
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-007.xml
@@ -0,0 +1,4 @@
+
+Body automatic 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-008.xml b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3c3477ba64cc94398c54e05697781bcabef59590
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/page-008.xml
@@ -0,0 +1,4 @@
+
+Body trailing 🦀 é preserved]]>
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/payloads.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/run.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..2db66012badd1d3a40daa0a7c952fd271cb98569
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/run.json
@@ -0,0 +1,19 @@
+{
+ "notebook": "/private/tmp/page-removal/run/parent/candidate/notebook",
+ "expected_pages": 9,
+ "author": "tools/native/page-removal-followup.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": "6e6bd74f0460457118d436345350c38594c52bd9199fb4a1e31efec3b3e954fd",
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/scripts/author.ps1 b/corpus/page-lifecycle/removal/rust-followup/parent/followup/scripts/author.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..1393a1ff21e7ae5c383cf8babf1f4f21d5f0c14b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/scripts/author.ps1
@@ -0,0 +1,27 @@
+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
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('Lifecycle.one', $notebook, [ref]$section, 0)
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 2)
+ $title = 'Native after Rust removal ' + [char]::ConvertFromUtf32(0x1F98B) + ' e' + [char]0x0301
+ $xml = 'Native body after Rust removal.]]>'
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+ $app.SyncHierarchy($notebook)
+ [IO.File]::WriteAllText("$Root\native-title.txt", $title, [Text.Encoding]::UTF8)
+} finally {
+ try { if ($notebook) { $app.CloseNotebook($notebook, $false) } }
+ finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 120 -ErrorAction SilentlyContinue
+ }
+}
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/scripts/cold.ps1 b/corpus/page-lifecycle/removal/rust-followup/parent/followup/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/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/page-lifecycle/removal/rust-followup/parent/followup/scripts/read.ps1 b/corpus/page-lifecycle/removal/rust-followup/parent/followup/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/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/page-lifecycle/removal/rust-followup/parent/followup/source.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..f73e5732ca13c896dfeda82225eed03d39252e21
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Lifecycle.one",
+ "bytes": 56688,
+ "sha256": "f7b163e466afb5c13530beefd2bb7b3303f08ed42f5a6f817a805abf7005d65b",
+ "mtime_ns": 1789111621160316671
+ },
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "a0530a11d189eaf6c64273f6997ca3a2e953ab911798993420b872794d1d3cbc",
+ "mtime_ns": 1789111621162916678
+ }
+]
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/table-autofit.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/table-autofit.json
new file mode 100644
index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/table-autofit.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/corpus/page-lifecycle/removal/rust-followup/parent/followup/teardown.json b/corpus/page-lifecycle/removal/rust-followup/parent/followup/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/parent/followup/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/page-lifecycle/removal/rust-followup/provenance.json b/corpus/page-lifecycle/removal/rust-followup/provenance.json
new file mode 100644
index 0000000000000000000000000000000000000000..c85c2973f1176eaa1643ad5017ae1210ac335ea4
--- /dev/null
+++ b/corpus/page-lifecycle/removal/rust-followup/provenance.json
@@ -0,0 +1,77 @@
+{
+ "generator": "ONESTORE_PAGE_REMOVAL_OUTPUT on removal_matches_all_native_cases_and_retains_every_old_revision",
+ "author_script": "tools/native/page-removal-followup.ps1",
+ "cases": {
+ "parent": {
+ "candidate_sha256": "f7b163e466afb5c13530beefd2bb7b3303f08ed42f5a6f817a805abf7005d65b",
+ "captures": {
+ "followup": {
+ "clone": "m6-e0700600",
+ "teardown_absent": true,
+ "notebook_sha256": "e1c9e25eb5bb28c90ecd2f9bd627497489382e485c47efbc38e719880e7c0f3d",
+ "pages": 9
+ },
+ "followup-cold": {
+ "clone": "m6-e51ea1b2",
+ "teardown_absent": true,
+ "notebook_sha256": "253fde4f872bb302632de20eb977180114ab1776d6032cc4e9e46905ff3e6366",
+ "pages": 9
+ }
+ }
+ },
+ "all": {
+ "candidate_sha256": "645169c4f76dedd0deb0c928a08339479763ae607c21b7f604c21d823eee53eb",
+ "captures": {
+ "followup": {
+ "clone": "m6-4c5ff100",
+ "teardown_absent": true,
+ "notebook_sha256": "36a33c1d7f8a13d93f6bcb0ea9431d5524a1f43aa8a8e121dc2c735e076cc029",
+ "pages": 1
+ },
+ "followup-cold": {
+ "clone": "m6-9d41f146",
+ "teardown_absent": true,
+ "notebook_sha256": "36a33c1d7f8a13d93f6bcb0ea9431d5524a1f43aa8a8e121dc2c735e076cc029",
+ "pages": 1
+ }
+ }
+ },
+ "features": {
+ "candidate_sha256": "266aaf86a29b942255349dc84ec8a8d9691c965aeafd16122326a97b3acb10ee",
+ "captures": {
+ "followup": {
+ "clone": "m6-49b2cf78",
+ "teardown_absent": true,
+ "notebook_sha256": "4c51e1d89b121d74fc15945b3d4e83698956d274bfa092f21a12a817f3acd95a",
+ "pages": 7
+ },
+ "followup-cold": {
+ "clone": "m6-53651ab4",
+ "teardown_absent": true,
+ "notebook_sha256": "5d7d209a9320bcd932879da30bd0b6d54e87b7ee85766037bf69954d3f2d66b0",
+ "pages": 7
+ }
+ }
+ },
+ "leading-parent": {
+ "candidate_sha256": "4aabf26fa4960cf6d99d76c1f76906ef1023f04bf01b65ea07464ac87449ca3e",
+ "captures": {
+ "followup": {
+ "clone": "m6-9a847cab",
+ "teardown_absent": true,
+ "notebook_sha256": "07d44900cbb3e03c7ccb27fb256afee1601fb2473cfa927b1c3e20aa662c3dd4",
+ "pages": 9
+ },
+ "followup-cold": {
+ "clone": "m6-063c5792",
+ "teardown_absent": true,
+ "notebook_sha256": "07d44900cbb3e03c7ccb27fb256afee1601fb2473cfa927b1c3e20aa662c3dd4",
+ "pages": 9
+ }
+ }
+ }
+ },
+ "rejected_attempts": [
+ "features followup-cold and leading-parent followup first ran while the lab configuration was moving to .env and failed before booting a clone; both were rerun"
+ ]
+}
diff --git a/crates/onestore/tests/page_deletion.rs b/crates/onestore/tests/page_deletion.rs
index 1dd8deab5e63a8f5396de11c9d8e79ffc432b44c..c3ceaa3706ce9617c7fcd4a6edfee76340991093 100644
--- a/crates/onestore/tests/page_deletion.rs
+++ b/crates/onestore/tests/page_deletion.rs
@@ -1,6 +1,6 @@
use onestore::{
ExGuid, FileDataReference, ObjectData, PreparedEdit, PropertySets, RevisionIndex, Store,
- document::Document,
+ document::{Document, Kind},
};
use std::collections::BTreeSet;
@@ -177,6 +177,82 @@ fn removal_rejects_duplicate_missing_and_non_page_spaces_without_publication() {
use page_schedule::{current, disk};
+/// OneNote created a page in each section after Rust permanently removed pages from it;
+/// the surviving pages keep their revisions through the native save and a cold reopen.
+#[test]
+fn native_pages_created_after_rust_removal_reopen_cold() {
+ let root = std::path::Path::new(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/../../corpus/page-lifecycle/removal/rust-followup"
+ ));
+ for case in ["parent", "all", "features", "leading-parent"] {
+ let read = |phase: &str| {
+ std::fs::read(root.join(case).join(phase).join("notebook/Lifecycle.one")).unwrap()
+ };
+ let (candidate, followup, cold) =
+ (read("candidate"), read("followup"), read("followup-cold"));
+ let title =
+ std::fs::read_to_string(root.join(case).join("followup/native-title.txt")).unwrap();
+ let title = title.trim_start_matches('\u{feff}').trim_end();
+ let stores = [&candidate, &followup, &cold].map(|bytes| Store::parse(bytes).unwrap());
+ let indexes = stores.each_ref().map(|store| {
+ assert!(store.checksum_mismatches.is_empty());
+ let index = RevisionIndex::parse(store).unwrap();
+ index.validate_current().unwrap();
+ index
+ });
+ let documents = indexes
+ .each_ref()
+ .map(|index| Document::parse(index).unwrap());
+ let before = documents[0].pages().unwrap();
+ for (document, index) in documents[1..].iter().zip(&indexes[1..]) {
+ let pages = document.pages().unwrap();
+ assert_eq!(pages.len(), before.len() + 1, "{case}");
+ assert_eq!(
+ &pages[..before.len()],
+ before.as_slice(),
+ "{case}: surviving page order"
+ );
+ for (sid, _) in &before {
+ assert_eq!(
+ index.active(*sid).unwrap(),
+ indexes[0].active(*sid).unwrap(),
+ "{case}: {sid}"
+ );
+ }
+ let (native, page) = pages[before.len()];
+ let revision = document.active(native).unwrap();
+ let metadata = revision
+ .roots
+ .get(&2)
+ .and_then(|id| revision.nodes.get(id))
+ .unwrap();
+ assert!(
+ matches!(&metadata.kind, Kind::Metadata { title: Some(name), .. } if name == title),
+ "{case}"
+ );
+ let body = revision
+ .parents(&[page])
+ .unwrap()
+ .into_keys()
+ .any(|id| matches!(&revision.nodes[&id].kind, Kind::RichText { text, .. } if text == "Native body after Rust removal."));
+ assert!(body, "{case}: native body");
+ }
+ // The section root may gain OneNote's per-series navigation metadata copies on open.
+ for sid in indexes[1]
+ .spaces
+ .keys()
+ .filter(|sid| **sid != indexes[1].root)
+ {
+ assert_eq!(
+ indexes[1].active(*sid).unwrap(),
+ indexes[2].active(*sid).unwrap(),
+ "{case}: cold reopen changed {sid}"
+ );
+ }
+ }
+}
+
#[test]
fn tombstones_and_first_page_promotion_survive_each_storage_interruption() {
let leading = include_bytes!(
diff --git a/tools/codex_usage_report.example.json b/tools/codex_usage_report.example.json
deleted file mode 100644
index ce4d61158f7a0178d789b7024e83fde0b696b363..0000000000000000000000000000000000000000
--- a/tools/codex_usage_report.example.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "threadIds": ["01a078fb-d5d2-7ad0-97f0-d14db4e4c94e"],
- "sources": [
- "/Users/clo/.codex/sessions/2026/09/06/rollout-2026-09-06T16-09-25-01a078fb-d5d2-7ad0-97f0-d14db4e4c94e.jsonl"
- ],
- "boundaries": [
- {
- "name": "Planning discussion",
- "kind": "discussion",
- "end": "2026-09-07T01:28:56Z"
- },
- {
- "name": "Canvas goal",
- "kind": "goal",
- "start": "2026-09-07T01:28:56Z"
- }
- ]
-}
diff --git a/tools/codex_usage_report.mjs b/tools/codex_usage_report.mjs
deleted file mode 100644
index 45cd27ff308511f6c86df1d755459e533085ea16..0000000000000000000000000000000000000000
--- a/tools/codex_usage_report.mjs
+++ /dev/null
@@ -1,129 +0,0 @@
-#!/usr/bin/env node
-
-import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
-import { homedir } from "node:os";
-import { dirname, join, resolve } from "node:path";
-
-const rates = { input: 10, cached: 1, output: 50 };
-
-function usageCost(usage) {
- return ((usage.input_tokens - usage.cached_input_tokens) * rates.input
- + usage.cached_input_tokens * rates.cached
- + usage.output_tokens * rates.output) / 1_000_000;
-}
-
-function duration(ms) {
- const seconds = Math.round(ms / 1000);
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const remainder = seconds % 60;
- return hours ? `${hours}h ${minutes}m` : minutes ? `${minutes}m ${remainder}s` : `${remainder}s`;
-}
-
-function unionDuration(intervals) {
- const merged = [];
- for (const interval of intervals.sort((a, b) => a[0] - b[0])) {
- const previous = merged.at(-1);
- if (previous && interval[0] <= previous[1]) previous[1] = Math.max(previous[1], interval[1]);
- else merged.push([...interval]);
- }
- return merged.reduce((total, [start, end]) => total + end - start, 0);
-}
-
-async function filesBelow(root) {
- const entries = await readdir(root, { withFileTypes: true });
- const nested = await Promise.all(entries.map((entry) => {
- const path = join(root, entry.name);
- return entry.isDirectory() ? filesBelow(path) : entry.name.endsWith(".jsonl") ? [path] : [];
- }));
- return nested.flat();
-}
-
-async function loadEvents(paths) {
- const events = [];
- for (const path of paths) {
- for (const line of (await readFile(path, "utf8")).trim().split("\n")) {
- if (!line) continue;
- try { events.push(JSON.parse(line)); } catch { /* Ignore a partially-written final line. */ }
- }
- }
- return events;
-}
-
-function snapshotBefore(records, time) {
- return records.filter((record) => Date.parse(record.timestamp) <= time).at(-1)?.payload.thread_token_usage;
-}
-
-function taskRows(events, start, end) {
- const tasks = new Map();
- for (const event of events) {
- if (event.type !== "event_msg" || !["task_started", "task_complete"].includes(event.payload.type)) continue;
- const prior = tasks.get(event.payload.turn_id) ?? {};
- tasks.set(event.payload.turn_id, { ...prior, ...event.payload });
- }
- return [...tasks.values()].filter((task) => task.started_at >= start && task.started_at < end && task.duration_ms);
-}
-
-function toolIntervals(events, start, end) {
- return events.flatMap((event) => {
- const item = event.type === "event_msg" && event.payload.type === "item_completed" ? event.payload.item : undefined;
- if (!item || !["CommandExecution", "McpToolCall"].includes(item.type)) return [];
- const milliseconds = (item.duration?.secs ?? 0) * 1000 + (item.duration?.nanos ?? 0) / 1_000_000;
- const finished = Date.parse(event.timestamp);
- return milliseconds && finished >= start && finished < end ? [[finished - milliseconds, finished]] : [];
- });
-}
-
-function segment(events, records, boundary, previous, finalTime) {
- const start = boundary.start ? Date.parse(boundary.start) : -Infinity;
- const end = boundary.end ? Date.parse(boundary.end) : finalTime;
- const tasks = taskRows(events, start, end);
- const tools = toolIntervals(events, start, end);
- const finish = snapshotBefore(records, end);
- const begin = previous ?? snapshotBefore(records, start);
- return {
- name: boundary.name,
- kind: boundary.kind ?? "segment",
- cost: finish ? usageCost(finish) - (begin ? usageCost(begin) : 0) : null,
- durationMs: tasks.reduce((total, task) => total + task.duration_ms, 0),
- apiTtftMs: tasks.reduce((total, task) => total + (task.time_to_first_token_ms ?? 0), 0),
- toolWallMs: unionDuration(tools),
- toolCoreMs: tools.reduce((total, [startTime, endTime]) => total + endTime - startTime, 0),
- };
-}
-
-function chart(segments) {
- const maximum = Math.max(...segments.map((segment) => segment.cost ?? 0), 1);
- return segments.map((segment) => `
${escapeHtml(segment.name)}
${segment.cost === null ? "—" : `$${segment.cost.toFixed(2)}`} `).join("\n");
-}
-
-function escapeHtml(value) {
- return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
-}
-
-function html(report) {
- const rows = report.segments.map((segment) => `| ${escapeHtml(segment.name)} | ${segment.kind} | ${segment.cost === null ? "—" : `$${segment.cost.toFixed(2)}`} | ${duration(segment.durationMs)} | ${duration(segment.toolWallMs)} | ${duration(segment.toolCoreMs)} | ≥${duration(segment.apiTtftMs)} |
`).join("\n");
- return `Codex usage reportCodex usage report
API-equivalent rate: Astra standard ($10/M uncached input, $1/M cached input, $50/M output). Tool core time counts parallel calls separately; API TTFT is a lower bound.
Cost
${chart(report.segments)}Segments
| Segment | Type | Cost | Duration | Tool wall | Tool core | Recorded API |
${rows}
`;
-}
-
-function usage() {
- console.error("Usage: node tools/codex_usage_report.mjs report.json [output.html]");
- process.exit(1);
-}
-
-const [configPath, outputPath] = process.argv.slice(2);
-if (!configPath) usage();
-const config = JSON.parse(await readFile(resolve(configPath), "utf8"));
-const codexHome = config.codexHome ?? join(homedir(), ".codex");
-const paths = config.sources ?? (await Promise.all(["sessions", "archived_sessions"].map((name) => filesBelow(join(codexHome, name))))).flat();
-const events = await loadEvents(paths);
-const selected = events.filter((event) => event.type !== "token_usage_record" || config.threadIds.includes(event.payload.thread_id));
-const records = selected.filter((event) => event.type === "token_usage_record").sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
-if (!records.length) throw new Error("No token records matched config.threadIds.");
-const finalTime = Date.parse(records.at(-1).timestamp);
-const segments = config.boundaries.map((boundary, index) => segment(selected, records, boundary, index ? snapshotBefore(records, Date.parse(config.boundaries[index - 1].end ?? new Date(finalTime).toISOString())) : undefined, finalTime));
-const report = { threadIds: config.threadIds, total: usageCost(records.at(-1).payload.thread_token_usage), segments };
-if (outputPath) {
- await mkdir(dirname(resolve(outputPath)), { recursive: true });
- await writeFile(resolve(outputPath), html(report));
-} else console.log(JSON.stringify(report, null, 2));
diff --git a/tools/native/page-removal-followup.ps1 b/tools/native/page-removal-followup.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..1393a1ff21e7ae5c383cf8babf1f4f21d5f0c14b
--- /dev/null
+++ b/tools/native/page-removal-followup.ps1
@@ -0,0 +1,27 @@
+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
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('Lifecycle.one', $notebook, [ref]$section, 0)
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 2)
+ $title = 'Native after Rust removal ' + [char]::ConvertFromUtf32(0x1F98B) + ' e' + [char]0x0301
+ $xml = 'Native body after Rust removal.]]>'
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+ $app.SyncHierarchy($notebook)
+ [IO.File]::WriteAllText("$Root\native-title.txt", $title, [Text.Encoding]::UTF8)
+} finally {
+ try { if ($notebook) { $app.CloseNotebook($notebook, $false) } }
+ finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 120 -ErrorAction SilentlyContinue
+ }
+}
diff --git a/tools/test_page_removal.py b/tools/test_page_removal.py
index e3ae6bbb25f427b19682063c8a26178d2eada497..a974d31aef6ad7f7c343db91a75bc0f8c26cd972 100644
--- a/tools/test_page_removal.py
+++ b/tools/test_page_removal.py
@@ -59,3 +59,22 @@ class PageRemovalTest(unittest.TestCase):
if __name__ == '__main__':
unittest.main()
+
+
+class RustRemovalFollowupTest(unittest.TestCase):
+ """OneNote creates a page in a section after Rust removed pages from it, then reopens cold."""
+
+ CASES = ('parent', 'all', 'features', 'leading-parent')
+
+ def test_native_page_after_rust_removal_matches_its_captures(self):
+ for case in self.CASES:
+ with self.subTest(case=case):
+ root = FIXTURE / 'rust-followup' / case
+ title = (root / 'followup/native-title.txt').read_text(encoding='utf-8-sig')
+ self.assertTrue(title.startswith('Native after Rust removal'))
+ for phase in ('followup', 'followup-cold'):
+ compare(root / phase / 'notebook', root / phase / 'read')
+ hierarchy = ET.parse(root / phase / 'read/hierarchy.xml').getroot()
+ names = [page.get('name') for page in hierarchy.findall('.//one:Page', ns)]
+ self.assertEqual(names.count(title), 1, names)
+ self.assertTrue(json.loads((root / phase / 'teardown.json').read_text())['absent'])
diff --git a/tools/zenith-locks b/tools/zenith-locks
deleted file mode 100755
index 095817d79bc5bb93f0b901b9db7720ccc1ce2136..0000000000000000000000000000000000000000
--- a/tools/zenith-locks
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/usr/bin/env python3
-"""Watch SMB byte-range locks on zenith (TrueNAS) as OneNote takes and drops them."""
-import json
-import os
-import subprocess
-import sys
-import time
-
-HOST = "10.0.0.1"
-SSH = ["ssh", "-o", "BatchMode=yes", "-o", "ControlMaster=auto",
- "-o", "ControlPath=/tmp/zenith-locks-%r@%h:%p", "-o", "ControlPersist=30s", HOST]
-SEP = "--%%--"
-
-
-def agent_socket():
- """SSH_AUTH_SOCK is usually absent here; find a live agent that holds an identity."""
- pids = subprocess.run(["pgrep", "ssh-agent"], capture_output=True, text=True).stdout.split()
- for pid in pids:
- out = subprocess.run(["lsof", "-a", "-U", "-p", pid], capture_output=True, text=True).stdout
- for word in out.split():
- if "/agent." not in word:
- continue
- env = dict(os.environ, SSH_AUTH_SOCK=word)
- if subprocess.run(["ssh-add", "-l"], env=env, capture_output=True).returncode == 0:
- return word
- return None
-
-
-def connect():
- if subprocess.run(SSH + ["true"], capture_output=True).returncode == 0:
- return
- sock = agent_socket()
- if sock is None:
- sys.exit("no ssh-agent with a loaded identity; cannot reach %s" % HOST)
- os.environ["SSH_AUTH_SOCK"] = sock
- if subprocess.run(SSH + ["true"], capture_output=True).returncode != 0:
- sys.exit("ssh %s failed even with agent %s" % (HOST, sock))
-
-
-def status(*views):
- cmd = (" ; echo %s ; " % SEP).join("midclt call smb.status " + v for v in views)
- out = subprocess.run(SSH + [cmd], capture_output=True, text=True)
- if out.returncode != 0:
- sys.exit(out.stderr.strip() or "ssh failed")
- return [json.loads(chunk) for chunk in out.stdout.split(SEP + "\n")]
-
-
-def locks(byterange):
- """(share_path, file, pid, type, start, size) per held range."""
- for entry in byterange:
- for lk in entry["locks"]:
- yield (entry["share_path"], entry["file_name"], lk["server_id"]["pid"],
- lk["type"], lk["start"], lk["size"])
-
-
-def table(rows, headers):
- rows = [[str(c) for c in r] for r in rows]
- width = [max(len(h), *(len(r[i]) for r in rows)) for i, h in enumerate(headers)]
- for r in [headers] + rows:
- print(" ".join(c.ljust(w) for c, w in zip(r, width)).rstrip())
-
-
-def show_locks():
- byterange, openfiles, shares = status("BYTERANGE", "LOCKS", "SHARES")
- held = sorted(locks(byterange))
- if not held:
- print("no byte-range locks held")
- return
- machine = {s["server_id"]["pid"]: s["machine"] for s in shares}
- user = {}
- for f in openfiles:
- for o in f["opens"].values():
- user[(f["service_path"], f["filename"], o["server_id"]["pid"])] = o["username"]
- rows = []
- for share, name, pid, typ, start, size in held:
- who = user.get((share, name, pid), "?")
- rows.append(["%s/%s" % (os.path.basename(share), os.path.basename(name)), pid, who,
- machine.get(pid, "?"), start, size,
- {"W": "write", "R": "read"}.get(typ, typ)])
- table(rows, ["FILE", "PID", "USER", "CLIENT", "OFFSET", "LENGTH", "TYPE"])
-
-
-def show_opens():
- openfiles, shares = status("LOCKS", "SHARES")
- machine = {s["server_id"]["pid"]: s["machine"] for s in shares}
- rows = []
- for f in openfiles:
- share = os.path.basename(f["service_path"])
- for o in f["opens"].values():
- pid = o["server_id"]["pid"]
- rows.append(["%s/%s" % (share, f["filename"]), pid, o["username"],
- machine.get(pid, "?"), o["access_mask"]["text"] or "-",
- o["sharemode"]["text"] or "-", o["opened_at"][11:23]])
- if not rows:
- print("no open files")
- return
- table(sorted(rows), ["FILE", "PID", "USER", "CLIENT", "ACCESS", "SHAREMODE", "OPENED"])
-
-
-def watch(interval):
- prev = set()
- while True:
- now = set(locks(status("BYTERANGE")[0]))
- t = time.time()
- stamp = time.strftime("%H:%M:%S", time.localtime(t)) + ".%03d" % (t % 1 * 1000)
- for sign, changed in (("-", prev - now), ("+", now - prev)):
- for share, name, pid, typ, start, size in sorted(changed):
- print("%s %s %s/%s pid=%s %s %d+%d" %
- (stamp, sign, os.path.basename(share), os.path.basename(name), pid,
- {"W": "write", "R": "read"}.get(typ, typ), start, size), flush=True)
- prev = now
- time.sleep(interval)
-
-
-def main():
- args = sys.argv[1:]
- connect()
- if args and args[0] == "--watch":
- watch(float(args[1]) if len(args) > 1 else 0.5)
- elif args == ["--opens"]:
- show_opens()
- elif not args:
- show_locks()
- else:
- sys.exit("usage: zenith-locks [--watch [SECONDS] | --opens]")
-
-
-if __name__ == "__main__":
- try:
- main()
- except KeyboardInterrupt:
- pass