git.frostfs.info/TrueCloudLab/frostfs-sdk-go@v0.0.0-20241022124111-5361f0ecebd3/object/patcher/patcher_test.go (about) 1 package patcher 2 3 import ( 4 "bytes" 5 "context" 6 "io" 7 "testing" 8 9 objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" 10 oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" 11 oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" 12 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer" 13 "github.com/stretchr/testify/require" 14 ) 15 16 type mockPatchedObjectWriter struct { 17 obj *objectSDK.Object 18 } 19 20 func (m *mockPatchedObjectWriter) Write(_ context.Context, chunk []byte) (int, error) { 21 res := append(m.obj.Payload(), chunk...) 22 23 m.obj.SetPayload(res) 24 m.obj.SetPayloadSize(uint64(len(res))) 25 26 return len(chunk), nil 27 } 28 29 func (m *mockPatchedObjectWriter) WriteHeader(_ context.Context, hdr *objectSDK.Object) error { 30 m.obj.ToV2().SetHeader(hdr.ToV2().GetHeader()) 31 return nil 32 } 33 34 func (m *mockPatchedObjectWriter) Close(context.Context) (*transformer.AccessIdentifiers, error) { 35 return &transformer.AccessIdentifiers{}, nil 36 } 37 38 type mockRangeProvider struct { 39 originalObjectPayload []byte 40 } 41 42 var _ RangeProvider = (*mockRangeProvider)(nil) 43 44 func (m *mockRangeProvider) GetRange(_ context.Context, rng *objectSDK.Range) io.Reader { 45 offset := rng.GetOffset() 46 length := rng.GetLength() 47 48 if length == 0 { 49 return bytes.NewReader(m.originalObjectPayload[offset:]) 50 } 51 return bytes.NewReader(m.originalObjectPayload[offset : offset+length]) 52 } 53 54 func newTestObject() (*objectSDK.Object, oid.Address) { 55 obj := objectSDK.New() 56 57 addr := oidtest.Address() 58 obj.SetContainerID(addr.Container()) 59 obj.SetID(addr.Object()) 60 61 return obj, addr 62 } 63 64 func rangeWithOffestWithLength(offset, length uint64) *objectSDK.Range { 65 rng := new(objectSDK.Range) 66 rng.SetOffset(offset) 67 rng.SetLength(length) 68 return rng 69 } 70 71 func TestPatchRevert(t *testing.T) { 72 obj, _ := newTestObject() 73 74 modifPatch := &objectSDK.Patch{ 75 PayloadPatch: &objectSDK.PayloadPatch{ 76 Range: rangeWithOffestWithLength(0, 0), 77 78 Chunk: []byte("inserted"), 79 }, 80 } 81 82 originalObjectPayload := []byte("*******************") 83 84 obj.SetPayload(originalObjectPayload) 85 obj.SetPayloadSize(uint64(len(originalObjectPayload))) 86 87 exp := []byte("inserted*******************") 88 89 rangeProvider := &mockRangeProvider{ 90 originalObjectPayload: originalObjectPayload, 91 } 92 93 patchedObj, _ := newTestObject() 94 95 wr := &mockPatchedObjectWriter{ 96 obj: patchedObj, 97 } 98 99 prm := Params{ 100 Header: obj.CutPayload(), 101 102 RangeProvider: rangeProvider, 103 104 ObjectWriter: wr, 105 } 106 107 patcher := New(prm) 108 109 err := patcher.ApplyAttributesPatch(context.Background(), modifPatch.NewAttributes, modifPatch.ReplaceAttributes) 110 require.NoError(t, err) 111 112 err = patcher.ApplyPayloadPatch(context.Background(), modifPatch.PayloadPatch) 113 require.NoError(t, err) 114 115 _, err = patcher.Close(context.Background()) 116 require.NoError(t, err) 117 118 require.Equal(t, exp, patchedObj.Payload()) 119 120 revertPatch := &objectSDK.Patch{ 121 PayloadPatch: &objectSDK.PayloadPatch{ 122 Range: rangeWithOffestWithLength(0, uint64(len("inserted"))), 123 124 Chunk: []byte{}, 125 }, 126 } 127 128 rangeProvider = &mockRangeProvider{ 129 originalObjectPayload: exp, 130 } 131 132 patchedPatchedObj, _ := newTestObject() 133 134 wr = &mockPatchedObjectWriter{ 135 obj: patchedPatchedObj, 136 } 137 138 prm = Params{ 139 Header: patchedObj.CutPayload(), 140 141 RangeProvider: rangeProvider, 142 143 ObjectWriter: wr, 144 } 145 146 patcher = New(prm) 147 148 err = patcher.ApplyAttributesPatch(context.Background(), revertPatch.NewAttributes, revertPatch.ReplaceAttributes) 149 require.NoError(t, err) 150 151 err = patcher.ApplyPayloadPatch(context.Background(), revertPatch.PayloadPatch) 152 require.NoError(t, err) 153 154 _, err = patcher.Close(context.Background()) 155 require.NoError(t, err) 156 157 require.Equal(t, originalObjectPayload, patchedPatchedObj.Payload()) 158 } 159 160 func TestPatchRepeatAttributePatch(t *testing.T) { 161 obj, _ := newTestObject() 162 163 modifPatch := &objectSDK.Patch{} 164 165 originalObjectPayload := []byte("*******************") 166 167 obj.SetPayload(originalObjectPayload) 168 obj.SetPayloadSize(uint64(len(originalObjectPayload))) 169 170 rangeProvider := &mockRangeProvider{ 171 originalObjectPayload: originalObjectPayload, 172 } 173 174 patchedObj, _ := newTestObject() 175 176 wr := &mockPatchedObjectWriter{ 177 obj: patchedObj, 178 } 179 180 prm := Params{ 181 Header: obj.CutPayload(), 182 183 RangeProvider: rangeProvider, 184 185 ObjectWriter: wr, 186 } 187 188 patcher := New(prm) 189 190 err := patcher.ApplyAttributesPatch(context.Background(), modifPatch.NewAttributes, modifPatch.ReplaceAttributes) 191 require.NoError(t, err) 192 193 err = patcher.ApplyAttributesPatch(context.Background(), modifPatch.NewAttributes, modifPatch.ReplaceAttributes) 194 require.ErrorIs(t, err, ErrAttrPatchAlreadyApplied) 195 } 196 197 func TestPatchEmptyPayloadPatch(t *testing.T) { 198 obj, _ := newTestObject() 199 200 modifPatch := &objectSDK.Patch{} 201 202 originalObjectPayload := []byte("*******************") 203 204 obj.SetPayload(originalObjectPayload) 205 obj.SetPayloadSize(uint64(len(originalObjectPayload))) 206 207 rangeProvider := &mockRangeProvider{ 208 originalObjectPayload: originalObjectPayload, 209 } 210 211 patchedObj, _ := newTestObject() 212 213 wr := &mockPatchedObjectWriter{ 214 obj: patchedObj, 215 } 216 217 prm := Params{ 218 Header: obj.CutPayload(), 219 220 RangeProvider: rangeProvider, 221 222 ObjectWriter: wr, 223 } 224 225 patcher := New(prm) 226 227 err := patcher.ApplyAttributesPatch(context.Background(), modifPatch.NewAttributes, modifPatch.ReplaceAttributes) 228 require.NoError(t, err) 229 230 err = patcher.ApplyPayloadPatch(context.Background(), nil) 231 require.ErrorIs(t, err, ErrPayloadPatchIsNil) 232 } 233 234 func newTestAttribute(key, val string) objectSDK.Attribute { 235 var attr objectSDK.Attribute 236 attr.SetKey(key) 237 attr.SetValue(val) 238 return attr 239 } 240 241 func TestPatch(t *testing.T) { 242 for _, test := range []struct { 243 name string 244 patches []objectSDK.Patch 245 originalObjectPayload []byte 246 patchedPayload []byte 247 originalHeaderAttributes []objectSDK.Attribute 248 patchedHeaderAttributes []objectSDK.Attribute 249 expectedPayloadPatchErr error 250 }{ 251 { 252 name: "invalid offset", 253 patches: []objectSDK.Patch{ 254 { 255 PayloadPatch: &objectSDK.PayloadPatch{ 256 Range: rangeWithOffestWithLength(100, 0), 257 Chunk: []byte(""), 258 }, 259 }, 260 }, 261 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 262 expectedPayloadPatchErr: ErrOffsetExceedsSize, 263 }, 264 { 265 name: "invalid following patch offset", 266 patches: []objectSDK.Patch{ 267 { 268 PayloadPatch: &objectSDK.PayloadPatch{ 269 Range: rangeWithOffestWithLength(10, 0), 270 Chunk: []byte(""), 271 }, 272 }, 273 { 274 PayloadPatch: &objectSDK.PayloadPatch{ 275 Range: rangeWithOffestWithLength(7, 0), 276 Chunk: []byte(""), 277 }, 278 }, 279 }, 280 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 281 expectedPayloadPatchErr: ErrInvalidPatchOffsetOrder, 282 }, 283 { 284 name: "only header patch", 285 patches: []objectSDK.Patch{ 286 { 287 NewAttributes: []objectSDK.Attribute{ 288 newTestAttribute("key1", "val2"), 289 newTestAttribute("key2", "val2"), 290 }, 291 }, 292 }, 293 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 294 patchedPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 295 patchedHeaderAttributes: []objectSDK.Attribute{ 296 newTestAttribute("key1", "val2"), 297 newTestAttribute("key2", "val2"), 298 }, 299 }, 300 { 301 name: "header and payload", 302 patches: []objectSDK.Patch{ 303 { 304 NewAttributes: []objectSDK.Attribute{ 305 newTestAttribute("key1", "val2"), 306 newTestAttribute("key2", "val2"), 307 }, 308 PayloadPatch: &objectSDK.PayloadPatch{ 309 Range: rangeWithOffestWithLength(0, 0), 310 Chunk: []byte("inserted at the beginning"), 311 }, 312 }, 313 }, 314 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 315 patchedPayload: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"), 316 patchedHeaderAttributes: []objectSDK.Attribute{ 317 newTestAttribute("key1", "val2"), 318 newTestAttribute("key2", "val2"), 319 }, 320 }, 321 { 322 name: "header only merge attributes", 323 patches: []objectSDK.Patch{ 324 { 325 NewAttributes: []objectSDK.Attribute{ 326 newTestAttribute("key1", "val2"), 327 newTestAttribute("key2", "val2-incoming"), 328 }, 329 PayloadPatch: &objectSDK.PayloadPatch{ 330 Range: rangeWithOffestWithLength(0, 0), 331 Chunk: []byte("inserted at the beginning"), 332 }, 333 }, 334 }, 335 originalHeaderAttributes: []objectSDK.Attribute{ 336 newTestAttribute("key2", "to be popped out"), 337 newTestAttribute("key3", "val3"), 338 }, 339 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 340 patchedPayload: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"), 341 patchedHeaderAttributes: []objectSDK.Attribute{ 342 newTestAttribute("key2", "val2-incoming"), 343 newTestAttribute("key3", "val3"), 344 newTestAttribute("key1", "val2"), 345 }, 346 }, 347 { 348 name: "header only then payload", 349 patches: []objectSDK.Patch{ 350 { 351 NewAttributes: []objectSDK.Attribute{ 352 newTestAttribute("key1", "val2"), 353 newTestAttribute("key2", "val2"), 354 }, 355 }, 356 { 357 PayloadPatch: &objectSDK.PayloadPatch{ 358 Range: rangeWithOffestWithLength(0, 0), 359 Chunk: []byte("inserted at the beginning"), 360 }, 361 }, 362 }, 363 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 364 patchedPayload: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"), 365 patchedHeaderAttributes: []objectSDK.Attribute{ 366 newTestAttribute("key1", "val2"), 367 newTestAttribute("key2", "val2"), 368 }, 369 }, 370 { 371 name: "no effect", 372 patches: []objectSDK.Patch{ 373 { 374 PayloadPatch: &objectSDK.PayloadPatch{ 375 Range: rangeWithOffestWithLength(0, 0), 376 Chunk: []byte(""), 377 }, 378 }, 379 { 380 PayloadPatch: &objectSDK.PayloadPatch{ 381 Range: rangeWithOffestWithLength(12, 0), 382 Chunk: []byte(""), 383 }, 384 }, 385 { 386 PayloadPatch: &objectSDK.PayloadPatch{ 387 Range: rangeWithOffestWithLength(20, 0), 388 Chunk: []byte(""), 389 }, 390 }, 391 }, 392 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 393 patchedPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 394 }, 395 { 396 name: "insert prefix", 397 patches: []objectSDK.Patch{ 398 { 399 PayloadPatch: &objectSDK.PayloadPatch{ 400 Range: rangeWithOffestWithLength(0, 0), 401 Chunk: []byte("inserted at the beginning"), 402 }, 403 }, 404 }, 405 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 406 patchedPayload: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"), 407 }, 408 { 409 name: "insert in the middle", 410 patches: []objectSDK.Patch{ 411 { 412 PayloadPatch: &objectSDK.PayloadPatch{ 413 Range: rangeWithOffestWithLength(5, 0), 414 Chunk: []byte("inserted somewhere in the middle"), 415 }, 416 }, 417 }, 418 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 419 patchedPayload: []byte("01234inserted somewhere in the middle56789qwertyuiopasdfghjklzxcvbnm"), 420 }, 421 { 422 name: "insert at the end", 423 patches: []objectSDK.Patch{ 424 { 425 PayloadPatch: &objectSDK.PayloadPatch{ 426 Range: rangeWithOffestWithLength(36, 0), 427 Chunk: []byte("inserted somewhere at the end"), 428 }, 429 }, 430 }, 431 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 432 patchedPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnminserted somewhere at the end"), 433 }, 434 { 435 name: "replace by range", 436 patches: []objectSDK.Patch{ 437 { 438 PayloadPatch: &objectSDK.PayloadPatch{ 439 Range: rangeWithOffestWithLength(0, 12), 440 Chunk: []byte("just replace"), 441 }, 442 }, 443 }, 444 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 445 patchedPayload: []byte("just replaceertyuiopasdfghjklzxcvbnm"), 446 }, 447 { 448 name: "replace and insert some bytes", 449 patches: []objectSDK.Patch{ 450 { 451 PayloadPatch: &objectSDK.PayloadPatch{ 452 Range: rangeWithOffestWithLength(0, 11), 453 Chunk: []byte("replace and append in the middle"), 454 }, 455 }, 456 }, 457 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 458 patchedPayload: []byte("replace and append in the middlewertyuiopasdfghjklzxcvbnm"), 459 }, 460 { 461 name: "replace and insert some bytes in the middle", 462 patches: []objectSDK.Patch{ 463 { 464 PayloadPatch: &objectSDK.PayloadPatch{ 465 Range: rangeWithOffestWithLength(5, 3), 466 Chunk: []byte("@@@@@"), 467 }, 468 }, 469 }, 470 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 471 patchedPayload: []byte("01234@@@@@89qwertyuiopasdfghjklzxcvbnm"), 472 }, 473 { 474 name: "a few patches: prefix, suffix", 475 patches: []objectSDK.Patch{ 476 { 477 PayloadPatch: &objectSDK.PayloadPatch{ 478 Range: rangeWithOffestWithLength(0, 0), 479 Chunk: []byte("this_will_be_prefix"), 480 }, 481 }, 482 { 483 PayloadPatch: &objectSDK.PayloadPatch{ 484 Range: rangeWithOffestWithLength(36, 0), 485 Chunk: []byte("this_will_be_suffix"), 486 }, 487 }, 488 }, 489 originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"), 490 patchedPayload: []byte("this_will_be_prefix0123456789qwertyuiopasdfghjklzxcvbnmthis_will_be_suffix"), 491 }, 492 { 493 name: "a few patches: replace and insert some bytes", 494 patches: []objectSDK.Patch{ 495 { 496 PayloadPatch: &objectSDK.PayloadPatch{ 497 Range: rangeWithOffestWithLength(10, 3), 498 Chunk: []byte("aaaaa"), 499 }, 500 }, 501 { 502 PayloadPatch: &objectSDK.PayloadPatch{ 503 Range: rangeWithOffestWithLength(16, 0), 504 Chunk: []byte("bbbbb"), 505 }, 506 }, 507 }, 508 originalObjectPayload: []byte("0123456789ABCDEF"), 509 patchedPayload: []byte("0123456789aaaaaDEFbbbbb"), 510 }, 511 { 512 name: "starting from the same offset", 513 patches: []objectSDK.Patch{ 514 { 515 PayloadPatch: &objectSDK.PayloadPatch{ 516 Range: rangeWithOffestWithLength(8, 3), 517 Chunk: []byte("1"), 518 }, 519 }, 520 { 521 PayloadPatch: &objectSDK.PayloadPatch{ 522 Range: rangeWithOffestWithLength(11, 0), 523 Chunk: []byte("2"), 524 }, 525 }, 526 { 527 PayloadPatch: &objectSDK.PayloadPatch{ 528 Range: rangeWithOffestWithLength(11, 0), 529 Chunk: []byte("3"), 530 }, 531 }, 532 }, 533 originalObjectPayload: []byte("abcdefghijklmnop"), 534 patchedPayload: []byte("abcdefgh123lmnop"), 535 }, 536 { 537 name: "a few patches: various modifiactions", 538 patches: []objectSDK.Patch{ 539 { 540 PayloadPatch: &objectSDK.PayloadPatch{ 541 Range: rangeWithOffestWithLength(4, 8), 542 Chunk: []byte("earliest"), 543 }, 544 }, 545 { 546 PayloadPatch: &objectSDK.PayloadPatch{ 547 Range: rangeWithOffestWithLength(13, 0), 548 Chunk: []byte("known "), 549 }, 550 }, 551 { 552 PayloadPatch: &objectSDK.PayloadPatch{ 553 Range: rangeWithOffestWithLength(35, 8), 554 Chunk: []byte("a small town"), 555 }, 556 }, 557 { 558 PayloadPatch: &objectSDK.PayloadPatch{ 559 Range: rangeWithOffestWithLength(62, 6), 560 Chunk: []byte("tablet"), 561 }, 562 }, 563 { 564 PayloadPatch: &objectSDK.PayloadPatch{ 565 Range: rangeWithOffestWithLength(87, 0), 566 Chunk: []byte("Shar-Kali-Sharri"), 567 }, 568 }, 569 }, 570 originalObjectPayload: []byte("The ******** mention of Babylon as [insert] appears on a clay ****** from the reign of "), 571 patchedPayload: []byte("The earliest known mention of Babylon as a small town appears on a clay tablet from the reign of Shar-Kali-Sharri"), 572 }, 573 } { 574 t.Run(test.name, func(t *testing.T) { 575 rangeProvider := &mockRangeProvider{ 576 originalObjectPayload: test.originalObjectPayload, 577 } 578 579 originalObject, _ := newTestObject() 580 originalObject.SetPayload(test.originalObjectPayload) 581 originalObject.SetPayloadSize(uint64(len(test.originalObjectPayload))) 582 originalObject.SetAttributes(test.originalHeaderAttributes...) 583 584 patchedObject, _ := newTestObject() 585 586 wr := &mockPatchedObjectWriter{ 587 obj: patchedObject, 588 } 589 590 prm := Params{ 591 Header: originalObject.CutPayload(), 592 593 RangeProvider: rangeProvider, 594 595 ObjectWriter: wr, 596 } 597 598 patcher := New(prm) 599 600 for i, patch := range test.patches { 601 if i == 0 { 602 _ = patcher.ApplyAttributesPatch(context.Background(), patch.NewAttributes, patch.ReplaceAttributes) 603 } 604 605 if patch.PayloadPatch == nil { 606 continue 607 } 608 609 err := patcher.ApplyPayloadPatch(context.Background(), patch.PayloadPatch) 610 if err != nil && test.expectedPayloadPatchErr != nil { 611 require.ErrorIs(t, err, test.expectedPayloadPatchErr) 612 return 613 } 614 require.NoError(t, err) 615 } 616 617 _, err := patcher.Close(context.Background()) 618 require.NoError(t, err) 619 require.Equal(t, test.patchedPayload, patchedObject.Payload()) 620 621 patchedAttrs := append([]objectSDK.Attribute{}, test.patchedHeaderAttributes...) 622 require.Equal(t, patchedAttrs, patchedObject.Attributes()) 623 }) 624 } 625 626 }