Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Changelog for NeoFS Node
- Optimized local HEAD request execution (#3783)
- Unpaid container's data is deleted now (#3691)
- Policer iterates engine-level object list now instead of shard-level (#3862)
- SN now forwards remote SN's response to the client as is (#3877)

### Removed
- `node.persistent_sessions.path` config option from SN config (#3846)
Expand Down
6 changes: 6 additions & 0 deletions internal/protobuf/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import (
"google.golang.org/protobuf/encoding/protowire"
)

// Common response field numbers.
// TODO: declare in SDK.
const (
FieldResponseBody = 1
)

// ParseAPIVersionField parses version.Version from the next field with known
// number and type at given offset. Also returns field length.
func ParseAPIVersionField(buf []byte, fNum protowire.Number, fTyp protowire.Type) (version.Version, int, error) {
Expand Down
19 changes: 15 additions & 4 deletions internal/protobuf/codecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,26 @@ type BufferedCodec struct{}

// Marshal implements [encoding.CodecV2].
func (BufferedCodec) Marshal(msg any) (mem.BufferSlice, error) {
if bs, ok := msg.(mem.Buffer); ok {
return mem.BufferSlice{bs}, nil
switch v := msg.(type) {
case mem.BufferSlice:
return v, nil
case mem.Buffer:
return mem.BufferSlice{v}, nil
default:
return encoding.GetCodecV2(proto.Name).Marshal(msg)
}
return encoding.GetCodecV2(proto.Name).Marshal(msg)
}

// Unmarshal implements [encoding.CodecV2].
func (BufferedCodec) Unmarshal(data mem.BufferSlice, msg any) error {
return encoding.GetCodecV2(proto.Name).Unmarshal(data, msg)
switch v := msg.(type) {
case *mem.BufferSlice:
data.Ref()
*v = data
return nil
default:
return encoding.GetCodecV2(proto.Name).Unmarshal(data, msg)
}
}

// Name implements [encoding.CodecV2].
Expand Down
34 changes: 27 additions & 7 deletions internal/protobuf/seekers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import (
// Note that SeekFieldByNumber does not check value of found field, but checks
// intermediate ones for correct message traverse.
func SeekFieldByNumber(buf []byte, seekNum protowire.Number) (int, int, protowire.Type, error) {
return seekFieldByNumber(buf, seekNum, true)
}

func seekFieldByNumber(buf []byte, seekNum protowire.Number, ordered bool) (int, int, protowire.Type, error) {
if err := checkFieldNumber(seekNum); err != nil {
return 0, 0, 0, err
}
Expand All @@ -34,13 +38,15 @@ func SeekFieldByNumber(buf []byte, seekNum protowire.Number) (int, int, protowir
if num == seekNum {
return off, n, typ, nil
}
if num > seekNum {
break
}
if num < prevNum {
return 0, 0, 0, NewUnorderedFieldsError(prevNum, num)
if ordered {
if num > seekNum {
break
}
if num < prevNum {
return 0, 0, 0, NewUnorderedFieldsError(prevNum, num)
}
prevNum = num
}
prevNum = num

off += n

Expand All @@ -65,7 +71,21 @@ func SeekFieldByNumber(buf []byte, seekNum protowire.Number) (int, int, protowir
//
// If there is an error, its text contains num.
func GetLENFieldBounds(buf []byte, num protowire.Number) (FieldBounds, error) {
off, tagLn, typ, err := SeekFieldByNumber(buf, num)
return getLENFieldBounds(buf, num, true)
}

// GetLENFieldBounds seeks LEN field in buf by number and parses its boundaries.
// If field is missing, no error is returned.
//
// If field is repeated, GetLENFieldBoundsUnordered stops on first encounter.
//
// If there is an error, its text contains num.
func GetLENFieldBoundsUnordered(buf []byte, num protowire.Number) (FieldBounds, error) {
return getLENFieldBounds(buf, num, false)
}

func getLENFieldBounds(buf []byte, num protowire.Number, ordered bool) (FieldBounds, error) {
off, tagLn, typ, err := seekFieldByNumber(buf, num, ordered)
if err != nil {
return FieldBounds{}, err
}
Expand Down
204 changes: 204 additions & 0 deletions internal/protobuf/seekers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,207 @@ func TestGetLENFieldBounds(t *testing.T) {
require.EqualValues(t, f.From+4, f.ValueFrom)
require.EqualValues(t, f.From+len(fld3), f.To)
}

func TestGetLENFieldBoundsUnordered(t *testing.T) {
t.Run("invalid number", func(t *testing.T) {
t.Run("0", func(t *testing.T) {
for _, n := range []protowire.Number{-1, 0, 536870912} {
_, err := iprotobuf.GetLENFieldBoundsUnordered([]byte{}, n)
require.EqualError(t, err, "invalid number "+strconv.Itoa(int(n)))
}
})
})

t.Run("empty buffer", func(t *testing.T) {
f, err := iprotobuf.GetLENFieldBoundsUnordered([]byte{}, 42)
require.NoError(t, err)
require.True(t, f.IsMissing())
})

// #1, VARINT, 1234567890
fld1 := []byte{8, 210, 133, 216, 204, 4}
// #100, I64, 2345678901
fld2 := []byte{161, 6, 210, 56, 251, 13, 0, 0, 0, 0}
// #5K, LEN, Hello, world!
fld3 := []byte{194, 184, 2, 13, 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33}
// #1KK, I32, 3456789012
fld4 := []byte{133, 164, 232, 3, 20, 106, 10, 206}

t.Run("seek failure", func(t *testing.T) {
t.Run("invalid tag", func(t *testing.T) {
t.Run("invalid varint", func(t *testing.T) {
for _, tc := range invalidVarintTestcases {
if len(tc.buf) == 0 {
continue
}
t.Run(tc.name, func(t *testing.T) {
buf := slices.Concat(fld1, tc.buf)
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 42)
require.ErrorContains(t, err, "parse tag at offset "+strconv.Itoa(len(fld1)))
require.ErrorContains(t, err, "parse varint")
require.ErrorContains(t, err, tc.err)
})
}
})

t.Run("invalid number", func(t *testing.T) {
t.Run("0", func(t *testing.T) {
buf := slices.Concat(fld1, []byte{2})
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 42) // 0,LEN
require.EqualError(t, err, "parse tag at offset "+strconv.Itoa(len(fld1))+": invalid number 0")
})
t.Run("negative", func(t *testing.T) {
buf := slices.Concat(fld1, []byte{250, 255, 255, 255, 255, 255, 255, 255, 255, 1}) // -1,LEN
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 42)
require.EqualError(t, err, "parse tag at offset "+strconv.Itoa(len(fld1))+": invalid number -1")
})
})
})

t.Run("parse intermediate field failure", func(t *testing.T) {
t.Run("varint", func(t *testing.T) {
fld := slices.Concat([]byte{208, 2}, uint64OverflowVarint)
buf := slices.Concat(fld1, fld, fld2, fld3, fld4)
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 5000)
require.ErrorContains(t, err, "parse field #42 of VARINT type")
require.ErrorContains(t, err, "variable length integer overflow")
})
t.Run("I64", func(t *testing.T) {
buf := slices.Concat(fld1, fld2[:len(fld2)-1])
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 5000)
require.EqualError(t, err, "parse field #100 of I64 type: unexpected EOF: need 8 bytes, left 7 in buffer")
})
t.Run("LEN", func(t *testing.T) {
tag := []byte{210, 2}
t.Run("invalid len", func(t *testing.T) {
for _, tc := range invalidVarintTestcases {
t.Run(tc.name, func(t *testing.T) {
buf := slices.Concat(fld1, tag, tc.buf)
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 43)
require.ErrorContains(t, err, "parse field #42 of LEN type")
require.ErrorContains(t, err, "parse varint")
require.ErrorContains(t, err, tc.err)
})
}
})
t.Run("buffer overflow", func(t *testing.T) {
t.Run("int overflow", func(t *testing.T) {
buf := make([]byte, binary.MaxVarintLen64)
n := binary.PutUvarint(buf, uint64(math.MaxInt+1))

buf = slices.Concat(fld1, tag, buf[:n])

_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 43)
require.EqualError(t, err, "parse field #42 of LEN type: value "+strconv.FormatUint(math.MaxInt+1, 10)+" overflows int")
})

for i, tc := range varintTestcases {
if tc.val == 0 || tc.val > 1<<20 {
continue
}

buf := slices.Concat(tc.buf, make([]byte, tc.val-1))
buf = slices.Concat(fld1, tag, buf)
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 43)
require.EqualError(t, err, "parse field #42 of LEN type: unexpected EOF: need "+strconv.FormatUint(tc.val, 10)+" bytes, left "+strconv.FormatUint(tc.val-1, 10)+" in buffer", i)
}
})
})
t.Run("SGROUP", func(t *testing.T) {
buf := slices.Concat(fld1, []byte{211, 2})
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 43)
require.EqualError(t, err, "parse field #42 of SGROUP type: type is not supported")
})
t.Run("EGROUP", func(t *testing.T) {
buf := slices.Concat(fld1, []byte{212, 2})
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 43)
require.EqualError(t, err, "parse field #42 of EGROUP type: type is not supported")
})
t.Run("I32", func(t *testing.T) {
buf := slices.Concat(fld1, fld4[:len(fld4)-1])
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 1_000_001)
require.EqualError(t, err, "parse field #1000000 of I32 type: unexpected EOF: need 4 bytes, left 3 in buffer")
})
})
})

t.Run("wrong type", func(t *testing.T) {
_, err := iprotobuf.GetLENFieldBoundsUnordered([]byte{208, 2}, 42)
require.EqualError(t, err, "wrong type of field #42: expected LEN, got VARINT")
})

t.Run("parse failure", func(t *testing.T) {
tag := []byte{210, 2}
t.Run("invalid len", func(t *testing.T) {
for _, tc := range invalidVarintTestcases {
t.Run(tc.name, func(t *testing.T) {
buf := slices.Concat(fld1, tag, tc.buf)
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 42)
require.ErrorContains(t, err, "parse field #42 of LEN type")
require.ErrorContains(t, err, "parse varint")
require.ErrorContains(t, err, tc.err)
})
}
})
t.Run("buffer overflow", func(t *testing.T) {
t.Run("int overflow", func(t *testing.T) {
buf := make([]byte, binary.MaxVarintLen64)
n := binary.PutUvarint(buf, uint64(math.MaxInt+1))

buf = slices.Concat(fld1, tag, buf[:n])

_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 42)
require.EqualError(t, err, "parse field #42 of LEN type: value "+strconv.FormatUint(math.MaxInt+1, 10)+" overflows int")
})

for i, tc := range varintTestcases {
if tc.val == 0 || tc.val > 1<<20 {
continue
}

buf := slices.Concat(tc.buf, make([]byte, tc.val-1))
buf = slices.Concat(fld1, tag, buf)
_, err := iprotobuf.GetLENFieldBoundsUnordered(buf, 42)
require.EqualError(t, err, "parse field #42 of LEN type: unexpected EOF: need "+strconv.FormatUint(tc.val, 10)+" bytes, left "+strconv.FormatUint(tc.val-1, 10)+" in buffer", i)
}
})
})

