<?php
/**
 * Antara — minimal CBOR decoder (RFC 8949 subset).
 *
 * Supports definite-length items of the major types actually emitted by
 * WebAuthn authenticators:
 *   0 unsigned int, 1 negative int, 2 byte string, 3 text string,
 *   4 array, 5 map, 7 simple/false/true/null.
 *
 * Floats and indefinite-length items are intentionally NOT decoded; they
 * are not part of any WebAuthn spec emission path we care about.
 */

final class AntaraCbor
{
    /** Decode the next CBOR item from $bin starting at &$pos. */
    public static function decodeFirst(string $bin, int &$pos = 0)
    {
        if ($pos >= strlen($bin)) {
            throw new RuntimeException('CBOR: unexpected end of input');
        }
        $ib = ord($bin[$pos++]);
        $major = $ib >> 5;
        $minor = $ib & 0x1f;
        $val = self::readUint($bin, $pos, $minor);

        switch ($major) {
            case 0: return $val;
            case 1: return -1 - $val;
            case 2:
                $s = substr($bin, $pos, $val);
                $pos += $val;
                return $s;
            case 3:
                $s = substr($bin, $pos, $val);
                $pos += $val;
                return $s;
            case 4:
                $arr = [];
                for ($i = 0; $i < $val; $i++) $arr[] = self::decodeFirst($bin, $pos);
                return $arr;
            case 5:
                $map = [];
                for ($i = 0; $i < $val; $i++) {
                    $k = self::decodeFirst($bin, $pos);
                    $v = self::decodeFirst($bin, $pos);
                    if (is_int($k) || is_string($k)) $map[$k] = $v;
                }
                return $map;
            case 6: // tagged item, just unwrap
                return self::decodeFirst($bin, $pos);
            case 7:
                if ($minor === 20) return false;
                if ($minor === 21) return true;
                if ($minor === 22) return null;
                if ($minor === 23) return null;
                throw new RuntimeException('CBOR: unsupported simple value');
            default:
                throw new RuntimeException('CBOR: unsupported major type ' . $major);
        }
    }

    private static function readUint(string $bin, int &$pos, int $minor): int
    {
        if ($minor < 24) return $minor;
        if ($minor === 24) {
            self::need($bin, $pos, 1);
            $v = ord($bin[$pos]);
            $pos += 1;
            return $v;
        }
        if ($minor === 25) {
            self::need($bin, $pos, 2);
            $v = unpack('n', substr($bin, $pos, 2))[1];
            $pos += 2;
            return $v;
        }
        if ($minor === 26) {
            self::need($bin, $pos, 4);
            $v = unpack('N', substr($bin, $pos, 4))[1];
            $pos += 4;
            return $v;
        }
        if ($minor === 27) {
            self::need($bin, $pos, 8);
            $hi = unpack('N', substr($bin, $pos, 4))[1];
            $lo = unpack('N', substr($bin, $pos + 4, 4))[1];
            $pos += 8;
            // PHP int is 64-bit on supported platforms.
            return ($hi << 32) | $lo;
        }
        throw new RuntimeException('CBOR: indefinite/reserved length not supported');
    }

    private static function need(string $bin, int $pos, int $n): void
    {
        if ($pos + $n > strlen($bin)) {
            throw new RuntimeException('CBOR: truncated input');
        }
    }
}
