From 6c3056c9e869aa01a5ed5f36ae9ea3215ee8c3d5 Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 14 Feb 2023 00:13:11 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=85=20Validate=20`response-tagged`=20i?= =?UTF-8?q?n=20the=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `response-tagged` parser now enforces `resp-cond-state`, raising `InvalidResponseError` for any invalid status conditions. It is also refactored to the new parser style. An explicit method for `tag` is added, which is parsed similarly to `astring_chars` and `atom`. ```abnf response-tagged = tag SP resp-cond-state CRLF resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text ; Status condition tag = 1* ``` Currently, any exception raised by the parser will abruptly drop the connection. In the future, if the response handler thread is made more robust against recoverable errors, `InvalidResponseError` should be considered non-recoverable. (There is a test that covers this already). --- lib/net/imap/response_parser.rb | 49 +++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index c32714289..cfea6585c 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -320,10 +320,13 @@ def unescape_quoted(quoted) ASTRING_TOKENS = [T_QUOTED, *ASTRING_CHARS_TOKENS, T_LITERAL].freeze - # atom = 1*ATOM-CHAR - # - # TODO: match atom entirely by regexp (in the "lexer") - def atom; -combine_adjacent(*ATOM_TOKENS) end + # tag = 1* + TAG_TOKENS = (ASTRING_CHARS_TOKENS - [T_PLUS]).freeze + + # TODO: handle atom, astring_chars, and tag entirely inside the lexer + def atom; combine_adjacent(*ATOM_TOKENS) end + def astring_chars; combine_adjacent(*ASTRING_CHARS_TOKENS) end + def tag; combine_adjacent(*TAG_TOKENS) end # the #accept version of #atom def atom?; -combine_adjacent(*ATOM_TOKENS) if lookahead?(*ATOM_TOKENS) end @@ -336,11 +339,6 @@ def case_insensitive__atom? -combine_adjacent(*ATOM_TOKENS).upcase if lookahead?(*ATOM_TOKENS) end - # TODO: handle astring_chars entirely inside the lexer - def astring_chars - combine_adjacent(*ASTRING_CHARS_TOKENS) - end - # astring = 1*ASTRING-CHAR / string def astring lookahead?(*ASTRING_CHARS_TOKENS) ? astring_chars : string @@ -357,6 +355,27 @@ def label(word) parse_error("unexpected atom %p, expected %p instead", val, word) end + # Use #label or #label_in to assert specific known labels + # (+tagged-ext-label+ only, not +atom+). + def label_in(*labels) + if labels.include?(actual = tagged_ext_label) + actual + else + parse_error("unexpected atom %p, expected one of %s instead", + actual, labels.join(" or ")) + end + end + + # asserts specific known labels, raises InvalidResponseError on failure + def response_type_restricted_to(*types) + label_in(*types) + rescue ResponseParseError => err + raise InvalidResponseError, err.message + .gsub(/\bunexpected\b/, "invalid") + .gsub(/\bexpected\b/, "required") + .gsub(/\b(label|astring|atom|token)\b/, "response type") + end + # nstring = string / nil def nstring NIL? ? nil : string @@ -452,13 +471,13 @@ def response_untagged end end + # RFC3501 & RFC9051: + # response-tagged = tag SP resp-cond-state CRLF + # tag = 1* def response_tagged - tag = astring_chars - match(T_SPACE) - token = match(T_ATOM) - name = token.value.upcase - match(T_SPACE) - return TaggedResponse.new(tag, name, resp_text, @str) + tag = self.tag; SP! + name = response_type_restricted_to("OK", "NO", "BAD"); SP! + TaggedResponse.new(tag, name, resp_text, @str) end def response_cond