message := slices.Concat(fld1, fld2, fld3, fld4)

t.Run("missing", func(t *testing.T) {
for _, n := range []protowire.Number{2, 99, 101, 4999, 50001, 999_999, 1_000_001} {
f, err := iprotobuf.GetLENFieldBoundsUnordered(message, n)
require.NoError(t, err, n)
require.True(t, f.IsMissing())
}
})

f, err := iprotobuf.GetLENFieldBoundsUnordered(fld3, 5000)
require.NoError(t, err)
require.False(t, f.IsMissing())
require.EqualValues(t, 0, f.From)
require.EqualValues(t, 4, f.ValueFrom)
require.EqualValues(t, len(fld3), f.To)

f, err = iprotobuf.GetLENFieldBoundsUnordered(slices.Concat(fld1, fld2, fld3), 5000)
require.NoError(t, err)
require.False(t, f.IsMissing())
require.EqualValues(t, len(fld1)+len(fld2), f.From)
require.EqualValues(t, f.From+4, f.ValueFrom)
require.EqualValues(t, f.From+len(fld3), f.To)

f, err = iprotobuf.GetLENFieldBoundsUnordered(message, 5_000)
require.NoError(t, err)
require.False(t, f.IsMissing())
require.EqualValues(t, len(fld1)+len(fld2), f.From)
require.EqualValues(t, f.From+4, f.ValueFrom)
require.EqualValues(t, f.From+len(fld3), f.To)

