Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions lib/net/imap/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,99 @@ def response_size_msg
end

# Error raised when a response from the server is non-parsable.
#
# NOTE: Parser attributes are provided for debugging and inspection only.
# Their names and semantics may change incompatibly in any release.
class ResponseParseError < Error
# Net::IMAP::ResponseParser, unless a custom parser produced the error.
attr_reader :parser_class

# The full raw response string which was being parsed.
attr_reader :string

# The parser's byte position in #string when the error was raised.
#
# _NOTE:_ This attribute is provided for debugging and inspection only.
# Its name and semantics may change incompatibly in any release.
attr_reader :pos

# The parser's lex state
#
# _NOTE:_ This attribute is provided for debugging and inspection only.
# Its name and semantics may change incompatibly in any release.
attr_reader :lex_state

# The last lexed token
#
# May be +nil+ when the parser has accepted the last token and peeked at
# the next byte without generating a token.
#
# _NOTE:_ This attribute is provided for debugging and inspection only.
# Its name and semantics may change incompatibly in any release.
attr_reader :token

def initialize(message = "unspecified parse error",
parser_class: Net::IMAP::ResponseParser,
parser_state: nil,
string: parser_state&.at(0), # see ParserUtils#parser_state
lex_state: parser_state&.at(1), # see ParserUtils#parser_state
pos: parser_state&.at(2), # see ParserUtils#parser_state
token: parser_state&.at(3)) # see ParserUtils#parser_state
@parser_class = parser_class
@string = string
@pos = pos
@lex_state = lex_state
@token = token
super(message)
end

# When +parser_state+ is true, debug info about the parser state is
# included. Defaults to the value of Net::IMAP.debug.
#
# When +parser_backtrace+ is true, a simplified backtrace is included,
# containing only frames which belong to methods in parser_class. Most
# parser method names are based on rules in the IMAP grammar.
def detailed_message(parser_state: Net::IMAP.debug,
parser_backtrace: false,
**)
return super unless parser_state || parser_backtrace
msg = super.dup
if parser_state && (string || pos || lex_state || token)
msg << "\n processed : %p" % processed_string
msg << "\n remaining : %p" % remaining_string
msg << "\n pos : %p" % pos
msg << "\n lex_state : %p" % lex_state
msg << "\n token : "
if token
msg << "%p => %p" % [token.symbol, token.value]
else
msg << "nil"
end
end
if parser_backtrace
backtrace_locations&.each_with_index do |loc, idx|
next if loc.base_label.include? "parse_error"
break if loc.base_label == "parse"
next unless loc.label.include?(parser_class.name)
msg << "\n caller[%2d]: %-30s (%s:%d)" % [
idx,
loc.base_label,
File.basename(loc.path, ".rb"),
loc.lineno
]
end
end
msg
rescue => error
msg ||= super.dup
msg << "\n BUG in %s#%s: %s" % [self.class, __method__,
error.detailed_message]
msg
end

def processed_string = string && pos && string[...pos]
def remaining_string = string && pos && string[pos..]

end

# Superclass of all errors used to encapsulate "fail" responses
Expand Down
31 changes: 12 additions & 19 deletions lib/net/imap/response_parser/parser_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -216,29 +216,22 @@ def shift_token
end

def parse_error(fmt, *args)
msg = format(fmt, *args)
raise exception format(fmt, *args)
rescue ResponseParseError => error
if config.debug?
local_path = File.dirname(__dir__)
tok = @token ? "%s: %p" % [@token.symbol, @token.value] : "nil"
warn "%s %s: %s" % [self.class, __method__, msg]
warn " tokenized : %s" % [@str[...@pos].dump]
warn " remaining : %s" % [@str[@pos..].dump]
warn " @lex_state: %s" % [@lex_state]
warn " @pos : %d" % [@pos]
warn " @token : %s" % [tok]
caller_locations(1..20).each_with_index do |cloc, idx|
next unless cloc.path&.start_with?(local_path)
warn " caller[%2d]: %-30s (%s:%d)" % [
idx,
cloc.base_label,
File.basename(cloc.path, ".rb"),
cloc.lineno
]
end
warn error.detailed_message(parser_state: true,
parser_backtrace: true)
end
raise ResponseParseError, msg
raise
end

