diff --git a/corpus/picture-edit/README.md b/corpus/picture-edit/README.md index d4793d321000015fda144dd5745d11cbf10c2985..dff677ee50977d55b8098d7c17b27ee03bdb848d 100644 --- a/corpus/picture-edit/README.md +++ b/corpus/picture-edit/README.md @@ -1,6 +1,6 @@ # Rust picture authoring -`candidate/` is the page writer's output for +`inserted/candidate` is the page writer's output for `a_picture_inserted_on_a_fresh_page_reads_back_and_can_be_removed` in `crates/onestore/tests/page_images.rs`: on a section created in Rust, a paragraph between two text paragraphs becomes a picture, stored the way @@ -10,11 +10,27 @@ object, a picture-container file object declaring it by identity and extension, and a picture object with the displayed size that the paragraph holds as content. OneNote's own picture objects also carry a DPAPI-protected blob (`0x1c001dfb`) that only the authoring Windows user can decrypt; the -writer omits it. +writer omits it. `inserted/cold` is a fresh OneNote 2010 read: the image +reads back as `format="png"` with the exact payload bytes, between the two +text paragraphs. -`cold/` is a fresh OneNote 2010 read: the image reads back as `format="png"` -with the exact payload bytes, between the two text paragraphs. -`tools/test_picture_edit.py` checks this without a VM. Regenerate with -`ONESTORE_IMAGE_EXPORT` set to a new absolute directory while running the -test, then cold-open it with `tools/native_runner.py OUTPUT COLD ---expected-pages 1 --collect-notebook`. +`native-resize` is OneNote 2010 editing that Rust-written picture +(`tools/native/picture-edit.ps1` on `inserted/candidate`): the picture gets +a 144 by 108 point user-set size and alternative text through the COM API +(`before.xml`, `update.xml`), and `read/` is the read after the edit. In +`notebook/`, OneNote stored the size as the picture object's layout width +and height with the user flag (`0x14001c1b`, `0x14001c1c`, `0x08001cbd`) and +left the intrinsic size (`0x140034cd/ce`) alone; it also rewrote the picture +container under a native identity with its DPAPI blob and hash. + +`resized/candidate` is the writer doing the same on the native picture of +`cold-05-05-image` (`a_native_picture_is_resized_and_described_then_reset`): +layout width, height, user flag and alternative text on the picture object. +`resized/cold` is its cold read with the size and description. + +`tools/test_picture_edit.py` checks all three without a VM. Regenerate the +candidates with `ONESTORE_IMAGE_EXPORT` and `ONESTORE_IMAGE_RESIZE_EXPORT` +set to new absolute directories while running the tests, then cold-open +them with `tools/native_runner.py OUTPUT COLD --expected-pages 1 +--collect-notebook`; regenerate `native-resize` with `--author +tools/native/picture-edit.ps1` on `inserted/candidate`. diff --git a/corpus/picture-edit/candidate/Open Notebook.onetoc2 b/corpus/picture-edit/candidate/Open Notebook.onetoc2 deleted file mode 100644 index 79f8f955db3b58cd2ae471c00ae2b66c95f81f62..0000000000000000000000000000000000000000 Binary files a/corpus/picture-edit/candidate/Open Notebook.onetoc2 and /dev/null differ diff --git a/corpus/picture-edit/candidate/pictures.one b/corpus/picture-edit/candidate/pictures.one deleted file mode 100644 index af4169c68f7c76ead1f32f25685131438a9fc7a1..0000000000000000000000000000000000000000 Binary files a/corpus/picture-edit/candidate/pictures.one and /dev/null differ diff --git a/corpus/picture-edit/cold/commands.jsonl b/corpus/picture-edit/cold/commands.jsonl deleted file mode 100644 index f077978e124b796017ac989959a7b5e48d27684c..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/commands.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"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-A5A8705B -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/picture-edit/cold/machine.json b/corpus/picture-edit/cold/machine.json deleted file mode 100644 index 80a47e2a0dd156babc8d6e6c895da3a3144fd28a..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/machine.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "m6-a5a8705b", "hostname": "ONE-M6-A5A8705B"} diff --git a/corpus/picture-edit/cold/notebook/Open Notebook.onetoc2 b/corpus/picture-edit/cold/notebook/Open Notebook.onetoc2 deleted file mode 100644 index 836282818b6fe532b91bf32edf78fba4fe160b7a..0000000000000000000000000000000000000000 Binary files a/corpus/picture-edit/cold/notebook/Open Notebook.onetoc2 and /dev/null differ diff --git a/corpus/picture-edit/cold/notebook/pictures.one b/corpus/picture-edit/cold/notebook/pictures.one deleted file mode 100644 index 0dd87dcf4b41be4ca1b5b4b921d9462358aae3b1..0000000000000000000000000000000000000000 Binary files a/corpus/picture-edit/cold/notebook/pictures.one and /dev/null differ diff --git a/corpus/picture-edit/cold/read/environment.json b/corpus/picture-edit/cold/read/environment.json deleted file mode 100644 index f1032fa5ff0c8dddcee05b60f81366ec2671b1f9..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/read/environment.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "powershell": "5.1.14409.1005", - "schema": "xs2010", - "hostname": "ONE-M6-A5A8705B", - "cold": true, - "onenote": "14.0.4763.1000" -} diff --git a/corpus/picture-edit/cold/read/hierarchy.xml b/corpus/picture-edit/cold/read/hierarchy.xml deleted file mode 100644 index dce86fad6744777127a6c19a6146cadb358f799f..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/read/hierarchy.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/corpus/picture-edit/cold/read/page-000.xml b/corpus/picture-edit/cold/read/page-000.xml deleted file mode 100644 index aa8840aa5f2fb3e14fe2cb7223dc8638d73f1a92..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/read/page-000.xml +++ /dev/null @@ -1,4 +0,0 @@ - -iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w -AAAAAElFTkSuQmCC - diff --git a/corpus/picture-edit/cold/read/payloads.json b/corpus/picture-edit/cold/read/payloads.json deleted file mode 100644 index 1ce91a413d6fff4d58765187b76756f0e97ae843..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/read/payloads.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - -] \ No newline at end of file diff --git a/corpus/picture-edit/cold/run.json b/corpus/picture-edit/cold/run.json deleted file mode 100644 index 6e98c47fb0e8f3f15888fbc77851de46e6db6301..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/run.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "notebook": "/private/tmp/m4/pictures", - "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/picture-edit/cold/scripts/cold.ps1 b/corpus/picture-edit/cold/scripts/cold.ps1 deleted file mode 100644 index a0b99e7aeb86f4dc10032537800501a52ad65b4f..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/scripts/cold.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -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/picture-edit/cold/scripts/read.ps1 b/corpus/picture-edit/cold/scripts/read.ps1 deleted file mode 100644 index 476094ab1e0c47077988a6461db2080cf208642b..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/scripts/read.ps1 +++ /dev/null @@ -1,142 +0,0 @@ -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/picture-edit/cold/source.json b/corpus/picture-edit/cold/source.json deleted file mode 100644 index 006d1122e0e6b26381b4b3c32834b6beb5866625..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/source.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "path": "Open Notebook.onetoc2", - "bytes": 1604, - "sha256": "0eddd691b52d26bac222cf7e52add3c9909446647b452fb193f03d49e273426a", - "mtime_ns": 1789243635388246438 - }, - { - "path": "pictures.one", - "bytes": 7064, - "sha256": "3076880e02cf74eb85400852c133060ab8ed50383c6b9df8a07bde8389891a70", - "mtime_ns": 1789243635387879438 - } -] diff --git a/corpus/picture-edit/cold/teardown.json b/corpus/picture-edit/cold/teardown.json deleted file mode 100644 index 05a47793de40f322e745c4c4183727e3590ecf70..0000000000000000000000000000000000000000 --- a/corpus/picture-edit/cold/teardown.json +++ /dev/null @@ -1 +0,0 @@ -{"absent": true} diff --git a/corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2 b/corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..79f8f955db3b58cd2ae471c00ae2b66c95f81f62 Binary files /dev/null and b/corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2 differ diff --git a/corpus/picture-edit/inserted/candidate/pictures.one b/corpus/picture-edit/inserted/candidate/pictures.one new file mode 100644 index 0000000000000000000000000000000000000000..af4169c68f7c76ead1f32f25685131438a9fc7a1 Binary files /dev/null and b/corpus/picture-edit/inserted/candidate/pictures.one differ diff --git a/corpus/picture-edit/inserted/cold/commands.jsonl b/corpus/picture-edit/inserted/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..f077978e124b796017ac989959a7b5e48d27684c --- /dev/null +++ b/corpus/picture-edit/inserted/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-A5A8705B -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/picture-edit/inserted/cold/machine.json b/corpus/picture-edit/inserted/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..80a47e2a0dd156babc8d6e6c895da3a3144fd28a --- /dev/null +++ b/corpus/picture-edit/inserted/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-a5a8705b", "hostname": "ONE-M6-A5A8705B"} diff --git a/corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2 b/corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..836282818b6fe532b91bf32edf78fba4fe160b7a Binary files /dev/null and b/corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/picture-edit/inserted/cold/notebook/pictures.one b/corpus/picture-edit/inserted/cold/notebook/pictures.one new file mode 100644 index 0000000000000000000000000000000000000000..0dd87dcf4b41be4ca1b5b4b921d9462358aae3b1 Binary files /dev/null and b/corpus/picture-edit/inserted/cold/notebook/pictures.one differ diff --git a/corpus/picture-edit/inserted/cold/read/environment.json b/corpus/picture-edit/inserted/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..f1032fa5ff0c8dddcee05b60f81366ec2671b1f9 --- /dev/null +++ b/corpus/picture-edit/inserted/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-A5A8705B", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/picture-edit/inserted/cold/read/hierarchy.xml b/corpus/picture-edit/inserted/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..dce86fad6744777127a6c19a6146cadb358f799f --- /dev/null +++ b/corpus/picture-edit/inserted/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/picture-edit/inserted/cold/read/page-000.xml b/corpus/picture-edit/inserted/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..aa8840aa5f2fb3e14fe2cb7223dc8638d73f1a92 --- /dev/null +++ b/corpus/picture-edit/inserted/cold/read/page-000.xml @@ -0,0 +1,4 @@ + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w +AAAAAElFTkSuQmCC + diff --git a/corpus/picture-edit/inserted/cold/read/payloads.json b/corpus/picture-edit/inserted/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/picture-edit/inserted/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/picture-edit/inserted/cold/run.json b/corpus/picture-edit/inserted/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..6e98c47fb0e8f3f15888fbc77851de46e6db6301 --- /dev/null +++ b/corpus/picture-edit/inserted/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/pictures", + "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/picture-edit/inserted/cold/scripts/cold.ps1 b/corpus/picture-edit/inserted/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/picture-edit/inserted/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/picture-edit/inserted/cold/scripts/read.ps1 b/corpus/picture-edit/inserted/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/picture-edit/inserted/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/picture-edit/inserted/cold/source.json b/corpus/picture-edit/inserted/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..006d1122e0e6b26381b4b3c32834b6beb5866625 --- /dev/null +++ b/corpus/picture-edit/inserted/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1604, + "sha256": "0eddd691b52d26bac222cf7e52add3c9909446647b452fb193f03d49e273426a", + "mtime_ns": 1789243635388246438 + }, + { + "path": "pictures.one", + "bytes": 7064, + "sha256": "3076880e02cf74eb85400852c133060ab8ed50383c6b9df8a07bde8389891a70", + "mtime_ns": 1789243635387879438 + } +] diff --git a/corpus/picture-edit/inserted/cold/teardown.json b/corpus/picture-edit/inserted/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/picture-edit/inserted/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/picture-edit/native-resize/before.xml b/corpus/picture-edit/native-resize/before.xml new file mode 100644 index 0000000000000000000000000000000000000000..2f41ea80baf7882e9a64a22c1681073dc963fb1f --- /dev/null +++ b/corpus/picture-edit/native-resize/before.xml @@ -0,0 +1,4 @@ + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w +AAAAAElFTkSuQmCC + diff --git a/corpus/picture-edit/native-resize/commands.jsonl b/corpus/picture-edit/native-resize/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..74e3124d0078ba8d0441bca1bd2cdcf0a982ad48 --- /dev/null +++ b/corpus/picture-edit/native-resize/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-3BE178F1", "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-3BE178F1 -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/picture-edit/native-resize/machine.json b/corpus/picture-edit/native-resize/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..2e39ea03be694091b4dc6f0da279a9afadc90614 --- /dev/null +++ b/corpus/picture-edit/native-resize/machine.json @@ -0,0 +1 @@ +{"name": "m6-3be178f1", "hostname": "ONE-M6-3BE178F1"} diff --git a/corpus/picture-edit/native-resize/notebook/Open Notebook.onetoc2 b/corpus/picture-edit/native-resize/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..86e88105642e35612d1f763f3b60ec98f580397f Binary files /dev/null and b/corpus/picture-edit/native-resize/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/picture-edit/native-resize/notebook/pictures.one b/corpus/picture-edit/native-resize/notebook/pictures.one new file mode 100644 index 0000000000000000000000000000000000000000..93dd8f4a7aafdc3a8acb415f99f8186d4f6584b6 Binary files /dev/null and b/corpus/picture-edit/native-resize/notebook/pictures.one differ diff --git a/corpus/picture-edit/native-resize/read/environment.json b/corpus/picture-edit/native-resize/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..86e4ad7fdad6944b936c59b65548ccb0d9fd272f --- /dev/null +++ b/corpus/picture-edit/native-resize/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-3BE178F1", + "cold": false, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/picture-edit/native-resize/read/hierarchy.xml b/corpus/picture-edit/native-resize/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..4beee5e1266a09e9aa953eb88ce2a08b5b08832f --- /dev/null +++ b/corpus/picture-edit/native-resize/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/picture-edit/native-resize/read/page-000.xml b/corpus/picture-edit/native-resize/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..2ffa250df34a4a2024b61d80ad78e577a00fef0b --- /dev/null +++ b/corpus/picture-edit/native-resize/read/page-000.xml @@ -0,0 +1,4 @@ + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w +AAAAAElFTkSuQmCC + diff --git a/corpus/picture-edit/native-resize/read/payloads.json b/corpus/picture-edit/native-resize/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/picture-edit/native-resize/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/picture-edit/native-resize/run.json b/corpus/picture-edit/native-resize/run.json new file mode 100644 index 0000000000000000000000000000000000000000..52ab401ffc7c29caa752562d112bc1be65c5b0e6 --- /dev/null +++ b/corpus/picture-edit/native-resize/run.json @@ -0,0 +1,19 @@ +{ + "notebook": "/Users/clo/dev/one/corpus/picture-edit/candidate", + "expected_pages": 1, + "author": "tools/native/picture-edit.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": "ac663d020a6049b4b688aed1be4ae2661a59fff25d83fe7c5eaabbf14d357f55", + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/picture-edit/native-resize/scripts/author.ps1 b/corpus/picture-edit/native-resize/scripts/author.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..ca0597f1475c8e4cccbb45fb4ed95c28eded912d --- /dev/null +++ b/corpus/picture-edit/native-resize/scripts/author.ps1 @@ -0,0 +1,49 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $sectionId = '' + $app.OpenHierarchy('pictures.one', $notebookId, [ref]$sectionId, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(30) + $page = $null + do { + $hierarchy = '' + $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $pages = @($tree.SelectNodes('//*[local-name()="Page"]')) + if ($pages.Count -eq 1) { + $content = '' + $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) + [xml]$page = $content + if (@($page.SelectNodes('//*[local-name()="Image"]')).Count -eq 1) { break } + } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + $images = @($page.SelectNodes('//*[local-name()="Image"]')) + if ($images.Count -ne 1) { throw 'Expected one picture on the page.' } + [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8) + $image = $images[0] + $image.SetAttribute('alt', 'Resized by OneNote') + $existing = $image.SelectSingleNode('*[local-name()="Size"]') + if ($existing) { [void]$image.RemoveChild($existing) } + $size = $page.CreateElement('one', 'Size', $namespace) + $size.SetAttribute('width', '144') + $size.SetAttribute('height', '108') + $size.SetAttribute('isSetByUser', 'true') + [void]$image.PrependChild($size) + [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8) + $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/corpus/picture-edit/native-resize/scripts/cold.ps1 b/corpus/picture-edit/native-resize/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/picture-edit/native-resize/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/picture-edit/native-resize/scripts/read.ps1 b/corpus/picture-edit/native-resize/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/picture-edit/native-resize/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/picture-edit/native-resize/source.json b/corpus/picture-edit/native-resize/source.json new file mode 100644 index 0000000000000000000000000000000000000000..0613aac0d637eacd23e38a986eb8d4aed828b657 --- /dev/null +++ b/corpus/picture-edit/native-resize/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1604, + "sha256": "0eddd691b52d26bac222cf7e52add3c9909446647b452fb193f03d49e273426a", + "mtime_ns": 1789243969388188834 + }, + { + "path": "pictures.one", + "bytes": 7064, + "sha256": "3076880e02cf74eb85400852c133060ab8ed50383c6b9df8a07bde8389891a70", + "mtime_ns": 1789243969387957378 + } +] diff --git a/corpus/picture-edit/native-resize/teardown.json b/corpus/picture-edit/native-resize/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/picture-edit/native-resize/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/picture-edit/native-resize/update.xml b/corpus/picture-edit/native-resize/update.xml new file mode 100644 index 0000000000000000000000000000000000000000..db5fa9154539ae2d26e03c0b6846f2364198c7cd --- /dev/null +++ b/corpus/picture-edit/native-resize/update.xml @@ -0,0 +1,3 @@ +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w +AAAAAElFTkSuQmCC + \ No newline at end of file diff --git a/corpus/picture-edit/resized/candidate/Open Notebook.onetoc2 b/corpus/picture-edit/resized/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..e26ea04d75bcdb38ca618ae744185e70d732a9f9 Binary files /dev/null and b/corpus/picture-edit/resized/candidate/Open Notebook.onetoc2 differ diff --git a/corpus/picture-edit/resized/candidate/synthetic.one b/corpus/picture-edit/resized/candidate/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..b97fd387030660493eadd435f7e110486261d358 Binary files /dev/null and b/corpus/picture-edit/resized/candidate/synthetic.one differ diff --git a/corpus/picture-edit/resized/cold/commands.jsonl b/corpus/picture-edit/resized/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..b129b8c37fa51742fcc27d106ba6f1d66c561bec --- /dev/null +++ b/corpus/picture-edit/resized/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-49185CAA -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/picture-edit/resized/cold/machine.json b/corpus/picture-edit/resized/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..00e839ceff043e0ae7dea19da900eb1137d7c256 --- /dev/null +++ b/corpus/picture-edit/resized/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-49185caa", "hostname": "ONE-M6-49185CAA"} diff --git a/corpus/picture-edit/resized/cold/notebook/Open Notebook.onetoc2 b/corpus/picture-edit/resized/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..5a459c9ca3f178e3040db2cfcfdc84b5c2df8958 Binary files /dev/null and b/corpus/picture-edit/resized/cold/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/picture-edit/resized/cold/notebook/synthetic.one b/corpus/picture-edit/resized/cold/notebook/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..891840859470c11d04c2c1f6d5cb52c2d314b440 Binary files /dev/null and b/corpus/picture-edit/resized/cold/notebook/synthetic.one differ diff --git a/corpus/picture-edit/resized/cold/read/environment.json b/corpus/picture-edit/resized/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..e9d185b2369bb479300d211d73afa7d1b9e2abd5 --- /dev/null +++ b/corpus/picture-edit/resized/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-49185CAA", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/picture-edit/resized/cold/read/hierarchy.xml b/corpus/picture-edit/resized/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..9e177b446447ecf78e96cae03078203a9ce860eb --- /dev/null +++ b/corpus/picture-edit/resized/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/picture-edit/resized/cold/read/page-000.xml b/corpus/picture-edit/resized/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a01b58e0bdf3bfbb936aa060b91c9d68f2e6959 --- /dev/null +++ b/corpus/picture-edit/resized/cold/read/page-000.xml @@ -0,0 +1,8 @@ + +Fictitious: café, 東京, مرحبا]]>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9J1uoA +AAAASUVORK5CYII= diff --git a/corpus/picture-edit/resized/cold/read/payloads.json b/corpus/picture-edit/resized/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/picture-edit/resized/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/picture-edit/resized/cold/run.json b/corpus/picture-edit/resized/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..05ca5a00ecf0c4d8938a7f46adbeb3a09b9aa8b2 --- /dev/null +++ b/corpus/picture-edit/resized/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/resized", + "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/picture-edit/resized/cold/scripts/cold.ps1 b/corpus/picture-edit/resized/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/picture-edit/resized/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/picture-edit/resized/cold/scripts/read.ps1 b/corpus/picture-edit/resized/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/picture-edit/resized/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/picture-edit/resized/cold/source.json b/corpus/picture-edit/resized/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..3cb2a274f69d8527485a3001b80cc1fe0c60a876 --- /dev/null +++ b/corpus/picture-edit/resized/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1612, + "sha256": "32c3fa727c56069e1c5e885e237b95dad32762b4c2ef269a2b0e566fa534382c", + "mtime_ns": 1789247968519334299 + }, + { + "path": "synthetic.one", + "bytes": 19040, + "sha256": "6cedb887b23e772353a266850d6bbf4f0a0e8ff6d0e6f513b2a1a8af8b2a37d8", + "mtime_ns": 1789247968518969882 + } +] diff --git a/corpus/picture-edit/resized/cold/teardown.json b/corpus/picture-edit/resized/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/picture-edit/resized/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/crates/notebook/README.md b/crates/notebook/README.md index d2fb85623a5db93250338f7956b8880ced069b87..136c30cd4e9c4cff1989b0a3e532432d396e83d4 100644 --- a/crates/notebook/README.md +++ b/crates/notebook/README.md @@ -14,7 +14,7 @@ diffs the supplied model against the page stored in the supplied local snapshot, writes the difference into the working image and queues one `Operation::Page(PageIntent { before, after, author })`; `before` is the page the edit started from and is the precondition reconciliation checks. Text, styles, -paragraph formatting, hyperlinks, bullets, numbering, note tags, table rows and columns, pictures and attachments, paragraph structure, +paragraph formatting, hyperlinks, bullets, numbering, note tags, table rows and columns, pictures (insertion, size, description) and attachments, paragraph structure, outline layout, insertions and deletions are all differences between `before` and `after`; the library never sees editor operations. diff --git a/crates/onestore/README.md b/crates/onestore/README.md index 784e3a7dee3818756ef49d8d618e946c16045bd1..2952cbffa9748b6362fef6a86437ee71eb4c5398 100644 --- a/crates/onestore/README.md +++ b/crates/onestore/README.md @@ -57,7 +57,7 @@ harness also accepts `--client-profile release`. | `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | | `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | -| `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks, bullets and numbering, note tags, table rows and columns, inserted pictures and attachments, paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | +| `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks, bullets and numbering, note tags, table rows and columns, inserted pictures and attachments, picture size and description, paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | | `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically | | `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter | | `read_file` | Read a snapshot under whole-file exclusion | diff --git a/crates/onestore/src/page/write.rs b/crates/onestore/src/page/write.rs index b7654c07f92eb4706936f8dd6b1da8cdf1907a7c..2cddf00e9329d72913ab52318a62b45054a7c370 100644 --- a/crates/onestore/src/page/write.rs +++ b/crates/onestore/src/page/write.rs @@ -1,7 +1,7 @@ //! Publishes an edited page model by lowering the difference from the stored page onto //! the typed writers, then squashing their transactions into one revision per space. -use super::{Outline, Page, PageObject, PageParagraph, ParagraphContent, Table}; +use super::{Image, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table}; use crate::{ Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets, RevisionIndex, Store, TextAttribute, TreeEdit, Value, @@ -386,8 +386,15 @@ impl Lowering<'_> { } (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {} (ParagraphContent::Image(a), ParagraphContent::Image(b)) => { - if a != b { - return Err(invalid("A stored picture cannot be edited")); + if a.id != b.id + || a.bytes != b.bytes + || a.size != b.size + || a.background != b.background + || (a.layout.x, a.layout.y) != (b.layout.x, b.layout.y) + { + return Err(invalid( + "A stored picture keeps its payload, intrinsic size, background state and position", + )); } } (ParagraphContent::Attachment(a), ParagraphContent::Attachment(b)) => { @@ -753,8 +760,11 @@ impl Lowering<'_> { continue; }; if let Some(previous) = old.paragraphs.get(paragraph_id) { - if previous.content != paragraph.content { - return Err(invalid("A stored picture cannot be edited")); + let ParagraphContent::Image(stored) = &previous.content else { + return Err(invalid("Paragraph content type cannot change")); + }; + if stored != image { + self.resize_image(stored, image)?; } continue; } @@ -824,6 +834,53 @@ impl Lowering<'_> { Ok(()) } + /// Writes a displayed size and description the way OneNote stores a resized picture: + /// the layout width and height with the user flag, leaving the intrinsic size alone. + fn resize_image(&mut self, stored: &Image, image: &Image) -> Result<(), Error> { + let mut values: Values = vec![( + 0x14001d7a, + crate::create::current_timestamps()? + .0 + .to_le_bytes() + .to_vec(), + )]; + let mut removed = Vec::new(); + if image.layout != stored.layout { + match (image.layout.max_width, image.layout.max_height) { + (Some(width), Some(height)) => { + if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) { + return Err(invalid("Picture size must be positive")); + } + values.push((0x14001c1b, (width / 36.0).to_le_bytes().to_vec())); + values.push((0x14001c1c, (height / 36.0).to_le_bytes().to_vec())); + let user_set = u32::from(image.layout.width_set_by_user == Some(true)); + values.push((0x08001cbd | (user_set << 31), Vec::new())); + } + (None, None) => removed.extend([0x14001c1b, 0x14001c1c, 0x08001cbd]), + _ => return Err(invalid("A picture size needs both dimensions")), + } + } + if image.alt != stored.alt { + match &image.alt { + Some(alt) => values.push((0x1c001e58, crate::create::string(alt))), + None => removed.push(0x1c001e58), + } + } + let (space, object) = (self.space, self.id(image.id)); + self.apply(|current| { + crate::write::write_revision(current, space, |raw| { + let mut picture = PropertyObject::from_object(&raw.objects[&object])?; + picture.remove(&removed)?; + let values: Vec<(u32, &[u8])> = values + .iter() + .map(|(id, bytes)| (*id, bytes.as_slice())) + .collect(); + picture.set(&values)?; + Ok(BTreeMap::from([(object, picture)])) + }) + }) + } + fn place_containers( &mut self, old: &View<'_>, diff --git a/crates/onestore/tests/page_images.rs b/crates/onestore/tests/page_images.rs index 44e7ea1a603e2741f8c0b45145c39f4bfb13a848..5d32d6225b666a61ce6f11ddd7e6453303b53b71 100644 --- a/crates/onestore/tests/page_images.rs +++ b/crates/onestore/tests/page_images.rs @@ -1,12 +1,14 @@ use onestore::{ ExGuid, PreparedEdit, RevisionIndex, Store, - document::{Document, Format}, + document::{Document, Format, Layout}, page::{Image, Page, PageObject, PageParagraph, ParagraphContent, TextObject, text::new_id}, }; use std::sync::Arc; const NATIVE: &[u8] = include_bytes!("../../../corpus/native/cold-05-05-image/notebook/synthetic.one"); +const NATIVE_RESIZE: &[u8] = + include_bytes!("../../../corpus/picture-edit/native-resize/notebook/pictures.one"); const AUTHOR: &str = "Picture author"; /// A one-pixel PNG. const PNG: &[u8] = &[ @@ -155,6 +157,85 @@ fn a_picture_inserted_on_a_fresh_page_reads_back_and_can_be_removed() { assert_eq!(body_paragraphs(&mut stored.clone()).len(), 2); } +fn resize(page: &mut Page, layout: Layout, alt: Option<&str>) -> ExGuid { + let mut found = None; + for object in &mut page.objects { + if let PageObject::Outline(outline) = object { + for paragraph in &mut outline.paragraphs { + if let ParagraphContent::Image(image) = &mut paragraph.content { + image.layout = layout.clone(); + image.alt = alt.map(str::to_owned); + assert!(found.replace(image.id).is_none()); + } + } + } + } + found.unwrap() +} + +/// `ONESTORE_IMAGE_RESIZE_EXPORT` names a new directory receiving the candidate for a cold reopen. +#[test] +fn a_native_picture_is_resized_and_described_then_reset() { + let (space, native) = first_page(NATIVE); + let mut resized = native.clone(); + let layout = Layout { + max_width: Some(144.0), + width_set_by_user: Some(true), + max_height: Some(108.0), + ..Default::default() + }; + resize(&mut resized, layout, Some("Resized in Rust")); + let written = PreparedEdit::page(NATIVE, space, &resized, AUTHOR).unwrap(); + let stored = page_in(written.as_bytes(), space); + assert_eq!(stored, resized); + assert_eq!( + PreparedEdit::page(written.as_bytes(), space, &stored, AUTHOR) + .unwrap() + .as_bytes(), + written.as_bytes() + ); + if let Some(directory) = std::env::var_os("ONESTORE_IMAGE_RESIZE_EXPORT") { + let directory = std::path::PathBuf::from(directory); + std::fs::create_dir(&directory).unwrap(); + std::fs::write(directory.join("synthetic.one"), written.as_bytes()).unwrap(); + let written_store = Store::parse(written.as_bytes()).unwrap(); + std::fs::write( + directory.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents( + "Open Notebook.onetoc2", + &[("synthetic.one", written_store.header.file_id)], + ) + .unwrap(), + ) + .unwrap(); + } + let mut reset = stored.clone(); + resize(&mut reset, Layout::default(), None); + let again = PreparedEdit::page(written.as_bytes(), space, &reset, AUTHOR).unwrap(); + assert_eq!(page_in(again.as_bytes(), space), native); +} + +#[test] +fn a_rust_picture_resized_natively_reads_back_with_its_size_and_description() { + let (space, page) = first_page(NATIVE_RESIZE); + let found = pictures(&page); + let [picture] = found.as_slice() else { + panic!("{page:?}"); + }; + assert_eq!(picture.bytes.as_deref(), Some(PNG)); + assert_eq!(picture.size, Some([0.75, 0.75])); + assert_eq!(picture.layout.max_width, Some(144.0)); + assert_eq!(picture.layout.max_height, Some(108.0)); + assert_eq!(picture.layout.width_set_by_user, Some(true)); + assert_eq!(picture.alt.as_deref(), Some("Resized by OneNote")); + let mut wider = page.clone(); + let mut layout = picture.layout.clone(); + layout.max_width = Some(200.0); + resize(&mut wider, layout, Some("Wider in Rust")); + let written = PreparedEdit::page(NATIVE_RESIZE, space, &wider, AUTHOR).unwrap(); + assert_eq!(page_in(written.as_bytes(), space), wider); +} + #[test] fn pictures_need_a_recognised_payload_and_stored_ones_stay_fixed() { let source = onestore::create_section("pictures.one", "Text", "Author").unwrap(); diff --git a/tools/native/picture-edit.ps1 b/tools/native/picture-edit.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..ca0597f1475c8e4cccbb45fb4ed95c28eded912d --- /dev/null +++ b/tools/native/picture-edit.ps1 @@ -0,0 +1,49 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $sectionId = '' + $app.OpenHierarchy('pictures.one', $notebookId, [ref]$sectionId, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(30) + $page = $null + do { + $hierarchy = '' + $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $pages = @($tree.SelectNodes('//*[local-name()="Page"]')) + if ($pages.Count -eq 1) { + $content = '' + $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) + [xml]$page = $content + if (@($page.SelectNodes('//*[local-name()="Image"]')).Count -eq 1) { break } + } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + $images = @($page.SelectNodes('//*[local-name()="Image"]')) + if ($images.Count -ne 1) { throw 'Expected one picture on the page.' } + [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8) + $image = $images[0] + $image.SetAttribute('alt', 'Resized by OneNote') + $existing = $image.SelectSingleNode('*[local-name()="Size"]') + if ($existing) { [void]$image.RemoveChild($existing) } + $size = $page.CreateElement('one', 'Size', $namespace) + $size.SetAttribute('width', '144') + $size.SetAttribute('height', '108') + $size.SetAttribute('isSetByUser', 'true') + [void]$image.PrependChild($size) + [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8) + $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/tools/test_picture_edit.py b/tools/test_picture_edit.py index f2227113405fec3023555b78d436dd16fef85ede..3abb872b16f7e0240e121825c56f6901a52feda8 100644 --- a/tools/test_picture_edit.py +++ b/tools/test_picture_edit.py @@ -14,20 +14,44 @@ FIXTURE = ROOT / 'corpus/picture-edit' compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] +def native_page(notebook, cold): + with TemporaryDirectory() as temporary: + read = Path(temporary) / 'read' + shutil.copytree(cold / 'read', read) + compare(notebook, read) + page, = (ET.parse(path).getroot() for path in sorted((cold / 'read').glob('page-*.xml'))) + return page + + +def texts(page): + return [re.sub(r'<[^>]*>', '', t.text or '') for t in page.findall('.//one:OE/one:T', ns)] + + class PictureEditTest(unittest.TestCase): def test_a_rust_inserted_picture_renders_natively_with_its_payload(self): - with TemporaryDirectory() as temporary: - read = Path(temporary) / 'read' - shutil.copytree(FIXTURE / 'cold/read', read) - compare(FIXTURE / 'candidate', read) - page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / 'cold/read').glob('page-*.xml'))) + page = native_page(FIXTURE / 'inserted/candidate', FIXTURE / 'inserted/cold') image, = page.iter('{%s}Image' % ns['one']) self.assertEqual(image.get('format'), 'png') payload = base64.b64decode(image.find('one:Data', ns).text) self.assertEqual(payload[:8], b'\x89PNG\r\n\x1a\n') self.assertEqual(len(payload), 69) - texts = [re.sub(r'<[^>]*>', '', t.text or '') for t in page.findall('.//one:OE/one:T', ns)] - self.assertEqual(texts, ['Before the picture', 'After the picture']) + self.assertIsNone(image.find('one:Size', ns)) + self.assertEqual(texts(page), ['Before the picture', 'After the picture']) + + def test_onenote_resizes_a_rust_inserted_picture_through_its_layout(self): + page = native_page(FIXTURE / 'native-resize/notebook', FIXTURE / 'native-resize') + image, = page.iter('{%s}Image' % ns['one']) + size = image.find('one:Size', ns) + self.assertEqual((size.get('width'), size.get('height'), size.get('isSetByUser')), ('144.0', '108.0', 'true')) + self.assertEqual(image.get('alt'), 'Resized by OneNote') + self.assertEqual(len(base64.b64decode(image.find('one:Data', ns).text)), 69) + + def test_a_rust_resized_native_picture_renders_with_its_size_and_description(self): + page = native_page(FIXTURE / 'resized/candidate', FIXTURE / 'resized/cold') + image, = page.iter('{%s}Image' % ns['one']) + size = image.find('one:Size', ns) + self.assertEqual((size.get('width'), size.get('height'), size.get('isSetByUser')), ('144.0', '108.0', 'true')) + self.assertEqual(image.get('alt'), 'Resized in Rust') if __name__ == '__main__':