diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 7c90a41dd..1d3f1af69 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -139,8 +139,9 @@ func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, erro return nil, nil, err } path := section.Get("Filename") + digest, digestKind := packageDigest(section) logf("Fetching %s...", path) - reader, err := index.fetch(path, section.Get("SHA256"), fetchBulk) + reader, err := index.fetch(path, digest, digestKind, fetchBulk) if err != nil { return nil, nil, err } @@ -280,7 +281,9 @@ func openUbuntu(options *Options) (Archive, error) { func (index *ubuntuIndex) fetchRelease() error { logf("Fetching %s %s %s suite details...", index.displayName(), index.version, index.suite) - reader, err := index.fetch(index.distPath("InRelease"), "", fetchDefault) + // InRelease has no digest to check against (it is verified by its PGP + // signature below), so the digest kind here is arbitrary. + reader, err := index.fetch(index.distPath("InRelease"), "", cache.SHA256, fetchDefault) if err != nil { return err } @@ -328,10 +331,47 @@ func (index *ubuntuIndex) fetchRelease() error { return nil } +// digestField is an archive checksum field Chisel can verify. Its name +// doubles as the by-hash directory name in the archive layout. +type digestField struct { + name string + kind cache.DigestKind +} + +// digestFields lists the checksum fields Chisel can verify, in order of +// preference: strongest first. The order also matches the by-hash archive +// layout, where only the by-hash directory of the strongest advertised hash +// is guaranteed to exist. +var digestFields = []digestField{ + {"SHA512", cache.SHA512}, + {"SHA256", cache.SHA256}, +} + +// findDigest returns the digest recorded for path in the release, along with +// the field it was found in, trying the given fields in order. +func findDigest(release control.Section, path string, order []digestField) (digest string, field digestField) { + for _, f := range order { + if d, _, ok := control.ParsePathInfo(release.Get(f.name), path); ok { + return d, f + } + } + return "", digestField{} +} + +func packageDigest(section control.Section) (digest string, kind cache.DigestKind) { + for _, f := range digestFields { + if d := section.Get(f.name); d != "" { + return d, f.kind + } + } + // No digest advertised; fall back to SHA256 so the package can still be + // cached and retrieved by its computed digest. + return "", cache.SHA256 +} + func (index *ubuntuIndex) fetchIndex() error { - releaseDigests := index.release.Get("SHA256") packagesPath := fmt.Sprintf("%s/binary-%s/Packages", index.component, index.arch) - packagesDigest, _, _ := control.ParsePathInfo(releaseDigests, packagesPath) + packagesDigest, field := findDigest(index.release, packagesPath, digestFields) if packagesDigest == "" { return fmt.Errorf("%s is missing from %s %s component digests", packagesPath, index.suite, index.component) } @@ -345,10 +385,14 @@ func (index *ubuntuIndex) fetchIndex() error { packagesGzPath := packagesPath + ".gz" var reader io.ReadSeekCloser if index.release.Get("Acquire-By-Hash") == "yes" { - packagesGzDigest, _, _ := control.ParsePathInfo(releaseDigests, packagesGzPath) + // By-hash directories are only guaranteed to exist for the strongest + // hash the archive advertises, which is what findDigest prefers. If + // the archive advertises a hash stronger than any Chisel knows, the + // URL may 404 and the named-path fallback below applies. + packagesGzDigest, byHashField := findDigest(index.release, packagesGzPath, digestFields) 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) + packagesByHashPath := fmt.Sprintf("%s/binary-%s/by-hash/%s/%s", index.component, index.arch, byHashField.name, packagesGzDigest) + r, err := index.fetch(index.distPath(packagesByHashPath), packagesDigest, field.kind, fetchBulk|fetchGzip) if err != nil && err != errNotFound { return err } @@ -358,7 +402,7 @@ func (index *ubuntuIndex) fetchIndex() error { } } if reader == nil { - r, err := index.fetch(index.distPath(packagesGzPath), packagesDigest, fetchBulk|fetchGzip) + r, err := index.fetch(index.distPath(packagesGzPath), packagesDigest, field.kind, fetchBulk|fetchGzip) if err != nil { return err } @@ -399,8 +443,7 @@ func (index *ubuntuIndex) distPath(suffix string) string { return "dists/" + index.suite + "/" + suffix } -func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadSeekCloser, error) { - const digestKind = cache.SHA256 +func (index *ubuntuIndex) fetch(path, digest string, digestKind cache.DigestKind, flags fetchFlags) (io.ReadSeekCloser, error) { reader, err := index.archive.cache.Open(digestKind, digest) if err == nil { return reader, nil diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 591defb60..a95a5065b 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" + "crypto/sha256" + "crypto/sha512" "debug/elf" "errors" "flag" @@ -129,10 +131,14 @@ func (s *httpSuite) prepareArchive(suite, version, arch string, components []str func (s *httpSuite) prepareArchiveAdjustRelease(suite, version, arch string, components []string, adjustRelease func(*testarchive.Release)) *testarchive.Release { release := &testarchive.Release{ - Suite: suite, - Version: version, - Label: "Ubuntu", - PrivKey: s.privKey, + Suite: suite, + Version: version, + Label: "Ubuntu", + PrivKey: s.privKey, + DigestKinds: []string{"SHA256"}, + } + if adjustRelease != nil { + adjustRelease(release) } for i, component := range components { index := &testarchive.PackageIndex{ @@ -142,10 +148,11 @@ func (s *httpSuite) prepareArchiveAdjustRelease(suite, version, arch string, com for j := range 2 { seq := 1 + i*2 + j index.Packages = append(index.Packages, &testarchive.Package{ - Name: fmt.Sprintf("mypkg%d", seq), - Version: fmt.Sprintf("1.%d", seq), - Arch: arch, - Component: component, + Name: fmt.Sprintf("mypkg%d", seq), + Version: fmt.Sprintf("1.%d", seq), + Arch: arch, + Component: component, + DigestKinds: release.DigestKinds, }) } release.Items = append(release.Items, index) @@ -155,10 +162,10 @@ func (s *httpSuite) prepareArchiveAdjustRelease(suite, version, arch string, com if err != nil { panic(err) } - if adjustRelease != nil { - adjustRelease(release) + err = release.Render(base.Path, s.responses) + if err != nil { + panic(err) } - release.Render(base.Path, s.responses) return release } @@ -264,6 +271,72 @@ func (s *httpSuite) TestFetchPackage(c *C) { c.Assert(read(pkg), Equals, "mypkg4 1.4 data") } +func (s *httpSuite) TestFetchSHA512Digests(c *C) { + // Ubuntu 26.10+ publishes SHA512-only indices (no SHA256 section), so both + // the index digest and the package digest must be read from SHA512. + s.prepareArchiveAdjustRelease("stonking", "25.10", "amd64", []string{"main", "universe"}, + func(release *testarchive.Release) { + release.DigestKinds = []string{"SHA512"} + }) + + options := archive.Options{ + Label: "ubuntu", + Version: "25.10", + Arch: "amd64", + Suites: []string{"stonking"}, + Components: []string{"main", "universe"}, + 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") +} + +func (s *httpSuite) TestFetchBothDigests(c *C) { + // An archive publishing both SHA256 and SHA512 sections (index table and + // package fields) must be handled, with the strongest digest preferred + // for verification and caching. PackageInfo.SHA256 still surfaces: it is + // read from the package section directly, not from the preference order. + s.prepareArchiveAdjustRelease("stonking", "25.10", "amd64", []string{"main", "universe"}, + func(release *testarchive.Release) { + release.DigestKinds = []string{"SHA256", "SHA512"} + }) + + options := archive.Options{ + Label: "ubuntu", + Version: "25.10", + Arch: "amd64", + Suites: []string{"stonking"}, + Components: []string{"main", "universe"}, + CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, + } + + testArchive, err := archive.Open(&options) + c.Assert(err, IsNil) + + pkg, info, err := testArchive.Fetch("mypkg1") + c.Assert(err, IsNil) + c.Assert(info, DeepEquals, &archive.PackageInfo{ + Name: "mypkg1", + Version: "1.1", + Arch: "amd64", + SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + }) + c.Assert(read(pkg), Equals, "mypkg1 1.1 data") + + // Pin the cache key: with both digests advertised, the package is cached + // under its strongest digest. + sha512Digest := fmt.Sprintf("%x", sha512.Sum512([]byte("mypkg1 1.1 data"))) + _, err = os.Stat(filepath.Join(options.CacheDir, "sha512", sha512Digest)) + c.Assert(err, IsNil) +} + func (s *httpSuite) TestFetchPortsPackage(c *C) { s.base = "http://ports.ubuntu.com/ubuntu-ports/" @@ -310,14 +383,16 @@ func (s *httpSuite) TestFetchSecurityPackage(c *C) { for i, suite := range []string{"jammy", "jammy-updates", "jammy-security"} { release := s.prepareArchive(suite, "22.04", "amd64", []string{"main", "universe"}) - release.Walk(func(item testarchive.Item) error { + err := release.Walk(func(item testarchive.Item) error { if p, ok := item.(*testarchive.Package); ok && p.Name == "mypkg1" { p.Version = fmt.Sprintf("%s.%d", p.Version, i) p.Data = []byte("package from " + suite) } return nil }) - release.Render("/ubuntu", s.responses) + c.Assert(err, IsNil) + err = release.Render("/ubuntu", s.responses) + c.Assert(err, IsNil) } options := archive.Options{ @@ -687,6 +762,191 @@ func (s *httpSuite) TestFetchByHashSucceedsWhenNamedPathIsStale(c *C) { c.Assert(status, Equals, 200) } +func (s *httpSuite) TestFetchByHashSHA512(c *C) { + // Ubuntu 26.10+ advertises Acquire-By-Hash with SHA512-only indices, so + // the by-hash URL must be built under the SHA512 directory. + s.prepareArchiveAdjustRelease("stonking", "26.10", "amd64", []string{"main"}, func(release *testarchive.Release) { + release.ByHash = true + release.DigestKinds = []string{"SHA512"} + }) + + // Stale content at the named Packages.gz path, so a fallback would fail + // the digest check -- only the by-hash path serves the correct bytes. + 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: "26.10", + Arch: "amd64", + Suites: []string{"stonking"}, + 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") + + // The SHA512 by-hash request must have been attempted and succeeded; + // the named path only has stale content. + attempted, status := s.fetchRequestStatus("/by-hash/SHA512/") + c.Assert(attempted, Equals, true) + c.Assert(status, Equals, 200) +} + +func (s *httpSuite) TestFetchByHashBothDigests(c *C) { + // When a by-hash archive publishes both digests, the by-hash URL must be + // built under SHA512: archives only guarantee a by-hash directory for the + // strongest hash they advertise. SHA256 by-hash must not be requested. + s.prepareArchiveAdjustRelease("stonking", "26.10", "amd64", []string{"main"}, func(release *testarchive.Release) { + release.ByHash = true + release.DigestKinds = []string{"SHA256", "SHA512"} + }) + + // Stale content at the named Packages.gz path, so a fallback would fail + // the digest check -- only the by-hash path serves the correct bytes. + 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: "26.10", + Arch: "amd64", + Suites: []string{"stonking"}, + 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") + + // The SHA512 by-hash request must have been made and succeeded; the SHA256 + // by-hash directory must never be touched. + attempted, status := s.fetchRequestStatus("/by-hash/SHA512/") + c.Assert(attempted, Equals, true) + c.Assert(status, Equals, 200) + attempted, _ = s.fetchRequestStatus("/by-hash/SHA256/") + c.Assert(attempted, Equals, false) +} + +// addSha256ByHashDirs mirrors every published by-hash/SHA512 entry under a +// by-hash/SHA256 path, like archives publishing by-hash directories for +// more hashes than just the strongest one (e.g. stonking). +func (s *httpSuite) addSha256ByHashDirs() { + mirrored := make(map[string][]byte) + for p, content := range s.responses { + prefix, _, found := strings.Cut(p, "/by-hash/SHA512/") + if !found { + continue + } + mirrored[fmt.Sprintf("%s/by-hash/SHA256/%x", prefix, sha256.Sum256(content))] = content + } + for p, content := range mirrored { + s.responses[p] = content + } +} + +func (s *httpSuite) TestFetchByHashManyDigestDirs(c *C) { + // An archive may publish by-hash directories for more hashes than the + // strongest one it advertises. The by-hash URL must still be built under + // SHA512, and the SHA256 directory left alone, even though a request + // there would now succeed. + s.prepareArchiveAdjustRelease("stonking", "26.10", "amd64", []string{"main"}, func(release *testarchive.Release) { + release.ByHash = true + release.DigestKinds = []string{"SHA256", "SHA512"} + }) + s.addSha256ByHashDirs() + + // Stale content at the named Packages.gz path, so a fallback would fail + // the digest check -- only the by-hash paths serve the correct bytes. + 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: "26.10", + Arch: "amd64", + Suites: []string{"stonking"}, + 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") + + attempted, status := s.fetchRequestStatus("/by-hash/SHA512/") + c.Assert(attempted, Equals, true) + c.Assert(status, Equals, 200) + attempted, _ = s.fetchRequestStatus("/by-hash/SHA256/") + c.Assert(attempted, Equals, false) +} + +func (s *httpSuite) TestFetchByHashOnlyWeakerDigestDir(c *C) { + // A buggy archive advertising both digests but publishing a by-hash + // directory only for the weaker one. The SHA512 by-hash request 404s and + // the named path takes over; the SHA256 directory must not be tried. + s.prepareArchiveAdjustRelease("stonking", "26.10", "amd64", []string{"main"}, func(release *testarchive.Release) { + release.ByHash = true + release.DigestKinds = []string{"SHA256", "SHA512"} + }) + s.addSha256ByHashDirs() + for p := range s.responses { + if strings.Contains(p, "/by-hash/SHA512/") { + delete(s.responses, p) + } + } + + options := archive.Options{ + Label: "ubuntu", + Version: "26.10", + Arch: "amd64", + Suites: []string{"stonking"}, + 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") + + attempted, status := s.fetchRequestStatus("/by-hash/SHA512/") + c.Assert(attempted, Equals, true) + c.Assert(status, Equals, 404) + 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) +} + func (s *httpSuite) TestFetchByHashFallsBackOnNotFound(c *C) { s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) { r.ByHash = true diff --git a/internal/archive/testarchive/testarchive.go b/internal/archive/testarchive/testarchive.go index 593b443a7..c4ef7822b 100644 --- a/internal/archive/testarchive/testarchive.go +++ b/internal/archive/testarchive/testarchive.go @@ -4,8 +4,10 @@ import ( "bytes" "compress/gzip" "crypto/sha256" + "crypto/sha512" "fmt" "path" + "slices" "strings" "golang.org/x/crypto/openpgp/clearsign" @@ -58,11 +60,12 @@ func (gz *Gzip) Content() []byte { } type Package struct { - Name string - Version string - Arch string - Component string - Data []byte + Name string + Version string + Arch string + Component string + Data []byte + DigestKinds []string } func (p *Package) Path() string { @@ -75,6 +78,13 @@ func (p *Package) Walk(f func(Item) error) error { func (p *Package) Section() []byte { content := p.Content() + digests := strings.Builder{} + for i, kind := range p.DigestKinds { + if i > 0 { + digests.WriteByte('\n') + } + fmt.Fprintf(&digests, "%s: %s", kind, makeDigest(kind, content)) + } section := fmt.Sprintf(string(testutil.Reindent(` Package: %s Architecture: %s @@ -86,11 +96,11 @@ func (p *Package) Section() []byte { Installed-Size: 10 Filename: %s Size: %d - SHA256: %s + %s Description: Description of %s Task: minimal - `)), p.Name, p.Arch, p.Version, p.Path(), len(content), makeSha256(content), p.Name) + `)), p.Name, p.Arch, p.Version, p.Path(), len(content), digests.String(), p.Name) return []byte(section) } @@ -107,6 +117,10 @@ type Release struct { Label string Items []Item PrivKey *packet.PrivateKey + // DigestKinds names the digest kinds published across this archive: the + // index table and every package it publishes. Callers must set it + // explicitly; an empty list publishes no digest sections at all. + DigestKinds []string // ByHash enables the Acquire-By-Hash flag in the Release file // and renders by-hash URLs alongside named paths. ByHash bool @@ -126,9 +140,12 @@ func (r *Release) Section() []byte { func (r *Release) Content() []byte { digests := bytes.Buffer{} - for _, item := range r.Items { - content := item.Content() - fmt.Fprintf(&digests, " %s %d %s\n", makeSha256(content), len(content), item.Path()) + for _, kind := range r.DigestKinds { + fmt.Fprintf(&digests, "%s:\n", kind) + for _, item := range r.Items { + content := item.Content() + fmt.Fprintf(&digests, " %s %d %s\n", makeDigest(kind, content), len(content), item.Path()) + } } acquireByHash := "" if r.ByHash { @@ -144,8 +161,7 @@ func (r *Release) Content() []byte { Architectures: amd64 arm64 armhf i386 ppc64el riscv64 s390x Components: main restricted universe multiverse Description: Ubuntu %s - %sSHA256: - %s + %s%s `)), r.Label, r.Suite, r.Version, r.Version, acquireByHash, digests.String()) var buf bytes.Buffer @@ -165,6 +181,7 @@ func (r *Release) Content() []byte { } func (r *Release) Render(prefix string, content map[string][]byte) error { + byHashKind := strongestKind(r.DigestKinds) return r.Walk(func(item Item) error { itemPath := item.Path() itemContent := item.Content() @@ -174,8 +191,13 @@ func (r *Release) Render(prefix string, content map[string][]byte) error { } distItemPath := path.Join(prefix, "dists", r.Suite, 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)) + // Archives only guarantee a by-hash directory for the strongest + // hash they advertise, though they may publish more. Render the + // guaranteed one only, so tests catch clients building by-hash + // URLs from a weaker hash; tests wanting extra directories add + // them on top of the rendered content. + if r.ByHash && itemPath != r.Path() && byHashKind != "" { + byHashPath := path.Join(prefix, "dists", r.Suite, path.Dir(itemPath), "by-hash", byHashKind, makeDigest(byHashKind, itemContent)) content[byHashPath] = itemContent } return nil @@ -212,8 +234,28 @@ func (pi *PackageIndex) Content() []byte { return MergeSections(pi.Packages) } -func makeSha256(b []byte) string { - return fmt.Sprintf("%x", sha256.Sum256(b)) +// digestKinds lists the digest kinds this package can render, strongest first. +var digestKinds = []string{"SHA512", "SHA256"} + +// strongestKind returns the strongest of kinds, or "" if kinds holds none of +// digestKinds. +func strongestKind(kinds []string) string { + for _, kind := range digestKinds { + if slices.Contains(kinds, kind) { + return kind + } + } + return "" +} + +func makeDigest(kind string, b []byte) string { + switch kind { + case "SHA512": + return fmt.Sprintf("%x", sha512.Sum512(b)) + case "SHA256": + return fmt.Sprintf("%x", sha256.Sum256(b)) + } + panic("unknown digest kind: " + kind) } func MakeGzip(b []byte) []byte { diff --git a/internal/cache/cache.go b/internal/cache/cache.go index aedc2df0f..3b5621ecc 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -2,6 +2,7 @@ package cache import ( "crypto/sha256" + "crypto/sha512" "encoding/hex" "fmt" "hash" @@ -96,9 +97,10 @@ type DigestKind string const ( SHA256 DigestKind = "sha256" SHA384 DigestKind = "sha384" + SHA512 DigestKind = "sha512" ) -var digestKinds = []DigestKind{SHA256, SHA384} +var digestKinds = []DigestKind{SHA256, SHA384, SHA512} var ErrMiss = fmt.Errorf("not cached") @@ -117,6 +119,8 @@ func (c *Cache) Create(digestKind DigestKind, digest string) *Writer { h = sha256.New() case SHA384: h = sha3.New384() + case SHA512: + h = sha512.New() default: return &Writer{err: fmt.Errorf("internal error: unsupported digest kind: %q", digestKind)} } diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index 100f02579..7267a2e85 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -179,10 +179,37 @@ func (s *S) TestCacheSHA384(c *C) { c.Assert(err, Equals, cache.ErrMiss) } -func (s *S) TestCacheExpireBothDigestKinds(c *C) { +func (s *S) TestCacheSHA512(c *C) { cc := cache.Cache{Dir: c.MkDir()} - // Write entries under both digest kinds. + w := cc.Create(cache.SHA512, "") + _, err := w.Write([]byte("data1")) + c.Assert(err, IsNil) + err = w.Close() + c.Assert(err, IsNil) + sha512Digest := w.Digest() + + c.Assert(sha512Digest, Not(Equals), data1Digest) + + data, err := cc.Read(cache.SHA512, sha512Digest) + c.Assert(err, IsNil) + c.Assert(string(data), Equals, "data1") + + // SHA512 entry must not be visible under SHA256. + _, err = cc.Open(cache.SHA256, sha512Digest) + c.Assert(err, Equals, cache.ErrMiss) + + // SHA256 entry must not be visible under SHA512. + err = cc.Write(cache.SHA256, data1Digest, []byte("data1")) + c.Assert(err, IsNil) + _, err = cc.Open(cache.SHA512, data1Digest) + c.Assert(err, Equals, cache.ErrMiss) +} + +func (s *S) TestCacheExpireAllDigestKinds(c *C) { + cc := cache.Cache{Dir: c.MkDir()} + + // Write entries under all digest kinds. err := cc.Write(cache.SHA256, data1Digest, []byte("data1")) c.Assert(err, IsNil) err = cc.Write(cache.SHA256, data2Digest, []byte("data2")) @@ -202,10 +229,26 @@ func (s *S) TestCacheExpireBothDigestKinds(c *C) { c.Assert(err, IsNil) sha384Digest2 := w.Digest() + w = cc.Create(cache.SHA512, "") + _, err = w.Write([]byte("sha512data1")) + c.Assert(err, IsNil) + err = w.Close() + c.Assert(err, IsNil) + sha512Digest1 := w.Digest() + + w = cc.Create(cache.SHA512, "") + _, err = w.Write([]byte("sha512data2")) + c.Assert(err, IsNil) + err = w.Close() + c.Assert(err, IsNil) + sha512Digest2 := w.Digest() + sha256Expired := filepath.Join(cc.Dir, "sha256", data1Digest) sha256Fresh := filepath.Join(cc.Dir, "sha256", data2Digest) sha384Expired := filepath.Join(cc.Dir, "sha384", sha384Digest1) sha384Fresh := filepath.Join(cc.Dir, "sha384", sha384Digest2) + sha512Expired := filepath.Join(cc.Dir, "sha512", sha512Digest1) + sha512Fresh := filepath.Join(cc.Dir, "sha512", sha512Digest2) // Mark one entry per digest kind as expired. now := time.Now() @@ -214,21 +257,27 @@ func (s *S) TestCacheExpireBothDigestKinds(c *C) { c.Assert(err, IsNil) err = os.Chtimes(sha384Expired, now, expiredTime) c.Assert(err, IsNil) + err = os.Chtimes(sha512Expired, now, expiredTime) + c.Assert(err, IsNil) err = cc.Expire(time.Hour) c.Assert(err, IsNil) - // Expired entries must be removed from both digest kind directories. + // Expired entries must be removed from all digest kind directories. _, err = os.Stat(sha256Expired) c.Assert(os.IsNotExist(err), Equals, true) _, err = os.Stat(sha384Expired) c.Assert(os.IsNotExist(err), Equals, true) + _, err = os.Stat(sha512Expired) + c.Assert(os.IsNotExist(err), Equals, true) - // Fresh entries must remain in both digest kind directories. + // Fresh entries must remain in all digest kind directories. _, err = os.Stat(sha256Fresh) c.Assert(err, IsNil) _, err = os.Stat(sha384Fresh) c.Assert(err, IsNil) + _, err = os.Stat(sha512Fresh) + c.Assert(err, IsNil) } func (s *S) TestCacheExpireMissingDigestKindDir(c *C) {