Skip to content
Closed
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.3] 2026-04-06

### Changed

* Accept int8/16/32 as `int` uint8/16/32 as `non-negative-int` to simplify upstream libraries.

## [0.3.2] 2026-04-06

### Deprecated
Expand Down
84 changes: 46 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,75 @@

[![PHP Version Requirement](https://img.shields.io/packagist/dependency-v/thesis/endian/php)](https://packagist.org/packages/thesis/endian)
[![GitHub Release](https://img.shields.io/github/v/release/thesisphp/endian)](https://github.com/thesisphp/endian/releases)
[![Code Coverage](https://codecov.io/gh/thesis-php/endian/branch/0.1.x/graph/badge.svg)](https://codecov.io/gh/thesis-php/endian/tree/0.1.x)
[![Code Coverage](https://codecov.io/gh/thesis-php/endian/branch/0.3.x/graph/badge.svg)](https://codecov.io/gh/thesis-php/endian/tree/0.3.x)

Pack and unpack binary integers and floats in any byte order.

## Installation

```shell
composer require thesis/endian
```

## Read/write in any byte order:

1. In `network` (`big endian`) byte order.
## Usage

```php
use Thesis\Endian;

echo Endian\Order::Network->unpackInt32(
Endian\Order::Network->packInt32(-200),
); // -200
// Big endian (= network byte order)
$bytes = Endian\Order::Big->packInt32(-200);
$value = Endian\Order::Big->unpackInt32($bytes); // -200

// Little endian
$bytes = Endian\Order::Little->packFloat(2.2);
$value = Endian\Order::Little->unpackFloat($bytes); // 2.2

// Native byte order of the current machine
$order = Endian\Order::native(); // Order::Big or Order::Little
```

2. In `big endian` byte order.
`Order::Network` is an alias for `Order::Big`.

```php
use Thesis\Endian;
## Design decisions

echo Endian\Order::Big->unpackInt8(
Endian\Order::Big->packInt8(17),
); // 17
```
### No narrow int types on input

3. In `little endian` byte order.
Pack methods accept plain `int` rather than narrow PHPStan types like `Int8` or `Int32`. This is intentional: requiring
callers to carry and assert narrow types would flood driver code with redundant checks and type imports. Thus, we've
decided to keep the validation on our side.

```php
use Thesis\Endian;
Bounds are checked with [`assert()`](https://www.php.net/manual/en/function.assert.php), which means zero overhead
in production when assertions are disabled ([`zend.assertions = -1`](https://www.php.net/manual/en/info.configuration.php#ini.zend.assertions)).

echo Endian\Order::Little->unpackFloat(
Endian\Order::Little->packFloat(2.2),
); // 2.2
```
### No object wrappers for 8/16/32-bit

8/16/32-bit integers are represented as native PHP `int`, not value objects. This avoids allocation and method-call
overhead on every pack/unpack — important in tight loops typical of binary protocol parsing.

### 64-bit integers

4. In `native endian` byte order.
64-bit values use [`BcMath\Number`](https://www.php.net/manual/en/class.bcmath-number.php)
to handle the full unsigned range beyond `PHP_INT_MAX`:

```php
use Thesis\Endian;
use BcMath\Number;

echo Endian\Order::native()
->unpackInt64(
Endian\Order::native()->packInt64(new Number('9223372036854775807')),
)
->value; // 9223372036854775807
$bytes = Endian\Order::Big->packUint64(new Number('18446744073709551615'));
$value = Endian\Order::Big->unpackUint64($bytes); // 18446744073709551615
```

### Supported types:
- [x] `int8`
- [x] `uint8`
- [x] `int16`
- [x] `uint16`
- [x] `int32`
- [x] `uint32`
- [x] `int64`
- [x] `uint64`
- [x] `float`
- [x] `double`
## Supported types

| Type | PHP type | Range |
|----------|-----------------|-----------------------------------------------------------------------------------------|
| `int8` | `int` | `−128 .. 127` |
| `uint8` | `int` | `0 .. 255` |
| `int16` | `int` | `−32 768 .. 32767` |
| `uint16` | `int` | `0 .. 65535` |
| `int32` | `int` | `−2147483648 .. 2147483647` |
| `uint32` | `int` | `0 .. 4294967295` |
| `int64` | `BcMath\Number` | `−2⁶³ .. 2⁶³−1` |
| `uint64` | `BcMath\Number` | `0 .. 2⁶⁴−1` |
| `float` | `float` | [32-bit IEEE 754](https://en.wikipedia.org/wiki/Single-precision_floating-point_format) |
| `double` | `float` | [64-bit IEEE 754](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) |
108 changes: 108 additions & 0 deletions src/Internal/Ints.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

declare(strict_types=1);

namespace Thesis\Endian\Internal;

use BcMath\Number;

/**
* @internal
*
* PHP does not allow `new` expressions in class constant initializers,
* so this namespace-level constant is used as a workaround to define
* BcMath\Number-based class constants (e.g. INT64_MIN, INT64_MAX).
*/
const __TWO = new Number(2);

/**
* @internal
*
* @phpstan-type Int8 = int<self::INT8_MIN, self::INT8_MAX>
* @phpstan-type Uint8 = int<0, self::UINT8_MAX>
* @phpstan-type Int16 = int<self::INT16_MIN, self::INT16_MAX>
* @phpstan-type Uint16 = int<0, self::UINT16_MAX>
* @phpstan-type Int32 = int<self::INT32_MIN, self::INT32_MAX>
* @phpstan-type Uint32 = int<0, self::UINT32_MAX>
*/
final readonly class Ints
{
public const int INT8_MIN = -2 ** 7;
public const int INT8_MAX = 2 ** 7 - 1;
public const int UINT8_MAX = 2 ** 8 - 1;
public const int UINT8_MOD = 2 ** 8;
public const int INT16_MIN = -2 ** 15;
public const int INT16_MAX = 2 ** 15 - 1;
public const int UINT16_MAX = 2 ** 16 - 1;
public const int UINT16_MOD = 2 ** 16;
public const int INT32_MIN = -2 ** 31;
public const int INT32_MAX = 2 ** 31 - 1;
public const int UINT32_MAX = 2 ** 32 - 1;
public const int UINT32_MOD = 2 ** 32;

/** @phpstan-ignore unaryOp.invalid */
public const Number INT64_MIN = -__TWO ** 63;
public const Number INT64_MAX = __TWO ** 63 - 1;
public const Number UINT64_MAX = __TWO ** 64 - 1;
public const Number UINT64_MOD = __TWO ** 64;

/**
* @phpstan-assert-if-true Int8 $num
*/
public static function isInt8(int $num): bool
{
return $num >= self::INT8_MIN && $num <= self::INT8_MAX;
}

/**
* @phpstan-assert-if-true Uint8 $num
*/
public static function isUint8(int $num): bool
{
return $num >= 0 && $num <= self::UINT8_MAX;
}

/**
* @phpstan-assert-if-true Int16 $num
*/
public static function isInt16(int $num): bool
{
return $num >= self::INT16_MIN && $num <= self::INT16_MAX;
}

/**
* @phpstan-assert-if-true Uint16 $num
*/
public static function isUint16(int $num): bool
{
return $num >= 0 && $num <= self::UINT16_MAX;
}

/**
* @phpstan-assert-if-true Int32 $num
*/
public static function isInt32(int $num): bool
{
return $num >= self::INT32_MIN && $num <= self::INT32_MAX;
}

/**
* @phpstan-assert-if-true Uint32 $num
*/
public static function isUint32(int $num): bool
{
return $num >= 0 && $num <= self::UINT32_MAX;
}

public static function isInt64(Number $num): bool
{
return $num >= self::INT64_MIN && $num <= self::INT64_MAX;
}

public static function isUint64(Number $num): bool
{
return $num >= 0 && $num <= self::UINT64_MAX;
}

private function __construct() {}
}
Loading