dnsruby 1.73.1 → 2.0.0

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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +314 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/dnsruby.gemspec +6 -4
  10. data/lib/dnsruby/cache.rb +279 -49
  11. data/lib/dnsruby/code_mapper.rb +35 -8
  12. data/lib/dnsruby/code_mappers.rb +63 -49
  13. data/lib/dnsruby/config.rb +77 -4
  14. data/lib/dnsruby/deadline_queue.rb +75 -0
  15. data/lib/dnsruby/dnssec.rb +148 -45
  16. data/lib/dnsruby/ipv6.rb +45 -5
  17. data/lib/dnsruby/key_cache.rb +202 -35
  18. data/lib/dnsruby/message/decoder.rb +85 -19
  19. data/lib/dnsruby/message/encoder.rb +27 -16
  20. data/lib/dnsruby/message/header.rb +8 -12
  21. data/lib/dnsruby/message/message.rb +53 -22
  22. data/lib/dnsruby/message/section.rb +12 -34
  23. data/lib/dnsruby/name.rb +5 -2
  24. data/lib/dnsruby/packet_sender.rb +664 -207
  25. data/lib/dnsruby/presentation.rb +147 -0
  26. data/lib/dnsruby/query_control.rb +132 -0
  27. data/lib/dnsruby/recursor.rb +798 -341
  28. data/lib/dnsruby/resolver.rb +472 -176
  29. data/lib/dnsruby/resource/CAA.rb +67 -16
  30. data/lib/dnsruby/resource/DLV.rb +6 -0
  31. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  32. data/lib/dnsruby/resource/DS.rb +27 -28
  33. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  34. data/lib/dnsruby/resource/IN.rb +21 -2
  35. data/lib/dnsruby/resource/MX.rb +2 -2
  36. data/lib/dnsruby/resource/NSEC.rb +6 -10
  37. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  38. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  39. data/lib/dnsruby/resource/OPT.rb +2 -3
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +986 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +53 -2
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +34 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +127 -14
  167. data/.coveralls.yml +0 -2
@@ -58,12 +58,19 @@ module Dnsruby
58
58
  in_string = false
59
59
  count = -1
60
60
  strings = []
61
- current_binary = ""
61
+ input_encoding = input.encoding
62
+ finish_strings = lambda do
63
+ strings.each do |string|
64
+ string.force_encoding(input_encoding)
65
+ string.force_encoding(Encoding::BINARY) unless string.valid_encoding?
66
+ end
67
+ end
68
+ current_binary = +""
62
69
  current_quote_char = '"'
63
70
  unquoted = false
64
71
  seen_strings = false
65
72
  pos = 0
