<?php
 declare(strict_types=1); namespace SpomkyLabs\Pki\ASN1\Type; use ArrayIterator; use Countable; use IteratorAggregate; use LogicException; use OutOfBoundsException; use SpomkyLabs\Pki\ASN1\Component\Identifier; use SpomkyLabs\Pki\ASN1\Component\Length; use SpomkyLabs\Pki\ASN1\Element; use SpomkyLabs\Pki\ASN1\Exception\DecodeException; use SpomkyLabs\Pki\ASN1\Feature\ElementBase; use function count; abstract class Structure extends Element implements Countable, IteratorAggregate { use UniversalClass; protected array $elements; private ?array $taggedMap = null; private ?array $unspecifiedTypes = null; protected function __construct(int $typeTag, ElementBase ...$elements) { parent::__construct($typeTag); $this->elements = array_map(static fn (ElementBase $el) => $el->asElement(), $elements); } public function __clone() { $this->taggedMap = null; $this->unspecifiedTypes = null; } public function isConstructed(): bool { return true; } public static function explodeDER(string $data): array { $offset = 0; $identifier = Identifier::fromDER($data, $offset); if (! $identifier->isConstructed()) { throw new DecodeException('Element is not constructed.'); } $length = Length::expectFromDER($data, $offset); if ($length->isIndefinite()) { throw new DecodeException('Explode not implemented for indefinite length encoding.'); } $end = $offset + $length->intLength(); $parts = []; while ($offset < $end) { $idx = $offset; Identifier::fromDER($data, $offset); $length = Length::expectFromDER($data, $offset)->intLength(); $parts[] = mb_substr($data, $idx, $offset - $idx + $length, '8bit'); $offset += $length; } return $parts; } public function withReplaced(int $idx, Element $el): self { if (! isset($this->elements[$idx])) { throw new OutOfBoundsException("Structure doesn't have element at index {$idx}."); } $obj = clone $this; $obj->elements[$idx] = $el; return $obj; } public function withInserted(int $idx, Element $el): self { if (count($this->elements) < $idx || $idx < 0) { throw new OutOfBoundsException("Index {$idx} is out of bounds."); } $obj = clone $this; array_splice($obj->elements, $idx, 0, [$el]); return $obj; } public function withAppended(Element $el): self { $obj = clone $this; array_push($obj->elements, $el); return $obj; } public function withPrepended(Element $el): self { $obj = clone $this; array_unshift($obj->elements, $el); return $obj; } public function withoutElement(int $idx): self { if (! isset($this->elements[$idx])) { throw new OutOfBoundsException("Structure doesn't have element at index {$idx}."); } $obj = clone $this; array_splice($obj->elements, $idx, 1); return $obj; } public function elements(): array { if (! isset($this->unspecifiedTypes)) { $this->unspecifiedTypes = array_map(UnspecifiedType::create(...), $this->elements); } return $this->unspecifiedTypes; } public function has(int $idx, ?int $expectedTag = null): bool { if (! isset($this->elements[$idx])) { return false; } if (isset($expectedTag)) { if (! $this->elements[$idx]->isType($expectedTag)) { return false; } } return true; } public function at(int $idx): UnspecifiedType { if (! isset($this->elements[$idx])) { throw new OutOfBoundsException("Structure doesn't have an element at index {$idx}."); } return UnspecifiedType::create($this->elements[$idx]); } public function hasTagged(int $tag): bool { if (! isset($this->taggedMap)) { $this->taggedMap = []; foreach ($this->elements as $element) { if ($element->isTagged()) { $this->taggedMap[$element->tag()] = $element; } } } return isset($this->taggedMap[$tag]); } public function getTagged(int $tag): TaggedType { if (! $this->hasTagged($tag)) { throw new LogicException("No tagged element for tag {$tag}."); } return $this->taggedMap[$tag]; } public function count(): int { return count($this->elements); } public function getIterator(): ArrayIterator { return new ArrayIterator($this->elements()); } protected function encodedAsDER(): string { $data = ''; foreach ($this->elements as $element) { $data .= $element->toDER(); } return $data; } } 