net-imap 0.3.7 → 0.4.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +46 -0
  3. data/.github/workflows/test.yml +5 -12
  4. data/.gitignore +2 -0
  5. data/Gemfile +3 -0
  6. data/README.md +15 -4
  7. data/Rakefile +0 -7
  8. data/docs/styles.css +0 -12
  9. data/lib/net/imap/authenticators.rb +26 -57
  10. data/lib/net/imap/command_data.rb +13 -6
  11. data/lib/net/imap/data_encoding.rb +14 -2
  12. data/lib/net/imap/deprecated_client_options.rb +139 -0
  13. data/lib/net/imap/errors.rb +20 -0
  14. data/lib/net/imap/fetch_data.rb +518 -0
  15. data/lib/net/imap/response_data.rb +178 -255
  16. data/lib/net/imap/response_parser/parser_utils.rb +240 -0
  17. data/lib/net/imap/response_parser.rb +1722 -1193
  18. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  19. data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
  20. data/lib/net/imap/sasl/authenticators.rb +118 -0
  21. data/lib/net/imap/sasl/client_adapter.rb +72 -0
  22. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +21 -11
  23. data/lib/net/imap/sasl/digest_md5_authenticator.rb +180 -0
  24. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  25. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  26. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +25 -16
  27. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  28. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  29. data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
  30. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  31. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  32. data/lib/net/imap/sasl/stringprep.rb +6 -66
  33. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  34. data/lib/net/imap/sasl.rb +144 -43
  35. data/lib/net/imap/sasl_adapter.rb +21 -0
  36. data/lib/net/imap/search_result.rb +150 -0
  37. data/lib/net/imap/sequence_set.rb +1414 -0
  38. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  39. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  40. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  41. data/lib/net/imap/stringprep/tables.rb +146 -0
  42. data/lib/net/imap/stringprep/trace.rb +85 -0
  43. data/lib/net/imap/stringprep.rb +159 -0
  44. data/lib/net/imap.rb +1213 -636
  45. data/net-imap.gemspec +5 -3
  46. data/rakelib/benchmarks.rake +91 -0
  47. data/rakelib/saslprep.rake +4 -4
  48. data/rakelib/string_prep_tables_generator.rb +82 -60
  49. metadata +34 -14
  50. data/benchmarks/stringprep.yml +0 -65
  51. data/benchmarks/table-regexps.yml +0 -39
  52. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  53. data/lib/net/imap/authenticators/plain.rb +0 -41
  54. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  55. data/lib/net/imap/sasl/saslprep.rb +0 -55
  56. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  57. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,1414 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+
