Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 19 additions & 7 deletions src/Serialization/BinaryEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
20 changes: 10 additions & 10 deletions tests/Unit/Deserialization/BinaryDecoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand Down
121 changes: 121 additions & 0 deletions tests/Unit/Serialization/BinaryEncoderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace Auxmoney\Avro\Tests\Unit\Serialization;

use Auxmoney\Avro\Contracts\WritableStreamInterface;
use Auxmoney\Avro\Serialization\BinaryEncoder;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class BinaryEncoderTest extends TestCase
{
private BinaryEncoder $encoder;
private WritableStreamInterface&\PHPUnit\Framework\MockObject\MockObject $stream;

protected function setUp(): void
{
$this->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<int, array{0: int, 1: string}>
*/
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"],
];
}
}