Skip to content

fix: PNG/APNG CRC verification, GIF bounds check, adjustBrightness LUT hoisting, PAM docs#106

Merged
Hexagon merged 4 commits into
mainfrom
copilot/review-completeness-low-hanging-fruit
Apr 15, 2026
Merged

fix: PNG/APNG CRC verification, GIF bounds check, adjustBrightness LUT hoisting, PAM docs#106
Hexagon merged 4 commits into
mainfrom
copilot/review-completeness-low-hanging-fruit

Conversation

Copilot AI commented Apr 14, 2026

Copy link
Copy Markdown
Contributor
  • Explore codebase and identify low-hanging fruit from TODO.md
  • L3: Move adjustBrightness clamp LUT to module-level constant (performance)
  • L4: Replace GIF background color || 0 with explicit bounds check (robustness)
  • L2: Improve PAM decoder JSDoc to document DEPTH/MAXVAL constraints (docs)
  • L1: Add PNG/APNG CRC verification during decode (correctness)
  • Fix security test to use valid IHDR CRC for crafted oversized PNG
  • Update CHANGELOG.md and TODO.md
  • Fix CI failure: reformat TODO.md to pass deno fmt --check
  • Address review: use subarray instead of slice for CRC verification in png.ts and apng.ts
  • Address review: add overflow-safe bounds checks in all PNG/APNG chunk parsing loops
  • Address review: use Math.round for negative-safe rounding in adjustBrightness
  • Precommit validation (626 tests pass)

@Hexagon

Hexagon commented Apr 14, 2026

Copy link
Copy Markdown
Member

@copilot have a look at the ci failure

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses several low-severity TODO findings by improving image decoding correctness/robustness, reducing a small recurring allocation in image processing, and clarifying PAM decoder limitations.

Changes:

  • Added CRC32 verification for PNG/APNG chunk parsing during decode and metadata extraction, and updated the existing security test PNG to include a correct IHDR CRC.
  • Hardened GIF background color selection by replacing || 0 fallbacks with an explicit palette-bounds check.
  • Hoisted adjustBrightness() clamp LUT to module scope to avoid per-call reconstruction, and documented PAM decoder’s supported subset.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/utils/security.test.ts Updates crafted PNG test bytes to include a correct IHDR CRC so the oversized-dimension check remains exercised.
src/utils/image_processing.ts Hoists brightness clamp LUT to module scope and updates adjustBrightness() to use it.
src/utils/gif_decoder.ts Adds explicit bounds checking for background palette index lookups.
src/formats/png.ts Verifies CRC for each parsed chunk in decode() and extractMetadata().
src/formats/apng.ts Verifies CRC for each parsed chunk in APNG first-pass decode and extractMetadata().
src/formats/pam.ts Adds JSDoc describing the supported PAM subset and expected errors for unsupported variants.
TODO.md Marks the addressed TODO items as fixed and summarizes the implemented fixes.
CHANGELOG.md Documents the new CRC verification, GIF bounds check, and LUT hoisting behavior changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/formats/apng.ts Outdated
pos += 4;

