Skip to content

Commit

Permalink
🥅 Validate response-tagged in the parser
Browse files Browse the repository at this point in the history
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*<any ASTRING-CHAR except "+">
```

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).
  • Loading branch information
nevans committed Oct 21, 2023
1 parent 609acd9 commit 6c3056c
Showing 1 changed file with 34 additions and 15 deletions.
49 changes: 34 additions & 15 deletions lib/net/imap/response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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*<any ASTRING-CHAR except "+">
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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -452,13 +471,13 @@ def response_untagged
end
end

# RFC3501 & RFC9051:
# response-tagged = tag SP resp-cond-state CRLF
# tag = 1*<any ASTRING-CHAR except "+">
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
Expand Down

0 comments on commit 6c3056c

Please sign in to comment.