From 1ec68a7fb61e7b7a5a70ff0e64241c13d4952945 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sat, 3 Oct 2026 12:54:08 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=85=20Improved=20SequenceSet=20implici?= =?UTF-8?q?t=20integer=20coercion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This gives errors that are a little bit closer to ruby core and stdlib, when implicit integer arguments are expected. Unfortunately, although ruby's CAPI handles this (via macros like `NUM2LONG`), ruby doesn't expose a standard _ruby_ API to do it! In addition to raising errors that are more similar to core ruby APIs, this also handles `nil` arguments differently from unsent arguments. --- lib/net/imap/sequence_set.rb | 39 +++++++++++++----------- lib/net/imap/utils.rb | 13 ++++++++ test/lib/helper.rb | 5 +++ test/net/imap/test_sequence_set.rb | 49 ++++++++++++++++++++++++++++++ test/net/imap/test_utils.rb | 32 +++++++++++++++++++ 5 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 test/net/imap/test_utils.rb diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb index d04ddb61d..cd2aceb4d 100644 --- a/lib/net/imap/sequence_set.rb +++ b/lib/net/imap/sequence_set.rb @@ -2,6 +2,8 @@ require "set" unless defined?(::Set) +require_relative "utils" + module Net class IMAP @@ -423,6 +425,8 @@ class IMAP # representation and returns +self+. # class SequenceSet + include Utils # :nodoc: + # The largest possible non-zero unsigned 32-bit integer UINT32_MAX = 2**32 - 1 @@ -1019,9 +1023,9 @@ def none_below?(number) # is empty. (+star+ is ignored when +count+ is given.) # # Related: #min, #minmax, #slice - def max(count = nil, star: :*) - if count - count = Integer(count.to_int) + def max(count = (unset_count = true; nil), star: :*) + if !unset_count + count = implicit_int(count) raise ArgumentError, 'negative count' if count < 0 return remain_frozen_empty if count == 0 if cardinality <= count @@ -1046,9 +1050,9 @@ def max(count = nil, star: :*) # is empty. (+star+ is ignored when +count+ is given.) # # Related: #max, #minmax, #slice - def min(count = nil, star: :*) - if count - count = Integer(count.to_int) + def min(count = (unset_count = true; nil), star: :*) + if !unset_count + count = implicit_int(count) raise ArgumentError, 'negative count' if count < 0 slice(0...count) || remain_frozen_empty elsif (val = min_num) @@ -1360,7 +1364,7 @@ def delete?(element) # # Related: #delete, #delete?, #slice!, #subtract, #difference def delete_at(index) - slice! Integer(index.to_int) + slice! implicit_int index end # :call-seq: @@ -1376,9 +1380,10 @@ def delete_at(index) # #string will be regenerated after deletion. # # Related: #slice, #delete_at, #delete, #delete?, #subtract, #difference - def slice!(index, length = nil) + def slice!(index, length = (length_unset = true; nil)) modifying! # short-circuit before slice - deleted = slice(index, length) and subtract deleted + deleted = length_unset ? slice(index) : slice(index, length) + subtract deleted if deleted deleted end @@ -1720,7 +1725,7 @@ def find_ordered_index(number) # # Related: #[], #slice, #ordered_at def at(index) - export_num sorted_set_num_at Integer index.to_int + export_num sorted_set_num_at implicit_int index end # :call-seq: ordered_at(index) -> integer or nil @@ -1733,7 +1738,7 @@ def at(index) # # Related: #[], #slice, #ordered_at def ordered_at(index) - export_num ordered_list_num_at Integer index.to_int + export_num ordered_list_num_at implicit_int index end # :call-seq: @@ -1870,8 +1875,8 @@ def ordered_at(index) # +range.size+ as +length+, when that both sides of the range are either # negative or non-negative. Note that the minimum +range.size+ is zero, # so this can't return +nil+ for a negative range length. - def [](index, length = nil) - if length then slice_length(index, length) + def [](index, length = (length_unset = true; nil)) + if !length_unset then slice_length(index, length) elsif index.is_a?(Range) then slice_range(index) else at(index) end @@ -2418,16 +2423,16 @@ def reverse_each_minmax_with_index(minmaxes) end def slice_length(start, length) - start = Integer(start.to_int) - length = Integer(length.to_int) + start = implicit_int(start) + length = implicit_int(length) return nil if length.negative? stop = start + length unless start.negative? && start.abs <= length slice_range(start...stop) end def slice_range(range) - first = range.begin || 0 - rend = range.end + first = implicit_int(range.begin || 0) + rend = implicit_int(range.end) unless range.end.nil? excl = range.exclude_end? last = !(excl && rend == 0) && # (i...0) (excl && rend&.pred || rend || -1) # (i...j) vs (i..j) vs (i...) diff --git a/lib/net/imap/utils.rb b/lib/net/imap/utils.rb index c11947622..abe89f337 100644 --- a/lib/net/imap/utils.rb +++ b/lib/net/imap/utils.rb @@ -10,6 +10,19 @@ class IMAP < Protocol module Utils module_function + # Something like this exists in ruby's C API. Why not in ruby's ruby? + def implicit_int(input) + Integer.try_convert(input) or + raise TypeError, case input + when nil + "no implicit conversion from %p to Integer" % [input] + when true, false + "no implicit conversion of %p into Integer" % [input] + else + "no implicit conversion of %s into Integer" % [input.class.name] + end + end + if defined?(Ractor.shareable_proc) def shareable case obj = yield diff --git a/test/lib/helper.rb b/test/lib/helper.rb index e52a1e192..5bc940811 100644 --- a/test/lib/helper.rb +++ b/test/lib/helper.rb @@ -204,6 +204,11 @@ def assert_pattern end end + def assert_implicit_integer_type_error(&) + msg = /\Ano implicit conversion (from|of) \w+ (in)?to Integer\z/ + assert_raise_with_message(TypeError, msg, &) + end + def assert_stream_closed_error assert_local_raise(IOError, /\A(?:stream closed|closed stream)\z/) do yield diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb index 6c6490334..ef8cf9ed0 100644 --- a/test/net/imap/test_sequence_set.rb +++ b/test/net/imap/test_sequence_set.rb @@ -440,6 +440,13 @@ def obj.to_sequence_set; 192_168.001_255 end assert_nil SequenceSet["101:110,201:210,301:310"][-44] end + test "#at(invalid), argument type errors" do + assert_implicit_integer_type_error do SequenceSet.full[nil] end + assert_implicit_integer_type_error do SequenceSet.full["1"] end + assert_implicit_integer_type_error do SequenceSet.full.at(nil) end + assert_implicit_integer_type_error do SequenceSet.full.at("1") end + end + test "#ordered_at(non-negative index)" do assert_nil SequenceSet.empty.ordered_at(0) assert_equal 1, SequenceSet.full.ordered_at(0) @@ -472,6 +479,11 @@ def obj.to_sequence_set; 192_168.001_255 end assert_equal :*, SequenceSet["1:10,*"].ordered_at(10) end + test "#ordered_at(invalid), argument type errors" do + assert_implicit_integer_type_error do SequenceSet.full.ordered_at(nil) end + assert_implicit_integer_type_error do SequenceSet.full.ordered_at("1") end + end + test "#[start, length]" do assert_equal SequenceSet[10..99], SequenceSet.full[9, 90] assert_equal 90, SequenceSet.full[9, 90].count @@ -487,6 +499,16 @@ def obj.to_sequence_set; 192_168.001_255 end SequenceSet[1...200][100, 10000] end + test "#[], argument type errors" do + assert_implicit_integer_type_error do SequenceSet.full[nil] end + assert_implicit_integer_type_error do SequenceSet.full["1"] end + assert_implicit_integer_type_error do SequenceSet.full[nil, 1] end + assert_implicit_integer_type_error do SequenceSet.full["1", 1] end + assert_implicit_integer_type_error do SequenceSet.full[1, nil] end + assert_implicit_integer_type_error do SequenceSet.full[1, "1"] end + assert_implicit_integer_type_error do SequenceSet.full["1".."2"] end + end + test "#slice(start, length) -> nil, for negative length" do set = SequenceSet[1...200] assert_nil set[ 100, -1] @@ -1155,6 +1177,16 @@ def obj.to_sequence_set; 192_168.001_255 end assert_raise(ArgumentError) { set.max(-1) } end + test "#min(invalid), argument type errors" do + assert_implicit_integer_type_error do SequenceSet.full.min("1") end + assert_implicit_integer_type_error do SequenceSet.full.min(nil) end + end + + test "#max(invalid), argument type errors" do + assert_implicit_integer_type_error do SequenceSet.full.max("1") end + assert_implicit_integer_type_error do SequenceSet.full.max(nil) end + end + test "#minmax" do assert_equal [ 3, 3], SequenceSet["3"].minmax assert_equal [ :*, :*], SequenceSet["*"].minmax @@ -1217,6 +1249,17 @@ def obj.to_sequence_set; 192_168.001_255 end assert_equal SequenceSet.empty, set.slice!(3..) end + test "#slice!, argument type errors" do + set = SequenceSet.new 1..20 + assert_implicit_integer_type_error do set.slice!(nil) end + assert_implicit_integer_type_error do set.slice!("1") end + assert_implicit_integer_type_error do set.slice!(nil, 1) end + assert_implicit_integer_type_error do set.slice!("1", 1) end + assert_implicit_integer_type_error do set.slice!(1, nil) end + assert_implicit_integer_type_error do set.slice!(1, "1") end + assert_implicit_integer_type_error do set.slice!("1".."2") end + end + test "#delete_at" do set = SequenceSet.new [5..10, 20] assert_nil set.delete_at(20) @@ -1229,6 +1272,12 @@ def obj.to_sequence_set; 192_168.001_255 end assert_equal SequenceSet[5, 7..8], set end + test "#delete_at, argument type errors" do + set = SequenceSet.new 1..20 + assert_implicit_integer_type_error do set.delete_at(nil) end + assert_implicit_integer_type_error do set.delete_at("1") end + end + test "#include_star?" do assert SequenceSet["2,*:12"].include_star? assert SequenceSet[-1].include_star? diff --git a/test/net/imap/test_utils.rb b/test/net/imap/test_utils.rb new file mode 100644 index 000000000..b23aa5e8c --- /dev/null +++ b/test/net/imap/test_utils.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "net/imap/utils" +require "test/unit" + +class UtilsTest < Net::IMAP::TestCase + include Net::IMAP::Utils + + test "#implicit_int" do + assert_equal 0, implicit_int(0) + assert_equal(-123, implicit_int(-123)) + assert_equal 4567, implicit_int(4567) + assert_equal 1, implicit_int(1.23) + + int = Object.new + def int.to_int = 890 + assert_equal 890, implicit_int(890) + + assert_implicit_integer_type_error do implicit_int(nil) end + assert_implicit_integer_type_error do implicit_int(true) end + assert_implicit_integer_type_error do implicit_int(false) end + assert_implicit_integer_type_error do implicit_int("123") end + assert_implicit_integer_type_error do implicit_int(:sym) end + assert_implicit_integer_type_error do implicit_int(Class) end + assert_implicit_integer_type_error do implicit_int(Object.new) end + + broken = Object.new + def broken.to_int = :wat + assert_raise(TypeError) do implicit_int(broken) end + end + +end