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
4 changes: 2 additions & 2 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github: [shahmal1yev, corebranch]
buy_me_a_coffee: shahmal1yev
github: [corebranch]
buy_me_a_coffee: shahmal1yev
2 changes: 1 addition & 1 deletion .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:

feature-tests:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
if: github.ref == 'refs/heads/1.x'
strategy:
matrix:
php-version: ['8.4']
Expand Down
21 changes: 18 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Created by https://www.toptal.com/developers/gitignore/api/phpstorm+all,composer
# Edit at https://www.toptal.com/developers/gitignore?templates=phpstorm+all,composer
# Created by https://www.toptal.com/developers/gitignore/api/phpstorm+all,composer,phpunit
# Edit at https://www.toptal.com/developers/gitignore?templates=phpstorm+all,composer,phpunit

### Composer ###
composer.phar
Expand Down Expand Up @@ -97,4 +97,19 @@ fabric.properties
!.idea/codeStyles
!.idea/runConfigurations

# End of https://www.toptal.com/developers/gitignore/api/phpstorm+all,composer
### PHPUnit ###
# Covers PHPUnit
# Reference: https://phpunit.de/

# Generated files
.phpunit.result.cache
.phpunit.cache

# PHPUnit
/app/phpunit.xml
/phpunit.xml

# Build data
/build/

# End of https://www.toptal.com/developers/gitignore/api/phpstorm+all,composer,phpunit
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
![Packagist Downloads](https://img.shields.io/packagist/dt/corebranch/atproto-core)
[![Discord](https://img.shields.io/badge/Discord-join%20server-5865F2?style=flat&logo=discord&logoColor=white)](https://discord.gg/tDajgYtBsZ)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcorebranch%2Fatproto-core.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcorebranch%2Fatproto-core?ref=badge_shield)


## License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcorebranch%2Fatproto-core.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcorebranch%2Fatproto-core?ref=badge_large)
26 changes: 26 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
shortenArraysForExportThreshold="10"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
displayDetailsOnPhpunitDeprecations="true"
failOnPhpunitDeprecation="true"
failOnRisky="true"
testdox="true"
failOnWarning="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>

<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
33 changes: 33 additions & 0 deletions src/CBOR/CBOR.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types=1);

/**
* This file is part of the ATProto Core package.
*
* (c) Core Branch
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

namespace ATProto\Core\CBOR;

use ATProto\Core\CBOR\MajorTypes\UnsignedInteger;

class CBOR
{
public static function encode(string|int|array $data): string
{
switch (gettype($data)) {
case 'integer':
return UnsignedInteger::encode($data);
break;
}

throw new \ValueError("Unsupported type: " . gettype($data));
}

public static function decode(string $data): int
{
return UnsignedInteger::decode((string) $data);
}
}
76 changes: 76 additions & 0 deletions src/CBOR/MajorTypes/UnsignedInteger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php declare(strict_types = 1);

/**
* This file is part of the ATProto Core package.
*
* (c) Core Branch
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

namespace ATProto\Core\CBOR\MajorTypes;

use ValueError;

class UnsignedInteger
{
public static function decode(string $data): int
{
$firstByte = ord($data[0]);
$majorType = ($firstByte >> 5) & 0x07;
$additionalInfo = $firstByte & 0x1F;

if ($majorType !== 0) {
throw new ValueError("Invalid major type for unsigned integer: $majorType");
}

if ($additionalInfo <= 23) {
$value = $additionalInfo;
} elseif ($additionalInfo === 24) {
$value = ord($data[1]);
} elseif ($additionalInfo === 25) {
$value = unpack('n', substr($data, 1, 2))[1];
} elseif ($additionalInfo === 26) {
$value = unpack('N', substr($data, 1, 4))[1];
} elseif ($additionalInfo === 27) {
$value = unpack('J', substr($data, 1, 8))[1];
} else {
throw new ValueError("Invalid additional information for unsigned integer: $additionalInfo");
}

if ($value < 0) {
throw new ValueError("Invalid CBOR data: Decoded value is negative, which is not valid for unsigned integers.");
}

return $value;
}

public static function encode(int $value): string
{
if ($value < 0) {
throw new ValueError("\$value must be greater than 0");
}

$prefixedPack = fn (string $format, ?string $prefix)
=> $prefix . pack($format, $value);

if ($value <= 0x17) {
return chr($value);
}

if ($value <= 0xFF) {
return "\x18" . chr($value);
}

if ($value <= 0xFFFF) {
return $prefixedPack('n', "\x19");
}

if ($value <= 0xFFFFFFFF) {
return $prefixedPack('N', "\x1A");
}

return $prefixedPack('J', "\x1B");
}
}
62 changes: 62 additions & 0 deletions tests/Unit/CBOR/CBORTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php declare(strict_types = 1);

/**
* This file is part of the ATProto Core package.
*
* (c) Core Branch
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

namespace Tests\Unit\CBOR;

use ATProto\Core\CBOR\CBOR;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class CBORTest extends TestCase
{
#[DataProvider('validCases')]
public function testEncode(int $data, string $expected): void
{
$encoded = CBOR::encode($data);
$this->assertSame($expected, $encoded);
}

#[DataProvider('validCases')]
public function testDecode(int $expected, string $data): void
{
$actual = CBOR::decode($data);

$this->assertSame($expected, $actual);
}

/**
* @return array[]
*/
public static function validCases(): array
{
return [
// Unsigned integer test cases
[1, hex2bin('01')], // 1 encoded as CBOR unsigned integer
[10, hex2bin('0a')], // 10 encoded as CBOR unsigned integer

// // String test cases
// [['hello'], hex2bin('6568656c6c6f')], // "hello" encoded as CBOR text string
//
// // Boolean test cases
// [[true], hex2bin('f5')], // true encoded as CBOR special type
// [[false], hex2bin('f4')], // false encoded as CBOR special type
//
// // Null test case
// [[null], hex2bin('f6')], // null encoded as CBOR special type
//
// // Array test cases
// [[[1, 2, 3]], hex2bin('83010203')], // [1, 2, 3] encoded as CBOR array
//
// // Map test cases
// [[['key' => 'value']], hex2bin('a1636b65796576616c7565')], // {"key": "value"} encoded as CBOR map
];
}
}
81 changes: 81 additions & 0 deletions tests/Unit/CBOR/MajorTypes/UnsignedIntegerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);

