Skip to content

test(pdf-codec): work toward a genuine 100% mutation score - #1270

Draft
Mearman wants to merge 92 commits into
mainfrom
feat/100-percent-mutation-pdf-codec
Draft

test(pdf-codec): work toward a genuine 100% mutation score#1270
Mearman wants to merge 92 commits into
mainfrom
feat/100-percent-mutation-pdf-codec

Conversation

@Mearman

@Mearman Mearman commented Sep 13, 2026

Copy link
Copy Markdown
Member

Working through pdf-codec's Stryker mutation baseline toward a genuine 100% score, no suppression comments.

First landed change: the unit suite's crypto and font tests were timing out under Stryker's instrumented dry run on this shared, heavily-loaded dev machine, not because of a defect in any one test but because instrumentation overhead scales with contention this machine has a lot of. Replaced scattered per-test timeout overrides with a single package-wide UNIT_TEST_TIMEOUT_MS.

Baseline mutation score and breakThreshold derivation still to come once a full run completes.

@Mearman
Mearman force-pushed the feat/100-percent-mutation-pdf-codec branch 4 times, most recently from 3dbbf36 to aa136a6 Compare September 14, 2026 08:44
…-machine contention

Individual crypto and font tests carried per-test timeout overrides (60s,
then briefly 300s) sized against an isolated, lightly-loaded run. Under
Stryker's mutation-instrumented dry run, contended against this shared
machine's other concurrent work, both AES-256 key-derivation tests and an
unrelated CFF font-parsing test missed timeouts far larger than their own
uninstrumented cost, proving the slowdown is general contention rather
than any one test's own logic.

Replace the scattered per-test overrides with a single UNIT_TEST_TIMEOUT_MS
applied to the whole unit project in vitest.config.ts, carried through
explicitly into vitest.mutation.config.ts since that file replaces the
base config's test block rather than merging into it. Raise pdf-codec's
dryRunTimeoutMinutes so the whole dry run has room for several worst-case
tests landing in the same run.
… time

The instrumented unit suite's heaviest tests (AES-256 key derivation, the
whole-Unicode-range glyphId enumeration) are already measured to exceed
their own generous timeout under this shared machine's contention;
running several Stryker workers concurrently multiplies that same
contention rather than avoiding it. Scoped to this package's own
stryker.config.ts, per PackageStrykerOptions.concurrency, rather than
lowering the workspace-wide default every other package's mutation run
would then pay for.
…surface

buildSfnt's and buildGsubTable's tag-writing loops iterated a hardcoded
4-character bound and wrote each byte manually; since a Uint8Array
coerces an out-of-range charCodeAt(4) to 0 and the byte was already 0,
an off-by-one bound mutation was byte-for-byte indistinguishable from
the original. Replace both loops with TextEncoder().encode(tag) plus a
single Uint8Array.set, which has no bound to mutate at all.

buildContextFormat1/buildContextFormat2 always passed empty backtrack
and lookahead arrays into the shared chained/non-chained SequenceRule
builder, but the non-chained branch never reads them -- the arrays
were live but their contents unobservable. Split the builder into
buildSequenceRuleBytes (plain, input only) and
buildChainSequenceRuleBytes (backtrack/input/lookahead), so the
non-chained callers no longer construct fields nothing consumes.

buildCmapTable indexed a parallel `encoded` array by position and
guarded the lookup with a throw for undefined, even though the array
is built by mapping over `subtables` one-to-one and can never actually
be short. Zip spec and encoded bytes into one array up front and
iterate that instead, removing the unreachable guard entirely.

