package storage import ( "context" "errors" "fmt" "io" "math" "strings" ) // ReadLimitError reports that a remote object exceeded its caller-owned read // limit. The limit is enforced against both available object metadata and the // bytes returned by the opened object body. type ReadLimitError struct { Key string Limit int64 Observed int64 } func (e *ReadLimitError) Error() string { return fmt.Sprintf("object %q exceeds %d-byte read limit (observed at least %d bytes)", e.Key, e.Limit, e.Observed) } // ReadObjectBounded opens one object version and retains at most maxBytes of // its content. Object metadata may reject an oversized body early, but a // limit-plus-one read always enforces the boundary when transfer begins. func ReadObjectBounded(ctx context.Context, store ObjectStore, key string, maxBytes int64) (info ObjectInfo, data []byte, err error) { key = strings.TrimSpace(key) if store == nil { return ObjectInfo{}, nil, fmt.Errorf("read bounded object: store is required") } if key == "" { return ObjectInfo{}, nil, fmt.Errorf("read bounded object: key is required") } if maxBytes <= 0 || maxBytes == math.MaxInt64 { return ObjectInfo{}, nil, fmt.Errorf("read bounded object %q: limit must be between 1 and %d bytes", key, int64(math.MaxInt64-1)) } if err := ctx.Err(); err != nil { return ObjectInfo{}, nil, err } info, body, err := store.Read(ctx, key) if err != nil { return ObjectInfo{}, nil, err } if body == nil { return ObjectInfo{}, nil, fmt.Errorf("read bounded object %q: store returned no body", key) } defer func() { if closeErr := body.Close(); closeErr != nil { data = nil err = errors.Join(err, fmt.Errorf("close object %q: %w", key, closeErr)) } }() if info.Size > maxBytes { return info, nil, &ReadLimitError{Key: key, Limit: maxBytes, Observed: info.Size} } data, err = io.ReadAll(io.LimitReader(contextReader{ctx: ctx, reader: body}, maxBytes+1)) if err != nil { return info, nil, err } if err := ctx.Err(); err != nil { return info, nil, err } if int64(len(data)) > maxBytes { return info, nil, &ReadLimitError{Key: key, Limit: maxBytes, Observed: int64(len(data))} } return info, data, nil } type contextReader struct { ctx context.Context reader io.Reader } func (r contextReader) Read(p []byte) (int, error) { if err := r.ctx.Err(); err != nil { return 0, err } n, err := r.reader.Read(p) if err == nil { if contextErr := r.ctx.Err(); contextErr != nil { return n, contextErr } } return n, err }