ES
apps/caramail-server / src / Chat / Frame.php · commit 070afda
<?phpnamespace App\Chat;/** * The wire format of the chat, unchanged since 2002: one frame per line ("\n"), its fields separated by tabs, * the first field is the numeric code. The server always writes "code TAB fields... LF" (a code without fields * still has its tab: "667\t\n"), as the original PHP server did. */final class Frame{    public const TAB = "\t";    public const EOL = "\n";    /** @param list<string> $fields */    public static function encode(string $code, array $fields = []): string    {        return $code . self::TAB . implode(self::TAB, array_map(self::clean(...), $fields)) . self::EOL;    }    /**     * A list joined the way the original server built its payloads: each item followed by a tab.     *     * @param list<string|int> $items     */    public static function list(array $items): string    {        return $items === [] ? '' : implode(self::TAB, array_map(fn ($v) => self::clean((string)$v), $items)) . self::TAB;    }    /**     * Splits complete lines off a buffer; what is left (an unfinished line) stays in $buffer.     *     * @return list<array{0: string, 1: list<string>}> [code, fields]     */    public static function decode(string &$buffer): array    {        $frames = [];        while (($end = strpos($buffer, self::EOL)) !== false) {            $line = rtrim(substr($buffer, 0, $end), "\r");            $buffer = (string)substr($buffer, $end + 1);            if ($line === '') {                continue;            }            $parts = explode(self::TAB, $line);            $code = array_shift($parts);            $frames[] = [$code, $parts];        }        return $frames;    }    /** A field never carries a line end (it would cut the frame). Tabs inside list payloads are kept on purpose. */    private static function clean(string $value): string    {        return str_replace(["\r", "\n"], ' ', $value);    }}