buildGdefTable computed `sets` from `markGlyphSets ?? []` unconditionally,
but the fallback only matters when markGlyphSets is undefined, which is
exactly when the value is never read (the IIFE that reads it only runs
when markGlyphSets is defined). Pass markGlyphSets directly into the
IIFE instead of materialising the fallback.
gsub-table.test.ts and gdef-table.test.ts only exercise these builders
indirectly through a real reader, which tolerates a wrong offset or
operator as long as the resulting bytes still parse into something
plausible. Add direct tests against every builder's raw output --
buildSfnt's directory records, all three cmap subtable formats
(including sorting mappings given out of insertion order and a
non-power-of-two segCount for format 4's searchRange/entrySelector/
rangeShift), post v2/v3, coverage/single-subst/ligature sorting and
byte placement, the plain vs chained SequenceRule bodies, format 3's
chained and plain layouts, GSUB's per-feature table placement and
lookup markFilteringSet width across all three flag cases, and GDEF's
v1.0/v1.2 header selection -- closing the coverage/precision gap the
indirect tests left around each builder's own arithmetic.

Also removes buildGdefTable's now-redundant `withSets` variable,
missed when the previous commit collapsed its two `=== undefined`
checks into one.
…tant gaps

buildCoverageFormat2 had no direct test at all, leaving every field
write and the running coverageIndex accumulation across ranges
unverified. Add a test with two ranges asserting the second range's
coverage index carries the first range's real glyph count forward.

buildFormat0's explicit view.setUint16(0, 0) wrote a value the buffer
already held from Uint8Array's own zero-initialization -- removing the
call changes nothing observable, so delete it rather than leave a
mutation target with no real behaviour to test.

putSequenceRuleTail's return value was unused by its one caller,
leaving the arithmetic that computed it untestable by construction.
Inline it into buildSequenceRuleBytes (its only caller) and drop the
dead return entirely, since a "shared" tail with exactly one caller
was never actually shared.

Strengthen the markFilteringSet lookup tests to check the lookup's own
byte length and the untouched subtable content, not just the recorded
offsets -- a wrongly-forced markFilteringSetWidth can leave the
recorded offsets self-consistent while still corrupting or
mis-sizing the bytes that follow. Also assert feature 0's own
lookupIndices values in the multi-feature layout test, previously only
checked for feature 1.
The KSA's state[i] = i loop bounded i < STATE_SIZE, but a typed array
silently drops an out-of-range integer-index write -- state[256] = 256
on a 256-entry Uint8Array is a no-op -- so a loop bound mutated to
i <= STATE_SIZE produced byte-for-byte the same state array, an
equivalent mutant no test could ever distinguish. Uint8Array.from's own
length argument builds the identical array with no comparison operator
for a mutation to target.
The big-endian bit-length loop stopped early once bitLength reached 0,
via `i < lengthBytes && bitLength > 0`. padded is already zero-filled,
so writing 0 % 256 into the remaining length-field bytes is a no-op,
and a JS number's own 2^53 precision ceiling never needs more than 7 of
SHA-256's 8 (or SHA-512's 16) length bytes to represent -- no reachable
message ever runs the loop far enough for the bitLength > 0 half of the
guard to be what stops it. Drop it and let the loop run its full
lengthBytes iterations unconditionally.
Every magnitude that could round to the literal string "-0" at
NUMBER_DECIMAL_PLACES -- including -0 itself -- already satisfies
abs(n) < NUMBER_EPSILON and returns "0" from the guard above, since
NUMBER_EPSILON is exactly one unit in the last of those decimal places.
toFixed can only produce "-0.0000" for a magnitude below half that
unit, which is caught by the same guard. The ternary comparing the
stripped string against "-0" was therefore dead code no input could
ever reach.
…y method

buildEncryptor's own method parameter is already narrowed to
Extract<CipherMethod, "rc4" | "aes"> (every SCHEME_SPECS entry only
ever carries one of those two), so the "identity" branch inside
applyEncryptMethod could never be reached through any real call path
in this module. Narrow its parameter to match and delete the dead
branch, rather than leave a comparison no test could ever exercise.
isBlack computed (a + b) % 2 === 0 from a = x/2|0 and b = y/2|0. Sum and
difference of two integers always share the same parity, so an
ArithmeticOperator mutation to a - b produces byte-for-byte the same
checkerboard no decoded bitmap could ever distinguish. Compare (a & 1)
against (b & 1) directly instead, leaving no arithmetic operator for
that mutation to target.
…apping

