Implement unsafe icon cache diagnostics (T-611)
This commit is contained in:
@@ -6,12 +6,14 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -209,6 +211,83 @@ func TestIconCacheRepairsCorruptDiskAndReportsOfflineFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIconCacheRejectsUnsafeDiskEntriesWithoutFallback(t *testing.T) {
|
||||
document := testPNG(t, 16, 16)
|
||||
request := iconRequest(document, 120)
|
||||
|
||||
t.Run("directory", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
entryPath := testIconCacheEntryPath(root, request)
|
||||
if err := os.Mkdir(entryPath, 0o700); err != nil {
|
||||
t.Fatalf("Mkdir(cache entry) error = %v", err)
|
||||
}
|
||||
markerPath := filepath.Join(entryPath, "keep.txt")
|
||||
if err := os.WriteFile(markerPath, []byte("keep"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(marker) error = %v", err)
|
||||
}
|
||||
fetchCalls := 0
|
||||
cache := NewIconCache(root, IconFetchFunc(func(
|
||||
context.Context,
|
||||
IconRequest,
|
||||
) (IconFetchResponse, error) {
|
||||
fetchCalls++
|
||||
return iconResponse(document), nil
|
||||
}))
|
||||
|
||||
_, err := cache.Load(context.Background(), request)
|
||||
if !errors.Is(err, ErrIconCacheUnsafe) {
|
||||
t.Fatalf("Load(directory) error = %v, want %v", err, ErrIconCacheUnsafe)
|
||||
}
|
||||
if fetchCalls != 0 {
|
||||
t.Fatalf("unsafe directory triggered %d fetches", fetchCalls)
|
||||
}
|
||||
marker, readErr := os.ReadFile(markerPath)
|
||||
if readErr != nil || string(marker) != "keep" {
|
||||
t.Fatalf("unsafe directory marker = %q, %v", marker, readErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("symlink", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
entryPath := testIconCacheEntryPath(root, request)
|
||||
targetPath := filepath.Join(t.TempDir(), "external-target.icon")
|
||||
target := []byte("external target must remain untouched")
|
||||
if err := os.WriteFile(targetPath, target, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(target) error = %v", err)
|
||||
}
|
||||
if err := os.Symlink(targetPath, entryPath); err != nil {
|
||||
t.Skipf("symlink creation is unavailable: %v", err)
|
||||
}
|
||||
fetchCalls := 0
|
||||
cache := NewIconCache(root, IconFetchFunc(func(
|
||||
context.Context,
|
||||
IconRequest,
|
||||
) (IconFetchResponse, error) {
|
||||
fetchCalls++
|
||||
return iconResponse(document), nil
|
||||
}))
|
||||
|
||||
_, err := cache.Load(context.Background(), request)
|
||||
if !errors.Is(err, ErrIconCacheUnsafe) {
|
||||
t.Fatalf("Load(symlink) error = %v, want %v", err, ErrIconCacheUnsafe)
|
||||
}
|
||||
if fetchCalls != 0 {
|
||||
t.Fatalf("unsafe symlink triggered %d fetches", fetchCalls)
|
||||
}
|
||||
gotTarget, readErr := os.ReadFile(targetPath)
|
||||
if readErr != nil || !bytes.Equal(gotTarget, target) {
|
||||
t.Fatalf("external target changed: %q, %v", gotTarget, readErr)
|
||||
}
|
||||
info, statErr := os.Lstat(entryPath)
|
||||
if statErr != nil {
|
||||
t.Fatalf("Lstat(unsafe symlink) error = %v", statErr)
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
t.Fatalf("unsafe symlink was replaced: mode=%v", info.Mode())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDecodeIcon(t *testing.T) {
|
||||
document := testPNG(t, 8, 8)
|
||||
decoded, err := DecodeIcon(document)
|
||||
@@ -235,6 +314,11 @@ func iconResponse(document []byte) IconFetchResponse {
|
||||
}
|
||||
}
|
||||
|
||||
func testIconCacheEntryPath(root string, request IconRequest) string {
|
||||
digest := strings.TrimPrefix(request.Reference, "sha256:")
|
||||
return filepath.Join(root, fmt.Sprintf("%s-%d.icon", digest, request.DPI))
|
||||
}
|
||||
|
||||
func testPNG(t *testing.T, width, height int) []byte {
|
||||
t.Helper()
|
||||
source := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
@@ -2,7 +2,9 @@ package catalog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -109,6 +111,63 @@ func TestIconEventDeliveryClassifiesFailuresWithoutRawErrorPayload(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIconEventDeliveryPublishesUnsafeFailureFromRealCache(t *testing.T) {
|
||||
document := testPNG(t, 8, 8)
|
||||
request := iconRequest(document, 96)
|
||||
identity := iconEventIdentity(t, document, "request-unsafe-cache")
|
||||
root := t.TempDir()
|
||||
entryPath := testIconCacheEntryPath(root, request)
|
||||
if err := os.Mkdir(entryPath, 0o700); err != nil {
|
||||
t.Fatalf("Mkdir(cache entry) error = %v", err)
|
||||
}
|
||||
fetchCalls := 0
|
||||
cache := NewIconCache(root, IconFetchFunc(func(
|
||||
context.Context,
|
||||
IconRequest,
|
||||
) (IconFetchResponse, error) {
|
||||
fetchCalls++
|
||||
return iconResponse(document), nil
|
||||
}))
|
||||
var events []application.Event
|
||||
delivery := IconEventDelivery{
|
||||
Loader: cache,
|
||||
Publisher: IconEventPublisherFunc(func(
|
||||
_ context.Context,
|
||||
event application.Event,
|
||||
) error {
|
||||
events = append(events, event)
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
|
||||
err := delivery.LoadAndPublish(context.Background(), identity)
|
||||
if !errors.Is(err, ErrIconCacheUnsafe) {
|
||||
t.Fatalf("LoadAndPublish() error = %v, want %v", err, ErrIconCacheUnsafe)
|
||||
}
|
||||
if fetchCalls != 0 {
|
||||
t.Fatalf("unsafe cache delivery triggered %d fetches", fetchCalls)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("published events = %d, want 1", len(events))
|
||||
}
|
||||
parsed, handled, parseErr := application.ParseIconEvent(events[0])
|
||||
if parseErr != nil || !handled {
|
||||
t.Fatalf("ParseIconEvent() = (%+v, %t, %v)", parsed, handled, parseErr)
|
||||
}
|
||||
if parsed.Type != application.EventIconFailed || parsed.Identity != identity ||
|
||||
parsed.ErrorCode != application.IconFailureUnsafe {
|
||||
t.Fatalf("unsafe failure event = %+v", parsed)
|
||||
}
|
||||
encoded, marshalErr := json.Marshal(events[0])
|
||||
if marshalErr != nil {
|
||||
t.Fatalf("json.Marshal(event) error = %v", marshalErr)
|
||||
}
|
||||
if strings.Contains(string(encoded), root) ||
|
||||
strings.Contains(string(encoded), "cache entry is not a regular file") {
|
||||
t.Fatalf("unsafe failure event leaked cache details: %s", encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIconEventDeliveryReportsPublishFailureAndCacheWarning(t *testing.T) {
|
||||
document := testPNG(t, 12, 12)
|
||||
identity := iconEventIdentity(t, document, "request-publish")
|
||||
|
||||
Reference in New Issue
Block a user