net-imap 0.3.7 → 0.5.6

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