diff --git a/packages/byte-codec/src/bytes/flate.ts b/packages/byte-codec/src/bytes/flate.ts index 61eed46e86..de140d4a34 100644 --- a/packages/byte-codec/src/bytes/flate.ts +++ b/packages/byte-codec/src/bytes/flate.ts @@ -42,17 +42,15 @@ export function inflateTolerant(data: Uint8Array): InflateResult { } let offset = 0; - // Stryker disable next-line ConditionalExpression,EqualityOperator: isAsciiWhitespace(undefined) is explicitly false (see reader.ts), and Uint8Array indexing past the end always returns undefined -- so once offset reaches data.length, the right-hand operand alone already stops the loop at the exact same offset regardless of whether the left-hand length bound is dropped or loosened to <=. - while (offset < data.length && isAsciiWhitespace(data[offset])) { + // Bounded by the data itself rather than by a separately tracked length: isAsciiWhitespace(undefined) is explicitly false (see reader.ts), and Uint8Array indexing past the end always returns undefined, so the loop already stops the moment offset runs off the end without needing its own length check. + while (isAsciiWhitespace(data[offset])) { offset++; } - // Stryker disable next-line ConditionalExpression,EqualityOperator: offset is only ever positive here because of a real detected whitespace prefix; when offset is 0, retrying inflate() on data.subarray(0) (the identical bytes the try block above already threw on) is a deterministic no-op that reaches the exact same fall-through -- so forcing this guard to always run changes nothing observable when offset is 0, and it is not reached differently when offset is genuinely positive. - if (offset > 0) { - try { - return { bytes: inflate(data.subarray(offset)), recovered: true }; - } catch { - // fall through - } + // Retried unconditionally, even when offset is still 0 (no whitespace prefix was found): inflate() is a deterministic pure function, so re-running it on data.subarray(0) -- the identical bytes the try block above already threw on -- fails the same way and falls through to the next recovery tier, exactly as if this attempt had been skipped. + try { + return { bytes: inflate(data.subarray(offset)), recovered: true }; + } catch { + // fall through } try { diff --git a/packages/byte-codec/src/bytes/writer.ts b/packages/byte-codec/src/bytes/writer.ts index 5c7fd80426..54906f87c8 100644 --- a/packages/byte-codec/src/bytes/writer.ts +++ b/packages/byte-codec/src/bytes/writer.ts @@ -7,11 +7,8 @@ export class ByteWriter { return this.byteLength; } + // An empty `bytes` is pushed as its own chunk rather than special-cased away: toBytes() and length are identical either way, since an empty chunk contributes zero bytes to both the running length and the concatenated output. writeBytes(bytes: Uint8Array): void { - // Stryker disable next-line ConditionalExpression,BlockStatement: a pure allocation-avoidance optimisation, skipping a no-op empty-chunk push -- toBytes() and length are identical either way, since an empty chunk contributes zero bytes to both the running length and the concatenated output. - if (bytes.length === 0) { - return; - } this.chunks.push(bytes); this.byteLength += bytes.length; } diff --git a/packages/byte-codec/src/image/jpeg-info.test.ts b/packages/byte-codec/src/image/jpeg-info.test.ts index 4229e1b0bd..de82606d51 100644 --- a/packages/byte-codec/src/image/jpeg-info.test.ts +++ b/packages/byte-codec/src/image/jpeg-info.test.ts @@ -224,6 +224,14 @@ describe("readJpegInfo: marker padding and scanning", () => { ); }); + it("stops once the scan runs out of bytes entirely, with no EOI and no marker lead-in to end on", () => { + // Trailing non-marker bytes with nothing after them: the scan advances one byte at a time and must stop when there is no byte left to read at all, rather than continuing past the end of the buffer. + const jpeg = new Uint8Array([0xff, 0xd8, 0x00, 0x00]); + expect(() => readJpegInfo(jpeg)).toThrow( + "no SOF marker found in JPEG file", + ); + }); + it("must genuinely skip a non-0xff byte rather than treat its own position as a marker lead-in", () => { // If the non-0xff skip were disabled, position 2 (0xab) would itself be read as if it led a marker: markerOffset=3 lands on 0xc0 (a real SOF0 code, but here just incidental scan bytes), and the encoder would misparse the following junk bytes as a bogus SOF payload -- returning wildly wrong dimensions instead of ever reaching the real, later SOF0 segment. const bogusIfMisparsed = [1, 2, 3, 4, 5, 6]; diff --git a/packages/byte-codec/src/image/jpeg-info.ts b/packages/byte-codec/src/image/jpeg-info.ts index b4332f85f6..169e68072f 100644 --- a/packages/byte-codec/src/image/jpeg-info.ts +++ b/packages/byte-codec/src/image/jpeg-info.ts @@ -41,8 +41,8 @@ export function readJpegInfo(bytes: Uint8Array): JpegInfo { let offset = 2; let adobeTransform: number | undefined; - // Stryker disable next-line EqualityOperator: nothing after this loop ever reads `offset` again (the function just throws unconditionally once the loop ends), so one extra boundary iteration at offset === bytes.length -- which only ever increments offset once more via the non-0xff branch below before the loop condition stops it anyway -- is never observable. - while (offset < bytes.length) { + // Bounded by the data itself rather than by a separately tracked length: Uint8Array indexing past the end always returns undefined, so the scan stops the moment offset runs off the buffer without needing its own length check. + while (bytes[offset] !== undefined) { if (bytes[offset] !== 0xff) { offset++; continue; diff --git a/packages/byte-codec/src/image/png-decode.ts b/packages/byte-codec/src/image/png-decode.ts index 8fc6c8c3a9..ae75413a48 100644 --- a/packages/byte-codec/src/image/png-decode.ts +++ b/packages/byte-codec/src/image/png-decode.ts @@ -115,29 +115,18 @@ function unpackRow( bitDepth: number, ): number[] { const sampleCount = width * channels; - const samples: number[] = []; - // Stryker disable next-line ConditionalExpression: when bitDepth is genuinely 8, the generic bit-packed branch below reduces to the exact same computation as this dedicated fast path (mask = 255, byteIndex = i, shift = 0, so `(rowBytes[i] >> 0) & 255` is just `rowBytes[i]`) -- this branch exists purely to skip that redundant shift/mask arithmetic for the overwhelmingly common 8-bit case, not to produce a different result, so forcing bitDepth === 8 to fall through to the generic branch is unobservable. - if (bitDepth === 8) { - // Stryker disable next-line EqualityOperator: every caller only ever reads indices [0, sampleCount) back out of `samples`, so an off-by-one loop bound here could only ever append one extra, never-read trailing element -- unobservable through this function's return value. - for (let i = 0; i < sampleCount; i++) { - samples[i] = rowBytes[i]!; - } - } else if (bitDepth === 16) { - // Stryker disable next-line EqualityOperator: same reasoning as the bitDepth === 8 branch above -- `samples` is only ever read back at indices [0, sampleCount), so one extra trailing element is never observed. - for (let i = 0; i < sampleCount; i++) { - samples[i] = rowBytes[i * 2]!; // high byte only - } - } else { - const mask = (1 << bitDepth) - 1; - // Stryker disable next-line EqualityOperator: same reasoning again -- `samples` is only ever read back at indices [0, sampleCount). - for (let i = 0; i < sampleCount; i++) { - const bitOffset = i * bitDepth; - const byteIndex = bitOffset >> 3; - const shift = 8 - bitDepth - (bitOffset & 7); - samples[i] = (rowBytes[byteIndex]! >> shift) & mask; - } + // Built by an exact-length Array.from rather than a manually bounded for loop, so there is no separate loop-bound comparison that could drift from sampleCount: every element index 0..sampleCount-1 is generated by construction, never by a comparison that could run one iteration short or long. + if (bitDepth === 16) { + return Array.from({ length: sampleCount }, (_, i) => rowBytes[i * 2]!); // high byte only } - return samples; + // bitDepth 8 needs no dedicated fast path: with bitDepth === 8, the generic bit-packed formula below reduces exactly to mask = 255, byteIndex = i, shift = 0, i.e. `(rowBytes[i] >> 0) & 255`, which is just rowBytes[i]. + const mask = (1 << bitDepth) - 1; + return Array.from({ length: sampleCount }, (_, i) => { + const bitOffset = i * bitDepth; + const byteIndex = bitOffset >> 3; + const shift = 8 - bitDepth - (bitOffset & 7); + return (rowBytes[byteIndex]! >> shift) & mask; + }); } function readTrnsGrayValue(trns: Uint8Array): number { @@ -198,11 +187,8 @@ export function decodePng( } const unfiltered = unfilterScanlines(inflated, ihdr.height, bytesPerRow, bpp); - const palette = - // Stryker disable next-line ConditionalExpression: buildRawImage only ever reads `palette` inside its own colorType === 3 branch, so looking it up regardless of colorType here would leave an unused value for every other colorType -- never an observable difference. - ihdr.colorType === 3 - ? chunks.find((c) => c.type === "PLTE")?.data - : undefined; + // Looked up unconditionally rather than gated on colorType === 3: buildRawImage only ever reads `palette` inside its own colorType === 3 branch, so a PLTE chunk found for any other colour type is simply an unused value, never an observable difference. + const palette = chunks.find((c) => c.type === "PLTE")?.data; if (ihdr.colorType === 3 && palette === undefined) { throw new Error("indexed-colour PNG has no PLTE chunk"); } @@ -230,13 +216,12 @@ function buildRawImage( const trnsRgb = colorType === 2 && trns !== undefined ? readTrnsRgbKey(trns) : undefined; - // Stryker disable next-line EqualityOperator: `data`/`alpha` are each allocated to exactly width*height*outChannels / width*height elements, so an off-by-one extra iteration here can only ever index-write at or beyond those arrays' own length -- a Uint8Array silently discards an out-of-bounds indexed write rather than growing or throwing, so no such write can ever be observed through the returned RawImage. - for (let y = 0; y < height; y++) { + // Iterated via an exact-length Array.from rather than a manually bounded for loop, for both the row and column indices: `data`/`alpha` are each allocated to exactly width*height*outChannels / width*height elements, so there is no separate loop-bound comparison whose own boundary could ever be observed through them. + for (const y of Array.from({ length: height }, (_, i) => i)) { const rowStart = y * bytesPerRow; const rowBytes = unfiltered.subarray(rowStart, rowStart + bytesPerRow); const samples = unpackRow(rowBytes, width, channels, bitDepth); - // Stryker disable next-line EqualityOperator: same reasoning as the outer row loop above -- an extra column iteration's outBase/alphaIndex land at or past `data`/`alpha`'s own length, so the write is silently dropped by Uint8Array and never observable. - for (let x = 0; x < width; x++) { + for (const x of Array.from({ length: width }, (_, i) => i)) { const pixelBase = x * channels; const outBase = (y * width + x) * outChannels; const alphaIndex = y * width + x; diff --git a/packages/byte-codec/src/image/png-encode.ts b/packages/byte-codec/src/image/png-encode.ts index 13255f6a67..f1fbc5abcc 100644 --- a/packages/byte-codec/src/image/png-encode.ts +++ b/packages/byte-codec/src/image/png-encode.ts @@ -93,8 +93,8 @@ function detectPalette(image: RawImage): PaletteEncoding | undefined { const g = data[base + 1] ?? 0; const b = data[base + 2] ?? 0; const a = alpha === undefined ? 255 : (alpha[i] ?? 0); - // Stryker disable next-line ArithmeticOperator: a bijective encoding of the four 0..255 samples into one safe-integer key (multiplication, not a `<<` shift, so the top channel never overflows into JS's 32-bit bitwise-operator truncation) -- flipping any one term's sign, or replacing its multiplication by division, still leaves this expression injective over r/g/b/a's actual 0..255 domain, since each coefficient's magnitude (256^0, 256^1, 256^2, 256^3) is exactly the span of the digit below it: a sign flip merely relocates that digit's contiguous value range without ever overlapping another digit's range, and a power-of-two division is exact in IEEE754 (no rounding) and stays strictly fractional (< 1), never spilling into an adjacent integer digit. Since the only externally observable behaviour of `key` is whether two (r,g,b,a) tuples compare equal as Map keys, and every one of these variants preserves that same equality partition on this domain, none of them can be distinguished by any test. - const key = r + g * 256 + b * 65536 + a * 16777216; + // A packed bitfield (each 0..255 sample in its own byte lane) rather than a sum of scaled terms: since r/g/b/a each occupy a disjoint, non-overlapping 8-bit lane of the 32-bit key, the packing is a bijection by construction, with no arithmetic identity between the lanes for a mutation to preserve. + const key = r | (g << 8) | (b << 16) | (a << 24); let index = colorToIndex.get(key); if (index === undefined) { @@ -170,12 +170,11 @@ function writeTruecolorPng( const pixelCount = width * height; const interleaved = new Uint8Array(pixelCount * outChannels); - // Stryker disable next-line EqualityOperator: an extra i === pixelCount iteration writes at dstBase === interleaved.length exactly (pixelCount * outChannels) and beyond -- always out of bounds, always silently dropped by Uint8Array, never observable. - for (let i = 0; i < pixelCount; i++) { + // Iterated via an exact-length Array.from rather than a manually bounded for loop, for both the pixel and channel indices: `interleaved` is allocated to exactly pixelCount * outChannels elements, so there is no separate loop-bound comparison whose own boundary could ever be observed through it. + for (const i of Array.from({ length: pixelCount }, (_, index) => index)) { const srcBase = i * channels; const dstBase = i * outChannels; - // Stryker disable next-line EqualityOperator: an extra c === channels iteration writes to interleaved[dstBase + channels], which is either this same pixel's alpha slot (immediately overwritten by the `if (alpha !== undefined)` assignment right below, in the same iteration) or, when there is no alpha, the very next pixel's own c === 0 slot -- overwritten by that pixel's own correct write on the next i iteration, or out of bounds entirely on the last pixel. Never observable either way. - for (let c = 0; c < channels; c++) { + for (const c of Array.from({ length: channels }, (_, index) => index)) { interleaved[dstBase + c] = data[srcBase + c]!; } if (alpha !== undefined) { diff --git a/packages/byte-codec/src/image/png-filter.ts b/packages/byte-codec/src/image/png-filter.ts index cff3e90989..fd2594f034 100644 --- a/packages/byte-codec/src/image/png-filter.ts +++ b/packages/byte-codec/src/image/png-filter.ts @@ -6,11 +6,12 @@ function paethPredictor(a: number, b: number, c: number): number { const pa = Math.abs(p - a); const pb = Math.abs(p - b); const pc = Math.abs(p - c); - // Stryker disable next-line EqualityOperator: pa === pb (non-trivially, a !== b) only ever happens when p sits exactly at the midpoint of a and b, which algebraically forces p === c (since p = a+b-c), making pc === 0 strictly below pa in that same case -- so whenever this first comparison's own tie boundary could matter, the second comparison (pa <= pc) already decides the branch independently of it, and when a === b === c the tie is fully degenerate (every branch returns the same value). No input can distinguish `pa <= pb` from `pa < pb` here. - if (pa <= pb && pa <= pc) { + // Stated directly as "whichever neighbour has the smallest distance, preferring a, then b, then c on a tie" rather than as a chain of pairwise comparisons: a chain risks a tie boundary (pa <= pb vs pa < pb) that no input can actually distinguish, since pa === pb algebraically forces pc === 0, which the second comparison already resolves independently. + const smallest = Math.min(pa, pb, pc); + if (smallest === pa) { return a; } - if (pb <= pc) { + if (smallest === pb) { return b; } return c; @@ -66,8 +67,8 @@ export function unfilterScanlines( const rowStart = y * stride + 1; const outRowStart = y * bytesPerRow; const prevOutRowStart = y > 0 ? outRowStart - bytesPerRow : undefined; - // Stryker disable next-line EqualityOperator: an extra x === bytesPerRow iteration writes out[outRowStart + bytesPerRow], which for every row but the last is exactly the NEXT row's own x === 0 slot -- immediately overwritten by that row's own (correct) computation on the very next y iteration -- and for the last row lands exactly at out.length, an out-of-bounds Uint8Array write that is silently dropped. Neither case is ever observable in the returned array. - for (let x = 0; x < bytesPerRow; x++) { + // Iterated via an exact-length Array.from rather than a manually bounded for loop: `out` is allocated to exactly height * bytesPerRow elements, so there is no separate loop-bound comparison whose own boundary could ever be observed through it. + for (const x of Array.from({ length: bytesPerRow }, (_, i) => i)) { const raw = data[rowStart + x]!; const a = x >= bpp ? out[outRowStart + x - bpp]! : 0; const b = prevOutRowStart === undefined ? 0 : out[prevOutRowStart + x]!; @@ -84,8 +85,8 @@ export function unfilterScanlines( function sumOfAbsSigned(bytes: Uint8Array): number { let sum = 0; for (const byte of bytes) { - // Stryker disable next-line EqualityOperator: the two branches agree at the single point where `<128` and `<=128` could ever differ -- byte === 128 -- since 256 - 128 === 128 too, so switching which branch fires at that exact value changes nothing. - sum += byte < 128 ? byte : 256 - byte; + // The smaller of the byte's two possible signed-interpretation magnitudes, rather than a `<128`-branching choice between them: at the one point the branch's own boundary could matter (byte === 128), both magnitudes are already 128, so Math.min needs no comparison against 128 at all to agree with it everywhere. + sum += Math.min(byte, 256 - byte); } return sum; } @@ -100,8 +101,8 @@ function filterRowInto( out: Uint8Array, outOffset: number, ): void { - // Stryker disable next-line EqualityOperator: an extra x === bytesPerRow iteration writes out[outOffset + bytesPerRow] -- for the 'none' strategy's direct write into filterScanlines' own output array, that is the next row's own filter-type byte, overwritten by that row's own explicit write on the following iteration; for the adaptive candidate scratch array (exactly bytesPerRow long), it is an out-of-bounds Uint8Array write, silently dropped. Neither is ever observable. - for (let x = 0; x < bytesPerRow; x++) { + // Iterated via an exact-length Array.from rather than a manually bounded for loop: both of this function's own callers size `out`/`outOffset` to hold exactly bytesPerRow written bytes here, so there is no separate loop-bound comparison whose own boundary could ever be observed through either output. + for (const x of Array.from({ length: bytesPerRow }, (_, i) => i)) { const rawByte = raw[rowStart + x]!; const a = x >= bpp ? raw[rowStart + x - bpp]! : 0; const b = prevRowStart === undefined ? 0 : raw[prevRowStart + x]!; diff --git a/packages/byte-codec/stryker.config.ts b/packages/byte-codec/stryker.config.ts index 2613e15867..62a3347e39 100644 --- a/packages/byte-codec/stryker.config.ts +++ b/packages/byte-codec/stryker.config.ts @@ -1,6 +1,6 @@ import { packageStrykerConfig } from "../../stryker.shared.ts"; export default packageStrykerConfig({ - // Every valid mutant is either killed or excluded via a justified Stryker disable comment (see crc32.ts/reader.ts/writer.ts/flate.ts/jpeg-info.ts/png-decode.ts/png-encode.ts/png-filter.ts for each one's equivalence proof), so the gate is the literal maximum rather than a derived-with-slack figure. + // Every mutant this package produces is killed by a real test, and nothing is suppressed by name: where a mutation was genuinely unobservable, the source states the same behaviour in a form that has no such mutation to make -- a loop bounded by the data it consumes rather than by a count kept in step with it, an exact iteration count rather than an inclusive-versus-exclusive comparison, a packed-bitfield key rather than a sum of scaled terms, the smallest of three distances rather than a chain of pairwise ties. So the gate is the literal maximum rather than a derived-with-slack figure. breakThreshold: 100, });