dnsruby 1.74.0 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +302 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/demo/rubydig.rb +0 -2
- data/dnsruby.gemspec +5 -4
- data/lib/dnsruby/cache.rb +279 -47
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +75 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +645 -203
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +795 -339
- data/lib/dnsruby/resolver.rb +468 -186
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +19 -0
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +979 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +42 -20
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +21 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +105 -10
- data/.coveralls.yml +0 -2
data/lib/dnsruby/resource/TXT.rb
CHANGED
|
@@ -58,12 +58,19 @@ module Dnsruby
|
|
|
58
58
|
in_string = false
|
|
59
59
|
count = -1
|
|
60
60
|
strings = []
|
|
61
|
-
|
|
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]
|
|
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
|
|
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]
|
|
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]
|
|
128
|
+
strings[count] << c
|
|
122
129
|
in_escaped = false
|
|
123
130
|
elsif (ESCAPE_CHARS[c])
|
|
124
131
|
in_escaped=false
|
|
125
|
-
strings[count]
|
|
132
|
+
strings[count] << ESCAPE_CHARS[c].chr
|
|
126
133
|
elsif (c<"0" || c>"9")
|
|
127
134
|
in_escaped = false
|
|
128
|
-
strings[count]
|
|
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
|
|
139
|
+
current_binary << c
|
|
133
140
|
# end
|
|
134
141
|
if ((current_binary.length == 3) ) # || (c < "0" || c > "9"))
|
|
135
|
-
strings[count]
|
|
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]
|
|
148
|
+
strings[count] << c
|
|
142
149
|
end
|
|
143
150
|
end
|
|
144
151
|
end
|
|
145
152
|
pos += 1
|
|
146
153
|
}
|
|
147
|
-
|
|
154
|
+
finish_strings.call
|
|
148
155
|
end
|
|
149
156
|
|
|
150
157
|
def TXT.display(str, do_escapes = true)
|
|
151
|
-
output = ""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
166
|
+
output << octet
|
|
175
167
|
end
|
|
176
|
-
|
|
177
|
-
|
|
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)
|