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
24 changes: 12 additions & 12 deletions lib/net/imap/response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2212,10 +2212,7 @@ def next_token
if $1
return Token.new(T_SPACE, $+)
elsif $2
len = $+.to_i
val = @str[@pos, len]
@pos += len
return Token.new(T_LITERAL8, val)
literal_token($+, T_LITERAL8)
elsif $3 && $7
# greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
return Token.new(T_ATOM, $3)
Expand Down Expand Up @@ -2243,10 +2240,7 @@ def next_token
elsif $15
return Token.new(T_RBRA, $+)
elsif $16
len = $+.to_i
val = @str[@pos, len]
@pos += len
return Token.new(T_LITERAL, val)
literal_token($+)
elsif $17
return Token.new(T_PERCENT, $+)
elsif $18
Expand All @@ -2272,10 +2266,7 @@ def next_token
elsif $4
return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
elsif $5
len = $+.to_i
val = @str[@pos, len]
@pos += len
return Token.new(T_LITERAL, val)
literal_token($+)
elsif $6
return Token.new(T_LPAR, $+)
elsif $7
Expand All @@ -2290,6 +2281,15 @@ def next_token
else
parse_error("invalid @lex_state - %s", @lex_state.inspect)
end
rescue DataFormatError => error
parse_error error.message
end

def literal_token(len, type = T_LITERAL)
len = NumValidator.coerce_number64 len
val = @str[@pos, len]
@pos += len
Token.new(type, val)
end

end
Expand Down
7 changes: 6 additions & 1 deletion lib/net/imap/response_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module Net
class IMAP
# See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2
class ResponseReader # :nodoc:
include NumValidator

attr_reader :client

def initialize(client, sock)
Expand Down Expand Up @@ -46,7 +48,10 @@ def done? = line_done? && !literal_size
def line_done? = buff.end_with?(CRLF)

def get_literal_size(buff)
buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) &&
coerce_number64($1)
rescue DataFormatError
raise DataFormatError, format("invalid response literal size (%s)", $1)
end

def read_line
Expand Down
16 changes: 16 additions & 0 deletions test/net/imap/fixtures/response_parser/quirky_behaviors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@
data:
raw_data: "* NOOP\r\n"

"literal numeric formatted with zero-prefix":
:response: "* 20367 FETCH (BODY[HEADER.FIELDS (Foo)] {012}\r\nFoo: bar\r\n\r\n)\r\n"
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
name: FETCH
data: !ruby/struct:Net::IMAP::FetchData
seqno: 20367
attr:
BODY[HEADER.FIELDS (Foo)]: "Foo: bar\r\n\r\n"
raw_data: "* 20367 FETCH (BODY[HEADER.FIELDS (Foo)] {012}\r\nFoo: bar\r\n\r\n)\r\n"

"invalid literal numeric format (too large)":
:test_type: :assert_parse_failure
:message: "number64 must be unsigned 63-bit integer: 99999999999999999999"
:response:
"* 20367 FETCH (BODY[] {99999999999999999999}\r\nwon't parse this)\r\n"

test_invalid_noop_response_with_unparseable_data:
:response: "* NOOP froopy snood\r\n"
:expected: !ruby/struct:Net::IMAP::IgnoredResponse
Expand Down
21 changes: 21 additions & 0 deletions test/net/imap/test_response_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def literal(str) = "{#{str.bytesize}}\r\n#{str}"
zero_literal = "tag ok #{literal ""} #{literal ""}\r\n"
illegal_crs = "tag ok #{many_crs} #{many_crs}\r\n"
illegal_lfs = "tag ok #{literal "\r"}\n#{literal "\r"}\n\r\n"
zero_padded = "+ {010}\r\n1234567890\r\n" # NOTE: it's decimal, not octal!
goofy_zero = "+ {000}\r\n\r\n"
io = StringIO.new([
simple,
long_line,
Expand All @@ -33,6 +35,8 @@ def literal(str) = "{#{str.bytesize}}\r\n#{str}"
zero_literal,
illegal_crs,
illegal_lfs,
zero_padded,
goofy_zero,
simple,
].join)
rcvr = Net::IMAP::ResponseReader.new(client, io)
Expand All @@ -43,6 +47,8 @@ def literal(str) = "{#{str.bytesize}}\r\n#{str}"
assert_equal zero_literal, rcvr.read_response_buffer.to_str
assert_equal illegal_crs, rcvr.read_response_buffer.to_str
assert_equal illegal_lfs, rcvr.read_response_buffer.to_str
assert_equal zero_padded, rcvr.read_response_buffer.to_str
assert_equal goofy_zero, rcvr.read_response_buffer.to_str
assert_equal simple, rcvr.read_response_buffer.to_str
assert_equal "", rcvr.read_response_buffer.to_str
end
Expand Down Expand Up @@ -82,6 +88,21 @@ def literal(str) = "{#{str.bytesize}}\r\n#{str}"
end
end

data(
bad_int64: "+ {99999999999999999999}\r\ndon't even try to read this...",
)
test "#read_response_buffer with invalid literal size" do |invalid|
client = FakeClient.new
client.config.max_response_size = nil # any size is allowed!
io = StringIO.new(invalid, "rb")
rcvr = Net::IMAP::ResponseReader.new(client, io)
assert_raise Net::IMAP::DataFormatError do
result = rcvr.read_response_buffer
flunk "Got result: %p" % [result]
end
# assert io.closed?
end

test "linear performance detecting literal continuation" do
omit_unless_cruby "flaky on different platforms"
omit_if(ENV["CI"], "slow and flaky, skipping in CI")
Expand Down
Loading