66
- input = input.sub(/^\s*\(\s*/, "")
73
+ input = input.b.sub(/^\s*\(\s*/, "")
67
74
  input.sub!(/\s*\)\s*$/, "")
68
75
  input.each_char {|c|
69
76
  if (((c == "'") || (c == '"')) && (!in_escaped) && (!unquoted))
@@ -72,24 +79,24 @@ module Dnsruby
72
79
  current_quote_char = c
73
80
  in_string = true
74
81
  count+=1
75
- strings[count] = ""
82
+ strings[count] = "".b
76
83
  else
77
84
  if (c == current_quote_char)
78
85
  in_string = false
79
86
  else
80
- strings[count]+=c
87
+ strings[count] << c
81
88
  end
82
89
  end
83
90
  else
84
91
  if (seen_strings && !in_string)
85
92
  if (c == ";")
86
93
  # Comment in zone file!
87
- return strings
94
+ return finish_strings.call
88
95
  end
89
96
  if (c != " " && c != "\t")
90
97
  in_string = true
91
98
  count+=1
92
- strings[count] = ""
99
+ strings[count] = "".b
93
100
  else
94
101
  next
95
102
  end
@@ -97,11 +104,11 @@ module Dnsruby
97
104
  if (pos == 0)
98
105
  unquoted = true
99
106
  count+=1
100
- strings[count] = ""
107
+ strings[count] = "".b
101
108
  elsif (unquoted)
102
109
  if (c == " ")
103
110
  count+=1
104
- strings[count] = ""
111
+ strings[count] = "".b
105
112
  pos += 1
106
113
  next
107
114
  end
@@ -110,7 +117,7 @@ module Dnsruby
110
117
  if (c == "\\")
111
118
  if (in_escaped)
112
119
  in_escaped = false
113
- strings[count]+=(c)
120
+ strings[count] << c
114
121
  else
115
122
  in_escaped = true
116
123
  end
@@ -118,63 +125,48 @@ module Dnsruby
118
125
  if (in_escaped)
119
126
  # Build up the binary
120
127
  if (c == ";") || (c == '"')
121
- strings[count]+=c
128
+ strings[count] << c
122
129
  in_escaped = false
123
130
  elsif (ESCAPE_CHARS[c])
124
131
  in_escaped=false
125
- strings[count]+=ESCAPE_CHARS[c].chr
132
+ strings[count] << ESCAPE_CHARS[c].chr
126
133
  elsif (c<"0" || c>"9")
127
134
  in_escaped = false
128
- strings[count]+=c
135
+ strings[count] << c
129
136
  else
130
137
  # Must be building up three digit string to identify binary value?
131
138
  # if (c >= "0" && c <= "9")
132
- current_binary += c
139
+ current_binary << c
133
140
  # end
134
141
  if ((current_binary.length == 3) ) # || (c < "0" || c > "9"))
135
- strings[count]+=current_binary.to_i.chr
142
+ strings[count] << current_binary.to_i.chr
136
143
  in_escaped = false
137
- current_binary = ""
144
+ current_binary = +""
138
145
  end
139
146
  end
140
147
  else
141
- strings[count]+=(c)
148
+ strings[count] << c
142
149
  end
143
150
  end
144
151
  end
145
152
  pos += 1
146
153
  }
147
- return strings
154
+ finish_strings.call
148
155
  end
149
156
 
150
157
  def TXT.display(str, do_escapes = true)
151
- output = ""
152
- # Probably need to scan through each string manually
153
- # Make sure to remember to escape binary characters.
154
- # Go through copying to output, and adding "\" characters as necessary?
155
- str.each_byte {|c|
156
- if (c == 34) || (c == 92) # || (c == 59)
157
- if (do_escapes)
158
- output+='\\'
159
- end
160
- output+=c.chr
161
- elsif (c < 32) # c is binary
162
- if (ESCAPE_CODES[c])
163
- output += c.chr
164
- else
165
- output+= '\\'
166
- num = c.to_i.to_s
167
- (3-num.length).times {|i|
168
- num="0"+num
169
- }
170
- output+= num # Need a 3 digit number here.
171
- end
172
-
158
+ output = "".b
159
+ str.each_byte do |octet|
160
+ if octet == 34 || octet == 92
161
+ output << 92 if do_escapes
162
+ output << octet
163
+ elsif octet < 32 && !ESCAPE_CODES.key?(octet)
164
+ output << format('\\%03d', octet)
173
165
  else
174
- output += c.chr
166
+ output << octet
175
167
  end
176
- }
177
- return output
168
+ end
169
+ output
178
170
  end
179
171
 
180
172
  def rdata_to_string
@@ -44,7 +44,7 @@ module Dnsruby
44
44
 
45
45
  def encode_rdata(msg, canonical=false) #:nodoc: all
46
46
  if !([Classes::NONE, Classes::ANY].include? klass) || @rdata.length > 0
47
- msg.put_name(@domainname, canonical)
47
+ msg.put_name(@domainname, canonical || type == Types::DNAME, canonical)
48
48
  end
49
49
  end
50
50
 
@@ -13,6 +13,8 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  # ++
16
+ require 'dnsruby/resource/generic_cache'
17
+
16
18
  module Dnsruby
17
19
  class RR
18
20
  # Class to store generic RRs (RFC 3597)
@@ -44,6 +46,9 @@ module Dnsruby
44
46
  end
45
47
 
46
48
  def self.create(type_value, class_value) #:nodoc:
49
+ if GenericCache.automatic?(type_value, class_value)
50
+ return GenericCache.create(type_value, class_value)
51
+ end
47
52
  c = Class.new(Generic)
48
53
  # c.type = type_value
49
54
  # c.klass = class_value
@@ -53,6 +58,29 @@ module Dnsruby
53
58
  ClassHash[[type_value, class_value]] = c
54
59
  return c
55
60
  end
61
+
62
+ def self.name
63
+ @generic_cache_entry&.klass.equal?(self) ? @generic_display_name : super
64
+ end
65
+
66
+ def self.to_s
67
+ @generic_cache_entry&.klass.equal?(self) ? @generic_display_name.dup : super
68
+ end
69
+
70
+ def self.inspect
71
+ @generic_cache_entry&.klass.equal?(self) ? @generic_display_name.dup : super
72
+ end
73
+
74
+ def self.const_missing(name)
75
+ # A subclass lookup with inherit=false must still reject a native
76
+ # ancestor binding. Only virtual generated names need this fallback.
77
+ return super if !equal?(Generic) && GenericConstantInspection::NATIVE_DEFINED.bind_call(self, name)
78
+ found = GenericCache.named(name.to_s)
79
+ return found if found
80
+ pair = /\AType(\d+)_Class(\d+)\z/.match(name.to_s)
81
+ return RR.find_class(*pair.captures.map(&:to_i)) if pair
82
+ super
83
+ end
56
84
  end
57
85
 
58
86
  # --
@@ -169,3 +197,6 @@ require 'dnsruby/resource/DHCID'
169
197
  require 'dnsruby/resource/GPOS'
170
198
  require 'dnsruby/resource/NXT'
171
199
  require 'dnsruby/resource/CAA'
200
+ require 'dnsruby/resource/SVCB'
201
+ require 'dnsruby/resource/HTTPS'
202
+ require 'dnsruby/resource/generic_constants'
@@ -0,0 +1,384 @@
1
+ require 'thread'
2
+ require 'weakref'
3
+
4
+ module Dnsruby
5
+ class RR
6
+ # Only automatically generated integer pairs belong to this cache. Explicit
7
+ # registrations and the public Generic.create factory retain their lifetime.
8
+ module GenericCache # :nodoc: all
9
+ LIMIT = 256
10
+ READ = Hash.instance_method(:fetch)
11
+ WRITE = Hash.instance_method(:[]=)
12
+ DELETE = Hash.instance_method(:delete)
13
+ KEYS = Hash.instance_method(:keys)
14
+ HAS_KEY = Hash.instance_method(:key?)
15
+ REMOVE_CONSTANT = Module.instance_method(:remove_const)
16
+ Entry = Struct.new(:name, :pair, :klass, :registry_key, :version, :automatic, :index)
17
+ NameIndex = Struct.new(:current)
18
+ # The FIFO and its ClassHash entries retain automatic classes. The
19
+ # name index is weak and also resolves caller-held classes after explicit
20
+ # registry deletion. Entry#automatic distinguishes reusable entries from
21
+ # those retained only for constant lookup.
22
+ # Each automatic class owns its Entry and the name's stable index cell.
23
+ # The cell refers weakly to the latest Entry, so an older caller-held class
24
+ # does not retain its replacement. Updating the cell avoids replacing a
25
+ # WeakMap value (unsafe on Ruby 3.2) or rebuilding maps (whose finalizers
26
+ # can retain every previous map on older Ruby versions).
27
+ # Entry versions prevent older mutation cleanup discarding a publication.
28
+ @lock = Mutex.new
29
+ @names = ObjectSpace::WeakMap.new
30
+ # Ruby 3.2 WeakMap enumeration can remove a dead forward entry without
31
+ # clearing its inverse reference. A later finalizer can then remove an
32
+ # unrelated entry whose key reused that address. Enumerate self-mapped
33
+ # index cells instead: each inverse reference has the same lifetime as
34
+ # its key, and @names is used only for lookup and publication.
35
+ @indices = ObjectSpace::WeakMap.new
36
+ @fifo = {}.compare_by_identity
37
+ @version = 0
38
+
39
+ class << self
40
+ def eligible?(type, klass)
41
+ type.instance_of?(Integer) && klass.instance_of?(Integer) && type >= 0 && klass >= 0
42
+ end
43
+
44
+ def key(type, klass)
45
+ # WeakMap uses object identity. Deduplicated strings give equal pairs
46
+ # the same collectible key, which the live Class owns.
47
+ -("Type#{type}_Class#{klass}".encode(Encoding::UTF_8))
48
+ end
49
+
50
+ def named(name)
51
+ return nil unless name.ascii_only?
52
+ @lock.synchronize { indexed_entry(-name.encode(Encoding::UTF_8))&.klass }
53
+ end
54
+
55
+ def find(type, klass)
56
+ return nil unless eligible?(type, klass)
57
+ pair = [type, klass]
58
+ @lock.synchronize do
59
+ registered = READ.bind_call(ClassHash, pair, nil)
60
+ return registered if registered
61
+ found = automatic_entry(key(type, klass))
62
+ retain(found) if reusable?(found)
63
+ end
64
+ end
65
+
66
+ def peek(type, klass)
67
+ return nil unless eligible?(type, klass)
68
+ @lock.synchronize { automatic_entry(key(type, klass))&.klass }
69
+ end
70
+
71
+ def factory(type, klass)
72
+ previous = Thread.current[:dnsruby_generic_factory]
73
+ Thread.current[:dnsruby_generic_factory] = eligible?(type, klass) ? [type, klass] : nil
74
+ yield
75
+ ensure
76
+ Thread.current[:dnsruby_generic_factory] = previous
77
+ end
78
+
79
+ def automatic?(type, klass)
80
+ return false unless eligible?(type, klass) && Thread.current[:dnsruby_generic_factory] == [type, klass]
81
+ Thread.current[:dnsruby_generic_factory] = nil
82
+ true
83
+ end
84
+
85
+ def create(type, klass)
86
+ # Class.new invokes the application's inherited hook. Never hold the
87
+ # registry lock while running it, or while setting up the new Class.
88
+ generated = Class.new(Generic)
89
+ generated.const_set(:TypeValue, type)
90
+ generated.const_set(:ClassValue, klass)
91
+ name = key(type, klass)
92
+ generated.instance_variable_set(:@generic_display_name, "Dnsruby::RR::Generic::#{name}".freeze)
93
+ entry = Entry.new(name, [type, klass].freeze, generated, nil, nil, true)
94
+ # WeakMap's values are weak too. The class keeps its mutable bookkeeping
95
+ # alive even when a client freezes the class itself.
96
+ generated.instance_variable_set(:@generic_cache_entry, entry)
97
+ @lock.synchronize do
98
+ registered = READ.bind_call(ClassHash, entry.pair, nil)
99
+ return registered if registered
100
+ found = automatic_entry(name)
101
+ return retain(found) if reusable?(found)
102
+ # A previous explicit factory call may have left a native constant
103
+ # after its registry entry was deleted. Let lookup (including YAML)
104
+ # reach the replacement class instead of that stale binding.
105
+ if GenericConstantInspection::NATIVE_DEFINED.bind_call(Generic, name, false)
106
+ REMOVE_CONSTANT.bind_call(Generic, name)
107
+ end
108
+ index = @names[name]
109
+ entry.index = index || NameIndex.new
110
+ entry.index.current = WeakRef.new(entry)
111
+ unless index
112
+ @names[name] = entry.index
113
+ @indices[entry.index] = entry.index
114
+ end
115
+ retain(entry)
116
+ end
117
+ end
118
+
119
+ # Custom Hash keys can address a different pair from their Array
120
+ # contents, or need not be Arrays at all. Resolve them with Ruby's own
121
+ # stored-key comparison, outside the cache lock (hash/eql? may call back).
122
+ def registry_pair(pair)
123
+ return pair if ClassHash.compare_by_identity?
124
+ native_method = Object.instance_method(:method)
125
+ if Array === pair && native_method.bind_call(pair, :hash).owner == Array && native_method.bind_call(pair, :eql?).owner == Array
126
+ return pair
127
+ end
128
+ copy = Hash.instance_method(:dup).bind_call(ClassHash)
129
+ keys = KEYS.bind_call(copy)
130
+ index = 0
131
+ Hash.instance_method(:transform_values!).bind_call(copy) do |_|
132
+ stored = keys[index]
133
+ index += 1
134
+ stored
135
+ end
136
+ missing = Object.new
137
+ stored = READ.bind_call(copy, pair, missing)
138
+ return stored unless stored.equal?(missing)
139
+ # A live entry may already have left the FIFO. An explicit deletion
140
+ # still invalidates that entry when the custom key addresses its pair.
141
+ entries = @lock.synchronize { automatic_entries }
142
+ weak = entries.to_h { |entry| [entry.pair, entry.pair] }
143
+ READ.bind_call(weak, pair, pair)
144
+ end
145
+
146
+ # Native Hash mutations run outside the lock. Their cleanup must not
147
+ # remove a newer publication by a concurrent or reentrant lookup.
148
+ def snapshot(pair = nil)
149
+ name = pair_name(pair) if pair
150
+ return {} if pair && !name
151
+ @lock.synchronize do
152
+ entries = if pair && ClassHash.compare_by_identity?
153
+ @fifo.keys.select { |entry| entry.registry_key.equal?(pair) }
154
+ elsif pair
155
+ [automatic_entry(name)].compact
156
+ else
157
+ automatic_entries + @fifo.keys
158
+ end
159
+ entries.each_with_object({}.compare_by_identity) { |entry, saved| saved[entry] = entry.version }
160
+ end
161
+ end
162
+
163
+ def forget(snapshot, pairs = nil, removed_only: false)
164
+ names = pairs&.filter_map { |pair| pair_name(pair) }
165
+ @lock.synchronize do
166
+ entries = snapshot.keys
167
+ entries.select! { |entry| names.include?(entry.name) } if names
168
+ entries += names ? names.filter_map { |name| automatic_entry(name) } : @fifo.keys
169
+ entries.each do |entry|
170
+ next if pairs && !pairs.any? { |pair| targets?(entry, pair) }
171
+ unchanged = snapshot.key?(entry) && snapshot[entry] == entry.version
172
+ unchanged &&= !READ.bind_call(ClassHash, registry_key(entry), nil).equal?(entry.klass) if removed_only
173
+ discard(entry) if unchanged || stale?(entry)
174
+ end
175
+ end
176
+ end
177
+
178
+ def register(snapshot, pair, value)
179
+ name = pair_name(pair)
180
+ return unless name
181
+ @lock.synchronize do
182
+ identity = ClassHash.compare_by_identity?
183
+ previous = snapshot.keys.find do |entry|
184
+ entry.klass.equal?(value) && targets?(entry, pair) && (identity ? entry.registry_key.equal?(pair) : entry.name == name)
185
+ end
186
+ # FIFO eviction may have run after the explicit write but before its
187
+ # cleanup. Preserve a successfully assigned automatic class.
188
+ if previous && previous.automatic && !@fifo.key?(previous) && !moved?(previous) && !READ.bind_call(ClassHash, registry_key(previous), nil)
189
+ actual = identity ? pair : previous.pair.dup
190
+ WRITE.bind_call(ClassHash, actual, value)
191
+ end
192
+ entries = identity ? @fifo.keys.select { |entry| entry.registry_key.equal?(pair) } : [automatic_entry(name)].compact
193
+ entries.each do |entry|
194
+ next unless targets?(entry, pair)
195
+ discard(entry) if READ.bind_call(ClassHash, registry_key(entry), nil).equal?(value)
196
+ end
197
+ end
198
+ end
199
+
200
+ def rehash
201
+ @lock.synchronize do
202
+ (automatic_entries + @fifo.keys).each { |entry| discard(entry) if moved?(entry) || stale?(entry) }
203
+ end
204
+ end
205
+
206
+ private
207
+
208
+ def automatic_entry(name)
209
+ entry = indexed_entry(name)
210
+ entry if entry&.automatic
211
+ end
212
+
213
+ def automatic_entries
214
+ @indices.keys.filter_map { |index| dereference(index) }.select(&:automatic)
215
+ end
216
+
217
+ def indexed_entry(name)
218
+ dereference(@names[name])
219
+ end
220
+
221
+ def dereference(index)
222
+ index&.current&.__getobj__
223
+ rescue WeakRef::RefError
224
+ nil
225
+ end
226
+
227
+ def pair_name(pair)
228
+ return unless Array === pair
229
+ # A native slice strips singleton methods, unlike Array#to_a.
230
+ pair = Array.instance_method(:[]).bind_call(pair, 0, 3)
231
+ key(*pair) if pair.length == 2 && eligible?(*pair)
232
+ end
233
+
234
+ def targets?(entry, pair)
235
+ return entry.registry_key.equal?(pair) if ClassHash.compare_by_identity?
236
+ return false unless pair_name(pair) == entry.name
237
+ keys = KEYS.bind_call(ClassHash)
238
+ pair.equal?(entry.registry_key) || !keys.any? { |item| item.equal?(pair) } || !keys.any? { |item| item.equal?(entry.registry_key) }
239
+ end
240
+
241
+ def moved?(entry)
242
+ entry.registry_key && !ClassHash.compare_by_identity? && !entry.pair.eql?(entry.registry_key)
243
+ end
244
+
245
+ def reusable?(entry)
246
+ return false unless entry
247
+ discard(entry) if moved?(entry)
248
+ !moved?(entry) && !ClassHash.compare_by_identity?
249
+ end
250
+
251
+ def registry_key(entry)
252
+ ClassHash.compare_by_identity? ? entry.registry_key : entry.pair
253
+ end
254
+
255
+ def stale?(entry)
256
+ @fifo.key?(entry) && !READ.bind_call(ClassHash, registry_key(entry), nil).equal?(entry.klass)
257
+ end
258
+
259
+ def discard(entry)
260
+ entry.automatic = false
261
+ @fifo.delete(entry)
262
+ end
263
+
264
+ def retain(entry)
265
+ pair = entry.pair
266
+ inserted = pair.dup
267
+ actual = inserted
268
+ unless ClassHash.compare_by_identity?
269
+ registered = READ.bind_call(ClassHash, pair, nil)
270
+ return registered if registered
271
+ if HAS_KEY.bind_call(ClassHash, pair)
272
+ actual = KEYS.bind_call(ClassHash).find { |item| pair.eql?(item) } || actual
273
+ end
274
+ end
275
+ WRITE.bind_call(ClassHash, inserted, entry.klass)
276
+ entry.registry_key = actual
277
+ entry.version = (@version += 1)
278
+ @fifo[entry] = true
279
+ while @fifo.length > LIMIT
280
+ old, = @fifo.shift
281
+ if moved?(old)
282
+ discard(old)
283
+ elsif READ.bind_call(ClassHash, registry_key(old), nil).equal?(old.klass)
284
+ DELETE.bind_call(ClassHash, registry_key(old))
285
+ end
286
+ end
287
+ entry.klass
288
+ end
289
+ end
290
+ end
291
+
292
+ # User edits to the public registry invalidate automatic lookup entries.
293
+ # Native Hash operations above distinguish internal eviction from deletion.
294
+ module GenericRegistryChanges # :nodoc: all
295
+ def []=(key, value)
296
+ return super unless equal?(Dnsruby::ClassHash) && !frozen?
297
+ pair = GenericCache.registry_pair(key)
298
+ before = GenericCache.snapshot(pair)
299
+ result = super
300
+ GenericCache.register(before, pair, value)
301
+ result
302
+ end
303
+ alias store []=
304
+
305
+ def delete(key, &block)
306
+ return super unless equal?(Dnsruby::ClassHash) && !frozen?
307
+ pair = GenericCache.registry_pair(key)
308
+ before = GenericCache.snapshot(pair)
309
+ result = super
310
+ GenericCache.forget(before, [pair], removed_only: true)
311
+ result
312
+ end
313
+
314
+ def clear
315
+ return super unless equal?(Dnsruby::ClassHash) && !frozen?
316
+ before = GenericCache.snapshot
317
+ result = super
318
+ GenericCache.forget(before)
319
+ GenericCache.rehash
320
+ result
321
+ end
322
+
323
+ def replace(other)
324
+ return super unless equal?(Dnsruby::ClassHash) && !frozen?
325
+ # Native conversion can call back into the factory. Snapshot afterwards
326
+ # so entries returned by to_hash become explicit registrations too.
327
+ replacement = {}.replace(other)
328
+ before = GenericCache.snapshot
329
+ result = super(replacement)
330
+ replacement.each_pair do |key, value|
331
+ GenericCache.register(before, GenericCache.registry_pair(key), value)
332
+ end
333
+ GenericCache.forget(before)
334
+ GenericCache.rehash
335
+ result
336
+ end
337
+
338
+ def rehash
339
+ result = super
340
+ GenericCache.rehash if equal?(Dnsruby::ClassHash)
341
+ result
342
+ end
343
+
344
+ [:merge!, :update].each do |operation|
345
+ define_method(operation) do |*args, &block|
346
+ return super(*args, &block) unless equal?(Dnsruby::ClassHash) && !frozen?
347
+ before = keys
348
+ snapshot = GenericCache.snapshot
349
+ touched = []
350
+ begin
351
+ super(*args) do |key, old, value|
352
+ value = block.call(key, old, value) if block
353
+ touched << [key, value]
354
+ value
355
+ end
356
+ ensure
357
+ touched.each { |key, value| GenericCache.register(snapshot, key, value) }
358
+ GenericCache.forget(snapshot, (keys - before) | touched.map(&:first))
359
+ GenericCache.rehash
360
+ end
361
+ end
362
+ end
363
+
364
+ [:delete_if, :reject!, :keep_if, :select!, :filter!, :shift,
365
+ :transform_keys!, :transform_values!, :compact!].each do |operation|
366
+ define_method(operation) do |*args, &block|
367
+ return super(*args, &block) unless equal?(Dnsruby::ClassHash) && !frozen?
368
+ before = dup
369
+ snapshot = GenericCache.snapshot
370
+ begin
371
+ super(*args, &block)
372
+ ensure
373
+ changed = (before.keys | keys).reject do |key|
374
+ before.key?(key) && key?(key) && before[key].equal?(self[key])
375
+ end
376
+ GenericCache.forget(snapshot, changed)
377
+ GenericCache.rehash
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
383
+ ClassHash.extend(RR::GenericRegistryChanges)
384
+ end
@@ -0,0 +1,79 @@
1
+ module Dnsruby
2
+ class RR
3
+ module GenericConstantInspection
4
+ NATIVE_DEFINED = Module.instance_method(:const_defined?)
5
+ NATIVE_GET = Module.instance_method(:const_get)
6
+ NATIVE_ANCESTORS = Module.instance_method(:ancestors)
7
+ EMPTY = Module.new
8
+
9
+ def self.native_defined?(receiver, path, inherit, nested = false)
10
+ if nested
11
+ first = path.split('::', -1).first
12
+ NATIVE_DEFINED.bind_call(EMPTY, first, false) # Validate; never reinterpret :: as an absolute suffix.
13
+ ancestors = inherit ? NATIVE_ANCESTORS.bind_call(receiver) : [receiver]
14
+ ancestors = ancestors.take_while { |ancestor| !ancestor.equal?(Object) } unless receiver.equal?(Object)
15
+ return false unless ancestors.any? { |ancestor| NATIVE_DEFINED.bind_call(ancestor, first, false) }
16
+ end
17
+ NATIVE_DEFINED.bind_call(receiver, path, inherit)
18
+ end
19
+
20
+ def self.nested_defined?(receiver, path, inherit)
21
+ native_defined?(receiver, path, inherit, true) || virtual_defined?(receiver, path, inherit, true)
22
+ end
23
+
24
+ def self.virtual_defined?(receiver, path, inherit, nested = false)
25
+ parts = path.split('::', -1)
26
+ parts.each_index do |index|
27
+ prefix = parts[0..index].join('::')
28
+ next if prefix.empty? && index.zero?
29
+ next if native_defined?(receiver, prefix, inherit, nested)
30
+ owner = if index.zero?
31
+ receiver
32
+ elsif index == 1 && parts.first.empty?
33
+ Object
34
+ else
35
+ NATIVE_GET.bind_call(receiver, parts[0...index].join('::'), inherit)
36
+ end
37
+ generic = Dnsruby::RR::Generic
38
+ return false unless owner.equal?(generic) || (inherit && owner.is_a?(Class) && owner < generic)
39
+ local = parts[index]
40
+ found = GenericCache.named(local)
41
+ numeric = /\AType(\d+)_Class(\d+)\z/.match(local)
42
+ return false unless found || numeric
43
+ return true if index == parts.length - 1
44
+ suffix = parts[(index + 1)..].join('::')
45
+ if found
46
+ return nested_defined?(found, suffix, inherit)
47
+ end
48
+ type, klass = numeric.captures.map(&:to_i)
49
+ registered = Dnsruby::ClassHash[[type, klass]] || GenericCache.peek(type, klass)
50
+ if registered
51
+ raise TypeError, "#{path} does not refer to class/module" unless registered.is_a?(Module)
52
+ return nested_defined?(registered, suffix, inherit)
53
+ end
54
+ NATIVE_DEFINED.bind_call(EMPTY, suffix, false) # Preserve delimiter validation before a projected value.
55
+ # These are the only constants every newly reconstructed Class owns.
56
+ if ['TypeValue', 'ClassValue'].include?(parts[index + 1])
57
+ return true if index + 2 == parts.length
58
+ raise TypeError, "#{path} does not refer to class/module"
59
+ end
60
+ ancestor = Dnsruby::RR::ClassInsensitiveTypes[type] || generic
61
+ return nested_defined?(inherit ? ancestor : EMPTY, suffix, inherit)
62
+ end
63
+ false
64
+ end
65
+
66
+ def const_defined?(name, inherit = true)
67
+ unless name.is_a?(String) || name.is_a?(Symbol)
68
+ original = name
69
+ name = String.try_convert(name)
70
+ raise TypeError, "no implicit conversion of #{original.class} into String" unless name
71
+ end
72
+ return true if super(name, inherit)
73
+ path = name.is_a?(Symbol) ? name.to_s : String.new(name)
74
+ GenericConstantInspection.virtual_defined?(self, path, inherit)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ Dnsruby::RR::Generic.singleton_class.prepend(Dnsruby::RR::GenericConstantInspection)