From 31bcd80a1d07e5f71c2c9f0438402be635fb1188 Mon Sep 17 00:00:00 2001 From: Guillaume Belanger Date: Fri, 15 May 2026 11:42:20 -0400 Subject: [PATCH 1/6] fix: prevent intermittent digest-mismatch errors during archive updates Signed-off-by: Guillaume Belanger --- internal/archive/archive.go | 38 ++++++-- internal/archive/archive_test.go | 99 ++++++++++++++++++++- internal/archive/testarchive/testarchive.go | 31 +++++-- 3 files changed, 156 insertions(+), 12 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index e24a9c248..193786696 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -64,10 +64,13 @@ func Open(options *Options) (Archive, error) { type fetchFlags uint const ( - fetchBulk fetchFlags = 1 << iota + fetchBulk fetchFlags = 1 << iota + fetchGzip fetchDefault fetchFlags = 0 ) +var notFoundErr = fmt.Errorf("cannot find archive data") + var httpClient = &http.Client{ Timeout: 30 * time.Second, } @@ -334,9 +337,32 @@ func (index *ubuntuIndex) fetchIndex() error { } logf("Fetching index for %s %s %s %s component...", index.displayName(), index.version, index.suite, index.component) - reader, err := index.fetch(index.distPath(packagesPath+".gz"), digest, fetchBulk) - if err != nil { - return err + + // Prefer acquire-by-hash when the archive advertises it. By-hash URLs + // are content-addressed and so are immune to the inconsistent view of + // InRelease vs Packages.gz that mirrors and CDNs can serve while a + // publication is propagating. See https://wiki.ubuntu.com/AptByHash. + gzPath := packagesPath + ".gz" + var reader io.ReadSeekCloser + if index.release.Get("Acquire-By-Hash") == "yes" { + gzDigest, _, _ := control.ParsePathInfo(digests, gzPath) + if gzDigest != "" { + byHashPath := fmt.Sprintf("%s/binary-%s/by-hash/SHA256/%s", index.component, index.arch, gzDigest) + r, err := index.fetch(index.distPath(byHashPath), digest, fetchBulk|fetchGzip) + if err != nil && err != notFoundErr { + return err + } + // On 404 fall through to the named-path fetch below: the hash + // may have been garbage-collected on the mirror. + reader = r + } + } + if reader == nil { + r, err := index.fetch(index.distPath(gzPath), digest, fetchBulk|fetchGzip) + if err != nil { + return err + } + reader = r } ctrl, err := control.ParseReader("Package", reader) if err != nil { @@ -410,13 +436,13 @@ func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadS case 401: return nil, fmt.Errorf("cannot fetch from %q: unauthorized", index.label) case 404: - return nil, fmt.Errorf("cannot find archive data") + return nil, notFoundErr default: return nil, fmt.Errorf("error from archive: %v", resp.Status) } body := resp.Body - if strings.HasSuffix(path, ".gz") { + if flags&fetchGzip != 0 { reader, err := gzip.NewReader(body) if err != nil { return nil, fmt.Errorf("cannot decompress data: %v", err) diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 8ca840e59..8d4e7e61e 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -4,6 +4,8 @@ import ( "golang.org/x/crypto/openpgp/packet" . "gopkg.in/check.v1" + "bytes" + "compress/gzip" "debug/elf" "errors" "flag" @@ -83,14 +85,18 @@ func (s *httpSuite) Do(req *http.Request) (*http.Response, error) { s.request = req s.requests = append(s.requests, req) body := s.response + status := s.status s.logf("Request: %s", req.URL.String()) if response, ok := s.responses[path.Clean(req.URL.Path)]; ok { body = string(response) + } else if len(s.responses) > 0 && s.status == 200 { + // Unknown path with responses populated: behave like a real archive. + status = 404 } rsp := &http.Response{ Body: io.NopCloser(strings.NewReader(body)), Header: s.header, - StatusCode: s.status, + StatusCode: status, } return rsp, s.err } @@ -625,6 +631,97 @@ func read(r io.Reader) string { return string(data) } +func gzipBytes(s string) []byte { + var buf bytes.Buffer + w := gzip.NewWriter(&buf) + if _, err := w.Write([]byte(s)); err != nil { + panic(err) + } + if err := w.Close(); err != nil { + panic(err) + } + return buf.Bytes() +} + +func (s *httpSuite) sawByHashRequest() bool { + for _, req := range s.requests { + if strings.Contains(req.URL.Path, "/by-hash/SHA256/") { + return true + } + } + return false +} + +func (s *httpSuite) TestFetchByHashSucceedsWhenNamedPathIsStale(c *C) { + s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) { + r.AcquireByHash = true + r.NamedPathContent = map[string][]byte{ + "main/binary-amd64/Packages.gz": gzipBytes("stale Packages from previous publication"), + } + }) + + options := archive.Options{ + Label: "ubuntu", + Version: "22.04", + Arch: "amd64", + Suites: []string{"jammy"}, + Components: []string{"main"}, + CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, + } + + testArchive, err := archive.Open(&options) + c.Assert(err, IsNil) + + pkg, _, err := testArchive.Fetch("mypkg1") + c.Assert(err, IsNil) + c.Assert(read(pkg), Equals, "mypkg1 1.1 data") + c.Assert(s.sawByHashRequest(), Equals, true) +} + +func (s *httpSuite) TestFetchByHashFallsBackOnNotFound(c *C) { + s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) { + r.AcquireByHash = true + r.ByHashSkip = []string{"main/binary-amd64/Packages.gz"} + }) + + options := archive.Options{ + Label: "ubuntu", + Version: "22.04", + Arch: "amd64", + Suites: []string{"jammy"}, + Components: []string{"main"}, + CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, + } + + testArchive, err := archive.Open(&options) + c.Assert(err, IsNil) + + pkg, _, err := testArchive.Fetch("mypkg1") + c.Assert(err, IsNil) + c.Assert(read(pkg), Equals, "mypkg1 1.1 data") + c.Assert(s.sawByHashRequest(), Equals, true) +} + +func (s *httpSuite) TestFetchSkipsByHashWhenNotAdvertised(c *C) { + s.prepareArchive("jammy", "22.04", "amd64", []string{"main"}) + + options := archive.Options{ + Label: "ubuntu", + Version: "22.04", + Arch: "amd64", + Suites: []string{"jammy"}, + Components: []string{"main"}, + CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, + } + + _, err := archive.Open(&options) + c.Assert(err, IsNil) + c.Assert(s.sawByHashRequest(), Equals, false) +} + // ---------------------------------------------------------------------------------------- // Real archive tests, only enabled via: // 1. --real-archive for non-Pro archives (e.g. standard jammy archive), diff --git a/internal/archive/testarchive/testarchive.go b/internal/archive/testarchive/testarchive.go index ea5b8e086..02b6dd3ef 100644 --- a/internal/archive/testarchive/testarchive.go +++ b/internal/archive/testarchive/testarchive.go @@ -107,6 +107,10 @@ type Release struct { Label string Items []Item PrivKey *packet.PrivateKey + // Fields below model acquire-by-hash and mirror inconsistencies for tests. + AcquireByHash bool + ByHashSkip []string + NamedPathContent map[string][]byte } func (r *Release) Walk(f func(Item) error) error { @@ -127,6 +131,10 @@ func (r *Release) Content() []byte { content := item.Content() fmt.Fprintf(&digests, " %s %d %s\n", makeSha256(content), len(content), item.Path()) } + acquireByHash := "" + if r.AcquireByHash { + acquireByHash = "Acquire-By-Hash: yes\n" + } content := fmt.Sprintf(string(testutil.Reindent(` Origin: Ubuntu Label: %s @@ -137,9 +145,9 @@ func (r *Release) Content() []byte { Architectures: amd64 arm64 armhf i386 ppc64el riscv64 s390x Components: main restricted universe multiverse Description: Ubuntu %s - SHA256: + %sSHA256: %s - `)), r.Label, r.Suite, r.Version, r.Version, digests.String()) + `)), r.Label, r.Suite, r.Version, r.Version, acquireByHash, digests.String()) var buf bytes.Buffer writer, err := clearsign.Encode(&buf, r.PrivKey, nil) @@ -158,14 +166,27 @@ func (r *Release) Content() []byte { } func (r *Release) Render(prefix string, content map[string][]byte) error { + skipByHash := make(map[string]bool, len(r.ByHashSkip)) + for _, p := range r.ByHashSkip { + skipByHash[p] = true + } return r.Walk(func(item Item) error { itemPath := item.Path() + itemContent := item.Content() if strings.HasPrefix(itemPath, "pool/") { - itemPath = path.Join(prefix, itemPath) + content[path.Join(prefix, itemPath)] = itemContent + return nil + } + distItemPath := path.Join(prefix, "dists", r.Suite, itemPath) + if override, ok := r.NamedPathContent[itemPath]; ok { + content[distItemPath] = override } else { - itemPath = path.Join(prefix, "dists", r.Suite, itemPath) + content[distItemPath] = itemContent + } + if r.AcquireByHash && itemPath != r.Path() && !skipByHash[itemPath] { + byHashPath := path.Join(prefix, "dists", r.Suite, path.Dir(itemPath), "by-hash", "SHA256", makeSha256(itemContent)) + content[byHashPath] = itemContent } - content[itemPath] = item.Content() return nil }) } From 0c9b69b8296ec61817d333eabee376b98da4ad4d Mon Sep 17 00:00:00 2001 From: Guillaume Belanger Date: Tue, 2 Jun 2026 12:38:40 -0400 Subject: [PATCH 2/6] chore: fix lint error Signed-off-by: Guillaume Belanger --- internal/archive/archive.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index aad09736f..43393aca5 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -69,7 +69,7 @@ const ( fetchDefault fetchFlags = 0 ) -var notFoundErr = fmt.Errorf("cannot find archive data") +var errNotFound = fmt.Errorf("cannot find archive data") var httpClient = &http.Client{ Timeout: 30 * time.Second, @@ -349,7 +349,7 @@ func (index *ubuntuIndex) fetchIndex() error { if gzDigest != "" { byHashPath := fmt.Sprintf("%s/binary-%s/by-hash/SHA256/%s", index.component, index.arch, gzDigest) r, err := index.fetch(index.distPath(byHashPath), digest, fetchBulk|fetchGzip) - if err != nil && err != notFoundErr { + if err != nil && err != errNotFound { return err } // On 404 fall through to the named-path fetch below: the hash @@ -436,7 +436,7 @@ func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadS case 401: return nil, fmt.Errorf("cannot fetch from %q: unauthorized", index.label) case 404: - return nil, notFoundErr + return nil, errNotFound default: return nil, fmt.Errorf("error from archive: %v", resp.Status) } From 483794041c40a9e69e5f0d4d73bba9a28fc8307e Mon Sep 17 00:00:00 2001 From: Guillaume Belanger Date: Mon, 15 Jun 2026 07:46:48 -0400 Subject: [PATCH 3/6] fix: replace gzipBytes with MakeGzip Signed-off-by: Guillaume Belanger --- internal/archive/archive_test.go | 20 +++----------------- internal/archive/testarchive/testarchive.go | 16 ++++++++-------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 8d4e7e61e..a2e3190b1 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -4,8 +4,6 @@ import ( "golang.org/x/crypto/openpgp/packet" . "gopkg.in/check.v1" - "bytes" - "compress/gzip" "debug/elf" "errors" "flag" @@ -631,18 +629,6 @@ func read(r io.Reader) string { return string(data) } -func gzipBytes(s string) []byte { - var buf bytes.Buffer - w := gzip.NewWriter(&buf) - if _, err := w.Write([]byte(s)); err != nil { - panic(err) - } - if err := w.Close(); err != nil { - panic(err) - } - return buf.Bytes() -} - func (s *httpSuite) sawByHashRequest() bool { for _, req := range s.requests { if strings.Contains(req.URL.Path, "/by-hash/SHA256/") { @@ -655,8 +641,8 @@ func (s *httpSuite) sawByHashRequest() bool { func (s *httpSuite) TestFetchByHashSucceedsWhenNamedPathIsStale(c *C) { s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) { r.AcquireByHash = true - r.NamedPathContent = map[string][]byte{ - "main/binary-amd64/Packages.gz": gzipBytes("stale Packages from previous publication"), + r.PathOverrides = map[string][]byte{ + "main/binary-amd64/Packages.gz": testarchive.MakeGzip([]byte("stale Packages from previous publication")), } }) @@ -682,7 +668,7 @@ func (s *httpSuite) TestFetchByHashSucceedsWhenNamedPathIsStale(c *C) { func (s *httpSuite) TestFetchByHashFallsBackOnNotFound(c *C) { s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) { r.AcquireByHash = true - r.ByHashSkip = []string{"main/binary-amd64/Packages.gz"} + r.ByHashSkips = []string{"main/binary-amd64/Packages.gz"} }) options := archive.Options{ diff --git a/internal/archive/testarchive/testarchive.go b/internal/archive/testarchive/testarchive.go index 02b6dd3ef..2fc6ce551 100644 --- a/internal/archive/testarchive/testarchive.go +++ b/internal/archive/testarchive/testarchive.go @@ -54,7 +54,7 @@ func (gz *Gzip) Section() []byte { } func (gz *Gzip) Content() []byte { - return makeGzip(gz.Item.Content()) + return MakeGzip(gz.Item.Content()) } type Package struct { @@ -108,9 +108,9 @@ type Release struct { Items []Item PrivKey *packet.PrivateKey // Fields below model acquire-by-hash and mirror inconsistencies for tests. - AcquireByHash bool - ByHashSkip []string - NamedPathContent map[string][]byte + AcquireByHash bool + ByHashSkips []string + PathOverrides map[string][]byte } func (r *Release) Walk(f func(Item) error) error { @@ -166,8 +166,8 @@ func (r *Release) Content() []byte { } func (r *Release) Render(prefix string, content map[string][]byte) error { - skipByHash := make(map[string]bool, len(r.ByHashSkip)) - for _, p := range r.ByHashSkip { + skipByHash := make(map[string]bool, len(r.ByHashSkips)) + for _, p := range r.ByHashSkips { skipByHash[p] = true } return r.Walk(func(item Item) error { @@ -178,7 +178,7 @@ func (r *Release) Render(prefix string, content map[string][]byte) error { return nil } distItemPath := path.Join(prefix, "dists", r.Suite, itemPath) - if override, ok := r.NamedPathContent[itemPath]; ok { + if override, ok := r.PathOverrides[itemPath]; ok { content[distItemPath] = override } else { content[distItemPath] = itemContent @@ -225,7 +225,7 @@ func makeSha256(b []byte) string { return fmt.Sprintf("%x", sha256.Sum256(b)) } -func makeGzip(b []byte) []byte { +func MakeGzip(b []byte) []byte { var buf bytes.Buffer gz := gzip.NewWriter(&buf) _, err := gz.Write(b) From 5f1bc3baac490d923646d05aff01e801b486f02a Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 16 Jun 2026 11:40:21 +0200 Subject: [PATCH 4/6] style: polish names in fetchIndex Signed-off-by: Paul Mars --- internal/archive/archive.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 43393aca5..262025fda 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -329,10 +329,10 @@ func (index *ubuntuIndex) fetchRelease() error { } func (index *ubuntuIndex) fetchIndex() error { - digests := index.release.Get("SHA256") + releaseDigests := index.release.Get("SHA256") packagesPath := fmt.Sprintf("%s/binary-%s/Packages", index.component, index.arch) - digest, _, _ := control.ParsePathInfo(digests, packagesPath) - if digest == "" { + packagesDigest, _, _ := control.ParsePathInfo(releaseDigests, packagesPath) + if packagesDigest == "" { return fmt.Errorf("%s is missing from %s %s component digests", packagesPath, index.suite, index.component) } @@ -342,13 +342,13 @@ func (index *ubuntuIndex) fetchIndex() error { // are content-addressed and so are immune to the inconsistent view of // InRelease vs Packages.gz that mirrors and CDNs can serve while a // publication is propagating. See https://wiki.ubuntu.com/AptByHash. - gzPath := packagesPath + ".gz" + packagesGzPath := packagesPath + ".gz" var reader io.ReadSeekCloser if index.release.Get("Acquire-By-Hash") == "yes" { - gzDigest, _, _ := control.ParsePathInfo(digests, gzPath) - if gzDigest != "" { - byHashPath := fmt.Sprintf("%s/binary-%s/by-hash/SHA256/%s", index.component, index.arch, gzDigest) - r, err := index.fetch(index.distPath(byHashPath), digest, fetchBulk|fetchGzip) + packagesGzDigest, _, _ := control.ParsePathInfo(releaseDigests, packagesGzPath) + if packagesGzDigest != "" { + packagesByHashPath := fmt.Sprintf("%s/binary-%s/by-hash/SHA256/%s", index.component, index.arch, packagesGzDigest) + r, err := index.fetch(index.distPath(packagesByHashPath), packagesDigest, fetchBulk|fetchGzip) if err != nil && err != errNotFound { return err } @@ -358,7 +358,7 @@ func (index *ubuntuIndex) fetchIndex() error { } } if reader == nil { - r, err := index.fetch(index.distPath(gzPath), digest, fetchBulk|fetchGzip) + r, err := index.fetch(index.distPath(packagesGzPath), packagesDigest, fetchBulk|fetchGzip) if err != nil { return err } From aa82b97adb957df2f2d9e1d8f5064205402fcff2 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 17 Jun 2026 10:36:35 +0200 Subject: [PATCH 5/6] tests: rework approach to test by-hash URL usage Signed-off-by: Paul Mars --- internal/archive/archive_test.go | 77 +++++++++++++++------ internal/archive/testarchive/testarchive.go | 6 +- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index a2e3190b1..bce7e4a65 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -22,19 +22,25 @@ import ( "github.com/canonical/chisel/internal/testutil" ) +type requestResult struct { + path string + status int +} + type httpSuite struct { - logf func(string, ...any) - base string - request *http.Request - requests []*http.Request - response string - responses map[string][]byte - err error - header http.Header - status int - restore func() - privKey *packet.PrivateKey - pubKey *packet.PublicKey + logf func(string, ...any) + base string + request *http.Request + requests []*http.Request + requestResults []requestResult + response string + responses map[string][]byte + err error + header http.Header + status int + restore func() + privKey *packet.PrivateKey + pubKey *packet.PublicKey } var _ = Suite(&httpSuite{}) @@ -54,6 +60,7 @@ func (s *httpSuite) SetUpTest(c *C) { s.base = "http://archive.ubuntu.com/ubuntu/" s.request = nil s.requests = nil + s.requestResults = nil s.response = "" s.responses = make(map[string][]byte) s.header = nil @@ -91,6 +98,7 @@ func (s *httpSuite) Do(req *http.Request) (*http.Response, error) { // Unknown path with responses populated: behave like a real archive. status = 404 } + s.requestResults = append(s.requestResults, requestResult{path: req.URL.Path, status: status}) rsp := &http.Response{ Body: io.NopCloser(strings.NewReader(body)), Header: s.header, @@ -629,18 +637,21 @@ func read(r io.Reader) string { return string(data) } -func (s *httpSuite) sawByHashRequest() bool { - for _, req := range s.requests { - if strings.Contains(req.URL.Path, "/by-hash/SHA256/") { - return true +// fetchRequestStatus checks whether a request was made whose URL path +// contains the given substring. If so, it returns true and the HTTP status +// code from the most recent matching request. If not, it returns false, 0. +func (s *httpSuite) fetchRequestStatus(pathSubstring string) (bool, int) { + for i := len(s.requestResults) - 1; i >= 0; i-- { + if strings.Contains(s.requestResults[i].path, pathSubstring) { + return true, s.requestResults[i].status } } - return false + return false, 0 } func (s *httpSuite) TestFetchByHashSucceedsWhenNamedPathIsStale(c *C) { s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) { - r.AcquireByHash = true + r.ByHash = true r.PathOverrides = map[string][]byte{ "main/binary-amd64/Packages.gz": testarchive.MakeGzip([]byte("stale Packages from previous publication")), } @@ -662,12 +673,18 @@ func (s *httpSuite) TestFetchByHashSucceedsWhenNamedPathIsStale(c *C) { pkg, _, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") - c.Assert(s.sawByHashRequest(), Equals, true) + + // The by-hash request must have been attempted and succeeded with 200, + // since the named path has stale content and only by-hash has the + // correct data. + attempted, status := s.fetchRequestStatus("/by-hash/SHA256/") + c.Assert(attempted, Equals, true) + c.Assert(status, Equals, 200) } func (s *httpSuite) TestFetchByHashFallsBackOnNotFound(c *C) { s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) { - r.AcquireByHash = true + r.ByHash = true r.ByHashSkips = []string{"main/binary-amd64/Packages.gz"} }) @@ -687,7 +704,16 @@ func (s *httpSuite) TestFetchByHashFallsBackOnNotFound(c *C) { pkg, _, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") - c.Assert(s.sawByHashRequest(), Equals, true) + + // The by-hash request must have been attempted but got 404 (the hash + // was garbage-collected), so we fell back to the named path which + // returned 200 with the correct data. + attempted, status := s.fetchRequestStatus("/by-hash/SHA256/") + c.Assert(attempted, Equals, true) + c.Assert(status, Equals, 404) + attempted, status = s.fetchRequestStatus("Packages.gz") + c.Assert(attempted, Equals, true) + c.Assert(status, Equals, 200) } func (s *httpSuite) TestFetchSkipsByHashWhenNotAdvertised(c *C) { @@ -705,7 +731,14 @@ func (s *httpSuite) TestFetchSkipsByHashWhenNotAdvertised(c *C) { _, err := archive.Open(&options) c.Assert(err, IsNil) - c.Assert(s.sawByHashRequest(), Equals, false) + + // When by-hash is not advertised, no by-hash request should be + // attempted; only the named Packages.gz path is fetched. + attempted, _ := s.fetchRequestStatus("/by-hash/SHA256/") + c.Assert(attempted, Equals, false) + attempted, status := s.fetchRequestStatus("Packages.gz") + c.Assert(attempted, Equals, true) + c.Assert(status, Equals, 200) } // ---------------------------------------------------------------------------------------- diff --git a/internal/archive/testarchive/testarchive.go b/internal/archive/testarchive/testarchive.go index 2fc6ce551..d38298753 100644 --- a/internal/archive/testarchive/testarchive.go +++ b/internal/archive/testarchive/testarchive.go @@ -108,7 +108,7 @@ type Release struct { Items []Item PrivKey *packet.PrivateKey // Fields below model acquire-by-hash and mirror inconsistencies for tests. - AcquireByHash bool + ByHash bool ByHashSkips []string PathOverrides map[string][]byte } @@ -132,7 +132,7 @@ func (r *Release) Content() []byte { fmt.Fprintf(&digests, " %s %d %s\n", makeSha256(content), len(content), item.Path()) } acquireByHash := "" - if r.AcquireByHash { + if r.ByHash { acquireByHash = "Acquire-By-Hash: yes\n" } content := fmt.Sprintf(string(testutil.Reindent(` @@ -183,7 +183,7 @@ func (r *Release) Render(prefix string, content map[string][]byte) error { } else { content[distItemPath] = itemContent } - if r.AcquireByHash && itemPath != r.Path() && !skipByHash[itemPath] { + if r.ByHash && itemPath != r.Path() && !skipByHash[itemPath] { byHashPath := path.Join(prefix, "dists", r.Suite, path.Dir(itemPath), "by-hash", "SHA256", makeSha256(itemContent)) content[byHashPath] = itemContent } From f80cc1b73762bb1475475e8cc07ce165ea439e4e Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 17 Jun 2026 15:59:13 +0200 Subject: [PATCH 6/6] test: simplify Render Signed-off-by: Paul Mars --- internal/archive/archive_test.go | 19 +++++++++++++++---- internal/archive/testarchive/testarchive.go | 19 +++++-------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index bce7e4a65..591defb60 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -652,11 +652,16 @@ func (s *httpSuite) fetchRequestStatus(pathSubstring string) (bool, int) { func (s *httpSuite) TestFetchByHashSucceedsWhenNamedPathIsStale(c *C) { s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) { r.ByHash = true - r.PathOverrides = map[string][]byte{ - "main/binary-amd64/Packages.gz": testarchive.MakeGzip([]byte("stale Packages from previous publication")), - } }) + // Simulate a mirror serving stale content at the named Packages.gz path + // while the by-hash path still has the correct data. + for p := range s.responses { + if strings.Contains(p, "Packages.gz") && !strings.Contains(p, "/by-hash/") { + s.responses[p] = testarchive.MakeGzip([]byte("stale Packages from previous publication")) + } + } + options := archive.Options{ Label: "ubuntu", Version: "22.04", @@ -685,9 +690,15 @@ func (s *httpSuite) TestFetchByHashSucceedsWhenNamedPathIsStale(c *C) { func (s *httpSuite) TestFetchByHashFallsBackOnNotFound(c *C) { s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) { r.ByHash = true - r.ByHashSkips = []string{"main/binary-amd64/Packages.gz"} }) + // Simulate a mirror that garbage-collected the by-hash entries. + for p := range s.responses { + if strings.Contains(p, "/by-hash/") { + delete(s.responses, p) + } + } + options := archive.Options{ Label: "ubuntu", Version: "22.04", diff --git a/internal/archive/testarchive/testarchive.go b/internal/archive/testarchive/testarchive.go index d38298753..593b443a7 100644 --- a/internal/archive/testarchive/testarchive.go +++ b/internal/archive/testarchive/testarchive.go @@ -107,10 +107,9 @@ type Release struct { Label string Items []Item PrivKey *packet.PrivateKey - // Fields below model acquire-by-hash and mirror inconsistencies for tests. - ByHash bool - ByHashSkips []string - PathOverrides map[string][]byte + // ByHash enables the Acquire-By-Hash flag in the Release file + // and renders by-hash URLs alongside named paths. + ByHash bool } func (r *Release) Walk(f func(Item) error) error { @@ -166,10 +165,6 @@ func (r *Release) Content() []byte { } func (r *Release) Render(prefix string, content map[string][]byte) error { - skipByHash := make(map[string]bool, len(r.ByHashSkips)) - for _, p := range r.ByHashSkips { - skipByHash[p] = true - } return r.Walk(func(item Item) error { itemPath := item.Path() itemContent := item.Content() @@ -178,12 +173,8 @@ func (r *Release) Render(prefix string, content map[string][]byte) error { return nil } distItemPath := path.Join(prefix, "dists", r.Suite, itemPath) - if override, ok := r.PathOverrides[itemPath]; ok { - content[distItemPath] = override - } else { - content[distItemPath] = itemContent - } - if r.ByHash && itemPath != r.Path() && !skipByHash[itemPath] { + content[distItemPath] = itemContent + if r.ByHash && itemPath != r.Path() { byHashPath := path.Join(prefix, "dists", r.Suite, path.Dir(itemPath), "by-hash", "SHA256", makeSha256(itemContent)) content[byHashPath] = itemContent }