|
4 | 4 |
|
5 | 5 | use VlyDev\Steam\InspectLink; |
6 | 6 | use VlyDev\Steam\ItemPreviewData; |
| 7 | +use VlyDev\Steam\MalformedInspectLinkException; |
7 | 8 | use VlyDev\Steam\Sticker; |
8 | 9 | use PHPUnit\Framework\TestCase; |
9 | 10 |
|
@@ -652,4 +653,86 @@ public function testRoundtrip_PaintKit(): void |
652 | 653 | $this->assertSame(37, $result->keychains[0]->stickerId); |
653 | 654 | $this->assertSame(7256, $result->keychains[0]->paintKit); |
654 | 655 | } |
| 656 | + |
| 657 | + // ----------------------------------------------------------------------- |
| 658 | + // Malformed URLs (regression: must not emit E_WARNING from hex2bin) |
| 659 | + // |
| 660 | + // Real-world URLs collected from a buggy upstream source that truncates |
| 661 | + // the hex payload mid-keychain, leaving an odd number of hex characters |
| 662 | + // and a missing CRC. The library must reject them with a clean |
| 663 | + // MalformedInspectLinkException — never let hex2bin trigger a PHP warning |
| 664 | + // (which Symfony's error handler would escalate to ErrorException and log). |
| 665 | + // ----------------------------------------------------------------------- |
| 666 | + |
| 667 | + /** @return array<string, array{string}> */ |
| 668 | + public static function malformedUrlProvider(): array |
| 669 | + { |
| 670 | + return [ |
| 671 | + 'truncated mid-keychain (defindex=1, key=0xAD)' => [ |
| 672 | + 'steam://run/730//+csgo_econ_action_preview%20ADBD1050393912ACB5AC8D45AC85A99DA9956A116D5FAEED21ACCFB4A5AFBD348EB0ADAD2D9280ADADDD6F90EDA37510E84D8BEE11CFB4A5ACBD348EB0ADAD2D9280ADAD5D6F906F2B4C13E84D93D591CFB9A5ADBD419EB0ADAD2D9290ADF22010E8B72FB213CFB4A5ADBD549EB0ADAD2D9280ADADED6C90CFD43F10E892DFE513CFB4A5ADBD549EB0ADAD2D9280ADAD85EE902F952210E82EB8A613C52E2D2D2DA1DDA90FACBBA5ADBD89902923AAECE83', |
| 673 | + ], |
| 674 | + 'truncated mid-keychain (defindex=9, key=0xEE)' => [ |
| 675 | + 'steam://run/730//+csgo_econ_action_preview%20EEFE3144332550EFF6E7CE28E8C6EADEEAD642323218EDAE4DEA8CFAE6ECFE35A7F302BFD6D1D3004FCB50ABAE5CF8528CF7E6EEFE0DA7F3394DDED1C3EEEE7EAFD39E25B3D3AB9EAE70D28CFAE6ECFE32A7F3EEEE6ED1D3595B17D3AB9E8E65538CFAE6ECFE64ADF3EEEE6ED1D3E597F3D3AB2EEA1AD58CF7E6EDFE0BCAF302BFD6D1C3EEEEEF2DD3AEF5F552ABEE31A855866D6E6E6EE29EE64CEFF8E6EEFED8D3B6CBCBACABFD70EED1A31F96E7AFBE5', |
| 676 | + ], |
| 677 | + 'truncated mid-keychain (defindex=1, key=0x4A)' => [ |
| 678 | + 'steam://run/730//+csgo_econ_action_preview%204A5A8EFCB1B9F44B524B6AA24B624E7A4E72BACFF6B8490AD449285E42485AAB75576316457577CA2422760F4A413E712853424A5AA679574A4ACA75674A4A8A8A770A7F85760FD04246F4285342495AB279574A4ACA75674A4A8A0A7799714A750F0A5140F7285342495AAD7277EB4547F40F0A00EB7122C9CACACA463A4EE84B5D424A5A4C776C02A10A0F34A5C17407F0145C0A1AA', |
| 679 | + ], |
| 680 | + 'truncated mid-keychain (AK-47 1035, key=0xFA)' => [ |
| 681 | + 'steam://run/730//+csgo_econ_action_preview%20FAEA5766387F45FBE2FDDA71F2D2FECAF3C2142C0C0EF9BA7CFFB2FAAAFA98EEF2F8EA3BFBD7FAFA3ABBC7EA2FB7C7BF9ACC47C698E3F2F9EA03C9E7FAFA7AC5D7FAFABA3AC780CA89C4BFFAAAD9C198E3F2F9EA03C9E7FAFA7AC5D7FAFACEB9C7B60177C4BFAAB82AC698EEF2F9EA03C9E7FAFA7AC5C7C11558C4BFFA43DBC198F5F2FBEA13DEC7759A1147BF9A16F64692797A7A7AF68AF258FBEFF2FAEADEC7DEAC32BBBF0BF9BAC4B760382CC5A2D24', |
| 682 | + ], |
| 683 | + 'truncated mid-keychain (defindex=40, key=0x9F)' => [ |
| 684 | + 'steam://run/730//+csgo_econ_action_preview%209F8F4F504C7C219E87B7BF629EB79CAF9BA73F1D53419CDF0699FD8B979F8F5CCF82050686A0A2F4038821DA17F3FD22FD8B979F8F49D4825C6AB7A0A25F50B224DA7F0CF222FD8B979D8F5DCF82F9F9B9A0A2B7B25422DA6F6F1422FD8B979D8F5DCF822781DAA0A247B731A2DA7F92EF23FD86979F8F5DCF827EE5CBA0B29F9FDFDFA285AD4322DA9FFD8AA3F71C1F1F1F93EF873D9E88979F8FDDA243C05EDFDA4CFB44A0D202B77EDFCF3F339C3DF89', |
| 685 | + ], |
| 686 | + 'truncated mid-keychain (M4A1-S 1130, key=0xFA)' => [ |
| 687 | + 'steam://run/730//+csgo_econ_action_preview%20FAEA5B24060844FBE2C6DA10F2D2FECAF3C2631A3308F9BA47F8B2FAAAFA98EEF2FEEA29B2E781EED4C5C7776B78C4BFFAFA21CD98EEF2FAEA09BCE7F02DD9C5C7FE6C57C7BF7A58ED4698EEF2FEEA6DBDE79C9CDCC5C7929F2F47BFFA461BC398E3F2FBEA15BDE7E57FD1C5D7FAFA8AB8C7C2CEDC46BF12973EC798EEF2FEEA24B8E781EED4C5C75696FCC5BF2AEBF2C792797A7A7AF68AF258FBEDF2FAEAD2C7B3683FBBBF065F8CC5B763A382BAAA0C5', |
| 688 | + ], |
| 689 | + 'truncated mid-keychain (defindex=35, key=0x4D)' => [ |
| 690 | + 'steam://run/730//+csgo_econ_action_preview%204D5D9DF8C7D2F34C556E6DDC4C654E7D4975B2D2AEBB4E0DAB4B2F59454E5D9A70604D4DBD8C7002A356F308CD4603F62F5445495DEB745011C20F72604D4D798F70797F9FF3089D49ABF12F5945495DEB74502B2BAB737045B3FAF308FD4BE8F12F5945495DF868508081417270BFB9D2F308ADD283F12F5945495DF86850AC375972702FA3CBF3083D2EBFF125CECDCDCD413D5AEF4C5A454D5D567084E4F80C08254C547200CE3E1D0D1DF9D24C63938', |
| 691 | + ], |
| 692 | + 'truncated mid-keychain (AK-47 1171, key=0xCF)' => [ |
| 693 | + 'steam://run/730//+csgo_econ_action_preview%20CFDF6258412F71CED7C8EF5CC6E7C9FFCBF7465B3B38CC8F7ACEADD6C7CEDF3BF2D2F2C5D8F0E2CFCFE40CF27F72E1728AF786F5F2ADC0C7CCDF3BF2F241D88EF18A0F03F6F2ADDBC7CCDF3CF2E2CFCF0F8DF27B3FB7F18A6F5ACDF2ADD6C7CCDF3CF2D2C518ECF0E2CFCF8F0EF2D5C29AF18ACFCFD8F5ADDBC7CFDF3CF2E2CFCF6D8DF24DB0DA718A2FA6DBF3A74C4F4F4FC3BFC76DCED8C7CFDF87F27B950C8E8A37D0A3F182A2B8A48F9F4332CD2C2B6', |
| 694 | + ], |
| 695 | + 'truncated mid-keychain (defindex=1 1050, key=0xCE)' => [ |
| 696 | + 'steam://run/730//+csgo_econ_action_preview%20CEDE51082D1C70CFD6CFEE54C6E6CAFEC7F631274538CD8E2DC886CE9ECEACDAC6CDDE0C8DD3CECE4EF1F382996B738B4E5E8D75ACD7C6CEDE0C8DD3CECE4EF1E3CECE8E0FF3A56603708BECDBE170ACD7C6CEDE0C8DD3CECE4EF1E3CECEDE0FF37682A5708B0650FB70ACD7C6CEDE0C8DD3CECE4EF1E3CECEDE0FF34E3CEC738BDAC6F870ACD7C6CDDE798AD3CECE4EF1E3CECE0E0EF3333BC5F18BAE8E9FF2A64D4E4E4EC2BECA6CCFD9C6CEDECFF37C6', |
| 697 | + ], |
| 698 | + 'odd-length bare hex' => ['ABC'], |
| 699 | + 'empty string' => [''], |
| 700 | + 'non-hex characters' => ['ZZZZZZZZZZZZ'], |
| 701 | + ]; |
| 702 | + } |
| 703 | + |
| 704 | + #[\PHPUnit\Framework\Attributes\DataProvider('malformedUrlProvider')] |
| 705 | + public function testMalformedUrlThrowsCleanException(string $url): void |
| 706 | + { |
| 707 | + $this->expectException(MalformedInspectLinkException::class); |
| 708 | + InspectLink::deserialize($url); |
| 709 | + } |
| 710 | + |
| 711 | + #[\PHPUnit\Framework\Attributes\DataProvider('malformedUrlProvider')] |
| 712 | + public function testMalformedUrlEmitsNoPhpWarning(string $url): void |
| 713 | + { |
| 714 | + // Promote any E_WARNING to ErrorException so we can detect it. |
| 715 | + // Bug pre-fix: hex2bin('odd-hex') emitted E_WARNING → Symfony escalated → log noise. |
| 716 | + // Post-fix: parity is checked before hex2bin, so no warning should ever be emitted. |
| 717 | + $errorHandler = static function (int $errno, string $errstr, string $errfile, int $errline): bool { |
| 718 | + throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); |
| 719 | + }; |
| 720 | + |
| 721 | + set_error_handler($errorHandler, E_WARNING); |
| 722 | + try { |
| 723 | + InspectLink::deserialize($url); |
| 724 | + $this->fail('Expected MalformedInspectLinkException was not thrown'); |
| 725 | + } catch (MalformedInspectLinkException) { |
| 726 | + // Expected — clean rejection without a PHP warning. |
| 727 | + $this->addToAssertionCount(1); |
| 728 | + } finally { |
| 729 | + restore_error_handler(); |
| 730 | + } |
| 731 | + } |
| 732 | + |
| 733 | + public function testMalformedUrlExceptionExtendsInvalidArgument(): void |
| 734 | + { |
| 735 | + // BC: existing callers catching \InvalidArgumentException must still work. |
| 736 | + $this->assertTrue(is_subclass_of(MalformedInspectLinkException::class, \InvalidArgumentException::class)); |
| 737 | + } |
655 | 738 | } |
0 commit comments