The outer per-component loop bounded c < fixture.componentCount, but an
off-by-one bound there would silently append one extra all-zero plane
(every index inside it reads past the end of `bytes`, and `?? 0`
swallows the resulting undefined) -- a difference visible only in the
returned array's own length, which nothing calling this helper actually
re-checks. Build the planes with Array.from's own length argument
instead of a counted for-loop, removing the vulnerable comparison
outright.
The existing test only checked the thrown value's constructor, leaving
both string literals passed to the DOMException constructor
unverified.
The existing CFF2 case (header size 5, no valid Name INDEX past it)
also fails for reasons unrelated to the majorVersion check, so removing
that check entirely left the test still passing. Add a case with
majorVersion 2 but an otherwise valid CFF 1.0 layout -- headerSize 4, a
readable Name INDEX, a plain Top DICT -- where the version check is the
only thing standing between it and a wrongly-defined probe result.
…trics

parseHhea's numberOfHMetrics === 0 guard had no test forcing it: every
existing case used a real font whose hhea always declares at least one
metric. Patch Carlito's own hhea table directly (a new patchU16InTable
helper alongside the existing dropTable/truncateTable) to zero that
field and confirm loadEmbeddedFace refuses the font.
Nothing exercised the columns <= 0 || rows <= 0 early return, including
the negative-rows case, which the ||-composed guard treats identically
to zero.
The round-trip fixture never included a form field, leaving
LayoutFormFieldSchema's own z.enum(fieldType) array (and every other
field on the schema) unparsed by any test in this file. Add a text
field and a group with one nested checkbox child to the fixture.
Neither had a test: no call ever passed an explicit level to deflate
(so the { level } object literal it builds had no coverage), and
MAX_INFLATE_OUTPUT_BYTES's throw was unreached by any real input. Mock
unzlibSync's return value for the size-guard case rather than actually
decompressing half a gigabyte on every one of this suite's mutation
runs -- the guard only ever reads the result's .length.
readFilespec's own missing-stream warning had no test: every existing
filespec fixture either resolved a real embedded stream or never
declared /EF at all. Add a catalog /AF entry whose /EF resolves to an
empty dict, and assert both the dropped attachment and the emitted
diagnostic.
…rals

Four literals -- the empty stream dict "<< >>", marked-content "EMC",
PDF version "1.4", and the Helvetica font dict -- were each retyped
verbatim at every one of dozens of call sites across independent
fixture functions. Since every copy is its own separate string literal
to the type checker (and to Stryker's mutation testing, its own
separate mutation target), a single fixture author typo in any one
copy would silently diverge from the rest with nothing to catch it.

Name each one once (EMPTY_DICT, EMC, PDF_1_4, HELVETICA_FONT_DICT) and
reference it everywhere it recurred, matching the existing
HELLO_CONTENT constant's own pattern. Also drop the redundant explicit
.header("1.7") argument at call sites that were only ever restating
FixtureBuilder.header's own default value.
isTrueTypeCollection's hasBytes(bytes, 0, 4) && u32(...) check had two
survivable mutants: forcing either side to a bare `true` made every
parse failure misreport as a TrueType Collection, and the existing
"generic parse failure" test could not catch it because the source
label it asserted on ("not-a-font.bin") also appears verbatim inside
the TTC message. Assert the actual generic wording instead, and add a
buffer too short to hold even the 4-byte tag -- hasBytes' own job is
keeping that case from ever reaching u32, which would throw past this
file's bounds rather than yield a clean FontFaceParseError.

Also assert FontFaceParseError's own .name, which nothing checked.
parseHmtx had no test file at all -- every existing exercise of it went
through embedded-font.ts's own guard, which already refuses a font
before hmtx's zero-metrics and missing-table throws could ever run.
Cover advance-width lookup, the last-entry fallback for glyph IDs past
numberOfHMetrics, both missing-table throws, and the zero-metrics
throw directly against hand-built hhea/hmtx bytes.
…fallback

Every existing font fixture named an explicit /BaseFont, leaving the ??
"Helvetica" default unreachable for both the simple and composite font
builders. Also cover readCidWidths' malformed-leading-operand recovery
(i++; continue), which had no test where a /W array actually contained
a non-numeric c/cFirst entry.
…case

