gif/gif
version 1.0
GIF87a and GIF89a with all extension types, local color tables and several images
- Format
- Graphics Interchange Format (gif)
- Authors
- The Fandango Fuzzer Team
- License
- Apache-2.0
- Fandango
- >=1.3
- Extensions
- gif
- Media type
- image/gif
- Specification
- https://www.w3.org/Graphics/GIF/spec-gif89a.txt
- Its files should decode
- always
- In the registry
- specs/gif/gif/gif.fan
Use it
fanbase install gif fandango fuzz -F gif -n 10
How it does
accepts 75%, covers 51.8%, 23/s. 1000 inputs, seed 1.
| Parser | Accepts | Version |
|---|---|---|
| ffmpeg | 40% | ffmpeg version 7.1.5-0+deb13u1 |
| giflib-cov | 52% | giflib 5.2.2 |
| giftext | 75% | |
| imagemagick | 62% | Version: ImageMagick 7.1.1-43 Q16 x86_64 22550 |
| pillow | 65% | 12.3.0 |
The spec says its files decode always: not as expected.
giflib-cov
- lines reached
- 51.8% of 875
- real files reach
- 44.9%
- only the generated files
- 60 lines
- only the real files
- 0 lines
Cite it
@software{fanbase_gif_gif_1_0,
author = {{The Fandango Fuzzer Team}},
title = {Graphics Interchange Format (gif/gif), a Fandango input specification},
version = {1.0},
url = {https://github.com/fandango-fuzzer/fanbase},
note = {Fanbase registry, specs/gif/gif/gif.fan, sha256 1750ae1728b6},
}
Source
Source (223 lines)
# GIF87a / GIF89a: header, logical screen descriptor with optional global
# color table, images and extensions (graphic control, comment, plain text,
# application, NETSCAPE loop), trailer.
#
# LZW data and the color tables come from helpers so they agree with the size
# bits in the headers. Screen sizes, color indices and block lengths often
# sit on edge values (0, 1, 0xFFFF, ...).
#
# TODO:
# * Replace constructor functions (build_*()) with grammar rules + constraints
# * Where are the docstrings for the constructor functions?
# * See Fanbase `gif-minimal.fan` and the Fandango docs GIF case study
# for a much better structure.
<start> ::= <gif>
<gif> ::= <header> <lsd> <body> <trailer>
# Header : version 87a (no extensions officially) vs 89a
<header> ::= <signature> <version>
<signature> ::= rb'GIF'
<version> ::= rb'87a' | rb'89a'
# Logical Screen Descriptor + Global Color Table
# Canvas width/height are 16-bit LE and fully decoupled from image data,
# so they roam the whole range incl. 0 and 0xFFFF.
<lsd> ::= <canvas> <gsd_packed> <bgcolor> <aspect> <gct>
<canvas> ::= <byte>+ := build_canvas()
<gsd_packed> ::= <byte>+ := build_gsd_packed()
<bgcolor> ::= <byte>+ := build_bgcolor()
<aspect> ::= rb'\x00' | rb'\x01' | rb'\x31' | rb'\x40' | rb'\xff' | <byte>
<gct> ::= <byte>* := build_gct()
# Body : extensions, then >=1 image, then more blocks
<body> ::= <extensions> <image_block> <more>
<extensions> ::= <extension>{0,4}
<more> ::= <block>{0,3}
<block> ::= <extension> | <image_block>
# Extensions (0x21 introducer)
<extension> ::= <gce> | <comment_ext> | <plaintext_ext> | <app_ext> | <app_netscape>
# Graphic Control Extension: fixed 4-byte block. Flags byte packs
# reserved(3) | disposal(3) | userInput(1) | transparency(1); alternation
# covers every disposal method, both flags, and set reserved bits.
<gce> ::= rb'\x21\xf9\x04' <gce_flags> <delay> <byte> rb'\x00'
<gce_flags> ::= rb'\x00' | rb'\x01' | rb'\x04' | rb'\x08' | rb'\x09' \
| rb'\x0c' | rb'\x1c' | rb'\x1d' | rb'\xe5' | <byte>
<delay> ::= <u16le>
# Comment Extension: framed sub-blocks + terminator (helper frames).
<comment_ext> ::= rb'\x21\xfe' <sub_stream>
# Plain Text Extension (89a, widely unimplemented): 12-byte grid params
# then a framed text sub-block stream.
<plaintext_ext> ::= rb'\x21\x01\x0c' <ptext_params> <sub_stream>
<ptext_params> ::= rb'[\x00-\xff]{12}'
# Application Extension: 11-byte identifier+auth, then framed sub-blocks.
<app_ext> ::= rb'\x21\xff\x0b' <app_id> <sub_stream>
<app_id> ::= rb'NETSCAPE2\.0' | rb'ANIMEXTS1\.0' | rb'ICCRGBG1012' \
| rb'XMP DataXMP' | rb'fractintgif' | rb'ZGATEXTI5\x00\x00' \
| rb'[\x20-\x7e]{11}'
# NETSCAPE looping extension with a well-formed loop sub-block
# (0x03,0x01,loopLE) + terminator. loop 0 = infinite; edge 0xFFFF.
<app_netscape> ::= rb'\x21\xff\x0bNETSCAPE2\.0\x03\x01' <loop_count> rb'\x00'
<loop_count> ::= <u16le>
<image_block> ::= <image_descriptor> <lct> <image_data>
<image_descriptor> ::= <byte>+ := build_image_descriptor()
<lct> ::= <byte>* := build_lct()
<image_data> ::= <byte>+ := build_image_data()
# Trailer
<trailer> ::= rb'\x3b'
<u16le> ::= rb'\x00\x00' | rb'\x01\x00' | rb'\x64\x00' | rb'\xff\x00' \
| rb'\x00\xff' | rb'\xff\xff' | rb'[\x00-\xff]{2}'
<sub_stream> ::= <byte>+ := build_sub_stream()
<byte> ::= rb'[\x00-\xff]'
where True
# Helpers. _S carries per-image / per-file geometry between the
# left-to-right generated symbols (set before the symbol that reads it).
import random
_S = {}
def build_canvas():
cw = random.choice([1, 3, 16, 64, 200, 255, 256, 320, 65535, 0])
ch = random.choice([1, 3, 16, 64, 200, 255, 256, 240, 65535, 0])
_S['cw'], _S['ch'] = cw, ch
return cw.to_bytes(2, 'little') + ch.to_bytes(2, 'little')
def _rgb(n):
return bytes(random.getrandbits(8) for _ in range(3 * n))
def build_gsd_packed():
# Global Color Table flag mostly on (so a GCT exists), sometimes off.
flag = random.choice([1, 1, 1, 0])
color_res = random.choice([0, 1, 3, 7, 7]) # bits 4-6
sort = random.choice([0, 0, 1]) # bit 3
size = random.choice([0, 0, 1, 2, 3, 5, 7]) # bits 0-2 -> 2^(n+1)
_S['gflag'] = flag
_S['gsize'] = size
_S['gncolors'] = (2 ** (size + 1)) if flag else 2
b = (flag << 7) | (color_res << 4) | (sort << 3) | size
return bytes([b])
def build_bgcolor():
n = _S.get('gncolors', 2)
return bytes([random.choice([0, n - 1, min(255, n), 255])])
def build_gct():
if not _S.get('gflag'):
_S['gncolors'] = 2
return b''
return _rgb(2 ** (_S['gsize'] + 1))
def build_image_descriptor():
cw, ch = _S.get('cw', 0), _S.get('ch', 0)
# keep pixel count small so LZW stays fast; hit the 1px edge but keep frames
# >=1 (a 0-size frame only arises from a 0-size screen), then clamp the frame
# inside the logical screen so the file stays spec-valid.
dims = [1, 1, 2, 3, 4, 5, 8, 10, 16, 24, 32]
iw = min(random.choice(dims), cw)
ih = min(random.choice(dims), ch)
while iw * ih > 1024:
ih = min(random.choice(dims), ch)
left = random.randint(0, max(0, cw - iw))
top = random.randint(0, max(0, ch - ih))
lflag = random.choice([0, 0, 0, 1]) # local table rare
interlace = random.choice([0, 0, 1])
sort = random.choice([0, 1])
lsize = random.choice([0, 1, 2, 3, 7])
_S['iw'], _S['ih'] = iw, ih
_S['lflag'], _S['lsize'], _S['interlace'] = lflag, lsize, interlace
packed = (lflag << 7) | (interlace << 6) | (sort << 5) | lsize
out = bytearray(b'\x2c') # image separator, then L,T,W,H
out += left.to_bytes(2, 'little')
out += top.to_bytes(2, 'little')
out += iw.to_bytes(2, 'little')
out += ih.to_bytes(2, 'little')
out.append(packed)
return bytes(out)
def build_lct():
if not _S.get('lflag'):
_S['ncolors'] = _S.get('gncolors', 2)
return b''
n = 2 ** (_S['lsize'] + 1)
_S['ncolors'] = n
return _rgb(n)
def _gif_lzw(indices, min_code):
clear = 1 << min_code
end = clear + 1
code_size = min_code + 1
table = {}
for i in range(clear):
table[bytes([i])] = i
next_code = clear + 2
bit_buf = 0
bit_cnt = 0
out = bytearray()
def write(code, size):
nonlocal bit_buf, bit_cnt
bit_buf |= code << bit_cnt
bit_cnt += size
while bit_cnt >= 8:
out.append(bit_buf & 0xFF)
bit_buf >>= 8
bit_cnt -= 8
write(clear, code_size)
if indices:
buf = bytes([indices[0]])
for k in indices[1:]:
cur = buf + bytes([k])
if cur in table:
buf = cur
else:
write(table[buf], code_size)
if next_code < 4096:
table[cur] = next_code
next_code += 1
# GIF decoders lag one entry behind, so grow the code
# size only once next_code passes 2^code_size.
if next_code > (1 << code_size) and code_size < 12:
code_size += 1
buf = bytes([k])
write(table[buf], code_size)
write(end, code_size)
if bit_cnt > 0:
out.append(bit_buf & 0xFF)
return bytes(out)
def _frame(data):
out = bytearray()
i = 0
while i < len(data):
chunk = data[i:i + 255]
out.append(len(chunk))
out += chunk
i += 255
out.append(0) # block terminator
return bytes(out)
def build_image_data():
iw, ih = _S.get('iw', 1), _S.get('ih', 1)
ncolors = max(2, _S.get('ncolors', 2))
min_code = max(2, (ncolors - 1).bit_length())
pixels = [random.randrange(ncolors) for _ in range(iw * ih)]
comp = _gif_lzw(pixels, min_code)
return bytes([min_code]) + _frame(comp)
def build_sub_stream():
length = random.choice([0, 1, 3, 254, 255, 256, 300, 510])
payload = bytes(random.getrandbits(8) for _ in range(length))
return _frame(payload)