1param(
2 [Parameter(Mandatory=$true)][string]$Root,
3 [int]$ExpectedPages = -1,
4 [switch]$UseCurrentCache,
5 [switch]$Pdf,
6 [switch]$KeepOpen,
7 [string]$CloneHost = ''
8)
9Set-StrictMode -Version Latest
10$ErrorActionPreference = 'Stop'
11$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
12if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
13 throw 'Choose a run directly below C:\one-tests\runs.'
14}
15$notebook = Join-Path $root 'notebook'
16$output = Join-Path $root 'read'
17if (Test-Path $output) { throw 'Choose a new read destination.' }
18if ($UseCurrentCache) {
19 if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
20 throw 'Park the personal OneNote profile before reading test notebooks.'
21 }
22} else {
23 & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
24}
25New-Item -ItemType Directory -Path $output | Out-Null
26$app = New-Object -ComObject OneNote.Application
27$notebookId = ''
28$failure = $null
29try {
30 $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
31 $process = Get-Process ONENOTE
32 @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
33 powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
34 ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
35 $sections = @()
36 foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
37 $id = ''
38 $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
39 $sections += $id
40 }
41 $deadline = [DateTime]::UtcNow.AddSeconds(300)
42 $previous = ''
43 $stableSince = [DateTime]::UtcNow
44 $settled = $false
45 do {
46 $pages = @{}
47 foreach ($section in $sections) {
48 $hierarchy = ''
49 $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
50 [xml]$xml = $hierarchy
51 foreach ($node in $xml.SelectNodes('//*[@path]')) {
52 if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
53 throw 'OneNote opened a section outside the copied notebook.'
54 }
55 }
56 foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
57 $id = $node.GetAttribute('ID')
58 $content = ''
59 $app.GetPageContent($id, [ref]$content, 1, 1)
60 $pages[$id] = $content
61 }
62 }
63 $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
64 if ($signature -ne $previous) {
65 $previous = $signature
66 $stableSince = [DateTime]::UtcNow
67 }
68 if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
69 ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
70 ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
71 Start-Sleep -Milliseconds 250
72 } while ([DateTime]::UtcNow -lt $deadline)
73 if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
74 $index = 0
75 foreach ($id in @($pages.Keys | Sort-Object)) {
76 [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
77 $index++
78 }
79 $hierarchy = ''
80 $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
81 [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
82 throw "Expected $ExpectedPages pages; OneNote returned $($pages.Count)."
83 }
84 $index = 0
85 $payloads = @()
86 foreach ($id in @($pages.Keys | Sort-Object)) {
87 [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
88 if ($Pdf) {
89 $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
90 $app.NavigateTo($id, '', $false)
91 $app.Publish($id, $pdfPath, 3, '')
92 if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
93 }
94 [xml]$page = $pages[$id]
95 foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
96 $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
97 $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
98 [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
99 $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
100 kind = $file.LocalName; name = $file.GetAttribute('preferredName');
101 sha256 = $hash; bytes = $bytes.Length }
102 }
103 $index++
104 }
105 [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
106 $all = ''
107 $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
108 [xml]$finalTree = $all
109 $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
110 if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
111 throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
112 }
113 [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
114 Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
115} catch {
116 $failure = $_
117 [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
118 throw
119} finally {
120 try {
121 try {
122 if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
123 if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
124 $app.CloseNotebook($notebookId, $false)
125 }
126 } catch {
127 if ($null -eq $failure) { throw }
128 [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
129 }
130 } finally {
131 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
132 }
133 $app = $null
134 [GC]::Collect()
135 [GC]::WaitForPendingFinalizers()
136 if (-not $UseCurrentCache -and -not $CloneHost) {
137 Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
138 }
139}