generated from Nodarx/template
feat: speed improvements
Signed-off-by: Sebastian Krupinski <root@LAPTOP-7DVOR6NC>
This commit is contained in:
@@ -33,22 +33,39 @@ use Gricob\IMAP\Protocol\Response\Line\Status\Code\UnseenCode;
|
|||||||
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
||||||
use Gricob\IMAP\Protocol\Response\Line\Status\StatusType;
|
use Gricob\IMAP\Protocol\Response\Line\Status\StatusType;
|
||||||
|
|
||||||
readonly class Parser
|
class Parser
|
||||||
{
|
{
|
||||||
private Lexer $lexer;
|
private Lexer $lexer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preloaded literal streams for the current parse() call.
|
||||||
|
* Populated by parse() when ResponseHandler has already read large
|
||||||
|
* literals into php://temp resources to keep them out of the lexer.
|
||||||
|
*
|
||||||
|
* @var list<resource>
|
||||||
|
*/
|
||||||
|
private array $literalStreams = [];
|
||||||
|
|
||||||
|
/** Sequential index into $literalStreams consumed by literal(). */
|
||||||
|
private int $nextLiteralIndex = 0;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->lexer = new Lexer();
|
$this->lexer = new Lexer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param list<resource> $literalStreams Pre-extracted large literal streams
|
||||||
|
* (see ResponseHandler::readNextRaw).
|
||||||
* @throws ParseError
|
* @throws ParseError
|
||||||
*/
|
*/
|
||||||
public function parse(string $raw): Line
|
public function parse(string $raw, array $literalStreams = []): Line
|
||||||
{
|
{
|
||||||
$raw = $this->sanitizeInvalidEncoding($raw);
|
$raw = $this->sanitizeInvalidEncoding($raw);
|
||||||
|
|
||||||
|
$this->literalStreams = $literalStreams;
|
||||||
|
$this->nextLiteralIndex = 0;
|
||||||
|
|
||||||
$this->lexer->setInput($raw);
|
$this->lexer->setInput($raw);
|
||||||
$this->lexer->moveNext();
|
$this->lexer->moveNext();
|
||||||
|
|
||||||
@@ -1076,6 +1093,15 @@ readonly class Parser
|
|||||||
$this->getToken(TokenType::CLOSE_BRACES);
|
$this->getToken(TokenType::CLOSE_BRACES);
|
||||||
$this->getToken(TokenType::CRLF);
|
$this->getToken(TokenType::CRLF);
|
||||||
|
|
||||||
|
// If ResponseHandler preloaded this literal (because it was too large
|
||||||
|
// to tokenise safely), consume from the php://temp resource instead
|
||||||
|
// of reading token-by-token through the lexer.
|
||||||
|
if (isset($this->literalStreams[$this->nextLiteralIndex])) {
|
||||||
|
$resource = $this->literalStreams[$this->nextLiteralIndex++];
|
||||||
|
rewind($resource);
|
||||||
|
return (string) stream_get_contents($resource);
|
||||||
|
}
|
||||||
|
|
||||||
$value = '';
|
$value = '';
|
||||||
while (strlen($value) < $size) {
|
while (strlen($value) < $size) {
|
||||||
$value .= $this->getToken()->value;
|
$value .= $this->getToken()->value;
|
||||||
|
|||||||
@@ -17,21 +17,67 @@ use RuntimeException;
|
|||||||
|
|
||||||
readonly class ResponseHandler
|
readonly class ResponseHandler
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Literals larger than this threshold (in bytes) are streamed into a
|
||||||
|
* temporary file instead of being held as a PHP string. This prevents
|
||||||
|
* the Doctrine Lexer from running preg_split() over multi-megabyte bodies,
|
||||||
|
* which is the root cause of OOM errors on large mailboxes.
|
||||||
|
*/
|
||||||
|
private const LARGE_LITERAL_THRESHOLD = 524288; // 512 KB
|
||||||
|
|
||||||
public function __construct(private Parser $parser)
|
public function __construct(private Parser $parser)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the next complete IMAP response line from $stream.
|
||||||
|
*
|
||||||
|
* Large literals (>= LARGE_LITERAL_THRESHOLD bytes) are read in 8 KB
|
||||||
|
* chunks into php://temp resources instead of being appended to $raw,
|
||||||
|
* so the body content never reaches the lexer as a plain string.
|
||||||
|
*
|
||||||
|
* @return array{string, list<resource>} [$raw, $preloadedLiterals]
|
||||||
|
*/
|
||||||
|
private function readNextRaw(ResponseStream $stream): array
|
||||||
|
{
|
||||||
|
$raw = $stream->readLine();
|
||||||
|
$preloaded = [];
|
||||||
|
|
||||||
|
while (preg_match('/\{(?<bytes>\d+)}\r\n$/', $raw, $matches)) {
|
||||||
|
$literalSize = (int) $matches['bytes'];
|
||||||
|
|
||||||
|
if ($literalSize >= self::LARGE_LITERAL_THRESHOLD) {
|
||||||
|
// Stream into a temp file to avoid holding a huge string in
|
||||||
|
// memory. php://temp uses RAM up to 2 MB then spills to disk.
|
||||||
|
$tmp = fopen('php://temp', 'r+');
|
||||||
|
$remaining = $literalSize;
|
||||||
|
while ($remaining > 0) {
|
||||||
|
$chunk = $stream->read(min(8192, $remaining));
|
||||||
|
fwrite($tmp, $chunk);
|
||||||
|
$remaining -= strlen($chunk);
|
||||||
|
}
|
||||||
|
rewind($tmp);
|
||||||
|
$preloaded[] = $tmp;
|
||||||
|
// Keep the {N}\r\n header in $raw so the parser can read the
|
||||||
|
// literal size, but do NOT append the N bytes — the parser
|
||||||
|
// will pull them from the preloaded resource instead.
|
||||||
|
} else {
|
||||||
|
$raw .= $stream->read($literalSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw .= $stream->readLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$raw, $preloaded];
|
||||||
|
}
|
||||||
|
|
||||||
public function handle(string $statusTag, ResponseStream $stream, ContinuationHandler $continuationHandler): Response
|
public function handle(string $statusTag, ResponseStream $stream, ContinuationHandler $continuationHandler): Response
|
||||||
{
|
{
|
||||||
$responseBuilder = new ResponseBuilder($statusTag);
|
$responseBuilder = new ResponseBuilder($statusTag);
|
||||||
|
|
||||||
do {
|
do {
|
||||||
$raw = $stream->readLine();
|
[$raw, $preloaded] = $this->readNextRaw($stream);
|
||||||
while (preg_match('/\{(?<bytes>\d+)}\r\n$/', $raw, $matches)) {
|
$line = $this->parser->parse($raw, $preloaded);
|
||||||
$raw .= $stream->read((int) $matches['bytes']);
|
|
||||||
$raw .= $stream->readLine();
|
|
||||||
}
|
|
||||||
$line = $this->parser->parse($raw);
|
|
||||||
|
|
||||||
if ($line instanceof CommandContinuation) {
|
if ($line instanceof CommandContinuation) {
|
||||||
$continuationHandler->continue();
|
$continuationHandler->continue();
|
||||||
@@ -59,12 +105,8 @@ readonly class ResponseHandler
|
|||||||
$status = null;
|
$status = null;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
$raw = $stream->readLine();
|
[$raw, $preloaded] = $this->readNextRaw($stream);
|
||||||
while (preg_match('/\{(?<bytes>\d+)}\r\n$/', $raw, $matches)) {
|
$line = $this->parser->parse($raw, $preloaded);
|
||||||
$raw .= $stream->read((int) $matches['bytes']);
|
|
||||||
$raw .= $stream->readLine();
|
|
||||||
}
|
|
||||||
$line = $this->parser->parse($raw);
|
|
||||||
|
|
||||||
if ($line instanceof CommandContinuation) {
|
if ($line instanceof CommandContinuation) {
|
||||||
$continuationHandler->continue();
|
$continuationHandler->continue();
|
||||||
|
|||||||
Reference in New Issue
Block a user