6
+ ##
7
+ # An \IMAP sequence set is a set of message sequence numbers or unique
8
+ # identifier numbers ("UIDs"). It contains numbers and ranges of numbers.
9
+ # The numbers are all non-zero unsigned 32-bit integers and one special
10
+ # value (<tt>"*"</tt>) that represents the largest value in the mailbox.
11
+ #
12
+ # Certain types of \IMAP responses will contain a SequenceSet, for example
13
+ # the data for a <tt>"MODIFIED"</tt> ResponseCode. Some \IMAP commands may
14
+ # receive a SequenceSet as an argument, for example IMAP#search, IMAP#fetch,
15
+ # and IMAP#store.
16
+ #
17
+ # == EXPERIMENTAL API
18
+ #
19
+ # SequenceSet is currently experimental. Only two methods, ::[] and
20
+ # #valid_string, are considered stable. Although the API isn't expected to
21
+ # change much, any other methods may be removed or changed without
22
+ # deprecation.
23
+ #
24
+ # == Creating sequence sets
25
+ #
26
+ # SequenceSet.new with no arguments creates an empty sequence set. Note
27
+ # that an empty sequence set is invalid in the \IMAP grammar.
28
+ #
29
+ # set = Net::IMAP::SequenceSet.new
30
+ # set.empty? #=> true
31
+ # set.valid? #=> false
32
+ # set.valid_string #!> raises DataFormatError
33
+ # set << 1..10
34
+ # set.empty? #=> false
35
+ # set.valid? #=> true
36
+ # set.valid_string #=> "1:10"
37
+ #
38
+ # SequenceSet.new may receive a single optional argument: a non-zero 32 bit
39
+ # unsigned integer, a range, a <tt>sequence-set</tt> formatted string,
40
+ # another sequence set, or an enumerable containing any of these.
41
+ #
42
+ # set = Net::IMAP::SequenceSet.new(1)
43
+ # set.valid_string #=> "1"
44
+ # set = Net::IMAP::SequenceSet.new(1..100)
45
+ # set.valid_string #=> "1:100"
46
+ # set = Net::IMAP::SequenceSet.new(1...100)
47
+ # set.valid_string #=> "1:99"
48
+ # set = Net::IMAP::SequenceSet.new([1, 2, 5..])
49
+ # set.valid_string #=> "1:2,5:*"
50
+ # set = Net::IMAP::SequenceSet.new("1,2,3:7,5,6:10,2048,1024")
51
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
52
+ # set = Net::IMAP::SequenceSet.new(1, 2, 3..7, 5, 6..10, 2048, 1024)
53
+ # set.valid_string #=> "1:10,55,1024:2048"
54
+ #
55
+ # Use ::[] with one or more arguments to create a frozen SequenceSet. An
56
+ # invalid (empty) set cannot be created with ::[].
57
+ #
58
+ # set = Net::IMAP::SequenceSet["1,2,3:7,5,6:10,2048,1024"]
59
+ # set.valid_string #=> "1,2,3:7,5,6:10,2048,1024"
60
+ # set = Net::IMAP::SequenceSet[1, 2, [3..7, 5], 6..10, 2048, 1024]
61
+ # set.valid_string #=> "1:10,55,1024:2048"
62
+ #
63
+ # == Normalized form
64
+ #
65
+ # When a sequence set is created with a single String value, that #string
66
+ # representation is preserved. SequenceSet's internal representation
67
+ # implicitly sorts all entries, de-duplicates numbers, and coalesces
68
+ # adjacent or overlapping ranges. Most enumeration methods and offset-based
69
+ # methods use this normalized representation. Most modification methods
70
+ # will convert #string to its normalized form.
71
+ #
72
+ # In some cases the order of the string representation is significant, such
73
+ # as the +ESORT+, <tt>CONTEXT=SORT</tt>, and +UIDPLUS+ extensions. Use
74
+ # #entries or #each_entry to enumerate the set in its original order. To
75
+ # preserve #string order while modifying a set, use #append, #string=, or
76
+ # #replace.
77
+ #
78
+ # == Using <tt>*</tt>
79
+ #
80
+ # \IMAP sequence sets may contain a special value <tt>"*"</tt>, which
81
+ # represents the largest number in use. From +seq-number+ in
82
+ # {RFC9051 §9}[https://www.rfc-editor.org/rfc/rfc9051.html#section-9-5]:
83
+ # >>>
84
+ # In the case of message sequence numbers, it is the number of messages
85
+ # in a non-empty mailbox. In the case of unique identifiers, it is the
86
+ # unique identifier of the last message in the mailbox or, if the
87
+ # mailbox is empty, the mailbox's current UIDNEXT value.
88
+ #
89
+ # When creating a SequenceSet, <tt>*</tt> may be input as <tt>-1</tt>,
90
+ # <tt>"*"</tt>, <tt>:*</tt>, an endless range, or a range ending in
91
+ # <tt>-1</tt>. When converting to #elements, #ranges, or #numbers, it will
92
+ # output as either <tt>:*</tt> or an endless range. For example:
93
+ #
94
+ # Net::IMAP::SequenceSet["1,3,*"].to_a #=> [1, 3, :*]
95
+ # Net::IMAP::SequenceSet["1,234:*"].to_a #=> [1, 234..]
96
+ # Net::IMAP::SequenceSet[1234..-1].to_a #=> [1234..]
97
+ # Net::IMAP::SequenceSet[1234..].to_a #=> [1234..]
98
+ #
99
+ # Net::IMAP::SequenceSet[1234..].to_s #=> "1234:*"
100
+ # Net::IMAP::SequenceSet[1234..-1].to_s #=> "1234:*"
101
+ #
102
+ # Use #limit to convert <tt>"*"</tt> to a maximum value. When a range
103
+ # includes <tt>"*"</tt>, the maximum value will always be matched:
104
+ #
105
+ # Net::IMAP::SequenceSet["9999:*"].limit(max: 25)
106
+ # #=> Net::IMAP::SequenceSet["25"]
107
+ #
108
+ # === Surprising <tt>*</tt> behavior
109
+ #
110
+ # When a set includes <tt>*</tt>, some methods may have surprising behavior.
111
+ #
112
+ # For example, #complement treats <tt>*</tt> as its own number. This way,
113
+ # the #intersection of a set and its #complement will always be empty.
114
+ # This is not how an \IMAP server interprets the set: it will convert
115
+ # <tt>*</tt> to either the number of messages in the mailbox or +UIDNEXT+,
116
+ # as appropriate. And there _will_ be overlap between a set and its
117
+ # complement after #limit is applied to each:
118
+ #
119
+ # ~Net::IMAP::SequenceSet["*"] == Net::IMAP::SequenceSet[1..(2**32-1)]
120
+ # ~Net::IMAP::SequenceSet[1..5] == Net::IMAP::SequenceSet["6:*"]
121
+ #
122
+ # set = Net::IMAP::SequenceSet[1..5]
123
+ # (set & ~set).empty? => true
124
+ #
125
+ # (set.limit(max: 4) & (~set).limit(max: 4)).to_a => [4]
126
+ #
127
+ # When counting the number of numbers in a set, <tt>*</tt> will be counted
128
+ # _except_ when UINT32_MAX is also in the set:
129
+ # UINT32_MAX = 2**32 - 1
130
+ # Net::IMAP::SequenceSet["*"].count => 1
131
+ # Net::IMAP::SequenceSet[1..UINT32_MAX - 1, :*].count => UINT32_MAX
132
+ #
133
+ # Net::IMAP::SequenceSet["1:*"].count => UINT32_MAX
134
+ # Net::IMAP::SequenceSet[UINT32_MAX, :*].count => 1
135
+ # Net::IMAP::SequenceSet[UINT32_MAX..].count => 1
136
+ #
137
+ # == What's here?
138
+ #
139
+ # SequenceSet provides methods for:
140
+ # * {Creating a SequenceSet}[rdoc-ref:SequenceSet@Methods+for+Creating+a+SequenceSet]
141
+ # * {Comparing}[rdoc-ref:SequenceSet@Methods+for+Comparing]
142
+ # * {Querying}[rdoc-ref:SequenceSet@Methods+for+Querying]
143
+ # * {Iterating}[rdoc-ref:SequenceSet@Methods+for+Iterating]
144
+ # * {Set Operations}[rdoc-ref:SequenceSet@Methods+for+Set+Operations]
145
+ # * {Assigning}[rdoc-ref:SequenceSet@Methods+for+Assigning]
146
+ # * {Deleting}[rdoc-ref:SequenceSet@Methods+for+Deleting]
147
+ # * {IMAP String Formatting}[rdoc-ref:SequenceSet@Methods+for+IMAP+String+Formatting]
148
+ #
149
+ # === Methods for Creating a \SequenceSet
150
+ # * ::[]: Creates a validated frozen sequence set from one or more inputs.
151
+ # * ::new: Creates a new mutable sequence set, which may be empty (invalid).
152
+ # * ::try_convert: Calls +to_sequence_set+ on an object and verifies that
153
+ # the result is a SequenceSet.
154
+ # * ::empty: Returns a frozen empty (invalid) SequenceSet.
155
+ # * ::full: Returns a frozen SequenceSet containing every possible number.
156
+ #
157
+ # === Methods for Comparing
158
+ #
159
+ # <i>Comparison to another \SequenceSet:</i>
160
+ # - #==: Returns whether a given set contains the same numbers as +self+.
161
+ # - #eql?: Returns whether a given set uses the same #string as +self+.
162
+ #
163
+ # <i>Comparison to objects which are convertible to \SequenceSet:</i>
164
+ # - #===:
165
+ # Returns whether a given object is fully contained within +self+, or
166
+ # +nil+ if the object cannot be converted to a compatible type.
167
+ # - #cover? (aliased as #===):
168
+ # Returns whether a given object is fully contained within +self+.
169
+ # - #intersect? (aliased as #overlap?):
170
+ # Returns whether +self+ and a given object have any common elements.
171
+ # - #disjoint?:
172
+ # Returns whether +self+ and a given object have no common elements.
173
+ #
174
+ # === Methods for Querying
175
+ # These methods do not modify +self+.
176
+ #
177
+ # <i>Set membership:</i>
178
+ # - #include? (aliased as #member?):
179
+ # Returns whether a given object (nz-number, range, or <tt>*</tt>) is
180
+ # contained by the set.
181
+ # - #include_star?: Returns whether the set contains <tt>*</tt>.
182
+ #
183
+ # <i>Minimum and maximum value elements:</i>
184
+ # - #min: Returns the minimum number in the set.
185
+ # - #max: Returns the maximum number in the set.
186
+ # - #minmax: Returns the minimum and maximum numbers in the set.
187
+ #
188
+ # <i>Accessing value by offset:</i>
189
+ # - #[] (aliased as #slice): Returns the number or consecutive subset at a
190
+ # given offset or range of offsets.
191
+ # - #at: Returns the number at a given offset.
192
+ # - #find_index: Returns the given number's offset in the set
193
+ #
194
+ # <i>Set cardinality:</i>
195
+ # - #count (aliased as #size): Returns the count of numbers in the set.
196
+ # - #empty?: Returns whether the set has no members. \IMAP syntax does not
197
+ # allow empty sequence sets.
198
+ # - #valid?: Returns whether the set has any members.
199
+ # - #full?: Returns whether the set contains every possible value, including
200
+ # <tt>*</tt>.
201
+ #
202
+ # === Methods for Iterating
203
+ #
204
+ # - #each_element: Yields each number and range in the set, sorted and
205
+ # coalesced, and returns +self+.
206
+ # - #elements (aliased as #to_a): Returns an Array of every number and range
207
+ # in the set, sorted and coalesced.
208
+ # - #each_entry: Yields each number and range in the set, unsorted and
209
+ # without deduplicating numbers or coalescing ranges, and returns +self+.
210
+ # - #entries: Returns an Array of every number and range in the set,
211
+ # unsorted and without deduplicating numbers or coalescing ranges.
212
+ # - #each_range:
213
+ # Yields each element in the set as a Range and returns +self+.
214
+ # - #ranges: Returns an Array of every element in the set, converting
215
+ # numbers into ranges of a single value.
216
+ # - #each_number: Yields each number in the set and returns +self+.
217
+ # - #numbers: Returns an Array with every number in the set, expanding
218
+ # ranges into all of their contained numbers.
219
+ # - #to_set: Returns a Set containing all of the #numbers in the set.
220
+ #
221
+ # === Methods for \Set Operations
222
+ # These methods do not modify +self+.
223
+ #
224
+ # - #| (aliased as #union and #+): Returns a new set combining all members
225
+ # from +self+ with all members from the other object.
226
+ # - #& (aliased as #intersection): Returns a new set containing all members
227
+ # common to +self+ and the other object.
228
+ # - #- (aliased as #difference): Returns a copy of +self+ with all members
229
+ # in the other object removed.
230
+ # - #^ (aliased as #xor): Returns a new set containing all members from
231
+ # +self+ and the other object except those common to both.
232
+ # - #~ (aliased as #complement): Returns a new set containing all members
233
+ # that are not in +self+
234
+ # - #limit: Returns a copy of +self+ which has replaced <tt>*</tt> with a
235
+ # given maximum value and removed all members over that maximum.
236
+ #
237
+ # === Methods for Assigning
238
+ # These methods add or replace elements in +self+.
239
+ #
240
+ # - #add (aliased as #<<): Adds a given object to the set; returns +self+.
241
+ # - #add?: If the given object is not an element in the set, adds it and
242
+ # returns +self+; otherwise, returns +nil+.
243
+ # - #merge: Merges multiple elements into the set; returns +self+.
244
+ # - #append: Adds a given object to the set, appending it to the existing
245
+ # string, and returns +self+.
246
+ # - #string=: Assigns a new #string value and replaces #elements to match.
247
+ # - #replace: Replaces the contents of the set with the contents
248
+ # of a given object.
249
+ # - #complement!: Replaces the contents of the set with its own #complement.
250
+ #
251
+ # === Methods for Deleting
252
+ # These methods remove elements from +self+.
253
+ #
254
+ # - #clear: Removes all elements in the set; returns +self+.
255
+ # - #delete: Removes a given object from the set; returns +self+.
256
+ # - #delete?: If the given object is an element in the set, removes it and
257
+ # returns it; otherwise, returns +nil+.
258
+ # - #delete_at: Removes the number at a given offset.
259
+ # - #slice!: Removes the number or consecutive numbers at a given offset or
260
+ # range of offsets.
261
+ # - #subtract: Removes each given object from the set; returns +self+.
262
+ # - #limit!: Replaces <tt>*</tt> with a given maximum value and removes all
263
+ # members over that maximum; returns +self+.
264
+ #
265
+ # === Methods for \IMAP String Formatting
266
+ #
267
+ # - #to_s: Returns the +sequence-set+ string, or an empty string when the
268
+ # set is empty.
269
+ # - #string: Returns the +sequence-set+ string, or nil when empty.
270
+ # - #valid_string: Returns the +sequence-set+ string, or raises
271
+ # DataFormatError when the set is empty.
272
+ # - #normalized_string: Returns a <tt>sequence-set</tt> string with its
273
+ # elements sorted and coalesced, or nil when the set is empty.
274
+ # - #normalize: Returns a new set with this set's normalized +sequence-set+
275
+ # representation.
276
+ # - #normalize!: Updates #string to its normalized +sequence-set+
277
+ # representation and returns +self+.
278
+ #
279
+ class SequenceSet
280
+ # The largest possible non-zero unsigned 32-bit integer
281
+ UINT32_MAX = 2**32 - 1
282
+
283
+ # represents "*" internally, to simplify sorting (etc)
284
+ STAR_INT = UINT32_MAX + 1
285
+ private_constant :STAR_INT
286
+
287
+ # valid inputs for "*"
288
+ STARS = [:*, ?*, -1].freeze
289
+ private_constant :STAR_INT, :STARS
290
+
291
+ COERCIBLE = ->{ _1.respond_to? :to_sequence_set }
292
+ ENUMABLE = ->{ _1.respond_to?(:each) && _1.respond_to?(:empty?) }
293
+ private_constant :COERCIBLE, :ENUMABLE
294
+
295
+ class << self
296
+
297
+ # :call-seq:
298
+ # SequenceSet[*values] -> valid frozen sequence set
299
+ #
300
+ # Returns a frozen SequenceSet, constructed from +values+.
301
+ #
302
+ # An empty SequenceSet is invalid and will raise a DataFormatError.
303
+ #
304
+ # Use ::new to create a mutable or empty SequenceSet.
305
+ def [](first, *rest)
306
+ if rest.empty?
307
+ if first.is_a?(SequenceSet) && set.frozen? && set.valid?
308
+ first
309
+ else
310
+ new(first).validate.freeze
311
+ end
312
+ else
313
+ new(first).merge(*rest).validate.freeze
314
+ end
315
+ end
316
+
317
+ # :call-seq:
318
+ # SequenceSet.try_convert(obj) -> sequence set or nil
319
+ #
320
+ # If +obj+ is a SequenceSet, returns +obj+. If +obj+ responds_to
321
+ # +to_sequence_set+, calls +obj.to_sequence_set+ and returns the result.
322
+ # Otherwise returns +nil+.
323
+ #
324
+ # If +obj.to_sequence_set+ doesn't return a SequenceSet, an exception is
325
+ # raised.
326
+ def try_convert(obj)
327
+ return obj if obj.is_a?(SequenceSet)
328
+ return nil unless respond_to?(:to_sequence_set)
329
+ obj = obj.to_sequence_set
330
+ return obj if obj.is_a?(SequenceSet)
331
+ raise DataFormatError, "invalid object returned from to_sequence_set"
332
+ end
333
+
334
+ # Returns a frozen empty set singleton. Note that valid \IMAP sequence
335
+ # sets cannot be empty, so this set is _invalid_.
336
+ def empty; EMPTY end
337
+
338
+ # Returns a frozen full set singleton: <tt>"1:*"</tt>
339
+ def full; FULL end
340
+
341
+ end
342
+
343
+ # Create a new SequenceSet object from +input+, which may be another
344
+ # SequenceSet, an IMAP formatted +sequence-set+ string, a number, a
345
+ # range, <tt>:*</tt>, or an enumerable of these.
346
+ #
347
+ # Use ::[] to create a frozen (non-empty) SequenceSet.
348
+ def initialize(input = nil) input ? replace(input) : clear end
349
+
350
+ # Removes all elements and returns self.
351
+ def clear; @tuples, @string = [], nil; self end
352
+
353
+ # Replace the contents of the set with the contents of +other+ and returns
354
+ # +self+.
355
+ #
356
+ # +other+ may be another SequenceSet, or it may be an IMAP +sequence-set+
357
+ # string, a number, a range, <tt>*</tt>, or an enumerable of these.
358
+ def replace(other)
359
+ case other
360
+ when SequenceSet then initialize_dup(other)
361
+ when String then self.string = other
362
+ else clear; merge other
363
+ end
364
+ self
365
+ end
366
+
367
+ # Returns the \IMAP +sequence-set+ string representation, or raises a
368
+ # DataFormatError when the set is empty.
369
+ #
370
+ # Use #string to return +nil+ or #to_s to return an empty string without
371
+ # error.
372
+ #
373
+ # Related: #string, #normalized_string, #to_s
374
+ def valid_string
375
+ raise DataFormatError, "empty sequence-set" if empty?
376
+ string
377
+ end
378
+
379
+ # Returns the \IMAP +sequence-set+ string representation, or +nil+ when
380
+ # the set is empty. Note that an empty set is invalid in the \IMAP
381
+ # syntax.
382
+ #
383
+ # Use #valid_string to raise an exception when the set is empty, or #to_s
384
+ # to return an empty string.
385
+ #
386
+ # If the set was created from a single string, it is not normalized. If
387
+ # the set is updated the string will be normalized.
388
+ #
389
+ # Related: #valid_string, #normalized_string, #to_s
390
+ def string; @string ||= normalized_string if valid? end
391
+
392
+ # Assigns a new string to #string and resets #elements to match. It
393
+ # cannot be set to an empty string—assign +nil+ or use #clear instead.
394
+ # The string is validated but not normalized.
395
+ #
396
+ # Use #add or #merge to add a string to an existing set.
397
+ #
398
+ # Related: #replace, #clear
399
+ def string=(str)
400
+ if str.nil?
401
+ clear
402
+ else
403
+ str = String.try_convert(str) or raise ArgumentError, "not a string"
404
+ tuples = str_to_tuples str
405
+ @tuples, @string = [], -str
406
+ tuples_add tuples
407
+ end
408
+ end
409
+
410
+ # Returns the \IMAP +sequence-set+ string representation, or an empty
411
+ # string when the set is empty. Note that an empty set is invalid in the
412
+ # \IMAP syntax.
413
+ #
414
+ # Related: #valid_string, #normalized_string, #to_s
415
+ def to_s; string || "" end
416
+
417
+ # Freezes and returns the set. A frozen SequenceSet is Ractor-safe.
418
+ def freeze
419
+ return self if frozen?
420
+ string
421
+ @tuples.each(&:freeze).freeze
422
+ super
423
+ end
424
+
425
+ # :call-seq: self == other -> true or false
426
+ #
427
+ # Returns true when the other SequenceSet represents the same message
428
+ # identifiers. Encoding difference—such as order, overlaps, or
429
+ # duplicates—are ignored.
430
+ #
431
+ # Net::IMAP::SequenceSet["1:3"] == Net::IMAP::SequenceSet["1:3"]
432
+ # #=> true
433
+ # Net::IMAP::SequenceSet["1,2,3"] == Net::IMAP::SequenceSet["1:3"]
434
+ # #=> true
435
+ # Net::IMAP::SequenceSet["1,3"] == Net::IMAP::SequenceSet["3,1"]
436
+ # #=> true
437
+ # Net::IMAP::SequenceSet["9,1:*"] == Net::IMAP::SequenceSet["1:*"]
438
+ # #=> true
439
+ #
440
+ # Related: #eql?, #normalize
441
+ def ==(other)
442
+ self.class == other.class &&
443
+ (to_s == other.to_s || tuples == other.tuples)
444
+ end
445
+
446
+ # :call-seq: eql?(other) -> true or false
447
+ #
448
+ # Hash equality requires the same encoded #string representation.
449
+ #
450
+ # Net::IMAP::SequenceSet["1:3"] .eql? Net::IMAP::SequenceSet["1:3"]
451
+ # #=> true
452
+ # Net::IMAP::SequenceSet["1,2,3"].eql? Net::IMAP::SequenceSet["1:3"]
453
+ # #=> false
454
+ # Net::IMAP::SequenceSet["1,3"] .eql? Net::IMAP::SequenceSet["3,1"]
455
+ # #=> false
456
+ # Net::IMAP::SequenceSet["9,1:*"].eql? Net::IMAP::SequenceSet["1:*"]
457
+ # #=> false
458
+ #
459
+ # Related: #==, #normalize
460
+ def eql?(other) self.class == other.class && string == other.string end
461
+
462
+ # See #eql?
463
+ def hash; [self.class, string].hash end
464
+
465
+ # :call-seq: self === other -> true | false | nil
466
+ #
467
+ # Returns whether +other+ is contained within the set. Returns +nil+ if a
468
+ # StandardError is raised while converting +other+ to a comparable type.
469
+ #
470
+ # Related: #cover?, #include?, #include_star?
471
+ def ===(other)
472
+ cover?(other)
473
+ rescue
474
+ nil
475
+ end
476
+
477
+ # :call-seq: cover?(other) -> true | false | nil
478
+ #
479
+ # Returns whether +other+ is contained within the set. +other+ may be any
480
+ # object that would be accepted by ::new.
481
+ #
482
+ # Related: #===, #include?, #include_star?
483
+ def cover?(other) input_to_tuples(other).none? { !include_tuple?(_1) } end
484
+
485
+ # Returns +true+ when a given number or range is in +self+, and +false+
486
+ # otherwise. Returns +false+ unless +number+ is an Integer, Range, or
487
+ # <tt>*</tt>.
488
+ #
489
+ # set = Net::IMAP::SequenceSet["5:10,100,111:115"]
490
+ # set.include? 1 #=> false
491
+ # set.include? 5..10 #=> true
492
+ # set.include? 11..20 #=> false
493
+ # set.include? 100 #=> true
494
+ # set.include? 6 #=> true, covered by "5:10"
495
+ # set.include? 4..9 #=> true, covered by "5:10"
496
+ # set.include? "4:9" #=> true, strings are parsed
497
+ # set.include? 4..9 #=> false, intersection is not sufficient
498
+ # set.include? "*" #=> false, use #limit to re-interpret "*"
499
+ # set.include? -1 #=> false, -1 is interpreted as "*"
500
+ #
501
+ # set = Net::IMAP::SequenceSet["5:10,100,111:*"]
502
+ # set.include? :* #=> true
503
+ # set.include? "*" #=> true
504
+ # set.include? -1 #=> true
505
+ # set.include? 200.. #=> true
506
+ # set.include? 100.. #=> false
507
+ #
508
+ # Related: #include_star?, #cover?, #===
509
+ def include?(element) include_tuple? input_to_tuple element end
510
+
511
+ alias member? include?
512
+
513
+ # Returns +true+ when the set contains <tt>*</tt>.
514
+ def include_star?; @tuples.last&.last == STAR_INT end
515
+
516
+ # Returns +true+ if the set and a given object have any common elements,
517
+ # +false+ otherwise.
518
+ #
519
+ # Net::IMAP::SequenceSet["5:10"].intersect? "7,9,11" #=> true
520
+ # Net::IMAP::SequenceSet["5:10"].intersect? "11:33" #=> false
521
+ #
522
+ # Related: #intersection, #disjoint?
523
+ def intersect?(other)
524
+ valid? && input_to_tuples(other).any? { intersect_tuple? _1 }
525
+ end
526
+ alias overlap? intersect?
527
+
528
+ # Returns +true+ if the set and a given object have no common elements,
529
+ # +false+ otherwise.
530
+ #
531
+ # Net::IMAP::SequenceSet["5:10"].disjoint? "7,9,11" #=> false
532
+ # Net::IMAP::SequenceSet["5:10"].disjoint? "11:33" #=> true
533
+ #
534
+ # Related: #intersection, #intersect?
535
+ def disjoint?(other)
536
+ empty? || input_to_tuples(other).none? { intersect_tuple? _1 }
537
+ end
538
+
539
+ # :call-seq: max(star: :*) => integer or star or nil
540
+ #
541
+ # Returns the maximum value in +self+, +star+ when the set includes
542
+ # <tt>*</tt>, or +nil+ when the set is empty.
543
+ def max(star: :*)
544
+ (val = @tuples.last&.last) && val == STAR_INT ? star : val
545
+ end
546
+
547
+ # :call-seq: min(star: :*) => integer or star or nil
548
+ #
549
+ # Returns the minimum value in +self+, +star+ when the only value in the
550
+ # set is <tt>*</tt>, or +nil+ when the set is empty.
551
+ def min(star: :*)
552
+ (val = @tuples.first&.first) && val == STAR_INT ? star : val
553
+ end
554
+
555
+ # :call-seq: minmax(star: :*) => nil or [integer, integer or star]
556
+ #
557
+ # Returns a 2-element array containing the minimum and maximum numbers in
558
+ # +self+, or +nil+ when the set is empty.
559
+ def minmax(star: :*); [min(star: star), max(star: star)] unless empty? end
560
+
561
+ # Returns false when the set is empty.
562
+ def valid?; !empty? end
563
+
564
+ # Returns true if the set contains no elements
565
+ def empty?; @tuples.empty? end
566
+
567
+ # Returns true if the set contains every possible element.
568
+ def full?; @tuples == [[1, STAR_INT]] end
569
+
570
+ # :call-seq:
571
+ # self + other -> sequence set
572
+ # self | other -> sequence set
573
+ # union(other) -> sequence set
574
+ #
575
+ # Returns a new sequence set that has every number in the +other+ object
576
+ # added.
577
+ #
578
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
579
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
580
+ # another sequence set, or an enumerable containing any of these.
581
+ #
582
+ # Net::IMAP::SequenceSet["1:5"] | 2 | [4..6, 99]
583
+ # #=> Net::IMAP::SequenceSet["1:6,99"]
584
+ #
585
+ # Related: #add, #merge
586
+ def |(other) remain_frozen dup.merge other end
587
+ alias :+ :|
588
+ alias union :|
589
+
590
+ # :call-seq:
591
+ # self - other -> sequence set
592
+ # difference(other) -> sequence set
593
+ #
594
+ # Returns a new sequence set built by duplicating this set and removing
595
+ # every number that appears in +other+.
596
+ #
597
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
598
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
599
+ # another sequence set, or an enumerable containing any of these.
600
+ #
601
+ # Net::IMAP::SequenceSet[1..5] - 2 - 4 - 6
602
+ # #=> Net::IMAP::SequenceSet["1,3,5"]
603
+ #
604
+ # Related: #subtract
605
+ def -(other) remain_frozen dup.subtract other end
606
+ alias difference :-
607
+
608
+ # :call-seq:
609
+ # self & other -> sequence set
610
+ # intersection(other) -> sequence set
611
+ #
612
+ # Returns a new sequence set containing only the numbers common to this
613
+ # set and +other+.
614
+ #
615
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
616
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
617
+ # another sequence set, or an enumerable containing any of these.
618
+ #
619
+ # Net::IMAP::SequenceSet[1..5] & [2, 4, 6]
620
+ # #=> Net::IMAP::SequenceSet["2,4"]
621
+ #
622
+ # <tt>(seqset & other)</tt> is equivalent to <tt>(seqset - ~other)</tt>.
623
+ def &(other)
624
+ remain_frozen dup.subtract SequenceSet.new(other).complement!
625
+ end
626
+ alias intersection :&
627
+
628
+ # :call-seq:
629
+ # self ^ other -> sequence set
630
+ # xor(other) -> sequence set
631
+ #
632
+ # Returns a new sequence set containing numbers that are exclusive between
633
+ # this set and +other+.
634
+ #
635
+ # +other+ may be any object that would be accepted by ::new: a non-zero 32
636
+ # bit unsigned integer, range, <tt>sequence-set</tt> formatted string,
637
+ # another sequence set, or an enumerable containing any of these.
638
+ #
639
+ # Net::IMAP::SequenceSet[1..5] ^ [2, 4, 6]
640
+ # #=> Net::IMAP::SequenceSet["1,3,5:6"]
641
+ #
642
+ # <tt>(seqset ^ other)</tt> is equivalent to <tt>((seqset | other) -
643
+ # (seqset & other))</tt>.
644
+ def ^(other) remain_frozen (self | other).subtract(self & other) end
645
+ alias xor :^
646
+
647
+ # :call-seq:
648
+ # ~ self -> sequence set
649
+ # complement -> sequence set
650
+ #
651
+ # Returns the complement of self, a SequenceSet which contains all numbers
652
+ # _except_ for those in this set.
653
+ #
654
+ # ~Net::IMAP::SequenceSet.full #=> Net::IMAP::SequenceSet.empty
655
+ # ~Net::IMAP::SequenceSet.empty #=> Net::IMAP::SequenceSet.full
656
+ # ~Net::IMAP::SequenceSet["1:5,100:222"]
657
+ # #=> Net::IMAP::SequenceSet["6:99,223:*"]
658
+ # ~Net::IMAP::SequenceSet["6:99,223:*"]
659
+ # #=> Net::IMAP::SequenceSet["1:5,100:222"]
660
+ #
661
+ # Related: #complement!
662
+ def ~; remain_frozen dup.complement! end
663
+ alias complement :~
664
+
665
+ # :call-seq:
666
+ # add(object) -> self
667
+ # self << other -> self
668
+ #
669
+ # Adds a range or number to the set and returns +self+.
670
+ #
671
+ # #string will be regenerated. Use #merge to add many elements at once.
672
+ #
673
+ # Related: #add?, #merge, #union
674
+ def add(object)
675
+ tuple_add input_to_tuple object
676
+ normalize!
677
+ end
678
+ alias << add
679
+
680
+ # Adds a range or number to the set and returns +self+.
681
+ #
682
+ # Unlike #add, #merge, or #union, the new value is appended to #string.
683
+ # This may result in a #string which has duplicates or is out-of-order.
684
+ def append(object)
685
+ tuple = input_to_tuple object
686
+ entry = tuple_to_str tuple
687
+ tuple_add tuple
688
+ @string = -(string ? "#{@string},#{entry}" : entry)
689
+ self
690
+ end
691
+
692
+ # :call-seq: add?(object) -> self or nil
693
+ #
694
+ # Adds a range or number to the set and returns +self+. Returns +nil+
695
+ # when the object is already included in the set.
696
+ #
697
+ # #string will be regenerated. Use #merge to add many elements at once.
698
+ #
699
+ # Related: #add, #merge, #union, #include?
700
+ def add?(object)
701
+ add object unless include? object
702
+ end
703
+
704
+ # :call-seq: delete(object) -> self
705
+ #
706
+ # Deletes the given range or number from the set and returns +self+.
707
+ #
708
+ # #string will be regenerated after deletion. Use #subtract to remove
709
+ # many elements at once.
710
+ #
711
+ # Related: #delete?, #delete_at, #subtract, #difference
712
+ def delete(object)
713
+ tuple_subtract input_to_tuple object
714
+ normalize!
715
+ end
716
+
717
+ # :call-seq:
718
+ # delete?(number) -> integer or nil
719
+ # delete?(star) -> :* or nil
720
+ # delete?(range) -> sequence set or nil
721
+ #
722
+ # Removes a specified value from the set, and returns the removed value.
723
+ # Returns +nil+ if nothing was removed.
724
+ #
725
+ # Returns an integer when the specified +number+ argument was removed:
726
+ # set = Net::IMAP::SequenceSet.new [5..10, 20]
727
+ # set.delete?(7) #=> 7
728
+ # set #=> #<Net::IMAP::SequenceSet "5:6,8:10,20">
729
+ # set.delete?("20") #=> 20
730
+ # set #=> #<Net::IMAP::SequenceSet "5:6,8:10">
731
+ # set.delete?(30) #=> nil
732
+ #
733
+ # Returns <tt>:*</tt> when <tt>*</tt> or <tt>-1</tt> is specified and
734
+ # removed:
735
+ # set = Net::IMAP::SequenceSet.new "5:9,20,35,*"
736
+ # set.delete?(-1) #=> :*
737
+ # set #=> #<Net::IMAP::SequenceSet "5:9,20,35">
738
+ #
739
+ # And returns a new SequenceSet when a range is specified:
740
+ #
741
+ # set = Net::IMAP::SequenceSet.new [5..10, 20]
742
+ # set.delete?(9..) #=> #<Net::IMAP::SequenceSet "9:10,20">
743
+ # set #=> #<Net::IMAP::SequenceSet "5:8">
744
+ # set.delete?(21..) #=> nil
745
+ #
746
+ # #string will be regenerated after deletion.
747
+ #
748
+ # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
749
+ def delete?(object)
750
+ tuple = input_to_tuple object
751
+ if tuple.first == tuple.last
752
+ return unless include_tuple? tuple
753
+ tuple_subtract tuple
754
+ normalize!
755
+ from_tuple_int tuple.first
756
+ else
757
+ copy = dup
758
+ tuple_subtract tuple
759
+ normalize!
760
+ copy if copy.subtract(self).valid?
761
+ end
762
+ end
763
+
764
+ # :call-seq: delete_at(index) -> number or :* or nil
765
+ #
766
+ # Deletes a number the set, indicated by the given +index+. Returns the
767
+ # number that was removed, or +nil+ if nothing was removed.
768
+ #
769
+ # #string will be regenerated after deletion.
770
+ #
771
+ # Related: #delete, #delete?, #slice!, #subtract, #difference
772
+ def delete_at(index)
773
+ slice! Integer(index.to_int)
774
+ end
775
+
776
+ # :call-seq:
777
+ # slice!(index) -> integer or :* or nil
778
+ # slice!(start, length) -> sequence set or nil
779
+ # slice!(range) -> sequence set or nil
780
+ #
781
+ # Deletes a number or consecutive numbers from the set, indicated by the
782
+ # given +index+, +start+ and +length+, or +range+ of offsets. Returns the
783
+ # number or sequence set that was removed, or +nil+ if nothing was
784
+ # removed. Arguments are interpreted the same as for #slice or #[].
785
+ #
786
+ # #string will be regenerated after deletion.
787
+ #
788
+ # Related: #slice, #delete_at, #delete, #delete?, #subtract, #difference
789
+ def slice!(index, length = nil)
790
+ deleted = slice(index, length) and subtract deleted
791
+ deleted
792
+ end
793
+
794
+ # Merges all of the elements that appear in any of the +inputs+ into the
795
+ # set, and returns +self+.
796
+ #
797
+ # The +inputs+ may be any objects that would be accepted by ::new:
798
+ # non-zero 32 bit unsigned integers, ranges, <tt>sequence-set</tt>
799
+ # formatted strings, other sequence sets, or enumerables containing any of
800
+ # these.
801
+ #
802
+ # #string will be regenerated after all inputs have been merged.
803
+ #
804
+ # Related: #add, #add?, #union
805
+ def merge(*inputs)
806
+ tuples_add input_to_tuples inputs
807
+ normalize!
808
+ end
809
+
810
+ # Removes all of the elements that appear in any of the given +objects+
811
+ # from the set, and returns +self+.
812
+ #
813
+ # The +objects+ may be any objects that would be accepted by ::new:
814
+ # non-zero 32 bit unsigned integers, ranges, <tt>sequence-set</tt>
815
+ # formatted strings, other sequence sets, or enumerables containing any of
816
+ # these.
817
+ #
818
+ # Related: #difference
819
+ def subtract(*objects)
820
+ tuples_subtract input_to_tuples objects
821
+ normalize!
822
+ end
823
+
824
+ # Returns an array of ranges and integers and <tt>:*</tt>.
825
+ #
826
+ # The entries are in the same order they appear in #string, with no
827
+ # sorting, deduplication, or coalescing. When #string is in its
828
+ # normalized form, this will return the same result as #elements.
829
+ # This is useful when the given order is significant, for example in a
830
+ # ESEARCH response to IMAP#sort.
831
+ #
832
+ # Related: #each_entry, #elements
833
+ def entries; each_entry.to_a end
834
+
835
+ # Returns an array of ranges and integers and <tt>:*</tt>.
836
+ #
837
+ # The returned elements are sorted and coalesced, even when the input
838
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
839
+ #
840
+ # By itself, <tt>*</tt> translates to <tt>:*</tt>. A range containing
841
+ # <tt>*</tt> translates to an endless range. Use #limit to translate both
842
+ # cases to a maximum value.
843
+ #
844
+ # If the original input was unordered or contains overlapping ranges, the
845
+ # returned ranges will be ordered and coalesced.
846
+ #
847
+ # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].elements
848
+ # #=> [2, 5..9, 11..12, :*]
849
+ #
850
+ # Related: #each_element, #ranges, #numbers
851
+ def elements; each_element.to_a end
852
+ alias to_a elements
853
+
854
+ # Returns an array of ranges
855
+ #
856
+ # The returned elements are sorted and coalesced, even when the input
857
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
858
+ #
859
+ # <tt>*</tt> translates to an endless range. By itself, <tt>*</tt>
860
+ # translates to <tt>:*..</tt>. Use #limit to set <tt>*</tt> to a maximum
861
+ # value.
862
+ #
863
+ # The returned ranges will be ordered and coalesced, even when the input
864
+ # #string is not. <tt>*</tt> will sort last. See #normalize.
865
+ #
866
+ # Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].ranges
867
+ # #=> [2..2, 5..9, 11..12, :*..]
868
+ # Net::IMAP::SequenceSet["123,999:*,456:789"].ranges
869
+ # #=> [123..123, 456..789, 999..]
870
+ #
871
+ # Related: #each_range, #elements, #numbers, #to_set
872
+ def ranges; each_range.to_a end
873
+
874
+ # Returns a sorted array of all of the number values in the sequence set.
875
+ #
876
+ # The returned numbers are sorted and de-duplicated, even when the input
877
+ # #string is not. See #normalize.
878
+ #
879
+ # Net::IMAP::SequenceSet["2,5:9,6,12:11"].numbers
880
+ # #=> [2, 5, 6, 7, 8, 9, 11, 12]
881
+ #
882
+ # If the set contains a <tt>*</tt>, RangeError is raised. See #limit.
883
+ #
884
+ # Net::IMAP::SequenceSet["10000:*"].numbers
885
+ # #!> RangeError
886
+ #
887
+ # *WARNING:* Even excluding sets with <tt>*</tt>, an enormous result can
888
+ # easily be created. An array with over 4 billion integers could be
889
+ # returned, requiring up to 32GiB of memory on a 64-bit architecture.
890
+ #
891
+ # Net::IMAP::SequenceSet[10000..2**32-1].numbers
892
+ # # ...probably freezes the process for a while...
893
+ # #!> NoMemoryError (probably)
894
+ #
895
+ # For safety, consider using #limit or #intersection to set an upper
896
+ # bound. Alternatively, use #each_element, #each_range, or even
897
+ # #each_number to avoid allocation of a result array.
898
+ #
899
+ # Related: #elements, #ranges, #to_set
900
+ def numbers; each_number.to_a end
901
+
902
+ # Yields each number or range in #string to the block and returns +self+.
903
+ # Returns an enumerator when called without a block.
904
+ #
905
+ # The entries are yielded in the same order they appear in #tring, with no
906
+ # sorting, deduplication, or coalescing. When #string is in its
907
+ # normalized form, this will yield the same values as #each_element.
908
+ #
909
+ # Related: #entries, #each_element
910
+ def each_entry(&block)
911
+ return to_enum(__method__) unless block_given?
912
+ return each_element(&block) unless @string
913
+ @string.split(",").each do yield tuple_to_entry str_to_tuple _1 end
914
+ self
915
+ end
916
+
917
+ # Yields each number or range (or <tt>:*</tt>) in #elements to the block
918
+ # and returns self. Returns an enumerator when called without a block.
919
+ #
920
+ # The returned numbers are sorted and de-duplicated, even when the input
921
+ # #string is not. See #normalize.
922
+ #
923
+ # Related: #elements, #each_entry
924
+ def each_element # :yields: integer or range or :*
925
+ return to_enum(__method__) unless block_given?
926
+ @tuples.each do yield tuple_to_entry _1 end
927
+ self
928
+ end
929
+
930
+ private def tuple_to_entry((min, max))
931
+ if min == STAR_INT then :*
932
+ elsif max == STAR_INT then min..
933
+ elsif min == max then min
934
+ else min..max
935
+ end
936
+ end
937
+
938
+ # Yields each range in #ranges to the block and returns self.
939
+ # Returns an enumerator when called without a block.
940
+ #
941
+ # Related: #ranges
942
+ def each_range # :yields: range
943
+ return to_enum(__method__) unless block_given?
944
+ @tuples.each do |min, max|
945
+ if min == STAR_INT then yield :*..
946
+ elsif max == STAR_INT then yield min..
947
+ else yield min..max
948
+ end
949
+ end
950
+ self
951
+ end
952
+
953
+ # Yields each number in #numbers to the block and returns self.
954
+ # If the set contains a <tt>*</tt>, RangeError will be raised.
955
+ #
956
+ # Returns an enumerator when called without a block (even if the set
957
+ # contains <tt>*</tt>).
958
+ #
959
+ # Related: #numbers
960
+ def each_number(&block) # :yields: integer
961
+ return to_enum(__method__) unless block_given?
962
+ raise RangeError, '%s contains "*"' % [self.class] if include_star?
963
+ each_element do |elem|
964
+ case elem
965
+ when Range then elem.each(&block)
966
+ when Integer then block.(elem)
967
+ end
968
+ end
969
+ self
970
+ end
971
+
972
+ # Returns a Set with all of the #numbers in the sequence set.
973
+ #
974
+ # If the set contains a <tt>*</tt>, RangeError will be raised.
975
+ #
976
+ # See #numbers for the warning about very large sets.
977
+ #
978
+ # Related: #elements, #ranges, #numbers
979
+ def to_set; Set.new(numbers) end
980
+
981
+ # Returns the count of #numbers in the set.
982
+ #
983
+ # If <tt>*</tt> and <tt>2**32 - 1</tt> (the maximum 32-bit unsigned
984
+ # integer value) are both in the set, they will only be counted once.
985
+ def count
986
+ @tuples.sum(@tuples.count) { _2 - _1 } +
987
+ (include_star? && include?(UINT32_MAX) ? -1 : 0)
988
+ end
989
+
990
+ alias size count
991
+
992
+ # Returns the index of +number+ in the set, or +nil+ if +number+ isn't in
993
+ # the set.
994
+ #
995
+ # Related: #[]
996
+ def find_index(number)
997
+ number = to_tuple_int number
998
+ each_tuple_with_index do |min, max, idx_min|
999
+ number < min and return nil
1000
+ number <= max and return from_tuple_int(idx_min + (number - min))
1001
+ end
1002
+ nil
1003
+ end
1004
+
1005
+ private def each_tuple_with_index
1006
+ idx_min = 0
1007
+ @tuples.each do |min, max|
1008
+ yield min, max, idx_min, (idx_max = idx_min + (max - min))
1009
+ idx_min = idx_max + 1
1010
+ end
1011
+ idx_min
1012
+ end
1013
+
1014
+ private def reverse_each_tuple_with_index
1015
+ idx_max = -1
1016
+ @tuples.reverse_each do |min, max|
1017
+ yield min, max, (idx_min = idx_max - (max - min)), idx_max
1018
+ idx_max = idx_min - 1
1019
+ end
1020
+ idx_max
1021
+ end
1022
+
1023
+ # :call-seq: at(index) -> integer or nil
1024
+ #
1025
+ # Returns a number from +self+, without modifying the set. Behaves the
1026
+ # same as #[], except that #at only allows a single integer argument.
1027
+ #
1028
+ # Related: #[], #slice
1029
+ def at(index)
1030
+ index = Integer(index.to_int)
1031
+ if index.negative?
1032
+ reverse_each_tuple_with_index do |min, max, idx_min, idx_max|
1033
+ idx_min <= index and return from_tuple_int(min + (index - idx_min))
1034
+ end
1035
+ else
1036
+ each_tuple_with_index do |min, _, idx_min, idx_max|
1037
+ index <= idx_max and return from_tuple_int(min + (index - idx_min))
1038
+ end
1039
+ end
1040
+ nil
1041
+ end
1042
+
1043
+ # :call-seq:
1044
+ # seqset[index] -> integer or :* or nil
1045
+ # slice(index) -> integer or :* or nil
1046
+ # seqset[start, length] -> sequence set or nil
1047
+ # slice(start, length) -> sequence set or nil
1048
+ # seqset[range] -> sequence set or nil
1049
+ # slice(range) -> sequence set or nil
1050
+ #
1051
+ # Returns a number or a subset from +self+, without modifying the set.
1052
+ #
1053
+ # When an Integer argument +index+ is given, the number at offset +index+
1054
+ # is returned:
1055
+ #
1056
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
1057
+ # set[0] #=> 10
1058
+ # set[5] #=> 15
1059
+ # set[10] #=> 26
1060
+ #
1061
+ # If +index+ is negative, it counts relative to the end of +self+:
1062
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
1063
+ # set[-1] #=> 26
1064
+ # set[-3] #=> 22
1065
+ # set[-6] #=> 15
1066
+ #
1067
+ # If +index+ is out of range, +nil+ is returned.
1068
+ #
1069
+ # set = Net::IMAP::SequenceSet["10:15,20:23,26"]
1070
+ # set[11] #=> nil
1071
+ # set[-12] #=> nil
1072
+ #
1073
+ # The result is based on the normalized set—sorted and de-duplicated—not
1074
+ # on the assigned value of #string.
1075
+ #
1076
+ # set = Net::IMAP::SequenceSet["12,20:23,11:16,21"]
1077
+ # set[0] #=> 11
1078
+ # set[-1] #=> 23
1079
+ #
1080
+ def [](index, length = nil)
1081
+ if length then slice_length(index, length)
1082
+ elsif index.is_a?(Range) then slice_range(index)
1083
+ else at(index)
1084
+ end
1085
+ end
1086
+
1087
+ alias slice :[]
1088
+
1089
+ private def slice_length(start, length)
1090
+ start = Integer(start.to_int)
1091
+ length = Integer(length.to_int)
1092
+ raise ArgumentError, "length must be positive" unless length.positive?
1093
+ last = start + length - 1 unless start.negative? && start.abs <= length
1094
+ slice_range(start..last)
1095
+ end
1096
+
1097
+ private def slice_range(range)
1098
+ first = range.begin || 0
1099
+ last = range.end || -1
1100
+ last -= 1 if range.exclude_end? && range.end && last != STAR_INT
1101
+ if (first * last).positive? && last < first
1102
+ SequenceSet.empty
1103
+ elsif (min = at(first))
1104
+ max = at(last)
1105
+ if max == :* then self & (min..)
1106
+ elsif min <= max then self & (min..max)
1107
+ else SequenceSet.empty
1108
+ end
1109
+ end
1110
+ end
1111
+
1112
+ # Returns a frozen SequenceSet with <tt>*</tt> converted to +max+, numbers
1113
+ # and ranges over +max+ removed, and ranges containing +max+ converted to
1114
+ # end at +max+.
1115
+ #
1116
+ # Net::IMAP::SequenceSet["5,10:22,50"].limit(max: 20).to_s
1117
+ # #=> "5,10:20"
1118
+ #
1119
+ # <tt>*</tt> is always interpreted as the maximum value. When the set
1120
+ # contains <tt>*</tt>, it will be set equal to the limit.
1121
+ #
1122
+ # Net::IMAP::SequenceSet["*"].limit(max: 37)
1123
+ # #=> Net::IMAP::SequenceSet["37"]
1124
+ # Net::IMAP::SequenceSet["5:*"].limit(max: 37)
1125
+ # #=> Net::IMAP::SequenceSet["5:37"]
1126
+ # Net::IMAP::SequenceSet["500:*"].limit(max: 37)
1127
+ # #=> Net::IMAP::SequenceSet["37"]
1128
+ #
1129
+ def limit(max:)
1130
+ max = to_tuple_int(max)
1131
+ if empty? then self.class.empty
1132
+ elsif !include_star? && max < min then self.class.empty
1133
+ elsif max(star: STAR_INT) <= max then frozen? ? self : dup.freeze
1134
+ else dup.limit!(max: max).freeze
1135
+ end
1136
+ end
1137
+
1138
+ # Removes all members over +max+ and returns self. If <tt>*</tt> is a
1139
+ # member, it will be converted to +max+.
1140
+ #
1141
+ # Related: #limit
1142
+ def limit!(max:)
1143
+ star = include_star?
1144
+ max = to_tuple_int(max)
1145
+ tuple_subtract [max + 1, STAR_INT]
1146
+ tuple_add [max, max ] if star
1147
+ normalize!
1148
+ end
1149
+
1150
+ # :call-seq: complement! -> self
1151
+ #
1152
+ # Converts the SequenceSet to its own #complement. It will contain all
1153
+ # possible values _except_ for those currently in the set.
1154
+ #
1155
+ # Related: #complement
1156
+ def complement!
1157
+ return replace(self.class.full) if empty?
1158
+ return clear if full?
1159
+ flat = @tuples.flat_map { [_1 - 1, _2 + 1] }
1160
+ if flat.first < 1 then flat.shift else flat.unshift 1 end
1161
+ if STAR_INT < flat.last then flat.pop else flat.push STAR_INT end
1162
+ @tuples = flat.each_slice(2).to_a
1163
+ normalize!
1164
+ end
1165
+
1166
+ # Returns a new SequenceSet with a normalized string representation.
1167
+ #
1168
+ # The returned set's #string is sorted and deduplicated. Adjacent or
1169
+ # overlapping elements will be merged into a single larger range.
1170
+ #
1171
+ # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalize
1172
+ # #=> Net::IMAP::SequenceSet["1:7,9:11"]
1173
+ #
1174
+ # Related: #normalize!, #normalized_string
1175
+ def normalize
1176
+ str = normalized_string
1177
+ return self if frozen? && str == string
1178
+ remain_frozen dup.instance_exec { @string = str&.-@; self }
1179
+ end
1180
+
1181
+ # Resets #string to be sorted, deduplicated, and coalesced. Returns
1182
+ # +self+.
1183
+ #
1184
+ # Related: #normalize, #normalized_string
1185
+ def normalize!
1186
+ @string = nil
1187
+ self
1188
+ end
1189
+
1190
+ # Returns a normalized +sequence-set+ string representation, sorted
1191
+ # and deduplicated. Adjacent or overlapping elements will be merged into
1192
+ # a single larger range. Returns +nil+ when the set is empty.
1193
+ #
1194
+ # Net::IMAP::SequenceSet["1:5,3:7,10:9,10:11"].normalized_string
1195
+ # #=> "1:7,9:11"
1196
+ #
1197
+ # Related: #normalize!, #normalize
1198
+ def normalized_string
1199
+ @tuples.empty? ? nil : -@tuples.map { tuple_to_str _1 }.join(",")
1200
+ end
1201
+
1202
+ def inspect
1203
+ if empty?
1204
+ (frozen? ? "%s.empty" : "#<%s empty>") % [self.class]
1205
+ elsif frozen?
1206
+ "%s[%p]" % [self.class, to_s]
1207
+ else
1208
+ "#<%s %p>" % [self.class, to_s]
1209
+ end
1210
+ end
1211
+
1212
+ # Returns self
1213
+ alias to_sequence_set itself
1214
+
1215
+ # Unstable API: currently for internal use only (Net::IMAP#validate_data)
1216
+ def validate # :nodoc:
1217
+ empty? and raise DataFormatError, "empty sequence-set is invalid"
1218
+ self
1219
+ end
1220
+
1221
+ # Unstable API: for internal use only (Net::IMAP#send_data)
1222
+ def send_data(imap, tag) # :nodoc:
1223
+ imap.__send__(:put_string, valid_string)
1224
+ end
1225
+
1226
+ protected
1227
+
1228
+ attr_reader :tuples # :nodoc:
1229
+
1230
+ private
1231
+
1232
+ def remain_frozen(set) frozen? ? set.freeze : set end
1233
+
1234
+ # frozen clones are shallow copied
1235
+ def initialize_clone(other)
1236
+ other.frozen? ? super : initialize_dup(other)
1237
+ end
1238
+
1239
+ def initialize_dup(other)
1240
+ @tuples = other.tuples.map(&:dup)
1241
+ @string = other.string&.-@
1242
+ super
1243
+ end
1244
+
1245
+ def input_to_tuple(obj)
1246
+ obj = input_try_convert obj
1247
+ case obj
1248
+ when *STARS, Integer then [int = to_tuple_int(obj), int]
1249
+ when Range then range_to_tuple(obj)
1250
+ when String then str_to_tuple(obj)
1251
+ else
1252
+ raise DataFormatError, "expected number or range, got %p" % [obj]
1253
+ end
1254
+ end
1255
+
1256
+ def input_to_tuples(obj)
1257
+ obj = input_try_convert obj
1258
+ case obj
1259
+ when *STARS, Integer, Range then [input_to_tuple(obj)]
1260
+ when String then str_to_tuples obj
1261
+ when SequenceSet then obj.tuples
1262
+ when ENUMABLE then obj.flat_map { input_to_tuples _1 }
1263
+ when nil then []
1264
+ else
1265
+ raise DataFormatError,
1266
+ "expected nz-number, range, string, or enumerable; " \
1267
+ "got %p" % [obj]
1268
+ end
1269
+ end
1270
+
1271
+ # unlike SequenceSet#try_convert, this returns an Integer, Range,
1272
+ # String, Set, Array, or... any type of object.
1273
+ def input_try_convert(input)
1274
+ SequenceSet.try_convert(input) ||
1275
+ # Integer.try_convert(input) || # ruby 3.1+
1276
+ input.respond_to?(:to_int) && Integer(input.to_int) ||
1277
+ String.try_convert(input) ||
1278
+ input
1279
+ end
1280
+
1281
+ def range_to_tuple(range)
1282
+ first = to_tuple_int(range.begin || 1)
1283
+ last = to_tuple_int(range.end || :*)
1284
+ last -= 1 if range.exclude_end? && range.end && last != STAR_INT
1285
+ unless first <= last
1286
+ raise DataFormatError, "invalid range for sequence-set: %p" % [range]
1287
+ end
1288
+ [first, last]
1289
+ end
1290
+
1291
+ def to_tuple_int(obj) STARS.include?(obj) ? STAR_INT : nz_number(obj) end
1292
+ def from_tuple_int(num) num == STAR_INT ? :* : num end
1293
+
1294
+ def tuple_to_str(tuple) tuple.uniq.map{ from_tuple_int _1 }.join(":") end
1295
+ def str_to_tuples(str) str.split(",", -1).map! { str_to_tuple _1 } end
1296
+ def str_to_tuple(str)
1297
+ raise DataFormatError, "invalid sequence set string" if str.empty?
1298
+ str.split(":", 2).map! { to_tuple_int _1 }.minmax
1299
+ end
1300
+
1301
+ def include_tuple?((min, max)) range_gte_to(min)&.cover?(min..max) end
1302
+
1303
+ def intersect_tuple?((min, max))
1304
+ range = range_gte_to(min) and
1305
+ range.include?(min) || range.include?(max) || (min..max).cover?(range)
1306
+ end
1307
+
1308
+ def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
1309
+ def tuples_subtract(tuples) tuples.each do tuple_subtract _1 end; self end
1310
+
1311
+ #
1312
+ # --|=====| |=====new tuple=====| append
1313
+ # ?????????-|=====new tuple=====|-|===lower===|-- insert
1314
+ #
1315
+ # |=====new tuple=====|
1316
+ # ---------??=======lower=======??--------------- noop
1317
+ #
1318
+ # ---------??===lower==|--|==| join remaining
1319
+ # ---------??===lower==|--|==|----|===upper===|-- join until upper
1320
+ # ---------??===lower==|--|==|--|=====upper===|-- join to upper
1321
+ def tuple_add(tuple)
1322
+ min, max = tuple
1323
+ lower, lower_idx = tuple_gte_with_index(min - 1)
1324
+ if lower.nil? then tuples << tuple
1325
+ elsif (max + 1) < lower.first then tuples.insert(lower_idx, tuple)
1326
+ else tuple_coalesce(lower, lower_idx, min, max)
1327
+ end
1328
+ end
1329
+
1330
+ def tuple_coalesce(lower, lower_idx, min, max)
1331
+ return if lower.first <= min && max <= lower.last
1332
+ lower[0] = [min, lower.first].min
1333
+ lower[1] = [max, lower.last].max
1334
+ lower_idx += 1
1335
+ return if lower_idx == tuples.count
1336
+ tmax_adj = lower.last + 1
1337
+ upper, upper_idx = tuple_gte_with_index(tmax_adj)
1338
+ if upper
1339
+ tmax_adj < upper.first ? (upper_idx -= 1) : (lower[1] = upper.last)
1340
+ end
1341
+ tuples.slice!(lower_idx..upper_idx)
1342
+ end
1343
+
1344
+ # |====tuple================|
1345
+ # --|====| no more 1. noop
1346
+ # --|====|---------------------------|====lower====|-- 2. noop
1347
+ # -------|======lower================|---------------- 3. split
1348
+ # --------|=====lower================|---------------- 4. trim beginning
1349
+ #
1350
+ # -------|======lower====????????????----------------- trim lower
1351
+ # --------|=====lower====????????????----------------- delete lower
1352
+ #
1353
+ # -------??=====lower===============|----------------- 5. trim/delete one
1354
+ # -------??=====lower====|--|====| no more 6. delete rest
1355
+ # -------??=====lower====|--|====|---|====upper====|-- 7. delete until
1356
+ # -------??=====lower====|--|====|--|=====upper====|-- 8. delete and trim
1357
+ def tuple_subtract(tuple)
1358
+ min, max = tuple
1359
+ lower, idx = tuple_gte_with_index(min)
1360
+ if lower.nil? then nil # case 1.
1361
+ elsif max < lower.first then nil # case 2.
1362
+ elsif max < lower.last then tuple_trim_or_split lower, idx, min, max
1363
+ else tuples_trim_or_delete lower, idx, min, max
1364
+ end
1365
+ end
1366
+
1367
+ def tuple_trim_or_split(lower, idx, tmin, tmax)
1368
+ if lower.first < tmin # split
1369
+ tuples.insert(idx, [lower.first, tmin - 1])
1370
+ end
1371
+ lower[0] = tmax + 1
1372
+ end
1373
+
1374
+ def tuples_trim_or_delete(lower, lower_idx, tmin, tmax)
1375
+ if lower.first < tmin # trim lower
1376
+ lower[1] = tmin - 1
1377
+ lower_idx += 1
1378
+ end
1379
+ if tmax == lower.last # case 5
1380
+ upper_idx = lower_idx
1381
+ elsif (upper, upper_idx = tuple_gte_with_index(tmax + 1))
1382
+ upper_idx -= 1 # cases 7 and 8
1383
+ upper[0] = tmax + 1 if upper.first <= tmax # case 8 (else case 7)
1384
+ end
1385
+ tuples.slice!(lower_idx..upper_idx)
1386
+ end
1387
+
1388
+ def tuple_gte_with_index(num)
1389
+ idx = tuples.bsearch_index { _2 >= num } and [tuples[idx], idx]
1390
+ end
1391
+
1392
+ def range_gte_to(num)
1393
+ first, last = tuples.bsearch { _2 >= num }
1394
+ first..last if first
1395
+ end
1396
+
1397
+ def nz_number(num)
1398
+ case num
1399
+ when Integer, /\A[1-9]\d*\z/ then num = Integer(num)
1400
+ else raise DataFormatError, "%p is not a valid nz-number" % [num]
1401
+ end
1402
+ NumValidator.ensure_nz_number(num)
1403
+ num
1404
+ end
1405
+
1406
+ # intentionally defined after the class implementation
1407
+
1408
+ EMPTY = new.freeze
1409
+ FULL = self["1:*"]
1410
+ private_constant :EMPTY, :FULL
1411
+
1412
+ end
1413
+ end
1414
+ end