net-imap 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,7 +51,196 @@ module Net
51
51
  end
52
52
 
53
53
  # Error raised when a response from the server is non-parsable.
54
+ #
55
+ # NOTE: Parser attributes are provided for debugging and inspection only.
56
+ # Their names and semantics may change incompatibly in any release.
54
57
  class ResponseParseError < Error
58
+ # returns "" for all highlights
59
+ ESC_NO_HL = Hash.new("").freeze
60
+ private_constant :ESC_NO_HL
61
+
62
+ # Translates hash[:"/foo"] to hash[:reset] when hash.key?(:foo), else ""
63
+ #
64
+ # TODO: DRY this up with Config::AttrTypeCoercion.safe
65
+ if defined?(::Ractor.shareable_proc)
66
+ default_highlight = Ractor.shareable_proc {|hash, key|
67
+ %r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
68
+ }
69
+ else
70
+ default_highlight = nil.instance_eval { Proc.new {|hash, key|
71
+ %r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
72
+ } }
73
+ ::Ractor.make_shareable(default_highlight) if defined?(::Ractor)
74
+ end
75
+
76
+ # ANSI highlights, but no colors
77
+ ESC_NO_COLOR = Hash.new(&default_highlight).update(
78
+ reset: "\e[m",
79
+ val: "\e[1m", # bold
80
+ alt: "\e[1;4m", # bold and underlined
81
+ sym: "\e[1m", # bold
82
+ label: "\e[1m", # bold
83
+ ).freeze
84
+ private_constant :ESC_NO_COLOR
85
+
86
+ # ANSI highlights, with color
87
+ ESC_COLORS = Hash.new(&default_highlight).update(
88
+ reset: "\e[m",
89
+ key: "\e[95m", # bright magenta
90
+ idx: "\e[34m", # blue
91
+ val: "\e[36;40m", # cyan on black (to ensure contrast)
92
+ alt: "\e[1;33;40m", # bold; yellow on black
93
+ sym: "\e[33;40m", # yellow on black
94
+ label: "\e[1m", # bold
95
+ nil: "\e[35m", # magenta
96
+ ).freeze
97
+ private_constant :ESC_COLORS
98
+
99
+ # Net::IMAP::ResponseParser, unless a custom parser produced the error.
100
+ attr_reader :parser_class
101
+
102
+ # The full raw response string which was being parsed.
103
+ attr_reader :string
104
+
105
+ # The parser's byte position in #string when the error was raised.
106
+ #
107
+ # _NOTE:_ This attribute is provided for debugging and inspection only.
108
+ # Its name and semantics may change incompatibly in any release.
109
+ attr_reader :pos
110
+
111
+ # The parser's lex state
112
+ #
113
+ # _NOTE:_ This attribute is provided for debugging and inspection only.
114
+ # Its name and semantics may change incompatibly in any release.
115
+ attr_reader :lex_state
116
+
117
+ # The last lexed token
118
+ #
119
+ # May be +nil+ when the parser has accepted the last token and peeked at
120
+ # the next byte without generating a token.
121
+ #
122
+ # _NOTE:_ This attribute is provided for debugging and inspection only.
123
+ # Its name and semantics may change incompatibly in any release.
124
+ attr_reader :token
125
+
126
+ def initialize(message = "unspecified parse error",
127
+ parser_class: Net::IMAP::ResponseParser,
128
+ parser_state: nil,
129
+ string: parser_state&.at(0), # see ParserUtils#parser_state
130
+ lex_state: parser_state&.at(1), # see ParserUtils#parser_state
131
+ pos: parser_state&.at(2), # see ParserUtils#parser_state
132
+ token: parser_state&.at(3)) # see ParserUtils#parser_state
133
+ @parser_class = parser_class
134
+ @string = string
135
+ @pos = pos
136
+ @lex_state = lex_state
137
+ @token = token
138
+ super(message)
139
+ end
140
+
141
+ # When +parser_state+ is true, debug info about the parser state is
142
+ # included. Defaults to the value of Net::IMAP.debug.
143
+ #
144
+ # When +parser_backtrace+ is true, a simplified backtrace is included,
145
+ # containing only frames for methods in parser_class (since ruby 3.4) or
146
+ # which have "net/imap/response_parser" in the path (before ruby 3.4).
147
+ # Most parser method names are based on rules in the IMAP grammar.
148
+ #
149
+ # When +highlight+ is not explicitly set, highlights may be enabled
150
+ # automatically, based on +TERM+ and +FORCE_COLOR+ environment variables.
151
+ #
152
+ # By default, +highlight+ uses colors from the basic ANSI palette. When
153
+ # +highlight_no_color+ is true or the +NO_COLOR+ environment variable is
154
+ # not empty, only monochromatic highlights are used: bold, underline, etc.
155
+ def detailed_message(parser_state: Net::IMAP.debug,
156
+ parser_backtrace: false,
157
+ highlight: default_highlight_from_env,
158
+ highlight_no_color: (ENV["NO_COLOR"] || "") != "",
159
+ **)
160
+ return super unless parser_state || parser_backtrace
161
+ msg = super.dup
162
+ esc = !highlight ? ESC_NO_HL : highlight_no_color ? ESC_NO_COLOR : ESC_COLORS
163
+ hl = ->str { str % esc }
164
+ val = ->str, val { hl[val.nil? ? "%{nil}%%p%{/nil}" : str] % val }
165
+ if parser_state && (string || pos || lex_state || token)
166
+ msg << hl["\n %{key}processed %{/key}: "] << val["%{val}%%p%{/val}", processed_string]
167
+ msg << hl["\n %{key}remaining %{/key}: "] << val["%{alt}%%p%{/alt}", remaining_string]
168
+ msg << hl["\n %{key}pos %{/key}: "] << val["%{val}%%p%{/val}", pos]
169
+ msg << hl["\n %{key}lex_state %{/key}: "] << val["%{sym}%%p%{/sym}", lex_state]
170
+ msg << hl["\n %{key}token %{/key}: "] << val[
171
+ "%{sym}%%<symbol>p%{/sym} => %{val}%%<value>p%{/val}", token&.to_h
172
+ ]
173
+ end
174
+ if parser_backtrace
175
+ normalized_parser_backtrace.each do |idx, path, lineno, label, base_label|
176
+ msg << "\n %s: %s (%s:%d)" % [
177
+ hl["%{key}caller[%{/key}%{idx}%%2d%{/idx}%{key}]%{/key}"] % idx,
178
+ hl["%{label}%%-30s%{/label}"] % base_label,
179
+ File.basename(path, ".rb"), lineno
180
+ ]
181
+ end
182
+ end
183
+ msg
184
+ rescue => error
185
+ msg ||= super.dup
186
+ msg << "\n BUG in %s#%s: %s" % [self.class, __method__,
187
+ error.detailed_message]
188
+ msg
189
+ end
190
+
191
+ def processed_string = string && pos && string[...pos]
192
+ def remaining_string = string && pos && string[pos..]
193
+
194
+ # Returns true when all attributes are equal, except for #backtrace and
195
+ # #backtrace_locations which are replaced with #parser_methods. This
196
+ # allows deserialized errors to be compared.
197
+ def ==(other)
198
+ return false if self.class != other.class
199
+ methods = parser_methods
200
+ other_methods = other.parser_methods
201
+ message == other.message &&
202
+ methods == other_methods &&
203
+ string == other.string &&
204
+ pos == other.pos &&
205
+ lex_state == other.lex_state &&
206
+ token == other.token
207
+ end
208
+
209
+ # Lists the methods (from #backtrace_locations or #backtrace) called on
210
+ # parser_class (since ruby 3.4) or which have "net/imap/response_parser"
211
+ # in the path (before ruby 3.4). Most parser method names are based on
212
+ # rules in the IMAP grammar.
213
+ def parser_methods = normalized_parser_backtrace.map(&:last)
214
+
215
+ private
216
+
217
+ def normalized_parser_backtrace
218
+ normalize_backtrace
219
+ .take_while {|_, _, _, _, base_label| base_label != "parse" }
220
+ .reject {|_, _, _, _, base_label| base_label.nil? }
221
+ .reject {|_, _, _, _, base_label| base_label.include? "parse_error" }
222
+ .select {|_, path, _, label, _|
223
+ if label.include?("#") # => Class#method, since ruby 3.4
224
+ label.include?(parser_class.name)
225
+ else
226
+ path.include?("net/imap/response_parser")
227
+ end
228
+ }
229
+ end
230
+
231
+ def normalize_backtrace
232
+ (backtrace_locations&.each_with_index&.map {|loc, idx|
233
+ [idx, loc.path, loc.lineno, loc.label, loc.base_label]
234
+ } || backtrace&.each_with_index&.map {|bt, idx|
235
+ [idx, *bt.match(/\A(\S+):(\d+):in [`'](.*?([\w]+[?!]?))'\z/)&.captures]
236
+ } || [])
237
+ end
238
+
239
+ def default_highlight_from_env
240
+ (ENV["FORCE_COLOR"] || "") !~ /\A(?:0|)\z/ ||
241
+ (ENV["TERM"] || "") !~ /\A(?:dumb|unknown|)\z/i
242
+ end
243
+
55
244
  end
56
245
 
57
246
  # Superclass of all errors used to encapsulate "fail" responses
@@ -75,9 +75,18 @@ module Net
75
75
  #
76
76
  # Net::IMAP::UnparsedData represents data for unknown response types or
77
77
  # unknown extensions to response types without a well-defined extension
78
- # grammar.
78
+ # grammar. UnparsedData represents the portion of the response which the
79
+ # parser has skipped over, without attempting to parse it.
79
80
  #
80
- # See also: UnparsedNumericResponseData, ExtensionData, IgnoredResponse
81
+ # parser = Net::IMAP::ResponseParser.new
82
+ # response = parser.parse "* X-UNKNOWN-TYPE can't parse this\r\n"
83
+ # response => Net::IMAP::UntaggedResponse(
84
+ # name: "X-UNKNOWN-TYPE",
85
+ # data: Net::IMAP::UnparsedData(unparsed_data: "can't parse this"),
86
+ # )
87
+ #
88
+ # See also: UnparsedNumericResponseData, ExtensionData, IgnoredResponse,
89
+ # InvalidParseData.
81
90
  class UnparsedData < Struct.new(:unparsed_data)
82
91
  ##
83
92
  # method: unparsed_data
@@ -86,6 +95,61 @@ module Net
86
95
  # The unparsed data
87
96
  end
88
97
 
98
+ # **Note:** This represents an intentionally _unstable_ API. Where
99
+ # instances of this class are returned, future releases may return a
100
+ # different (incompatible) object <em>without deprecation or warning</em>.
101
+ #
102
+ # When the response parser encounters a recoverable error,
103
+ # Net::IMAP::InvalidParseData represents that portion of the response which
104
+ # could not be parsed, allowing the parser to parse the remainder of the
105
+ # response. InvalidParseData is always associated with a ResponseParseError
106
+ # which has been rescued.
107
+ #
108
+ # This could be caused by a malformed server response, by a bug in
109
+ # Net::IMAP::ResponseParser, or by an unsupported extension to the response
110
+ # syntax. For example, if a server supports +UIDPLUS+, but sends an invalid
111
+ # +COPYUID+ response code:
112
+ #
113
+ # parser = Net::IMAP::ResponseParser.new
114
+ # parsed = parser.parse "* OK [COPYUID 701 ] copied one message\r\n"
115
+ # parsed => {
116
+ # data: Net::IMAP::ResponseText(
117
+ # code: Net::IMAP::ResponseCode(
118
+ # name: "COPYUID",
119
+ # data: Net::IMAP::InvalidParseData(
120
+ # parse_error: Net::IMAP::ResponseParseError,
121
+ # unparsed_data: "701 ",
122
+ # parsed_data: nil,
123
+ # )
124
+ # )
125
+ # )
126
+ # }
127
+ #
128
+ # In this example, although <tt>[COPYUID 701 ]</tt> uses valid syntax for a
129
+ # _generic_ ResponseCode, it is _invalid_ syntax for a +COPYUID+ response
130
+ # code.
131
+ #
132
+ # See also: UnparsedData, ExtensionData
133
+ class InvalidParseData < Data.define(:parse_error, :unparsed_data, :parsed_data)
134
+ ##
135
+ # method: parse_error
136
+ # :call-seq: parse_error -> ResponseParseError
137
+ #
138
+ # Returns the rescued ResponseParseError.
139
+
140
+ ##
141
+ # method: unparsed_data
142
+ # :call-seq: unparsed_data -> string
143
+ #
144
+ # Returns the raw string which was skipped over by the parser.
145
+
146
+ ##
147
+ # method: parsed_data
148
+ #
149
+ # May return a partial parse result for unparsed_data, which had already
150
+ # been parsed before the parse_error.
151
+ end
152
+
89
153
  # **Note:** This represents an intentionally _unstable_ API. Where
90
154
  # instances of this class are returned, future releases may return a
91
155
  # different (incompatible) object <em>without deprecation or warning</em>.
@@ -93,7 +157,17 @@ module Net
93
157
  # Net::IMAP::UnparsedNumericResponseData represents data for unhandled
94
158
  # response types with a numeric prefix. See the documentation for #number.
95
159
  #
96
- # See also: UnparsedData, ExtensionData, IgnoredResponse
160
+ # parser = Net::IMAP::ResponseParser.new
161
+ # response = parser.parse "* 123 X-UNKNOWN-TYPE can't parse this\r\n"
162
+ # response => Net::IMAP::UntaggedResponse(
163
+ # name: "X-UNKNOWN-TYPE",
164
+ # data: Net::IMAP::UnparsedNumericData(
165
+ # number: 123,
166
+ # unparsed_data: "can't parse this"
167
+ # ),
168
+ # )
169
+ #
170
+ # See also: UnparsedData, ExtensionData, IgnoredResponse, InvalidParseData
97
171
  class UnparsedNumericResponseData < Struct.new(:number, :unparsed_data)
98
172
  ##
99
173
  # method: number
@@ -306,6 +380,14 @@ module Net
306
380
  # because the server doesn't allow deletion of mailboxes with children.
307
381
  # #data is +nil+.
308
382
  #
383
+ # === <tt>QUOTA=RES-*</tt> response codes
384
+ # See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
385
+ # * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
386
+ # with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
387
+ # the command would put the target mailbox over any quota, and with an
388
+ # untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
389
+ # be external events). #data is +nil+.
390
+ #
309
391
  # === +CONDSTORE+ extension
310
392
  # See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
311
393
  # * +NOMODSEQ+, when selecting a mailbox that does not support
@@ -324,9 +406,10 @@ module Net
324
406
  #
325
407
  # Response codes are backwards compatible: Servers are allowed to send new
326
408
  # response codes even if the client has not enabled the extension that
327
- # defines them. When Net::IMAP does not know how to parse response
328
- # code text, #data returns the unparsed string.
329
- #
409
+ # defines them. When ResponseParser does not know how to parse the response
410
+ # code data, #data may return the unparsed string, ExtensionData, or
411
+ # UnparsedData. When ResponseParser attempts but fails to parse the
412
+ # response code data, #data returns InvalidParseData.
330
413
  class ResponseCode < Struct.new(:name, :data)
331
414
  ##
332
415
  # method: name
@@ -341,8 +424,13 @@ module Net
341
424
  #
342
425
  # Returns the parsed response code data, e.g: an array of capabilities
343
426
  # strings, an array of character set strings, a list of permanent flags,
344
- # an Integer, etc. The response #code determines what form the response
345
- # code data can take.
427
+ # an Integer, etc. The response #name determines what form the response
428
+ # code #data can take.
429
+ #
430
+ # When ResponseParser does not know how to parse the response code data,
431
+ # #data may return the unparsed string, ExtensionData, or UnparsedData.
432
+ # When ResponseParser attempts but fails to parse the response code data,
433
+ # #data returns InvalidParseData.
346
434
  end
347
435
 
348
436
  # MailboxList represents the data of an untagged +LIST+ response, for a
@@ -383,14 +471,23 @@ module Net
383
471
  # and MailboxQuota objects.
384
472
  #
385
473
  # == Required capability
474
+ #
386
475
  # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
387
- # capability.
476
+ # or <tt>QUOTA=RES-STORAGE</tt>
477
+ # [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
388
478
  class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
389
479
  ##
390
480
  # method: mailbox
391
481
  # :call-seq: mailbox -> string
392
482
  #
393
- # The mailbox with the associated quota.
483
+ # The quota root with the associated quota.
484
+ #
485
+ # NOTE: this was mistakenly named "mailbox". But the quota root's name may
486
+ # differ from the mailbox. A single quota root may cover multiple
487
+ # mailboxes, and a single mailbox may be governed by multiple quota roots.
488
+
489
+ # The quota root with the associated quota.
490
+ alias quota_root mailbox
394
491
 
395
492
  ##
396
493
  # method: usage
@@ -402,7 +499,7 @@ module Net
402
499
  # method: quota
403
500
  # :call-seq: quota -> Integer
404
501
  #
405
- # Quota limit imposed on the mailbox.
502
+ # Storage limit imposed on the mailbox.
406
503
  #
407
504
  end
408
505
 
@@ -215,29 +215,20 @@ module Net
215
215
  @token = nil
216
216
  end
217
217
 
218
- def parse_error(fmt, *args)
219
- msg = format(fmt, *args)
220
- if config.debug?
221
- local_path = File.dirname(__dir__)
222
- tok = @token ? "%s: %p" % [@token.symbol, @token.value] : "nil"
223
- warn "%s %s: %s" % [self.class, __method__, msg]
224
- warn " tokenized : %s" % [@str[...@pos].dump]
225
- warn " remaining : %s" % [@str[@pos..].dump]
226
- warn " @lex_state: %s" % [@lex_state]
227
- warn " @pos : %d" % [@pos]
228
- warn " @token : %s" % [tok]
229
- caller_locations(1..20).each_with_index do |cloc, idx|
230
- next unless cloc.path&.start_with?(local_path)
231
- warn " caller[%2d]: %-30s (%s:%d)" % [
232
- idx,
233
- cloc.base_label,
234
- File.basename(cloc.path, ".rb"),
235
- cloc.lineno
236
- ]
237
- end
238
- end
239
- raise ResponseParseError, msg
240
- end
218
+ def parse_error(fmt, *args) = raise exception format(fmt, *args)
219
+
220
+ def exception(message) = ResponseParseError.new(
221
+ message, parser_state:, parser_class: self.class
222
+ )
223
+
224
+ # This can be used to backtrack after a parse error, and re-attempt to
225
+ # parse using a fallback.
226
+ #
227
+ # NOTE: Reckless backtracking could lead to O(n²) situations, so this
228
+ # should very rarely be used. Ideally, fallbacks should not backtrack.
229
+ def restore_state(state) = (@lex_state, @pos, @token = state)
230
+ def current_state = [@lex_state, @pos, @token]
231
+ def parser_state = [@str, *current_state]
241
232
 
242
233
  end
243
234
  end
@@ -38,6 +38,11 @@ module Net
38
38
  @lex_state = EXPR_BEG
39
39
  @token = nil
40
40
  return response
41
+ rescue ResponseParseError => error
42
+ if config.debug?
43
+ warn error.detailed_message(parser_state: true, parser_backtrace: true)
44
+ end
45
+ raise
41
46
  end
42
47
 
43
48
  private
@@ -686,6 +691,8 @@ module Net
686
691
  CRLF!
687
692
  EOF!
688
693
  resp
694
+ rescue SystemStackError
695
+ parse_error("response recursion too deep")
689
696
  end
690
697
 
691
698
  # RFC3501 & RFC9051:
@@ -1883,12 +1890,17 @@ module Net
1883
1890
  # We leniently re-interpret this as
1884
1891
  # resp-text = ["[" resp-text-code "]" [SP [text]] / [text]
1885
1892
  def resp_text
1886
- if lbra?
1887
- code = resp_text_code; rbra
1888
- ResponseText.new(code, SP? && text? || "")
1889
- else
1890
- ResponseText.new(nil, text? || "")
1893
+ begin
1894
+ state = current_state
1895
+ if lbra?
1896
+ code = resp_text_code; rbra
1897
+ return ResponseText.new(code, SP? && text? || "")
1898
+ end
1899
+ rescue ResponseParseError => error
1900
+ raise if /\buid-set\b/i.match? error.message
1901
+ restore_state state
1891
1902
  end
1903
+ ResponseText.new(nil, text? || "")
1892
1904
  end
1893
1905
 
1894
1906
  # RFC3501 (See https://www.rfc-editor.org/errata/rfc3501):
@@ -1951,6 +1963,7 @@ module Net
1951
1963
  # resp-text-code =/ "UIDREQUIRED"
1952
1964
  def resp_text_code
1953
1965
  name = resp_text_code__name
1966
+ state = current_state
1954
1967
  data =
1955
1968
  case name
1956
1969
  when "CAPABILITY" then resp_code__capability
@@ -1973,8 +1986,18 @@ module Net
1973
1986
  when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1974
1987
  when "UIDREQUIRED" then # RFC9586: UIDONLY
1975
1988
  else
1989
+ state = nil # don't backtrack
1976
1990
  SP? and text_chars_except_rbra
1977
1991
  end
1992
+ peek_rbra? or
1993
+ parse_error("expected resp-text-code %p to be complete", name)
1994
+ ResponseCode.new(name, data)
1995
+ rescue Net::IMAP::ResponseParseError => parse_error
1996
+ raise unless state
1997
+ raise if parse_error.message.include?("uid-set")
1998
+ restore_state state
1999
+ unparsed_data = SP? && text_chars_except_rbra
2000
+ data = InvalidParseData[parse_error:, unparsed_data:, parsed_data: data]
1978
2001
  ResponseCode.new(name, data)
1979
2002
  end
1980
2003
 
@@ -2017,13 +2040,18 @@ module Net
2017
2040
  CopyUID(validity, src_uids, dst_uids)
2018
2041
  end
2019
2042
 
2043
+ PARSER_PATH = File.expand_path(__FILE__).delete_suffix(".rb")
2044
+
2020
2045
  # TODO: remove this code in the v0.6.0 release
2021
2046
  def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
2022
2047
  return unless config.parser_use_deprecated_uidplus_data
2048
+ uplevel = caller_locations
2049
+ .find_index { !_1.path.start_with?(PARSER_PATH) }
2050
+ &.succ
2023
2051
  warn("#{Config}#parser_use_deprecated_uidplus_data is ignored " \
2024
2052
  "since v0.6.0. Disable this warning by setting " \
2025
2053
  "config.parser_use_deprecated_uidplus_data = false.",
2026
- category: :deprecated, uplevel: 9)
2054
+ category: :deprecated, uplevel:)
2027
2055
  nil
2028
2056
  end
2029
2057
 
@@ -8,51 +8,60 @@ module Net
8
8
 
9
9
  def initialize(client, sock)
10
10
  @client, @sock = client, sock
11
+ # cached config
12
+ @max_response_size = nil
13
+ # response buffer state
14
+ @buff = @literal_size = nil
11
15
  end
12
16
 
13
17
  def read_response_buffer
18
+ @max_response_size = client.max_response_size
14
19
  @buff = String.new
15
20
  catch :eof do
16
21
  while true
22
+ guard_response_too_large!
17
23
  read_line
18
- break unless (@literal_size = get_literal_size)
24
+ # check before allocating memory for literal
25
+ guard_response_too_large!
26
+ break unless literal_size
19
27
  read_literal
20
28
  end
21
29
  end
22
30
  buff
23
31
  ensure
24
- @buff = nil
32
+ @buff = @literal_size = nil
25
33
  end
26
34
 
27
35
  private
28
36
 
37
+ # cached config
38
+ attr_reader :max_response_size
39
+
40
+ # response buffer state
29
41
  attr_reader :buff, :literal_size
30
42
 
31
43
  def bytes_read = buff.bytesize
32
44
  def empty? = buff.empty?
33
- def done? = line_done? && !get_literal_size
45
+ def done? = line_done? && !literal_size
34
46
  def line_done? = buff.end_with?(CRLF)
35
- def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
47
+
48
+ def get_literal_size(buff)
49
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
50
+ end
36
51
 
37
52
  def read_line
38
- buff << (@sock.gets(CRLF, read_limit) or throw :eof)
39
- max_response_remaining! unless line_done?
53
+ line = (@sock.gets(CRLF, max_response_remaining) or throw :eof)
54
+ @literal_size = get_literal_size(line)
55
+ buff << line
40
56
  end
41
57
 
42
58
  def read_literal
43
- # check before allocating memory for literal
44
- max_response_remaining!
45
59
  literal = String.new(capacity: literal_size)
46
- buff << (@sock.read(read_limit(literal_size), literal) or throw :eof)
60
+ buff << (@sock.read(literal_size, literal) or throw :eof)
47
61
  ensure
48
62
  @literal_size = nil
49
63
  end
50
64
 
51
- def read_limit(limit = nil)
52
- [limit, max_response_remaining!].compact.min
53
- end
54
-
55
- def max_response_size = client.max_response_size
56
65
  def max_response_remaining = max_response_size &.- bytes_read
57
66
  def response_too_large? = max_response_size &.< min_response_size
58
67
  def min_response_size = bytes_read + min_response_remaining
@@ -61,8 +70,8 @@ module Net
61
70
  empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
62
71
  end
63
72
 
64
- def max_response_remaining!
65
- return max_response_remaining unless response_too_large?
73
+ def guard_response_too_large!
74
+ return unless response_too_large?
66
75
  raise ResponseTooLargeError.new(
67
76
  max_response_size:, bytes_read:, literal_size:,
68
77
  )