mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-02 22:27:14 +00:00
The cutter materialized every interior cut up front, so a tiny sidecar declaring one enormous extent could allocate gigabytes of cut offsets before any media byte arrived. Keep only the extent start offsets, bounded by the extent count, and derive each cut arithmetically.
200 lines
5.6 KiB
Go
200 lines
5.6 KiB
Go
package format
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
)
|
|
|
|
const (
|
|
layoutVersion = 1
|
|
|
|
// MaxExtentCount bounds decoded layouts; it also bounds the chunk count a
|
|
// layout can force on an entry.
|
|
MaxExtentCount = 1 << 20
|
|
// MaxPayloadBytes bounds the adapter payload carried in entry metadata.
|
|
MaxPayloadBytes = 16 << 20
|
|
)
|
|
|
|
// TotalSize returns the file size the layout describes.
|
|
func (l *Layout) TotalSize() int64 {
|
|
var total int64
|
|
for _, size := range l.ExtentSizes {
|
|
total += size
|
|
}
|
|
return total
|
|
}
|
|
|
|
// ExtentRange returns the byte range of extent i.
|
|
func (l *Layout) ExtentRange(i int) (offset, size int64, ok bool) {
|
|
if i < 0 || i >= len(l.ExtentSizes) {
|
|
return 0, 0, false
|
|
}
|
|
for _, extentSize := range l.ExtentSizes[:i] {
|
|
offset += extentSize
|
|
}
|
|
return offset, l.ExtentSizes[i], true
|
|
}
|
|
|
|
// Validate checks layout consistency. A negative fileSize skips the total size
|
|
// check.
|
|
func (l *Layout) Validate(fileSize int64) error {
|
|
if l.Format == "" {
|
|
return fmt.Errorf("layout has no format name")
|
|
}
|
|
if l.Align < 1 {
|
|
return fmt.Errorf("layout align %d is invalid", l.Align)
|
|
}
|
|
if len(l.ExtentSizes) == 0 {
|
|
return fmt.Errorf("layout has no extents")
|
|
}
|
|
if len(l.ExtentSizes) > MaxExtentCount {
|
|
return fmt.Errorf("layout has too many extents: %d", len(l.ExtentSizes))
|
|
}
|
|
if len(l.Payload) > MaxPayloadBytes {
|
|
return fmt.Errorf("layout payload is too large: %d bytes", len(l.Payload))
|
|
}
|
|
var total int64
|
|
for i, size := range l.ExtentSizes {
|
|
if size <= 0 {
|
|
return fmt.Errorf("extent %d has invalid size %d", i, size)
|
|
}
|
|
if size > math.MaxInt64-total {
|
|
return fmt.Errorf("extent %d overflows the file size", i)
|
|
}
|
|
total += size
|
|
}
|
|
if fileSize >= 0 && total != fileSize {
|
|
return fmt.Errorf("layout describes %d bytes but the file has %d", total, fileSize)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Encode serializes the layout for the LayoutKey extended attribute.
|
|
func (l *Layout) Encode() ([]byte, error) {
|
|
if err := l.Validate(-1); err != nil {
|
|
return nil, err
|
|
}
|
|
out := []byte{layoutVersion}
|
|
out = binary.AppendUvarint(out, uint64(len(l.Format)))
|
|
out = append(out, l.Format...)
|
|
out = binary.AppendUvarint(out, uint64(l.Align))
|
|
out = binary.AppendUvarint(out, uint64(len(l.ExtentSizes)))
|
|
for _, size := range l.ExtentSizes {
|
|
out = binary.AppendUvarint(out, uint64(size))
|
|
}
|
|
out = binary.AppendUvarint(out, uint64(len(l.Payload)))
|
|
out = append(out, l.Payload...)
|
|
return out, nil
|
|
}
|
|
|
|
// DecodeLayout parses an encoded layout and validates it.
|
|
func DecodeLayout(data []byte) (*Layout, error) {
|
|
reader := bytes.NewReader(data)
|
|
version, err := reader.ReadByte()
|
|
if err != nil || version != layoutVersion {
|
|
return nil, fmt.Errorf("unsupported layout version")
|
|
}
|
|
name, err := readUvarintBytes(reader, 256)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read layout format: %w", err)
|
|
}
|
|
align, err := binary.ReadUvarint(reader)
|
|
if err != nil || align > math.MaxInt64 {
|
|
return nil, fmt.Errorf("read layout align: invalid")
|
|
}
|
|
count, err := binary.ReadUvarint(reader)
|
|
if err != nil || count > MaxExtentCount {
|
|
return nil, fmt.Errorf("read layout extent count: invalid")
|
|
}
|
|
sizes := make([]int64, count)
|
|
for i := range sizes {
|
|
size, err := binary.ReadUvarint(reader)
|
|
if err != nil || size > math.MaxInt64 {
|
|
return nil, fmt.Errorf("read extent %d size: invalid", i)
|
|
}
|
|
sizes[i] = int64(size)
|
|
}
|
|
payload, err := readUvarintBytes(reader, MaxPayloadBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read layout payload: %w", err)
|
|
}
|
|
if reader.Len() != 0 {
|
|
return nil, fmt.Errorf("layout has %d trailing bytes", reader.Len())
|
|
}
|
|
layout := &Layout{Format: string(name), ExtentSizes: sizes, Align: int64(align), Payload: payload}
|
|
if err := layout.Validate(-1); err != nil {
|
|
return nil, err
|
|
}
|
|
return layout, nil
|
|
}
|
|
|
|
func readUvarintBytes(reader *bytes.Reader, limit uint64) ([]byte, error) {
|
|
length, err := binary.ReadUvarint(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if length > limit || length > uint64(reader.Len()) {
|
|
return nil, fmt.Errorf("length %d is out of bounds", length)
|
|
}
|
|
if length == 0 {
|
|
return nil, nil
|
|
}
|
|
data := make([]byte, length)
|
|
if _, err := reader.Read(data); err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// Cutter yields upload chunk boundaries: every extent boundary, plus
|
|
// align-quantized cuts inside extents larger than maxChunkSize. A non-positive
|
|
// maxChunkSize keeps each extent in one chunk. Interior cuts are computed
|
|
// lazily, so memory stays bounded by the extent count no matter how large an
|
|
// untrusted layout declares its extents to be.
|
|
type Cutter struct {
|
|
starts []int64 // extent start offsets, ascending
|
|
total int64
|
|
quantum int64 // interior cut spacing; 0 = one chunk per extent
|
|
}
|
|
|
|
func (l *Layout) Cutter(maxChunkSize int64) *Cutter {
|
|
quantum := maxChunkSize
|
|
if quantum > 0 && l.Align > 1 {
|
|
quantum -= quantum % l.Align
|
|
if quantum <= 0 {
|
|
// An align larger than the chunk limit still cuts on whole atoms.
|
|
quantum = l.Align
|
|
}
|
|
}
|
|
starts := make([]int64, len(l.ExtentSizes))
|
|
var offset int64
|
|
for i, size := range l.ExtentSizes {
|
|
starts[i] = offset
|
|
offset += size
|
|
}
|
|
return &Cutter{starts: starts, total: offset, quantum: quantum}
|
|
}
|
|
|
|
// NextChunkSize returns the size of the chunk starting at offset, or 0 past
|
|
// the end. It satisfies the filer upload loop's ChunkBoundaries interface.
|
|
func (c *Cutter) NextChunkSize(offset int64) int64 {
|
|
if offset < 0 || offset >= c.total {
|
|
return 0
|
|
}
|
|
i := sort.Search(len(c.starts), func(i int) bool { return c.starts[i] > offset }) - 1
|
|
end := c.total
|
|
if i+1 < len(c.starts) {
|
|
end = c.starts[i+1]
|
|
}
|
|
remaining := end - offset
|
|
if c.quantum > 0 {
|
|
if step := c.quantum - (offset-c.starts[i])%c.quantum; step < remaining {
|
|
return step
|
|
}
|
|
}
|
|
return remaining
|
|
}
|