Skip to content

Commit

Permalink
[Encoding] introduce encoding component (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
azjezz committed Feb 16, 2021
1 parent 78289a1 commit 4caeb0a
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 0 deletions.
49 changes: 49 additions & 0 deletions src/Psl/Encoding/Base64/decode.php
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Psl\Encoding\Base64;

use Psl\Encoding\Exception;
use Psl\Str;
use Psl\Type;

use function base64_decode;
use function preg_match;

/**
* Decode a base64-encoded string into raw binary.
*
* Base64 character set:
* [A-Z] [a-z] [0-9] + /
* 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
*
* @psalm-pure
*
* @throws Exception\RangeException If the encoded string contains characters outside
* the base64 characters range.
* @throws Exception\IncorrectPaddingException If the encoded string has an incorrect padding.
*/
function decode(string $base64): string
{
if (!preg_match('%^[a-zA-Z0-9/+]*={0,2}$%', $base64)) {
throw new Exception\RangeException(
'The given base64 string contains characters outside the base64 range.'
);
}

/** @psalm-suppress MissingThrowsDocblock */
$remainder = Str\length($base64) % 4;
if (0 !== $remainder) {
throw new Exception\IncorrectPaddingException(
'The given base64 string has incorrect padding.'
);
}

/**
* @psalm-suppress ImpureFunctionCall
* @psalm-suppress ImpureMethodCall
* @psalm-suppress MissingThrowsDocblock
*/
return Type\string()->assert(base64_decode($base64, true));
}
21 changes: 21 additions & 0 deletions src/Psl/Encoding/Base64/encode.php
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Psl\Encoding\Base64;

use function base64_encode;

/**
* Convert a binary string into a base64-encoded string.
*
* Base64 character set:
* [A-Z] [a-z] [0-9] + /
* 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
*
* @psalm-pure
*/
function encode(string $binary): string
{
return base64_encode($binary);
}
11 changes: 11 additions & 0 deletions src/Psl/Encoding/Exception/ExceptionInterface.php
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Psl\Encoding\Exception;

use Psl\Exception;

interface ExceptionInterface extends Exception\ExceptionInterface
{
}
11 changes: 11 additions & 0 deletions src/Psl/Encoding/Exception/IncorrectPaddingException.php
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Psl\Encoding\Exception;

use Psl\Exception;

final class IncorrectPaddingException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}
9 changes: 9 additions & 0 deletions src/Psl/Encoding/Exception/RangeException.php
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Psl\Encoding\Exception;

final class RangeException extends \RangeException implements ExceptionInterface
{
}
39 changes: 39 additions & 0 deletions src/Psl/Encoding/Hex/decode.php
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Psl\Encoding\Hex;

use Psl\Encoding\Exception;
use Psl\Str;

/**
* Convert a hexadecimal string into a binary string.
*
* Hex ( Base16 ) character set:
* [0-9] [a-f] [A-F]
* 0x30-0x39, 0x61-0x66, 0x41-0x46
*
* @psalm-pure
*
* @throws Exception\RangeException If the hexadecimal string contains characters outside the base16 range,
* or an odd number of characters.
*/
function decode(string $hexadecimal): string
{
if (!ctype_xdigit($hexadecimal)) {
throw new Exception\RangeException(
'The given hexadecimal string contains characters outside the base16 range.'
);
}

/** @psalm-suppress MissingThrowsDocblock */
$hex_len = Str\length($hexadecimal, '8bit');
if (($hex_len & 1) !== 0) {
throw new Exception\RangeException(
'Expected an even number of hexadecimal characters.',
);
}

return hex2bin($hexadecimal);
}
19 changes: 19 additions & 0 deletions src/Psl/Encoding/Hex/encode.php
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Psl\Encoding\Hex;

