Skip to content

Commit

Permalink
feat: implemented Byter
Browse files Browse the repository at this point in the history
  • Loading branch information
petrknap committed Apr 17, 2024
1 parent d196bd9 commit 50d1d91
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 15 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Library for work with binary data and objects

Simple library for work with binary data and objects in PHP.
See the examples below for more information, or check out [`Encoder`](./src/Encoder.php), [`Decoder`](./src/Decoder.php) and [`Serializer`](./src/Serializer.php).
See the examples below for more information, or check out [`Encoder`](./src/Encoder.php), [`Decoder`](./src/Decoder.php), [`Serializer`](./src/Serializer.php) and [`Byter`](./src/Byter.php).

```php
use PetrKnap\Binary\Binary;
Expand All @@ -26,6 +26,22 @@ $unserialized = Binary::unserialize($serialized);
printf('Data was serialized into `%s` %s.', base64_encode($serialized), $unserialized === $data ? 'successfully' : 'unsuccessfully');
```

```php
use PetrKnap\Binary\Binary;

$data = base64_decode('hmlpFnFwbchsoQARSibVpfbWVfuwAHLbGxjFl9eC8fiGaWkWcXBtyGyhABFKJtWl9tZV+7AActsbGMWX14Lx+A==');
$sha1 = sha1($data, binary: true);
$md5 = md5($data, binary: true);
$unbitten = Binary::unbite($sha1, $md5, $data);
[$sha1Bite, $md5Bite, $dataBite] = Binary::bite($unbitten, 20, 16);

printf(
'Hashes and data was unbitten into `%s` %s.',
base64_encode($unbitten),
$sha1Bite === $sha1 && $md5Bite === $md5 && $dataBite === $data ? 'successfully' : 'unsuccessfully',
);
```

---

Run `composer require petrknap/binary` to install it.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"name": "petrknap/binary",
"require": {
"php": ">=8.1",
"beberlei/assert": "^3.3",
"petrknap/shorts": "^2.0"
},
"require-dev": {
Expand All @@ -65,7 +66,7 @@
},
"suggest": {
"ext-igbinary": "Required to serialize data via igbinary",
"ext-mbstring": "Required to check checksum",
"ext-mbstring": "Required to bite bytes",
"ext-zlib": "Required to compress data"
}
}
16 changes: 16 additions & 0 deletions src/Binary.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,20 @@ public static function unserialize(string $data): mixed
{
return (new Serializer())->unserialize(serialized: $data);
}

/**
* @see Byter::bite()
*/
public static function bite(string $data, int $size1, int ...$sizeN): array
{
return (new Byter())->bite($data, $size1, ...$sizeN);
}

/**
* @see Byter::unbite()
*/
public static function unbite(string $bite1, string ...$biteN): string
{
return (new Byter())->unbite($bite1, ...$biteN);
}
}
71 changes: 71 additions & 0 deletions src/Byter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace PetrKnap\Binary;

use Assert\Assert;
use PetrKnap\Shorts\HasRequirements;
use Throwable;

class Byter
{
use HasRequirements;

public const ENCODING = '8bit';

public function __construct()
{
self::checkRequirements(
functions: [
'mb_strcut',
'mb_strlen',
],
);
}

/**
* Bites data into bites of specified sizes
*
* @param int $size1 bytes
* @param int $sizeN bytes
*
* @return array<string>
*
* @throws Exception\CouldNotBiteData
*/
public function bite(string $data, int $size1, int ...$sizeN): array
{
try {
Assert::that($this->size($data))->greaterThan(1, 'Too short data');
$remains = $data;
$bites = [];
foreach ([$size1, ...$sizeN] as $size) {
Assert::that($size)->greaterThan(0, 'Size must be positive');
$bites[] = mb_strcut($remains, 0, $size, encoding: self::ENCODING);
$remains = mb_strcut($remains, $size, encoding: self::ENCODING);
}
return [...$bites, $remains];
} catch (Throwable $reason) {
throw new Exception\CouldNotBiteData(__METHOD__, $data, $reason);
}
}

/**
* Backward version of {@see self::bite()}
*
* @link https://en.wikipedia.org/wiki/Backwards_(Red_Dwarf)
*/
public function unbite(string $bite1, string ...$biteN): string
{
return implode([$bite1, ...$biteN]);
}

/**
* @return int bytes
*/
public function size(string $data): int
{
return mb_strlen($data, self::ENCODING);
}
}
20 changes: 7 additions & 13 deletions src/Coder/Checksum.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace PetrKnap\Binary\Coder;

