Fanbase

png/png-32x32

version 1.0

32x32 RGB PNG with lengths and CRCs written as constraints

Format
Portable Network Graphics (png)
Authors
The Fandango Fuzzer Team
License
Apache-2.0
Fandango
>=1.3
Extensions
png
Media type
image/png
Specification
https://www.w3.org/TR/png-3/
Its files should decode
always
In the registry
specs/png/png-32x32/png-32x32.fan

Use it

fanbase install png-32x32
fandango fuzz -F png-32x32 -n 10

How it does

accepts 100%, covers 11.0%, 0.85/s. 86 inputs, seed 1.

ParserAcceptsVersion
ffmpeg100%ffmpeg version 7.1.5-0+deb13u1
imagemagick100%Version: ImageMagick 7.1.1-43 Q16 x86_64 22550
libpng-cov100%libpng 1.6.59
pillow100%12.3.0
pngcheck5%PNGcheck version 3.0.3 of 25 April 2021

The spec says its files decode always: as expected.

libpng-cov

lines reached
11.0% of 8119
real files reach
16.1%
only the generated files
7 lines
only the real files
420 lines
11.0% after 1 inputs, 11.0% after 86real files: 16.1%

Cite it

@software{fanbase_png_png_32x32_1_0,
  author  = {{The Fandango Fuzzer Team}},
  title   = {Portable Network Graphics (png/png-32x32), a Fandango input specification},
  version = {1.0},
  url     = {https://github.com/fandango-fuzzer/fanbase},
  note    = {Fanbase registry, specs/png/png-32x32/png-32x32.fan, sha256 d511f021663d},
}

Source

Source (169 lines)
# PNG, always 32x32 8-bit RGB, with optional tEXt chunks and a real zlib
# stream in IDAT. Lengths and CRCs are set with where constraints instead of
# helper functions.
#
# TODO:
# * Consider replacingc constructor functions with grammar rules + constraints
# * Try not to depend on `random`; let Fandango do this

import struct
import zlib
import binascii
import random

IMAGE_WIDTH: int = 32
IMAGE_HEIGHT: int = 32
TEXT_LENGTH: int = 100

<start> ::= \
    <png_signature> \
    <ihdr_chunk> \
    <text_chunk>* \
    <idat_chunk>+ \
    <iend_chunk>

<png_signature> ::= b"\x89PNG\r\n\x1a\n"

<ihdr_chunk> ::= \
    <ihdr_length> \
    <ihdr_type> \
    <ihdr_data> \
    <ihdr_crc>

<ihdr_length> ::= <byte>{4}
where <ihdr_length> == int_to_be_bytes(13)

<ihdr_type> ::= b"IHDR"

<ihdr_data> ::= \
    <ihdr_width> \
    <ihdr_height> \
    <ihdr_bit_depth> \
    <ihdr_color_type> \
    <ihdr_compression> \
    <ihdr_filter> \
    <ihdr_interlace>

<ihdr_width> ::= <byte>{4} := int_to_be_bytes(IMAGE_WIDTH)

<ihdr_height> ::= <byte>{4} := int_to_be_bytes(IMAGE_HEIGHT)

<ihdr_bit_depth> ::= b"\x08"

<ihdr_color_type> ::= b"\x02"

<ihdr_compression> ::= b"\x00"

<ihdr_filter> ::= b"\x00"

<ihdr_interlace> ::= b"\x00"

<ihdr_crc> ::= <byte>{4}
where <ihdr_crc> == crc32_bytes( \
    bytes(<ihdr_type>) + bytes(<ihdr_data>) \
)

<text_chunk> ::= \
    <text_length> \
    <text_type> \
    <text_data> \
    <text_crc>

<text_length> ::= <byte>{4}
where <text_length> == int_to_be_bytes(len(bytes(<text_data>)))

<text_type> ::= b"tEXt"

<text_data> ::= <byte>{TEXT_LENGTH} := generate_ascii_text(TEXT_LENGTH)

<text_crc> ::= <byte>{4}
where <text_crc> == crc32_bytes( \
    bytes(<text_type>) + bytes(<text_data>) \
)

<idat_chunk> ::= \
    <idat_length> \
    <idat_type> \
    <idat_data> \
    <idat_crc>

<idat_length> ::= <byte>{4}
where <idat_length> == int_to_be_bytes(len(bytes(<idat_data>)))

<idat_type> ::= b"IDAT"

<idat_data> ::= <byte>* := generate_idat_data(IMAGE_WIDTH, IMAGE_HEIGHT)

<idat_crc> ::= <byte>{4}
where <idat_crc> == crc32_bytes( \
    bytes(<idat_type>) + bytes(<idat_data>) \
)

<iend_chunk> ::= \
    <iend_length> \
    <iend_type> \
    <iend_crc>

<iend_length> ::= <byte>{4}
where <iend_length> == int_to_be_bytes(0)

<iend_type> ::= b"IEND"

<iend_crc> ::= <byte>{4}
where <iend_crc> == crc32_bytes(bytes(<iend_type>))

# Helper Functions

def int_to_be_bytes(value: int) -> bytes:
    """
    Convert integer to 4-byte big-endian format.

    :param value: Integer value.
    :return: 4-byte big-endian byte sequence.
    """
    return struct.pack(">I", value)

def crc32_bytes(data: bytes) -> bytes:
    """
    Compute PNG CRC32 checksum.

    :param data: Input bytes.
    :return: 4-byte big-endian CRC.
    """
    crc: int = binascii.crc32(bytes(data)) & 0xffffffff
    return struct.pack(">I", crc)

def generate_ascii_text(length: int) -> bytes:
    """
    Generate ASCII text of fixed length.

    :param length: Number of characters.
    :return: ASCII byte string.
    """
    return bytes(
        random.randint(32, 126)
        for _ in range(length)
    )

def generate_idat_data(width: int, height: int) -> bytes:
    """
    Generate valid PNG scanlines and compress them.

    For each scanline:
      - 1 filter byte (0)
      - width * 3 RGB bytes

    The concatenated raw data is zlib-compressed.

    :param width: Image width.
    :param height: Image height.
    :return: zlib-compressed image data.
    """
    raw: bytearray = bytearray()

    for _ in range(height):
        raw.append(0)
        for _ in range(width * 3):
            raw.append(random.getrandbits(8))

    return zlib.compress(bytes(raw))