clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

main
atlas / format / bundle / fuzz_test.go
fuzz_test.go 7.7K
1package bundle_test23import (4 "bytes"5 "math"6 "testing"78 "github.com/FelineStateMachine/atlas/format/bundle"9)1011// The fuzz targets. A bundle's bytes arrive from a file somebody else wrote12// and a packed payload's bytes arrive from inside one, so both readers are13// held to the only promise arbitrary bytes allow: never panic, and answer14// garbage with an error rather than with a view over memory that is not15// there. The seeds live here in code rather than in a committed corpus --16// each one is a valid artifact built through the real writer, plus the17// corruptions of it worth steering the fuzzer toward.1819// seedBundle writes a tiny valid bundle through the real writer, the same20// shape the synthetic fixtures build, so the seed corpus starts from bytes a21// reader must accept rather than from bytes it may refuse.22func seedBundle(f *testing.F) []byte {23 f.Helper()24 manifest := bundle.Manifest{25 Format: bundle.Format,26 FormatVersion: bundle.FormatVersion,27 Volume: bundle.Volume{Slug: "seed", Title: "Seed"},28 Version: bundle.Version{Stamp: bundle.HashBytes([]byte("seed")), CreatedAt: "2026-01-01T00:00:00Z"},29 TileGrid: bundle.TileGrid{SourceZoom: 13, FirstTile: 4064, TileSize: 256, Size: 8192},30 Worlds: []bundle.WorldEntry{{Slug: "overworld", Title: "Overworld", Points: 1, UpdatedAt: "2026-01-01T00:00:00Z"}},31 }32 var out bytes.Buffer33 writer, err := bundle.NewWriter(&out, manifest)34 if err != nil {35 f.Fatal(err)36 }37 detail := "{" + fixtureLenses + "," + fixturePointCollection + "}"38 packed := bundle.PackLocations([]bundle.Location{{ID: 1, Title: "Origin"}})39 steps := []error{40 writer.AddDeflated(bundle.WorldEntryName("overworld", bundle.WorldSuffix), []byte(detail)),41 writer.AddStored(bundle.WorldEntryName("overworld", bundle.PackedSuffix), bytes.NewReader(packed)),42 writer.AddDeflated(bundle.WorldEntryName("overworld", bundle.TextSuffix), []byte("{}")),43 writer.AddStored(bundle.TilesPrefix+"overworld/0/0/0.jpg", bytes.NewReader([]byte("raster"))),44 writer.AddDeflated(bundle.IconsPrefix+"marker.svg", []byte("<svg/>")),45 writer.Close(),46 }47 for _, err := range steps {48 if err != nil {49 f.Fatal(err)50 }51 }52 return out.Bytes()53}5455// The reader over arbitrary bytes. Whatever arrives, it must not panic; when56// it does open something, what opened is a bundle whose manifest already57// validated, and walking it -- validation, names, every entry -- must hold to58// the same standard.59func FuzzReader(f *testing.F) {60 seed := seedBundle(f)61 f.Add(seed)62 f.Add(seed[:len(seed)/2])63 f.Add(seed[len(seed)/2:])64 flipped := bytes.Clone(seed)65 flipped[len(flipped)/3] ^= 0xff66 f.Add(flipped)67 f.Add([]byte{})68 f.Add([]byte("not a zip at all"))69 f.Add([]byte("PK\x03\x04 but nothing behind the signature"))7071 f.Fuzz(func(t *testing.T, data []byte) {72 // A zip inflates up to about a thousandfold, so bounding the input73 // bounds what a hostile archive can make Validate read.74 if len(data) > 1<<16 {75 return76 }77 reader, err := bundle.NewReader(bytes.NewReader(data), int64(len(data)))78 if err != nil {79 return80 }81 // It opened, so the manifest is one this package accepts; everything82 // downstream must cope with whatever the rest of the archive holds.83 if err := reader.Manifest.Validate(); err != nil {84 t.Fatalf("a reader opened over a manifest it refuses: %v", err)85 }86 _ = bundle.VersionedFileName(reader.Manifest)87 _ = reader.Descriptor()88 _ = reader.Validate()89 for _, name := range reader.Names() {90 _, _ = reader.ReadEntry(name)91 _ = reader.Stored(name)92 }93 })94}9596// The packed-locations codec over arbitrary bytes: opening must never panic,97// and any payload that does open must round-trip -- unpack, pack, unpack --98// to the same locations and then to identical bytes, so the packing is a99// fixed point rather than a drift.100func FuzzPackedLocations(f *testing.F) {101 empty := bundle.PackLocations(nil)102 full := bundle.PackLocations([]bundle.Location{103 {ID: 1, Title: "Origin"},104 {ID: -7, Title: "Ölüdeniz", Lat: 12.5, Lng: -3.25, Member: 900, Shard: 3, Owner: 65535},105 })106 f.Add(empty)107 f.Add(full)108 f.Add(full[:len(full)-3])109 f.Add(full[:8])110 miscounted := bytes.Clone(full)111 miscounted[10] = 0xff // a count the payload's length cannot hold112 f.Add(miscounted)113 f.Add([]byte("ATLASLOC"))114 f.Add([]byte{})115 // The fuzzer's own first find: a well-formed payload whose latitude116 // column spells a signaling NaN, which is why locations compare by bits117 // below rather than by ==.118 f.Add([]byte("ATLASLOC\x03\x00\x02\x00\x00\x0000000000000000000000\xff\xff" +119 "00000000000000000000\x00\x00\x00\x00\x06\x00\x00\x00\x10\x00\x00\x00" +120 "00000000000000000000"))121122 f.Fuzz(func(t *testing.T, data []byte) {123 locations, err := bundle.UnpackLocations(data)124 if err != nil {125 return126 }127 packed := bundle.PackLocations(locations)128 again, err := bundle.UnpackLocations(packed)129 if err != nil {130 t.Fatalf("a payload this package packed does not unpack: %v", err)131 }132 if !sameLocations(again, locations) {133 t.Fatalf("%d locations came back as %d different ones", len(locations), len(again))134 }135 if final := bundle.PackLocations(again); !bytes.Equal(final, packed) {136 t.Fatalf("packing is not a fixed point: %d bytes, then %d", len(packed), len(final))137 }138 })139}140141// sameLocations compares two decodings field for field, with coordinates142// compared by their bits: a payload may carry a NaN, and a NaN that survives143// the trip is a round-trip even though it will not say so under ==.144func sameLocations(got, want []bundle.Location) bool {145 if len(got) != len(want) {146 return false147 }148 for index := range got {149 g, w := got[index], want[index]150 if g.ID != w.ID || g.Title != w.Title || g.Member != w.Member ||151 g.Shard != w.Shard || g.Owner != w.Owner ||152 math.Float64bits(g.Lat) != math.Float64bits(w.Lat) ||153 math.Float64bits(g.Lng) != math.Float64bits(w.Lng) {154 return false155 }156 }157 return true158}159160// The codec over arbitrary values: whatever a producer holds, packing and161// unpacking must return it up to the wire's own documented narrowing --162// coordinates to single precision, identities to 32 bits -- and never panic163// on a title that is not text.164func FuzzLocationsSurviveThePacking(f *testing.F) {165 f.Add(int64(1), 12.5, -3.25, int64(0), int64(0), uint16(0), "Origin", "")166 f.Add(int64(-1), -80.0, 170.0, int64(-5), int64(3), uint16(65535), "Ölüdeniz", "東京")167 f.Add(int64(math.MaxInt64), math.Inf(1), math.NaN(), int64(math.MinInt64), int64(math.MaxInt32), uint16(7), "\x00\xff not utf-8", "")168169 f.Fuzz(func(t *testing.T, id int64, lat, lng float64, member, shard int64, owner uint16, title, second string) {170 locations := []bundle.Location{171 {ID: id, Title: title, Lat: lat, Lng: lng, Member: member, Shard: shard, Owner: owner},172 {ID: id + 1, Title: second},173 }174 back, err := bundle.UnpackLocations(bundle.PackLocations(locations))175 if err != nil {176 t.Fatalf("a payload this package packed does not unpack: %v", err)177 }178 if len(back) != len(locations) {179 t.Fatalf("%d locations came back as %d", len(locations), len(back))180 }181 want := bundle.Location{182 ID: int64(int32(id)),183 Title: title,184 Lat: float64(float32(lat)),185 Lng: float64(float32(lng)),186 Member: int64(int32(member)),187 Shard: int64(int32(shard)),188 Owner: owner,189 }190 got := back[0]191 if got.ID != want.ID || got.Title != want.Title || got.Member != want.Member ||192 got.Shard != want.Shard || got.Owner != want.Owner {193 t.Fatalf("location came back as %+v, want %+v", got, want)194 }195 // Coordinates compare by bits, so a NaN that went in comes out the196 // same NaN rather than failing its own equality.197 if math.Float64bits(got.Lat) != math.Float64bits(want.Lat) ||198 math.Float64bits(got.Lng) != math.Float64bits(want.Lng) {199 t.Fatalf("coordinates came back as (%v, %v), want (%v, %v)", got.Lat, got.Lng, want.Lat, want.Lng)200 }201 })202}