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 e45d925
Show file tree
Hide file tree
Showing 9 changed files with 203 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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,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"
}
}
18 changes: 18 additions & 0 deletions src/Binary.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,22 @@ public static function unserialize(string $data): mixed
{
return (new Serializer())->unserialize(serialized: $data);
}

/**
* @see Byter::bite()
*
* @return array<string>
*/
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);
}
}
75 changes: 75 additions & 0 deletions src/Byter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace PetrKnap\Binary;

use PetrKnap\Shorts\HasRequirements;
use RuntimeException;

class Byter
{
use HasRequirements;

private const ENCODING = '8bit';

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

/**
* @param int $size1 size of bite in bytes; if negative, bites from the end
*
* @return array<string> bites of specified sizes; and remains, if any
*
* @throws Exception\CouldNotBiteData
*/
public function bite(string $data, int $size1, int ...$sizeN): array
{
$remains = $data;
$bites = [];
foreach ([$size1, ...$sizeN] as $size) {
if (abs($size) > $this->size($remains)) {
throw new Exception\CouldNotBiteData(__METHOD__, $data, new RuntimeException(
'Remains are smaller than bite',
));
}
$bite = mb_strcut($remains, 0, $size, encoding: self::ENCODING);
$remains = mb_strcut($remains, $size, encoding: self::ENCODING);
if ($size < 0) {
$bites[] = $remains;
$remains = $bite;
} else {
$bites[] = $bite;
}
}
if ($remains !== '') {
$bites[] = $remains;
}
return $bites;
}

/**
* 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 size in bytes
*/
public function size(string $data): int
{
return mb_strlen($data, encoding: self::ENCODING);
}
}
19 changes: 6 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,13 @@ 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));
[,$decoded] = $this->byter->bite($encoded, -$checksumLength);
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
{
}
62 changes: 62 additions & 0 deletions tests/ByterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php declare(strict_types=1);

namespace PetrKnap\Binary;

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

final class ByterTest extends TestCase
{
#[DataProvider('dataBitesData')]
public function testBitesData(array $expected, array $sizes): void
{
self::assertSame(
$expected,
(new Byter())->bite(self::getData(), ...$sizes),
);
}

public static function dataBitesData(): array
{
return [
'one left bite' => [[hex2bin('0102'), hex2bin('030405')], [2]],
'one right bite' => [[hex2bin('0405'), hex2bin('010203')], [-2]],
'empty bite' => [['', hex2bin('0102030405')], [0]],
'full bite' => [[hex2bin('0102030405')], [5]],
'many bites' => [[hex2bin('0102'), hex2bin('0405'), '', hex2bin('03')], [2, -2, 0, 1]],
];
}

public function testBiteThrowsWhenThereIsNotEnoughData(): void
{
self::expectException(Exception\CouldNotBiteData::class);

$data = self::getData();
$byter = new Byter();
$byter->bite(
$data,
$byter->size($data) + 1,
);
}

public function testUnbitesBites(): void
{
self::assertSame(
self::getData(),
(new Byter())->unbite(hex2bin('0102'), hex2bin('030405')),
);
}

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

private static function getData(): string
{
return hex2bin('0102030405');
}
}
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 e45d925

Please sign in to comment.