Files
pixa/internal/httpfetcher/mock.go
T
clawbot d47aea67b8
check / check (push) Waiting to run
Support SVG sources (closes #68)
The fetcher accepts image/svg+xml; internal/magic detects SVG. orig of
an SVG at the orig size is the SVG unchanged, cached, with a
Content-Security-Policy allowing only its styles and data: images; at
another size, ErrSVGNotOriginalSize (400). Any other format renders it
at the least size covering the output, at most 8192 on a side, so it
stays sharp. Rendering refuses an SVG with a side over 8192 times the
other (ErrSVGTooThin, 415), and the orig size of one above 8192 on a
side (ErrSVGTooLargeForOrigSize, 400). The cache reads up to 1024 first
bytes to find a file's format. Tests pinning the SVG refusal now expect
SVG. Render time and memory are unbounded: P1 in TODO.md until the
owner decides on #68.

Model: opus-5-5
2026-11-11 16:42:08 +00:00

97 lines
2.5 KiB
Go

package httpfetcher
import (
"context"
"errors"
"fmt"
"io/fs"
"net/http"
"strings"
"sneak.berlin/go/pixa/internal/imagerequest"
)
// MockFetcher implements Fetcher using an embedded filesystem.
// Files are organized as: hostname/path/to/file.ext
// The source with host example.com and path /images/photo.jpg maps to
// example.com/images/photo.jpg; the source query is ignored.
type MockFetcher struct {
fs fs.FS
}
// NewMock creates a new mock fetcher backed by the given filesystem.
func NewMock(fsys fs.FS) *MockFetcher {
return &MockFetcher{fs: fsys}
}
// Fetch retrieves the source's content from the mock filesystem. It ignores
// ifNoneMatch and ifModifiedSince, and always sends the content.
func (m *MockFetcher) Fetch(
ctx context.Context, source imagerequest.Source, _, _ string,
) (*FetchResult, error) {
// Check context cancellation
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
path := source.Host + source.Path
// Open the file
f, err := m.fs.Open(path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("%w: status 404", ErrUpstreamError)
}
return nil, fmt.Errorf("failed to open mock file: %w", err)
}
// Get file info for content length
stat, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, fmt.Errorf("failed to stat mock file: %w", err)
}
// Detect content type from extension
contentType := detectContentTypeFromPath(path)
return &FetchResult{
Content: f,
ContentLength: stat.Size(),
ContentType: contentType,
Headers: make(http.Header),
}, nil
}
// contentTypeOctetStream is the content type the mock fetcher gives files in
// no format pixa reads.
const contentTypeOctetStream = "application/octet-stream"
// detectContentTypeFromPath returns the MIME type based on file extension.
func detectContentTypeFromPath(path string) string {
path = strings.ToLower(path)
switch {
case strings.HasSuffix(path, ".jpg"), strings.HasSuffix(path, ".jpeg"):
return imagerequest.FormatJPEG.MIMEType()
case strings.HasSuffix(path, ".png"):
return imagerequest.FormatPNG.MIMEType()
case strings.HasSuffix(path, ".gif"):
return imagerequest.FormatGIF.MIMEType()
case strings.HasSuffix(path, ".webp"):
return imagerequest.FormatWebP.MIMEType()
case strings.HasSuffix(path, ".avif"):
return imagerequest.FormatAVIF.MIMEType()
case strings.HasSuffix(path, ".jxl"):
return imagerequest.FormatJXL.MIMEType()
case strings.HasSuffix(path, ".svg"):
return imagerequest.FormatSVG.MIMEType()
default:
return contentTypeOctetStream
}
}