Skip to content

Commit b1d6967

Browse files
authored
Merge pull request #3 from corebranch/apc-3
[APC-3] CBOR Implementation (TS)
2 parents 54cb75c + eea8a03 commit b1d6967

File tree

6 files changed

+206
-13
lines changed

6 files changed

+206
-13
lines changed

src/CBOR/CBOR.php

+14-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace ATProto\Core\CBOR;
1313

14+
use ATProto\Core\CBOR\MajorTypes\TextString;
1415
use ATProto\Core\CBOR\MajorTypes\UnsignedInteger;
1516

1617
class CBOR
@@ -21,13 +22,24 @@ public static function encode(string|int|array $data): string
2122
case 'integer':
2223
return UnsignedInteger::encode($data);
2324
break;
25+
case 'string':
26+
return TextString::encode($data);
27+
break;
2428
}
2529

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

29-
public static function decode(string $data): int
33+
public static function decode(string $data): int|string
3034
{
31-
return UnsignedInteger::decode((string) $data);
35+
if (TextString::validate($data)) {
36+
return TextString::decode($data);
37+
}
38+
39+
if (UnsignedInteger::validate($data)) {
40+
return UnsignedInteger::decode($data);
41+
}
42+
43+
throw new \ValueError("Unsupported type.");
3244
}
3345
}

src/CBOR/MajorTypes/TextString.php

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php declare(strict_types = 1);
2+
3+
/**
4+
* This file is part of the ATProto Core package.
5+
*
6+
* (c) Core Branch
7+
*
8+
* This source code is licensed under the MIT license found in the
9+
* LICENSE file in the root directory of this source tree.
10+
*/
11+
12+
namespace ATProto\Core\CBOR\MajorTypes;
13+
14+
class TextString
15+
{
16+
public static function encode(string $input): string
17+
{
18+
$length = strlen($input);
19+
20+
if ($length <= 23) {
21+
$header = chr((0x03 << 5) | $length);
22+
} elseif ($length <= 0xFF) {
23+
$header = chr((0x03 << 5) | 24) . chr($length);
24+
} elseif ($length <= 0xFFFF) {
25+
$header = chr((0x03 << 5) | 25) . pack('n', $length);
26+
} elseif ($length <= 0xFFFFFFFF) {
27+
$header = chr((0x03 << 5) | 26) . pack('N', $length);
28+
} else {
29+
$header = chr((0x03 << 5) | 27) . pack('J', $length);
30+
}
31+
32+
return $header . $input;
33+
}
34+
35+
public static function decode(string $input): string
36+
{
37+
if (! self::validate($input)) {
38+
throw new \ValueError('Invalid CBOR TextString major type.');
39+
}
40+
41+
$additionalInfo = ord($input[0]) & 0x1F;
42+
$offset = 1;
43+
44+
if ($additionalInfo <= 23) {
45+
$length = $additionalInfo;
46+
} elseif ($additionalInfo === 24) {
47+
$length = ord($input[$offset]);
48+
$offset += 1;
49+
} elseif ($additionalInfo === 25) {
50+
$length = unpack('n', substr($input, $offset, 2))[1];
51+
$offset += 2;
52+
} elseif ($additionalInfo === 26) {
53+
$length = unpack('N', substr($input, $offset, 4))[1];
54+
$offset += 4;
55+
} elseif ($additionalInfo === 27) {
56+
$length = unpack('J', substr($input, $offset, 8))[1];
57+
$offset += 8;
58+
} else {
59+
throw new \ValueError('Invalid CBOR TextString length information.');
60+
}
61+
62+
$text = substr($input, $offset, $length);
63+
64+
if (strlen($text) !== $length) {
65+
throw new \ValueError('Invalid CBOR TextString length mismatch.');
66+
}
67+
68+
return $text;
69+
}
70+
71+
public static function validate(string $input): bool
72+
{
73+
return ((ord($input[0]) >> 5) & 0x07) === 0x03;
74+
}
75+
}

src/CBOR/MajorTypes/UnsignedInteger.php

