diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a9ab91..dc79a55 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,9 @@ jobs: - name: Checkout uses: actions/checkout@v1 + - name: Build docker image + run: USER_ID=$(stat -c %u .) GROUP_ID=$(stat -c %g .) ./docker-dev build dev + - name: Install dependencies run: ./docker-dev run --rm dev composer install diff --git a/src/Serialization/BinaryEncoder.php b/src/Serialization/BinaryEncoder.php index 17dfa6c..ee708ee 100644 --- a/src/Serialization/BinaryEncoder.php +++ b/src/Serialization/BinaryEncoder.php @@ -34,13 +34,25 @@ public function encodeDouble(float $double): string private function encodeLong(int $value): string { - $n = ($value << 1) ^ ($value >> 63); - $str = ''; - while (0 != ($n & ~0x7F)) { - $str .= chr(($n & 0x7F) | 0x80); - $n >>= 7; + $n = (int) $value; + $n = ($n << 1) ^ ($n >> 63); + + if ($n >= 0 && $n < 0x80) { + return chr($n); } - $str .= chr($n); - return $str; + + $buf = []; + if (($n & ~0x7F) != 0) { + $buf[] = ($n | 0x80) & 0xFF; + $n = ($n >> 7) ^ (($n >> 63) << 57); // unsigned shift right ($n >>> 7) + + while ($n > 0x7F) { + $buf[] = ($n | 0x80) & 0xFF; + $n >>= 7; // $n is always positive here + } + } + + $buf[] = $n; + return pack('C*', ...$buf); } } diff --git a/tests/Unit/Deserialization/BinaryDecoderTest.php b/tests/Unit/Deserialization/BinaryDecoderTest.php index 50a310b..630ed49 100644 --- a/tests/Unit/Deserialization/BinaryDecoderTest.php +++ b/tests/Unit/Deserialization/BinaryDecoderTest.php @@ -61,7 +61,7 @@ public function testSkipLongWithMultipleBytes(): void public function testReadFloatWithValidValue(): void { // Test reading a float value (3.14) - $floatBytes = pack('g', 3.14); + $floatBytes = "\xC3\xF5\x48\x40"; // IEEE 754 representation of 3.14 $this->stream->expects($this->once()) ->method('read') @@ -76,7 +76,7 @@ public function testReadFloatWithValidValue(): void public function testReadFloatWithZero(): void { // Test reading zero float - $floatBytes = pack('g', 0.0); + $floatBytes = "\x00\x00\x00\x00"; // IEEE 754 representation of 0.0 $this->stream->expects($this->once()) ->method('read') @@ -91,7 +91,7 @@ public function testReadFloatWithZero(): void public function testReadFloatWithNegativeValue(): void { // Test reading a negative float - $floatBytes = pack('g', -2.5); + $floatBytes = "\x00\x00\x20\xC0"; // IEEE 754 representation of -2.5 $this->stream->expects($this->once()) ->method('read') @@ -106,7 +106,7 @@ public function testReadFloatWithNegativeValue(): void public function testReadFloatWithInfinity(): void { // Test reading infinity - $floatBytes = pack('g', INF); + $floatBytes = "\x00\x00\x80\x7F"; // IEEE 754 representation of +infinity $this->stream->expects($this->once()) ->method('read') @@ -121,7 +121,7 @@ public function testReadFloatWithInfinity(): void public function testReadFloatWithNaN(): void { // Test reading NaN - $floatBytes = pack('g', NAN); + $floatBytes = "\x00\x00\xC0\x7F"; // IEEE 754 representation of NaN $this->stream->expects($this->once()) ->method('read') @@ -136,7 +136,7 @@ public function testReadFloatWithNaN(): void public function testReadDoubleWithValidValue(): void { // Test reading a double value (3.14159265359) - $doubleBytes = pack('e', 3.14159265359); + $doubleBytes = "\xEA\x2E\x44\x54\xFB\x21\x09\x40"; // IEEE 754 representation of 3.14159265359 $this->stream->expects($this->once()) ->method('read') @@ -151,7 +151,7 @@ public function testReadDoubleWithValidValue(): void public function testReadDoubleWithZero(): void { // Test reading zero double - $doubleBytes = pack('e', 0.0); + $doubleBytes = "\x00\x00\x00\x00\x00\x00\x00\x00"; // IEEE 754 representation of 0.0 $this->stream->expects($this->once()) ->method('read') @@ -166,7 +166,7 @@ public function testReadDoubleWithZero(): void public function testReadDoubleWithNegativeValue(): void { // Test reading a negative double - $doubleBytes = pack('e', -2.71828182846); + $doubleBytes = "\xCF\x5F\x14\x8B\x0A\xBF\x05\xC0"; // IEEE 754 representation of -2.71828182846 $this->stream->expects($this->once()) ->method('read') @@ -181,7 +181,7 @@ public function testReadDoubleWithNegativeValue(): void public function testReadDoubleWithInfinity(): void { // Test reading infinity - $doubleBytes = pack('e', INF); + $doubleBytes = "\x00\x00\x00\x00\x00\x00\xF0\x7F"; // IEEE 754 representation of +infinity $this->stream->expects($this->once()) ->method('read') @@ -196,7 +196,7 @@ public function testReadDoubleWithInfinity(): void public function testReadDoubleWithNaN(): void { // Test reading NaN - $doubleBytes = pack('e', NAN); + $doubleBytes = "\x00\x00\x00\x00\x00\x00\xF8\x7F"; // IEEE 754 representation of NaN $this->stream->expects($this->once()) ->method('read') diff --git a/tests/Unit/Serialization/BinaryEncoderTest.php b/tests/Unit/Serialization/BinaryEncoderTest.php new file mode 100644 index 0000000..27a009f --- /dev/null +++ b/tests/Unit/Serialization/BinaryEncoderTest.php @@ -0,0 +1,121 @@ +encoder = new BinaryEncoder(); + $this->stream = $this->createMock(WritableStreamInterface::class); + } + + #[DataProvider('encodeLongDataProvider')] + public function testWriteLong(int $value, string $expectedEncoding): void + { + $this->stream->expects($this->once()) + ->method('write') + ->with($expectedEncoding); + + $this->encoder->writeLong($this->stream, $value); + } + + public function testWriteStringWithEmptyString(): void + { + $value = ''; + $expectedLengthEncoding = "\x00"; // length 0 encoded as long + + $this->stream->expects($this->once()) + ->method('write') + ->with($expectedLengthEncoding); + + $this->encoder->writeString($this->stream, $value); + } + + public function testWriteStringWithNonEmptyString(): void + { + $value = 'Hello, World!'; + $length = strlen($value); + $expectedLengthEncoding = "\x1A"; // length 13 encoded as long + + $this->stream->expects($this->exactly(2)) + ->method('write'); + + $this->encoder->writeString($this->stream, $value); + } + + public function testEncodeFloat(): void + { + $value = 123.45; + $result = $this->encoder->encodeFloat($value); + + // Verify it's a 4-byte float encoding + $this->assertSame(4, strlen($result)); + + // Verify the exact byte representation + $expectedBytes = "\x66\xE6\xF6\x42"; // IEEE 754 representation of 123.45 + $this->assertSame($expectedBytes, $result); + } + + public function testEncodeDouble(): void + { + $value = 123.456789; + $result = $this->encoder->encodeDouble($value); + + // Verify it's an 8-byte double encoding + $this->assertSame(8, strlen($result)); + + // Verify the exact byte representation + $expectedBytes = "\x0B\x0B\xEE\x07\x3C\xDD\x5E\x40"; // IEEE 754 representation of 123.456789 + $this->assertSame($expectedBytes, $result); + } + + /** + * @return array + */ + public static function encodeLongDataProvider(): array + { + return [ + // Negative values + [(int) -9223372036854775808, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"], + [-(1 << 62), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F"], + [-(1 << 61), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F"], + [-4294967295, "\xFD\xFF\xFF\xFF\x1F"], + [-(1 << 24), "\xFF\xFF\xFF\x0F"], + [-(1 << 16), "\xFF\xFF\x07"], + [-255, "\xFD\x03"], + [-128, "\xFF\x01"], + [-127, "\xFD\x01"], + [-10, "\x13"], + [-3, "\x05"], + [-2, "\x03"], + [-1, "\x01"], + + // Zero and positive values + [0, "\x00"], + [1, "\x02"], + [2, "\x04"], + [3, "\x06"], + [10, "\x14"], + [127, "\xFE\x01"], + [128, "\x80\x02"], + [255, "\xFE\x03"], + [1 << 16, "\x80\x80\x08"], + [1 << 24, "\x80\x80\x80\x10"], + [4294967295, "\xFE\xFF\xFF\xFF\x1F"], + [1 << 61, "\x80\x80\x80\x80\x80\x80\x80\x80\x40"], + [1 << 62, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01"], + [9223372036854775807, "\xFE\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"], + ]; + } +}