Skip to content

Commit 8070925

Browse files
committed
⚡️ Parse STATUS faster, using RFC4466 grammar
Although "number" is still the default `status-att-val`, this uses ExtensionData with RFC4466's `tagged_ext_val` for any unknown non-numeric `STATUS` attribute. Running the benchmarks (on my phone, without YJIT) shows a 40% speedup! invalid_status_response_trailing_space v0.4.4-16-g0be6b65b: 43956.1 i/s 0.4.4: 31788.6 i/s - 1.38x slower rfc3501_7.2.4_STATUS_response_example v0.4.4-16-g0be6b65b: 45436.2 i/s 0.4.4: 32458.5 i/s - 1.40x slower status_response_uidnext_uidvalidity v0.4.4-16-g0be6b65b: 45334.2 i/s 0.4.4: 32709.1 i/s - 1.39x slower Various changes: * Add alias for `mailbox` to `astring`. * Use char token matchers (faster than `match(T_#{name})`). * Extract `status-att-list` and `status-att-val` methods, to mimic ABNF. * Add a case statement to `status-att-val` and explicitly match all RFC3501 and RFC9051 status attributes.
1 parent c52853e commit 8070925

File tree

2 files changed

+114
-23
lines changed

2 files changed

+114
-23
lines changed

lib/net/imap/response_parser.rb

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,16 @@ def tagged_ext_val
600600
end
601601
end
602602

603+
# mailbox = "INBOX" / astring
604+
# ; INBOX is case-insensitive. All case variants of
605+
# ; INBOX (e.g., "iNbOx") MUST be interpreted as INBOX
606+
# ; not as an astring. An astring which consists of
607+
# ; the case-insensitive sequence "I" "N" "B" "O" "X"
608+
# ; is considered to be INBOX and not an astring.
609+
# ; Refer to section 5.1 for further
610+
# ; semantic details of mailbox names.
611+
alias mailbox astring
612+
603613
# valid number ranges are not enforced by parser
604614
# number64 = 1*DIGIT
605615
# ; Unsigned 63-bit integer
@@ -1508,31 +1518,79 @@ def thread_branch(token)
15081518
return rootmember
15091519
end
15101520

