diff --git a/TODO.md b/TODO.md index 3817c02..ce15d30 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 @@ -1015,6 +1018,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) + } + }) + } + } +}