From 21fa87ddf6f8c853a046d2bf55f8f0e0e1089b37 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 16 Oct 2026 00:38:00 +0000 Subject: [PATCH] Test that a side computed for a size with one side 0 is at least 1 An 8192x1 source at 100x0 and 1300x0 and an 8192x2 source at 100x0, and the same turned on their side, in every fit mode. The test fails until the fix for https://git.eeqj.de/sneak/pixa/issues/283 lands; TODO.md records the bug at the top as P1 until then. Model: opus-5-5 --- TODO.md | 6 +- .../computed_side_internal_test.go | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 4c56de7..50dd5ba 100644 --- a/TODO.md +++ b/TODO.md @@ -26,7 +26,10 @@ resolved. The disk cache is now size-bounded with LRU eviction # Next Step -P2: security: per-origin rate limiting +P1: security: a size with one side `0` whose side computed from the source's +proportions rounds down to 0 comes out square, can make libvips ask for about 39 +GB (an 8192x1 source at `1300x0`), or answers 500 +(https://git.eeqj.de/sneak/pixa/issues/283) # Completed Steps @@ -1010,6 +1013,7 @@ P2: security: per-origin rate limiting # Future Steps +- P2: security: per-origin rate limiting - P2: HTTP response handling - Last-Modified headers - P2: configuration diff --git a/internal/imageprocessor/computed_side_internal_test.go b/internal/imageprocessor/computed_side_internal_test.go index f5c3d5a..231087d 100644 --- a/internal/imageprocessor/computed_side_internal_test.go +++ b/internal/imageprocessor/computed_side_internal_test.go @@ -50,3 +50,69 @@ func TestImageProcessor_RefusesComputedSideAboveMaximum(t *testing.T) { }) } } + +// TestImageProcessor_ComputedSideIsAtLeastOne verifies that, for a size with +// one side 0, the side computed from the input's proportions is 1 where it +// would round down to 0, and that Process makes that size in every fit mode +// but FitOutside, which scales the input to cover the size and so can make +// it longer. Each case checks the size targetDimensions gives before it +// calls Process: with that side at 0, an 8192x1 input at 1300x0 makes +// libvips ask for about 39 GB with FitCover. +func TestImageProcessor_ComputedSideIsAtLeastOne(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + inputWidth, inputHeight int + size Size + wantWidth, wantHeight int + }{ + {"8192x1 at 100x0", 8192, 1, Size{Width: 100}, 100, 1}, + {"8192x1 at 1300x0", 8192, 1, Size{Width: 1300}, 1300, 1}, + {"8192x2 at 100x0", 8192, 2, Size{Width: 100}, 100, 1}, + {"1x8192 at 0x100", 1, 8192, Size{Height: 100}, 1, 100}, + {"1x8192 at 0x1300", 1, 8192, Size{Height: 1300}, 1, 1300}, + {"2x8192 at 0x100", 2, 8192, Size{Height: 100}, 1, 100}, + } + + fits := []FitMode{FitCover, FitContain, FitFill, FitInside, FitOutside} + + for _, tt := range tests { + for _, fit := range fits { + t.Run(tt.name+" "+string(fit), func(t *testing.T) { + t.Parallel() + + width, height, err := targetDimensions(tt.size, + tt.inputWidth, tt.inputHeight) + if err != nil || width != tt.wantWidth || height != tt.wantHeight { + t.Fatalf("targetDimensions() = %dx%d, %v, want %dx%d", + width, height, err, tt.wantWidth, tt.wantHeight) + } + + input := createTestJPEG(t, tt.inputWidth, tt.inputHeight) + + result, err := New(Params{}).Process(t.Context(), + bytes.NewReader(input), + &Request{Size: tt.size, Format: FormatJPEG, FitMode: fit}) + if err != nil { + t.Fatalf("Process() error = %v", err) + } + + defer func() { _ = result.Content.Close() }() + + t.Logf("Process() made %dx%d", result.Width, result.Height) + + ok := result.Width == tt.wantWidth && result.Height == tt.wantHeight + if fit == FitOutside { + ok = result.Width >= tt.wantWidth && result.Height >= tt.wantHeight + } + + if !ok { + t.Errorf("Process() made %dx%d, want %dx%d (at least, for %s)", + result.Width, result.Height, tt.wantWidth, tt.wantHeight, + FitOutside) + } + }) + } + } +}