use PetrKnap\Shorts\HasRequirements;
use PetrKnap\Binary\Byter;

/**
* @see hash()
Expand All @@ -13,20 +13,14 @@
*/
final class Checksum extends Coder
{
use HasRequirements;

public const ALGORITHM = 'crc32';

private string $algorithm;
private readonly Byter $byter;

public function __construct()
{
self::checkRequirements(
functions: [
'mb_strlen',
'mb_strcut',
],
);
$this->byter = new Byter();
}

public function encode(string $decoded, ?string $algorithm = null): string
Expand All @@ -44,14 +38,14 @@ public function decode(string $encoded, ?string $algorithm = null): string
protected function doEncode(string $decoded): string
{
$checksum = hash($this->algorithm, $decoded, binary: true);
return $decoded . $checksum;
return $this->byter->unbite($decoded, $checksum);
}

protected function doDecode(string $encoded): string
{
$checksumLength = mb_strlen(hash($this->algorithm, '', binary: true), encoding: '8bit');
$dataLength = mb_strlen($encoded, encoding: '8bit') - $checksumLength;
$decoded = mb_strcut($encoded, 0, $dataLength, encoding: '8bit');
$checksumLength = $this->byter->size(hash($this->algorithm, '', binary: true));
$dataLength = $this->byter->size($encoded) - $checksumLength;
[$decoded] = $this->byter->bite($encoded, $dataLength);
if ($this->doEncode($decoded) !== $encoded) {
throw new Exception\CouldNotDecodeData(__METHOD__, $encoded);
}
Expand Down
9 changes: 9 additions & 0 deletions src/Exception/ByterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace PetrKnap\Binary\Exception;

interface ByterException extends BinaryException
{
}
14 changes: 14 additions & 0 deletions src/Exception/CouldNotBiteData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace PetrKnap\Binary\Exception;

use PetrKnap\Shorts\Exception\CouldNotProcessData;

/**
* @extends CouldNotProcessData<string>
*/
final class CouldNotBiteData extends CouldNotProcessData implements ByterException
{
}
56 changes: 56 additions & 0 deletions tests/ByterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php declare(strict_types=1);

namespace PetrKnap\Binary;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class ByterTest extends TestCase
{
public function testBitesData(): void
{
self::assertSame(
[hex2bin('01'), hex2bin('0203'), hex2bin('040506'), hex2bin('070809')],
(new Byter())->bite(self::getData(), 1, 2, 3),
);
}

#[DataProvider('dataBiteThrows')]
public function testBiteThrows(string $data, int $size)
{
self::expectException(Exception\CouldNotBiteData::class);

(new Byter())->bite($data, $size);
}

public static function dataBiteThrows(): array
{
return [
'negative size' => [self::getData(), -1],
'zero size' => [self::getData(), 0],
'empty data' => ['', 1],
'short data' => [hex2bin('01'), 1],
];
}

public function testUnbitesBites(): void
{
self::assertSame(
self::getData(),
(new Byter())->unbite(hex2bin('01'), hex2bin('0203'), hex2bin('040506'), hex2bin('070809')),
);
}

public function testReturnsSizeOfData(): void
{
self::assertSame(
9,
(new Byter())->size(self::getData()),
);
}

private static function getData(): string
{
return hex2bin('010203040506070809');
}
}
1 change: 1 addition & 0 deletions tests/ReadmeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static function getExpectedOutputsOfPhpExamples(): iterable
return [
'coder' => 'Data was coded into `a8vMFCssyD2Rs5BB0Evt6tJv10J_b2Aoui0tcXT69aaPP9oIyB-fLeAHAA` successfully.',
'serializer' => 'Data was serialized into `S7QysqoutjKxUiqpLEhVsi62srRSysxNTE/VL8hLB/GBUimJJYkgpoWxlVJngJ87L5cUFwMDA6+nh0sQkGYEYQ42ICkveqQTxCkOcndiWHdO5iVYlYtjiER48o/9Ux7aM7C9Z1qixnnFBCjB4Onq57LOKaFJyboWAA==` successfully.',
'byter' => 'Hashes and data was unbitten into `IoPwxcGHZQM0gfF966vHI3kleehoRKHtC32Xh30RDlg5E026hmlpFnFwbchsoQARSibVpfbWVfuwAHLbGxjFl9eC8fiGaWkWcXBtyGyhABFKJtWl9tZV+7AActsbGMWX14Lx+A==` successfully.',
];
}
}

0 comments on commit 50d1d91

Please sign in to comment.