package patcher import ( "bytes" "context" "io" "sort" "strings" "testing" objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer" "github.com/stretchr/testify/require" ) type mockPatchedObjectWriter struct { obj *objectSDK.Object } func (m *mockPatchedObjectWriter) Write(_ context.Context, chunk []byte) (int, error) { res := append(m.obj.Payload(), chunk...) m.obj.SetPayload(res) m.obj.SetPayloadSize(uint64(len(res))) return len(chunk), nil } func (m *mockPatchedObjectWriter) WriteHeader(_ context.Context, hdr *objectSDK.Object) error { m.obj.ToV2().SetHeader(hdr.ToV2().GetHeader()) return nil } func (m *mockPatchedObjectWriter) Close(context.Context) (*transformer.AccessIdentifiers, error) { return &transformer.AccessIdentifiers{}, nil } type mockHeaderProvider struct { obj *objectSDK.Object } var _ HeaderProvider = (*mockHeaderProvider)(nil) func (m *mockHeaderProvider) GetObjectHeader(ctx context.Context, addr oid.Address) (*objectSDK.Object, error) { return m.obj.CutPayload(), nil } type mockRangeProvider struct { originalObjectPayload []byte } var _ RangeProvider = (*mockRangeProvider)(nil) func (m *mockRangeProvider) ReadRange(_ context.Context, addr oid.Address, rng *objectSDK.Range) io.Reader { offset := rng.GetOffset() length := rng.GetLength() if length == 0 { return bytes.NewReader(m.originalObjectPayload[offset:]) } return bytes.NewReader(m.originalObjectPayload[offset : offset+length]) } func newTestObject() (*objectSDK.Object, oid.Address) { obj := objectSDK.New() addr := oidtest.Address() obj.SetContainerID(addr.Container()) obj.SetID(addr.Object()) return obj, addr } func TestPatchRevert(t *testing.T) { obj, addr := newTestObject() modifPatch := &objectSDK.Patch{ Address: addr, PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("inserted"), }, } originalObjectPayload := []byte("*******************") obj.SetPayload(originalObjectPayload) obj.SetPayloadSize(uint64(len(originalObjectPayload))) exp := []byte("inserted*******************") rangeProvider := &mockRangeProvider{ originalObjectPayload: originalObjectPayload, } hdrProvider := &mockHeaderProvider{ obj: obj, } patchedObj, patchedAddr := newTestObject() wr := &mockPatchedObjectWriter{ obj: patchedObj, } patcher := New(hdrProvider, rangeProvider, wr) applyRes := patcher.ApplyPatch(context.Background(), modifPatch) require.True(t, applyRes) _, err := patcher.Close(context.Background()) require.NoError(t, err) require.Equal(t, exp, patchedObj.Payload()) revertPatch := &objectSDK.Patch{ Address: patchedAddr, PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, uint64(len("inserted"))), Chunk: []byte{}, }, } rangeProvider = &mockRangeProvider{ originalObjectPayload: exp, } patchedPatchedObj, _ := newTestObject() wr = &mockPatchedObjectWriter{ obj: patchedPatchedObj, } hdrProvider = &mockHeaderProvider{ obj: patchedObj, } patcher = New(hdrProvider, rangeProvider, wr) applyRes = patcher.ApplyPatch(context.Background(), revertPatch) require.True(t, applyRes) _, err = patcher.Close(context.Background()) require.NoError(t, err) require.Equal(t, originalObjectPayload, patchedPatchedObj.Payload()) } func TestDifferentPatchTargetAddresses(t *testing.T) { obj, addr := newTestObject() _, anyOtherAddr := newTestObject() modifPatch := &objectSDK.Patch{ Address: addr, PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("inserted"), }, } invalidPatch := &objectSDK.Patch{ Address: anyOtherAddr, PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("inserted"), }, } originalObjectPayload := []byte("*******************") obj.SetPayload(originalObjectPayload) obj.SetPayloadSize(uint64(len(originalObjectPayload))) rangeProvider := &mockRangeProvider{ originalObjectPayload: originalObjectPayload, } hdrProvider := &mockHeaderProvider{ obj: obj, } patchedObj, _ := newTestObject() wr := &mockPatchedObjectWriter{ obj: patchedObj, } patcher := New(hdrProvider, rangeProvider, wr) applyRes := patcher.ApplyPatch(context.Background(), modifPatch) require.True(t, applyRes) applyRes = patcher.ApplyPatch(context.Background(), invalidPatch) require.False(t, applyRes) _, err := patcher.Close(context.Background()) require.ErrorIs(t, err, ErrInvalidPatchObjectAddress) } func rangeWithOffestWithLength(offset, length uint64) *objectSDK.Range { rng := new(objectSDK.Range) rng.SetOffset(offset) rng.SetLength(length) return rng } type attr struct { key string val string } type attrs []attr func (a attrs) ToSDKAttributes() []objectSDK.Attribute { res := make([]objectSDK.Attribute, len(a)) for i := range a { res[i].SetKey(a[i].key) res[i].SetValue(a[i].val) } return res } func TestPatch(t *testing.T) { for _, test := range []struct { name string newAttrs attrs replaceAttrs bool patchPayloads []*objectSDK.PayloadPatch originalObjectPayload []byte patched []byte expectedErr error }{ { name: "invalid offset", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(100, 0), Chunk: []byte(""), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), expectedErr: ErrOffsetExceedsSize, }, { name: "empty payload patch in the second patch", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(0, 0), Chunk: []byte(""), }, nil, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), expectedErr: ErrEmptyPayloadPatch, }, { name: "invalid following patch offset", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(10, 0), Chunk: []byte(""), }, { Range: rangeWithOffestWithLength(7, 0), Chunk: []byte(""), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), expectedErr: ErrInvalidPatchOffsetOrder, }, { name: "only header patch", newAttrs: attrs{ attr{ key: "key1", val: "val1", }, attr{ key: "key2", val: "val2", }, }, patchPayloads: []*objectSDK.PayloadPatch{ nil, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), }, { name: "header and payload", newAttrs: attrs{ attr{ key: "key1", val: "val1", }, attr{ key: "key2", val: "val2", }, }, patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("inserted at the beginning"), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"), }, { name: "header, then payload", newAttrs: attrs{ attr{ key: "key1", val: "val1", }, attr{ key: "key2", val: "val2", }, }, patchPayloads: []*objectSDK.PayloadPatch{ nil, { Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("inserted at the beginning"), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"), }, { name: "no effect", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(0, 0), Chunk: []byte(""), }, { Range: rangeWithOffestWithLength(12, 0), Chunk: []byte(""), }, { Range: rangeWithOffestWithLength(20, 0), Chunk: []byte(""), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), }, { name: "insert prefix", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("inserted at the beginning"), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"), }, { name: "insert in the middle", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(5, 0), Chunk: []byte("inserted somewhere in the middle"), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("01234inserted somewhere in the middle56789qwertyuiopasdfghjklzxcvbnm"), }, { name: "insert at the end", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(36, 0), Chunk: []byte("inserted somewhere at the end"), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("0123456789qwertyuiopasdfghjklzxcvbnminserted somewhere at the end"), }, { name: "replace by range", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(0, 12), Chunk: []byte("just replace"), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("just replaceertyuiopasdfghjklzxcvbnm"), }, { name: "replace and insert some bytes", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(0, 11), Chunk: []byte("replace and append in the middle"), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("replace and append in the middlewertyuiopasdfghjklzxcvbnm"), }, { name: "replace and insert some bytes in the middle", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(5, 3), Chunk: []byte("@@@@@"), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("01234@@@@@89qwertyuiopasdfghjklzxcvbnm"), }, { name: "a few patches: prefix, suffix", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("this_will_be_prefix"), }, { Range: rangeWithOffestWithLength(36, 0), Chunk: []byte("this_will_be_suffix"), }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("this_will_be_prefix0123456789qwertyuiopasdfghjklzxcvbnmthis_will_be_suffix"), }, { name: "a few patches: replace and insert some bytes", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(10, 3), Chunk: []byte("aaaaa"), }, { Range: rangeWithOffestWithLength(16, 0), Chunk: []byte("bbbbb"), }, }, originalObjectPayload: []byte("0123456789ABCDEF"), patched: []byte("0123456789aaaaaDEFbbbbb"), }, { name: "a few patches: various modifiactions", patchPayloads: []*objectSDK.PayloadPatch{ { Range: rangeWithOffestWithLength(4, 8), Chunk: []byte("earliest"), }, { Range: rangeWithOffestWithLength(13, 0), Chunk: []byte("known "), }, { Range: rangeWithOffestWithLength(35, 8), Chunk: []byte("a small town"), }, { Range: rangeWithOffestWithLength(62, 6), Chunk: []byte("tablet"), }, { Range: rangeWithOffestWithLength(87, 0), Chunk: []byte("Shar-Kali-Sharri"), }, }, originalObjectPayload: []byte("The ******** mention of Babylon as [insert] appears on a clay ****** from the reign of "), patched: []byte("The earliest known mention of Babylon as a small town appears on a clay tablet from the reign of Shar-Kali-Sharri"), }, } { t.Run(test.name, func(t *testing.T) { rangeProvider := &mockRangeProvider{ originalObjectPayload: test.originalObjectPayload, } originalObject, originalAddr := newTestObject() originalObject.SetPayload(test.originalObjectPayload) originalObject.SetPayloadSize(uint64(len(test.originalObjectPayload))) patchedObject, _ := newTestObject() wr := &mockPatchedObjectWriter{ obj: patchedObject, } hdrProvider := &mockHeaderProvider{ obj: originalObject, } patcher := New(hdrProvider, rangeProvider, wr) for i, pp := range test.patchPayloads { patch := &objectSDK.Patch{ Address: originalAddr, PayloadPatch: pp, } if i == 0 { patch.NewAttributes = test.newAttrs.ToSDKAttributes() patch.ReplaceAttributes = test.replaceAttrs } if !patcher.ApplyPatch(context.Background(), patch) { break } } _, err := patcher.Close(context.Background()) if err != nil { require.ErrorIs(t, err, test.expectedErr) } else { require.NoError(t, err) require.Equal(t, test.patched, patchedObject.Payload()) attrOfPatchedObject := patchedObject.Attributes() sort.Slice(attrOfPatchedObject, func(i, j int) bool { return strings.Compare(attrOfPatchedObject[i].Key(), attrOfPatchedObject[j].Key()) < 0 }) testAttrs := test.newAttrs.ToSDKAttributes() sort.Slice(testAttrs, func(i, j int) bool { return strings.Compare(testAttrs[i].Key(), testAttrs[j].Key()) < 0 }) require.Equal(t, testAttrs, attrOfPatchedObject) } }) } }