/**
* Convert a binary string into a hexadecimal string.
*
* Hex ( Base16 ) character set:
* [0-9] [a-f]
* 0x30-0x39, 0x61-0x66
*
* @psalm-pure
*/
function encode(string $binary): string
{
return bin2hex($binary);
}
7 changes: 7 additions & 0 deletions src/Psl/Internal/Loader.php
Expand Up @@ -433,6 +433,10 @@ final class Loader
'Psl\Str\Grapheme\before_ci',
'Psl\Str\Grapheme\before_last',
'Psl\Str\Grapheme\before_last_ci',
'Psl\Encoding\Base64\encode',
'Psl\Encoding\Base64\decode',
'Psl\Encoding\Hex\encode',
'Psl\Encoding\Hex\decode',
];

public const INTERFACES = [
Expand All @@ -450,6 +454,7 @@ final class Loader
'Psl\Observer\SubjectInterface',
'Psl\Observer\ObserverInterface',
'Psl\Result\ResultInterface',
'Psl\Encoding\Exception\ExceptionInterface',
];

public const TRAITS = [
Expand Down Expand Up @@ -495,6 +500,8 @@ final class Loader
'Psl\Json\Exception\DecodeException',
'Psl\Json\Exception\EncodeException',
'Psl\Hash\Context',
'Psl\Encoding\Exception\IncorrectPaddingException',
'Psl\Encoding\Exception\RangeException',
];

private const TYPE_CONSTANTS = 1;
Expand Down
2 changes: 2 additions & 0 deletions src/Psl/Internal/validate_offset.php
Expand Up @@ -15,6 +15,8 @@
* @codeCoverageIgnore
*
* @throws Psl\Exception\InvariantViolationException If the offset is out-of-bounds.
*
* @internal
*/
function validate_offset(int $offset, int $length): int
{
Expand Down
2 changes: 2 additions & 0 deletions src/Psl/Internal/validate_offset_lower_bound.php
Expand Up @@ -13,6 +13,8 @@
* @psalm-pure
*
* @throws Psl\Exception\InvariantViolationException If $offset is out-of-bounds.
*
* @internal
*/
function validate_offset_lower_bound(int $offset, int $length): int
{
Expand Down
43 changes: 43 additions & 0 deletions tests/Psl/Encoding/Base64Test.php
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Encoding;

use PHPUnit\Framework\TestCase;
use Psl\Encoding\Base64;
use Psl\Encoding\Exception;
use Psl\SecureRandom;

final class Base64Test extends TestCase
{
/**
* @dataProvider provideRandomBytes
*/
public function testEncodeAndDecode(string $random): void
{
$encoded = Base64\encode($random);
static::assertSame($random, Base64\decode($encoded));
}

public function testDecodeThrowsForCharactersOutsideTheBase64Range(): void
{
$this->expectException(Exception\RangeException::class);

Base64\decode('@~==');
}

public function testDecodeThrowsForIncorrectPadding(): void
{
$this->expectException(Exception\IncorrectPaddingException::class);

Base64\decode('ab');
}

public function provideRandomBytes(): iterable
{
for ($i = 1; $i < 128; ++$i) {
yield [SecureRandom\bytes($i)];
}
}
}
48 changes: 48 additions & 0 deletions tests/Psl/Encoding/HexTest.php
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Encoding;

use PHPUnit\Framework\TestCase;
use Psl\Encoding\Exception;
use Psl\Encoding\Hex;
use Psl\SecureRandom;

use function bin2hex;

final class HexTest extends TestCase
{
/**
* @dataProvider provideRandomBytes
*/
public function testRandom(string $random): void
{
$enc = Hex\encode($random);
static::assertSame($random, Hex\decode($enc));
static::assertSame(bin2hex($random), $enc);
$enc = Hex\encode($random);
static::assertSame($random, Hex\decode($enc));
}

public function testDecodeThrowsForCharactersOutsideTheHexRange(): void
{
$this->expectException(Exception\RangeException::class);

Hex\decode('gf');
}

public function testDecodeThrowsForAnOddNumberOfCharacters(): void
{
$this->expectException(Exception\RangeException::class);

Hex\decode('f');
}

public function provideRandomBytes(): iterable
{
for ($i = 1; $i < 128; ++$i) {
yield [SecureRandom\bytes($i)];
}
}
}

0 comments on commit 4caeb0a

Please sign in to comment.