From 291b44147f2ec8d2d673cc62e497272422889603 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Wed, 22 Apr 2026 10:05:38 -0600 Subject: [PATCH 1/2] fix(FromTarGz): use Extraction field, compare basename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two back-to-back bugs in the mholt/archives port (commit 017d542): 1. archives.CompressedArchive.Extract() reads ca.Extraction (not ca.Archival) and returns "no extraction format" if it's nil. We were setting Archival, which made every Fetch() fail silently — every error was swallowed by the retry loop up to the 30-try ceiling. 2. info.NameInArchive is the full stored path (e.g. "GeoLite2-City_20260116/GeoLite2-City.mmdb"). Callers pass a basename, matching the archiver/v3 behavior this replaced. Compare on path.Base so real tarballs with dated directory prefixes (as MaxMind ships) actually match. Observed downstream: lantern-box's MaxMind fetch never completed on any VPS, so every proxy.io data point was tagged geo.country.iso_code="" and the dashboard's country filter matched zero everywhere. --- source.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/source.go b/source.go index 4be5fd2..ea0bd5a 100644 --- a/source.go +++ b/source.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "os" + "path" "sync" "time" @@ -92,14 +93,20 @@ func (s *tarGzSource) Fetch(ifNewerThan time.Time) (io.ReadCloser, error) { } defer rc.Close() + // archives.CompressedArchive.Extract() reads ca.Extraction (not ca.Archival), + // and returns "no extraction format" if it's nil. Setting Archival here was a + // bug that made every Fetch() fail silently. format := archives.CompressedArchive{ Compression: archives.Gz{}, - Archival: archives.Tar{}, + Extraction: archives.Tar{}, } var buf []byte err = format.Extract(context.Background(), rc, func(ctx context.Context, info archives.FileInfo) error { - if info.NameInArchive == s.expectedName { + // NameInArchive is the full stored path (e.g. "GeoLite2-City_20260116/GeoLite2-City.mmdb"). + // Callers pass the basename, so compare on basename — mirrors the behavior of the + // archiver/v3-based implementation this replaced. + if path.Base(info.NameInArchive) == s.expectedName { f, err := info.Open() if err != nil { return err From 07acadc872c0994ce26a186047b52324727f2fc9 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Wed, 22 Apr 2026 10:12:16 -0600 Subject: [PATCH 2/2] test: cover FromTarGz basename match + missing-file error Regression tests for the two bugs fixed in the prior commit. Verified that both fail without the fix: - TestFromTarGzBasenameMatch: "no extraction format" (bug 1) and, after that's fixed, falls through to the full-path compare (bug 2). - TestFromTarGzMissingFile: surfaces "not found" rather than the silent "no extraction format" that hid the original regression. Uses an in-memory tar.gz with a MaxMind-style dated directory prefix so no fixture files are needed. --- keepcurrent_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/keepcurrent_test.go b/keepcurrent_test.go index d5dfe77..5cfd116 100644 --- a/keepcurrent_test.go +++ b/keepcurrent_test.go @@ -1,7 +1,9 @@ package keepcurrent import ( + "archive/tar" "bytes" + "compress/gzip" "crypto/rand" "encoding/json" "io" @@ -203,3 +205,69 @@ func TestBackoffOnFail(t *testing.T) { assert.EqualValues(t, 1, atomic.LoadInt32(&updates)) assert.EqualValues(t, 1, atomic.LoadInt32(&finalFailures)) } + +// bytesSource is a Source that returns a fixed byte slice every Fetch. +type bytesSource struct{ data []byte } + +func (b *bytesSource) Fetch(time.Time) (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(b.data)), nil +} + +// makeTarGz builds an in-memory .tar.gz containing a single file stored +// under the given path (e.g. "dir/file") so tests can exercise the basename- +// match path without shipping a fixture. +func makeTarGz(t *testing.T, storedPath string, payload []byte) []byte { + t.Helper() + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + tw := tar.NewWriter(gz) + if err := tw.WriteHeader(&tar.Header{ + Name: storedPath, + Mode: 0644, + Size: int64(len(payload)), + Typeflag: tar.TypeReg, + ModTime: time.Now(), + }); err != nil { + t.Fatalf("tar header: %v", err) + } + if _, err := tw.Write(payload); err != nil { + t.Fatalf("tar write: %v", err) + } + if err := tw.Close(); err != nil { + t.Fatalf("tar close: %v", err) + } + if err := gz.Close(); err != nil { + t.Fatalf("gz close: %v", err) + } + return buf.Bytes() +} + +// Regression test for the two bugs fixed in PR #7: FromTarGz must extract +// a file whose stored path includes a directory prefix (as MaxMind ships +// its mmdb tarballs), matched by the basename the caller passes. +func TestFromTarGzBasenameMatch(t *testing.T) { + want := []byte("hello world") + tgz := makeTarGz(t, "GeoLite2-City_20260116/GeoLite2-City.mmdb", want) + + src := FromTarGz(&bytesSource{data: tgz}, "GeoLite2-City.mmdb") + rc, err := src.Fetch(time.Time{}) + assert.NoError(t, err, "Fetch should find the file by basename") + if err != nil { + return + } + defer rc.Close() + got, err := io.ReadAll(rc) + assert.NoError(t, err) + assert.Equal(t, want, got) +} + +// When the requested file isn't in the archive we should return a clear +// error, rather than the silent "no extraction format" that hid the +// original regression. +func TestFromTarGzMissingFile(t *testing.T) { + tgz := makeTarGz(t, "some/other-file.txt", []byte("x")) + src := FromTarGz(&bytesSource{data: tgz}, "GeoLite2-City.mmdb") + _, err := src.Fetch(time.Time{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") +}