// Verify CRC (covers chunk type + chunk data)
if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) {

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRC verification uses data.slice(...), which copies the chunk bytes for every chunk during metadata extraction. Prefer data.subarray(typePos, typePos + 4 + length) (with an upfront bounds check) to avoid extra allocations and reduce overhead.

Suggested change
if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) {
if (
storedCrc !== this.crc32(data.subarray(typePos, typePos + 4 + length))
) {

Copilot uses AI. Check for mistakes.
Comment thread src/utils/image_processing.ts Outdated
Comment on lines 125 to 126
// Use bitwise OR for fast rounding (equivalent to Math.round for positive numbers)
const adjustInt = (adjust + 0.5) | 0;

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adjustInt is computed with (adjust + 0.5) | 0, which only matches Math.round for positive values. Since amount can be negative, this yields incorrect rounding for negative brightness adjustments (e.g., -10.2 rounds to -9), which slightly skews results. Use Math.round(adjust) (or a sign-aware bitwise rounding) to keep brightness adjustment symmetric for negative/positive amounts.

Suggested change
// Use bitwise OR for fast rounding (equivalent to Math.round for positive numbers)
const adjustInt = (adjust + 0.5) | 0;
// Use symmetric rounding so negative and positive brightness adjustments behave consistently
const adjustInt = Math.round(adjust);

Copilot uses AI. Check for mistakes.
Comment thread src/formats/png.ts
Comment on lines 71 to +74
const chunkData = data.slice(pos, pos + length);
pos += length;
pos += 4; // Skip CRC
const storedCrc = this.readUint32(data, pos);
pos += 4;

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In PNGFormat.decode's chunk loop there is no bounds check that pos + length + 4 is within data.length before slicing chunk data and reading the stored CRC. Because readUint32 treats out-of-range reads as zero, a truncated chunk can be misclassified as a CRC failure (and can even be bypassed if the computed CRC happens to be 0), and chunkData can be shorter than the declared length. Add an explicit bounds check (similar to extractMetadata's if (pos + length + 4 > data.length) ...) and throw a clear truncation/invalid chunk error before CRC verification.

Copilot uses AI. Check for mistakes.
Comment thread src/formats/png.ts Outdated
pos += 4;

// Verify CRC (covers chunk type + chunk data)
if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) {

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRC verification currently computes this.crc32(data.slice(typePos, typePos + 4 + length)), which allocates a new Uint8Array for every chunk. Since crc32 only reads, prefer data.subarray(typePos, typePos + 4 + length) (after bounds validation) to avoid per-chunk copies during metadata extraction.

Suggested change
if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) {
if (
storedCrc !== this.crc32(data.subarray(typePos, typePos + 4 + length))
) {

Copilot uses AI. Check for mistakes.
Comment thread src/formats/apng.ts
Comment on lines 127 to 131
while (pos < data.length) {
const length = this.readUint32(data, pos);
pos += 4;
const typePos = pos;
const type = String.fromCharCode(

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APNGFormat.decodeFrames first-pass chunk parsing does not validate that there are at least 8 bytes for length+type, nor that pos + length + 4 is within data.length before slicing chunk data and reading the stored CRC. With readUint32's out-of-range behavior, truncated/corrupt inputs can be mis-parsed and can produce misleading CRC errors (or be bypassed if CRC reads as 0). Add the same bounds checks used in extractMetadata before reading type/data/CRC, and throw a clear truncation/invalid chunk error.

Copilot uses AI. Check for mistakes.

Copilot AI commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

@copilot have a look at the ci failure

Fixed in 8341271. The CI was failing because deno fmt --check found overly-long lines in TODO.md introduced by the previous commit. Ran deno fmt on the file to reformat it.

Copilot AI requested a review from Hexagon April 14, 2026 22:41
@Hexagon

Hexagon commented Apr 14, 2026

Copy link
Copy Markdown
Member

@copilot apply changes based on the comments in this thread

Copilot AI commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

Applied all 5 review comments in commits 54bf326 and e583259:

  • png.ts decode loop: Added if (pos + 8 > data.length) break and overflow-safe length > data.length - pos - 4 bounds check before CRC verification
  • png.ts extractMetadata: Switched data.slice()data.subarray() for CRC, and overflow-safe bounds check
  • apng.ts decodeFrames first-pass: Added both pos + 8 and overflow-safe length > data.length - pos - 4 bounds checks before CRC
  • apng.ts extractMetadata: Switched data.slice()data.subarray() for CRC, and overflow-safe bounds check
  • image_processing.ts: Changed (adjust + 0.5) | 0Math.round(adjust) for correct negative brightness rounding

All 626 tests pass.

@Hexagon Hexagon merged commit 3707cd7 into main Apr 15, 2026
5 checks passed
@Hexagon Hexagon deleted the copilot/review-completeness-low-hanging-fruit branch April 15, 2026 19:30
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.

3 participants