From d7e6ec43ffcc83ac600275b359a245a26b01c6d1 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 11 Aug 2026 22:39:30 +1000 Subject: [PATCH] fix(deb): normalize apt flat-repo ./ dist prefix in local index Real apt appends the flat-repo dist "./" verbatim, requesting /./Packages (and ./Release, ./InRelease). ServeLocalIndex matched the literal filename and 404d, breaking the documented `deb ... / ./` one-liner; curl/browsers pre-normalize /./ which masked it. Collapse the dot-segment via a small normalizeIndexPath helper before matching, a no-op for pool/*.deb lookups. --- internal/provider/deb/deb.go | 12 +++++++- internal/provider/deb/deb_test.go | 47 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/internal/provider/deb/deb.go b/internal/provider/deb/deb.go index 815d83d..2dee1b4 100644 --- a/internal/provider/deb/deb.go +++ b/internal/provider/deb/deb.go @@ -14,6 +14,7 @@ import ( "io" "log/slog" "net/http" + "path" "regexp" "strconv" "strings" @@ -305,7 +306,16 @@ func parseControlFields(control string) map[string]string { return fields } -func (p *Provider) ServeLocalIndex(w http.ResponseWriter, r *http.Request, files provider.FileStore, repoName, path string) bool { +// normalizeIndexPath collapses apt's verbatim dist prefix from a flat-repo +// request. For `deb ... / ./`, apt appends the "./" dist literally and asks +// for "./Packages" (and "./Release", "./InRelease"); dot-segments must be +// collapsed so the index matcher sees "Packages". A no-op for pool/*.deb paths. +func normalizeIndexPath(p string) string { + return strings.TrimPrefix(path.Clean("/"+p), "/") +} + +func (p *Provider) ServeLocalIndex(w http.ResponseWriter, r *http.Request, files provider.FileStore, repoName, reqPath string) bool { + path := normalizeIndexPath(reqPath) switch path { case "Packages", "Packages.gz", "Release": default: diff --git a/internal/provider/deb/deb_test.go b/internal/provider/deb/deb_test.go index 36fe109..5e30752 100644 --- a/internal/provider/deb/deb_test.go +++ b/internal/provider/deb/deb_test.go @@ -295,6 +295,53 @@ func TestDebServeLocalIndex(t *testing.T) { } } +// Real apt appends the flat-repo dist "./" verbatim, so it requests "./Packages" +// / "./Release" (curl pre-normalizes /./ which masks this). The handler must +// collapse the dot-segment and return the same bytes as the un-prefixed request. +func TestDebServeLocalIndexAptDotSegment(t *testing.T) { + p := &Provider{} + reader := fakeDebReader{metas: []provider.DebMetadata{ + {Name: "aaa", Version: "1.0", Architecture: "amd64", FilePath: "pool/aaa_1.0_amd64.deb", + Control: "Package: aaa\nVersion: 1.0\nArchitecture: amd64", Size: 100, MD5: "md5aaa", SHA256: "sha256aaa"}, + }} + + serve := func(path string) *httptest.ResponseRecorder { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/"+path, nil) + if !p.ServeLocalIndex(w, r, reader, "myrepo", path) { + t.Fatalf("ServeLocalIndex returned false for %q", path) + } + return w + } + + // Packages is deterministic: require exact byte identity. + if plain, dotted := serve("Packages"), serve("./Packages"); plain.Code != 200 || dotted.Code != 200 { + t.Fatalf("Packages: plain=%d dotted=%d, want 200/200", plain.Code, dotted.Code) + } else if !bytes.Equal(plain.Body.Bytes(), dotted.Body.Bytes()) { + t.Error("./Packages body differs from Packages body") + } + + // Release carries a Date: header stamped from time.Now(); compare the rest. + plain, dotted := serve("Release"), serve("./Release") + if plain.Code != 200 || dotted.Code != 200 { + t.Fatalf("Release: plain=%d dotted=%d, want 200/200", plain.Code, dotted.Code) + } + if stripDate(plain.Body.String()) != stripDate(dotted.Body.String()) { + t.Error("./Release body differs from Release body (ignoring Date)") + } +} + +func stripDate(s string) string { + var out []string + for _, line := range strings.Split(s, "\n") { + if strings.HasPrefix(line, "Date:") { + continue + } + out = append(out, line) + } + return strings.Join(out, "\n") +} + func TestDebServeMetadataError(t *testing.T) { p := &Provider{} for _, path := range []string{"Packages", "Packages.gz", "Release"} {