Skip to content

Commit 5a92c75

Browse files
authored
🔀 Merge pull request #201 from ruby/parser/better-faster-cleaner-response_data
âš¡ Simpler, faster `response-data` parser
2 parents a054a09 + 5a70326 commit 5a92c75

File tree

1 file changed

+89
-58
lines changed

1 file changed

+89
-58
lines changed

‎lib/net/imap/response_parser.rb

Lines changed: 89 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,14 @@ module RFC3629
210210
TEXT_rev1 = /#{TEXT_CHAR}+/
211211
TEXT_rev2 = /#{Regexp.union TEXT_CHAR, UTF8_2, UTF8_3, UTF8_4}+/
212212

213+
# tagged-label-fchar = ALPHA / "-" / "_" / "."
214+
TAGGED_LABEL_FCHAR = /[a-zA-Z\-_.]/n
215+
# tagged-label-char = tagged-label-fchar / DIGIT / ":"
216+
TAGGED_LABEL_CHAR = /[a-zA-Z\-_.0-9:]*/n
217+
# tagged-ext-label = tagged-label-fchar *tagged-label-char
218+
# ; Is a valid RFC 3501 "atom".
219+
TAGGED_EXT_LABEL = /#{TAGGED_LABEL_FCHAR}#{TAGGED_LABEL_CHAR}*/n
220+
213221
# RFC3501:
214222
# literal = "{" number "}" CRLF *CHAR8
215223
# ; Number represents the number of CHAR8s
@@ -284,6 +292,7 @@ def unescape_quoted(quoted)
284292

285293
def_char_matchers :SP, " ", :T_SPACE
286294
def_char_matchers :PLUS, "+", :T_PLUS
295+
def_char_matchers :STAR, "*", :T_STAR
287296

288297
def_char_matchers :lpar, "(", :T_LPAR
289298
def_char_matchers :rpar, ")", :T_RPAR
@@ -406,6 +415,13 @@ def case_insensitive__nstring
406415
alias number64 number
407416
alias number64? number?
408417

418+
# valid number ranges are not enforced by parser
419+
# nz-number = digit-nz *DIGIT
420+
# ; Non-zero unsigned 32-bit integer
421+
# ; (0 < n < 4,294,967,296)
422+
alias nz_number number
423+
alias nz_number? number?
424+
409425
# [RFC3501 & RFC9051:]
410426
# response = *(continue-req / response-data) response-done
411427
#
@@ -439,47 +455,64 @@ def continue_req
439455
ContinuationRequest.new(SP? ? resp_text : ResponseText::EMPTY, @str)
440456
end
441457

