+package main
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/hex"
+ "os"
+ "path/filepath"
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestImagesArePrivateSharedAndWrittenOnce(t *testing.T) {
+ root, err := openAgent(filepath.Join(t.TempDir(), "agent"))
+ require.NoError(t, err)
+
+ child := &Agent{
+ dir: filepath.Join(root.dir, "children", "House"),
+ config: AgentConfig{Parent: root.config.Name},
+ }
+ require.Equal(t, root.Images().dir, child.Images().dir)
+
+ data := imageTestPNG(t, 3, 2)
+ sum := sha256.Sum256(data)
+ expected := hex.EncodeToString(sum[:]) + ".png"
+ store := root.Images()
+ name, err := store.Save(data)
+ require.NoError(t, err)
+ require.Equal(t, expected, name)
+
+ info, err := os.Stat(filepath.Join(store.dir, name))
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
+
+ var workers sync.WaitGroup
+
+ for range 8 {
+ workers.Go(func() {
+ saved, saveErr := child.Images().Save(data)
+ require.NoError(t, saveErr)
+ require.Equal(t, name, saved)
+ })
+ }
+
+ workers.Wait()
+
+ again, err := os.Stat(filepath.Join(store.dir, name))
+ require.NoError(t, err)
+ require.True(t, os.SameFile(info, again))
+
+ files, err := os.ReadDir(store.dir)
+ require.NoError(t, err)
+ require.Len(t, files, 1)
+
+ actual, err := store.Read(name)
+ require.NoError(t, err)
+ require.Equal(t, data, actual)
+}
+
+func TestImageStoreRejectsLargeUnsupportedAndUnsafeImages(t *testing.T) {
+ store := &ImageStore{dir: filepath.Join(t.TempDir(), imageDirName)}
+ _, err := store.Save(bytes.Repeat([]byte{0}, imageMaxBytes+1))
+ require.ErrorContains(t, err, "exceeds 20 MB")
+
+ _, err = store.Save([]byte("not an image"))
+ require.ErrorContains(t, err, "JPEG, PNG, or WebP")
+
+ _, err = store.Read("../secret.png")
+ require.ErrorContains(t, err, "invalid image name")
+
+ _, err = os.Stat(store.dir)
+ require.ErrorIs(t, err, os.ErrNotExist)
+}
+
+func TestBackupKeepsImagesAndTheirModes(t *testing.T) {
+ dir := t.TempDir()
+ store := &ImageStore{dir: filepath.Join(dir, imageDirName)}
+ data := imageTestPNG(t, 2, 2)
+ name, err := store.Save(data)
+ require.NoError(t, err)
+ require.NoError(t, os.WriteFile(filepath.Join(store.dir, ".image-incomplete"), []byte("partial"), 0o600))
+
+ stage := t.TempDir()
+ archive := filepath.Join(t.TempDir(), "images.tar.gz")
+ require.NoError(t, backupCopy(t.Context(), dir, stage, archive))
+
+ _, err = backupArchive(t.Context(), stage, archive)
+ require.NoError(t, err)
+
+ restored := t.TempDir()
+ require.NoError(t, backupRestore(t.Context(), archive, restored))
+
+ actual, err := (&ImageStore{dir: filepath.Join(restored, imageDirName)}).Read(name)
+ require.NoError(t, err)
+ require.Equal(t, data, actual)
+
+ info, err := os.Stat(filepath.Join(restored, imageDirName, name))
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
+
+ _, err = os.Stat(filepath.Join(restored, imageDirName, ".image-incomplete"))
+ require.ErrorIs(t, err, os.ErrNotExist)
+}
+
+func TestImageMarkersKeepCaptionAndOnlySafeNames(t *testing.T) {
+ name := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.png"
+ text := imageText([]string{name}, "Read the label.")
+ require.Equal(t, "[image "+name+"]\nRead the label.", text)
+ require.Equal(t, []string{name}, imageFiles(text+"\n[image ../secret.png]\ntext [image "+name+"]"))
+}