mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 17:30:42 +01:00
* fix(volume): derive needle body tail bound from the version layout The size guard in ReadNeedleBodyBytes computed the tail length as checksum, plus timestamp only for Version3. Forks and future on-disk formats whose tail carries more fields would silently under-check and still panic in readNeedleTail on a truncated body. Derive the tail from NeedleBodyLength minus data and padding so the bound stays exact for every version. Iterate IsSupportedVersion in the new tests instead of hardcoding v1-v3 so downstream formats get covered automatically, and skip versions the build cannot write rather than failing on them. * test: skip needle write only on the unsupported-version error A blanket skip would hide a real writer regression. Skip the version subtest only when the writer reports the version is not supported in this build (the error text differs between builds), and fail on any other write error.
301 lines
9.4 KiB
Go
301 lines
9.4 KiB
Go
package needle
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
|
|
. "github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
const (
|
|
FlagIsCompressed = 0x01
|
|
FlagHasName = 0x02
|
|
FlagHasMime = 0x04
|
|
FlagHasLastModifiedDate = 0x08
|
|
FlagHasTtl = 0x10
|
|
FlagHasPairs = 0x20
|
|
FlagIsChunkManifest = 0x80
|
|
LastModifiedBytesLength = 5
|
|
TtlBytesLength = 2
|
|
)
|
|
|
|
var ErrorSizeMismatch = errors.New("size mismatch")
|
|
var ErrorSizeInvalid = errors.New("size invalid")
|
|
|
|
// ErrorCorrupted marks a needle whose on-disk bytes cannot be parsed because
|
|
// of data corruption: bad CRC, malformed v2/v3/v4 headers, or out-of-range
|
|
// fields. Wrap errors with %w so callers (e.g. vacuum compaction) can
|
|
// distinguish "the bytes are bad" from a genuine I/O fault.
|
|
var ErrorCorrupted = errors.New("needle data corrupted")
|
|
|
|
func (n *Needle) DiskSize(version Version) int64 {
|
|
return GetActualSize(n.Size, version)
|
|
}
|
|
|
|
func ReadNeedleBlob(r backend.BackendStorageFile, offset int64, size Size, version Version) (dataSlice []byte, err error) {
|
|
|
|
dataSize := GetActualSize(size, version)
|
|
dataSlice = make([]byte, int(dataSize))
|
|
|
|
var n int
|
|
n, err = r.ReadAt(dataSlice, offset)
|
|
if err != nil && int64(n) == dataSize {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
fileSize, _, _ := r.GetStat()
|
|
glog.Errorf("%s read %d dataSize %d offset %d fileSize %d: %v", r.Name(), n, dataSize, offset, fileSize, err)
|
|
}
|
|
return dataSlice, err
|
|
|
|
}
|
|
|
|
// ReadBytes hydrates the needle from the bytes buffer, with only n.Id is set.
|
|
func (n *Needle) ReadBytes(bytes []byte, offset int64, size Size, version Version) (err error) {
|
|
n.ParseNeedleHeader(bytes)
|
|
if n.Size != size {
|
|
if OffsetSize == 4 && offset < int64(MaxPossibleVolumeSize) {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorSizeMismatchOffsetSize).Inc()
|
|
return ErrorSizeMismatch
|
|
}
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorSizeMismatch).Inc()
|
|
return fmt.Errorf("%w: entry not found: offset %d found id %x size %d, expected size %d", ErrorSizeMismatch, offset, n.Id, n.Size, size)
|
|
}
|
|
if version == Version1 {
|
|
n.Data = bytes[NeedleHeaderSize : NeedleHeaderSize+size]
|
|
} else {
|
|
err := n.readNeedleDataVersion2(bytes[NeedleHeaderSize : NeedleHeaderSize+int(size)])
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
}
|
|
err = n.readNeedleTail(bytes[NeedleHeaderSize+size:], version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReadData hydrates the needle from the file, with only n.Id is set.
|
|
func (n *Needle) ReadData(r backend.BackendStorageFile, offset int64, size Size, version Version) (err error) {
|
|
bytes, err := ReadNeedleBlob(r, offset, size, version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = n.ReadBytes(bytes, offset, size, version)
|
|
if err == ErrorSizeMismatch && OffsetSize == 4 {
|
|
offset = offset + int64(MaxPossibleVolumeSize)
|
|
bytes, err = ReadNeedleBlob(r, offset, size, version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = n.ReadBytes(bytes, offset, size, version)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (n *Needle) ParseNeedleHeader(bytes []byte) {
|
|
n.Cookie = BytesToCookie(bytes[0:CookieSize])
|
|
n.Id = BytesToNeedleId(bytes[CookieSize : CookieSize+NeedleIdSize])
|
|
n.Size = BytesToSize(bytes[CookieSize+NeedleIdSize : NeedleHeaderSize])
|
|
}
|
|
|
|
func (n *Needle) readNeedleDataVersion2(bytes []byte) (err error) {
|
|
index, lenBytes := 0, len(bytes)
|
|
if index < lenBytes {
|
|
n.DataSize = util.BytesToUint32(bytes[index : index+4])
|
|
index = index + 4
|
|
if int(n.DataSize)+index > lenBytes {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorIndexOutOfRange).Inc()
|
|
return fmt.Errorf("index out of range %d: %w", 1, ErrorCorrupted)
|
|
}
|
|
n.Data = bytes[index : index+int(n.DataSize)]
|
|
index = index + int(n.DataSize)
|
|
}
|
|
_, err = n.readNeedleDataVersion2NonData(bytes[index:])
|
|
return
|
|
}
|
|
func (n *Needle) readNeedleDataVersion2NonData(bytes []byte) (index int, err error) {
|
|
lenBytes := len(bytes)
|
|
if index < lenBytes {
|
|
n.Flags = bytes[index]
|
|
index = index + 1
|
|
}
|
|
if index < lenBytes && n.HasName() {
|
|
n.NameSize = uint8(bytes[index])
|
|
index = index + 1
|
|
if int(n.NameSize)+index > lenBytes {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorIndexOutOfRange).Inc()
|
|
return index, fmt.Errorf("index out of range %d: %w", 2, ErrorCorrupted)
|
|
}
|
|
n.Name = bytes[index : index+int(n.NameSize)]
|
|
index = index + int(n.NameSize)
|
|
}
|
|
if index < lenBytes && n.HasMime() {
|
|
n.MimeSize = uint8(bytes[index])
|
|
index = index + 1
|
|
if int(n.MimeSize)+index > lenBytes {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorIndexOutOfRange).Inc()
|
|
return index, fmt.Errorf("index out of range %d: %w", 3, ErrorCorrupted)
|
|
}
|
|
n.Mime = bytes[index : index+int(n.MimeSize)]
|
|
index = index + int(n.MimeSize)
|
|
}
|
|
if index < lenBytes && n.HasLastModifiedDate() {
|
|
if LastModifiedBytesLength+index > lenBytes {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorIndexOutOfRange).Inc()
|
|
return index, fmt.Errorf("index out of range %d: %w", 4, ErrorCorrupted)
|
|
}
|
|
n.LastModified = util.BytesToUint64(bytes[index : index+LastModifiedBytesLength])
|
|
index = index + LastModifiedBytesLength
|
|
}
|
|
if index < lenBytes && n.HasTtl() {
|
|
if TtlBytesLength+index > lenBytes {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorIndexOutOfRange).Inc()
|
|
return index, fmt.Errorf("index out of range %d: %w", 5, ErrorCorrupted)
|
|
}
|
|
n.Ttl = LoadTTLFromBytes(bytes[index : index+TtlBytesLength])
|
|
index = index + TtlBytesLength
|
|
}
|
|
if index < lenBytes && n.HasPairs() {
|
|
if 2+index > lenBytes {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorIndexOutOfRange).Inc()
|
|
return index, fmt.Errorf("index out of range %d: %w", 6, ErrorCorrupted)
|
|
}
|
|
n.PairsSize = util.BytesToUint16(bytes[index : index+2])
|
|
index += 2
|
|
if int(n.PairsSize)+index > lenBytes {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorIndexOutOfRange).Inc()
|
|
return index, fmt.Errorf("index out of range %d: %w", 7, ErrorCorrupted)
|
|
}
|
|
end := index + int(n.PairsSize)
|
|
n.Pairs = bytes[index:end]
|
|
index = end
|
|
}
|
|
return index, nil
|
|
}
|
|
|
|
func ReadNeedleHeader(r backend.BackendStorageFile, version Version, offset int64) (n *Needle, bytes []byte, bodyLength int64, err error) {
|
|
n = new(Needle)
|
|
|
|
bytes = make([]byte, NeedleHeaderSize)
|
|
|
|
var count int
|
|
count, err = r.ReadAt(bytes, offset)
|
|
if err == io.EOF && count == NeedleHeaderSize {
|
|
err = nil
|
|
}
|
|
if count <= 0 || err != nil {
|
|
return nil, bytes, 0, err
|
|
}
|
|
|
|
n.ParseNeedleHeader(bytes)
|
|
bodyLength = NeedleBodyLength(n.Size, version)
|
|
|
|
return
|
|
}
|
|
|
|
// n should be a needle already read the header
|
|
// the input stream will read until next file entry
|
|
func (n *Needle) ReadNeedleBody(r backend.BackendStorageFile, version Version, offset int64, bodyLength int64) (bytes []byte, err error) {
|
|
|
|
if bodyLength <= 0 {
|
|
return nil, nil
|
|
}
|
|
bytes = make([]byte, bodyLength)
|
|
readCount, err := r.ReadAt(bytes, offset)
|
|
if err == io.EOF && int64(readCount) == bodyLength {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
glog.Errorf("%s read %d bodyLength %d offset %d: %v", r.Name(), readCount, bodyLength, offset, err)
|
|
return
|
|
}
|
|
|
|
err = n.ReadNeedleBodyBytes(bytes, version)
|
|
|
|
return
|
|
}
|
|
|
|
func (n *Needle) ReadNeedleBodyBytes(needleBody []byte, version Version) (err error) {
|
|
|
|
// n.Size comes from the on-disk header, so a corrupted header can carry a
|
|
// negative size or one the body cannot hold along with its tail. Deriving
|
|
// the tail from the version's own layout keeps the bound exact for every
|
|
// on-disk format.
|
|
tailSize := NeedleBodyLength(n.Size, version) - int64(n.Size) - int64(PaddingLength(n.Size, version))
|
|
if n.Size < 0 || int64(n.Size)+tailSize > int64(len(needleBody)) {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorIndexOutOfRange).Inc()
|
|
return fmt.Errorf("needle %v size %d out of range for body length %d: %w", n.Id, n.Size, len(needleBody), ErrorCorrupted)
|
|
}
|
|
switch version {
|
|
case Version1:
|
|
n.Data = needleBody[:n.Size]
|
|
err = n.readNeedleTail(needleBody[n.Size:], version)
|
|
case Version2, Version3:
|
|
err = n.readNeedleDataVersion2(needleBody[0:n.Size])
|
|
if err == nil {
|
|
err = n.readNeedleTail(needleBody[n.Size:], version)
|
|
}
|
|
default:
|
|
err = fmt.Errorf("unsupported version %d!", version)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (n *Needle) IsCompressed() bool {
|
|
return n.Flags&FlagIsCompressed > 0
|
|
}
|
|
func (n *Needle) SetIsCompressed() {
|
|
n.Flags = n.Flags | FlagIsCompressed
|
|
}
|
|
func (n *Needle) HasName() bool {
|
|
return n.Flags&FlagHasName > 0
|
|
}
|
|
func (n *Needle) SetHasName() {
|
|
n.Flags = n.Flags | FlagHasName
|
|
}
|
|
func (n *Needle) HasMime() bool {
|
|
return n.Flags&FlagHasMime > 0
|
|
}
|
|
func (n *Needle) SetHasMime() {
|
|
n.Flags = n.Flags | FlagHasMime
|
|
}
|
|
func (n *Needle) HasLastModifiedDate() bool {
|
|
return n.Flags&FlagHasLastModifiedDate > 0
|
|
}
|
|
func (n *Needle) SetHasLastModifiedDate() {
|
|
n.Flags = n.Flags | FlagHasLastModifiedDate
|
|
}
|
|
func (n *Needle) HasTtl() bool {
|
|
return n.Flags&FlagHasTtl > 0
|
|
}
|
|
func (n *Needle) SetHasTtl() {
|
|
n.Flags = n.Flags | FlagHasTtl
|
|
}
|
|
|
|
func (n *Needle) IsChunkedManifest() bool {
|
|
return n.Flags&FlagIsChunkManifest > 0
|
|
}
|
|
|
|
func (n *Needle) SetIsChunkManifest() {
|
|
n.Flags = n.Flags | FlagIsChunkManifest
|
|
}
|
|
|
|
func (n *Needle) HasPairs() bool {
|
|
return n.Flags&FlagHasPairs != 0
|
|
}
|
|
|
|
func (n *Needle) SetHasPairs() {
|
|
n.Flags = n.Flags | FlagHasPairs
|
|
}
|
|
|
|
func GetActualSize(size Size, version Version) int64 {
|
|
return NeedleHeaderSize + NeedleBodyLength(size, version)
|
|
}
|