diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7f3ec765..e303f9ce 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,85 +1,76 @@ - + - $data + - - - $port - - - $output::OUTPUT_RAW - $output::OUTPUT_RAW + + - $logger + - \is_resource($fp) + - $buf === null + - $time + - $response + - $time + - $response + - - $time - $time - - $time + - $payload + getAttribute('begin_at', null)]]> getAttribute('begin_at', null)]]> - $payload + - $offset - $currentPos + - $headers - $itemHeader + + - $headers - $itemHeader - $type + + + - TReturn + method]]> @@ -90,10 +81,10 @@ - $values - $values - $values - $values + + + + @@ -101,17 +92,10 @@ stream->getSize()]]> - int + - - request->getBody()->getSize() + \array_reduce( - $this->request->getUploadedFiles(), - static fn(int $carry, array $file): int => $carry + $file['size'], - 0 - )]]> - @@ -138,31 +122,16 @@ - $payload + - - int - - - - - - - request->getUploadedFiles()]]> - array + - - int - - - request->getBody()->getSize()]]> - - \json_decode($payload, true, JSON_THROW_ON_ERROR) + @@ -174,46 +143,38 @@ }]]> - $data - $data + + - $data + - static + - $item - $item + + - $item + - public function __construct( + - - - $payload - - - $payload - - - $payload + @@ -221,12 +182,12 @@ - Files::normalizeSize($size) + - $frame + getSize()]]> getCookieParams()]]> @@ -241,9 +202,9 @@ - $file - $file - $function + + + $output->writeln(]]> @@ -253,29 +214,25 @@ - $row - $stacktrace + + - $file - $file - $function + + + - renderMessageHeader + - - - - $message - $meta - $tags + + @@ -289,301 +246,75 @@ payload]]> - $type payload['exceptions']]]> - $type + - - $type - - - - - getHeaders()]]> - getHeaders()]]> - - - getClientFilename()]]> - getClientMediaType()]]> - - - - - render - - - - - - - - $context - $controller - $data - $fileLink - - - - + + - - $meta - - $context - - $data + + - $controller - $describer - $fileLink + + + - $payload - [$data, $context] + + - describe + - - dumper->dump($controller, true)]]> - dumper->dump($data, true)]]> - - - $item - - - - \array_keys($data) - - $item + + - - - $size - - - - - - - $key - - $value + - - - Termwind::renderUsing($output) - new HtmlRenderer() - - - Termwind::renderUsing($output) - - - new SplQueue() + - - - payloadSize]]> - - - \Closure::bind($callable(...), $this) - \Closure::bind($callable(...), $this) - - - - - socket]]> - socket]]> - - - new \SplQueue() + getClass()]]> - $value - $value + + getName()]]]> getName()]]]> - $value + getName()]]]> - getDescriptorByClassName + - - - - - - - - $value - $values - - - headers[$header]]]> - - - headers[$header]]]> - headers[$new->headerNames[$normalized]]]]> - headers[$header]]]> - headers[$header]]]> - headers[$header]]]> - - - headers[$header]]]> - - - $header - $header - $header - - - string[] - - - - - FieldDataArray - $this->value, - ]]]> - - - - - FileDataArray - $this->fileName, - 'size' => $this->getSize(), - ]]]> - - - - - new File($headers, $name, $fileName), - false => new Field($headers, $name), - }]]> - - - $headers - $headers - $headers - - - static - - - - - $addrs - protocol['BCC'] ?? []]]> - - - protocol]]> - - - $attach - - - $message - - - $attach - $message - - - >]]> - - - $attachments - $messages - - - - - $boundary - $headersBlock - read($blockEnd - $pos + 2)]]> - - - $parts - $result - - - >]]> - array{0: non-empty-string, 1: non-empty-string, 2: non-empty-string} - - - getSize()]]> - - - $value - $value - - - - - $boundary - $headerBlock - - - - - AbstractCloner::$defaultCasters[EnumValue::class] - AbstractCloner::$defaultCasters[MapField::class] - AbstractCloner::$defaultCasters[Message::class] - AbstractCloner::$defaultCasters[RepeatedField::class] - - - AbstractCloner::$defaultCasters[EnumValue::class] - AbstractCloner::$defaultCasters[MapField::class] - AbstractCloner::$defaultCasters[Message::class] - AbstractCloner::$defaultCasters[RepeatedField::class] - - - $value - AbstractCloner::$defaultCasters[EnumValue::class] - AbstractCloner::$defaultCasters[MapField::class] - AbstractCloner::$defaultCasters[Message::class] - AbstractCloner::$defaultCasters[RepeatedField::class] - - diff --git a/src/Proto/Frame/Smtp.php b/src/Proto/Frame/Smtp.php index ae4a2229..b460f24f 100644 --- a/src/Proto/Frame/Smtp.php +++ b/src/Proto/Frame/Smtp.php @@ -14,6 +14,7 @@ /** * @internal * @psalm-internal Buggregator + * @psalm-import-type TArrayData from Message\Smtp */ final class Smtp extends Frame implements FilesCarrier { @@ -34,6 +35,7 @@ public function __toString(): string public static function fromString(string $payload, DateTimeImmutable $time): static { + /** @var TArrayData $payload */ $payload = \json_decode($payload, true, \JSON_THROW_ON_ERROR); $message = Message\Smtp::fromArray($payload); diff --git a/src/Sender/Console/Renderer/SentryEnvelope.php b/src/Sender/Console/Renderer/SentryEnvelope.php index 68b4783b..5b7c169a 100644 --- a/src/Sender/Console/Renderer/SentryEnvelope.php +++ b/src/Sender/Console/Renderer/SentryEnvelope.php @@ -36,7 +36,7 @@ public function render(OutputInterface $output, Frame $frame): void ++$i; try { $type = $item->headers['type'] ?? null; - Common::renderHeader2($output, "Item $i", green: $type); + Common::renderHeader2($output, "Item $i", green: (string)$type); Header::renderMessageHeader($output, $item->payload); $this->renderItem($output, $item); diff --git a/src/Sender/Console/Renderer/Smtp.php b/src/Sender/Console/Renderer/Smtp.php index 9f48a8ea..430d9519 100644 --- a/src/Sender/Console/Renderer/Smtp.php +++ b/src/Sender/Console/Renderer/Smtp.php @@ -58,9 +58,9 @@ public function render(OutputInterface $output, Frame $frame): void foreach ($message->getAttachments() as $attach) { Files::renderFile( $output, - $attach->getClientFilename(), + $attach->getClientFilename() ?? '', $attach->getSize(), - $attach->getClientMediaType(), + $attach->getClientMediaType() ?? '', ); } $output->writeln(''); diff --git a/src/Sender/Console/Renderer/TemplateRenderer.php b/src/Sender/Console/Renderer/TemplateRenderer.php index 63b0800e..d0b98bff 100644 --- a/src/Sender/Console/Renderer/TemplateRenderer.php +++ b/src/Sender/Console/Renderer/TemplateRenderer.php @@ -20,6 +20,7 @@ public function __construct( public function render(string $template, array $data = []): void { + /** @psalm-suppress InternalMethod */ $this->renderer->render( $this->templateEngine->render($template, $data), 0 diff --git a/src/Sender/Console/Renderer/VarDumper.php b/src/Sender/Console/Renderer/VarDumper.php index 868e07ae..11edf05a 100644 --- a/src/Sender/Console/Renderer/VarDumper.php +++ b/src/Sender/Console/Renderer/VarDumper.php @@ -58,7 +58,7 @@ public function __construct( } /** - * @psalm-suppress RiskyTruthyFalsyComparison + * @psalm-suppress RiskyTruthyFalsyComparison, MixedArrayAccess, MixedArgument */ public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void { @@ -94,7 +94,7 @@ public function describe(OutputInterface $output, Data $data, array $context, in empty($request['method'] ?? '') or $meta['Method'] = $request['method']; empty($request['uri'] ?? '') or $meta['URI'] = $request['uri']; if ($controller = $request['controller']) { - $meta['Controller'] = rtrim($this->dumper->dump($controller, true), "\n"); + $meta['Controller'] = \rtrim((string) $this->dumper->dump($controller, true), "\n"); } } elseif (isset($context['cli'])) { $meta['Command'] = $context['cli']['command_line']; @@ -107,7 +107,7 @@ public function describe(OutputInterface $output, Data $data, array $context, in Common::renderMetadata($output, $meta); $output->writeln(''); - $output->write($this->dumper->dump($data, true), true, OutputInterface::OUTPUT_RAW); + $output->write((string) $this->dumper->dump($data, true), true, OutputInterface::OUTPUT_RAW); } }; } diff --git a/src/Sender/Console/Support/Common.php b/src/Sender/Console/Support/Common.php index 21695e7d..83adf92c 100644 --- a/src/Sender/Console/Support/Common.php +++ b/src/Sender/Console/Support/Common.php @@ -30,6 +30,10 @@ public static function renderHeader2(OutputInterface $output, string $title, str { $parts = ["# $title "]; foreach ($sub as $color => $value) { + if ($value === '') { + continue; + } + $color = \is_string($color) ? $color : 'gray'; $parts[] = \sprintf(' %s ', $color, $value); } @@ -57,9 +61,11 @@ public static function renderHighlightedLine(OutputInterface $output, string $li */ public static function renderMetadata(OutputInterface $output, array $data): void { - $maxHeaderLength = \max(\array_map('strlen', \array_keys($data))); + $maxHeaderLength = \max(0, ...\array_map( + static fn(string|int $key): int => \strlen((string) $key), + \array_keys($data)), + ); - /** @var mixed $value */ foreach ($data as $head => $value) { // Align headers to the right self::renderHeader( diff --git a/src/Sender/Console/Support/Files.php b/src/Sender/Console/Support/Files.php index 387e5376..800d791b 100644 --- a/src/Sender/Console/Support/Files.php +++ b/src/Sender/Console/Support/Files.php @@ -13,6 +13,8 @@ final class Files { /** + * @param non-negative-int|null $size + * * Render file info. Example: * ┌───┐ logo.ico * │ico│ 20.06 KiB @@ -33,8 +35,10 @@ public static function renderFile( string ...$additional ): void { // File extension - $ex = \substr($fileName, \strrpos($fileName, '.') + 1); - $ex = \strlen($ex) > 3 ? ' ' : \str_pad($ex, 3, ' ', \STR_PAD_BOTH); + $dotPos = \strrpos($fileName, '.'); + $ex = $dotPos === false || \strlen($fileName) - $dotPos > 4 + ? ' ' + : \str_pad(\substr($fileName, $dotPos + 1), 3, ' ', \STR_PAD_BOTH); // File size $sizeStr = self::normalizeSize($size) ?? 'unknown size'; diff --git a/src/Sender/Console/Support/Tables.php b/src/Sender/Console/Support/Tables.php index 80f99f7c..987bd5ed 100644 --- a/src/Sender/Console/Support/Tables.php +++ b/src/Sender/Console/Support/Tables.php @@ -23,7 +23,7 @@ public static function renderKeyValueTable(OutputInterface $output, string $titl return; } - $keyLength = \max(\array_map(static fn($key) => \strlen($key), \array_keys($data))); + $keyLength = \max(\array_map(static fn($key) => \strlen((string) $key), \array_keys($data))); $valueLength = \max(1, (new Terminal())->getWidth() - 7 - $keyLength); $table->setRows([...(static function (array $data) use ($valueLength): iterable { diff --git a/src/Sender/ConsoleSender.php b/src/Sender/ConsoleSender.php index 1d626a1a..58cdb975 100644 --- a/src/Sender/ConsoleSender.php +++ b/src/Sender/ConsoleSender.php @@ -21,8 +21,10 @@ final class ConsoleSender implements Sender { public static function create(OutputInterface $output): self { + /** @psalm-suppress InternalMethod, InternalClass */ Termwind::renderUsing($output); + /** @psalm-suppress InternalClass */ $templateRenderer = new TemplateRenderer( new HtmlRenderer(), new TemplateEngine(Info::TRAP_ROOT . '/resources/templates') diff --git a/src/Socket/Client.php b/src/Socket/Client.php index 2594393a..494eba8c 100644 --- a/src/Socket/Client.php +++ b/src/Socket/Client.php @@ -26,6 +26,9 @@ final class Client implements Destroyable private \Closure $onPayload; private \Closure $onClose; + /** + * @param positive-int $payloadSize + */ private function __construct( private readonly \Socket $socket, private readonly int $payloadSize, @@ -112,21 +115,21 @@ protected function onInit(): void } /** - * @param callable(string): void $callable Non-static callable. + * @param callable(string): void $callable If non-static callable, it will be bound to the current instance. * @psalm-assert callable(string): void $callable */ public function setOnPayload(callable $callable): void { - $this->onPayload = \Closure::bind($callable(...), $this); + $this->onPayload = \Closure::bind($callable(...), $this) ?? $callable(...); } /** - * @param callable(): void $callable Non-static callable. + * @param callable(): void $callable If non-static callable, it will be bound to the current instance. * @psalm-assert callable(): void $callable */ public function setOnClose(callable $callable): void { - $this->onClose = \Closure::bind($callable(...), $this); + $this->onClose = \Closure::bind($callable(...), $this) ?? $callable(...); } public function send(string $payload): void diff --git a/src/Socket/Server.php b/src/Socket/Server.php index 73c1ae50..994c5113 100644 --- a/src/Socket/Server.php +++ b/src/Socket/Server.php @@ -20,8 +20,7 @@ */ final class Server implements Processable, Cancellable, Destroyable { - /** @var false|resource|Socket */ - private $socket; + private Socket $socket; /** @var array */ private array $clients = []; @@ -41,13 +40,11 @@ private function __construct( private readonly ?Closure $clientInflector, private readonly Logger $logger, ) { - $this->socket = @\socket_create_listen($port); + $this->socket = @\socket_create_listen($port) ?: throw new \RuntimeException('Socket create failed.'); + /** @link https://github.com/buggregator/trap/pull/14 */ // \socket_set_option($this->socket, \SOL_SOCKET, \SO_LINGER, ['l_linger' => 0, 'l_onoff' => 1]); - if ($this->socket === false) { - throw new \RuntimeException('Socket create failed.'); - } \socket_set_nonblock($this->socket); $logger->status('Application', 'Server started on 127.0.0.1:%s', $port); @@ -92,6 +89,7 @@ public static function init( public function process(): void { + // /** @psalm-suppress PossiblyInvalidArgument */ while (!$this->cancelled and false !== ($socket = \socket_accept($this->socket))) { $client = null; try { diff --git a/src/Support/StreamHelper.php b/src/Support/StreamHelper.php index e6190296..e937a24d 100644 --- a/src/Support/StreamHelper.php +++ b/src/Support/StreamHelper.php @@ -67,7 +67,7 @@ public static function strpos(StreamInterface $stream, string $substr): int|fals * * @param non-empty-string $boundary * - * @return int Bytes written + * @return int<0, max> Bytes written */ public static function writeStreamUntil(StreamInterface $from, StreamInterface $to, string $boundary): int { diff --git a/src/Traffic/Dispatcher/Http.php b/src/Traffic/Dispatcher/Http.php index 8e7fa06a..d06361f0 100644 --- a/src/Traffic/Dispatcher/Http.php +++ b/src/Traffic/Dispatcher/Http.php @@ -52,7 +52,7 @@ public function __construct( /** @see RequestHandler::handle() */ 'handle', static function (): never { throw new \LogicException('No handler found for request.'); }, - Generator::class, + 'never', ); } diff --git a/src/Traffic/Dispatcher/Smtp.php b/src/Traffic/Dispatcher/Smtp.php index f3525c8d..f670a3ad 100644 --- a/src/Traffic/Dispatcher/Smtp.php +++ b/src/Traffic/Dispatcher/Smtp.php @@ -31,7 +31,6 @@ public function __construct( public function dispatch(StreamClient $stream): iterable { $stream->sendData($this->createResponse(self::READY, 'mailamie')); - $protocol = []; $message = null; @@ -40,9 +39,11 @@ public function dispatch(StreamClient $stream): iterable if (\preg_match('/^(?:EHLO|HELO)/', $response)) { $stream->sendData($this->createResponse(self::OK)); } elseif (\preg_match('/^MAIL FROM:\s*<(.*)>/', $response, $matches)) { + /** @var array{0: non-empty-string, 1: string} $matches */ $protocol['FROM'][] = $matches[1]; $stream->sendData($this->createResponse(self::OK)); } elseif (\preg_match('/^RCPT TO:\s*<(.*)>/', $response, $matches)) { + /** @var array{0: non-empty-string, 1: string} $matches */ $protocol['BCC'][] = $matches[1]; $stream->sendData($this->createResponse(self::OK)); } elseif (\str_starts_with($response, 'QUIT')) { diff --git a/src/Traffic/Message/Headers.php b/src/Traffic/Message/Headers.php index 3be3a37f..5c25892a 100644 --- a/src/Traffic/Message/Headers.php +++ b/src/Traffic/Message/Headers.php @@ -9,12 +9,15 @@ */ trait Headers { - /** @var array Map of all registered headers, as original name => array of values */ + /** @var array> Map of all registered headers */ private array $headers = []; - /** @var array Map of lowercase header name => original name at registration */ + /** @var array Map of lowercase header name => original name at registration */ private array $headerNames = []; + /** + * @return array> + */ public function getHeaders(): array { return $this->headers; @@ -26,7 +29,7 @@ public function hasHeader(string $header): bool } /** - * @return string[] + * @return list */ public function getHeader(string $header): array { @@ -45,9 +48,10 @@ public function getHeaderLine(string $header): string return \implode(', ', $this->getHeader($header)); } - public function withHeader(string $header, $value): static + public function withHeader(string $header, mixed $value): static { $value = $this->validateAndTrimHeader($header, $value); + /** @var non-empty-string $normalized */ $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); $new = clone $this; @@ -97,7 +101,9 @@ private function setHeaders(array $headers): void // We must cast it back to a string in order to comply with validation. $header = (string)$header; } + $value = $this->validateAndTrimHeader($header, $value); + /** @var non-empty-string $normalized */ $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; @@ -126,8 +132,12 @@ private function setHeaders(array $headers): void * field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] ) * * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 + * + * @psalm-assert non-empty-string $header + * + * @return non-empty-list */ - private function validateAndTrimHeader(string $header, $values): array + private function validateAndTrimHeader(string $header, mixed $values): array { if (1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@D", $header)) { throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); @@ -170,17 +180,17 @@ private function validateAndTrimHeader(string $header, $values): array /** * List of header values. * - * @param array> $headers + * @param array> $headers * @param non-empty-string $header * - * @return list + * @return list */ private static function findHeader(array $headers, string $header): array { $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); $result = []; foreach ($headers as $name => $values) { - if (\strtr($name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') === $header) { + if (\strtr((string) $name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') === $header) { $result = [...$result, ...$values]; } } diff --git a/src/Traffic/Message/Multipart/Field.php b/src/Traffic/Message/Multipart/Field.php index 3ceb14fd..80c74563 100644 --- a/src/Traffic/Message/Multipart/Field.php +++ b/src/Traffic/Message/Multipart/Field.php @@ -6,7 +6,7 @@ /** * @psalm-type FieldDataArray = array{ - * headers: array>, + * headers: array>, * name?: string, * value: string * } @@ -15,6 +15,9 @@ */ final class Field extends Part { + /** + * @param array> $headers + */ public function __construct(array $headers, ?string $name = null, private string $value = '') { parent::__construct($headers, $name); @@ -30,6 +33,7 @@ public static function fromArray(array $data): self /** * @return FieldDataArray + * @psalm-suppress ImplementedReturnTypeMismatch */ public function jsonSerialize(): array { diff --git a/src/Traffic/Message/Multipart/File.php b/src/Traffic/Message/Multipart/File.php index 890a71f4..3ce9738c 100644 --- a/src/Traffic/Message/Multipart/File.php +++ b/src/Traffic/Message/Multipart/File.php @@ -10,10 +10,10 @@ /** * @psalm-type FileDataArray = array{ - * headers: array>, + * headers: array>, * name?: string, - * fileName: string, - * size?: int + * fileName?: string, + * size?: non-negative-int * } * * @internal @@ -21,8 +21,12 @@ final class File extends Part implements UploadedFileInterface { private ?UploadedFileInterface $uploadedFile = null; + /** @var non-negative-int|null */ private ?int $fileSize = null; + /** + * @param array> $headers + */ public function __construct(array $headers, ?string $name = null, private ?string $fileName = null) { parent::__construct($headers, $name); @@ -33,24 +37,31 @@ public function __construct(array $headers, ?string $name = null, private ?strin */ public static function fromArray(array $data): self { - $self = new self($data['headers'], $data['name'] ?? null, $data['fileName']); + $self = new self($data['headers'], $data['name'] ?? null, $data['fileName'] ?? null); $self->fileSize = $data['size'] ?? null; return $self; } /** * @return FileDataArray + * + * @psalm-suppress ImplementedReturnTypeMismatch */ public function jsonSerialize(): array { - return parent::jsonSerialize() + [ - 'fileName' => $this->fileName, - 'size' => $this->getSize(), - ]; + $data = parent::jsonSerialize(); + $this->fileName === null or $data['fileName'] = $this->fileName; + $this->fileSize === null or $data['size'] = $this->fileSize; + + return $data; } + /** + * @param non-negative-int|null $size + */ public function setStream(StreamInterface $stream, ?int $size = null, int $code = \UPLOAD_ERR_OK): void { + /** @psalm-suppress PropertyTypeCoercion */ $this->fileSize = $size ?? $stream->getSize() ?? null; $this->uploadedFile = new UploadedFile( $stream, @@ -83,6 +94,9 @@ public function moveTo(string $targetPath): void $this->getUploadedFile()->moveTo($targetPath); } + /** + * @return non-negative-int|null + */ public function getSize(): ?int { return $this->fileSize; diff --git a/src/Traffic/Message/Multipart/Part.php b/src/Traffic/Message/Multipart/Part.php index 8db1fb02..0e958b5d 100644 --- a/src/Traffic/Message/Multipart/Part.php +++ b/src/Traffic/Message/Multipart/Part.php @@ -6,6 +6,7 @@ use Buggregator\Trap\Traffic\Message\Headers; use JsonSerializable; +use RuntimeExceptio; use RuntimeException; /** @@ -15,6 +16,9 @@ abstract class Part implements JsonSerializable { use Headers; + /** + * @param array> $headers + */ protected function __construct( array $headers, protected ?string $name, @@ -22,7 +26,10 @@ protected function __construct( $this->setHeaders($headers); } - public static function create(array $headers): static + /** + * @param array> $headers + */ + public static function create(array $headers): Part { /** * Check Content-Disposition header @@ -31,6 +38,9 @@ public static function create(array $headers): static */ $contentDisposition = self::findHeader($headers, 'Content-Disposition')[0] ?? throw new RuntimeException('Missing Content-Disposition header.'); + if ($contentDisposition === '') { + throw new RuntimeException('Missing Content-Disposition header, can\'t be empty'); + } // Get field name and file name $name = \preg_match('/\bname=(?:(?[^" ;,]++)|"(?[^"]++)")/', $contentDisposition, $matches) === 1 @@ -63,11 +73,22 @@ public function withName(?string $name): static return $clone; } + /** + * @return array{ + * headers: array>, + * name?: string + * } + */ public function jsonSerialize(): array { - return [ - 'name' => $this->name, + $data = [ 'headers' => $this->headers, ]; + + if ($this->name !== null) { + $data['name'] = $this->name; + } + + return $data; } } diff --git a/src/Traffic/Message/Smtp.php b/src/Traffic/Message/Smtp.php index 12d9c3e5..896b3bad 100644 --- a/src/Traffic/Message/Smtp.php +++ b/src/Traffic/Message/Smtp.php @@ -6,6 +6,7 @@ use Buggregator\Trap\Traffic\Message\Multipart\Field; use Buggregator\Trap\Traffic\Message\Multipart\File; +use Buggregator\Trap\Traffic\Message\Multipart\Part; use Buggregator\Trap\Traffic\Message\Smtp\Contact; use Buggregator\Trap\Traffic\Message\Smtp\MessageFormat; use JsonSerializable; @@ -17,6 +18,13 @@ * @psalm-import-type FieldDataArray from Field * @psalm-import-type FileDataArray from File * + * @psalm-type TArrayData = array{ + * protocol: array>, + * headers: array>, + * messages: list, + * attachments: list, + * } + * * @internal */ final class Smtp implements JsonSerializable @@ -31,8 +39,8 @@ final class Smtp implements JsonSerializable private array $attachments = []; /** - * @param array> $protocol - * @param array> $headers + * @param array> $protocol + * @param array> $headers */ private function __construct( private readonly array $protocol, @@ -42,14 +50,17 @@ private function __construct( } /** - * @param array> $protocol - * @param array> $headers + * @param array> $protocol + * @param array> $headers */ public static function create(array $protocol, array $headers): self { return new self($protocol, $headers); } + /** + * @param TArrayData $data + */ public static function fromArray(array $data): self { $self = new self($data['protocol'], $data['headers']); @@ -74,7 +85,7 @@ public function jsonSerialize(): array } /** - * @return Field[] + * @return list */ public function getMessages(): array { @@ -82,7 +93,7 @@ public function getMessages(): array } /** - * @return File[] + * @return list */ public function getAttachments(): array { @@ -90,7 +101,7 @@ public function getAttachments(): array } /** - * @param Field[] $messages + * @param list $messages */ public function withMessages(array $messages): self { @@ -100,7 +111,7 @@ public function withMessages(array $messages): self } /** - * @param File[] $attachments + * @param list $attachments */ public function withAttachments(array $attachments): self { @@ -110,7 +121,7 @@ public function withAttachments(array $attachments): self } /** - * @return array> + * @return array> */ public function getProtocol(): array { diff --git a/src/Traffic/Parser/Http.php b/src/Traffic/Parser/Http.php index 147f4afb..e541946e 100644 --- a/src/Traffic/Parser/Http.php +++ b/src/Traffic/Parser/Http.php @@ -41,7 +41,7 @@ public function parseStream(StreamClient $stream): ServerRequestInterface $request = $this->factory->createServerRequest($method, $uri, []) ->withProtocolVersion($protocol); foreach ($headers as $name => $value) { - $request = $request->withHeader($name, $value); + $request = $request->withHeader((string) $name, $value); } // Todo refactor: @@ -64,8 +64,11 @@ public function parseStream(StreamClient $stream): ServerRequestInterface $rawCookies = \explode(';', $request->getHeaderLine('Cookie')); $cookies = []; foreach ($rawCookies as $cookie) { - [$name, $value] = \explode('=', \trim($cookie), 2); - $cookies[$name] = $value; + if (\str_contains($cookie, '=')) { + /** @psalm-suppress PossiblyUndefinedArrayOffset */ + [$name, $value] = \explode('=', \trim($cookie), 2); + $cookies[$name] = $value; + } } $request = $request->withCookieParams($cookies); @@ -77,7 +80,7 @@ public function parseStream(StreamClient $stream): ServerRequestInterface /** * @param string $line * - * @return array{0: non-empty-string, 1: non-empty-string, 2: non-empty-string} + * @return array{non-empty-string, non-empty-string, non-empty-string} */ private function parseFirstLine(string $line): array { @@ -85,7 +88,11 @@ private function parseFirstLine(string $line): array if (\count($parts) !== 3) { throw new \InvalidArgumentException('Invalid first line.'); } + $parts[2] = \explode('/', $parts[2], 2)[1] ?? $parts[2]; + if ($parts[0] === '' || $parts[1] === '' || $parts[2] === '') { + throw new \InvalidArgumentException('Invalid first line.'); + } return $parts; } @@ -143,8 +150,9 @@ private function processMultipartForm(ServerRequestInterface $request): ServerRe if (\preg_match('/boundary="?([^"\\s;]++)"?/', $request->getHeaderLine('Content-Type'), $matches) !== 1) { return $request; } - + /** @var non-empty-string $boundary */ $boundary = $matches[1]; + $parts = self::parseMultipartBody($request->getBody(), $boundary); $uploadedFiles = $parsedBody = []; foreach ($parts as $part) { @@ -159,7 +167,7 @@ private function processMultipartForm(ServerRequestInterface $request): ServerRe if ($part instanceof File) { $uploadedFiles[$name][] = new UploadedFile( $part->getStream(), - $part->getSize(), + (int) $part->getSize(), $part->getError(), $part->getClientFilename(), $part->getClientMediaType(), @@ -201,7 +209,7 @@ private function createBody(StreamClient $stream, ?int $limit): StreamInterface } /** - * @return array> + * @return array> */ public static function parseHeaders(string $headersBlock): array { diff --git a/src/Traffic/Parser/Smtp.php b/src/Traffic/Parser/Smtp.php index 5a915c79..50310489 100644 --- a/src/Traffic/Parser/Smtp.php +++ b/src/Traffic/Parser/Smtp.php @@ -18,7 +18,7 @@ final class Smtp { /** - * @param array> $protocol + * @param array> $protocol */ public function parseStream(array $protocol, StreamClient $stream): Message\Smtp { @@ -99,6 +99,7 @@ private function processSingleBody(Message\Smtp $message, StreamInterface $strea { $content = \preg_replace(["/^\.([^\r])/m", "/(\r\n\\.\r\n)$/D"], ['$1', ''], $stream->getContents()); + /** @psalm-suppress InvalidArgument */ $body = new Field( headers: \array_intersect_key($message->getHeaders(), ['Content-Type' => true]), value: $content, diff --git a/src/functions.php b/src/functions.php index 713171b9..0566d2f5 100644 --- a/src/functions.php +++ b/src/functions.php @@ -30,9 +30,13 @@ function trap(mixed ...$values): TrapHandle * Register the var-dump caster for protobuf messages */ if (\class_exists(AbstractCloner::class)) { + /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[Message::class] ??= [ProtobufCaster::class, 'cast']; + /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[RepeatedField::class] ??= [ProtobufCaster::class, 'castRepeated']; + /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[MapField::class] ??= [ProtobufCaster::class, 'castMap']; + /** @psalm-suppress MixedAssignment */ AbstractCloner::$defaultCasters[EnumValue::class] ??= [ProtobufCaster::class, 'castEnum']; } diff --git a/tests/Unit/Traffic/Parser/HttpParserTest.php b/tests/Unit/Traffic/Parser/HttpParserTest.php index f4c8f2d7..b505cb07 100644 --- a/tests/Unit/Traffic/Parser/HttpParserTest.php +++ b/tests/Unit/Traffic/Parser/HttpParserTest.php @@ -52,6 +52,39 @@ public function testSimpleGet(): void ], $request->getCookieParams()); } + /**\ + * Parer doesn't fail on wrong cookies + */ + public function testWrongCookie(): void + { + $body = \str_split( + <<parseStream($body); + + $this->assertSame('GET', $request->getMethod()); + $this->assertSame('/foo/bar', $request->getUri()->getPath()); + $this->assertSame('1.1', $request->getProtocolVersion()); + $this->assertSame(['127.0.0.1:9912'], $request->getHeader('host')); + $this->assertSame(['ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3'], $request->getHeader('accept-language')); + $this->assertSame([ + 'csrf-token.sig' => 'X0fR', + ], $request->getCookieParams()); + } + public function testPostUrlEncoded(): void { $body = \str_split(