def exception(message) = ResponseParseError.new(
message, parser_state:, parser_class: self.class
)

def current_state = [@lex_state, @pos, @token]
def parser_state = [@str, *current_state]

end
end
end
Expand Down
85 changes: 85 additions & 0 deletions test/net/imap/test_errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,91 @@
require "test/unit"

class IMAPErrorsTest < Net::IMAP::TestCase
Token = Net::IMAP::ResponseParser::Token
CSI = "\e["
def self.CSI(*args) = CSI + args.join
def self.SGR(*attr) = CSI attr.join(?;), ?m
RESET = SGR "" # could also use 0
BOLD = SGR 1
BOLD_UNDERLINE = SGR 1, 4

test "ResponseParseError" do
# The first examples don't add parser state, so this makes no difference.
# It affects the last example, which has parser state.
Net::IMAP.debug = true

name = Net::IMAP::ResponseParseError.name

msg = "unspecified parse error"
err = Net::IMAP::ResponseParseError.new
assert_equal msg, err.message
assert_equal Net::IMAP::ResponseParser, err.parser_class
assert_nil err.string
assert_nil err.pos
assert_nil err.token
assert_nil err.lex_state
assert_equal("#{msg} (#{name})", err.detailed_message(parser_state: false))
assert_equal("#{msg} (#{name})", err.detailed_message)
assert_equal(
"#{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET}",
err.detailed_message(highlight: true)
)

msg = "unexpected ATOM (expected NSTRING)"
err = Net::IMAP::ResponseParseError.new(msg)
assert_equal msg, err.message
assert_nil err.string
assert_nil err.pos
assert_nil err.token
assert_nil err.lex_state
assert_equal("#{msg} (#{name})", err.detailed_message(parser_state: false))
assert_equal("#{msg} (#{name})", err.detailed_message)
assert_equal(
"#{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET}",
err.detailed_message(highlight: true)
)

msg = "unexpected QUOTED (expected \"]\")"
string = "tag OK [Error=\"Microsoft.Exchange.Error: foo\"] done\r\n"
token = Net::IMAP::ResponseParser::Token[:QUOTED, string[15, 29]]
parser_state = [string, :EXPR_BEG, 45, token]
err = Net::IMAP::ResponseParseError.new(msg, string:, parser_state:)
assert_equal msg, err.message
assert_equal string, err.string
assert_equal 45, err.pos
assert_same token, err.token
assert_equal :EXPR_BEG, err.lex_state
assert_equal msg, err.message
assert_equal("#{msg} (#{name})", err.detailed_message(parser_state: false))
assert_equal(
"#{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET}",
err.detailed_message(highlight: true, parser_state: false)
)
assert_equal(<<~MSG.strip, err.detailed_message)
#{msg} (#{name})
processed : "tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""
remaining : "] done\\r\\n"
pos : 45
lex_state : :EXPR_BEG
token : :QUOTED => "Microsoft.Exchange.Error: foo"
MSG
assert_equal(<<~MSG.strip, err.detailed_message(highlight: true))
#{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET}
processed : "tag OK [Error=\\"Microsoft.Exchange.Error: foo\\""
remaining : "] done\\r\\n"
pos : 45
lex_state : :EXPR_BEG
token : :QUOTED => "Microsoft.Exchange.Error: foo"
MSG

# `parser_state` defaults to `Net::IMAP.debug`:
Net::IMAP.debug = false
assert_equal("#{msg} (#{name})", err.detailed_message)
assert_equal(
"#{BOLD}#{msg} (#{BOLD_UNDERLINE}#{name}#{RESET}#{BOLD})#{RESET}",
err.detailed_message(highlight: true)
)
end

test "ResponseTooLargeError" do
err = Net::IMAP::ResponseTooLargeError.new
Expand Down