+9-6
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@ class UnsignedInteger
1717
{
1818
public static function decode(string $data): int
1919
{
20-
$firstByte = ord($data[0]);
21-
$majorType = ($firstByte >> 5) & 0x07;
22-
$additionalInfo = $firstByte & 0x1F;
23-
24-
if ($majorType !== 0) {
25-
throw new ValueError("Invalid major type for unsigned integer: $majorType");
20+
if (! self::validate($data)) {
21+
throw new ValueError("Invalid major type for unsigned integer.");
2622
}
2723

24+
$additionalInfo = ord($data[0]) & 0x1F;
25+
2826
if ($additionalInfo <= 23) {
2927
$value = $additionalInfo;
3028
} elseif ($additionalInfo === 24) {
@@ -73,4 +71,9 @@ public static function encode(int $value): string
7371

7472
return $prefixedPack('J', "\x1B");
7573
}
74+
75+
public static function validate(string $input): bool
76+
{
77+
return ((ord($input[0]) >> 5) & 0x07) === 0x00;
78+
}
7679
}

tests/Unit/CBOR/CBORTest.php

+20-4
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,36 @@
1818
class CBORTest extends TestCase
1919
{
2020
#[DataProvider('validCases')]
21-
public function testEncode(int $data, string $expected): void
21+
public function testEncode(int|string $data, string $expected): void
2222
{
2323
$encoded = CBOR::encode($data);
2424
$this->assertSame($expected, $encoded);
2525
}
2626

2727
#[DataProvider('validCases')]
28-
public function testDecode(int $expected, string $data): void
28+
public function testDecode(int|string $expected, string $data): void
2929
{
3030
$actual = CBOR::decode($data);
3131

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

35+
public function testCBORDecodeThrowsExceptionWhenPassedUnsupportedType(): void
36+
{
37+
$this->expectException(\ValueError::class);
38+
$this->expectExceptionMessage('Unsupported type.');
39+
40+
CBOR::decode("\x80"); // CBOR array
41+
}
42+
43+
public function testCBOREncodeThrowsExceptionWhenPassedUnsupportedType(): void
44+
{
45+
$this->expectException(\ValueError::class);
46+
$this->expectExceptionMessage('Unsupported type: array');
47+
48+
CBOR::encode(array());
49+
}
50+
3551
/**
3652
* @return array[]
3753
*/
@@ -42,8 +58,8 @@ public static function validCases(): array
4258
[1, hex2bin('01')], // 1 encoded as CBOR unsigned integer
4359
[10, hex2bin('0a')], // 10 encoded as CBOR unsigned integer
4460

45-
// // String test cases
46-
// [['hello'], hex2bin('6568656c6c6f')], // "hello" encoded as CBOR text string
61+
// String test cases
62+
['hello', hex2bin('6568656C6C6F')], // "hello" encoded as CBOR text string
4763
//
4864
// // Boolean test cases
4965
// [[true], hex2bin('f5')], // true encoded as CBOR special type
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php declare(strict_types = 1);
2+
3+
/**
4+
* This file is part of the ATProto Core package.
5+
*
6+
* (c) Core Branch
7+
*
8+
* This source code is licensed under the MIT license found in the
9+
* LICENSE file in the root directory of this source tree.
10+
*/
11+
12+
namespace Tests\Unit\CBOR\MajorTypes;
13+
14+
use ATProto\Core\CBOR\MajorTypes\TextString;
15+
use PHPUnit\Framework\Attributes\DataProvider;
16+
use PHPUnit\Framework\TestCase;
17+
18+
class TextStringTest extends TestCase
19+
{
20+
#[DataProvider('provideValidCases')]
21+
public function testEncodeProducesCorrectCBORRepresentation(string $input, string $expectedHeader): void
22+
{
23+
$actual = bin2hex(TextString::encode($input));
24+
$expected = bin2hex($expectedHeader . $input);
25+
26+
$this->assertSame($expected, $actual);
27+
}
28+
29+
#[DataProvider('provideValidCases')]
30+
public function testDecodeExtractsOriginalStringFromCBORRepresentation(string $input, string $header): void
31+
{
32+
$encoded = $header . $input;
33+
34+
$actual = TextString::decode($encoded);
35+
$expected = $input;
36+
37+
$this->assertSame($expected, $actual);
38+
}
39+
40+
public static function provideValidCases(): array
41+
{
42+
return [
43+
["f", "\x61"],
44+
["fo", "\x62"],
45+
["foo", "\x63"],
46+
["foob", "\x64"],
47+
["fooba", "\x65"],
48+
["foobar", "\x66"],
49+
[
50+
"This is a longer string. This is a longer string. This is a longer string. This is a longer string.",
51+
"\x78\x63"
52+
],
53+
[
54+
"This is a longer string. This is a longer string. This is a longer string. This is a longer string.
55+
This is a longer string. This is a longer string. This is a longer string. This is a longer string.
56+
This is a longer string. This is a longer string. This is a longer string. This is a longer string.",
57+
"\x79\x01\x4D"
58+
]
59+
];
60+
}
61+
62+
public function testDecodeThrowsExceptionForInvalidMajorType(): void
63+
{
64+
$this->expectException(\ValueError::class);
65+
$this->expectExceptionMessage("Invalid CBOR TextString major type.");
66+
67+
TextString::decode("\x0C");
68+
}
69+
70+
public function testDecodeThrowsExceptionForLengthMismatch(): void
71+
{
72+
$this->expectException(\ValueError::class);
73+
$this->expectExceptionMessage("Invalid CBOR TextString length mismatch.");
74+
75+
// Encoded length is 5, but actual length is 4
76+
TextString::decode("\x65\x66\x6F\x6F\x62");
77+
}
78+
79+
public function testDecodeThrowsExceptionForInvalidAdditionalInformation(): void
80+
{
81+
$this->expectException(\ValueError::class);
82+
$this->expectExceptionMessage("Invalid CBOR TextString length information.");
83+
84+
// Additional info 28 is invalid for text strings
85+
TextString::decode("\x7C\x01");
86+
}
87+
}

tests/Unit/CBOR/MajorTypes/UnsignedIntegerTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function testEncodeThrowsAnExceptionForValueGreaterThanIntMax(): void
4747
public function testDecodeThrowsAnExceptionWhenPassedInvalidValue(string $case): void
4848
{
4949
$this->expectException(\ValueError::class);
50-
$this->expectExceptionMessage("Invalid major type for unsigned integer: ");
50+
$this->expectExceptionMessage("Invalid major type for unsigned integer.");
5151

5252
UnsignedInteger::decode($case);
5353
}

0 commit comments

Comments
 (0)