1521+
# mailbox-data =/ "STATUS" SP mailbox SP "(" [status-att-list] ")"
15111522
def mailbox_data__status
1512-
token = match(T_ATOM)
1513-
name = token.value.upcase
1514-
match(T_SPACE)
1515-
mailbox = astring
1516-
match(T_SPACE)
1517-
match(T_LPAR)
1518-
attr = {}
1519-
while true
1520-
token = lookahead
1521-
case token.symbol
1522-
when T_RPAR
1523-
shift_token
1524-
break
1525-
when T_SPACE
1526-
shift_token
1523+
resp_name = label("STATUS"); SP!
1524+
mbox_name = mailbox; SP!
1525+
lpar; attr = status_att_list; rpar
1526+
UntaggedResponse.new(resp_name, StatusData.new(mbox_name, attr), @str)
1527+
end
1528+
1529+
# RFC3501
1530+
# status-att-list = status-att SP number *(SP status-att SP number)
1531+
# RFC4466, RFC9051, and RFC3501 Errata
1532+
# status-att-list = status-att-val *(SP status-att-val)
1533+
def status_att_list
1534+
attrs = [status_att_val]
1535+
while SP? do attrs << status_att_val end
1536+
attrs.to_h
1537+
end
1538+
1539+
# RFC3501 Errata:
1540+
# status-att-val = ("MESSAGES" SP number) / ("RECENT" SP number) /
1541+
# ("UIDNEXT" SP nz-number) / ("UIDVALIDITY" SP nz-number) /
1542+
# ("UNSEEN" SP number)
1543+
# RFC4466:
1544+
# status-att-val = ("MESSAGES" SP number) /
1545+
# ("RECENT" SP number) /
1546+
# ("UIDNEXT" SP nz-number) /
1547+
# ("UIDVALIDITY" SP nz-number) /
1548+
# ("UNSEEN" SP number)
1549+
# ;; Extensions to the STATUS responses
1550+
# ;; should extend this production.
1551+
# ;; Extensions should use the generic
1552+
# ;; syntax defined by tagged-ext.
1553+
# RFC9051:
1554+
# status-att-val = ("MESSAGES" SP number) /
1555+
# ("UIDNEXT" SP nz-number) /
1556+
# ("UIDVALIDITY" SP nz-number) /
1557+
# ("UNSEEN" SP number) /
1558+
# ("DELETED" SP number) /
1559+
# ("SIZE" SP number64)
1560+
# ; Extensions to the STATUS responses
1561+
# ; should extend this production.
1562+
# ; Extensions should use the generic
1563+
# ; syntax defined by tagged-ext.
1564+
# RFC7162:
1565+
# status-att-val =/ "HIGHESTMODSEQ" SP mod-sequence-valzer
1566+
# ;; Extends non-terminal defined in [RFC4466].
1567+
# ;; Value 0 denotes that the mailbox doesn't
1568+
# ;; support persistent mod-sequences
1569+
# ;; as described in Section 3.1.2.2.
1570+
# RFC7889:
1571+
# status-att-val =/ "APPENDLIMIT" SP (number / nil)
1572+
# ;; status-att-val is defined in RFC 4466
1573+
# RFC8438:
1574+
# status-att-val =/ "SIZE" SP number64
1575+
# RFC8474:
1576+
# status-att-val =/ "MAILBOXID" SP "(" objectid ")"
1577+
# ; follows tagged-ext production from [RFC4466]
1578+
def status_att_val
1579+
key = tagged_ext_label
1580+
SP!
1581+
val =
1582+
case key
1583+
when "MESSAGES" then number # RFC3501, RFC9051
1584+
when "UNSEEN" then number # RFC3501, RFC9051
1585+
when "DELETED" then number # RFC3501, RFC9051
1586+
when "UIDNEXT" then nz_number # RFC3501, RFC9051
1587+
when "UIDVALIDITY" then nz_number # RFC3501, RFC9051
1588+
when "RECENT" then number # RFC3501 (obsolete)
1589+
when "SIZE" then number64 # RFC8483, RFC9051
1590+
else
1591+
number? || ExtensionData.new(tagged_ext_val)
15271592
end
1528-
token = match(T_ATOM)
1529-
key = token.value.upcase
1530-
match(T_SPACE)
1531-
val = number
1532-
attr[key] = val
1533-
end
1534-
data = StatusData.new(mailbox, attr)
1535-
return UntaggedResponse.new(name, data, @str)
1593+
[key, val]
15361594
end
15371595

15381596
# The presence of "IMAP4rev1" or "IMAP4rev2" is unenforced here.

test/net/imap/fixtures/response_parser/status_responses.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,36 @@
3737
UIDNEXT: 1
3838
UIDVALIDITY: 1234
3939
raw_data: "* STATUS INBOX (UIDNEXT 1 UIDVALIDITY 1234) \r\n"
40+
41+
test_imaginary_status_response_using_tagged-ext-val:
42+
:response: &test_imaginary_status_response_using_tagged_ext_val
43+
"* STATUS mbox (num 1 seq 1234:5,*:789654 comp-empty ()
44+
comp-quoted (\"quoted string\") comp-astring (nil) comp-multi (1 \"str\"
45+
2:3,7:77 nil (nested (several (layers)))))\r\n"
46+
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
47+
name: STATUS
48+
data: !ruby/struct:Net::IMAP::StatusData
49+
mailbox: mbox
50+
attr:
51+
NUM: 1
52+
SEQ: !ruby/struct:Net::IMAP::ExtensionData
53+
data: !ruby/object:Net::IMAP::SequenceSet
54+
atom: 1234:5,*:789654
55+
COMP-EMPTY: !ruby/struct:Net::IMAP::ExtensionData
56+
data: []
57+
COMP-QUOTED: !ruby/struct:Net::IMAP::ExtensionData
58+
data:
59+
- quoted string
60+
COMP-ASTRING: !ruby/struct:Net::IMAP::ExtensionData
61+
data:
62+
- nil
63+
COMP-MULTI: !ruby/struct:Net::IMAP::ExtensionData
64+
data:
65+
- 1
66+
- str
67+
- 2:3,7:77
68+
- nil
69+
- - nested
70+
- - several
71+
- - layers
72+
raw_data: *test_imaginary_status_response_using_tagged_ext_val

0 commit comments

Comments
 (0)