The one existing filled-double-stroke test never set fillRule, so the
evenodd -> "f*" branch had no coverage. Also cover averageNormal's
zero-length case directly: an open path that goes out and immediately
reverses along the same line gives its shared vertex two exactly
opposite chord normals, which sum to the zero vector rather than a
divide-by-zero -- that vertex stays at its original coordinates on
both offset copies while the two open ends still move along their own
single chord's normal.
decodeGenericRegion/decodeRefinementRegion had no test file of their
own; every exercise came through jbig2.ts's own segment parser, which
always masks GBTEMPLATE/GRTEMPLATE to the 2-bit/1-bit range the real
template tables cover -- their own out-of-range guards were dead from
that one call path. Both functions are exported, so a direct caller is
not bound by that masking; call each with an out-of-range template
directly and assert the resulting Jbig2UnsupportedError.
Every existing embedding test used the vendored Carlito Regular, an
upright design, leaving the italicAngleDegrees !== 0 branch untested.
Build the same object group from the vendored Caladea Italic instead
and assert the descriptor's own Flags carries the ITALIC bit.
Every exported fixture function feeds FixtureBuilder well-formed dicts
and object numbers that genuinely exist, leaving its own /Length-
insertion regex, xref padding, offsetOf's misuse guard, and the
maxObjNum arithmetic in /Size and the xref subsection header with no
route to direct coverage. Export the class and test it against
adversarial input directly: a nested dict to prove /Length lands at
the true end rather than the first '>>' encountered, dicts with no or
trailing whitespace around the final '>>' to prove the anchor and
quantifier are both load-bearing, a fixed-width offset assertion to
prove padStart's zero-pad character actually pads, and an unwritten
object number to prove offsetOf's guard actually throws.

Also remove FixtureBuilder's rawBytes method, which no fixture in this
file has ever called.
…nd-built programs

The vendored STIX Two Math font is a well-formed program from a real
font toolchain, so its charstrings never reach execute()'s or
executeEscaped()'s own interpreter limits and malformed-input paths:
subroutine nesting past the spec's own depth limit, a glyph whose
operator count runs past the per-glyph ceiling, an operand stack
overrun, a hintmask whose mask bytes run past the end of the
charstring, a reserved operator byte, callsubr/callgsubr with no index
on the stack or no matching subroutine, and endchar's own four-argument
seac-like form. Add cffFontWithCharstrings, a fixture builder that
wraps caller-supplied charstrings (and an optional Private DICT with a
Local Subrs INDEX) in an otherwise real CFF program, and drive every
one of those paths directly, plus the success path of a glyph drawn
through a real local subroutine and an implicit vstem list ahead of a
hintmask.
…a fake GlyfTable

The vendored Carlito face is a well-formed program from a real font
toolchain, so decodeSimpleContours' own malformed-input paths and
decodeOutline's composite-recursion limit never arise from walking it:
a glyph truncated before its end-point array, end points that do not
strictly increase, a glyph truncated before its flags array, a repeat
flag with no count byte or a count that overruns the point total, a
short- or long-form X/Y coordinate truncated before its own bytes, a
composite chain recursing past the depth limit, an unreadable
component list, a point-matched component, and a nested component's
own decode failure propagating up through its parent.

Add a fake GlyfTable that supplies simple-glyph bytes and composite
component records directly, sidestepping both the real sfnt/glyf
container and the composite record's own byte format, since
decodeGlyphOutline reaches both only through GlyfTable's interface.
Also cover the point-to-contour assignment directly with a hand-built
two-contour glyph, which the real-font tests above only ever exercise
incidentally.
…mInfo fields

Extracts embedded-font-write.test.ts's own assemblePdf/AllocatedObject helper into
test-support/write-pdf-fixture.ts so math-font-write.test.ts can build the same kind of
fixture without duplicating it, dropping the unreachable "object never written" guard in the
process: both current callers number their objects contiguously from 1, so the offset can be
recorded inline as each object is written rather than looked up afterwards from a map that
could theoretically miss.

