carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "io"
11 "os"
12 "path/filepath"
13 "strings"
14 "sync"
15 "testing"
16 "time"
17
18 "github.com/carlosframework/platform/internal/geoip"
19 "github.com/carlosframework/platform/internal/release"
20)
21
22// The repo's fixtures hold none of the production probe addresses
23// (FACTS.md), so tests swap in addresses the fixtures do resolve.
24func withFixtureProbes(t *testing.T) {
25 prev := geoipProbeAddrs
26 geoipProbeAddrs = []string{"81.2.69.142", "216.160.83.56", "89.160.20.112", "2001:218::1", "67.43.156.1"}
27 t.Cleanup(func() { geoipProbeAddrs = prev })
28}
29
30func fixture(t *testing.T, name string) []byte {
31 b, err := os.ReadFile("../geo/testdata/" + name)
32 if err != nil {
33 t.Fatal(err)
34 }
35 return b
36}
37
38func newTestPublisher(t *testing.T) (*geoipPublisher, release.Store) {
39 s := release.NewDirStore(t.TempDir())
40 return newGeoipPublisher(s, t.TempDir()), s
41}
42
43func blobKeys(t *testing.T, s release.Store) []string {
44 keys, err := s.List(context.Background(), geoip.Prefix)
45 if err != nil {
46 t.Fatal(err)
47 }
48 var out []string
49 for _, k := range keys {
50 if strings.HasSuffix(k, ".mmdb") {
51 out = append(out, k)
52 }
53 }
54 return out
55}
56
57func TestPublishWritesBlobThenPointer(t *testing.T) {
58 withFixtureProbes(t)
59 p, s := newTestPublisher(t)
60 ptr, err := p.Publish(context.Background(), bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"})
61 if err != nil {
62 t.Fatal(err)
63 }
64 if ptr.DatabaseType != "GeoIP2-City" || ptr.NodeCount != 1547 || ptr.Origin != "manual" || ptr.Key != geoip.BlobKey(ptr.SHA256) {
65 t.Fatalf("pointer = %+v", ptr)
66 }
67 raw, _, err := s.Get(context.Background(), geoip.PointerKey)
68 if err != nil {
69 t.Fatal(err)
70 }
71 if got, err := geoip.ParsePointer(raw); err != nil || got.SHA256 != ptr.SHA256 {
72 t.Fatalf("stored pointer = (%+v, %v)", got, err)
73 }
74 if keys := blobKeys(t, s); len(keys) != 1 || keys[0] != ptr.Key {
75 t.Fatalf("blobs = %v", keys)
76 }
77}
78
79func TestPublishRefusals(t *testing.T) {
80 withFixtureProbes(t)
81 ctx := context.Background()
82 p, _ := newTestPublisher(t)
83 for name, body := range map[string][]byte{
84 "not a database": []byte("junk"),
85 "no country data": fixture(t, "MaxMind-DB-test-ipv4-24.mmdb"),
86 } {
87 if _, err := p.Publish(ctx, bytes.NewReader(body), geoipPublishOpts{Origin: "manual"}); !errors.Is(err, errGeoipRefused) {
88 t.Errorf("%s: err = %v, want errGeoipRefused", name, err)
89 }
90 }
91}
92
93func TestPublishShrinkCheckOnlyForTheSameType(t *testing.T) {
94 withFixtureProbes(t)
95 ctx := context.Background()
96 p, s := newTestPublisher(t)
97 if _, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-Country-Test.mmdb")), geoipPublishOpts{Origin: "manual"}); err != nil {
98 t.Fatal(err)
99 }
100 // Inflate the recorded node_count so a same-type republish looks like a
101 // truncation (fewer than half).
102 raw, etag, _ := s.Get(ctx, geoip.PointerKey)
103 inflated := bytes.Replace(raw, []byte(`"node_count": 1704`), []byte(`"node_count": 99999`), 1)
104 if bytes.Equal(inflated, raw) {
105 t.Fatalf("pointer JSON shape changed; adjust the test: %s", raw)
106 }
107 if _, err := s.Put(ctx, geoip.PointerKey, inflated, etag); err != nil {
108 t.Fatal(err)
109 }
110 same := fixture(t, "GeoIP2-Country-Test.mmdb")
111 if _, err := p.Publish(ctx, bytes.NewReader(same), geoipPublishOpts{Origin: "manual"}); !errors.Is(err, errGeoipRefused) {
112 t.Fatalf("same-type shrink = %v, want refused", err)
113 }
114 if _, err := p.Publish(ctx, bytes.NewReader(same), geoipPublishOpts{Origin: "manual", Force: true}); err != nil {
115 t.Fatalf("forced = %v", err)
116 }
117 // A different type skips the check entirely. Re-inflate first, or the
118 // City fixture (1547 nodes) would pass against Country's real 1704 even
119 // if the check were wrongly applied across types (plan review R1-20).
120 raw, etag, _ = s.Get(ctx, geoip.PointerKey)
121 inflated = bytes.Replace(raw, []byte(`"node_count": 1704`), []byte(`"node_count": 99999`), 1)
122 if _, err := s.Put(ctx, geoip.PointerKey, inflated, etag); err != nil {
123 t.Fatal(err)
124 }
125 if _, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}); err != nil {
126 t.Fatalf("different type = %v", err)
127 }
128}
129
130func TestPublishSameFileTwiceAndXYXKeepsTheCurrentBlob(t *testing.T) {
131 withFixtureProbes(t)
132 ctx := context.Background()
133 p, s := newTestPublisher(t)
134 x, y := fixture(t, "GeoIP2-City-Test.mmdb"), fixture(t, "GeoIP2-Country-Test.mmdb")
135 for i, body := range [][]byte{x, x, y, x} {
136 ptr, err := p.Publish(ctx, bytes.NewReader(body), geoipPublishOpts{Origin: "manual", Force: true})
137 if err != nil {
138 t.Fatalf("publish %d: %v", i, err)
139 }
140 keys := blobKeys(t, s)
141 if len(keys) != 1 || keys[0] != ptr.Key {
142 t.Fatalf("after publish %d blobs = %v, want only %s", i, keys, ptr.Key)
143 }
144 }
145}
146
147func TestPublishIsOneAtATime(t *testing.T) {
148 withFixtureProbes(t)
149 p, _ := newTestPublisher(t)
150 p.mu.Lock() // a publish in progress
151 _, err := p.Publish(context.Background(), bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"})
152 p.mu.Unlock()
153 if !errors.Is(err, errGeoipBusy) {
154 t.Fatalf("concurrent publish = %v, want errGeoipBusy", err)
155 }
156}
157
158func TestPublishRefusesOversizeWhileCopying(t *testing.T) {
159 withFixtureProbes(t)
160 p, _ := newTestPublisher(t)
161 p.maxBytes = 1024
162 _, err := p.Publish(context.Background(), bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"})
163 if !errors.Is(err, errGeoipRefused) {
164 t.Fatalf("oversize = %v", err)
165 }
166 if entries, _ := os.ReadDir(p.tmpRoot); len(entries) != 0 {
167 t.Fatalf("temp left behind: %v", entries)
168 }
169}
170
171func TestOffAndSweep(t *testing.T) {
172 withFixtureProbes(t)
173 ctx := context.Background()
174 p, s := newTestPublisher(t)
175 ptr, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"})
176 if err != nil {
177 t.Fatal(err)
178 }
179 // An orphan from a crashed publish (spec §3 orphan sweep).
180 orphan := geoip.BlobKey(strings.Repeat("a", 64))
181 if _, err := s.Put(ctx, orphan, []byte("x"), ""); err != nil {
182 t.Fatal(err)
183 }
184 if err := p.Sweep(ctx); err != nil {
185 t.Fatal(err)
186 }
187 if keys := blobKeys(t, s); len(keys) != 1 || keys[0] != ptr.Key {
188 t.Fatalf("after sweep = %v", keys)
189 }
190 if err := p.Off(ctx); err != nil {
191 t.Fatal(err)
192 }
193 if _, _, err := s.Get(ctx, geoip.PointerKey); !errors.Is(err, os.ErrNotExist) {
194 t.Fatalf("pointer after off: %v", err)
195 }
196 if keys := blobKeys(t, s); len(keys) != 0 {
197 t.Fatalf("blobs after off = %v", keys)
198 }
199}
200
201func TestExpireIfDueMarksGeoLiteExpired(t *testing.T) {
202 ctx := context.Background()
203 p, s := newTestPublisher(t)
204 sha := strings.Repeat("b", 64)
205 ptr := geoip.Pointer{V: 1, SHA256: sha, Key: geoip.BlobKey(sha), Size: 1, DatabaseType: "GeoLite2-City",
206 BuildEpoch: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC).Unix(), Origin: "manual"}
207 putPointer(t, s, ptr)
208 if _, err := s.Put(ctx, ptr.Key, []byte("x"), ""); err != nil {
209 t.Fatal(err)
210 }
211 if did, err := p.ExpireIfDue(ctx, time.Date(2026, 9, 20, 0, 0, 0, 0, time.UTC)); did || err != nil {
212 t.Fatalf("early = (%v, %v)", did, err)
213 }
214 if did, err := p.ExpireIfDue(ctx, time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)); !did || err != nil {
215 t.Fatalf("due = (%v, %v)", did, err)
216 }
217 got, _, present, err := p.Current(ctx)
218 if err != nil || !present || !got.Expired {
219 t.Fatalf("pointer = (%+v, %v, %v)", got, present, err)
220 }
221 if keys := blobKeys(t, s); len(keys) != 0 {
222 t.Fatalf("blob not destroyed: %v", keys)
223 }
224}
225
226func putPointer(t *testing.T, s release.Store, p geoip.Pointer) {
227 t.Helper()
228 b, err := marshalPointer(p)
229 if err != nil {
230 t.Fatal(err)
231 }
232 if _, err := s.Put(context.Background(), geoip.PointerKey, b, ""); err != nil {
233 t.Fatal(err)
234 }
235}
236
237var _ = sync.Mutex{}
238
239func TestPublisherStartupClearsAbandonedStaging(t *testing.T) {
240 withFixtureProbes(t)
241 root := filepath.Join(t.TempDir(), "geoip-staging")
242 abandoned := filepath.Join(root, "geoip-publish-123")
243 if err := os.MkdirAll(abandoned, 0o700); err != nil {
244 t.Fatal(err)
245 }
246 if err := os.WriteFile(filepath.Join(abandoned, "db.mmdb"), []byte("half an upload"), 0o600); err != nil {
247 t.Fatal(err)
248 }
249 p := newGeoipPublisher(release.NewDirStore(t.TempDir()), root)
250 if entries, _ := os.ReadDir(root); len(entries) != 0 {
251 t.Fatalf("staging not cleared at startup: %v", entries)
252 }
253 if _, err := p.Publish(context.Background(), bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}); err != nil {
254 t.Fatalf("publish after restart: %v", err)
255 }
256}
257
258func TestPublishRefusesAWrongObjectAtTheBlobKey(t *testing.T) {
259 withFixtureProbes(t)
260 ctx := context.Background()
261 p, s := newTestPublisher(t)
262 body := fixture(t, "GeoIP2-City-Test.mmdb")
263 sum := sha256.Sum256(body)
264 key := geoip.BlobKey(hex.EncodeToString(sum[:]))
265 if _, err := s.Put(ctx, key, body[:len(body)/2], ""); err != nil { // a truncated object squatting the name
266 t.Fatal(err)
267 }
268 if _, err := p.Publish(ctx, bytes.NewReader(body), geoipPublishOpts{Origin: "manual"}); !errors.Is(err, errGeoipRefused) {
269 t.Fatalf("publish over a wrong object = %v, want refused", err)
270 }
271 if _, _, err := s.Get(ctx, geoip.PointerKey); !errors.Is(err, os.ErrNotExist) {
272 t.Fatal("the pointer moved onto a wrong blob")
273 }
274}
275
276func TestPublishRefusesAnExpiredDatabaseEvenWithForce(t *testing.T) {
277 withFixtureProbes(t)
278 ctx := context.Background()
279 p, s := newTestPublisher(t)
280 good, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"})
281 if err != nil {
282 t.Fatal(err)
283 }
284 p.expired = func(string, time.Time, time.Time) bool { return true }
285 _, err = p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-Country-Test.mmdb")), geoipPublishOpts{Origin: "manual", Force: true})
286 if !errors.Is(err, errGeoipRefused) {
287 t.Fatalf("expired candidate with force = %v, want refused (codex R7-1)", err)
288 }
289 cur, _, _, _ := p.Current(ctx)
290 if cur.SHA256 != good.SHA256 {
291 t.Fatal("a refused expired candidate moved the pointer")
292 }
293 if keys := blobKeys(t, s); len(keys) != 1 || keys[0] != good.Key {
294 t.Fatalf("blobs after refusal = %v, want only the previous good one", keys)
295 }
296}
297
298func TestCommitRefusesAForeignPointerWrite(t *testing.T) {
299 withFixtureProbes(t)
300 ctx := context.Background()
301 p, s := newTestPublisher(t)
302 first, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"})
303 if err != nil {
304 t.Fatal(err)
305 }
306 st, err := p.Stage(ctx, bytes.NewReader(fixture(t, "GeoIP2-Country-Test.mmdb")))
307 if err != nil {
308 t.Fatal(err)
309 }
310 // Something outside this console rewrites the pointer between staging
311 // and commit (spec §3 step 3).
312 raw, etag, _ := s.Get(ctx, geoip.PointerKey)
313 if _, err := s.Put(ctx, geoip.PointerKey, append(raw, ' '), etag); err != nil {
314 t.Fatal(err)
315 }
316 if _, err := p.Commit(ctx, st, geoipPublishOpts{Origin: "manual", Force: true}); err == nil {
317 t.Fatal("commit over a foreign pointer write succeeded")
318 }
319 cur, _, _, _ := p.Current(ctx)
320 if cur.SHA256 != first.SHA256 {
321 t.Fatal("the foreign pointer was overwritten")
322 }
323 if !p.mu.TryLock() {
324 t.Fatal("a failed commit must release the publish slot")
325 }
326 p.mu.Unlock()
327}
328
329func TestOffRefusesDuringAPublishThenSucceeds(t *testing.T) {
330 withFixtureProbes(t)
331 ctx := context.Background()
332 p, s := newTestPublisher(t)
333 st, err := p.Stage(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")))
334 if err != nil {
335 t.Fatal(err)
336 }
337 // Off refuses rather than waits while the slot is held (codex R9-2)...
338 if err := p.Off(ctx); !errors.Is(err, errGeoipBusy) {
339 t.Fatalf("off during a publish = %v, want errGeoipBusy", err)
340 }
341 if _, err := p.Commit(ctx, st, geoipPublishOpts{Origin: "manual"}); err != nil {
342 t.Fatal(err)
343 }
344 // ...and succeeds once the publish has finished.
345 if err := p.Off(ctx); err != nil {
346 t.Fatal(err)
347 }
348 if _, _, err := s.Get(ctx, geoip.PointerKey); !errors.Is(err, os.ErrNotExist) {
349 t.Fatal("off, serialised after the publish, must leave no pointer")
350 }
351 if keys := blobKeys(t, s); len(keys) != 0 {
352 t.Fatalf("blobs after off = %v", keys)
353 }
354}
355
356// Code review R1-3: an oversized object at the pointer key is never read
357// whole, and is reported as present-but-not-a-pointer.
358func TestCurrentRefusesAnOversizedPointer(t *testing.T) {
359 ctx := context.Background()
360 p, s := newTestPublisher(t)
361 if _, err := s.Put(ctx, geoip.PointerKey, bytes.Repeat([]byte("x"), geoip.MaxPointerBytes+1), ""); err != nil {
362 t.Fatal(err)
363 }
364 _, etag, present, err := p.Current(ctx)
365 if !errors.Is(err, errPointerTooLarge) || !present {
366 t.Fatalf("Current = (%v, %v), want errPointerTooLarge, present", present, err)
367 }
368 _ = etag
369}
370
371type panicReader struct{}
372
373func (panicReader) Read([]byte) (int, error) { panic("a busy Stage read its body") }
374
375// Code review R1-5: a Stage refused for a held slot never touches its body.
376func TestStageNeverReadsWhenTheSlotIsHeld(t *testing.T) {
377 p, _ := newTestPublisher(t)
378 p.mu.Lock()
379 defer p.mu.Unlock()
380 if _, err := p.Stage(context.Background(), panicReader{}); !errors.Is(err, errGeoipBusy) {
381 t.Fatalf("Stage = %v, want errGeoipBusy", err)
382 }
383}
384
385// Code review R1-6: the other row sorts last whatever its size.
386func TestGeoCountriesKeepsOtherLast(t *testing.T) {
387 m := map[string]int64{}
388 for i := 0; i < 15; i++ {
389 m[fmt.Sprintf("C%02d", i)] = 10
390 }
391 m["GB"] = 11
392 rows := sortedKV(geoCountries(m))
393 if rows[len(rows)-1].Key != "_other" || geoLabel(rows[len(rows)-1].Key) != "other" {
394 t.Fatalf("last row = %+v, want _other rendered as other", rows[len(rows)-1])
395 }
396}
397
398func TestFeedPublishRules(t *testing.T) {
399 withFixtureProbes(t)
400 ctx := context.Background()
401 p, _ := newTestPublisher(t)
402 city := fixture(t, "GeoIP2-City-Test.mmdb")
403 if _, err := p.Publish(ctx, bytes.NewReader(city), geoipPublishOpts{Origin: "feed:x", RequireType: "GeoIP2-Country", Monotonic: true}); !errors.Is(err, errGeoipRefused) || !strings.Contains(err.Error(), "not GeoIP2-Country") {
404 t.Fatalf("wrong type on first publish = %v", err)
405 }
406 first, err := p.Publish(ctx, bytes.NewReader(city), geoipPublishOpts{Origin: "feed:x", RequireType: "GeoIP2-City", Monotonic: true})
407 if err != nil {
408 t.Fatal(err)
409 }
410 if _, err := p.Publish(ctx, bytes.NewReader(city), geoipPublishOpts{Origin: "feed:x", RequireType: "GeoIP2-City", Monotonic: true}); !errors.Is(err, errGeoipRefused) || !strings.Contains(err.Error(), "not newer") {
411 t.Fatalf("same edition again = %v", err)
412 }
413 if cur, _, _, _ := p.Current(ctx); cur.SHA256 != first.SHA256 {
414 t.Fatal("a refused feed publish moved the pointer")
415 }
416 // A manual pointer does not hold the feed back.
417 if _, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-Country-Test.mmdb")), geoipPublishOpts{Origin: "manual", Force: true}); err != nil {
418 t.Fatal(err)
419 }
420 if _, err := p.Publish(ctx, bytes.NewReader(city), geoipPublishOpts{Origin: "feed:x", RequireType: "GeoIP2-City", Monotonic: true}); err != nil {
421 t.Fatalf("feed over a manual pointer = %v", err)
422 }
423}
424
425type stallingStore struct{ release.Store }
426
427func (stallingStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) {
428 <-ctx.Done()
429 return nil, "", ctx.Err()
430}
431
432func (s stallingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
433 <-ctx.Done()
434 return nil, "", ctx.Err()
435}
436
437// Code review R4-1: a stalled bucket cannot hold the publish slot forever.
438func TestStageGivesUpOnAStalledStoreAndFreesTheSlot(t *testing.T) {
439 p, s := newTestPublisher(t)
440 p.store = stallingStore{s}
441 p.storeTimeout = 50 * time.Millisecond
442 start := time.Now()
443 if _, err := p.Stage(context.Background(), bytes.NewReader([]byte("x"))); err == nil {
444 t.Fatal("Stage succeeded against a stalled store")
445 }
446 if time.Since(start) > 5*time.Second {
447 t.Fatal("Stage waited on the stalled store")
448 }
449 if !p.mu.TryLock() {
450 t.Fatal("the slot stayed held after giving up")
451 }
452 p.mu.Unlock()
453}
454
455// Code review R8-1: a crafted tree is refused in time, the slot is freed,
456// staging is cleaned and the previous pointer survives.
457func TestPublishRefusesAPathologicalTreeInTime(t *testing.T) {
458 withFixtureProbes(t)
459 ctx := context.Background()
460 p, _ := newTestPublisher(t)
461 good, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"})
462 if err != nil {
463 t.Fatal(err)
464 }
465 start := time.Now()
466 _, err = p.Publish(ctx, bytes.NewReader(fixture(t, "pathological-dag.mmdb")), geoipPublishOpts{Origin: "manual", Force: true})
467 if !errors.Is(err, errGeoipRefused) || !strings.Contains(err.Error(), "search tree") {
468 t.Fatalf("err = %v", err)
469 }
470 if time.Since(start) > 5*time.Second {
471 t.Fatal("took too long")
472 }
473 if cur, _, _, _ := p.Current(ctx); cur.SHA256 != good.SHA256 {
474 t.Fatal("the previous pointer was replaced")
475 }
476 if !p.mu.TryLock() {
477 t.Fatal("slot still held")
478 }
479 p.mu.Unlock()
480 if entries, _ := os.ReadDir(p.tmpRoot); len(entries) != 0 {
481 t.Fatalf("staging left behind: %v", entries)
482 }
483}
484
485// Code review R9: databases that hang or panic the parser are refused in
486// time, by the child, and the console survives with its slot and pointer.
487func TestPublishSurvivesDatabasesThatHangOrPanicTheParser(t *testing.T) {
488 withFixtureProbes(t)
489 prev := geoipVerifyTimeout
490 geoipVerifyTimeout = 2 * time.Second
491 t.Cleanup(func() { geoipVerifyTimeout = prev })
492 ctx := context.Background()
493 p, _ := newTestPublisher(t)
494 good, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"})
495 if err != nil {
496 t.Fatal(err)
497 }
498 for _, f := range []string{"pathological-empty-dag.mmdb", "pathological-metadata.mmdb", "pathological-bigmap.mmdb"} {
499 start := time.Now()
500 if _, err := p.Publish(ctx, bytes.NewReader(fixture(t, f)), geoipPublishOpts{Origin: "manual", Force: true}); !errors.Is(err, errGeoipRefused) {
501 t.Fatalf("%s: err = %v", f, err)
502 }
503 if d := time.Since(start); d > 15*time.Second {
504 t.Fatalf("%s took %s", f, d)
505 }
506 if cur, _, _, _ := p.Current(ctx); cur.SHA256 != good.SHA256 {
507 t.Fatalf("%s replaced the pointer", f)
508 }
509 if !p.mu.TryLock() {
510 t.Fatalf("%s left the slot held", f)
511 }
512 p.mu.Unlock()
513 if entries, _ := os.ReadDir(p.tmpRoot); len(entries) != 0 {
514 t.Fatalf("%s left staging: %v", f, entries)
515 }
516 }
517}
518
519// junkRecordMMDB writes an IPv4 database whose lower half answers GB and
520// whose upper half carries a 6M-element ignored array beside its country
521// (code review R11-1): generic verification walks the array, but the
522// production schema's skip recurses once per element.
523func junkRecordMMDB(t *testing.T) string {
524 t.Helper()
525 var buf bytes.Buffer
526 str := func(s string) { buf.WriteByte(0x40 | byte(len(s))); buf.WriteString(s) }
527 u16 := func(v uint16) { buf.WriteByte(0xA2); buf.WriteByte(byte(v >> 8)); buf.WriteByte(byte(v)) }
528 // One node: left -> data at 0 (A), right -> data at offsetB (B).
529 recA := []byte{0xE1, 0x47, 'c', 'o', 'u', 'n', 't', 'r', 'y', 0xE1, 0x48, 'i', 's', 'o', '_', 'c', 'o', 'd', 'e', 0x42, 'G', 'B'}
530 offsetB := len(recA)
531 ptr := func(dataOff int) []byte { v := 1 + 16 + dataOff; return []byte{byte(v >> 16), byte(v >> 8), byte(v)} }
532 buf.Write(ptr(0))
533 buf.Write(ptr(offsetB))
534 buf.Write(make([]byte, 16))
535 buf.Write(recA)
536 // B: {country: {iso_code: FR}, junk: [16,000,000 x false]}
537 buf.Write([]byte{0xE2, 0x47, 'c', 'o', 'u', 'n', 't', 'r', 'y', 0xE1, 0x48, 'i', 's', 'o', '_', 'c', 'o', 'd', 'e', 0x42, 'F', 'R', 0x44, 'j', 'u', 'n', 'k'})
538 const n = 6_000_000
539 v := n - 65821
540 buf.Write([]byte{0x1F, 0x04, byte(v >> 16), byte(v >> 8), byte(v)}) // array, size 31 form
541 falseBool := []byte{0x00, 0x07}
542 for i := 0; i < n; i++ {
543 buf.Write(falseBool)
544 }
545 buf.WriteString("\xAB\xCD\xEFMaxMind.com")
546 buf.WriteByte(0xE0 | 9)
547 str("node_count")
548 buf.Write([]byte{0xC1, 1})
549 str("record_size")
550 u16(24)
551 str("ip_version")
552 u16(4)
553 str("database_type")
554 str("Junk-Record")
555 str("languages")
556 buf.Write([]byte{0x01, 0x04})
557 str("en")
558 str("binary_format_major_version")
559 u16(2)
560 str("binary_format_minor_version")
561 u16(0)
562 str("build_epoch")
563 buf.Write([]byte{0x04, 0x02, 0x6a, 0xbc, 0x00, 0x00})
564 str("description")
565 buf.WriteByte(0xE1)
566 str("en")
567 str("junk")
568 p := filepath.Join(t.TempDir(), "junk.mmdb")
569 if err := os.WriteFile(p, buf.Bytes(), 0o644); err != nil {
570 t.Fatal(err)
571 }
572 return p
573}
574
575// Code review R11-1: a record that would overflow a serving lookup's stack
576// is refused by the verifier child, even when every probe address is fine.
577func TestPublishRefusesARecordThatWouldCrashALookup(t *testing.T) {
578 prevProbes := geoipProbeAddrs
579 geoipProbeAddrs = []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"} // all in the clean half
580 t.Cleanup(func() { geoipProbeAddrs = prevProbes })
581 prev := geoipVerifyTimeout
582 geoipVerifyTimeout = 90 * time.Second
583 t.Cleanup(func() { geoipVerifyTimeout = prev })
584 ctx := context.Background()
585 p, _ := newTestPublisher(t)
586 f, err := os.Open(junkRecordMMDB(t))
587 if err != nil {
588 t.Fatal(err)
589 }
590 defer f.Close()
591 if _, err := p.Publish(ctx, f, geoipPublishOpts{Origin: "manual"}); !errors.Is(err, errGeoipRefused) {
592 t.Fatalf("a crash-on-lookup database = %v, want refused", err)
593 }
594 if _, _, present, _ := p.Current(ctx); present {
595 t.Fatal("it was published")
596 }
597}
598