diff --git a/.github/workflows/push_gem.yml b/.github/workflows/push_gem.yml
index b5a7bbf9..a085de5c 100644
--- a/.github/workflows/push_gem.yml
+++ b/.github/workflows/push_gem.yml
@@ -24,7 +24,7 @@ jobs:
steps:
# Set up
- name: Harden Runner
- uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+ uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
with:
egress-policy: audit
diff --git a/Gemfile b/Gemfile
index cbceddc9..4b1dfca8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,6 +8,7 @@ gem "digest"
gem "strscan"
gem "base64"
+gem "irb"
gem "rake"
gem "rdoc"
gem "test-unit"
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index 7c8e0506..1729f0b7 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -744,7 +744,7 @@ module Net
# * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
#
class IMAP < Protocol
- VERSION = "0.5.5"
+ VERSION = "0.5.6"
# Aliases for supported capabilities, to be used with the #enable command.
ENABLE_ALIASES = {
@@ -1239,13 +1239,21 @@ def logout!
#
def starttls(**options)
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
- send_command("STARTTLS") do |resp|
+ error = nil
+ ok = send_command("STARTTLS") do |resp|
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
clear_cached_capabilities
clear_responses
start_tls_session
end
+ rescue Exception => error
+ raise # note that the error backtrace is in the receiver_thread
end
+ if error
+ disconnect
+ raise error
+ end
+ ok
end
# :call-seq:
diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb
index edeea30e..1e0300c5 100644
--- a/lib/net/imap/config.rb
+++ b/lib/net/imap/config.rb
@@ -287,6 +287,67 @@ def self.[](config)
#
# Alias for responses_without_block
+ # Whether ResponseParser should use the deprecated UIDPlusData or
+ # CopyUIDData for +COPYUID+ response codes, and UIDPlusData or
+ # AppendUIDData for +APPENDUID+ response codes.
+ #
+ # UIDPlusData stores its data in arrays of numbers, which is vulnerable to
+ # a memory exhaustion denial of service attack from an untrusted or
+ # compromised server. Set this option to +false+ to completely block this
+ # vulnerability. Otherwise, parser_max_deprecated_uidplus_data_size
+ # mitigates this vulnerability.
+ #
+ # AppendUIDData and CopyUIDData are _mostly_ backward-compatible with
+ # UIDPlusData. Most applications should be able to upgrade with little
+ # or no changes.
+ #
+ # (Parser support for +UIDPLUS+ added in +v0.3.2+.)
+ #
+ # (Config option added in +v0.4.19+ and +v0.5.6+.)
+ #
+ # UIDPlusData will be removed in +v0.6+ and this config setting will
+ # be ignored.
+ #
+ # ==== Valid options
+ #
+ # [+true+ (original default)]
+ # ResponseParser only uses UIDPlusData.
+ #
+ # [+:up_to_max_size+ (default since +v0.5.6+)]
+ # ResponseParser uses UIDPlusData when the +uid-set+ size is below
+ # parser_max_deprecated_uidplus_data_size. Above that size,
+ # ResponseParser uses AppendUIDData or CopyUIDData.
+ #
+ # [+false+ (planned default for +v0.6+)]
+ # ResponseParser _only_ uses AppendUIDData and CopyUIDData.
+ attr_accessor :parser_use_deprecated_uidplus_data, type: [
+ true, :up_to_max_size, false
+ ]
+
+ # The maximum +uid-set+ size that ResponseParser will parse into
+ # deprecated UIDPlusData. This limit only applies when
+ # parser_use_deprecated_uidplus_data is not +false+.
+ #
+ # (Parser support for +UIDPLUS+ added in +v0.3.2+.)
+ #
+ # Support for limiting UIDPlusData to a maximum size was added in
+ # +v0.3.8+, +v0.4.19+, and +v0.5.6+.
+ #
+ # UIDPlusData will be removed in +v0.6+.
+ #
+ # ==== Versioned Defaults
+ #
+ # Because this limit guards against a remote server causing catastrophic
+ # memory exhaustion, the versioned default (used by #load_defaults) also
+ # applies to versions without the feature.
+ #
+ # * +0.3+ and prior: 10,000
+ # * +0.4+: 1,000
+ # * +0.5+: 100
+ # * +0.6+: 0
+ #
+ attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer
+
# Creates a new config object and initialize its attribute with +attrs+.
#
# If +parent+ is not given, the global config is used by default.
@@ -367,6 +428,8 @@ def defaults_hash
sasl_ir: true,
enforce_logindisabled: true,
responses_without_block: :warn,
+ parser_use_deprecated_uidplus_data: :up_to_max_size,
+ parser_max_deprecated_uidplus_data_size: 100,
).freeze
@global = default.new
@@ -378,6 +441,8 @@ def defaults_hash
sasl_ir: false,
responses_without_block: :silence_deprecation_warning,
enforce_logindisabled: false,
+ parser_use_deprecated_uidplus_data: true,
+ parser_max_deprecated_uidplus_data_size: 10_000,
).freeze
version_defaults[0.0] = Config[0]
version_defaults[0.1] = Config[0]
@@ -386,12 +451,15 @@ def defaults_hash
version_defaults[0.4] = Config[0.3].dup.update(
sasl_ir: true,
+ parser_max_deprecated_uidplus_data_size: 1000,
).freeze
version_defaults[0.5] = Config[:current]
version_defaults[0.6] = Config[0.5].dup.update(
responses_without_block: :frozen_dup,
+ parser_use_deprecated_uidplus_data: false,
+ parser_max_deprecated_uidplus_data_size: 0,
).freeze
version_defaults[:next] = Config[0.6]
version_defaults[:future] = Config[:next]
diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb
index 16ff306c..d862deaa 100644
--- a/lib/net/imap/response_data.rb
+++ b/lib/net/imap/response_data.rb
@@ -7,6 +7,9 @@ class IMAP < Protocol
autoload :UIDFetchData, "#{__dir__}/fetch_data"
autoload :SearchResult, "#{__dir__}/search_result"
autoload :SequenceSet, "#{__dir__}/sequence_set"
+ autoload :UIDPlusData, "#{__dir__}/uidplus_data"
+ autoload :AppendUIDData, "#{__dir__}/uidplus_data"
+ autoload :CopyUIDData, "#{__dir__}/uidplus_data"
autoload :VanishedData, "#{__dir__}/vanished_data"
# Net::IMAP::ContinuationRequest represents command continuation requests.
@@ -344,55 +347,6 @@ class ResponseCode < Struct.new(:name, :data)
# code data can take.
end
- # UIDPlusData represents the ResponseCode#data that accompanies the
- # +APPENDUID+ and +COPYUID+ {response codes}[rdoc-ref:ResponseCode].
- #
- # A server that supports +UIDPLUS+ should send a UIDPlusData object inside
- # every TaggedResponse returned by the append[rdoc-ref:Net::IMAP#append],
- # copy[rdoc-ref:Net::IMAP#copy], move[rdoc-ref:Net::IMAP#move], {uid
- # copy}[rdoc-ref:Net::IMAP#uid_copy], and {uid
- # move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the destination
- # mailbox reports +UIDNOTSTICKY+.
- #
- # == Required capability
- # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
- # or +IMAP4rev2+ capability.
- #
- class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids)
- ##
- # method: uidvalidity
- # :call-seq: uidvalidity -> nonzero uint32
- #
- # The UIDVALIDITY of the destination mailbox.
-
- ##
- # method: source_uids
- # :call-seq: source_uids -> nil or an array of nonzero uint32
- #
- # The UIDs of the copied or moved messages.
- #
- # Note:: Returns +nil+ for Net::IMAP#append.
-
- ##
- # method: assigned_uids
- # :call-seq: assigned_uids -> an array of nonzero uint32
- #
- # The newly assigned UIDs of the copied, moved, or appended messages.
- #
- # Note:: This always returns an array, even when it contains only one UID.
-
- ##
- # :call-seq: uid_mapping -> nil or a hash
- #
- # Returns a hash mapping each source UID to the newly assigned destination
- # UID.
- #
- # Note:: Returns +nil+ for Net::IMAP#append.
- def uid_mapping
- source_uids&.zip(assigned_uids)&.to_h
- end
- end
-
# MailboxList represents the data of an untagged +LIST+ response, for a
# _single_ mailbox path. IMAP#list returns an array of MailboxList objects.
#
diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb
index f6dc438a..03e54b20 100644
--- a/lib/net/imap/response_parser.rb
+++ b/lib/net/imap/response_parser.rb
@@ -13,13 +13,17 @@ class ResponseParser
attr_reader :config
- # :call-seq: Net::IMAP::ResponseParser.new -> Net::IMAP::ResponseParser
+ # Creates a new ResponseParser.
+ #
+ # When +config+ is frozen or global, the parser #config inherits from it.
+ # Otherwise, +config+ will be used directly.
def initialize(config: Config.global)
@str = nil
@pos = nil
@lex_state = nil
@token = nil
@config = Config[config]
+ @config = @config.new if @config == Config.global || @config.frozen?
end
# :call-seq:
@@ -1997,11 +2001,10 @@ def charset__list
#
# n.b, uniqueid ⊂ uid-set. To avoid inconsistent return types, we always
# match uid_set even if that returns a single-member array.
- #
def resp_code_apnd__data
validity = number; SP!
dst_uids = uid_set # uniqueid ⊂ uid-set
- UIDPlusData.new(validity, nil, dst_uids)
+ AppendUID(validity, dst_uids)
end
# already matched: "COPYUID"
@@ -2011,7 +2014,25 @@ def resp_code_copy__data
validity = number; SP!
src_uids = uid_set; SP!
dst_uids = uid_set
- UIDPlusData.new(validity, src_uids, dst_uids)
+ CopyUID(validity, src_uids, dst_uids)
+ end
+
+ def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
+ def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
+
+ # TODO: remove this code in the v0.6.0 release
+ def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
+ return unless config.parser_use_deprecated_uidplus_data
+ compact_uid_sets = [src_uids, dst_uids].compact
+ count = compact_uid_sets.map { _1.count_with_duplicates }.max
+ max = config.parser_max_deprecated_uidplus_data_size
+ if count <= max
+ src_uids &&= src_uids.each_ordered_number.to_a
+ dst_uids = dst_uids.each_ordered_number.to_a
+ UIDPlusData.new(validity, src_uids, dst_uids)
+ elsif config.parser_use_deprecated_uidplus_data != :up_to_max_size
+ parse_error("uid-set is too large: %d > %d", count, max)
+ end
end
ADDRESS_REGEXP = /\G
@@ -2137,15 +2158,9 @@ def nparens__objectid; NIL? ? nil : parens__objectid end
# uniqueid = nz-number
# ; Strictly ascending
def uid_set
- token = match(T_NUMBER, T_ATOM)
- case token.symbol
- when T_NUMBER then [Integer(token.value)]
- when T_ATOM
- token.value.split(",").flat_map {|range|
- range = range.split(":").map {|uniqueid| Integer(uniqueid) }
- range.size == 1 ? range : Range.new(range.min, range.max).to_a
- }
- end
+ set = sequence_set
+ parse_error("uid-set cannot contain '*'") if set.include_star?
+ set
end
def nil_atom
diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb
index ea8b0a67..02cd150d 100644
--- a/lib/net/imap/sequence_set.rb
+++ b/lib/net/imap/sequence_set.rb
@@ -56,18 +56,20 @@ class IMAP
# set = Net::IMAP::SequenceSet[1, 2, [3..7, 5], 6..10, 2048, 1024]
# set.valid_string #=> "1:10,55,1024:2048"
#
- # == Normalized form
+ # == Ordered and Normalized sets
#
- # When a sequence set is created with a single String value, that #string
- # representation is preserved. SequenceSet's internal representation
- # implicitly sorts all entries, de-duplicates numbers, and coalesces
- # adjacent or overlapping ranges. Most enumeration methods and offset-based
- # methods use this normalized representation. Most modification methods
- # will convert #string to its normalized form.
+ # Sometimes the order of the set's members is significant, such as with the
+ # +ESORT+, CONTEXT=SORT, and +UIDPLUS+ extensions. So, when a
+ # sequence set is created by the parser or with a single string value, that
+ # #string representation is preserved.
#
- # In some cases the order of the string representation is significant, such
- # as the +ESORT+, CONTEXT=SORT, and +UIDPLUS+ extensions. Use
- # #entries or #each_entry to enumerate the set in its original order. To
+ # Internally, SequenceSet stores a normalized representation which sorts all
+ # entries, de-duplicates numbers, and coalesces adjacent or overlapping
+ # ranges. Most methods use this normalized representation to achieve
+ # O(lg n) porformance. Use #entries or #each_entry to enumerate
+ # the set in its original order.
+ #
+ # Most modification methods convert #string to its normalized form. To
# preserve #string order while modifying a set, use #append, #string=, or
# #replace.
#
@@ -160,7 +162,7 @@ class IMAP
# - #===:
# Returns whether a given object is fully contained within +self+, or
# +nil+ if the object cannot be converted to a compatible type.
- # - #cover? (aliased as #===):
+ # - #cover?:
# Returns whether a given object is fully contained within +self+.
# - #intersect? (aliased as #overlap?):
# Returns whether +self+ and a given object have any common elements.
@@ -181,30 +183,41 @@ class IMAP
# - #max: Returns the maximum number in the set.
# - #minmax: Returns the minimum and maximum numbers in the set.
#
- # Accessing value by offset:
+ # Accessing value by offset in sorted set:
# - #[] (aliased as #slice): Returns the number or consecutive subset at a
- # given offset or range of offsets.
- # - #at: Returns the number at a given offset.
- # - #find_index: Returns the given number's offset in the set
+ # given offset or range of offsets in the sorted set.
+ # - #at: Returns the number at a given offset in the sorted set.
+ # - #find_index: Returns the given number's offset in the sorted set.
+ #
+ # Accessing value by offset in ordered entries
+ # - #ordered_at: Returns the number at a given offset in the ordered entries.
+ # - #find_ordered_index: Returns the index of the given number's first
+ # occurrence in entries.
#
# Set cardinality:
# - #count (aliased as #size): Returns the count of numbers in the set.
+ # Duplicated numbers are not counted.
# - #empty?: Returns whether the set has no members. \IMAP syntax does not
# allow empty sequence sets.
# - #valid?: Returns whether the set has any members.
# - #full?: Returns whether the set contains every possible value, including
# *.
#
+ # Denormalized properties:
+ # - #has_duplicates?: Returns whether the ordered entries repeat any
+ # numbers.
+ # - #count_duplicates: Returns the count of repeated numbers in the ordered
+ # entries.
+ # - #count_with_duplicates: Returns the count of numbers in the ordered
+ # entries, including any repeated numbers.
+ #
# === Methods for Iterating
#
+ # Normalized (sorted and coalesced):
# - #each_element: Yields each number and range in the set, sorted and
# coalesced, and returns +self+.
# - #elements (aliased as #to_a): Returns an Array of every number and range
# in the set, sorted and coalesced.
- # - #each_entry: Yields each number and range in the set, unsorted and
- # without deduplicating numbers or coalescing ranges, and returns +self+.
- # - #entries: Returns an Array of every number and range in the set,
- # unsorted and without deduplicating numbers or coalescing ranges.
# - #each_range:
# Yields each element in the set as a Range and returns +self+.
# - #ranges: Returns an Array of every element in the set, converting
@@ -214,6 +227,14 @@ class IMAP
# ranges into all of their contained numbers.
# - #to_set: Returns a Set containing all of the #numbers in the set.
#
+ # Order preserving:
+ # - #each_entry: Yields each number and range in the set, unsorted and
+ # without deduplicating numbers or coalescing ranges, and returns +self+.
+ # - #entries: Returns an Array of every number and range in the set,
+ # unsorted and without deduplicating numbers or coalescing ranges.
+ # - #each_ordered_number: Yields each number in the ordered entries and
+ # returns +self+.
+ #
# === Methods for \Set Operations
# These methods do not modify +self+.
#
@@ -233,19 +254,29 @@ class IMAP
# === Methods for Assigning
# These methods add or replace elements in +self+.
#
+ # Normalized (sorted and coalesced):
+ #
+ # These methods always update #string to be fully sorted and coalesced.
+ #
# - #add (aliased as #<<): Adds a given object to the set; returns +self+.
# - #add?: If the given object is not an element in the set, adds it and
# returns +self+; otherwise, returns +nil+.
# - #merge: Merges multiple elements into the set; returns +self+.
+ # - #complement!: Replaces the contents of the set with its own #complement.
+ #
+ # Order preserving:
+ #
+ # These methods _may_ cause #string to not be sorted or coalesced.
+ #
# - #append: Adds a given object to the set, appending it to the existing
# string, and returns +self+.
# - #string=: Assigns a new #string value and replaces #elements to match.
# - #replace: Replaces the contents of the set with the contents
# of a given object.
- # - #complement!: Replaces the contents of the set with its own #complement.
#
# === Methods for Deleting
- # These methods remove elements from +self+.
+ # These methods remove elements from +self+, and update #string to be fully
+ # sorted and coalesced.
#
# - #clear: Removes all elements in the set; returns +self+.
# - #delete: Removes a given object from the set; returns +self+.
@@ -681,8 +712,9 @@ def append(object)
modifying!
tuple = input_to_tuple object
entry = tuple_to_str tuple
+ string unless empty? # write @string before tuple_add
tuple_add tuple
- @string = -(string ? "#{@string},#{entry}" : entry)
+ @string = -(@string ? "#{@string},#{entry}" : entry)
self
end
@@ -838,8 +870,8 @@ def entries; each_entry.to_a end
# * translates to an endless range. Use #limit to translate both
# cases to a maximum value.
#
- # If the original input was unordered or contains overlapping ranges, the
- # returned ranges will be ordered and coalesced.
+ # The returned elements will be sorted and coalesced, even when the input
+ # #string is not. * will sort last. See #normalize.
#
# Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].elements
# #=> [2, 5..9, 11..12, :*]
@@ -857,7 +889,7 @@ def elements; each_element.to_a end
# translates to :*... Use #limit to set * to a maximum
# value.
#
- # The returned ranges will be ordered and coalesced, even when the input
+ # The returned ranges will be sorted and coalesced, even when the input
# #string is not. * will sort last. See #normalize.
#
# Net::IMAP::SequenceSet["2,5:9,6,*,12:11"].ranges
@@ -906,9 +938,7 @@ def numbers; each_number.to_a end
# Related: #entries, #each_element
def each_entry(&block) # :yields: integer or range or :*
return to_enum(__method__) unless block_given?
- return each_element(&block) unless @string
- @string.split(",").each do yield tuple_to_entry str_to_tuple _1 end
- self
+ each_entry_tuple do yield tuple_to_entry _1 end
end
# Yields each number or range (or :*) in #elements to the block
@@ -926,6 +956,16 @@ def each_element # :yields: integer or range or :*
private
+ def each_entry_tuple(&block)
+ return to_enum(__method__) unless block_given?
+ if @string
+ @string.split(",") do block.call str_to_tuple _1 end
+ else
+ @tuples.each(&block)
+ end
+ self
+ end
+
def tuple_to_entry((min, max))
if min == STAR_INT then :*
elsif max == STAR_INT then min..
@@ -957,19 +997,36 @@ def each_range # :yields: range
# Returns an enumerator when called without a block (even if the set
# contains *).
#
- # Related: #numbers
+ # Related: #numbers, #each_ordered_number
def each_number(&block) # :yields: integer
return to_enum(__method__) unless block_given?
raise RangeError, '%s contains "*"' % [self.class] if include_star?
- each_element do |elem|
- case elem
- when Range then elem.each(&block)
- when Integer then block.(elem)
- end
- end
+ @tuples.each do each_number_in_tuple _1, _2, &block end
self
end
+ # Yields each number in #entries to the block and returns self.
+ # If the set contains a *, RangeError will be raised.
+ #
+ # Returns an enumerator when called without a block (even if the set
+ # contains *).
+ #
+ # Related: #entries, #each_number
+ def each_ordered_number(&block)
+ return to_enum(__method__) unless block_given?
+ raise RangeError, '%s contains "*"' % [self.class] if include_star?
+ each_entry_tuple do each_number_in_tuple _1, _2, &block end
+ end
+
+ private def each_number_in_tuple(min, max, &block)
+ if min == STAR_INT then yield :*
+ elsif min == max then yield min
+ elsif max != STAR_INT then (min..max).each(&block)
+ else
+ raise RangeError, "#{SequenceSet} cannot enumerate range with '*'"
+ end
+ end
+
# Returns a Set with all of the #numbers in the sequence set.
#
# If the set contains a *, RangeError will be raised.
@@ -981,8 +1038,10 @@ def to_set; Set.new(numbers) end
# Returns the count of #numbers in the set.
#
- # If * and 2**32 - 1 (the maximum 32-bit unsigned
- # integer value) are both in the set, they will only be counted once.
+ # * will be counted as 2**32 - 1 (the maximum 32-bit
+ # unsigned integer value).
+ #
+ # Related: #count_with_duplicates
def count
@tuples.sum(@tuples.count) { _2 - _1 } +
(include_star? && include?(UINT32_MAX) ? -1 : 0)
@@ -990,33 +1049,87 @@ def count
alias size count
- # Returns the index of +number+ in the set, or +nil+ if +number+ isn't in
- # the set.
+ # Returns the count of numbers in the ordered #entries, including any
+ # repeated numbers.
+ #
+ # * will be counted as 2**32 - 1 (the maximum 32-bit
+ # unsigned integer value).
+ #
+ # When #string is normalized, this behaves the same as #count.
#
- # Related: #[]
+ # Related: #entries, #count_duplicates, #has_duplicates?
+ def count_with_duplicates
+ return count unless @string
+ each_entry_tuple.sum {|min, max|
+ max - min + ((max == STAR_INT && min != STAR_INT) ? 0 : 1)
+ }
+ end
+
+ # Returns the count of repeated numbers in the ordered #entries, the
+ # difference between #count_with_duplicates and #count.
+ #
+ # When #string is normalized, this is zero.
+ #
+ # Related: #entries, #count_with_duplicates, #has_duplicates?
+ def count_duplicates
+ return 0 unless @string
+ count_with_duplicates - count
+ end
+
+ # :call-seq: has_duplicates? -> true | false
+ #
+ # Returns whether or not the ordered #entries repeat any numbers.
+ #
+ # Always returns +false+ when #string is normalized.
+ #
+ # Related: #entries, #count_with_duplicates, #count_duplicates?
+ def has_duplicates?
+ return false unless @string
+ count_with_duplicates != count
+ end
+
+ # Returns the (sorted and deduplicated) index of +number+ in the set, or
+ # +nil+ if +number+ isn't in the set.
+ #
+ # Related: #[], #at, #find_ordered_index
def find_index(number)
number = to_tuple_int number
- each_tuple_with_index do |min, max, idx_min|
+ each_tuple_with_index(@tuples) do |min, max, idx_min|
number < min and return nil
number <= max and return from_tuple_int(idx_min + (number - min))
end
nil
end
+ # Returns the first index of +number+ in the ordered #entries, or
+ # +nil+ if +number+ isn't in the set.
+ #
+ # Related: #find_index
+ def find_ordered_index(number)
+ number = to_tuple_int number
+ each_tuple_with_index(each_entry_tuple) do |min, max, idx_min|
+ if min <= number && number <= max
+ return from_tuple_int(idx_min + (number - min))
+ end
+ end
+ nil
+ end
+
private
- def each_tuple_with_index
+ def each_tuple_with_index(tuples)
idx_min = 0
- @tuples.each do |min, max|
- yield min, max, idx_min, (idx_max = idx_min + (max - min))
+ tuples.each do |min, max|
+ idx_max = idx_min + (max - min)
+ yield min, max, idx_min, idx_max
idx_min = idx_max + 1
end
idx_min
end
- def reverse_each_tuple_with_index
+ def reverse_each_tuple_with_index(tuples)
idx_max = -1
- @tuples.reverse_each do |min, max|
+ tuples.reverse_each do |min, max|
yield min, max, (idx_min = idx_max - (max - min)), idx_max
idx_max = idx_min - 1
end
@@ -1027,18 +1140,38 @@ def reverse_each_tuple_with_index
# :call-seq: at(index) -> integer or nil
#
- # Returns a number from +self+, without modifying the set. Behaves the
- # same as #[], except that #at only allows a single integer argument.
+ # Returns the number at the given +index+ in the sorted set, without
+ # modifying the set.
#
- # Related: #[], #slice
+ # +index+ is interpreted the same as in #[], except that #at only allows a
+ # single integer argument.
+ #
+ # Related: #[], #slice, #ordered_at
def at(index)
+ lookup_number_by_tuple_index(tuples, index)
+ end
+
+ # :call-seq: ordered_at(index) -> integer or nil
+ #
+ # Returns the number at the given +index+ in the ordered #entries, without
+ # modifying the set.
+ #
+ # +index+ is interpreted the same as in #at (and #[]), except that
+ # #ordered_at applies to the ordered #entries, not the sorted set.
+ #
+ # Related: #[], #slice, #ordered_at
+ def ordered_at(index)
+ lookup_number_by_tuple_index(each_entry_tuple, index)
+ end
+
+ private def lookup_number_by_tuple_index(tuples, index)
index = Integer(index.to_int)
if index.negative?
- reverse_each_tuple_with_index do |min, max, idx_min, idx_max|
+ reverse_each_tuple_with_index(tuples) do |min, max, idx_min, idx_max|
idx_min <= index and return from_tuple_int(min + (index - idx_min))
end
else
- each_tuple_with_index do |min, _, idx_min, idx_max|
+ each_tuple_with_index(tuples) do |min, _, idx_min, idx_max|
index <= idx_max and return from_tuple_int(min + (index - idx_min))
end
end
@@ -1053,17 +1186,18 @@ def at(index)
# seqset[range] -> sequence set or nil
# slice(range) -> sequence set or nil
#
- # Returns a number or a subset from +self+, without modifying the set.
+ # Returns a number or a subset from the _sorted_ set, without modifying
+ # the set.
#
# When an Integer argument +index+ is given, the number at offset +index+
- # is returned:
+ # in the sorted set is returned:
#
# set = Net::IMAP::SequenceSet["10:15,20:23,26"]
# set[0] #=> 10
# set[5] #=> 15
# set[10] #=> 26
#
- # If +index+ is negative, it counts relative to the end of +self+:
+ # If +index+ is negative, it counts relative to the end of the sorted set:
# set = Net::IMAP::SequenceSet["10:15,20:23,26"]
# set[-1] #=> 26
# set[-3] #=> 22
@@ -1075,13 +1209,14 @@ def at(index)
# set[11] #=> nil
# set[-12] #=> nil
#
- # The result is based on the normalized set—sorted and de-duplicated—not
- # on the assigned value of #string.
+ # The result is based on the sorted and de-duplicated set, not on the
+ # ordered #entries in #string.
#
# set = Net::IMAP::SequenceSet["12,20:23,11:16,21"]
# set[0] #=> 11
# set[-1] #=> 23
#
+ # Related: #at
def [](index, length = nil)
if length then slice_length(index, length)
elsif index.is_a?(Range) then slice_range(index)
@@ -1337,8 +1472,8 @@ def tuple_add(tuple)
modifying!
min, max = tuple
lower, lower_idx = tuple_gte_with_index(min - 1)
- if lower.nil? then tuples << tuple
- elsif (max + 1) < lower.first then tuples.insert(lower_idx, tuple)
+ if lower.nil? then tuples << [min, max]
+ elsif (max + 1) < lower.first then tuples.insert(lower_idx, [min, max])
else tuple_coalesce(lower, lower_idx, min, max)
end
end
diff --git a/lib/net/imap/uidplus_data.rb b/lib/net/imap/uidplus_data.rb
new file mode 100644
index 00000000..0e593636
--- /dev/null
+++ b/lib/net/imap/uidplus_data.rb
@@ -0,0 +1,244 @@
+# frozen_string_literal: true
+
+module Net
+ class IMAP < Protocol
+
+ # *NOTE:* UIDPlusData is deprecated and will be removed in the +0.6.0+
+ # release. To use AppendUIDData and CopyUIDData before +0.6.0+, set
+ # Config#parser_use_deprecated_uidplus_data to +false+.
+ #
+ # UIDPlusData represents the ResponseCode#data that accompanies the
+ # +APPENDUID+ and +COPYUID+ {response codes}[rdoc-ref:ResponseCode].
+ #
+ # A server that supports +UIDPLUS+ should send UIDPlusData in response to
+ # the append[rdoc-ref:Net::IMAP#append], copy[rdoc-ref:Net::IMAP#copy],
+ # move[rdoc-ref:Net::IMAP#move], {uid copy}[rdoc-ref:Net::IMAP#uid_copy],
+ # and {uid move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the
+ # destination mailbox reports +UIDNOTSTICKY+.
+ #
+ # Note that append[rdoc-ref:Net::IMAP#append], copy[rdoc-ref:Net::IMAP#copy]
+ # and {uid_copy}[rdoc-ref:Net::IMAP#uid_copy] return UIDPlusData in their
+ # TaggedResponse. But move[rdoc-ref:Net::IMAP#copy] and
+ # {uid_move}[rdoc-ref:Net::IMAP#uid_move] _should_ send UIDPlusData in an
+ # UntaggedResponse response before sending their TaggedResponse. However
+ # some servers do send UIDPlusData in the TaggedResponse for +MOVE+
+ # commands---this complies with the older +UIDPLUS+ specification but is
+ # discouraged by the +MOVE+ extension and disallowed by +IMAP4rev2+.
+ #
+ # == Required capability
+ # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
+ # or +IMAP4rev2+ capability.
+ #
+ class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids)
+ ##
+ # method: uidvalidity
+ # :call-seq: uidvalidity -> nonzero uint32
+ #
+ # The UIDVALIDITY of the destination mailbox.
+
+ ##
+ # method: source_uids
+ # :call-seq: source_uids -> nil or an array of nonzero uint32
+ #
+ # The UIDs of the copied or moved messages.
+ #
+ # Note:: Returns +nil+ for Net::IMAP#append.
+
+ ##
+ # method: assigned_uids
+ # :call-seq: assigned_uids -> an array of nonzero uint32
+ #
+ # The newly assigned UIDs of the copied, moved, or appended messages.
+ #
+ # Note:: This always returns an array, even when it contains only one UID.
+
+ ##
+ # :call-seq: uid_mapping -> nil or a hash
+ #
+ # Returns a hash mapping each source UID to the newly assigned destination
+ # UID.
+ #
+ # Note:: Returns +nil+ for Net::IMAP#append.
+ def uid_mapping
+ source_uids&.zip(assigned_uids)&.to_h
+ end
+ end
+
+ # >>>
+ # *NOTE:* AppendUIDData will replace UIDPlusData for +APPENDUID+ in the
+ # +0.6.0+ release. To use AppendUIDData before +0.6.0+, set
+ # Config#parser_use_deprecated_uidplus_data to +false+.
+ #
+ # AppendUIDData represents the ResponseCode#data that accompanies the
+ # +APPENDUID+ {response code}[rdoc-ref:ResponseCode].
+ #
+ # A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send
+ # AppendUIDData inside every TaggedResponse returned by the
+ # append[rdoc-ref:Net::IMAP#append] command---unless the target mailbox
+ # reports +UIDNOTSTICKY+.
+ #
+ # == Required capability
+ # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
+ # or +IMAP4rev2+ capability.
+ class AppendUIDData < Data.define(:uidvalidity, :assigned_uids)
+ def initialize(uidvalidity:, assigned_uids:)
+ uidvalidity = Integer(uidvalidity)
+ assigned_uids = SequenceSet[assigned_uids]
+ NumValidator.ensure_nz_number(uidvalidity)
+ if assigned_uids.include_star?
+ raise DataFormatError, "uid-set cannot contain '*'"
+ end
+ super
+ end
+
+ ##
+ # attr_reader: uidvalidity
+ # :call-seq: uidvalidity -> nonzero uint32
+ #
+ # The UIDVALIDITY of the destination mailbox.
+
+ ##
+ # attr_reader: assigned_uids
+ #
+ # A SequenceSet with the newly assigned UIDs of the appended messages.
+
+ # Returns the number of messages that have been appended.
+ def size
+ assigned_uids.count_with_duplicates
+ end
+ end
+
+ # >>>
+ # *NOTE:* CopyUIDData will replace UIDPlusData for +COPYUID+ in the
+ # +0.6.0+ release. To use CopyUIDData before +0.6.0+, set
+ # Config#parser_use_deprecated_uidplus_data to +false+.
+ #
+ # CopyUIDData represents the ResponseCode#data that accompanies the
+ # +COPYUID+ {response code}[rdoc-ref:ResponseCode].
+ #
+ # A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send CopyUIDData
+ # in response to
+ # copy[rdoc-ref:Net::IMAP#copy], {uid_copy}[rdoc-ref:Net::IMAP#uid_copy],
+ # move[rdoc-ref:Net::IMAP#copy], and {uid_move}[rdoc-ref:Net::IMAP#uid_move]
+ # commands---unless the destination mailbox reports +UIDNOTSTICKY+.
+ #
+ # Note that copy[rdoc-ref:Net::IMAP#copy] and
+ # {uid_copy}[rdoc-ref:Net::IMAP#uid_copy] return CopyUIDData in their
+ # TaggedResponse. But move[rdoc-ref:Net::IMAP#copy] and
+ # {uid_move}[rdoc-ref:Net::IMAP#uid_move] _should_ send CopyUIDData in an
+ # UntaggedResponse response before sending their TaggedResponse. However
+ # some servers do send CopyUIDData in the TaggedResponse for +MOVE+
+ # commands---this complies with the older +UIDPLUS+ specification but is
+ # discouraged by the +MOVE+ extension and disallowed by +IMAP4rev2+.
+ #
+ # == Required capability
+ # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
+ # or +IMAP4rev2+ capability.
+ class CopyUIDData < Data.define(:uidvalidity, :source_uids, :assigned_uids)
+ def initialize(uidvalidity:, source_uids:, assigned_uids:)
+ uidvalidity = Integer(uidvalidity)
+ source_uids = SequenceSet[source_uids]
+ assigned_uids = SequenceSet[assigned_uids]
+ NumValidator.ensure_nz_number(uidvalidity)
+ if source_uids.include_star? || assigned_uids.include_star?
+ raise DataFormatError, "uid-set cannot contain '*'"
+ elsif source_uids.count_with_duplicates != assigned_uids.count_with_duplicates
+ raise DataFormatError, "mismatched uid-set sizes for %s and %s" % [
+ source_uids, assigned_uids
+ ]
+ end
+ super
+ end
+
+ ##
+ # attr_reader: uidvalidity
+ #
+ # The +UIDVALIDITY+ of the destination mailbox (a nonzero unsigned 32 bit
+ # integer).
+
+ ##
+ # attr_reader: source_uids
+ #
+ # A SequenceSet with the original UIDs of the copied or moved messages.
+
+ ##
+ # attr_reader: assigned_uids
+ #
+ # A SequenceSet with the newly assigned UIDs of the copied or moved
+ # messages.
+
+ # Returns the number of messages that have been copied or moved.
+ # source_uids and the assigned_uids will both the same number of UIDs.
+ def size
+ assigned_uids.count_with_duplicates
+ end
+
+ # :call-seq:
+ # assigned_uid_for(source_uid) -> uid
+ # self[source_uid] -> uid
+ #
+ # Returns the UID in the destination mailbox for the message that was
+ # copied from +source_uid+ in the source mailbox.
+ #
+ # This is the reverse of #source_uid_for.
+ #
+ # Related: source_uid_for, each_uid_pair, uid_mapping
+ def assigned_uid_for(source_uid)
+ idx = source_uids.find_ordered_index(source_uid) and
+ assigned_uids.ordered_at(idx)
+ end
+ alias :[] :assigned_uid_for
+
+ # :call-seq:
+ # source_uid_for(assigned_uid) -> uid
+ #
+ # Returns the UID in the source mailbox for the message that was copied to
+ # +assigned_uid+ in the source mailbox.
+ #
+ # This is the reverse of #assigned_uid_for.
+ #
+ # Related: assigned_uid_for, each_uid_pair, uid_mapping
+ def source_uid_for(assigned_uid)
+ idx = assigned_uids.find_ordered_index(assigned_uid) and
+ source_uids.ordered_at(idx)
+ end
+
+ # Yields a pair of UIDs for each copied message. The first is the
+ # message's UID in the source mailbox and the second is the UID in the
+ # destination mailbox.
+ #
+ # Returns an enumerator when no block is given.
+ #
+ # Please note the warning on uid_mapping before calling methods like
+ # +to_h+ or +to_a+ on the returned enumerator.
+ #
+ # Related: uid_mapping, assigned_uid_for, source_uid_for
+ def each_uid_pair
+ return enum_for(__method__) unless block_given?
+ source_uids.each_ordered_number.lazy
+ .zip(assigned_uids.each_ordered_number.lazy) do
+ |source_uid, assigned_uid|
+ yield source_uid, assigned_uid
+ end
+ end
+ alias each_pair each_uid_pair
+ alias each each_uid_pair
+
+ # :call-seq: uid_mapping -> hash
+ #
+ # Returns a hash mapping each source UID to the newly assigned destination
+ # UID.
+ #
+ # *Warning:* The hash that is created may consume _much_ more
+ # memory than the data used to create it. When handling responses from an
+ # untrusted server, check #size before calling this method.
+ #
+ # Related: each_uid_pair, assigned_uid_for, source_uid_for
+ def uid_mapping
+ each_uid_pair.to_h
+ end
+
+ end
+
+ end
+end
diff --git a/test/net/imap/fake_server/test_helper.rb b/test/net/imap/fake_server/test_helper.rb
index eee6a153..1987d048 100644
--- a/test/net/imap/fake_server/test_helper.rb
+++ b/test/net/imap/fake_server/test_helper.rb
@@ -4,7 +4,7 @@
module Net::IMAP::FakeServer::TestHelper
- def run_fake_server_in_thread(ignore_io_error: false, timeout: 5, **opts)
+ def run_fake_server_in_thread(ignore_io_error: false, timeout: 10, **opts)
Timeout.timeout(timeout) do
server = Net::IMAP::FakeServer.new(timeout: timeout, **opts)
@threads << Thread.new do
diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb
index f3e53c40..dd955b8d 100644
--- a/test/net/imap/test_imap.rb
+++ b/test/net/imap/test_imap.rb
@@ -46,14 +46,18 @@ def test_imaps_with_ca_file
# Otherwise, failures can't logout and need to wait for the timeout.
verified, imap = :unknown, nil
assert_nothing_raised do
- imaps_test do |port|
- imap = Net::IMAP.new("localhost",
- port: port,
- ssl: { :ca_file => CA_FILE })
- verified = imap.tls_verified?
- imap
- rescue SystemCallError
- skip $!
+ begin
+ imaps_test do |port|
+ imap = Net::IMAP.new("localhost",
+ port: port,
+ ssl: { :ca_file => CA_FILE })
+ verified = imap.tls_verified?
+ imap
+ rescue SystemCallError
+ skip $!
+ end
+ rescue OpenSSL::SSL::SSLError => e
+ raise e unless /darwin/ =~ RUBY_PLATFORM
end
end
assert_equal true, verified
@@ -69,14 +73,18 @@ def test_imaps_verify_none
# Otherwise, failures can't logout and need to wait for the timeout.
verified, imap = :unknown, nil
assert_nothing_raised do
- imaps_test do |port|
- imap = Net::IMAP.new(
- server_addr,
- port: port,
- ssl: { :verify_mode => OpenSSL::SSL::VERIFY_NONE }
- )
- verified = imap.tls_verified?
- imap
+ begin
+ imaps_test do |port|
+ imap = Net::IMAP.new(
+ server_addr,
+ port: port,
+ ssl: { :verify_mode => OpenSSL::SSL::VERIFY_NONE }
+ )
+ verified = imap.tls_verified?
+ imap
+ end
+ rescue OpenSSL::SSL::SSLError => e
+ raise e unless /darwin/ =~ RUBY_PLATFORM
end
end
assert_equal false, verified
@@ -105,17 +113,16 @@ def test_starttls_unknown_ca
omit "This test is not working with Windows" if RUBY_PLATFORM =~ /mswin|mingw/
imap = nil
- assert_raise(OpenSSL::SSL::SSLError) do
- ex = nil
- starttls_test do |port|
- imap = Net::IMAP.new("localhost", port: port)
+ ex = nil
+ starttls_test do |port|
+ imap = Net::IMAP.new("localhost", port: port)
+ begin
imap.starttls
- imap
rescue => ex
- imap
end
- raise ex if ex
+ imap
end
+ assert_kind_of(OpenSSL::SSL::SSLError, ex)
assert_equal false, imap.tls_verified?
assert_equal({}, imap.ssl_ctx_params)
assert_equal(nil, imap.ssl_ctx.ca_file)
diff --git a/test/net/imap/test_imap_response_data.rb b/test/net/imap/test_imap_response_data.rb
deleted file mode 100644
index 0bee8b9a..00000000
--- a/test/net/imap/test_imap_response_data.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require "net/imap"
-require "test/unit"
-
-class IMAPResponseDataTest < Test::Unit::TestCase
-
- def setup
- Net::IMAP.config.reset
- @do_not_reverse_lookup = Socket.do_not_reverse_lookup
- Socket.do_not_reverse_lookup = true
- end
-
- def teardown
- Socket.do_not_reverse_lookup = @do_not_reverse_lookup
- end
-
- def test_uidplus_copyuid__uid_mapping
- parser = Net::IMAP::ResponseParser.new
- response = parser.parse(
- "A004 OK [copyUID 9999 20:19,500:495 92:97,101:100] Done\r\n"
- )
- code = response.data.code
- assert_equal(
- {
- 19 => 92,
- 20 => 93,
- 495 => 94,
- 496 => 95,
- 497 => 96,
- 498 => 97,
- 499 => 100,
- 500 => 101,
- },
- code.data.uid_mapping
- )
- end
-
- def test_thread_member_to_sequence_set
- # copied from the fourth example in RFC5256: (3 6 (4 23)(44 7 96))
- thmember = Net::IMAP::ThreadMember.method :new
- thread = thmember.(3, [
- thmember.(6, [
- thmember.(4, [
- thmember.(23, [])
- ]),
- thmember.(44, [
- thmember.(7, [
- thmember.(96, [])
- ])
- ])
- ])
- ])
- expected = Net::IMAP::SequenceSet.new("3:4,6:7,23,44,96")
- assert_equal(expected, thread.to_sequence_set)
- end
-
-end
diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb
index e1eb16c5..fa6ba757 100644
--- a/test/net/imap/test_imap_response_parser.rb
+++ b/test/net/imap/test_imap_response_parser.rb
@@ -118,6 +118,41 @@ def teardown
# response data, should still use normal tests, below
############################################################################
+ test "default config inherits from Config.global" do
+ parser = Net::IMAP::ResponseParser.new
+ refute parser.config.frozen?
+ refute_equal Net::IMAP::Config.global, parser.config
+ assert_same Net::IMAP::Config.global, parser.config.parent
+ end
+
+ test "config can be passed in to #initialize" do
+ config = Net::IMAP::Config.global.new
+ parser = Net::IMAP::ResponseParser.new config: config
+ assert_same config, parser.config
+ end
+
+ test "passing in global config inherits from Config.global" do
+ parser = Net::IMAP::ResponseParser.new config: Net::IMAP::Config.global
+ refute parser.config.frozen?
+ refute_equal Net::IMAP::Config.global, parser.config
+ assert_same Net::IMAP::Config.global, parser.config.parent
+ end
+
+ test "config will inherits from passed in frozen config" do
+ parser = Net::IMAP::ResponseParser.new config: {debug: true}
+ refute_equal Net::IMAP::Config.global, parser.config.parent
+ refute parser.config.frozen?
+
+ assert parser.config.parent.frozen?
+ assert parser.config.debug?
+ assert parser.config.inherited?(:debug)
+
+ config = Net::IMAP::Config[debug: true]
+ parser = Net::IMAP::ResponseParser.new(config:)
+ refute_equal Net::IMAP::Config.global, parser.config.parent
+ assert_same config, parser.config.parent
+ end
+
# Strangly, there are no example responses for BINARY[section] in either
# RFC3516 or RFC9051! The closest I found was RFC5259, and those examples
# aren't FETCH responses.
@@ -167,4 +202,137 @@ def test_fetch_binary_and_binary_size
Net::IMAP.debug = debug
end
+ test "APPENDUID with '*'" do
+ parser = Net::IMAP::ResponseParser.new
+ assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set cannot contain '\*'/ do
+ parser.parse(
+ "A004 OK [appendUID 1 1:*] Done\r\n"
+ )
+ end
+ end
+
+ test "APPENDUID with parser_use_deprecated_uidplus_data = true" do
+ parser = Net::IMAP::ResponseParser.new(config: {
+ parser_use_deprecated_uidplus_data: true,
+ parser_max_deprecated_uidplus_data_size: 10_000,
+ })
+ assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
+ parser.parse(
+ "A004 OK [APPENDUID 1 10000:20000,1] Done\r\n"
+ )
+ end
+ response = parser.parse("A004 OK [APPENDUID 1 100:200] Done\r\n")
+ uidplus = response.data.code.data
+ assert_equal 101, uidplus.assigned_uids.size
+ parser.config.parser_max_deprecated_uidplus_data_size = 100
+ assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
+ parser.parse(
+ "A004 OK [APPENDUID 1 100:200] Done\r\n"
+ )
+ end
+ response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
+ uidplus = response.data.code.data
+ assert_instance_of Net::IMAP::UIDPlusData, uidplus
+ assert_equal 100, uidplus.assigned_uids.size
+ end
+
+ test "APPENDUID with parser_use_deprecated_uidplus_data = :up_to_max_size" do
+ parser = Net::IMAP::ResponseParser.new(config: {
+ parser_use_deprecated_uidplus_data: :up_to_max_size,
+ parser_max_deprecated_uidplus_data_size: 100
+ })
+ response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
+ assert_instance_of Net::IMAP::UIDPlusData, response.data.code.data
+ response = parser.parse("A004 OK [APPENDUID 1 100:200] Done\r\n")
+ assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
+ end
+
+ test "APPENDUID with parser_use_deprecated_uidplus_data = false" do
+ parser = Net::IMAP::ResponseParser.new(config: {
+ parser_use_deprecated_uidplus_data: false,
+ parser_max_deprecated_uidplus_data_size: 10_000_000,
+ })
+ response = parser.parse("A004 OK [APPENDUID 1 10] Done\r\n")
+ assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
+ end
+
+ test "COPYUID with backwards ranges" do
+ parser = Net::IMAP::ResponseParser.new
+ response = parser.parse(
+ "A004 OK [copyUID 9999 20:19,500:495 92:97,101:100] Done\r\n"
+ )
+ code = response.data.code
+ assert_equal(
+ {
+ 19 => 92,
+ 20 => 93,
+ 495 => 94,
+ 496 => 95,
+ 497 => 96,
+ 498 => 97,
+ 499 => 100,
+ 500 => 101,
+ },
+ code.data.uid_mapping
+ )
+ end
+
+ test "COPYUID with '*'" do
+ parser = Net::IMAP::ResponseParser.new
+ assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set cannot contain '\*'/ do
+ parser.parse(
+ "A004 OK [copyUID 1 1:* 1:*] Done\r\n"
+ )
+ end
+ end
+
+ test "COPYUID with parser_use_deprecated_uidplus_data = true" do
+ parser = Net::IMAP::ResponseParser.new(config: {
+ parser_use_deprecated_uidplus_data: true,
+ parser_max_deprecated_uidplus_data_size: 10_000,
+ })
+ assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
+ parser.parse(
+ "A004 OK [copyUID 1 10000:20000,1 1:10001] Done\r\n"
+ )
+ end
+ response = parser.parse("A004 OK [copyUID 1 100:200 1:101] Done\r\n")
+ uidplus = response.data.code.data
+ assert_equal 101, uidplus.assigned_uids.size
+ assert_equal 101, uidplus.source_uids.size
+ parser.config.parser_max_deprecated_uidplus_data_size = 100
+ assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
+ parser.parse(
+ "A004 OK [copyUID 1 100:200 1:101] Done\r\n"
+ )
+ end
+ response = parser.parse("A004 OK [copyUID 1 101:200 1:100] Done\r\n")
+ uidplus = response.data.code.data
+ assert_instance_of Net::IMAP::UIDPlusData, uidplus
+ assert_equal 100, uidplus.assigned_uids.size
+ assert_equal 100, uidplus.source_uids.size
+ end
+
+ test "COPYUID with parser_use_deprecated_uidplus_data = :up_to_max_size" do
+ parser = Net::IMAP::ResponseParser.new(config: {
+ parser_use_deprecated_uidplus_data: :up_to_max_size,
+ parser_max_deprecated_uidplus_data_size: 100
+ })
+ response = parser.parse("A004 OK [COPYUID 1 101:200 1:100] Done\r\n")
+ copyuid = response.data.code.data
+ assert_instance_of Net::IMAP::UIDPlusData, copyuid
+ response = parser.parse("A004 OK [COPYUID 1 100:200 1:101] Done\r\n")
+ copyuid = response.data.code.data
+ assert_instance_of Net::IMAP::CopyUIDData, copyuid
+ end
+
+ test "COPYUID with parser_use_deprecated_uidplus_data = false" do
+ parser = Net::IMAP::ResponseParser.new(config: {
+ parser_use_deprecated_uidplus_data: false,
+ parser_max_deprecated_uidplus_data_size: 10_000_000,
+ })
+ response = parser.parse("A004 OK [COPYUID 1 101 1] Done\r\n")
+ assert_instance_of Net::IMAP::CopyUIDData, response.data.code.data
+ end
+
end
diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb
index 85204b24..aad844ac 100644
--- a/test/net/imap/test_sequence_set.rb
+++ b/test/net/imap/test_sequence_set.rb
@@ -167,6 +167,21 @@ def obj.to_sequence_set; 192_168.001_255 end
assert_raise DataFormatError do SequenceSet.try_convert(obj) end
end
+ test "#at(non-negative index)" do
+ assert_nil SequenceSet.empty.at(0)
+ assert_equal 1, SequenceSet[1..].at(0)
+ assert_equal 1, SequenceSet.full.at(0)
+ assert_equal 111, SequenceSet.full.at(110)
+ assert_equal 4, SequenceSet[2,4,6,8].at(1)
+ assert_equal 8, SequenceSet[2,4,6,8].at(3)
+ assert_equal 6, SequenceSet[4..6].at(2)
+ assert_nil SequenceSet[4..6].at(3)
+ assert_equal 205, SequenceSet["101:110,201:210,301:310"].at(14)
+ assert_equal 310, SequenceSet["101:110,201:210,301:310"].at(29)
+ assert_nil SequenceSet["101:110,201:210,301:310"].at(44)
+ assert_equal :*, SequenceSet["1:10,*"].at(10)
+ end
+
test "#[non-negative index]" do
assert_nil SequenceSet.empty[0]
assert_equal 1, SequenceSet[1..][0]
@@ -182,18 +197,64 @@ def obj.to_sequence_set; 192_168.001_255 end
assert_equal :*, SequenceSet["1:10,*"][10]
end
+ test "#at(negative index)" do
+ assert_nil SequenceSet.empty.at(-1)
+ assert_equal :*, SequenceSet[1..].at(-1)
+ assert_equal 1, SequenceSet.full.at(-(2**32))
+ assert_equal 111, SequenceSet[1..111].at(-1)
+ assert_equal 6, SequenceSet[2,4,6,8].at(-2)
+ assert_equal 2, SequenceSet[2,4,6,8].at(-4)
+ assert_equal 4, SequenceSet[4..6].at(-3)
+ assert_nil SequenceSet[4..6].at(-4)
+ assert_equal 207, SequenceSet["101:110,201:210,301:310"].at(-14)
+ assert_equal 102, SequenceSet["101:110,201:210,301:310"].at(-29)
+ assert_nil SequenceSet["101:110,201:210,301:310"].at(-44)
+ end
+
test "#[negative index]" do
- assert_nil SequenceSet.empty[0]
+ assert_nil SequenceSet.empty[-1]
assert_equal :*, SequenceSet[1..][-1]
assert_equal 1, SequenceSet.full[-(2**32)]
assert_equal 111, SequenceSet[1..111][-1]
- assert_equal 4, SequenceSet[2,4,6,8][1]
- assert_equal 8, SequenceSet[2,4,6,8][3]
- assert_equal 6, SequenceSet[4..6][2]
- assert_nil SequenceSet[4..6][3]
- assert_equal 205, SequenceSet["101:110,201:210,301:310"][14]
- assert_equal 310, SequenceSet["101:110,201:210,301:310"][29]
- assert_nil SequenceSet["101:110,201:210,301:310"][44]
+ assert_equal 6, SequenceSet[2,4,6,8][-2]
+ assert_equal 2, SequenceSet[2,4,6,8][-4]
+ assert_equal 4, SequenceSet[4..6][-3]
+ assert_nil SequenceSet[4..6][-4]
+ assert_equal 207, SequenceSet["101:110,201:210,301:310"][-14]
+ assert_equal 102, SequenceSet["101:110,201:210,301:310"][-29]
+ assert_nil SequenceSet["101:110,201:210,301:310"][-44]
+ end
+
+ test "#ordered_at(non-negative index)" do
+ assert_nil SequenceSet.empty.ordered_at(0)
+ assert_equal 1, SequenceSet.full.ordered_at(0)
+ assert_equal 111, SequenceSet.full.ordered_at(110)
+ assert_equal 1, SequenceSet["1:*"].ordered_at(0)
+ assert_equal :*, SequenceSet["*,1"].ordered_at(0)
+ assert_equal 4, SequenceSet["6,4,8,2"].ordered_at(1)
+ assert_equal 2, SequenceSet["6,4,8,2"].ordered_at(3)
+ assert_equal 6, SequenceSet["9:11,4:6,1:3"].ordered_at(5)
+ assert_nil SequenceSet["9:11,4:6,1:3"].ordered_at(9)
+ assert_equal 105, SequenceSet["201:210,101:110,301:310"].ordered_at(14)
+ assert_equal 310, SequenceSet["201:210,101:110,301:310"].ordered_at(29)
+ assert_nil SequenceSet["201:210,101:110,301:310"].ordered_at(30)
+ assert_equal :*, SequenceSet["1:10,*"].ordered_at(10)
+ end
+
+ test "#ordered_at(negative index)" do
+ assert_nil SequenceSet.empty.ordered_at(-1)
+ assert_equal :*, SequenceSet["1:*"].ordered_at(-1)
+ assert_equal 1, SequenceSet.full.ordered_at(-(2**32))
+ assert_equal :*, SequenceSet["*,1"].ordered_at(0)
+ assert_equal 8, SequenceSet["6,4,8,2"].ordered_at(-2)
+ assert_equal 6, SequenceSet["6,4,8,2"].ordered_at(-4)
+ assert_equal 4, SequenceSet["9:11,4:6,1:3"].ordered_at(-6)
+ assert_equal 10, SequenceSet["9:11,4:6,1:3"].ordered_at(-8)
+ assert_nil SequenceSet["9:11,4:6,1:3"].ordered_at(-12)
+ assert_equal 107, SequenceSet["201:210,101:110,301:310"].ordered_at(-14)
+ assert_equal 201, SequenceSet["201:210,101:110,301:310"].ordered_at(-30)
+ assert_nil SequenceSet["201:210,101:110,301:310"].ordered_at(-31)
+ assert_equal :*, SequenceSet["1:10,*"].ordered_at(10)
end
test "#[start, length]" do
@@ -251,6 +312,44 @@ def obj.to_sequence_set; 192_168.001_255 end
assert_equal 2**32 - 1, SequenceSet.full.find_index(:*)
end
+ test "#find_ordered_index" do
+ assert_equal 9, SequenceSet.full.find_ordered_index(10)
+ assert_equal 99, SequenceSet.full.find_ordered_index(100)
+ assert_equal 2**32 - 1, SequenceSet.full.find_ordered_index(:*)
+ assert_nil SequenceSet.empty.find_index(1)
+ set = SequenceSet["9,8,7,6,5,4,3,2,1"]
+ assert_equal 0, set.find_ordered_index(9)
+ assert_equal 1, set.find_ordered_index(8)
+ assert_equal 2, set.find_ordered_index(7)
+ assert_equal 3, set.find_ordered_index(6)
+ assert_equal 4, set.find_ordered_index(5)
+ assert_equal 5, set.find_ordered_index(4)
+ assert_equal 6, set.find_ordered_index(3)
+ assert_equal 7, set.find_ordered_index(2)
+ assert_equal 8, set.find_ordered_index(1)
+ assert_nil set.find_ordered_index(10)
+ set = SequenceSet["7:9,5:6"]
+ assert_equal 0, set.find_ordered_index(7)
+ assert_equal 1, set.find_ordered_index(8)
+ assert_equal 2, set.find_ordered_index(9)
+ assert_equal 3, set.find_ordered_index(5)
+ assert_equal 4, set.find_ordered_index(6)
+ assert_nil set.find_ordered_index(4)
+ set = SequenceSet["1000:1111,1:100"]
+ assert_equal 0, set.find_ordered_index(1000)
+ assert_equal 100, set.find_ordered_index(1100)
+ assert_equal 112, set.find_ordered_index(1)
+ assert_equal 121, set.find_ordered_index(10)
+ set = SequenceSet["1,1,1,1,51,50,4,11"]
+ assert_equal 0, set.find_ordered_index(1)
+ assert_equal 4, set.find_ordered_index(51)
+ assert_equal 5, set.find_ordered_index(50)
+ assert_equal 6, set.find_ordered_index(4)
+ assert_equal 7, set.find_ordered_index(11)
+ assert_equal 1, SequenceSet["1,*"].find_ordered_index(-1)
+ assert_equal 0, SequenceSet["*,1"].find_ordered_index(-1)
+ end
+
test "#limit" do
set = SequenceSet["1:100,500"]
assert_equal [1..99], set.limit(max: 99).ranges
@@ -344,6 +443,14 @@ def obj.to_sequence_set; 192_168.001_255 end
assert_equal "1:6,4:9", SequenceSet.new("1:6").append("4:9").string
assert_equal "1:4,5:*", SequenceSet.new("1:4").append(5..).string
assert_equal "5:*,1:4", SequenceSet.new("5:*").append(1..4).string
+ # also works from empty
+ assert_equal "5,1", SequenceSet.new.append(5).append(1).string
+ # also works when *previously* input was non-strings
+ assert_equal "*,1", SequenceSet.new(:*).append(1).string
+ assert_equal "1,5", SequenceSet.new(1).append("5").string
+ assert_equal "1:6,4:9", SequenceSet.new(1..6).append(4..9).string
+ assert_equal "1:4,5:*", SequenceSet.new(1..4).append(5..).string
+ assert_equal "5:*,1:4", SequenceSet.new(5..).append(1..4).string
end
test "#merge" do
@@ -354,6 +461,11 @@ def obj.to_sequence_set; 192_168.001_255 end
assert_equal seqset["1:3,5,7:9"], seqset["1,3,5,7:8"].merge(seqset["2,8:9"])
assert_equal seqset["1:*"], seqset["5:*"].merge(1..4)
assert_equal seqset["1:5"], seqset["1,3,5"].merge(seqset["2,4"])
+ # when merging frozen SequenceSet
+ set = SequenceSet.new
+ set.merge SequenceSet[1, 3, 5]
+ set.merge SequenceSet[2..33]
+ assert_equal seqset[1..33], set
end
test "set - other" do
@@ -670,6 +782,7 @@ def test_inspect((expected, input, freeze))
entries: [46, 6..7, 15, 1..3],
ranges: [1..3, 6..7, 15..15, 46..46],
numbers: [1, 2, 3, 6, 7, 15, 46],
+ ordered: [46, 6, 7, 15, 1, 2, 3],
to_s: "46,7:6,15,3:1",
normalize: "1:3,6:7,15,46",
count: 7,
@@ -694,9 +807,11 @@ def test_inspect((expected, input, freeze))
entries: [1..5, 3..7, 9..10, 10..11],
ranges: [1..7, 9..11],
numbers: [1, 2, 3, 4, 5, 6, 7, 9, 10, 11],
+ ordered: [1,2,3,4,5, 3,4,5,6,7, 9,10, 10,11],
to_s: "1:5,3:7,10:9,10:11",
normalize: "1:7,9:11",
count: 10,
+ count_dups: 4,
complement: "8,12:*",
}, keep: true
@@ -706,12 +821,27 @@ def test_inspect((expected, input, freeze))
entries: [1..5, 3..4, 9..11, 10],
ranges: [1..5, 9..11],
numbers: [1, 2, 3, 4, 5, 9, 10, 11],
+ ordered: [1,2,3,4,5, 3,4, 9,10,11, 10],
to_s: "1:5,3:4,9:11,10",
normalize: "1:5,9:11",
count: 8,
+ count_dups: 3,
complement: "6:8,12:*",
}, keep: true
+ data "multiple *", {
+ input: "2:*,3:*,*",
+ elements: [2..],
+ entries: [2.., 3.., :*],
+ ranges: [2..],
+ numbers: RangeError,
+ to_s: "2:*,3:*,*",
+ normalize: "2:*",
+ count: 2**32 - 2,
+ count_dups: 2**32 - 2,
+ complement: "1",
+ }, keep: true
+
data "array", {
input: ["1:5,3:4", 9..11, "10", 99, :*],
elements: [1..5, 9..11, 99, :*],
@@ -764,12 +894,22 @@ def test_inspect((expected, input, freeze))
assert_equal data[:elements], SequenceSet.new(data[:input]).elements
end
- test "#each_element" do |data|
- seqset = SequenceSet.new(data[:input])
+ def assert_seqset_enum(expected, seqset, enum)
array = []
- assert_equal seqset, seqset.each_element { array << _1 }
- assert_equal data[:elements], array
- assert_equal data[:elements], seqset.each_element.to_a
+ assert_equal seqset, seqset.send(enum) { array << _1 }
+ assert_equal expected, array
+
+ array = []
+ assert_equal seqset, seqset.send(enum).each { array << _1 }
+ assert_equal expected, array
+
+ assert_equal expected, seqset.send(enum).to_a
+ end
+
+ test "#each_element" do |data|
+ seqset = SequenceSet.new(data[:input])
+ expected = data[:elements]
+ assert_seqset_enum expected, seqset, :each_element
end
test "#entries" do |data|
@@ -777,17 +917,60 @@ def test_inspect((expected, input, freeze))
end
test "#each_entry" do |data|
- seqset = SequenceSet.new(data[:input])
- array = []
- assert_equal seqset, seqset.each_entry { array << _1 }
- assert_equal data[:entries], array
- assert_equal data[:entries], seqset.each_entry.to_a
+ seqset = SequenceSet.new(data[:input])
+ expected = data[:entries]
+ assert_seqset_enum expected, seqset, :each_entry
+ end
+
+ test "#each_range" do |data|
+ seqset = SequenceSet.new(data[:input])
+ expected = data[:ranges]
+ assert_seqset_enum expected, seqset, :each_range
end
test "#ranges" do |data|
assert_equal data[:ranges], SequenceSet.new(data[:input]).ranges
end
+ test "#each_number" do |data|
+ seqset = SequenceSet.new(data[:input])
+ expected = data[:numbers]
+ if expected.is_a?(Class) && expected < Exception
+ assert_raise expected do
+ seqset.each_number do fail "shouldn't get here" end
+ end
+ enum = seqset.each_number
+ assert_raise expected do enum.to_a end
+ assert_raise expected do enum.each do fail "shouldn't get here" end end
+ else
+ assert_seqset_enum expected, seqset, :each_number
+ end
+ end
+
+ test "#each_ordered_number" do |data|
+ seqset = SequenceSet.new(data[:input])
+ expected = data[:ordered] || data[:numbers]
+ if expected.is_a?(Class) && expected < Exception
+ assert_raise expected do
+ seqset.each_ordered_number do fail "shouldn't get here" end
+ end
+ enum = seqset.each_ordered_number
+ assert_raise expected do enum.to_a end
+ assert_raise expected do enum.each do fail "shouldn't get here" end end
+ else
+ assert_seqset_enum expected, seqset, :each_ordered_number
+ end
+ end
+
+ test "#numbers" do |data|
+ expected = data[:numbers]
+ if expected.is_a?(Class) && expected < Exception
+ assert_raise expected do SequenceSet.new(data[:input]).numbers end
+ else
+ assert_equal expected, SequenceSet.new(data[:input]).numbers
+ end
+ end
+
test "#string" do |data|
set = SequenceSet.new(data[:input])
str = data[:to_s]
@@ -833,6 +1016,25 @@ def test_inspect((expected, input, freeze))
assert_equal data[:count], SequenceSet.new(data[:input]).count
end
+ test "#count_with_duplicates" do |data|
+ dups = data[:count_dups] || 0
+ count = data[:count] + dups
+ seqset = SequenceSet.new(data[:input])
+ assert_equal count, seqset.count_with_duplicates
+ end
+
+ test "#count_duplicates" do |data|
+ dups = data[:count_dups] || 0
+ seqset = SequenceSet.new(data[:input])
+ assert_equal dups, seqset.count_duplicates
+ end
+
+ test "#has_duplicates?" do |data|
+ has_dups = !(data[:count_dups] || 0).zero?
+ seqset = SequenceSet.new(data[:input])
+ assert_equal has_dups, seqset.has_duplicates?
+ end
+
test "#valid_string" do |data|
if (expected = data[:to_s]).empty?
assert_raise DataFormatError do
@@ -849,15 +1051,6 @@ def test_inspect((expected, input, freeze))
assert_equal(data[:complement], (~set).to_s)
end
- test "#numbers" do |data|
- expected = data[:numbers]
- if expected.is_a?(Class) && expected < Exception
- assert_raise expected do SequenceSet.new(data[:input]).numbers end
- else
- assert_equal expected, SequenceSet.new(data[:input]).numbers
- end
- end
-
test "SequenceSet[input]" do |input|
case (input = data[:input])
when nil
diff --git a/test/net/imap/test_thread_member.rb b/test/net/imap/test_thread_member.rb
new file mode 100644
index 00000000..afa933fb
--- /dev/null
+++ b/test/net/imap/test_thread_member.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "net/imap"
+require "test/unit"
+
+class ThreadMemberTest < Test::Unit::TestCase
+
+ test "#to_sequence_set" do
+ # copied from the fourth example in RFC5256: (3 6 (4 23)(44 7 96))
+ thmember = Net::IMAP::ThreadMember.method :new
+ thread = thmember.(3, [
+ thmember.(6, [
+ thmember.(4, [
+ thmember.(23, [])
+ ]),
+ thmember.(44, [
+ thmember.(7, [
+ thmember.(96, [])
+ ])
+ ])
+ ])
+ ])
+ expected = Net::IMAP::SequenceSet.new("3:4,6:7,23,44,96")
+ assert_equal(expected, thread.to_sequence_set)
+ end
+
+end
diff --git a/test/net/imap/test_uidplus_data.rb b/test/net/imap/test_uidplus_data.rb
new file mode 100644
index 00000000..0088c346
--- /dev/null
+++ b/test/net/imap/test_uidplus_data.rb
@@ -0,0 +1,232 @@
+# frozen_string_literal: true
+
+require "net/imap"
+require "test/unit"
+
+class TestUIDPlusData < Test::Unit::TestCase
+
+ test "#uid_mapping with sorted source_uids" do
+ uidplus = Net::IMAP::UIDPlusData.new(
+ 1, [19, 20, *(495..500)], [*(92..97), 100, 101],
+ )
+ assert_equal(
+ {
+ 19 => 92,
+ 20 => 93,
+ 495 => 94,
+ 496 => 95,
+ 497 => 96,
+ 498 => 97,
+ 499 => 100,
+ 500 => 101,
+ },
+ uidplus.uid_mapping
+ )
+ end
+
+ test "#uid_mapping for with source_uids in unsorted order" do
+ uidplus = Net::IMAP::UIDPlusData.new(
+ 1, [*(495..500), 19, 20], [*(92..97), 100, 101],
+ )
+ assert_equal(
+ {
+ 495 => 92,
+ 496 => 93,
+ 497 => 94,
+ 498 => 95,
+ 499 => 96,
+ 500 => 97,
+ 19 => 100,
+ 20 => 101,
+ },
+ uidplus.uid_mapping
+ )
+ end
+
+end
+
+class TestAppendUIDData < Test::Unit::TestCase
+ # alias for convenience
+ AppendUIDData = Net::IMAP::AppendUIDData
+ SequenceSet = Net::IMAP::SequenceSet
+ DataFormatError = Net::IMAP::DataFormatError
+ UINT32_MAX = 2**32 - 1
+
+ test "#uidvalidity must be valid nz-number" do
+ assert_equal 1, AppendUIDData.new(1, 99).uidvalidity
+ assert_equal UINT32_MAX, AppendUIDData.new(UINT32_MAX, 1).uidvalidity
+ assert_raise DataFormatError do AppendUIDData.new(0, 1) end
+ assert_raise DataFormatError do AppendUIDData.new(2**32, 1) end
+ end
+
+ test "#assigned_uids must be a valid uid-set" do
+ assert_equal SequenceSet[1], AppendUIDData.new(99, "1").assigned_uids
+ assert_equal SequenceSet[1..9], AppendUIDData.new(1, "1:9").assigned_uids
+ assert_equal(SequenceSet[UINT32_MAX],
+ AppendUIDData.new(1, UINT32_MAX.to_s).assigned_uids)
+ assert_raise DataFormatError do AppendUIDData.new(1, 0) end
+ assert_raise DataFormatError do AppendUIDData.new(1, "*") end
+ assert_raise DataFormatError do AppendUIDData.new(1, "1:*") end
+ end
+
+ test "#size returns the number of UIDs" do
+ assert_equal(10, AppendUIDData.new(1, "1:10").size)
+ assert_equal(4_000_000_000, AppendUIDData.new(1, 1..4_000_000_000).size)
+ end
+
+ test "#assigned_uids is converted to SequenceSet" do
+ assert_equal SequenceSet[1], AppendUIDData.new(99, "1").assigned_uids
+ assert_equal SequenceSet[1..4], AppendUIDData.new(1, [1, 2, 3, 4]).assigned_uids
+ end
+
+end
+
+class TestCopyUIDData < Test::Unit::TestCase
+ # alias for convenience
+ CopyUIDData = Net::IMAP::CopyUIDData
+ SequenceSet = Net::IMAP::SequenceSet
+ DataFormatError = Net::IMAP::DataFormatError
+ UINT32_MAX = 2**32 - 1
+
+ test "#uidvalidity must be valid nz-number" do
+ assert_equal 1, CopyUIDData.new(1, 99, 99).uidvalidity
+ assert_equal UINT32_MAX, CopyUIDData.new(UINT32_MAX, 1, 1).uidvalidity
+ assert_raise DataFormatError do CopyUIDData.new(0, 1, 1) end
+ assert_raise DataFormatError do CopyUIDData.new(2**32, 1, 1) end
+ end
+
+ test "#source_uids must be valid uid-set" do
+ assert_equal SequenceSet[1], CopyUIDData.new(99, "1", 99).source_uids
+ assert_equal SequenceSet[5..8], CopyUIDData.new(1, 5..8, 1..4).source_uids
+ assert_equal(SequenceSet[UINT32_MAX],
+ CopyUIDData.new(1, UINT32_MAX.to_s, 1).source_uids)
+ assert_raise DataFormatError do CopyUIDData.new(99, nil, 99) end
+ assert_raise DataFormatError do CopyUIDData.new(1, 0, 1) end
+ assert_raise DataFormatError do CopyUIDData.new(1, "*", 1) end
+ end
+
+ test "#assigned_uids must be a valid uid-set" do
+ assert_equal SequenceSet[1], CopyUIDData.new(99, 1, "1").assigned_uids
+ assert_equal SequenceSet[1..9], CopyUIDData.new(1, 1..9, "1:9").assigned_uids
+ assert_equal(SequenceSet[UINT32_MAX],
+ CopyUIDData.new(1, 1, UINT32_MAX.to_s).assigned_uids)
+ assert_raise DataFormatError do CopyUIDData.new(1, 1, 0) end
+ assert_raise DataFormatError do CopyUIDData.new(1, 1, "*") end
+ assert_raise DataFormatError do CopyUIDData.new(1, 1, "1:*") end
+ end
+
+ test "#size returns the number of UIDs" do
+ assert_equal(10, CopyUIDData.new(1, "9,8,7,6,1:5,10", "1:10").size)
+ assert_equal(4_000_000_000,
+ CopyUIDData.new(
+ 1, "2000000000:4000000000,1:1999999999", 1..4_000_000_000
+ ).size)
+ end
+
+ test "#source_uids and #assigned_uids must be same size" do
+ assert_raise DataFormatError do CopyUIDData.new(1, 1..5, 1) end
+ assert_raise DataFormatError do CopyUIDData.new(1, 1, 1..5) end
+ end
+
+ test "#source_uids is converted to SequenceSet" do
+ assert_equal SequenceSet[1], CopyUIDData.new(99, "1", 99).source_uids
+ assert_equal SequenceSet[5, 6, 7, 8], CopyUIDData.new(1, 5..8, 1..4).source_uids
+ end
+
+ test "#assigned_uids is converted to SequenceSet" do
+ assert_equal SequenceSet[1], CopyUIDData.new(99, 1, "1").assigned_uids
+ assert_equal SequenceSet[1, 2, 3, 4], CopyUIDData.new(1, "1:4", 1..4).assigned_uids
+ end
+
+ test "#uid_mapping maps source_uids to assigned_uids" do
+ uidplus = CopyUIDData.new(9999, "20:19,500:495", "92:97,101:100")
+ assert_equal(
+ {
+ 19 => 92,
+ 20 => 93,
+ 495 => 94,
+ 496 => 95,
+ 497 => 96,
+ 498 => 97,
+ 499 => 100,
+ 500 => 101,
+ },
+ uidplus.uid_mapping
+ )
+ end
+
+ test "#uid_mapping for with source_uids in unsorted order" do
+ uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
+ assert_equal(
+ {
+ 495 => 92,
+ 496 => 93,
+ 497 => 94,
+ 498 => 95,
+ 499 => 96,
+ 500 => 97,
+ 19 => 100,
+ 20 => 101,
+ },
+ uidplus.uid_mapping
+ )
+ end
+
+ test "#assigned_uid_for(source_uid)" do
+ uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
+ assert_equal 92, uidplus.assigned_uid_for(495)
+ assert_equal 93, uidplus.assigned_uid_for(496)
+ assert_equal 94, uidplus.assigned_uid_for(497)
+ assert_equal 95, uidplus.assigned_uid_for(498)
+ assert_equal 96, uidplus.assigned_uid_for(499)
+ assert_equal 97, uidplus.assigned_uid_for(500)
+ assert_equal 100, uidplus.assigned_uid_for( 19)
+ assert_equal 101, uidplus.assigned_uid_for( 20)
+ end
+
+ test "#[](source_uid)" do
+ uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
+ assert_equal 92, uidplus[495]
+ assert_equal 93, uidplus[496]
+ assert_equal 94, uidplus[497]
+ assert_equal 95, uidplus[498]
+ assert_equal 96, uidplus[499]
+ assert_equal 97, uidplus[500]
+ assert_equal 100, uidplus[ 19]
+ assert_equal 101, uidplus[ 20]
+ end
+
+ test "#source_uid_for(assigned_uid)" do
+ uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
+ assert_equal 495, uidplus.source_uid_for( 92)
+ assert_equal 496, uidplus.source_uid_for( 93)
+ assert_equal 497, uidplus.source_uid_for( 94)
+ assert_equal 498, uidplus.source_uid_for( 95)
+ assert_equal 499, uidplus.source_uid_for( 96)
+ assert_equal 500, uidplus.source_uid_for( 97)
+ assert_equal 19, uidplus.source_uid_for(100)
+ assert_equal 20, uidplus.source_uid_for(101)
+ end
+
+ test "#each_uid_pair" do
+ uidplus = CopyUIDData.new(1, "495:500,20:19", "92:97,101:100")
+ expected = {
+ 495 => 92,
+ 496 => 93,
+ 497 => 94,
+ 498 => 95,
+ 499 => 96,
+ 500 => 97,
+ 19 => 100,
+ 20 => 101,
+ }
+ actual = {}
+ uidplus.each_uid_pair do |src, dst| actual[src] = dst end
+ assert_equal expected, actual
+ assert_equal expected, uidplus.each_uid_pair.to_h
+ assert_equal expected.to_a, uidplus.each_uid_pair.to_a
+ assert_equal expected, uidplus.each_pair.to_h
+ assert_equal expected, uidplus.each.to_h
+ end
+
+end