Also asserts the embedded CIDFontType2's own CIDSystemInfo Registry and Ordering decode to
"Adobe" and "Identity" -- previously only Supplement was checked, leaving the two string
literals with no test proving their actual content.
… and ToUnicode filtering

math-font-write.ts had no test file at all (0% branch/function coverage): nothing exercised
buildMathFontObjects, so its Type0/CIDFontType0 shape, its FontDescriptor's design-unit-to-
glyph-space scaling, its /W array's own sort-by-glyph-ID, its FontFile3 compression, or its
dropping of code-point-less glyphs from the ToUnicode CMap had ever run.

Uses a synthetic MathFont with a non-1000 unitsPerEm (2048, a power of two so every scaled
value is an exactly representable double) for the descriptor arithmetic, since the real
vendored STIX Two Math font is drawn on a 1000-unit em and would make the scale factor an
identity -- indistinguishable from a font with no scaling applied at all.
…nd stroke items

The existing suite only ever exercised the "assembled-glyphs" item kind (54% statement
coverage, 0% of writeGlyphRun/writeRule/writeStroke). Adds direct coverage of the other three
MathLayoutItem kinds: an ordinary glyph run's own CID encoding (including skipping a character
with no glyph in the font's cmap, and emitting nothing when every character is unmapped), a
filled rule's top-left-to-bottom-edge re-anchoring, and a stroke's moveto/lineto sequence
(including the under-two-points no-op case).
…cycle guard

navigation.ts's own destination and outline logic (74% statement, 53% branch coverage) had
several branches no fixture-based test ever reached: five of the eight display types
(FitH/FitV/FitR/FitB/FitBH/FitBV), a bare non-negative-integer page number, a page element the
page-index lookup can't place, an unrecognised or missing display type name, a duplicate name
in the /Names /Dests tree specifically (as opposed to the old-style /Dests dictionary), the
dest1/dest2/dest3 minting collision loop, an outline item's own missing /Title, its /A /GoTo
destination path, and -- most importantly -- the outline cycle guard, never exercised at all.

Calls parseDestination/createDestinationRegistry/readOutline directly against hand-built
PdfObject values and a small ref-table resolver, rather than growing the existing
FixtureBuilder-based PDF fixture to cover every one of these combinations by hand.
…he /Dests dictionary loop

destsDict.entries is a Map, whose own key uniqueness already guarantees every name the
old-style /Dests dictionary loop sees is distinct within that loop -- a dictionary literal's
own duplicate keys, if the source bytes had any, were already collapsed to last-wins by the
parser that built this Map, long before createDestinationRegistry ever runs. The duplicate
check that loop carried could never observe a true duplicate, unlike the /Names /Dests
name-tree walk immediately after it, which genuinely can encounter the same name from two
different leaf nodes.

Also strengthens the surrounding tests: several assertions only checked a diagnostic's code,
not its message, letting a StringLiteral mutation on the message text survive; several others
used toEqual against an object where an optional field's absence versus an explicit undefined
value are the exact thing under test, which toEqual treats as equal and toStrictEqual does not.
…nt-write's byte packing

