Files
udpscope/tools/check_help.sh
Martino Ferrari 6138e6bdce fix: the figures carried a chunk WebKit refuses, which is why a page never loaded
The user's report was precise and I misread it twice: opening trigger.page as a *file* rendered the
page with no image, while opening it through the app's `help:` URI spun forever. Both are one cause.
The three figures carried a `bKGD` chunk -- and the PNG specification does not permit `bKGD` on an
image with an alpha channel, which all three are (colour type 6). ImageMagick both writes and reads
it, so my "decodes fully" check proved nothing: I tested with a decoder more permissive than the one
that has to load the file. WebKit rejects it, so the image never appears; over `help:` the fetch
never completes either, so the page's load event never fires and Yelp's spinner never stops.

It came from `import`, which records the root window's background. Every figure now carries only the
chunks a decoder is guaranteed to accept (IHDR/IDAT/IEND), and `tools/make-help-shots.sh` normalises
its output at the end so a re-shoot cannot reintroduce it -- with the reason written where the next
reader will be, since "ImageMagick reads it" is exactly the trap.

`tools/check_help.sh` gains the rule, because the four yelp-check subcommands and the structural
rules are all blind to a figure no decoder will accept: a `bKGD` on colour type 4 or 6 is a finding,
checked directly rather than through a permissive tool. It is a whitelist because a decoder is one.

Verified: the rule fires on the committed figures (colour type 6, bKGD present) and passes on the
fixed ones; the figures still decode and are still 1280x800 / 938x420; check_help.sh is green on the
source and installed sets; make -C build test-help passes.
2026-10-15 00:01:52 +02:00

110 lines
4.6 KiB
Bash
Executable File

#!/bin/sh
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2026 Martino Ferrari
#
# The manual's check, the way tools/check_ui.py is the .ui files' check: it runs the four
# yelp-check subcommands that need no network, checks the structural rules below, checks the
# figures, then renders the whole set to HTML with yelp-build so a page that only fails at the
# transform step is caught too.
#
# `yelp-check validate` is deliberately absent, and it is worth being exact about why: it
# builds the RelaxNG schema locally with `xsltproc` and `mal-rng.xsl`, but the schema it
# generates includes the base grammar over the network, so on a machine where fetching is
# blocked it fails for that reason rather than for a fault in the pages.
#
# What that leaves unverified is *structure*, and that is not a small gap: three pages
# carrying a `<figure>` whose `<title>` was a sentence and which had no `<desc>` opened in
# nothing, while links, media, orphans, ids and the HTML render all passed. So the rules this
# project has actually needed are checked here. Add a rule when a page *teaches* one -- and do
# not add one you have not seen broken: a rule that fails a legal page is worse than no rule.
#
# Every XPath expression uses local-name() on purpose. A Mallard page declares
# `xmlns="http://projectmallard.org/1.0/"`, and XPath 1.0 has no default namespace, so
# `//figure` matches *nothing* and every rule below silently passes. Written naively, this
# check reported "ok" for the very defect it was written for.
#
# Exit status is nonzero on the first finding.
set -eu
dir=${1:?usage: check_help.sh HELP_DIR}
if [ ! -f "$dir/index.page" ]; then
echo "check_help: no index.page under $dir" >&2
exit 1
fi
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
for sub in links media orphans ids; do
printf 'yelp-check %s %s\n' "$sub" "$dir"
yelp-check "$sub" "$dir" >/dev/null
done
failed=0
for page in "$dir"/*.page; do
while IFS='|' read -r what expr; do
[ -n "$what" ] || continue
n=$(xmllint --xpath "$expr" "$page" 2>/dev/null || echo "?")
if [ "$n" != "0" ]; then
printf ' %s: %s (%s)\n' "$(basename "$page")" "$what" "$n" >&2
failed=1
fi
done <<'RULES'
a figure describes itself|count(//*[local-name()='figure'][not(*[local-name()='desc'])])
a section has a title|count(//*[local-name()='section'][not(*[local-name()='title'])])
info is the page's first child|count(/*[local-name()='page']/*[1][local-name()!='info'])
no text sits directly under page|count(/*[local-name()='page']/text()[normalize-space()])
a media names its src|count(//*[local-name()='media'][not(@src)])
RULES
done
# The figures, and this one is a *decoder's* rule rather than ours: a `bKGD` chunk is forbidden
# by the PNG specification on an image with an alpha channel, and `import` writes one because it
# records the root window's background. ImageMagick both writes and reads it, so a round trip
# through it proves nothing -- which is how three figures passed every check while WebKit
# refused them, showing a figure that never loaded and, over the `help:` URI, a page that spun
# forever. Only the chunks PNG allows for the colour type are accepted; a whitelist, because a
# decoder is one.
python3 - "$dir" <<'PY' || failed=1
import glob, os, struct, sys
seen = 0
for path in sorted(glob.glob(os.path.join(sys.argv[1], 'figures', '*.png'))):
seen += 1
with open(path, 'rb') as f:
if f.read(8) != b'\x89PNG\r\n\x1a\n':
print(f" {os.path.basename(path)}: not a PNG", file=sys.stderr)
continue
chunks = []
while True:
head = f.read(8)
if len(head) < 8:
break
length, kind = struct.unpack('>I4s', head)
f.read(length)
f.read(4)
chunks.append(kind)
# Colour type 4 (grey+alpha) and 6 (RGB+alpha) have an alpha channel; bKGD is not permitted
# on either. Anything else is left alone: this checks the rule a page was broken by.
colour_type = None
with open(path, 'rb') as f:
f.read(16)
colour_type = f.read(1)[0]
if colour_type in (4, 6) and b'bKGD' in chunks:
print(f" {os.path.basename(path)}: bKGD on an image with alpha (colour type {colour_type})"
" -- PNG forbids it and a strict decoder refuses the file", file=sys.stderr)
sys.exit(1)
if seen == 0:
print(" no figures to check", file=sys.stderr)
PY
if [ "$failed" != 0 ]; then
echo "check_help: findings above" >&2
exit 1
fi
printf 'structure %s: ok; figures: ok\n' "$dir"
mkdir -p "$tmp/html"
yelp-build html -o "$tmp/html" "$dir"
pages=$(find "$tmp/html" -maxdepth 1 -name '*.html' | wc -l)
printf 'yelp-build html: %s page(s) rendered\n' "$pages"