From d342c0bc16750d61563894c98898c9a93aca748c Mon Sep 17 00:00:00 2001 From: Airat Arifullin Date: Thu, 12 Sep 2024 15:12:52 +0300 Subject: [PATCH] [#268] client: Make `PayloadPatch` correctly receive empty patch payload * Make the method `PatchPayload` send a patch with empty payload patch if range's length is non-zero and if it's the first call. * Empty payload patches just cut original object payload. So, these patches are also valid. Signed-off-by: Airat Arifullin --- client/object_patch.go | 15 +++++++ client/object_patch_test.go | 87 +++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/client/object_patch.go b/client/object_patch.go index 7af0835..ca7e8ed 100644 --- a/client/object_patch.go +++ b/client/object_patch.go @@ -175,6 +175,21 @@ func (x *objectPatcher) PatchPayload(_ context.Context, rng *object.Range, paylo return false } if n == 0 { + if patchIter == 0 { + if rng.GetLength() == 0 { + x.err = errors.New("zero-length empty payload patch can't be applied") + return false + } + if !x.patch(&object.Patch{ + Address: x.addr, + PayloadPatch: &object.PayloadPatch{ + Range: rng, + Chunk: []byte{}, + }, + }) { + return false + } + } break } diff --git a/client/object_patch_test.go b/client/object_patch_test.go index 2349602..839c453 100644 --- a/client/object_patch_test.go +++ b/client/object_patch_test.go @@ -193,6 +193,93 @@ func TestObjectPatcher(t *testing.T) { } } +func TestRepeatPayloadPatch(t *testing.T) { + t.Run("no payload patch partioning", func(t *testing.T) { + m := &mockPatchStream{} + + pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + + const maxChunkLen = 20 + + patcher := objectPatcher{ + client: &Client{}, + stream: m, + addr: oidtest.Address(), + key: pk, + maxChunkLen: maxChunkLen, + } + + for _, pp := range []struct { + patchPayload string + rng *object.Range + }{ + { + patchPayload: "xxxxxxxxxx", + rng: newRange(1, 6), + }, + { + patchPayload: "yyyyyyyyyy", + rng: newRange(5, 9), + }, + { + patchPayload: "zzzzzzzzzz", + rng: newRange(10, 0), + }, + } { + success := patcher.PatchPayload(context.Background(), pp.rng, bytes.NewReader([]byte(pp.patchPayload))) + require.True(t, success) + } + + requireRangeChunk(t, m.streamedPayloadPatches[0], 1, 6, "xxxxxxxxxx") + requireRangeChunk(t, m.streamedPayloadPatches[1], 5, 9, "yyyyyyyyyy") + requireRangeChunk(t, m.streamedPayloadPatches[2], 10, 0, "zzzzzzzzzz") + }) + + t.Run("payload patch partioning", func(t *testing.T) { + m := &mockPatchStream{} + + pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + + const maxChunkLen = 5 + + patcher := objectPatcher{ + client: &Client{}, + stream: m, + addr: oidtest.Address(), + key: pk, + maxChunkLen: maxChunkLen, + } + + for _, pp := range []struct { + patchPayload string + rng *object.Range + }{ + { + patchPayload: "xxxxxxxxxx", + rng: newRange(1, 6), + }, + { + patchPayload: "yyyyyyyyyy", + rng: newRange(5, 9), + }, + { + patchPayload: "zzzzzzzzzz", + rng: newRange(10, 0), + }, + } { + success := patcher.PatchPayload(context.Background(), pp.rng, bytes.NewReader([]byte(pp.patchPayload))) + require.True(t, success) + } + + requireRangeChunk(t, m.streamedPayloadPatches[0], 1, 6, "xxxxx") + requireRangeChunk(t, m.streamedPayloadPatches[1], 7, 0, "xxxxx") + requireRangeChunk(t, m.streamedPayloadPatches[2], 5, 9, "yyyyy") + requireRangeChunk(t, m.streamedPayloadPatches[3], 14, 0, "yyyyy") + requireRangeChunk(t, m.streamedPayloadPatches[4], 10, 0, "zzzzz") + requireRangeChunk(t, m.streamedPayloadPatches[5], 10, 0, "zzzzz") + }) +} + func requireRangeChunk(t *testing.T, pp *object.PayloadPatch, offset, length int, chunk string) { require.NotNil(t, pp) require.Equal(t, uint64(offset), pp.Range.GetOffset())