f, err = iprotobuf.GetLENFieldBoundsUnordered(slices.Concat(fld2, fld1, fld4, fld3), 5000)
require.NoError(t, err)
require.False(t, f.IsMissing())
require.EqualValues(t, len(fld1)+len(fld2)+len(fld4), f.From)
require.EqualValues(t, f.From+4, f.ValueFrom)
require.EqualValues(t, f.From+len(fld3), f.To)
}
10 changes: 0 additions & 10 deletions pkg/services/object/acl/eacl/v2/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
protoobject "github.com/nspcc-dev/neofs-sdk-go/proto/object"
"github.com/nspcc-dev/neofs-sdk-go/version"
"google.golang.org/protobuf/encoding/protowire"
)

type sysObjHdr struct {
Expand Down Expand Up @@ -108,21 +107,12 @@ func headersFromBinaryObjectHeader(buf []byte, cnr cid.ID, id *oid.ID) ([]eaclSD
res := make([]eaclSDK.Header, 0, 10)

var off int
var prevNum protowire.Number
for {
num, typ, n, err := iprotobuf.ParseTag(buf[off:])
if err != nil {
return nil, err
}

if num < prevNum {
return nil, iprotobuf.NewUnorderedFieldsError(prevNum, num)
}
if num == prevNum && num != protoobject.FieldHeaderAttributes {
return nil, iprotobuf.NewRepeatedFieldError(num)
}
prevNum = num

off += n

switch num {
Expand Down
8 changes: 7 additions & 1 deletion pkg/services/object/get/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ type execCtx struct {
// If an error occurs after that, the stream is already corrupted and
// no retry should be attempted.
headerWritten bool

forwardHeadFn HeadRequestForwarder

submitBufferedResponseFn SubmitBufferedResponseFunc
}

type execOption func(*execCtx)
Expand All @@ -73,9 +77,11 @@ const (
statusNotFound
)

func headOnly() execOption {
func headOnly(forwardRequestFn HeadRequestForwarder, submitBufferedResponseFn SubmitBufferedResponseFunc) execOption {
return func(c *execCtx) {
c.head = true
c.forwardHeadFn = forwardRequestFn
c.submitBufferedResponseFn = submitBufferedResponseFn
}
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/services/object/get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func (s *Service) Head(ctx context.Context, prm HeadPrm) error {
}

if len(repRules) > 0 {
err := s.get(ctx, prm.commonPrm, headOnly(), withPreSortedContainerNodes(nodeLists[:len(repRules)], repRules)).err
err := s.get(ctx, prm.commonPrm, headOnly(prm.forwardRequestFn, prm.submitBufferedResponseFn), withPreSortedContainerNodes(nodeLists[:len(repRules)], repRules)).err
if len(ecRules) == 0 || !errors.Is(err, apistatus.ErrObjectNotFound) {
return err
}
Expand All @@ -275,7 +275,7 @@ func (s *Service) Head(ctx context.Context, prm HeadPrm) error {
for i := range ecRules {
repRules[i] = uint(ecRules[i].DataPartNum + ecRules[i].ParityPartNum)
}
return s.get(ctx, prm.commonPrm, headOnly(), withPreSortedContainerNodes(ecNodeLists, repRules)).err
return s.get(ctx, prm.commonPrm, headOnly(prm.forwardRequestFn, prm.submitBufferedResponseFn), withPreSortedContainerNodes(ecNodeLists, repRules)).err
}

return s.copyECObjectHeader(ctx, prm.objWriter, prm.addr.Container(), prm.addr.Object(), prm.common.SessionToken(),
Expand Down
Loading
Loading