Skip to content

v0.13.3

Latest

Choose a tag to compare

@github-actions github-actions released this 02 Sep 12:06
· 4 commits to main since this release

Two missed errors reported by a user, and eleven wrong ones found while
fixing them and one other report.
Doitsu's report (MobileRead #266) named an
empty dc:creator and <meta http-equiv>; both are real and both are closed
here. Neither resets the false-positive counter — a missed error harms nobody
who has it. But looking at the two rules that were supposed to cover them turned
up ten false positives in rules that were already shipping, and DNSB's
attached files (#268) turned up an eleventh. Those are the part of this release
a reader can actually meet.

One of them is not rare: <meta http-equiv="content-type" content="text/html;charset=utf-8"/>, without the space, was an error here and
is valid everywhere else. It occurs 860 times across the local 444-book shelf.

The encoding declaration's value was compared, not matched

epubcheck asks this with one Schematron assertion,
matches(normalize-space(@content),'text/html;\s*charset=utf-8','i'), and every
part of that expression carries weight. We compared the raw attribute value to
the single string "text/html; charset=utf-8", case-insensitively — a
different test in three ways at once: no whitespace normalization, no \s*, and
anchored to the whole value.

Seven spellings epubcheck accepts were errors here, one book each against 5.3.0:

@content before now
text/html;charset=utf-8 RSC-005 accepted
text/html; charset=utf-8 RSC-005 accepted
text/html;<TAB>charset=utf-8 RSC-005 accepted
text/html; charset=utf-8 RSC-005 accepted
text/html; charset=utf-8 RSC-005 accepted
xxx text/html; charset=utf-8 RSC-005 accepted
text/html; charset=utf-8 xxx RSC-005 accepted

The last two look wrong and are not: matches() asks whether the pattern occurs
anywhere in the value, not whether it is the whole of it. The boundary is
unchanged in the other direction — a space before the semicolon,
charset=utf8, application/xhtml+xml; charset=utf-8 and an empty value are
still errors, as they are there.

The shelf could not see this, and the reason is worth keeping. All 860
occurrences of the unspaced spelling sit in EPUB 2 books, and this rule is EPUB
3 only — of the seven EPUB 3 books on the shelf that carry http-equiv at all,
every one spells the declaration the one way the old comparison accepted.

NO-BREAK SPACE is not XML whitespace

XML, XSD's whiteSpace facet and XPath's normalize-space() all mean exactly
four characters: space, tab, CR and LF. Rust's split_whitespace means
Unicode's whitespace class, which also swallows NO-BREAK SPACE — so a value
containing nothing but &#160; read as empty here and did not there.

Three more false positives, one book each: <dc:title>&#160;</dc:title>,
<dc:identifier>&#160;</dc:identifier> and a &#160;-only <meta property>
were all errors here and are all valid to epubcheck. The fix is in the XPath
engine's normalize-space(), so every Schematron rule built on it moves with
it, and in the one hand-coded sibling.

Zero books on the 444-book shelf carry such a value, so this is a boundary fix
rather than a visible one — but it is the shape that would have added a
thirteenth false positive to the dc:* work below had it not been found first.

<meta http-equiv> was not checked at all

Reported by Doitsu with four cases, of which we answered none: http-equiv was
granted to every EPUB 3 element with any value. epubcheck asks three
separate questions, and all three are now asked (measured one book each):

  • it is a <meta> attribute, not a global one. <p http-equiv="refresh">
    is "attribute not allowed here";
  • its value is one of five pragma directives, case-insensitively —
    content-type, refresh, default-style, content-security-policy,
    x-ua-compatible. foo is an error, and so is the plausible-looking
    Content-Style-Type;
  • @content is then required. A bare <meta http-equiv="refresh"/> is
    "element "meta" missing required attribute "content"".

X-UA-Compatible is valid, contrary to the first reading of the report:
epubcheck's schema defines it as one of the five and accepts it. <meta charset>
came along with it — the XML serialization permits utf-8 in any case and
nothing else.

What is deliberately not copied is epubcheck's disjointness. It models
<meta> as four alternatives that cannot mix, so name beside http-equiv, a
<meta name> with no @content, or a bare <meta/> are errors there and are
accepted here. Those are false negatives, measured and left: they are a
different family from the reported one, and every extra restriction on the
commonest element in a <head> carries its own false-positive risk.

All fifteen dc:* elements must be non-empty, not three

package-30.rnc types every Dublin Core element datatype.string.nonempty.
We had three of them — dc:identifier, dc:title, dc:language — so an empty
dc:creator, the case Doitsu reported, drew nothing. The other twelve are now
covered: creator, contributor, publisher, description, subject,
source, type, format, relation, coverage, rights and date. An
empty dc:date keeps its OPF-053 as well, which is what epubcheck does.

EPUB 3 only, like its three siblings: EPUB 2's opf20.rng permits an empty
value there, where the finding is OPF-072 at usage level and was already
reported.

Known and left, because closing it means reproducing epubcheck's error
recovery rather than its rules: for a dc:* element containing a child
element, epubcheck reports the rejected child and a second "incomplete;
expected data". We report the child only. Our count is lower, never higher.

A fatal OPF was told its unique-identifier matched nothing

DNSB reported a disagreement (MobileRead #268) with two output files, and the
files themselves were the first finding: his EPUB 3 export has an unclosed
<spine>
, epubcheck gives the same FATAL(RSC-016) on it that we do, and the
two .txt files had been produced from different states of the book. Repaired
with one </spine>, the two tools agree exactly — same five findings, same ids,
same files.

What his file did show is ours. Beside the fatal we reported OPF-030: package unique-identifier 'BookId' does not match any dc:identifier id, on an OPF whose
<dc:identifier id="BookId"> sits four lines above the fault. A fatal leaves us
with no tree, so the recovery added in 0.13.0 (issue #126) resolved the
unique-identifier against an empty identifier list — and an empty list
after a fatal means "the parse stopped", not "the book declares none". We were
asserting absence from ignorance.

epubcheck answers it by stream position: it reports OPF-030 only where its
handler had not yet passed a matching <dc:identifier> start tag. We now decide
the same question from how far our parser got — does the unique-identifier
value appear anywhere in the bytes read before the fault. That needs no
namespace resolution, so it is not the second parser the recovery was written to
avoid, and it errs toward silence. Eight books measured against 5.3.0, one per
fatal position, and it agrees with epubcheck on all eight, including both of
epubcheck's own malformed-OPF fixtures, where the fault sits above the
identifier and OPF-030 is correct.

The missing-attribute half is untouched and still runs after a fatal: RSC-005,
OPF-048 and OPF-030 together when @unique-identifier is absent. That one is
read off the root start tag, so its absence is knowledge rather than ignorance.

Zero of the 444 shelf books have a malformed OPF, so no instrument here could
have found this. It took a user's file.

One message improved from the same file, where the two outputs sat side by
side on the same line: an empty <ol> in a navigation document now reads
element "ol" incomplete; missing required element "li", as epubcheck's does
and as its own <li> sibling already did. Same id, same position, same count —
it simply said less than it knew.

ADV-010: an EPUB 2 manifest resource nothing draws, applies or loads

JSWolf asked for "a warning for any images not being used" (MobileRead #221).
It turns out not to be a missing check but a missing version: epubcheck's
OPF-097 asks exactly that question, and asks it of EPUB 3 books only. We match
it there exactly — five reference shapes probed one book each, img src, CSS
background-image, link rel=stylesheet, link rel=icon, link rel=Preview,
and the two tools agree on all five. On an EPUB 2 book neither tool says
anything
, which is why the check looked missing to JSWolf and present to
Doitsu: they validate different versions and both were right about their own
books.

Reporting where epubcheck is silent is the restrictive direction, so this is
opt-in behind --advisory, carries an id of ours rather than epubcheck's, and
never moves the verdict or the exit code — verified across all 444 shelf
books, where not one changes its exit code with the flag on.

It gets its own id rather than reusing OPF-097 for a concrete reason: OPF-097
is usage severity and usage is hidden unless you pass -u, so reusing it
would have required both flags. ADV-* findings are shown by --advisory
alone.

Measured before it was written, by removing the version gate and counting
over the shelf:

  • 90 of 362 EPUB 2 books (25%), 197 findings. Median one per book; 64 of
    the 90 have exactly one. It discriminates — an advisory that fires on nearly
    every book teaches people never to pass the flag.
  • 143 of the 197 verified by hand, one at a time: 138 of the file names
    occur nowhere else in the book at all. Four of the five that did are true
    findings anyway — three books declare a second, dead cover.jpg beside the
    real one, and one declares on_D.jpg while its page draws on_D_fmt.jpeg.
  • The 143rd is a <link rel="Preview"> Word artefact, and epubcheck reports
    OPF-097 for that shape too
    . So the wording here is chosen to be true of it
    as well: nothing draws, applies or loads the file.
  • The <meta name="cover"> target is not exempt, and that was counted
    rather than assumed: 6 of 143, 4%. Nothing draws those either, the EPUB 3
    path does not exempt properties="cover-image", and neither does epubcheck.

Not restricted to images, though images are 128 of the 143 — a dead stylesheet
or an orphan XHTML is the same question with the same accuracy.

One limit worth stating plainly: both editor plugins run with --advisory
off by default, so this reaches a plugin user only if they switch it on.

The two editor plugins are named in our own documentation at last

Doitsu's Sigil and calibre plugins had been in both official indexes since
2026-08-20, and our own docs never mentioned either — a reader who does not use
a terminal was told to pick one of eight archives and get past a first-run
security prompt. USAGE.md now offers the plugins as Option 2, before the
work they let a reader skip, and the README names them twice.

Two things it says plainly, because a link would otherwise read as an
endorsement: the plugins are not ours (Doitsu's, GPL-3, bug reports to his
forum threads; our only claim is that we read both sources and reported what we
found), and both hide usage and --advisory by default, so a reader whose
book looks cleaner in the editor than on the command line knows why.

INTEGRATING.md roughly doubled in the same pass, from reading both plugin
sources in full. Six of the nine things that pass found were our documentation's
fault rather than theirs — -u gated at fetch time makes the summary describe
the flag instead of the book; tool_version is already in the envelope, so
neither plugin needed to spawn epubveri -V; our own buried advice pointed at
musl where both plugins pick gnu; data.advisory_basis was documented only
in the WASM section; location is a full path; and exit code 2 has three
shapes where we had documented one
— a bad command line answers on stderr,
while a missing file and an unnameable input both return a full envelope with
stderr empty. The rule is now published: parse stdout first, fall back to
stderr only when there is no envelope. There is also a recipe for verifying a
downloaded archive against SHA256SUMS.txt, run against v0.13.2 in both
directions — neither plugin verifies what it downloads, so 0.12.4's checksums
and attestation were reaching nobody.

Measured

633 tests, corpus 603/603 exact-ID with 0 false positives, epubtests
byte-identical to the previous release, diff-shelf.sh unchanged over 444 real
books, hostile clean. compare over all 981 corpus books: 899 agree on the ID
set exactly
, with one id only we report — the documented CHK-008/RSC-001
pair (issue #127) — against 830 and sixteen at the last recorded run. Over the
five malformed-OPF fixtures alone, agreement went from one to three.

For consumers

Two new rule keys. opf.package.dc_value_not_empty, whose params[0] is the
element name (dc:creator) — the same shape opf.metadata.empty_element uses
for the EPUB 2 half of the same question; its findings are RSC-005 errors that
were not reported at all before. And
opf.manifest_item.never_referenced_epub2, carrying ADV-010 with the href in
params[0]; it is deliberately not the EPUB 3 site's
opf.manifest_item.never_referenced, so a consumer can tell our advisory from
epubcheck's finding without reading the version. It appears only under
--advisory.

No existing key, message or severity moves.

Full notes: CHANGELOG.md. Also on crates.io.