package main import ( "archive/tar" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestBackupArchiveRestoresBytesAndModes(t *testing.T) { root := t.TempDir() require.NoError(t, os.Mkdir(filepath.Join(root, "agent"), 0o750)) require.NoError(t, os.WriteFile(filepath.Join(root, "agent", "instructions.md"), []byte("Keep the whole chat.\n"), 0o640)) archive := filepath.Join(t.TempDir(), "backup.tar.gz") size, err := backupArchive(t.Context(), root, archive) require.NoError(t, err) info, err := os.Stat(archive) require.NoError(t, err) require.Equal(t, size, info.Size()) require.Equal(t, os.FileMode(0o600), info.Mode().Perm()) restored := t.TempDir() require.NoError(t, backupExtract(t.Context(), archive, restored)) data, err := os.ReadFile(filepath.Join(restored, "agent", "instructions.md")) require.NoError(t, err) require.Equal(t, "Keep the whole chat.\n", string(data)) info, err = os.Stat(filepath.Join(restored, "agent")) require.NoError(t, err) require.Equal(t, os.FileMode(0o750), info.Mode().Perm()) info, err = os.Stat(filepath.Join(restored, "agent", "instructions.md")) require.NoError(t, err) require.Equal(t, os.FileMode(0o640), info.Mode().Perm()) } func TestRestoreRejectsUnsafeArchiveEntries(t *testing.T) { testCases := []struct { name string header tar.Header }{ { name: "absolute paths fail", header: tar.Header{ Name: "/escape", Typeflag: tar.TypeReg, Mode: 0o600, }, }, { name: "parent paths fail", header: tar.Header{ Name: "../escape", Typeflag: tar.TypeReg, Mode: 0o600, }, }, { name: "embedded parent paths fail", header: tar.Header{ Name: "agent/../../escape", Typeflag: tar.TypeReg, Mode: 0o600, }, }, { name: "symbolic links fail", header: tar.Header{ Name: "agent", Typeflag: tar.TypeSymlink, Linkname: "../escape", Mode: 0o777, }, }, { name: "hard links fail", header: tar.Header{ Name: "agent", Typeflag: tar.TypeLink, Linkname: "/escape", Mode: 0o600, }, }, { name: "device files fail", header: tar.Header{ Name: "device", Typeflag: tar.TypeChar, Mode: 0o600, }, }, { name: "setuid modes fail", header: tar.Header{ Name: "agent", Typeflag: tar.TypeReg, Mode: 0o4600, }, }, { name: "lock replacement fails", header: tar.Header{ Name: "auth.lock", Typeflag: tar.TypeReg, Mode: 0o600, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { archive := backupTestArchive(t, tc.header) require.Error(t, backupExtract(t.Context(), archive, t.TempDir())) }) } } func TestBackupFailureKeepsTheDestination(t *testing.T) { root := t.TempDir() require.NoError(t, os.Symlink("missing", filepath.Join(root, "link"))) archive := filepath.Join(t.TempDir(), "backup.tar.gz") require.NoError(t, os.WriteFile(archive, []byte("keep"), 0o600)) _, err := backupArchive(t.Context(), root, archive) require.ErrorContains(t, err, "unsupported backup entry") data, err := os.ReadFile(archive) require.NoError(t, err) require.Equal(t, "keep", string(data)) entries, err := os.ReadDir(filepath.Dir(archive)) require.NoError(t, err) require.Len(t, entries, 1) }