Skip to content

Commit fd201c5

Browse files
authored
Merge pull request #95 from DirectoryTree/add-number-token
Parse `Number` tokens in tokenizer
2 parents a02a207 + df09c9c commit fd201c5

File tree

4 files changed

+93
-5
lines changed

4 files changed

+93
-5
lines changed

src/Connection/ImapTokenizer.php

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use DirectoryTree\ImapEngine\Connection\Tokens\ListClose;
1010
use DirectoryTree\ImapEngine\Connection\Tokens\ListOpen;
1111
use DirectoryTree\ImapEngine\Connection\Tokens\Literal;
12+
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
1213
use DirectoryTree\ImapEngine\Connection\Tokens\QuotedString;
1314
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeClose;
1415
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeOpen;
@@ -120,8 +121,8 @@ public function nextToken(): ?Token
120121
return $this->readLiteral();
121122
}
122123

123-
// Otherwise, parse an atom.
124-
return $this->readAtom();
124+
// Otherwise, parse a number or atom.
125+
return $this->readNumberOrAtom();
125126
}
126127

127128
/**
@@ -300,6 +301,64 @@ protected function readLiteral(): Literal
300301
return new Literal($literal);
301302
}
302303

304+
/**
305+
* Reads a number or atom token.
306+
*/
307+
protected function readNumberOrAtom(): Token
308+
{
309+
$position = $this->position;
310+
311+
// First char must be a digit to even consider a number.
312+
if (! ctype_digit($this->buffer[$position] ?? '')) {
313+
return $this->readAtom();
314+
}
315+
316+
// Walk forward to find the end of the digit run.
317+
while (ctype_digit($this->buffer[$position] ?? '')) {
318+
$position++;
319+
320+
$this->ensureBuffer($position - $this->position + 1);
321+
}
322+
323+
$next = $this->buffer[$position] ?? null;
324+
325+
// If next is EOF or a delimiter, it's a Number.
326+
if ($next === null || $this->isDelimiter($next)) {
327+
return $this->readNumber();
328+
}
329+
330+
// Otherwise it's an Atom.
331+
return $this->readAtom();
332+
}
333+
334+
/**
335+
* Reads a number token.
336+
*
337+
* A number consists of one or more digit characters and represents a numeric value.
338+
*/
339+
protected function readNumber(): Number
340+
{
341+
$start = $this->position;
342+
343+
while (true) {
344+
$this->ensureBuffer(1);
345+
346+
$char = $this->currentChar();
347+
348+
if ($char === null) {
349+
break;
350+
}
351+
352+
if (! ctype_digit($char)) {
353+
break;
354+
}
355+
356+
$this->advance();
357+
}
358+
359+
return new Number(substr($this->buffer, $start, $this->position - $start));
360+
}
361+
303362
/**
304363
* Reads an atom token.
305364
*
@@ -311,6 +370,7 @@ protected function readAtom(): Atom
311370

312371
while (true) {
313372
$this->ensureBuffer(1);
373+
314374
$char = $this->currentChar();
315375

316376
if ($char === null) {

src/Connection/Tokens/Number.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace DirectoryTree\ImapEngine\Connection\Tokens;
4+
5+
class Number extends Token {}

src/MessageQuery.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder;
99
use DirectoryTree\ImapEngine\Connection\Responses\MessageResponseParser;
1010
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
11-
use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
11+
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
1212
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
1313
use DirectoryTree\ImapEngine\Enums\ImapFlag;
1414
use DirectoryTree\ImapEngine\Exceptions\ImapCommandException;
@@ -305,7 +305,7 @@ protected function search(): Collection
305305
]);
306306

307307
return new Collection(array_map(
308-
fn (Atom $token) => $token->value,
308+
fn (Token $token) => $token->value,
309309
$response->tokensAfter(2)
310310
));
311311
}

tests/Unit/Connection/ImapTokenizerTest.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use DirectoryTree\ImapEngine\Connection\Tokens\ListClose;
99
use DirectoryTree\ImapEngine\Connection\Tokens\ListOpen;
1010
use DirectoryTree\ImapEngine\Connection\Tokens\Literal;
11+
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
1112
use DirectoryTree\ImapEngine\Connection\Tokens\QuotedString;
1213
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeClose;
1314
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeOpen;
@@ -257,7 +258,7 @@
257258
expect($token->value)->toBe('UIDNEXT');
258259

259260
$token = $tokenizer->nextToken();
260-
expect($token)->toBeInstanceOf(Atom::class);
261+
expect($token)->toBeInstanceOf(Number::class);
261262
expect($token->value)->toBe('1000');
262263

263264
$token = $tokenizer->nextToken();
@@ -334,6 +335,28 @@
334335
],
335336
]);
336337

338+
test('tokenizer handles edge cases', function (string $feed, Token ...$expectedTokens) {
339+
$stream = new FakeStream;
340+
$stream->open();
341+
342+
$stream->feed($feed);
343+
344+
$tokenizer = new ImapTokenizer($stream);
345+
346+
foreach ($expectedTokens as $expectedToken) {
347+
$actualToken = $tokenizer->nextToken();
348+
349+
expect($actualToken)->toBeInstanceOf(get_class($expectedToken));
350+
expect($actualToken->value)->toBe($expectedToken->value);
351+
}
352+
})->with([
353+
['UID 48273)', new Atom('UID'), new Number('48273'), new ListClose(')')],
354+
['* 23 EXISTS', new Atom('*'), new Number('23'), new Atom('EXISTS')],
355+
['OK (0.002 secs)', new Atom('OK'), new ListOpen('('), new Atom('0.002'), new Atom('secs'), new ListClose(')')],
356+
['OK 404NotFound', new Atom('OK'), new Atom('404NotFound')],
357+
['A1 OK [UIDNEXT 1000]', new Atom('A1'), new Atom('OK'), new ResponseCodeOpen('['), new Atom('UIDNEXT'), new Number('1000'), new ResponseCodeClose(']')],
358+
]);
359+
337360
test('all tokens implement the token interface', function (string $data) {
338361
$stream = new FakeStream;
339362
$stream->open();

0 commit comments

Comments
 (0)