458+
RE_RESPONSE_TYPE = /\G(?:\d+ )?(?<type>#{Patterns::TAGGED_EXT_LABEL})/n
459+
460+
# [RFC3501:]
461+
# response-data = "*" SP (resp-cond-state / resp-cond-bye /
462+
# mailbox-data / message-data / capability-data) CRLF
463+
# [RFC4466:]
464+
# response-data = "*" SP response-payload CRLF
465+
# response-payload = resp-cond-state / resp-cond-bye /
466+
# mailbox-data / message-data / capability-data
467+
# RFC5161 (ENABLE capability):
468+
# response-data =/ "*" SP enable-data CRLF
469+
# RFC5255 (LANGUAGE capability)
470+
# response-payload =/ language-data
471+
# RFC5255 (I18NLEVEL=1 and I18NLEVEL=2 capabilities)
472+
# response-payload =/ comparator-data
473+
# [RFC9051:]
474+
# response-data = "*" SP (resp-cond-state / resp-cond-bye /
475+
# mailbox-data / message-data / capability-data /
476+
# enable-data) CRLF
477+
#
478+
# [merging in greeting and response-fatal:]
479+
# greeting = "*" SP (resp-cond-auth / resp-cond-bye) CRLF
480+
# response-fatal = "*" SP resp-cond-bye CRLF
481+
# response-data =/ "*" SP (resp-cond-auth / resp-cond-bye) CRLF
482+
# [removing duplicates, this is simply]
483+
# response-payload =/ resp-cond-auth
484+
#
485+
# TODO: remove resp-cond-auth and handle greeting separately
442486
def response_data
443-
match(T_STAR)
444-
match(T_SPACE)
445-
token = lookahead
446-
if token.symbol == T_NUMBER
447-
return numeric_response
448-
elsif token.symbol == T_ATOM
449-
case token.value
450-
when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
451-
return response_cond
452-
when /\A(?:FLAGS)\z/ni
453-
return flags_response
454-
when /\A(?:ID)\z/ni
455-
return id_response
456-
when /\A(?:LIST|LSUB|XLIST)\z/ni
457-
return list_response
458-
when /\A(?:NAMESPACE)\z/ni
459-
return namespace_response
460-
when /\A(?:QUOTA)\z/ni
461-
return getquota_response
462-
when /\A(?:QUOTAROOT)\z/ni
463-
return getquotaroot_response
464-
when /\A(?:ACL)\z/ni
465-
return getacl_response
466-
when /\A(?:SEARCH|SORT)\z/ni
467-
return search_response
468-
when /\A(?:THREAD)\z/ni
469-
return thread_response
470-
when /\A(?:STATUS)\z/ni
471-
return status_response
472-
when /\A(?:CAPABILITY)\z/ni
473-
return capability_data__untagged
474-
when /\A(?:NOOP)\z/ni
475-
return ignored_response
476-
when /\A(?:ENABLED)\z/ni
477-
return enable_data
478-
else
479-
return unparsed_response
480-
end
481-
else
482-
parse_error("unexpected token %s", token.symbol)
487+
STAR!; SP!
488+
m = peek_re(RE_RESPONSE_TYPE) or parse_error("unparsable response")
489+
case m["type"].upcase
490+
when "OK" then response_cond # RFC3501, RFC9051
491+
when "FETCH" then message_data__fetch # RFC3501, RFC9051
492+
when "EXPUNGE" then message_data__expunge # RFC3501, RFC9051
493+
when "EXISTS" then mailbox_data__exists # RFC3501, RFC9051
494+
when "SEARCH" then search_response # RFC3501 (obsolete)
495+
when "CAPABILITY" then capability_data__untagged # RFC3501, RFC9051
496+
when "FLAGS" then flags_response # RFC3501, RFC9051
497+
when "LIST" then list_response # RFC3501, RFC9051
498+
when "STATUS" then status_response # RFC3501, RFC9051
499+
when "NAMESPACE" then namespace_response # RFC2342, RFC9051
500+
when "ENABLED" then enable_data # RFC5161, RFC9051
501+
when "BAD" then response_cond # RFC3501, RFC9051
502+
when "NO" then response_cond # RFC3501, RFC9051
503+
when "PREAUTH" then response_cond # RFC3501, RFC9051
504+
when "BYE" then response_cond # RFC3501, RFC9051
505+
when "RECENT" then mailbox_data__recent # RFC3501 (obsolete)
506+
when "SORT" then sort_data # RFC5256, RFC7162
507+
when "THREAD" then thread_response # RFC5256
508+
when "QUOTA" then getquota_response # RFC2087, RFC9208
509+
when "QUOTAROOT" then getquotaroot_response # RFC2087, RFC9208
510+
when "ID" then id_response # RFC2971
511+
when "ACL" then getacl_response # RFC4314
512+
when "LSUB" then list_response # RFC3501 (obsolete)
513+
when "XLIST" then list_response # deprecated
514+
when "NOOP" then ignored_response
515+
else unparsed_response
483516
end
484517
end
485518

@@ -520,26 +553,24 @@ def response_cond
520553
return UntaggedResponse.new(name, resp_text, @str)
521554
end
522555

523-
def numeric_response
524-
n = number
525-
match(T_SPACE)
526-
token = match(T_ATOM)
527-
name = token.value.upcase
528-
case name
529-
when "EXISTS", "RECENT", "EXPUNGE"
530-
return UntaggedResponse.new(name, n, @str)
531-
when "FETCH"
532-
shift_token
533-
match(T_SPACE)
534-
data = FetchData.new(n, msg_att(n))
535-
return UntaggedResponse.new(name, data, @str)
536-
else
537-
klass = name == "NOOP" ? IgnoredResponse : UntaggedResponse
538-
SP?; txt = remaining_unparsed
539-
klass.new(name, UnparsedData.new(n, txt), @str)
540-
end
556+
# message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
557+
def message_data__fetch
558+
seq = nz_number; SP!
559+
name = label "FETCH"; SP!
560+
data = FetchData.new(seq, msg_att(seq))
561+
UntaggedResponse.new(name, data, @str)
562+
end
563+
564+
def response_data__simple_numeric
565+
data = nz_number; SP!
566+
name = tagged_ext_label
567+
UntaggedResponse.new(name, data, @str)
541568
end
542569

570+
alias message_data__expunge response_data__simple_numeric
571+
alias mailbox_data__exists response_data__simple_numeric
572+
alias mailbox_data__recent response_data__simple_numeric
573+
543574
def msg_att(n)
544575
match(T_LPAR)
545576
attr = {}

0 commit comments

Comments
 (0)