2026-03-31 14:52:50 -03:00
|
|
|
package image
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-01 12:45:43 -03:00
|
|
|
"archive/zip"
|
2026-04-01 12:52:13 -03:00
|
|
|
"crypto/md5"
|
2026-03-31 14:52:50 -03:00
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
2026-04-11 00:17:10 -03:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2026-03-31 14:52:50 -03:00
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestDownloadDecompressesXZFileURLs(t *testing.T) {
|
|
|
|
|
if _, err := exec.LookPath("xz"); err != nil {
|
|
|
|
|
t.Skip("xz not available")
|
|
|
|
|
}
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
raw := filepath.Join(dir, "base.img")
|
|
|
|
|
if err := os.WriteFile(raw, []byte("metis-xz-test"), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
compressed := raw + ".xz"
|
|
|
|
|
cmd := exec.Command("xz", "-zk", raw)
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
t.Fatalf("xz: %v: %s", err, string(out))
|
|
|
|
|
}
|
|
|
|
|
dest := filepath.Join(dir, "copy.img")
|
|
|
|
|
if err := Download("file://"+compressed, dest); err != nil {
|
|
|
|
|
t.Fatalf("Download: %v", err)
|
|
|
|
|
}
|
2026-03-31 19:00:48 -03:00
|
|
|
data, err := os.ReadFile(dest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadFile: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if string(data) != "metis-xz-test" {
|
|
|
|
|
t.Fatalf("unexpected decompressed content: %q", string(data))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDownloadAndVerifyUsesArchiveChecksumForXZ(t *testing.T) {
|
|
|
|
|
if _, err := exec.LookPath("xz"); err != nil {
|
|
|
|
|
t.Skip("xz not available")
|
|
|
|
|
}
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
raw := filepath.Join(dir, "base.img")
|
|
|
|
|
if err := os.WriteFile(raw, []byte("metis-xz-test"), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
compressed := raw + ".xz"
|
|
|
|
|
cmd := exec.Command("xz", "-zk", raw)
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
t.Fatalf("xz: %v: %s", err, string(out))
|
|
|
|
|
}
|
|
|
|
|
archiveBytes, err := os.ReadFile(compressed)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadFile archive: %v", err)
|
|
|
|
|
}
|
|
|
|
|
archiveSum := sha256.Sum256(archiveBytes)
|
|
|
|
|
dest := filepath.Join(dir, "copy.img")
|
|
|
|
|
localPath, err := DownloadAndVerify("file://"+compressed, dest, "sha256:"+hex.EncodeToString(archiveSum[:]))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("DownloadAndVerify: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if localPath != dest {
|
|
|
|
|
t.Fatalf("expected local path %s, got %s", dest, localPath)
|
|
|
|
|
}
|
|
|
|
|
data, err := os.ReadFile(dest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadFile dest: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if string(data) != "metis-xz-test" {
|
|
|
|
|
t.Fatalf("unexpected decompressed content: %q", string(data))
|
2026-03-31 14:52:50 -03:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 19:06:56 -03:00
|
|
|
|
|
|
|
|
func TestDownloadAndVerifyReplacesStaleBadArchiveCache(t *testing.T) {
|
|
|
|
|
if _, err := exec.LookPath("xz"); err != nil {
|
|
|
|
|
t.Skip("xz not available")
|
|
|
|
|
}
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
raw := filepath.Join(dir, "base.img")
|
|
|
|
|
if err := os.WriteFile(raw, []byte("metis-xz-test"), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
compressed := raw + ".xz"
|
|
|
|
|
cmd := exec.Command("xz", "-zk", raw)
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
t.Fatalf("xz: %v: %s", err, string(out))
|
|
|
|
|
}
|
|
|
|
|
archiveBytes, err := os.ReadFile(compressed)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadFile archive: %v", err)
|
|
|
|
|
}
|
|
|
|
|
archiveSum := sha256.Sum256(archiveBytes)
|
|
|
|
|
dest := filepath.Join(dir, "copy.img")
|
|
|
|
|
staleArchive := dest + ".xz"
|
|
|
|
|
if err := os.WriteFile(staleArchive, []byte("bad-cache"), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("WriteFile stale archive: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := DownloadAndVerify("file://"+compressed, dest, "sha256:"+hex.EncodeToString(archiveSum[:])); err != nil {
|
|
|
|
|
t.Fatalf("DownloadAndVerify with stale archive: %v", err)
|
|
|
|
|
}
|
|
|
|
|
data, err := os.ReadFile(dest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadFile dest: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if string(data) != "metis-xz-test" {
|
|
|
|
|
t.Fatalf("unexpected decompressed content: %q", string(data))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-01 12:45:43 -03:00
|
|
|
|
|
|
|
|
func TestDownloadDecompressesZIPFileURLs(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
archive := filepath.Join(dir, "base.zip")
|
|
|
|
|
if err := writeTestZIP(archive, map[string]string{
|
|
|
|
|
"nested/base.img": "metis-zip-test",
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("writeTestZIP: %v", err)
|
|
|
|
|
}
|
|
|
|
|
dest := filepath.Join(dir, "copy.img")
|
|
|
|
|
if err := Download("file://"+archive, dest); err != nil {
|
|
|
|
|
t.Fatalf("Download: %v", err)
|
|
|
|
|
}
|
|
|
|
|
data, err := os.ReadFile(dest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadFile: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if string(data) != "metis-zip-test" {
|
|
|
|
|
t.Fatalf("unexpected decompressed content: %q", string(data))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDownloadAndVerifyUsesArchiveChecksumForZIP(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
archive := filepath.Join(dir, "base.zip")
|
|
|
|
|
if err := writeTestZIP(archive, map[string]string{
|
|
|
|
|
"base.img": "metis-zip-test",
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("writeTestZIP: %v", err)
|
|
|
|
|
}
|
|
|
|
|
archiveBytes, err := os.ReadFile(archive)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadFile archive: %v", err)
|
|
|
|
|
}
|
|
|
|
|
archiveSum := sha256.Sum256(archiveBytes)
|
|
|
|
|
dest := filepath.Join(dir, "copy.img")
|
|
|
|
|
localPath, err := DownloadAndVerify("file://"+archive, dest, "sha256:"+hex.EncodeToString(archiveSum[:]))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("DownloadAndVerify: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if localPath != dest {
|
|
|
|
|
t.Fatalf("expected local path %s, got %s", dest, localPath)
|
|
|
|
|
}
|
|
|
|
|
data, err := os.ReadFile(dest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadFile dest: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if string(data) != "metis-zip-test" {
|
|
|
|
|
t.Fatalf("unexpected decompressed content: %q", string(data))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 12:52:13 -03:00
|
|
|
func TestVerifyChecksumAcceptsMD5(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
path := filepath.Join(dir, "sample.img")
|
|
|
|
|
if err := os.WriteFile(path, []byte("metis-md5-test"), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("WriteFile: %v", err)
|
|
|
|
|
}
|
|
|
|
|
sum := md5.Sum([]byte("metis-md5-test"))
|
|
|
|
|
if err := VerifyChecksum(path, "md5:"+hex.EncodeToString(sum[:])); err != nil {
|
|
|
|
|
t.Fatalf("VerifyChecksum md5: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 00:17:10 -03:00
|
|
|
func TestDownloadAndVerifyUsesHTTPAndCachedFile(t *testing.T) {
|
|
|
|
|
body := []byte("metis-http-test")
|
|
|
|
|
sum := sha256.Sum256(body)
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path != "/image.img" {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, _ = w.Write(body)
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
dest := filepath.Join(dir, "image.img")
|
|
|
|
|
path, err := DownloadAndVerify(srv.URL+"/image.img", dest, "sha256:"+hex.EncodeToString(sum[:]))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("DownloadAndVerify: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if path != dest {
|
|
|
|
|
t.Fatalf("path = %q, want %q", path, dest)
|
|
|
|
|
}
|
|
|
|
|
if got, _ := os.ReadFile(dest); string(got) != string(body) {
|
|
|
|
|
t.Fatalf("downloaded body = %q", string(got))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := os.WriteFile(dest, body, 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := DownloadAndVerify(srv.URL+"/image.img", dest, "sha256:"+hex.EncodeToString(sum[:])); err != nil {
|
|
|
|
|
t.Fatalf("cached DownloadAndVerify: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDownloadAndVerifyRejectsBadChecksum(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
src := filepath.Join(dir, "src.img")
|
|
|
|
|
if err := os.WriteFile(src, []byte("bad"), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := DownloadAndVerify("file://"+src, filepath.Join(dir, "dest.img"), "sha256:deadbeef"); err == nil {
|
|
|
|
|
t.Fatal("expected checksum mismatch")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDownloadAndVerifyRawAndErrorBranches(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
src := filepath.Join(dir, "src.img")
|
|
|
|
|
if err := os.WriteFile(src, []byte("raw"), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
dest := filepath.Join(dir, "dest.img")
|
|
|
|
|
if _, err := DownloadAndVerify("file://"+src, dest, ""); err != nil {
|
|
|
|
|
t.Fatalf("DownloadAndVerify raw: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := VerifyChecksum(dest, "bogus"); err == nil {
|
|
|
|
|
t.Fatal("expected invalid checksum format error")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
http.Error(w, "boom", http.StatusInternalServerError)
|
|
|
|
|
}))
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
if err := downloadRaw(srv.URL, filepath.Join(dir, "bad.img")); err == nil {
|
|
|
|
|
t.Fatal("expected HTTP error from downloadRaw")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
archive := filepath.Join(dir, "empty.zip")
|
|
|
|
|
if err := writeTestZIP(archive, map[string]string{}); err != nil {
|
|
|
|
|
t.Fatalf("writeTestZIP: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := decompressZIP(archive, filepath.Join(dir, "out.img")); err == nil {
|
|
|
|
|
t.Fatal("expected empty zip error")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 12:45:43 -03:00
|
|
|
func writeTestZIP(path string, files map[string]string) error {
|
|
|
|
|
out, err := os.Create(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer out.Close()
|
|
|
|
|
zw := zip.NewWriter(out)
|
|
|
|
|
for name, content := range files {
|
|
|
|
|
w, err := zw.Create(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
_ = zw.Close()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if _, err := w.Write([]byte(content)); err != nil {
|
|
|
|
|
_ = zw.Close()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := zw.Close(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return out.Sync()
|
|
|
|
|
}
|