<?php
 declare(strict_types=1); namespace SpomkyLabs\Pki\ASN1\Component; use Brick\Math\BigInteger; use DomainException; use LogicException; use SpomkyLabs\Pki\ASN1\Exception\DecodeException; use SpomkyLabs\Pki\ASN1\Feature\Encodable; use SpomkyLabs\Pki\ASN1\Util\BigInt; use function count; use function mb_strlen; use function ord; use function sprintf; final class Length implements Encodable { private readonly BigInt $_length; private function __construct( BigInteger|int $length, private readonly bool $_indefinite = false ) { $this->_length = BigInt::create($length); } public static function create(BigInteger|int $length, bool $_indefinite = false): self { return new self($length, $_indefinite); } public static function fromDER(string $data, ?int &$offset = null): self { $idx = $offset ?? 0; $datalen = mb_strlen($data, '8bit'); if ($idx >= $datalen) { throw new DecodeException('Unexpected end of data while decoding length.'); } $indefinite = false; $byte = ord($data[$idx++]); $length = (0x7f & $byte); if ((0x80 & $byte) !== 0) { if ($length === 0) { $indefinite = true; } else { if ($idx + $length > $datalen) { throw new DecodeException('Unexpected end of data while decoding long form length.'); } $length = self::decodeLongFormLength($length, $data, $idx); } } if (isset($offset)) { $offset = $idx; } return self::create($length, $indefinite); } public static function expectFromDER(string $data, int &$offset, ?int $expected = null): self { $idx = $offset; $length = self::fromDER($data, $idx); if (isset($expected)) { if ($length->isIndefinite()) { throw new DecodeException(sprintf('Expected length %d, got indefinite.', $expected)); } if ($expected !== $length->intLength()) { throw new DecodeException(sprintf('Expected length %d, got %d.', $expected, $length->intLength())); } } if (! $length->isIndefinite() && mb_strlen($data, '8bit') < $idx + $length->intLength()) { throw new DecodeException( sprintf( 'Length %d overflows data, %d bytes left.', $length->intLength(), mb_strlen($data, '8bit') - $idx ) ); } $offset = $idx; return $length; } public function toDER(): string { $bytes = []; if ($this->_indefinite) { $bytes[] = 0x80; } else { $num = $this->_length->getValue(); if ($num->isGreaterThan(127)) { $octets = []; for (; $num->isGreaterThan(0); $num = $num->shiftedRight(8)) { $octets[] = BigInteger::of(0xff)->and($num)->toInt(); } $count = count($octets); if ($count >= 127) { throw new DomainException('Too many length octets.'); } $bytes[] = 0x80 | $count; foreach (array_reverse($octets) as $octet) { $bytes[] = $octet; } } else { $bytes[] = $num->toInt(); } } return pack('C*', ...$bytes); } public function length(): string { if ($this->_indefinite) { throw new LogicException('Length is indefinite.'); } return $this->_length->base10(); } public function intLength(): int { if ($this->_indefinite) { throw new LogicException('Length is indefinite.'); } return $this->_length->toInt(); } public function isIndefinite(): bool { return $this->_indefinite; } private static function decodeLongFormLength(int $length, string $data, int &$offset): BigInteger { if ($length === 127) { throw new DecodeException('Invalid number of length octets.'); } $num = BigInteger::of(0); while (--$length >= 0) { $byte = ord($data[$offset++]); $num = $num->shiftedLeft(8) ->or($byte); } return $num; } } 