package patcher import ( "bytes" "context" "io" "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 mockRangeProvider struct { originalObjectPayload []byte } var _ RangeProvider = (*mockRangeProvider)(nil) func (m *mockRangeProvider) ReadRange(_ context.Context, 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 rangeWithOffestWithLength(offset, length uint64) *objectSDK.Range { rng := new(objectSDK.Range) rng.SetOffset(offset) rng.SetLength(length) return rng } func TestPatchRevert(t *testing.T) { obj, _ := newTestObject() modifPatch := &objectSDK.Patch{ 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, } patchedObj, _ := newTestObject() wr := &mockPatchedObjectWriter{ obj: patchedObj, } patcher := New(obj.CutPayload(), rangeProvider, wr) err := patcher.ApplyAttributesPatch(context.Background(), modifPatch.NewAttributes, modifPatch.ReplaceAttributes) require.NoError(t, err) err = patcher.ApplyPayloadPatch(context.Background(), modifPatch.PayloadPatch) require.NoError(t, err) _, err = patcher.Close(context.Background()) require.NoError(t, err) require.Equal(t, exp, patchedObj.Payload()) revertPatch := &objectSDK.Patch{ PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, uint64(len("inserted"))), Chunk: []byte{}, }, } rangeProvider = &mockRangeProvider{ originalObjectPayload: exp, } patchedPatchedObj, _ := newTestObject() wr = &mockPatchedObjectWriter{ obj: patchedPatchedObj, } patcher = New(patchedObj.CutPayload(), rangeProvider, wr) err = patcher.ApplyAttributesPatch(context.Background(), revertPatch.NewAttributes, revertPatch.ReplaceAttributes) require.NoError(t, err) err = patcher.ApplyPayloadPatch(context.Background(), revertPatch.PayloadPatch) require.NoError(t, err) _, err = patcher.Close(context.Background()) require.NoError(t, err) require.Equal(t, originalObjectPayload, patchedPatchedObj.Payload()) } func newTestAttribute(key, val string) objectSDK.Attribute { var attr objectSDK.Attribute attr.SetKey(key) attr.SetValue(val) return attr } func TestPatch(t *testing.T) { for _, test := range []struct { name string patches []objectSDK.Patch originalObjectPayload []byte patched []byte expectedPayloadPatchErr error }{ { name: "invalid offset", patches: []objectSDK.Patch{ { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(100, 0), Chunk: []byte(""), }, }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), expectedPayloadPatchErr: ErrOffsetExceedsSize, }, { name: "invalid following patch offset", patches: []objectSDK.Patch{ { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(10, 0), Chunk: []byte(""), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(7, 0), Chunk: []byte(""), }, }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), expectedPayloadPatchErr: ErrInvalidPatchOffsetOrder, }, { name: "only header patch", patches: []objectSDK.Patch{ { NewAttributes: []objectSDK.Attribute{ newTestAttribute("key1", "val2"), newTestAttribute("key2", "val2"), }, }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), }, { name: "header and payload", patches: []objectSDK.Patch{ { NewAttributes: []objectSDK.Attribute{ newTestAttribute("key1", "val2"), newTestAttribute("key2", "val2"), }, PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("inserted at the beginning"), }, }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"), }, { name: "header only then payload", patches: []objectSDK.Patch{ { NewAttributes: []objectSDK.Attribute{ newTestAttribute("key1", "val2"), newTestAttribute("key2", "val2"), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("inserted at the beginning"), }, }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"), expectedPayloadPatchErr: ErrPayloadPatchIsNil, }, { name: "no effect", patches: []objectSDK.Patch{ { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, 0), Chunk: []byte(""), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(12, 0), Chunk: []byte(""), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(20, 0), Chunk: []byte(""), }, }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), }, { name: "insert prefix", patches: []objectSDK.Patch{ { PayloadPatch: &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", patches: []objectSDK.Patch{ { PayloadPatch: &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", patches: []objectSDK.Patch{ { PayloadPatch: &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", patches: []objectSDK.Patch{ { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, 12), Chunk: []byte("just replace"), }, }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("just replaceertyuiopasdfghjklzxcvbnm"), }, { name: "replace and insert some bytes", patches: []objectSDK.Patch{ { PayloadPatch: &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", patches: []objectSDK.Patch{ { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(5, 3), Chunk: []byte("@@@@@"), }, }, }, originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), patched: []byte("01234@@@@@89qwertyuiopasdfghjklzxcvbnm"), }, { name: "a few patches: prefix, suffix", patches: []objectSDK.Patch{ { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(0, 0), Chunk: []byte("this_will_be_prefix"), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ 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", patches: []objectSDK.Patch{ { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(10, 3), Chunk: []byte("aaaaa"), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(16, 0), Chunk: []byte("bbbbb"), }, }, }, originalObjectPayload: []byte("0123456789ABCDEF"), patched: []byte("0123456789aaaaaDEFbbbbb"), }, { name: "a few patches: various modifiactions", patches: []objectSDK.Patch{ { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(4, 8), Chunk: []byte("earliest"), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(13, 0), Chunk: []byte("known "), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(35, 8), Chunk: []byte("a small town"), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ Range: rangeWithOffestWithLength(62, 6), Chunk: []byte("tablet"), }, }, { PayloadPatch: &objectSDK.PayloadPatch{ 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, _ := newTestObject() originalObject.SetPayload(test.originalObjectPayload) originalObject.SetPayloadSize(uint64(len(test.originalObjectPayload))) patchedObject, _ := newTestObject() wr := &mockPatchedObjectWriter{ obj: patchedObject, } patcher := New(originalObject.CutPayload(), rangeProvider, wr) for i, patch := range test.patches { if i == 0 { _ = patcher.ApplyAttributesPatch(context.Background(), patch.NewAttributes, patch.ReplaceAttributes) } err := patcher.ApplyPayloadPatch(context.Background(), patch.PayloadPatch) if err != nil { if test.expectedPayloadPatchErr != nil { require.ErrorIs(t, err, test.expectedPayloadPatchErr) return } else { require.NoError(t, err) } } } _, err := patcher.Close(context.Background()) 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) }) } }