Several survived mutants traced back to test fixtures whose chosen glyph IDs or code units
happened to share a zero byte with the mutated one, making the mutation invisible: a two-Latin-
letter glyph run where both CIDs fit under 0xff never exercises the high-byte offset for a
second CID, and a surrogate pair whose low surrogate's own low byte is 0x00 never exercises the
low-byte offset for a second UTF-16 code unit. Swaps in characters whose bytes are actually
non-zero at the positions under test, and adds a two-point stroke (the boundary a "fewer than
two points" check must not also exclude) and a synthetic font that deliberately collides two
code points onto one glyph ID (proving collectUsedGlyphs' first-write-wins guard, which the
real font's own injective cmap can never exercise).
…rithmetic, and dict keys

FLAG_SERIF was never set in any test (every vendored face used elsewhere is a sans family);
adds a dedicated case using the real, vendored Caladea (a genuine serif face). The subset
tag's own comma separator and its base-26 letter-extraction direction had no test able to tell
a comma-joined glyph list from a concatenated one, or floor-division from multiplication --
adds a collision pair for the former and an independently-computed expected tag (via the
package's own already-tested crc32()) for the latter. The FontDescriptor's own /Type key,
/StemV, and the CIDFontType2 dict's own /Type key were never read back at all; the /FontBBox
check used optional chaining that let a blanked-out key vacuously pass with the array read as
undefined instead of failing.
…y computed offset

Replaces a pre-sized Uint8Array written at manually computed offsets (2 + i * 2, 3 + i * 2)
with a plain array appended to in sequence, then converted once at the end. Removes the
computed-offset arithmetic entirely rather than getting it right: the result's length now
falls out of how many bytes were actually appended, instead of being asserted up front and
then relied on to match.
Only axisHeightPt and fractionRuleThicknessPt were checked against the
vendored STIXTwoMath-Regular.otf's real values; the other 25 *Pt fields
metricsAt derives from math-table.ts's MATH_VALUE_RECORD_INDEX table went
unchecked, so a wrong index (pointing a field at a neighbouring
MathValueRecord slot) would leave axisHeight/fractionRuleThickness correct
while every other constant silently read the wrong value.

Expected design-unit values come from a standalone script reading the
font's own sfnt bytes directly, the same independent verification method
the surrounding test file's own top comment describes.
…function that reads it

ENUMERATED_COLOUR_SPACES was a module-level constant, evaluated once at import
time -- Stryker's per-test coverage analysis attributes a mutation to such
static code to whichever single test happens to trigger the first import, not
to the tests that actually exercise the enumerated-colour-space branch, so a
wrong lookup table could silently ship undetected. Moving the Map literal
inside readColourSpecification makes its construction run per call, so a
mutation is correctly attributed to the tests that call it.
…ingDefaults

PROGRESSION_ORDERS was a module-level constant, evaluated once at import time
-- Stryker's per-test coverage analysis attributes a mutation to such static
code to whichever single test happens to trigger the first import, not to the
tests that actually decode a COD marker's progression order, so a wrong entry
could silently ship undetected. Moving the array inside readCodingDefaults
makes its construction run per call, so a mutation is correctly attributed to
the tests that call it.
…Filter

LIFT_ALPHA/BETA/GAMMA/DELTA/K were module-level constants, evaluated once at
import time -- Stryker's per-test coverage analysis attributes a mutation to
such static code to whichever single test happens to trigger the first
import, not to the tests that actually exercise the irreversible 9-7 filter,
so a wrong lifting coefficient (a sign flip on LIFT_ALPHA survived undetected
this way) could silently ship. Moving the constants inside inverse97Filter
makes their construction run per call, so a mutation is correctly attributed
to the tests that call it.
@Mearman
Mearman force-pushed the feat/100-percent-mutation-pdf-codec branch from d05507d to fb45c14 Compare September 14, 2026 14:53
…nds check

looksLikeBareCodestream's own data.length >= 4 check is redundant: with
noUncheckedIndexedAccess, an out-of-bounds byte read is already undefined,
and undefined === 0xff is already false, so the four comparisons already
reject a short input on their own.

readChannelDefinitions' end - start < 2 guard and readColourSpecification's
end - start < 3 guard are likewise redundant: both functions' own later
checks (entry + 6 > end, and the >= 7 / > 3 thresholds each branch needs)
already refuse to act on a payload too short to satisfy them, whatever
garbage a short read produces first.

readBox never returns a box whose nextBoxStart fails to advance past its
own offset -- it throws instead when a declared length would undercut its
own header -- so the || box.nextBoxStart <= offset half of both
box-walking loops' termination checks was unreachable.
…00 decoder

Covers the isolated-byte and length-boundary cases looksLikeBareCodestream's
own comparison chain needs, the extended (64-bit) box length's truncation
and nonzero-high-word paths, a box declaring a length shorter than its own
header, an image header box shorter than 14 bytes, a component count and a
channel-definition type spanning both bytes of their field, a
channel-definition count read from its own field rather than an adjacent
header byte, a colr box too short for its own method byte and each
method's own minimum length, two colr boxes (first wins) and a method the
decoder does not recognise, a cmap-only palette box, two jp2c boxes (first
wins), and the signature-box-recognised-but-truncated and
signature-box-absent-but-box-shaped cases the "neither codestream nor
box" / "no contiguous codestream" error messages depend on.
…ock-size check

Exports MarkerCursor so its own bounds-checking and 32-bit assembly can be
tested directly: readHeaderSegment, its sole production caller, already
re-derives and re-checks segmentEnd against cursor.data.length before ever
calling bytes(), so the length it passes always already satisfies
position + length <= data.length on its own, leaving no way to observe
that half of bytes()'s guard except by driving the cursor directly.

Drops the codeBlockWidthExp > 10 and codeBlockHeightExp > 10 checks from
readCodingStyleParameters: each exponent has a floor of 2 (from the SPcod
"transmitted value + 2" encoding a few lines above), so either one alone
exceeding 10 already puts codeBlockWidthExp + codeBlockHeightExp past 12
(11 + 2 = 13), which the sum check right below already throws for.
…rsor edge cases

Adds a direct MarkerCursor suite (uint32 assembly, the bytes() bounds
check's own length < 0 and overflow paths, and its position advancing
past a read slice) alongside a hand-built minimal-codestream constructor
for every header-segment guard a real encoder's own output never trips:
SIZ's zero-component, short-component-list, no-area and zero-tile checks;
COD's undefined-transform, code-block-area, progression-order and
zero-layer checks; QCD's undefined-style check; a marker segment shorter
than its own length field; COC/QCC/POC/RGN/PPT recording their own
overrides; an unexpected SOC/SOD inside the main header; a main header
missing COD or QCD; a tile-part header ending without SOD or running into
a second SOT; a Psot shorter than its own header; the trailing-EOC trim
on a tile-part's own data; and a tile-part header overriding only COD, or
only QCD, or neither.
…e for testing

Each of these three functions has a real, meaningful contract of its own
(coordinate placement, symmetric extension, one-dimensional synthesis),
but every one of their guards against a degenerate input -- mirrorIndex's
length <= 1, synthesiseLine's length <= 0 -- is already unreachable
through their sole production callers: inverseDwt53Level/97Level's own
width <= 0 || height <= 0 guard returns before either ever gets called
with i1 - i0 that small. Exporting them lets a direct test drive that
input rather than removing the guard a future caller might still need.
…x-arithmetic cases

Adds direct interleave/mirrorIndex/synthesiseLine suites for the guards
and loop bounds only reachable that way (see the sibling refactor commit),
zero-width/zero-height cases for both inverseDwt53Level and
inverseDwt97Level, and two non-square, nonzero-origin reconstructions
(one flat, one a single high-pass sample at an odd row and column) that
distinguish an output-index mutant adding an axis origin back in from one
correctly subtracting it -- indistinguishable from a flat signal or a
zero origin alone, which every existing test before this one used.
…t cases

Adds a component index wide enough to need its own two-byte field (257+
components), a signed SIZ component depth, a derived-style quantization's
own step sizes, explicit per-resolution-level precincts from both COD and
COC, a non-Latin COM registration that must not surface as a comment, an
otherwise-unhandled marker segment (TLM) skipped without recording
anything, the exact SOT length-mismatch message, a tile-part header
running into EOC rather than SOD, a Psot landing exactly on an empty
tile-part's own data with nothing to trim, and the three ways a
tile-part's trailing bytes can fail to match the EOC signature without
being trimmed. Also fixes two existing tile-part-override assertions that
checked a field read back as undefined without checking it was genuinely
absent as a key, which a mutant that always spread both cod and qcd
together could satisfy by coincidence.
readTilePart, this function's sole caller, always passes a start sitting
immediately after a real SOD marker (0xFF 0x93). Whenever the resulting
range is under 2 bytes, at least one of the two positions the byte
comparisons check falls on that marker's own fixed bytes instead of on
real tile-part data -- and 0x93 can never be mistaken for 0xD9 -- so the
comparisons already refuse a too-short range on their own, with no need
to measure it first. Drops the now-unused start parameter along with it.
…ent boundaries

Adds a hasOwn check for the tile-part-header-overrides-nothing case (the
same gap the sibling COD-only/QCD-only tests were already fixed for: a
field read back undefined doesn't prove it's genuinely absent as a key),
a quantization step-size loop that stops exactly at its own segment
boundary rather than one iteration short of needing another pair, a
marker segment whose declared length runs exactly to the codestream's
own end, an otherwise-unhandled marker segment (TLM) whose own body is
deliberately shaped like a registration-1 COM segment so a mutant that
misreads it as one would surface as a spurious comment, and a tile-part
whose data is exactly the 2-byte EOC signature and nothing else.
…e-dimension guard

interleave's own loops, sized from the same u0/u1/v0/v1, never iterate
when width or height is non-positive (subbandBounds collapses each such
range to an empty one), and both reconstruction loops below are bounded
by width/height directly, so they no-op the same way. All a non-positive
dimension could still threaten is scratch's own allocation, now floored
at 0 the same way output's already is a few lines above -- removing the
one remaining reason a caller needed the guard at all.

Rewrites mirrorIndex's own negative-offset normalisation as the standard
double modulo instead of a separate negative-offset branch: JS's % result
already follows the sign of its dividend, so folding it into [0, period)
this way needs no comparison of its own, and produces the identical
result for every input the branching version did.
…ning edges

Adds direct inverse53Filter/inverse97Filter suites that fill a buffer with
a sentinel value distinguishable from anything either filter's own
arithmetic would compute, then read back exactly which cells changed --
pinning each filter's own loop bounds directly rather than through the
much larger surface of a full 2D reconstruction. Adds a scratch-allocation
crash regression test for inverseDwt53Level/97Level covering the
grossly-inverted-bounds case the sibling refactor commit's removed guard
used to handle, and strengthens the fill-loop test to compare synthesiseLine's
own fed source indices against mirrorIndex itself (already independently
verified correct) rather than only their count and range.
…o a testable primitive

HOR_SR's row loop writes each row at row * width, which for row === height
lands exactly on output's own one-past-the-end index -- silently absorbed
by TypedArray semantics (an out-of-bounds write is a no-op there, an
out-of-bounds read is undefined) regardless of what that row's own
reconstruction would have computed. A wrong loop bound is therefore
unobservable through either function's own returned array, no matter what
input a test supplies. Extracting the loop into times(), an exported,
directly callable primitive, makes its own call count and argument
sequence observable on their own terms instead.
…2/F-13 boundaries

Adds a direct times() suite (call count and argument sequence, including
zero and negative counts). Fixes the fill-loop/mirrorIndex comparison
test's own length-2 bounds, whose period-2 mirroring makes i0 + k and
i0 - k indistinguishable by parity alone, by widening it to length 4.

Pins F-12's and F-13's own outermost cells (n = last + 1 and n = last,
respectively) against exact Float32Array values computed independently
from the same constants and equations the production code uses: both
lie within F-8/F-9's own already-touched range, so only their specific
numeric contribution, not which cells changed at all, can show whether
either pass's own loop reached that last iteration.
…ound-trip

mirrorIndex immediately computed position - i0 as its own first step, so
every caller had to add i0 back on only for this function to subtract it
straight back out. Taking the offset from i0 directly removes that
round-trip and, as a side effect, removes the one call site (i0 + k) that
could never actually be distinguished from a caller mistakenly writing
i0 - k: mirroring about i0 is symmetric in the offset by definition, so
offset and -offset always mirror identically regardless of which one a
caller happens to pass in.
…et-from-i0 signature

Updates every call site for mirrorIndex's new offsetFromI0 parameter,
adds a nonzero-i0 case that genuinely exercises the difference between an
offset and an absolute position (every prior case used i0 = 0, where the
two coincide), and simplifies the fill-loop ground-truth comparison now
that the call site passes k directly rather than i0 + k.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant