Add 4 libFuzzer targets for Tier A/B API surface; addresses #312
New targets under tests/fuzz/, wired into Makefile FUZZ_TARGETS, each with a
seed corpus (parser seeds are real fixtures prefixed with a loader selector
byte):
- fuzz_audio_dsp SoundBuffer factories + 14 DSP effects + concat/mix.
Self-contained (CPU sample math, no device).
- fuzz_import_parsers TileSetFile/TileMapFile/LdtkProject. Loaders take a
path, so each iteration writes mutated bytes to a temp
file; OSError (IOError) is swallowed as an expected
parse-failure outcome.
- fuzz_texture_factory Texture.from_bytes/composite/hsl_shift byte ingestion.
Multiplication-overflow path documented as out of scope
(would OOM, not crash cleanly).
- fuzz_shader_bindings uniforms[] + PropertyBinding/CallableBinding lifetime,
target Drawable destroyed mid-flight (#270/#271/#277
pattern). Degrades to pure binding-lifetime fuzzing if
shaders are unavailable.
All four signature-validated against the live mcrfpy API before running.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
This commit is contained in:
parent
ea9251c4e8
commit
925699ef0b
24 changed files with 1204 additions and 4 deletions
2
Makefile
2
Makefile
|
|
@ -120,7 +120,7 @@ asan-test: asan
|
|||
# a Python `fuzz_one_input(data)` function loaded from the script named by
|
||||
# the MCRF_FUZZ_TARGET env var. libFuzzer instruments the C++ engine code
|
||||
# where all the #258-#278 bugs live. No atheris dependency.
|
||||
FUZZ_TARGETS := grid_entity property_types anim_timer_scene maps_procgen fov pathfinding_behavior
|
||||
FUZZ_TARGETS := grid_entity property_types anim_timer_scene maps_procgen fov pathfinding_behavior audio_dsp import_parsers texture_factory shader_bindings
|
||||
FUZZ_SECONDS ?= 30
|
||||
|
||||
# Shared env for running the fuzz binary. PYTHONHOME points at the build-fuzz
|
||||
|
|
|
|||
|
|
@ -43,9 +43,19 @@ dir). Seed inputs committed to `tests/fuzz/seeds/<target>/` are read-only.
|
|||
| `fuzz_grid_entity.py` | EntityCollection append/remove/insert/extend/slice across differently-sized grids, `entity.die` during iteration | #258-#263, #273, #274 |
|
||||
| `fuzz_property_types.py` | Random property get/set with type confusion on Frame/Caption/Sprite/Entity/Grid/TileLayer/ColorLayer | #267, #268, #272 |
|
||||
| `fuzz_anim_timer_scene.py` | Animation + Timer state machine, Frame reparenting, scene swap in callbacks | #269, #270, #275, #277 |
|
||||
| `fuzz_maps_procgen.py` | HeightMap/DiscreteMap ops and conversions, NoiseSource.sample, BSP.to_heightmap | new |
|
||||
| `fuzz_fov.py` | grid.compute_fov + is_in_fov, transparent toggling | new |
|
||||
| `fuzz_pathfinding_behavior.py` | DijkstraMap, grid.step, entity behavior fields | #273-adjacent |
|
||||
| `fuzz_maps_procgen.py` | HeightMap/DiscreteMap ops and conversions, NoiseSource.sample, BSP.to_heightmap, ColorLayer/TileLayer `apply_threshold`/`apply_ranges`/`apply_gradient` | new |
|
||||
| `fuzz_fov.py` | grid.compute_fov + is_in_fov, transparent toggling, ColorLayer perspective (`apply_perspective`/`update_perspective`/`clear_perspective`/`draw_fov`) | new |
|
||||
| `fuzz_pathfinding_behavior.py` | DijkstraMap, grid.step, entity behavior fields, `Grid.find_path` + full AStarPath (peek/len/bool/iter) | #273-adjacent |
|
||||
| `fuzz_audio_dsp.py` | SoundBuffer DSP chain: from_samples/tone/sfxr, concat/mix, pitch_shift/lp/hp/echo/reverb/distortion/bit_crush/gain/normalize/reverse/slice/sfxr_mutate | #312 |
|
||||
| `fuzz_import_parsers.py` | Tiled/LDtk external file parsers (`TileSetFile`/`TileMapFile`/`LdtkProject`) via temp-file mutation of real fixtures | #312 |
|
||||
| `fuzz_texture_factory.py` | `Texture.from_bytes`/`composite`/`hsl_shift` byte-ingestion + pixel transforms | #312 |
|
||||
| `fuzz_shader_bindings.py` | Shader uniform binding lifetime: `uniforms[]`, `PropertyBinding`/`CallableBinding`, target destroyed mid-flight (#270/#271/#277 pattern) | #312 |
|
||||
|
||||
Tier C surfaces from #312 are folded into existing targets rather than new
|
||||
files: Line/Circle/Arc, `Scene.children` collection ops, and `find`/`find_all`/
|
||||
`bresenham`/`lock` live in `fuzz_property_types.py`; grid spatial queries +
|
||||
GridPoint dynamic attrs in `fuzz_grid_entity.py`. (The benchmark triplet is
|
||||
deliberately excluded — `end_benchmark()` writes a file per call.)
|
||||
|
||||
Any target not yet implemented is a stub that still compiles and runs cleanly
|
||||
— `make fuzz` reports it as a no-op.
|
||||
|
|
|
|||
194
tests/fuzz/fuzz_audio_dsp.py
Normal file
194
tests/fuzz/fuzz_audio_dsp.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"""fuzz_audio_dsp - SoundBuffer DSP chain fuzzing (#312, Tier B).
|
||||
|
||||
Targets the 14 signal-processing methods + factory/composition entry points
|
||||
on mcrfpy.SoundBuffer. None of these were exercised by the #283 harness.
|
||||
|
||||
Surface (verified against src/PySoundBuffer.cpp and src/audio/AudioEffects.cpp):
|
||||
Factories : from_samples(data:y*, channels:II), tone(...), sfxr(...)
|
||||
Compose : concat([...], overlap), mix([...])
|
||||
Effects : pitch_shift, low_pass, high_pass, echo, reverb, distortion,
|
||||
bit_crush, gain, normalize, reverse, slice, sfxr_mutate
|
||||
Props : duration, sample_count, sample_rate, channels, sfxr_params
|
||||
|
||||
Why it bites: the AudioEffects math runs on a CPU vector<int16_t> with several
|
||||
unguarded divisions by sample_rate and no NaN/inf clamping on most parameters
|
||||
(echo feedback, reverb room/damping, distortion drive, lp/hp cutoff). Extreme
|
||||
and special-float parameters are deliberately injected to flush out UB.
|
||||
|
||||
No audio device or GL context is needed -- this is pure buffer math, so it runs
|
||||
in the windowless fuzz build. Contract: fuzz_one_input(data: bytes) -> None.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
from fuzz_common import ByteStream, EXPECTED_EXCEPTIONS, safe_reset
|
||||
|
||||
MAX_OPS = 24
|
||||
|
||||
WAVEFORMS = ("sine", "square", "saw", "triangle", "noise", "bogus", "")
|
||||
SFXR_PRESETS = ("coin", "laser", "explosion", "powerup", "hurt", "jump",
|
||||
"blip", "not_a_preset", "")
|
||||
|
||||
|
||||
def weird_float(stream):
|
||||
"""A float that is sometimes a special value (inf/-inf/nan) or extreme."""
|
||||
sel = stream.u8() % 8
|
||||
if sel == 0:
|
||||
return float("inf")
|
||||
if sel == 1:
|
||||
return float("-inf")
|
||||
if sel == 2:
|
||||
return float("nan")
|
||||
if sel == 3:
|
||||
return 0.0
|
||||
if sel == 4:
|
||||
return stream.float_in_range(-1e9, 1e9)
|
||||
if sel == 5:
|
||||
return stream.float_in_range(-1.0, 1.0)
|
||||
if sel == 6:
|
||||
return -stream.float_in_range(0.0, 1e6)
|
||||
return stream.float_in_range(0.0, 1000.0)
|
||||
|
||||
|
||||
def weird_int(stream):
|
||||
sel = stream.u8() % 5
|
||||
if sel == 0:
|
||||
return 0
|
||||
if sel == 1:
|
||||
return -stream.int_in_range(0, 1_000_000)
|
||||
if sel == 2:
|
||||
return stream.int_in_range(0, 32)
|
||||
if sel == 3:
|
||||
return stream.int_in_range(0, 1_000_000)
|
||||
return stream.int_in_range(-5, 20)
|
||||
|
||||
|
||||
def make_from_samples(stream):
|
||||
"""Build a SoundBuffer from raw fuzzer bytes interpreted as int16 PCM."""
|
||||
n = stream.int_in_range(0, 4096)
|
||||
raw = stream.take(n)
|
||||
channels = stream.int_in_range(0, 4) # 0 -> ValueError (guarded)
|
||||
rate = stream.pick_one((0, 1, 8000, 22050, 44100, 48000, 96000))
|
||||
return mcrfpy.SoundBuffer.from_samples(raw, channels, rate)
|
||||
|
||||
|
||||
def make_tone(stream):
|
||||
freq = weird_float(stream)
|
||||
dur = stream.float_in_range(0.0, 0.2) # keep buffers small/fast
|
||||
wave = stream.pick_one(WAVEFORMS)
|
||||
return mcrfpy.SoundBuffer.tone(
|
||||
freq, dur, wave,
|
||||
stream.float_in_range(-0.1, 0.5), # attack
|
||||
stream.float_in_range(-0.1, 0.5), # decay
|
||||
stream.float_in_range(-0.5, 2.0), # sustain
|
||||
stream.float_in_range(-0.1, 0.5), # release
|
||||
stream.pick_one((0, 1, 8000, 44100)), # sample_rate
|
||||
)
|
||||
|
||||
|
||||
def make_sfxr(stream):
|
||||
if stream.bool():
|
||||
return mcrfpy.SoundBuffer.sfxr(stream.pick_one(SFXR_PRESETS),
|
||||
stream.u32() if stream.bool() else None)
|
||||
# Custom-parameter mode: 24 floats, several pushed to extremes.
|
||||
params = tuple(weird_float(stream) for _ in range(24))
|
||||
return mcrfpy.SoundBuffer.sfxr(None, None, *params)
|
||||
|
||||
|
||||
FACTORIES = (make_from_samples, make_tone, make_sfxr)
|
||||
|
||||
|
||||
def make_buffer(stream):
|
||||
"""Return a SoundBuffer or None (never raises out)."""
|
||||
factory = stream.pick_one(FACTORIES)
|
||||
try:
|
||||
return factory(stream)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
return None
|
||||
|
||||
|
||||
def apply_effect(stream, buf):
|
||||
"""Apply one randomly chosen effect, returning the (possibly new) buffer."""
|
||||
which = stream.u8() % 12
|
||||
try:
|
||||
if which == 0:
|
||||
return buf.pitch_shift(weird_float(stream))
|
||||
if which == 1:
|
||||
return buf.low_pass(weird_float(stream))
|
||||
if which == 2:
|
||||
return buf.high_pass(weird_float(stream))
|
||||
if which == 3:
|
||||
return buf.echo(weird_float(stream), weird_float(stream), weird_float(stream))
|
||||
if which == 4:
|
||||
return buf.reverb(weird_float(stream), weird_float(stream), weird_float(stream))
|
||||
if which == 5:
|
||||
return buf.distortion(weird_float(stream))
|
||||
if which == 6:
|
||||
return buf.bit_crush(weird_int(stream), weird_int(stream))
|
||||
if which == 7:
|
||||
return buf.gain(weird_float(stream))
|
||||
if which == 8:
|
||||
return buf.normalize()
|
||||
if which == 9:
|
||||
return buf.reverse()
|
||||
if which == 10:
|
||||
return buf.slice(weird_float(stream), weird_float(stream))
|
||||
return buf.sfxr_mutate(weird_float(stream),
|
||||
stream.u32() if stream.bool() else None)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
return buf
|
||||
|
||||
|
||||
def read_props(buf):
|
||||
for name in ("duration", "sample_count", "sample_rate", "channels", "sfxr_params"):
|
||||
try:
|
||||
_ = getattr(buf, name)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
|
||||
|
||||
def fuzz_compose(stream, pool):
|
||||
"""Exercise concat/mix over a list of accumulated buffers."""
|
||||
if not pool:
|
||||
return None
|
||||
k = stream.int_in_range(0, len(pool))
|
||||
chosen = [pool[stream.int_in_range(0, len(pool) - 1)] for _ in range(k)]
|
||||
try:
|
||||
if stream.bool():
|
||||
return mcrfpy.SoundBuffer.concat(chosen, weird_float(stream))
|
||||
return mcrfpy.SoundBuffer.mix(chosen)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
return None
|
||||
|
||||
|
||||
def fuzz_one_input(data):
|
||||
stream = ByteStream(data)
|
||||
pool = []
|
||||
try:
|
||||
n = stream.int_in_range(1, MAX_OPS)
|
||||
for _ in range(n):
|
||||
if stream.remaining < 1:
|
||||
break
|
||||
choice = stream.u8() % 5
|
||||
if choice == 0 or not pool:
|
||||
buf = make_buffer(stream)
|
||||
if buf is not None:
|
||||
read_props(buf)
|
||||
if len(pool) < 6:
|
||||
pool.append(buf)
|
||||
elif choice == 1:
|
||||
buf = fuzz_compose(stream, pool)
|
||||
if buf is not None and len(pool) < 6:
|
||||
pool.append(buf)
|
||||
else:
|
||||
# Apply a chain of effects to an existing buffer.
|
||||
buf = pool[stream.int_in_range(0, len(pool) - 1)]
|
||||
chain = stream.int_in_range(1, 4)
|
||||
for _ in range(chain):
|
||||
buf = apply_effect(stream, buf)
|
||||
if buf is None:
|
||||
break
|
||||
if buf is not None:
|
||||
read_props(buf)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
184
tests/fuzz/fuzz_import_parsers.py
Normal file
184
tests/fuzz/fuzz_import_parsers.py
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
"""fuzz_import_parsers - Tiled/LDtk external file parser fuzzing (#312, Tier A).
|
||||
|
||||
External file parsers are historically the highest-yield fuzz target. This
|
||||
drives the three loaders that ingest untrusted XML/JSON from disk:
|
||||
|
||||
mcrfpy.TileSetFile(path) -- .tsx (XML) / .tsj / .json
|
||||
mcrfpy.TileMapFile(path) -- .tmx (XML) / .tmj / .json
|
||||
mcrfpy.LdtkProject(path) -- .ldtk (JSON)
|
||||
|
||||
All three take a filesystem PATH (verified: src/tiled/PyTileSetFile.cpp:20,
|
||||
src/tiled/PyTileMapFile.cpp:22, src/ldtk/PyLdtkProject.cpp:24 -- each parses
|
||||
"s"). So each iteration writes the fuzzer bytes to a temp file with a
|
||||
format-appropriate extension, then loads it.
|
||||
|
||||
Input layout: byte[0] selects the loader/extension (see LOADERS); the rest is
|
||||
the file body. Seeds are built as bytes([selector]) + <fixture file bytes> so
|
||||
libFuzzer mutates real .tsx/.tmx/.tmj/.ldtk content (see tests/fuzz/seeds/).
|
||||
|
||||
After a successful load the parsed object's properties, lookups, and dynamic
|
||||
IntEnum builders (terrain_enum) are exercised -- the underlying parse code has
|
||||
unguarded std::stoi on wangid tokens (src/tiled/TiledParse.cpp:200-202) and
|
||||
divisions that depend on parsed grid_size/tile_size (src/ldtk/LdtkParse.cpp).
|
||||
|
||||
Contract: fuzz_one_input(data: bytes) -> None.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import mcrfpy
|
||||
|
||||
from fuzz_common import ByteStream, EXPECTED_EXCEPTIONS, safe_reset
|
||||
|
||||
# The loaders wrap std::exceptions (malformed file, unresolvable external
|
||||
# tileset reference, etc.) in PyExc_IOError == OSError, which the shared
|
||||
# EXPECTED_EXCEPTIONS tuple does not include. Swallow it here: a parse failure
|
||||
# on garbage input is the expected outcome, not a bug.
|
||||
PARSER_EXPECTED = EXPECTED_EXCEPTIONS + (OSError,)
|
||||
|
||||
# (extension, loader-callable). byte[0] % len(LOADERS) selects one.
|
||||
LOADERS = (
|
||||
("tsx", lambda p: mcrfpy.TileSetFile(p)),
|
||||
("tmx", lambda p: mcrfpy.TileMapFile(p)),
|
||||
("tmj", lambda p: mcrfpy.TileMapFile(p)),
|
||||
("ldtk", lambda p: mcrfpy.LdtkProject(p)),
|
||||
("tsj", lambda p: mcrfpy.TileSetFile(p)),
|
||||
("json", lambda p: mcrfpy.TileMapFile(p)),
|
||||
)
|
||||
|
||||
_TMP_DIR = os.environ.get("TMPDIR", "/tmp")
|
||||
_TMP_BASE = os.path.join(_TMP_DIR, "mcrf_fuzz_parser_%d" % os.getpid())
|
||||
|
||||
|
||||
def _read_seq(obj, names):
|
||||
"""Read a list of property names, swallowing expected errors."""
|
||||
for name in names:
|
||||
try:
|
||||
_ = getattr(obj, name)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
|
||||
|
||||
def exercise_tileset(stream, ts):
|
||||
_read_seq(ts, ("name", "tile_width", "tile_height", "tile_count", "columns",
|
||||
"margin", "spacing", "image_source", "properties", "wang_sets"))
|
||||
# tile_info over a few ids including out-of-range
|
||||
for _ in range(stream.int_in_range(0, 4)):
|
||||
try:
|
||||
ts.tile_info(stream.int_in_range(-5, 4096))
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
# Walk wang sets; terrain_enum builds a Python IntEnum from parsed colors.
|
||||
try:
|
||||
for ws in ts.wang_sets:
|
||||
_read_seq(ws, ("name", "type", "color_count", "colors"))
|
||||
try:
|
||||
ws.terrain_enum()
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
|
||||
|
||||
def exercise_tilemap(stream, tm):
|
||||
_read_seq(tm, ("width", "height", "tile_width", "tile_height", "orientation",
|
||||
"properties", "tileset_count", "tile_layer_names",
|
||||
"object_layer_names"))
|
||||
try:
|
||||
for i in range(min(tm.tileset_count, 4)):
|
||||
try:
|
||||
tm.tileset(i)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
try:
|
||||
for name in list(tm.tile_layer_names)[:3]:
|
||||
try:
|
||||
tm.tile_layer_data(name)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
for _ in range(stream.int_in_range(0, 4)):
|
||||
try:
|
||||
tm.resolve_gid(stream.int_in_range(-5, 1 << 20))
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
try:
|
||||
for name in list(tm.object_layer_names)[:3]:
|
||||
try:
|
||||
tm.object_layer(name)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
|
||||
|
||||
def exercise_ldtk(stream, proj):
|
||||
_read_seq(proj, ("version", "tileset_names", "ruleset_names", "level_names",
|
||||
"enums"))
|
||||
try:
|
||||
for name in list(proj.tileset_names)[:3]:
|
||||
try:
|
||||
proj.tileset(name)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
try:
|
||||
for name in list(proj.ruleset_names)[:3]:
|
||||
try:
|
||||
rs = proj.ruleset(name)
|
||||
_read_seq(rs, ("name", "grid_size", "value_count", "values",
|
||||
"rule_count", "group_count"))
|
||||
try:
|
||||
rs.terrain_enum()
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
try:
|
||||
for name in list(proj.level_names)[:3]:
|
||||
try:
|
||||
proj.level(name)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
|
||||
|
||||
EXERCISE = {
|
||||
"tsx": exercise_tileset,
|
||||
"tsj": exercise_tileset,
|
||||
"tmx": exercise_tilemap,
|
||||
"tmj": exercise_tilemap,
|
||||
"json": exercise_tilemap,
|
||||
"ldtk": exercise_ldtk,
|
||||
}
|
||||
|
||||
|
||||
def fuzz_one_input(data):
|
||||
stream = ByteStream(data)
|
||||
if stream.remaining < 1:
|
||||
return
|
||||
ext, loader = LOADERS[stream.u8() % len(LOADERS)]
|
||||
body = stream.take(stream.remaining)
|
||||
path = "%s.%s" % (_TMP_BASE, ext)
|
||||
try:
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(body)
|
||||
except OSError:
|
||||
return
|
||||
try:
|
||||
obj = loader(path)
|
||||
EXERCISE[ext](stream, obj)
|
||||
except PARSER_EXPECTED:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
203
tests/fuzz/fuzz_shader_bindings.py
Normal file
203
tests/fuzz/fuzz_shader_bindings.py
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
"""fuzz_shader_bindings - shader uniform binding lifetime fuzzing (#312, Tier A).
|
||||
|
||||
Targets the exact pattern that produced #270 / #271 / #277: uniform bindings and
|
||||
the UniformCollection are lifetime-coupled to a Drawable via weak_ptr, and the
|
||||
binding/collection can outlive the Drawable. This hammers create -> bind ->
|
||||
destroy-target -> evaluate sequences.
|
||||
|
||||
Surface (verified against src/PyUniformBinding.cpp, src/PyUniformCollection.cpp,
|
||||
src/PyShader.cpp, src/UIDrawable.cpp):
|
||||
drawable.uniforms -> UniformCollection (weak_ptr owner)
|
||||
uniforms[name] = float | (x,y[,z[,w]]) | PropertyBinding | CallableBinding
|
||||
PropertyBinding(target, property) -- weak_ptr<UIDrawable> target
|
||||
CallableBinding(callable) -- Python callable, evaluated lazily
|
||||
drawable.shader = Shader(frag_src) -- requires GL; degrades gracefully
|
||||
|
||||
If sf::Shader::isAvailable() is false in the windowless fuzz build, Shader()
|
||||
raises RuntimeError (PyShader.cpp:82) which is swallowed -- but the binding /
|
||||
UniformCollection bookkeeping (where the bugs lived) runs regardless of whether
|
||||
a GLSL program actually compiles. Drawables are kept DETACHED (never appended to
|
||||
a scene) so that `del` drops the last shared_ptr and the weak_ptr safety paths
|
||||
are actually taken.
|
||||
|
||||
Contract: fuzz_one_input(data: bytes) -> None.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
from fuzz_common import ByteStream, EXPECTED_EXCEPTIONS, safe_reset
|
||||
|
||||
MAX_OPS = 24
|
||||
|
||||
# Animatable float properties shared widely enough to satisfy hasProperty()
|
||||
# on most drawables; invalid pairings raise ValueError and are swallowed.
|
||||
PROP_NAMES = ("x", "y", "opacity", "w", "h", "z_index", "radius", "thickness",
|
||||
"rotation", "outline", "not_a_real_property")
|
||||
|
||||
GOOD_FRAG = (
|
||||
"uniform float u; void main(){ gl_FragColor = vec4(u,0.0,0.0,1.0); }"
|
||||
)
|
||||
BAD_FRAG = "this is not glsl {{{"
|
||||
|
||||
|
||||
def make_drawable(stream):
|
||||
"""Construct one DETACHED drawable (not added to any scene)."""
|
||||
which = stream.u8() % 7
|
||||
try:
|
||||
if which == 0:
|
||||
return mcrfpy.Frame(pos=(0, 0), size=(10, 10))
|
||||
if which == 1:
|
||||
return mcrfpy.Caption((0, 0), None, "x")
|
||||
if which == 2:
|
||||
return mcrfpy.Sprite(pos=(0, 0))
|
||||
if which == 3:
|
||||
return mcrfpy.Grid(grid_size=(4, 4))
|
||||
if which == 4:
|
||||
return mcrfpy.Line(start=(0, 0), end=(5, 5))
|
||||
if which == 5:
|
||||
return mcrfpy.Circle(center=(0, 0), radius=4.0)
|
||||
return mcrfpy.Arc(center=(0, 0), radius=4.0, start_angle=0.0, end_angle=90.0)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
return None
|
||||
|
||||
|
||||
def uniform_value(stream, target):
|
||||
"""Return a value to assign into a uniform slot."""
|
||||
sel = stream.u8() % 7
|
||||
if sel == 0:
|
||||
return stream.float_in_range(-1e6, 1e6)
|
||||
if sel == 1:
|
||||
return (stream.float_in_range(-1, 1), stream.float_in_range(-1, 1))
|
||||
if sel == 2:
|
||||
return tuple(stream.float_in_range(-1, 1) for _ in range(3))
|
||||
if sel == 3:
|
||||
return tuple(stream.float_in_range(-1, 1) for _ in range(4))
|
||||
if sel == 4 and target is not None:
|
||||
# PropertyBinding back to the target drawable (or another one).
|
||||
return ("propbind", stream.pick_one(PROP_NAMES))
|
||||
if sel == 5:
|
||||
return ("callbind", stream.u8() % 3)
|
||||
# Deliberately wrong shapes.
|
||||
return stream.pick_one((None, "str", (), (1, 2, 3, 4, 5), {"x": 1}))
|
||||
|
||||
|
||||
def _callable_for(kind):
|
||||
if kind == 0:
|
||||
return lambda: 1.0
|
||||
if kind == 1:
|
||||
return lambda: (_ for _ in ()).throw(ValueError("boom")) # raises on call
|
||||
return lambda: "not a float" # wrong return type
|
||||
|
||||
|
||||
def set_uniform(stream, drawable, name):
|
||||
raw = uniform_value(stream, drawable)
|
||||
value = raw
|
||||
try:
|
||||
if isinstance(raw, tuple) and len(raw) == 2 and raw[0] == "propbind":
|
||||
value = mcrfpy.PropertyBinding(drawable, raw[1])
|
||||
elif isinstance(raw, tuple) and len(raw) == 2 and raw[0] == "callbind":
|
||||
value = mcrfpy.CallableBinding(_callable_for(raw[1]))
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
return None
|
||||
try:
|
||||
drawable.uniforms[name] = value
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
return value if isinstance(value, (mcrfpy.PropertyBinding, mcrfpy.CallableBinding)) else None
|
||||
|
||||
|
||||
def read_uniforms(drawable, names):
|
||||
try:
|
||||
coll = drawable.uniforms
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
return
|
||||
try:
|
||||
_ = len(coll)
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
pass
|
||||
for name in names:
|
||||
try:
|
||||
_ = coll[name]
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
try:
|
||||
for _k in coll:
|
||||
pass
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
|
||||
|
||||
def read_binding(binding):
|
||||
"""Read a binding after its target may have died (the safety check)."""
|
||||
if binding is None:
|
||||
return
|
||||
for name in ("value", "is_valid", "target", "property", "callable"):
|
||||
try:
|
||||
_ = getattr(binding, name)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
try:
|
||||
repr(binding)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
|
||||
|
||||
def try_assign_shader(stream, drawable):
|
||||
src = GOOD_FRAG if stream.bool() else BAD_FRAG
|
||||
try:
|
||||
sh = mcrfpy.Shader(src, bool(stream.bool()))
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
return
|
||||
try:
|
||||
drawable.shader = sh
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
|
||||
|
||||
def fuzz_lifecycle(stream):
|
||||
"""The core lifetime pattern: bind to a target, drop it, then evaluate."""
|
||||
target = make_drawable(stream)
|
||||
if target is None:
|
||||
return
|
||||
if stream.bool():
|
||||
try_assign_shader(stream, target)
|
||||
bindings = []
|
||||
names = []
|
||||
for _ in range(stream.int_in_range(1, 5)):
|
||||
name = stream.ascii_str(6) or "u"
|
||||
names.append(name)
|
||||
b = set_uniform(stream, target, name)
|
||||
if b is not None:
|
||||
bindings.append(b)
|
||||
read_uniforms(target, names)
|
||||
# Drop the only strong ref to the target while bindings/collection persist.
|
||||
if stream.bool():
|
||||
try:
|
||||
coll = target.uniforms # keep the collection alive past target
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
coll = None
|
||||
del target
|
||||
for b in bindings:
|
||||
read_binding(b)
|
||||
if coll is not None:
|
||||
for name in names:
|
||||
try:
|
||||
_ = coll[name]
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
else:
|
||||
del target
|
||||
for b in bindings:
|
||||
read_binding(b)
|
||||
|
||||
|
||||
def fuzz_one_input(data):
|
||||
stream = ByteStream(data)
|
||||
try:
|
||||
n = stream.int_in_range(1, MAX_OPS)
|
||||
for _ in range(n):
|
||||
if stream.remaining < 1:
|
||||
break
|
||||
fuzz_lifecycle(stream)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
164
tests/fuzz/fuzz_texture_factory.py
Normal file
164
tests/fuzz/fuzz_texture_factory.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
"""fuzz_texture_factory - Texture byte-ingestion + pixel-transform fuzzing
|
||||
(#312, Tier B).
|
||||
|
||||
Raw byte ingestion and pixel transforms are classic memory-safety surfaces.
|
||||
Drives the three Texture factory/transform entry points (verified against
|
||||
src/PyTexture.cpp):
|
||||
|
||||
Texture.from_bytes(data:y*, width, height, sprite_w, sprite_h, name='') [classmethod]
|
||||
Texture.composite(layers:list, sprite_w, sprite_h, name='') [classmethod]
|
||||
texture.hsl_shift(hue, sat=0.0, lit=0.0) [instance]
|
||||
|
||||
from_bytes validates len(data) == width*height*4 (PyTexture.cpp:283) but does
|
||||
NOT bound width/height to be positive, so zero dimensions and mismatched
|
||||
lengths are stressed here. composite checks list/element types and equal layer
|
||||
dimensions (PyTexture.cpp:315-360); empty lists, None/non-Texture elements, and
|
||||
mismatched sizes are all exercised.
|
||||
|
||||
NOTE: the width*height*4 multiplication-overflow path (huge dimensions paired
|
||||
with a coincidentally-matching tiny buffer, PyTexture.cpp:283) is intentionally
|
||||
NOT probed here -- forcing it would trigger a multi-GB sf::Image allocation and
|
||||
OOM the fuzzer rather than produce a clean crash. Dimensions are bounded so the
|
||||
length check rejects oversized inputs before any allocation. That overflow case
|
||||
warrants a separate, manual ASan check.
|
||||
|
||||
Creating sf::Texture needs a GL context; the fuzz build is SFML-backed (not
|
||||
headless) and already loads default_texture at startup, so creation works.
|
||||
|
||||
Contract: fuzz_one_input(data: bytes) -> None.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
from fuzz_common import ByteStream, EXPECTED_EXCEPTIONS, safe_reset
|
||||
|
||||
MAX_OPS = 24
|
||||
|
||||
|
||||
def weird_float(stream):
|
||||
sel = stream.u8() % 6
|
||||
if sel == 0:
|
||||
return float("inf")
|
||||
if sel == 1:
|
||||
return float("-inf")
|
||||
if sel == 2:
|
||||
return float("nan")
|
||||
if sel == 3:
|
||||
return stream.float_in_range(-720.0, 720.0)
|
||||
if sel == 4:
|
||||
return stream.float_in_range(-5.0, 5.0)
|
||||
return 0.0
|
||||
|
||||
|
||||
def _default_texture():
|
||||
try:
|
||||
return getattr(mcrfpy, "default_texture", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def make_valid_texture(stream):
|
||||
"""Construct a small, valid Texture via from_bytes (matched length)."""
|
||||
w = stream.int_in_range(1, 16)
|
||||
h = stream.int_in_range(1, 16)
|
||||
need = w * h * 4
|
||||
raw = stream.take(need)
|
||||
if len(raw) < need:
|
||||
raw = raw + bytes(need - len(raw))
|
||||
sw = stream.int_in_range(1, w)
|
||||
sh = stream.int_in_range(1, h)
|
||||
return mcrfpy.Texture.from_bytes(raw, w, h, sw, sh)
|
||||
|
||||
|
||||
def fuzz_from_bytes_mismatch(stream):
|
||||
"""Deliberately mismatched dims/length/zero dims -> error paths."""
|
||||
n = stream.int_in_range(0, 1024)
|
||||
raw = stream.take(n)
|
||||
w = stream.int_in_range(-2, 64)
|
||||
h = stream.int_in_range(-2, 64)
|
||||
sw = stream.int_in_range(-2, 64)
|
||||
sh = stream.int_in_range(-2, 64)
|
||||
try:
|
||||
mcrfpy.Texture.from_bytes(raw, w, h, sw, sh)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except MemoryError:
|
||||
pass
|
||||
|
||||
|
||||
def fuzz_composite(stream, pool):
|
||||
"""Composite a list mixing valid textures, None, non-textures, mismatches."""
|
||||
layers = []
|
||||
count = stream.int_in_range(0, 5)
|
||||
for _ in range(count):
|
||||
sel = stream.u8() % 5
|
||||
if sel == 0 and pool:
|
||||
layers.append(pool[stream.int_in_range(0, len(pool) - 1)])
|
||||
elif sel == 1:
|
||||
layers.append(None)
|
||||
elif sel == 2:
|
||||
layers.append("not a texture")
|
||||
elif sel == 3:
|
||||
dt = _default_texture()
|
||||
layers.append(dt)
|
||||
else:
|
||||
try:
|
||||
layers.append(make_valid_texture(stream))
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except MemoryError:
|
||||
pass
|
||||
sw = stream.int_in_range(-2, 32)
|
||||
sh = stream.int_in_range(-2, 32)
|
||||
# Sometimes pass a non-list to hit the PyList_Check path.
|
||||
arg = layers if stream.bool() else tuple(layers)
|
||||
try:
|
||||
mcrfpy.Texture.composite(arg, sw, sh)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except MemoryError:
|
||||
pass
|
||||
|
||||
|
||||
def fuzz_hsl_shift(stream, pool):
|
||||
tex = None
|
||||
if pool and stream.bool():
|
||||
tex = pool[stream.int_in_range(0, len(pool) - 1)]
|
||||
else:
|
||||
tex = _default_texture()
|
||||
if tex is None:
|
||||
return
|
||||
try:
|
||||
tex.hsl_shift(weird_float(stream), weird_float(stream), weird_float(stream))
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except MemoryError:
|
||||
pass
|
||||
|
||||
|
||||
def fuzz_one_input(data):
|
||||
stream = ByteStream(data)
|
||||
pool = []
|
||||
try:
|
||||
n = stream.int_in_range(1, MAX_OPS)
|
||||
for _ in range(n):
|
||||
if stream.remaining < 1:
|
||||
break
|
||||
choice = stream.u8() % 4
|
||||
if choice == 0:
|
||||
try:
|
||||
tex = make_valid_texture(stream)
|
||||
if len(pool) < 6:
|
||||
pool.append(tex)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
except MemoryError:
|
||||
pass
|
||||
elif choice == 1:
|
||||
fuzz_from_bytes_mismatch(stream)
|
||||
elif choice == 2:
|
||||
fuzz_composite(stream, pool)
|
||||
else:
|
||||
fuzz_hsl_shift(stream, pool)
|
||||
except EXPECTED_EXCEPTIONS:
|
||||
pass
|
||||
0
tests/fuzz/seeds/audio_dsp/.gitkeep
Normal file
0
tests/fuzz/seeds/audio_dsp/.gitkeep
Normal file
BIN
tests/fuzz/seeds/audio_dsp/seed_compose.bin
Normal file
BIN
tests/fuzz/seeds/audio_dsp/seed_compose.bin
Normal file
Binary file not shown.
BIN
tests/fuzz/seeds/audio_dsp/seed_from_samples.bin
Normal file
BIN
tests/fuzz/seeds/audio_dsp/seed_from_samples.bin
Normal file
Binary file not shown.
BIN
tests/fuzz/seeds/audio_dsp/seed_sfxr.bin
Normal file
BIN
tests/fuzz/seeds/audio_dsp/seed_sfxr.bin
Normal file
Binary file not shown.
BIN
tests/fuzz/seeds/audio_dsp/seed_tone.bin
Normal file
BIN
tests/fuzz/seeds/audio_dsp/seed_tone.bin
Normal file
Binary file not shown.
0
tests/fuzz/seeds/import_parsers/.gitkeep
Normal file
0
tests/fuzz/seeds/import_parsers/.gitkeep
Normal file
326
tests/fuzz/seeds/import_parsers/seed_ldtk.bin
Normal file
326
tests/fuzz/seeds/import_parsers/seed_ldtk.bin
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
{
|
||||
"__header__": {
|
||||
"fileType": "LDtk Project JSON",
|
||||
"app": "LDtk",
|
||||
"doc": "https://ldtk.io/json",
|
||||
"schema": "https://ldtk.io/files/JSON_SCHEMA.json",
|
||||
"appAuthor": "Sebastien 'deepnight' Benard",
|
||||
"appVersion": "1.5.3",
|
||||
"url": "https://ldtk.io"
|
||||
},
|
||||
"iid": "test-project-iid",
|
||||
"jsonVersion": "1.5.3",
|
||||
"appBuildId": 0,
|
||||
"nextUid": 100,
|
||||
"identifierStyle": "Capitalize",
|
||||
"toc": [],
|
||||
"worldLayout": "Free",
|
||||
"worldGridWidth": 256,
|
||||
"worldGridHeight": 256,
|
||||
"defaultLevelWidth": 256,
|
||||
"defaultLevelHeight": 256,
|
||||
"defaultPivotX": 0,
|
||||
"defaultPivotY": 0,
|
||||
"defaultGridSize": 16,
|
||||
"defaultEntityWidth": 16,
|
||||
"defaultEntityHeight": 16,
|
||||
"bgColor": "#40465B",
|
||||
"defaultLevelBgColor": "#696A79",
|
||||
"minifyJson": false,
|
||||
"externalLevels": false,
|
||||
"exportTiled": false,
|
||||
"simplifiedExport": false,
|
||||
"imageExportMode": "None",
|
||||
"exportLevelBg": true,
|
||||
"pngFilePattern": null,
|
||||
"backupOnSave": false,
|
||||
"backupLimit": 10,
|
||||
"backupRelPath": null,
|
||||
"levelNamePattern": "Level_%idx",
|
||||
"tutorialDesc": null,
|
||||
"customCommands": [],
|
||||
"flags": [],
|
||||
"defs": {
|
||||
"layers": [
|
||||
{
|
||||
"__type": "IntGrid",
|
||||
"identifier": "Terrain",
|
||||
"type": "IntGrid",
|
||||
"uid": 1,
|
||||
"doc": null,
|
||||
"uiColor": null,
|
||||
"gridSize": 16,
|
||||
"guideGridWid": 0,
|
||||
"guideGridHei": 0,
|
||||
"displayOpacity": 1,
|
||||
"inactiveOpacity": 0.6,
|
||||
"hideInList": false,
|
||||
"hideFieldsWhenInactive": true,
|
||||
"canSelectWhenInactive": true,
|
||||
"renderInWorldView": true,
|
||||
"pxOffsetX": 0,
|
||||
"pxOffsetY": 0,
|
||||
"parallaxFactorX": 0,
|
||||
"parallaxFactorY": 0,
|
||||
"parallaxScaling": true,
|
||||
"requiredTags": [],
|
||||
"excludedTags": [],
|
||||
"autoTilesetDefUid": 10,
|
||||
"tilesetDefUid": 10,
|
||||
"tilePivotX": 0,
|
||||
"tilePivotY": 0,
|
||||
"biomeFieldUid": null,
|
||||
"intGridValues": [
|
||||
{ "value": 1, "identifier": "wall", "color": "#FFFFFF", "tile": null, "groupUid": 0 },
|
||||
{ "value": 2, "identifier": "floor", "color": "#808080", "tile": null, "groupUid": 0 },
|
||||
{ "value": 3, "identifier": "water", "color": "#0000FF", "tile": null, "groupUid": 0 }
|
||||
],
|
||||
"intGridValuesGroups": [],
|
||||
"autoRuleGroups": [
|
||||
{
|
||||
"uid": 50,
|
||||
"name": "Walls",
|
||||
"color": null,
|
||||
"icon": null,
|
||||
"active": true,
|
||||
"isOptional": false,
|
||||
"rules": [
|
||||
{
|
||||
"uid": 51,
|
||||
"active": true,
|
||||
"size": 3,
|
||||
"tileRectsIds": [[[0, 0]]],
|
||||
"alpha": 1,
|
||||
"chance": 1,
|
||||
"breakOnMatch": true,
|
||||
"pattern": [
|
||||
0, 0, 0,
|
||||
0, 1, 0,
|
||||
0, 0, 0
|
||||
],
|
||||
"flipX": false,
|
||||
"flipY": false,
|
||||
"xModulo": 1,
|
||||
"yModulo": 1,
|
||||
"xOffset": 0,
|
||||
"yOffset": 0,
|
||||
"tileXOffset": 0,
|
||||
"tileYOffset": 0,
|
||||
"tileRandomXMin": 0,
|
||||
"tileRandomXMax": 0,
|
||||
"tileRandomYMin": 0,
|
||||
"tileRandomYMax": 0,
|
||||
"checker": "None",
|
||||
"tileMode": "Single",
|
||||
"pivotX": 0,
|
||||
"pivotY": 0,
|
||||
"outOfBoundsValue": -1,
|
||||
"perlinActive": false,
|
||||
"perlinSeed": 0,
|
||||
"perlinScale": 0.2,
|
||||
"perlinOctaves": 2,
|
||||
"invalidated": false
|
||||
},
|
||||
{
|
||||
"uid": 52,
|
||||
"active": true,
|
||||
"size": 3,
|
||||
"tileRectsIds": [[[16, 0]]],
|
||||
"alpha": 1,
|
||||
"chance": 1,
|
||||
"breakOnMatch": true,
|
||||
"pattern": [
|
||||
0, -1, 0,
|
||||
0, 1, 0,
|
||||
0, 0, 0
|
||||
],
|
||||
"flipX": true,
|
||||
"flipY": false,
|
||||
"xModulo": 1,
|
||||
"yModulo": 1,
|
||||
"xOffset": 0,
|
||||
"yOffset": 0,
|
||||
"tileXOffset": 0,
|
||||
"tileYOffset": 0,
|
||||
"tileRandomXMin": 0,
|
||||
"tileRandomXMax": 0,
|
||||
"tileRandomYMin": 0,
|
||||
"tileRandomYMax": 0,
|
||||
"checker": "None",
|
||||
"tileMode": "Single",
|
||||
"pivotX": 0,
|
||||
"pivotY": 0,
|
||||
"outOfBoundsValue": -1,
|
||||
"perlinActive": false,
|
||||
"perlinSeed": 0,
|
||||
"perlinScale": 0.2,
|
||||
"perlinOctaves": 2,
|
||||
"invalidated": false
|
||||
}
|
||||
],
|
||||
"usesWizard": false,
|
||||
"requiredBiomeValues": [],
|
||||
"biomeRequirementMode": 0
|
||||
},
|
||||
{
|
||||
"uid": 60,
|
||||
"name": "Floors",
|
||||
"color": null,
|
||||
"icon": null,
|
||||
"active": true,
|
||||
"isOptional": false,
|
||||
"rules": [
|
||||
{
|
||||
"uid": 61,
|
||||
"active": true,
|
||||
"size": 1,
|
||||
"tileRectsIds": [[[32, 0]], [[48, 0]]],
|
||||
"alpha": 1,
|
||||
"chance": 1,
|
||||
"breakOnMatch": true,
|
||||
"pattern": [2],
|
||||
"flipX": false,
|
||||
"flipY": false,
|
||||
"xModulo": 1,
|
||||
"yModulo": 1,
|
||||
"xOffset": 0,
|
||||
"yOffset": 0,
|
||||
"tileXOffset": 0,
|
||||
"tileYOffset": 0,
|
||||
"tileRandomXMin": 0,
|
||||
"tileRandomXMax": 0,
|
||||
"tileRandomYMin": 0,
|
||||
"tileRandomYMax": 0,
|
||||
"checker": "None",
|
||||
"tileMode": "Single",
|
||||
"pivotX": 0,
|
||||
"pivotY": 0,
|
||||
"outOfBoundsValue": -1,
|
||||
"perlinActive": false,
|
||||
"perlinSeed": 0,
|
||||
"perlinScale": 0.2,
|
||||
"perlinOctaves": 2,
|
||||
"invalidated": false
|
||||
}
|
||||
],
|
||||
"usesWizard": false,
|
||||
"requiredBiomeValues": [],
|
||||
"biomeRequirementMode": 0
|
||||
}
|
||||
],
|
||||
"autoSourceLayerDefUid": null
|
||||
}
|
||||
],
|
||||
"entities": [],
|
||||
"tilesets": [
|
||||
{
|
||||
"__cWid": 4,
|
||||
"__cHei": 4,
|
||||
"identifier": "Test_Tileset",
|
||||
"uid": 10,
|
||||
"relPath": "test_tileset.png",
|
||||
"embedAtlas": null,
|
||||
"pxWid": 64,
|
||||
"pxHei": 64,
|
||||
"tileGridSize": 16,
|
||||
"spacing": 0,
|
||||
"padding": 0,
|
||||
"tags": [],
|
||||
"tagsSourceEnumUid": null,
|
||||
"enumTags": [],
|
||||
"customData": [],
|
||||
"savedSelections": [],
|
||||
"cachedPixelData": null
|
||||
}
|
||||
],
|
||||
"enums": [
|
||||
{
|
||||
"identifier": "TileType",
|
||||
"uid": 20,
|
||||
"values": [
|
||||
{ "id": "Solid", "tileRect": null, "color": 0 },
|
||||
{ "id": "Platform", "tileRect": null, "color": 0 }
|
||||
],
|
||||
"iconTilesetUid": null,
|
||||
"externalRelPath": null,
|
||||
"externalFileChecksum": null,
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"externalEnums": [],
|
||||
"levelFields": []
|
||||
},
|
||||
"levels": [
|
||||
{
|
||||
"identifier": "Level_0",
|
||||
"iid": "level-0-iid",
|
||||
"uid": 30,
|
||||
"worldX": 0,
|
||||
"worldY": 0,
|
||||
"worldDepth": 0,
|
||||
"pxWid": 80,
|
||||
"pxHei": 80,
|
||||
"__bgColor": "#696A79",
|
||||
"bgColor": null,
|
||||
"useAutoIdentifier": false,
|
||||
"bgRelPath": null,
|
||||
"bgPos": null,
|
||||
"bgPivotX": 0.5,
|
||||
"bgPivotY": 0.5,
|
||||
"__smartColor": "#ADADB5",
|
||||
"__bgPos": null,
|
||||
"externalRelPath": null,
|
||||
"fieldInstances": [],
|
||||
"layerInstances": [
|
||||
{
|
||||
"__identifier": "Terrain",
|
||||
"__type": "IntGrid",
|
||||
"__cWid": 5,
|
||||
"__cHei": 5,
|
||||
"__gridSize": 16,
|
||||
"__opacity": 1,
|
||||
"__pxTotalOffsetX": 0,
|
||||
"__pxTotalOffsetY": 0,
|
||||
"__tilesetDefUid": 10,
|
||||
"__tilesetRelPath": "test_tileset.png",
|
||||
"iid": "layer-iid",
|
||||
"levelId": 30,
|
||||
"layerDefUid": 1,
|
||||
"pxOffsetX": 0,
|
||||
"pxOffsetY": 0,
|
||||
"visible": true,
|
||||
"optionalRules": [],
|
||||
"intGridCsv": [
|
||||
1, 1, 1, 1, 1,
|
||||
1, 2, 2, 2, 1,
|
||||
1, 2, 3, 2, 1,
|
||||
1, 2, 2, 2, 1,
|
||||
1, 1, 1, 1, 1
|
||||
],
|
||||
"autoLayerTiles": [
|
||||
{ "px": [0, 0], "src": [0, 0], "f": 0, "t": 0, "d": [51], "a": 1 },
|
||||
{ "px": [16, 0], "src": [0, 0], "f": 0, "t": 0, "d": [51], "a": 1 },
|
||||
{ "px": [32, 0], "src": [0, 0], "f": 0, "t": 0, "d": [51], "a": 1 },
|
||||
{ "px": [48, 0], "src": [0, 0], "f": 0, "t": 0, "d": [51], "a": 1 },
|
||||
{ "px": [64, 0], "src": [0, 0], "f": 0, "t": 0, "d": [51], "a": 1 },
|
||||
{ "px": [16, 16], "src": [32, 0], "f": 0, "t": 2, "d": [61], "a": 1 },
|
||||
{ "px": [32, 16], "src": [32, 0], "f": 0, "t": 2, "d": [61], "a": 1 },
|
||||
{ "px": [48, 16], "src": [32, 0], "f": 0, "t": 2, "d": [61], "a": 1 },
|
||||
{ "px": [16, 32], "src": [32, 0], "f": 0, "t": 2, "d": [61], "a": 1 },
|
||||
{ "px": [48, 32], "src": [32, 0], "f": 0, "t": 2, "d": [61], "a": 1 },
|
||||
{ "px": [16, 48], "src": [32, 0], "f": 0, "t": 2, "d": [61], "a": 1 },
|
||||
{ "px": [32, 48], "src": [32, 0], "f": 0, "t": 2, "d": [61], "a": 1 },
|
||||
{ "px": [48, 48], "src": [32, 0], "f": 0, "t": 2, "d": [61], "a": 1 }
|
||||
],
|
||||
"seed": 1234,
|
||||
"overrideTilesetUid": null,
|
||||
"gridTiles": [],
|
||||
"entityInstances": []
|
||||
}
|
||||
],
|
||||
"__neighbours": []
|
||||
}
|
||||
],
|
||||
"worlds": [],
|
||||
"dummyWorldIid": "dummy-iid"
|
||||
}
|
||||
83
tests/fuzz/seeds/import_parsers/seed_tmj.bin
Normal file
83
tests/fuzz/seeds/import_parsers/seed_tmj.bin
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
{ "compressionlevel":-1,
|
||||
"height":4,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[1,2,1,1,2,2,1,3,1,1,1,1,3,3,2,1],
|
||||
"height":4,
|
||||
"id":1,
|
||||
"name":"Ground",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[0,0,0,0,0,9,10,0,0,11,12,0,0,0,0,0],
|
||||
"height":4,
|
||||
"id":2,
|
||||
"name":"Overlay",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":4,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"Objects",
|
||||
"objects":[
|
||||
{
|
||||
"id":1,
|
||||
"name":"spawn",
|
||||
"type":"point",
|
||||
"x":32,
|
||||
"y":32,
|
||||
"point":true,
|
||||
"properties":[
|
||||
{"name":"player_start", "type":"bool", "value":true}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":2,
|
||||
"name":"trigger_zone",
|
||||
"x":0,
|
||||
"y":0,
|
||||
"width":64,
|
||||
"height":64,
|
||||
"properties":[
|
||||
{"name":"zone_id", "type":"int", "value":42}
|
||||
]
|
||||
}
|
||||
],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
}
|
||||
],
|
||||
"nextlayerid":4,
|
||||
"nextobjectid":3,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.10.2",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"firstgid":1,
|
||||
"source":"test_tileset.tsj"
|
||||
}
|
||||
],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":"1.10",
|
||||
"width":4,
|
||||
"properties":[
|
||||
{"name":"map_name", "type":"string", "value":"test"}
|
||||
]
|
||||
}
|
||||
36
tests/fuzz/seeds/import_parsers/seed_tmx.bin
Normal file
36
tests/fuzz/seeds/import_parsers/seed_tmx.bin
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="3">
|
||||
<properties>
|
||||
<property name="map_name" value="test"/>
|
||||
</properties>
|
||||
<tileset firstgid="1" source="test_tileset.tsx"/>
|
||||
<layer id="1" name="Ground" width="4" height="4">
|
||||
<data encoding="csv">
|
||||
1,2,1,1,
|
||||
2,2,1,3,
|
||||
1,1,1,1,
|
||||
3,3,2,1
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="Overlay" width="4" height="4">
|
||||
<data encoding="csv">
|
||||
0,0,0,0,
|
||||
0,9,10,0,
|
||||
0,11,12,0,
|
||||
0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="3" name="Objects">
|
||||
<object id="1" name="spawn" type="point" x="32" y="32">
|
||||
<properties>
|
||||
<property name="player_start" type="bool" value="true"/>
|
||||
</properties>
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="trigger_zone" x="0" y="0" width="64" height="64">
|
||||
<properties>
|
||||
<property name="zone_id" type="int" value="42"/>
|
||||
</properties>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
BIN
tests/fuzz/seeds/import_parsers/seed_tsx.bin
Normal file
BIN
tests/fuzz/seeds/import_parsers/seed_tsx.bin
Normal file
Binary file not shown.
0
tests/fuzz/seeds/shader_bindings/.gitkeep
Normal file
0
tests/fuzz/seeds/shader_bindings/.gitkeep
Normal file
BIN
tests/fuzz/seeds/shader_bindings/seed_callbind.bin
Normal file
BIN
tests/fuzz/seeds/shader_bindings/seed_callbind.bin
Normal file
Binary file not shown.
BIN
tests/fuzz/seeds/shader_bindings/seed_propbind.bin
Normal file
BIN
tests/fuzz/seeds/shader_bindings/seed_propbind.bin
Normal file
Binary file not shown.
BIN
tests/fuzz/seeds/shader_bindings/seed_shapes.bin
Normal file
BIN
tests/fuzz/seeds/shader_bindings/seed_shapes.bin
Normal file
Binary file not shown.
0
tests/fuzz/seeds/texture_factory/.gitkeep
Normal file
0
tests/fuzz/seeds/texture_factory/.gitkeep
Normal file
BIN
tests/fuzz/seeds/texture_factory/seed_composite.bin
Normal file
BIN
tests/fuzz/seeds/texture_factory/seed_composite.bin
Normal file
Binary file not shown.
BIN
tests/fuzz/seeds/texture_factory/seed_mismatch.bin
Normal file
BIN
tests/fuzz/seeds/texture_factory/seed_mismatch.bin
Normal file
Binary file not shown.
BIN
tests/fuzz/seeds/texture_factory/seed_valid.bin
Normal file
BIN
tests/fuzz/seeds/texture_factory/seed_valid.bin
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue