mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 07:39:08 +00:00
M-egui037: enforce control admission and per-family font notices
This commit is contained in:
1 parent
7dbb08827e
commit
d13dd1db20
7 files changed
+137
-11
No files matched your search
@@ -32,6 +32,9 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Windows font notice regressions
|
||||
shell: pwsh
|
||||
run: ./packaging/windows/test-font-notices.ps1
|
||||
- name: Git dependencies come from storytold
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -82,6 +82,8 @@ fn startup_deadline_expired() -> String {
|
||||
|
||||
/// One request from a client, answered on `reply`.
|
||||
pub struct ControlRequest {
|
||||
/// Admission time on the sending thread, including time waiting for the next app tick.
|
||||
pub since: web_time::Instant,
|
||||
pub method: String,
|
||||
pub params: Value,
|
||||
pub reply: Sender<Reply>,
|
||||
@@ -98,7 +100,7 @@ impl ControlClient {
|
||||
/// Queue a request; the reply arrives on the returned receiver once the app has handled it.
|
||||
pub fn send(&self, method: &str, params: Value) -> Receiver<Reply> {
|
||||
let (reply, rx) = channel();
|
||||
let req = ControlRequest { method: method.into(), params, reply };
|
||||
let req = ControlRequest { since: web_time::Instant::now(), method: method.into(), params, reply };
|
||||
if let Err(e) = self.tx.send(req) {
|
||||
let _ = e.0.reply.send(Err("the app has closed".into()));
|
||||
}
|
||||
@@ -620,8 +622,13 @@ impl Control {
|
||||
}
|
||||
while let Ok(req) = self.rx.try_recv() {
|
||||
// One admission timestamp per request: queued input, pending frames and an admitted
|
||||
// screenshot all measure their deadline from this arrival, never from a later phase.
|
||||
let since = web_time::Instant::now();
|
||||
// screenshot all measure their deadline from the sending thread, including time
|
||||
// waiting in this channel, never from a later phase.
|
||||
let since = req.since;
|
||||
if web_time::Instant::now().duration_since(since) > timeout() {
|
||||
let _ = req.reply.send(Err(if ready { startup_deadline_expired() } else { still_starting() }));
|
||||
continue;
|
||||
}
|
||||
if !ready {
|
||||
if self.deferred.len() < 32 {
|
||||
self.deferred.push_back(DeferredRequest { request: req, since });
|
||||
@@ -2384,6 +2391,23 @@ mod lifecycle_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_request_expired_in_the_channel_is_never_dispatched_or_deferred() {
|
||||
for ready in [false, true] {
|
||||
let ctx = egui::Context::default();
|
||||
let (mut control, client) = attach(&ctx);
|
||||
let mut host = ReadinessHost::new(ready);
|
||||
let (reply, waiting) = channel();
|
||||
let since = web_time::Instant::now().checked_sub(std::time::Duration::from_secs(10)).unwrap();
|
||||
client.tx.send(ControlRequest { since, method: "ui.state".into(), params: json!({}), reply }).unwrap();
|
||||
control.tick(&ctx, &mut host);
|
||||
let error = waiting.try_recv().expect("expired channel request replies on the first tick").expect_err("expired request cannot dispatch");
|
||||
assert!(error.contains(if ready { "finished starting" } else { "still starting" }), "{error}");
|
||||
assert_eq!(host.states.get(), 0);
|
||||
assert!(control.deferred.is_empty() && control.pending.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// A request held during startup keeps its arrival time. Once the app accepts input, one
|
||||
/// older than the control deadline fails instead of being dispatched into a later UI.
|
||||
#[test]
|
||||
|
||||
@@ -152,6 +152,12 @@ neither the MSI nor the portable zip needs the Visual C++ redistributable.
|
||||
OFL notices; `-SkipBuild` requires `CRAFT_FONTS_DIR` so they can be staged, or
|
||||
`-BuiltWithoutCraftFonts` for binaries built without that input, and `test-msi.ps1` checks that
|
||||
the compiled MSI has OFL notice rows when expected (and none for a no-font build).
|
||||
Before either package is created, `font-notices.ps1` validates every declared OFL notice in
|
||||
`fonts/manifest.txt` and the native `fonts/simplified-chinese-manifest.txt` when present
|
||||
(required when `CRAFT_FONTS_REQUIRED` is set). Missing or empty family notices fail packaging;
|
||||
one family's notice cannot cover another. The check conservatively includes declared faces
|
||||
omitted by build-time size caps. `test-font-notices.ps1` exercises these failures in CI.
|
||||
The supplied checkout must match the existing binaries when using `-SkipBuild`.
|
||||
That presence check does not establish byte-for-byte payload or installation coverage;
|
||||
final package qualification still checks all notices against the staged files.
|
||||
- **Signing:** `packaging/windows/sign.ps1` signs both executables and the MSI with `signtool`
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Validate every declared native font notice before either Windows package is created.
|
||||
# Checking all declared faces is deliberately conservative: size-capped faces may not be embedded.
|
||||
function Get-CraftFontNotices([string] $FontRoot, [bool] $RequireChinese = $false) {
|
||||
$rootPath = (Resolve-Path -LiteralPath $FontRoot).Path
|
||||
$notices = @{}
|
||||
foreach ($name in 'manifest.txt', 'simplified-chinese-manifest.txt') {
|
||||
$manifest = Join-Path $rootPath "fonts/$name"
|
||||
if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) {
|
||||
if ($name -eq 'manifest.txt' -or $RequireChinese) { throw "Missing font manifest: $manifest" }
|
||||
continue
|
||||
}
|
||||
foreach ($line in Get-Content -LiteralPath $manifest) {
|
||||
$line = $line.Trim()
|
||||
if (-not $line -or $line.StartsWith('#')) { continue }
|
||||
$fields = @($line -split ' \| ' | ForEach-Object { $_.Trim() })
|
||||
if ($fields.Count -lt 6 -or $fields[4] -ne 'OFL-1.1') { throw "Missing OFL metadata in ${manifest}: $line" }
|
||||
# Manifest notice paths must name a family-local file, never escape the font input.
|
||||
$relative = $fields[5].Replace('\', '/')
|
||||
if ($relative -notmatch '^fonts/([a-zA-Z0-9_-]+)/OFL\.txt$') { throw "Invalid font notice path: $relative" }
|
||||
$family = $Matches[1]
|
||||
$fontPath = $fields[2].Replace('\', '/')
|
||||
if ($fontPath -notmatch ('^fonts/' + [regex]::Escape($family) + '/[^/]+$')) {
|
||||
throw "Font and notice must belong to the same family directory: $fontPath / $relative"
|
||||
}
|
||||
$notice = Join-Path $rootPath $relative
|
||||
if (-not (Test-Path -LiteralPath $notice -PathType Leaf) -or (Get-Item -LiteralPath $notice).Length -eq 0) {
|
||||
throw "Missing or empty OFL notice for $($fields[0]): $notice"
|
||||
}
|
||||
$notices[$family] = Get-Item -LiteralPath $notice
|
||||
}
|
||||
}
|
||||
if ($notices.Count -eq 0) { throw "No declared font notices in $rootPath" }
|
||||
foreach ($family in @($notices.Keys | Sort-Object)) { $notices[$family] }
|
||||
}
|
||||
|
||||
# MSI FileName may be short|long. Compare the long names with the staged inventory.
|
||||
function Assert-CraftFontNoticeNames([string[]] $Expected, [string[]] $Actual) {
|
||||
$names = @($Actual | ForEach-Object { ($_ -split '\|')[-1] })
|
||||
foreach ($name in $Expected) {
|
||||
if ($name -notin $names) { throw "The MSI is missing staged font notice: $name" }
|
||||
}
|
||||
}
|
||||
@@ -128,15 +128,14 @@ $Docs = Join-Path $Stage 'font-licences'
|
||||
New-Item -ItemType Directory -Force -Path $Docs | Out-Null
|
||||
$HasCraftFontLicences = 0
|
||||
if ($env:CRAFT_FONTS_DIR) {
|
||||
foreach ($ofl in Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts\*\OFL.txt') -File -ErrorAction SilentlyContinue) {
|
||||
. (Join-Path $PSScriptRoot 'font-notices.ps1')
|
||||
# Validate both native manifests before copying anything. One family's notice cannot
|
||||
# stand in for another's; retain complete files, including copyright and reserved names.
|
||||
$fontNotices = @(Get-CraftFontNotices $env:CRAFT_FONTS_DIR (Test-Path Env:CRAFT_FONTS_REQUIRED))
|
||||
foreach ($ofl in $fontNotices) {
|
||||
Copy-Item -LiteralPath $ofl.FullName -Destination (Join-Path $Docs "OFL-$($ofl.Directory.Name).txt")
|
||||
$HasCraftFontLicences = 1
|
||||
}
|
||||
# CRAFT_FONTS_DIR means the build embeds its faces; packaging without their notices would be
|
||||
# a licence omission. Fail instead of silently shipping HasCraftFontLicences=0.
|
||||
if ($HasCraftFontLicences -ne 1) {
|
||||
throw "CRAFT_FONTS_DIR=$env:CRAFT_FONTS_DIR has no fonts\<family>\OFL.txt; refusing to package embedded craft-fonts without their licence notices"
|
||||
}
|
||||
}
|
||||
# ---- end font notices for the MSI --------------------------------------------------------------
|
||||
|
||||
@@ -151,7 +150,10 @@ Invoke-Native 'wix build' {
|
||||
}
|
||||
# Inspect the built MSI, not just the XML, before signing/publishing it. In a child process, so
|
||||
# its Windows Installer database handle is gone before signtool opens the MSI.
|
||||
Invoke-Native 'test-msi' { & (Get-Process -Id $PID).Path -NoProfile -File (Join-Path $PSScriptRoot 'test-msi.ps1') $Msi -CraftFontLicences $HasCraftFontLicences }
|
||||
Invoke-Native 'test-msi' {
|
||||
& (Get-Process -Id $PID).Path -NoProfile -File (Join-Path $PSScriptRoot 'test-msi.ps1') $Msi `
|
||||
-CraftFontLicences $HasCraftFontLicences -ExpectedFontNoticesDirectory $Docs
|
||||
}
|
||||
# wix writes its debug symbols (.wixpdb) next to the MSI; keep them out of the release assets.
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($Msi, '.wixpdb'))
|
||||
& (Join-Path $PSScriptRoot 'sign.ps1') $Msi
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
param([string] $CraftFontsRoot)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $PSScriptRoot 'font-notices.ps1')
|
||||
$fixture = Join-Path ([IO.Path]::GetTempPath()) ('pdfcraft-font-notices-' + [guid]::NewGuid())
|
||||
function Assert-Rejected([scriptblock] $Action, [string] $Message) {
|
||||
$rejected = $false
|
||||
try { & $Action | Out-Null } catch { $rejected = $true }
|
||||
if (-not $rejected) { throw "Expected rejection: $Message" }
|
||||
}
|
||||
try {
|
||||
foreach ($family in 'core', 'chinese') {
|
||||
New-Item -ItemType Directory -Path (Join-Path $fixture "fonts/$family") -Force | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $fixture "fonts/$family/OFL.txt") -Value 'Synthetic notice fixture'
|
||||
}
|
||||
$core = 'Core | Regular | fonts/core/face.ttf | Latn | OFL-1.1 | fonts/core/OFL.txt'
|
||||
$chinese = 'Chinese | Regular | fonts/chinese/face.ttf | Hans | OFL-1.1 | fonts/chinese/OFL.txt'
|
||||
Set-Content (Join-Path $fixture 'fonts/manifest.txt') @('# comment', $core, $core)
|
||||
Set-Content (Join-Path $fixture 'fonts/simplified-chinese-manifest.txt') $chinese
|
||||
if (@(Get-CraftFontNotices $fixture $true).Count -ne 2) { throw 'Expected both unique family notices' }
|
||||
Set-Content (Join-Path $fixture 'fonts/simplified-chinese-manifest.txt') ($chinese.Replace('fonts/chinese/OFL.txt', 'fonts/core/OFL.txt'))
|
||||
Assert-Rejected { Get-CraftFontNotices $fixture $true } 'another family cannot supply this notice'
|
||||
Set-Content (Join-Path $fixture 'fonts/simplified-chinese-manifest.txt') $chinese
|
||||
Remove-Item (Join-Path $fixture 'fonts/chinese/OFL.txt')
|
||||
Assert-Rejected { Get-CraftFontNotices $fixture $true } 'one remaining OFL cannot cover the Chinese family'
|
||||
Set-Content (Join-Path $fixture 'fonts/chinese/OFL.txt') '' -NoNewline
|
||||
Assert-Rejected { Get-CraftFontNotices $fixture $true } 'empty family notice'
|
||||
Set-Content (Join-Path $fixture 'fonts/simplified-chinese-manifest.txt') ($chinese.Replace('fonts/chinese/OFL.txt', '../OFL.txt'))
|
||||
Assert-Rejected { Get-CraftFontNotices $fixture $true } 'notice path escapes font input'
|
||||
Remove-Item (Join-Path $fixture 'fonts/simplified-chinese-manifest.txt')
|
||||
Assert-Rejected { Get-CraftFontNotices $fixture $true } 'required Chinese manifest missing'
|
||||
if (@(Get-CraftFontNotices $fixture).Count -ne 1) { throw 'Optional Chinese manifest may be absent' }
|
||||
Set-Content (Join-Path $fixture 'fonts/manifest.txt') 'malformed'
|
||||
Assert-Rejected { Get-CraftFontNotices $fixture } 'malformed metadata'
|
||||
Assert-CraftFontNoticeNames @('OFL-core.txt', 'OFL-chinese.txt') @('SHORT.TXT|OFL-core.txt', 'OFL-chinese.txt')
|
||||
Assert-Rejected { Assert-CraftFontNoticeNames @('OFL-core.txt', 'OFL-chinese.txt') @('OFL-core.txt') } 'MSI lost one staged notice'
|
||||
if ($CraftFontsRoot) {
|
||||
$actual = @(Get-CraftFontNotices $CraftFontsRoot $true)
|
||||
Write-Output "Verified $($actual.Count) notices from the actual native manifests"
|
||||
}
|
||||
Write-Output 'Font notice regression checks passed'
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $fixture -Recurse -Force
|
||||
}
|
||||
@@ -8,7 +8,8 @@ param(
|
||||
[Parameter(Mandatory)] [string] $Path,
|
||||
# 1 when package.ps1 staged craft-fonts OFL notices, 0 when the binaries embed no craft-fonts.
|
||||
# -1 (the default) skips the check when the MSI is inspected by hand.
|
||||
[int] $CraftFontLicences = -1
|
||||
[int] $CraftFontLicences = -1,
|
||||
[string] $ExpectedFontNoticesDirectory
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Installer = New-Object -ComObject WindowsInstaller.Installer
|
||||
@@ -93,6 +94,11 @@ if ($CraftFontLicences -eq 1 -and $fontNotices.Count -eq 0) {
|
||||
if ($CraftFontLicences -eq 0 -and $fontNotices.Count -gt 0) {
|
||||
throw "the MSI carries craft-font notices that packaging did not stage: $($fontNotices -join ', ')"
|
||||
}
|
||||
if ($ExpectedFontNoticesDirectory) {
|
||||
. (Join-Path $PSScriptRoot 'font-notices.ps1')
|
||||
$expected = @(Get-ChildItem -LiteralPath $ExpectedFontNoticesDirectory -File -Filter 'OFL-*.txt' | ForEach-Object { $_.Name })
|
||||
Assert-CraftFontNoticeNames $expected $fontNotices
|
||||
}
|
||||
$status = Read-Row 'SELECT `Text` FROM `Control` WHERE `Dialog_` = ''InstallProgress'' AND `Control` = ''Status''' 1
|
||||
Assert-Equal $status[0] 'Please wait while setup completes.' 'Persistent progress message'
|
||||
Assert-NoRow 'SELECT `Event` FROM `EventMapping` WHERE `Dialog_` = ''InstallProgress'' AND `Control_` = ''Status''' 'progress text subscription'
|
||||
|
||||
Reference in new issue
Block a user