/**
* This file is part of the ATProto Core package.
*
* (c) Core Branch
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

namespace Tests\Unit\CBOR\MajorTypes;

use ATProto\Core\CBOR\MajorTypes\UnsignedInteger;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class UnsignedIntegerTest extends TestCase
{
#[DataProvider('provideCases')]
public function testEncodeCanEncodeCorrectly(int $case, string $expected): void
{
$actual = bin2hex(UnsignedInteger::encode($case));
$expected = bin2hex($expected);

$this->assertSame($actual, $expected);
}

#[DataProvider('provideCases')]
public function testDecode(int $expected, string $case): void
{
$actual = bin2hex((string) UnsignedInteger::decode($case));
$expected = bin2hex((string) $expected);

$this->assertSame($expected, $actual);
}

public function testEncodeThrowsAnExceptionForValueGreaterThanIntMax(): void
{
$this->expectException(\ValueError::class);
$this->expectExceptionMessage("Invalid CBOR data: Decoded value is negative, which is not valid for unsigned integers.");

UnsignedInteger::decode("\x1B\x80\x00\x00\x00\x00\x00\x00\x00"); // PHP_INT_MAX + 1
}

#[DataProvider('provideNegativeCases')]
public function testDecodeThrowsAnExceptionWhenPassedInvalidValue(string $case): void
{
$this->expectException(\ValueError::class);
$this->expectExceptionMessage("Invalid major type for unsigned integer: ");

UnsignedInteger::decode($case);
}

public static function provideNegativeCases(): array
{
$arr = [];

for ($i = -1; $i >= -100; $i--) {
$arr[] = [hex2bin(dechex($i))];
}

return $arr;
}

public static function provideCases(): iterable
{
return [
[0, "\x00"],
[1, "\x01"],
[10, "\x0a"],
[23, "\x17"],
[24, "\x18\x18"],
[25, "\x18\x19"],
[100, "\x18\x64"],
[1000, "\x19\x03\xe8"],
[1000000, "\x1a\x00\x0f\x42\x40"],
[1000000000000, "\x1b\x00\x00\x00\xe8\xd4\xa5\x10\x00"],
];
}
}
Loading