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.
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 +302 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/demo/rubydig.rb +0 -2
  10. data/dnsruby.gemspec +5 -4
  11. data/lib/dnsruby/cache.rb +279 -47
  12. data/lib/dnsruby/code_mapper.rb +35 -8
  13. data/lib/dnsruby/code_mappers.rb +63 -49
  14. data/lib/dnsruby/config.rb +75 -4
  15. data/lib/dnsruby/deadline_queue.rb +75 -0
  16. data/lib/dnsruby/dnssec.rb +148 -45
  17. data/lib/dnsruby/ipv6.rb +45 -5
  18. data/lib/dnsruby/key_cache.rb +202 -35
  19. data/lib/dnsruby/message/decoder.rb +85 -19
  20. data/lib/dnsruby/message/encoder.rb +27 -16
  21. data/lib/dnsruby/message/header.rb +8 -12
  22. data/lib/dnsruby/message/message.rb +53 -22
  23. data/lib/dnsruby/message/section.rb +12 -34
  24. data/lib/dnsruby/name.rb +5 -2
  25. data/lib/dnsruby/packet_sender.rb +645 -203
  26. data/lib/dnsruby/presentation.rb +147 -0
  27. data/lib/dnsruby/query_control.rb +132 -0
  28. data/lib/dnsruby/recursor.rb +795 -339
  29. data/lib/dnsruby/resolver.rb +468 -186
  30. data/lib/dnsruby/resource/CAA.rb +67 -16
  31. data/lib/dnsruby/resource/DLV.rb +6 -0
  32. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  33. data/lib/dnsruby/resource/DS.rb +27 -28
  34. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  35. data/lib/dnsruby/resource/IN.rb +19 -0
  36. data/lib/dnsruby/resource/MX.rb +2 -2
  37. data/lib/dnsruby/resource/NSEC.rb +6 -10
  38. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  39. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  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 +979 -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 +42 -20
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +21 -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 +105 -10
  167. data/.coveralls.yml +0 -2
@@ -14,6 +14,7 @@
14
14
  #
15
15
 
16
16
  require 'dnsruby/code_mappers'
17
+ require 'dnsruby/presentation'
17
18
 
18
19
  module Dnsruby
19
20
  class RR
@@ -31,7 +32,7 @@ class RR
31
32
  end
32
33
 
33
34
  # A regular expression which catches any valid resource record.
34
- @@RR_REGEX = Regexp.new("^\\s*(\\S+)\\s*(\\d+)?\\s*(#{Classes.regexp +
35
+ @@RR_REGEX = Regexp.new("^\\s*((?:\\\\.|\\S)+)\\s*(\\d+)?\\s*(#{Classes.regexp +
35
36
  "|CLASS\\d+"})?\\s*(#{Types.regexp + '|TYPE\\d+'})?\\s*([\\s\\S]*)\$") #:nodoc: all
36
37
 
37
38
  @@implemented_rr_map = nil
@@ -74,7 +75,11 @@ class RR
74
75
 
75
76
  def clone
76
77
  encoded = MessageEncoder.new { |encoder| encoder.put_rr(self, true) }.to_s
77
- MessageDecoder.new(encoded).get_rr
78
+ copy = MessageDecoder.new(encoded).get_rr
79
+ [:@dnsruby_received_at, :@dnsruby_authenticated_expiration, :@dnsruby_learned_trust].each do |field|
80
+ copy.instance_variable_set(field, instance_variable_get(field)) if instance_variable_defined?(field)
81
+ end
82
+ copy
78
83
  end
79
84
 
80
85
 
@@ -178,11 +183,20 @@ class RR
178
183
  #
179
184
  #
180
185
  def RR.new_from_string(rrstring)
181
- # strip out comments
182
- # Test for non escaped ";" by means of the look-behind assertion
183
- # (the backslash is escaped)
184
- rrstring = rrstring.gsub(/(\?<!\\);.*/o, '')
186
+ # Standalone records allow unmatched parentheses, as in 1.74.0; ZoneReader
187
+ # requires balanced continuations. Skip the scanner for records without
188
+ # quotes, escapes, comments, parentheses or line breaks.
189
+ if /[\\'"();\r\n]/.match?(rrstring)
190
+ rrstring, = Presentation.scan(rrstring, allow_unbalanced: true,
191
+ single_quoted_from: rrstring.include?("'") ? Presentation.single_quoted_start(rrstring) : nil)
192
+ end
193
+ new_from_normalised_string(rrstring)
194
+ end
185
195
 
196
+ # ZoneReader already handles comments, grouping and quoted continuations.
197
+ # Keep this internal path separate from the public string constructors so
198
+ # normalized records need not run through the lexical scanner again.
199
+ def RR.new_from_normalised_string(rrstring) #:nodoc: all
186
200
  matches = (/#{@@RR_REGEX}/xo).match(rrstring)
187
201
  unless matches
188
202
  raise "#{rrstring} did not match RR pattern. Please report this to the author!"
@@ -194,7 +208,9 @@ class RR
194
208
  rrtype = matches[4] || ''
195
209
  rdata = matches[5] || ''
196
210
 
197
- rdata.gsub!(/\s+$/o, '') if rdata
211
+ # Only remove trailing record layout. `$` also matches before an embedded
212
+ # newline and would delete spaces that belong to a quoted TXT value.
213
+ rdata.sub!(/\s+\z/o, '') if rdata
198
214
 
199
215
  # RFC3597 tweaks
200
216
  # This converts to known class and type if specified as TYPE###
@@ -216,60 +232,93 @@ class RR
216
232
  rrtype = 'ANY'
217
233
  end
218
234
 
219
- unless %w(NAPTR TXT).include?(rrtype)
220
- if rdata
221
- rdata.gsub!('(', '')
222
- rdata.gsub!(')', '')
223
- end
235
+ if ttl == 0 && rdata.empty? && empty_update_type?(rrtype, rrclass)
236
+ return empty_update_record(name, rrtype, rrclass)
224
237
  end
225
238
 
226
- test_length = ->(hexdump, rdlength) do
227
- if hexdump.length != rdlength * 2
228
- raise "#{rdata} is inconsistent; length should be #{rdlength * 2} but is #{hexdump.length}."
239
+ if /\A\s*\\#/.match?(rdata)
240
+ # RFC 3597 section 5 permits zero octets with no hexadecimal field.
241
+ # Validate before packing: pack('H*') alone also accepts non-hex input.
242
+ generic = /\A\s*\\#\s+(\d+)(?:\s+([0-9a-fA-F\s]*))?\z/.match(rdata)
243
+ raise DecodeError, 'Expected RFC3597 representation of RDATA' unless generic
244
+ rdlength = generic[1].to_i
245
+ hexdump = (generic[2] || '').gsub(/\s+/, '')
246
+ if rdlength > 0xffff || hexdump.length != rdlength * 2
247
+ raise DecodeError, "#{rdata} is inconsistent with its RDATA length #{rdlength}."
229
248
  end
230
- end
231
-
232
- pack_rdata = ->(regex) do
233
- rdata =~ regex
234
- matches = regex.match(rdata)
235
- rdlength = matches[1].to_i
236
- hexdump = matches[2].gsub(/\s*/, '')
237
-
238
- test_length.(hexdump, rdlength)
239
- packed_rdata = [hexdump].pack('H*')
240
-
241
- [packed_rdata, rdlength]
242
- end
243
-
244
- if implemented_rrs.include?(rrtype) && rdata !~/^\s*\\#/o
249
+ return new_from_data(name, rrtype, rrclass, ttl, rdlength, [hexdump].pack('H*'), 0)
250
+ elsif implemented_rrs.include?(rrtype)
245
251
  return _get_subclass(name, rrtype, rrclass, ttl, rdata)
246
- elsif implemented_rrs.include?(rrtype) # A known RR type starting with \#
247
- packed_rdata, rdlength = pack_rdata.(/\\#\s+(\d+)\s+(.*)$/o)
248
- return new_from_data(name, rrtype, rrclass, ttl, rdlength, packed_rdata, 0) # rdata.length() - rdlength);
249
- elsif rdata =~ /\s*\\#\s+\d+\s+/o
250
- regex = /\\#\s+(\d+)\s+(.*)$/o
251
- # We are now dealing with the truly unknown.
252
- raise 'Expected RFC3597 representation of RDATA' unless rdata =~ regex
253
- packed_rdata, rdlength = pack_rdata.(regex)
254
- return new_from_data(name, rrtype, rrclass, ttl, rdlength, packed_rdata, 0) # rdata.length() - rdlength);
255
252
  else
256
253
  # God knows how to handle these...
257
254
  return _get_subclass(name, rrtype, rrclass, ttl, '')
258
255
  end
259
256
  end
257
+ private_class_method :new_from_normalised_string
260
258
 
261
259
  def RR.new_from_data(*args) #:nodoc: all
262
260
  name, rrtype, rrclass, ttl, rdlength, data, offset = args
263
261
  rdata = data ? data[offset, rdlength] : []
264
262
  decoder = MessageDecoder.new(rdata)
265
- record = get_class(rrtype, rrclass).decode_rdata(decoder)
263
+ record_class = get_class(rrtype, rrclass)
264
+ record = if rdlength == 0 && ttl == 0 && empty_update_type?(rrtype, rrclass)
265
+ empty_update_record(name, rrtype, rrclass)
266
+ else
267
+ record_class.decode_rdata(decoder)
268
+ end
266
269
  record.name = Name.create(name)
267
270
  record.ttl = ttl
268
271
  record.type = rrtype
269
272
  record.klass = Classes.new(rrclass)
273
+ record.validate_decoded_rdata
270
274
  record
271
275
  end
272
276
 
277
+ # Some formats depend on the actual RR class, which is assigned after the
278
+ # format-specific decoder runs (notably ANY/NONE UPDATE markers).
279
+ def validate_decoded_rdata #:nodoc: all
280
+ end
281
+
282
+ # RFC 2136 prerequisites and whole-RRset deletes carry no RDATA, including
283
+ # for types whose ordinary records require fields. Keep the familiar typed
284
+ # record while representing this operation separately from an empty value.
285
+ module EmptyUpdateData #:nodoc: all
286
+ def rdata_to_string
287
+ ''
288
+ end
289
+
290
+ def data
291
+ ''
292
+ end
293
+
294
+ def rdlength
295
+ 0
296
+ end
297
+
298
+ def encode_rdata(_encoder, _canonical = false)
299
+ unless ttl == 0 && RR.empty_update_type?(type, klass)
300
+ raise EncodeError, 'Empty UPDATE records require TTL zero and class ANY or NONE'
301
+ end
302
+ end
303
+
304
+ def validate_decoded_rdata
305
+ end
306
+ end
307
+
308
+ def self.empty_update_type?(type, klass) #:nodoc: all
309
+ code = Types.new(type).code
310
+ [Classes::ANY, Classes::NONE].include?(Classes.new(klass)) &&
311
+ code > 0 && code != Types::OPT && !(128..254).cover?(code)
312
+ end
313
+
314
+ def self.empty_update_record(name, type, klass) #:nodoc: all
315
+ raise ArgumentError, 'Invalid empty UPDATE record type or class' unless empty_update_type?(type, klass)
316
+ record_class = get_class(type, klass)
317
+ record = record_class <= RR::DomainName ? record_class.new(Name.new([])) : record_class.new
318
+ record.name, record.type, record.klass, record.ttl, record.rdata = name, type, klass, 0, ''
319
+ record.extend(EmptyUpdateData)
320
+ end
321
+
273
322
  # Return an array of all the currently implemented RR types
274
323
  def RR.implemented_rrs
275
324
  @@implemented_rr_map ||= ClassHash.keys.map { |key| Dnsruby::Types.to_string(key[0]) }
@@ -319,30 +368,21 @@ class RR
319
368
  raise DecodeError.new("#{self.class} is RR.")
320
369
  end
321
370
 
371
+ # Metadata and alternate presentations are not part of record identity.
372
+ def equality_fields #:nodoc:
373
+ fields = instance_variables - [:@ttl, :@rdata, :@dnsruby_learned_trust,
374
+ :@dnsruby_authenticated_expiration, :@dnsruby_received_at]
375
+ fields.delete(:@digest) if is_a?(RR::DS)
376
+ fields.sort
377
+ end
378
+ protected :equality_fields
379
+
322
380
  def ==(other)
323
381
  return false unless self.class == other.class
324
-
325
- ivars_to_compare = ->(object) do
326
- ivars = object.instance_variables.map { |var| var.to_s }
327
- ivars.delete '@ttl' # RFC 2136 section 1.1
328
- ivars.delete '@rdata'
329
- if self.type == Types.DS
330
- ivars.delete '@digest'
331
- end
332
- ivars.sort
333
- end
334
-
335
- get_instance_var_values = ->(object, ivar_names) do
336
- ivar_names.map { |ivar_name| object.instance_variable_get(ivar_name) }
382
+ fields = equality_fields
383
+ fields == other.equality_fields && fields.all? do |field|
384
+ instance_variable_get(field) == other.instance_variable_get(field)
337
385
  end
338
-
339
- self_ivars = ivars_to_compare.(self)
340
- other_ivars = ivars_to_compare.(other)
341
- return false unless self_ivars == other_ivars
342
-
343
- self_values = get_instance_var_values.(self, self_ivars)
344
- other_values = get_instance_var_values.(other, other_ivars)
345
- self_values == other_values
346
386
  end
347
387
 
348
388
  def eql?(other) #:nodoc:
@@ -350,22 +390,25 @@ class RR
350
390
  end
351
391
 
352
392
  def hash # :nodoc:
353
- vars = (self.instance_variables - [:@ttl, :@rdata]).sort
354
- vars.inject(0) do |hash_value, var_name|
355
- hash_value ^ self.instance_variable_get(var_name).hash
393
+ equality_fields.inject(0) do |hash_value, field|
394
+ hash_value ^ instance_variable_get(field).hash
356
395
  end
357
396
  end
358
397
 
359
398
  def self.find_class(type_value, class_value) # :nodoc: all
360
399
  if !! (ret = ClassHash[[type_value, class_value]])
361
400
  return ret
401
+ elsif !! (ret = GenericCache.find(type_value, class_value))
402
+ return ret
362
403
  elsif !! (val = ClassInsensitiveTypes[type_value])
363
404
  klass = Class.new(val)
364
405
  klass.const_set(:TypeValue, type_value)
365
406
  klass.const_set(:ClassValue, class_value)
366
407
  return klass
367
408
  else
368
- return Generic.create(type_value, class_value)
409
+ return GenericCache.factory(type_value, class_value) do
410
+ Generic.create(type_value, class_value)
411
+ end
369
412
  end
370
413
  end
371
414
 
@@ -240,15 +240,15 @@ module Dnsruby
240
240
 
241
241
  def encode_rdata(msg, canonical=false) #:nodoc: all
242
242
  # 2 octets, then 2 sets of 1 octet
243
- msg.put_pack('ncc', @type_covered.to_i, @algorithm.to_i, @labels)
243
+ msg.put_pack('nCC', @type_covered.to_i, @algorithm.to_i, @labels)
244
244
  msg.put_pack("NNN", @original_ttl, @expiration, @inception)
245
245
  msg.put_pack("n", @key_tag)
246
- msg.put_name(@signers_name, canonical, false)
246
+ msg.put_name(@signers_name, true, false)
247
247
  msg.put_bytes(@signature)
248
248
  end
249
249
 
250
250
  def self.decode_rdata(msg) #:nodoc: all
251
- type_covered, algorithm, labels = msg.get_unpack('ncc')
251
+ type_covered, algorithm, labels = msg.get_unpack('nCC')
252
252
  original_ttl, expiration, inception = msg.get_unpack('NNN')
253
253
  key_tag, = msg.get_unpack('n')
254
254
  signers_name = msg.get_name
@@ -263,7 +263,7 @@ module Dnsruby
263
263
  # with the Signer's Name field in canonical form and
264
264
  # the Signature field excluded;
265
265
  data = MessageEncoder.new { |msg|
266
- msg.put_pack('ncc', @type_covered.to_i, @algorithm.to_i, @labels)
266
+ msg.put_pack('nCC', @type_covered.to_i, @algorithm.to_i, @labels)
267
267
  msg.put_pack("NNN", @original_ttl, @expiration, @inception)
268
268
  msg.put_pack("n", @key_tag)
269
269
  msg.put_name(@signers_name, true)
@@ -272,4 +272,4 @@ module Dnsruby
272
272
  end
273
273
  end
274
274
  end
275
- end
275
+ end
@@ -16,7 +16,12 @@ class RRSet
16
16
  end
17
17
  @rrs = []
18
18
  @num_sigs = 0
19
+ # During construction all entries are private wire copies. Discard this
20
+ # temporary index before exposing the set: public RR objects are mutable.
21
+ @membership_index = {}
19
22
  rrs.each {|rr| add(rr)}
23
+ ensure
24
+ @membership_index = nil
20
25
  end
21
26
  def self.new_from_string(string)
22
27
  rr_strings = string.split("\n")
@@ -32,7 +37,14 @@ class RRSet
32
37
  return @rrs[0, @rrs.length-@num_sigs]
33
38
  end
34
39
  def privateAdd(r) #:nodoc:
35
- if @rrs.include?r
40
+ budget = ValidationBudget.current if defined?(ValidationBudget)
41
+ duplicate = if @membership_index
42
+ budget&.check!(1)
43
+ @membership_index.key?(r)
44
+ else
45
+ @rrs.any? { |existing| budget&.check!(1); existing == r }
46
+ end
47
+ if duplicate
36
48
  return true
37
49
  end
38
50
  new_pos = @rrs.length - @num_sigs
@@ -47,6 +59,7 @@ class RRSet
47
59
  @num_sigs += 1
48
60
  end
49
61
  @rrs.insert(new_pos, r)
62
+ @membership_index[r] = true if @membership_index
50
63
  return true
51
64
  end
52
65
 
@@ -56,25 +69,19 @@ class RRSet
56
69
  def add(rin, do_clone = true)
57
70
  if (rin.instance_of?RRSet)
58
71
  ret = false
59
- [rin.rrs, rin.sigs].each {|rr| ret = add(rr)}
72
+ (rin.rrs + rin.sigs).each {|rr| ret = add(rr, do_clone)}
60
73
  return ret
61
74
  end
62
- # r = RR.create(r.to_s) # clone the record
63
- r = nil
64
- if do_clone
65
- r = rin.clone
66
- else
67
- r = rin
68
- end
69
- if (@rrs.size() == 0) # && !(r.type == Types.RRSIG))
70
- return privateAdd(r)
71
- end
72
- # Check the type, klass and ttl are correct
73
- first = @rrs[0]
74
- if (!r.sameRRset(first))
75
- return false
76
- # raise ArgumentError.new("record does not match rrset")
77
- end
75
+ @membership_index = nil unless do_clone
76
+ add_record(rin, do_clone)
77
+ end
78
+
79
+ private def add_record(rin, do_clone)
80
+ # Reject unrelated records before copying their wire data.
81
+ first = @rrs.first
82
+ return false if first && !rin.sameRRset(first)
83
+ r = do_clone ? rin.clone : rin
84
+ return privateAdd(r) unless first
78
85
 
79
86
  if (!(r.type == Types::RRSIG) && (!(first.type == Types::RRSIG)))
80
87
  if (r.ttl != first.ttl) # RFC2181, section 5.2
@@ -114,12 +121,19 @@ class RRSet
114
121
  end
115
122
 
116
123
  return_rrs = RRSet.new
117
- canonical_rrs.keys.sort.each { |rdata|
118
- return_rrs.add(canonical_rrs[rdata], false)
119
- }
124
+ return_rrs.send(:add_canonical_records, canonical_rrs.keys.sort.map { |rdata| canonical_rrs[rdata] })
120
125
  return return_rrs
121
126
  end
122
127
 
128
+ private def add_canonical_records(records)
129
+ # Preserve the original references and TTL normalization of add(rr, false).
130
+ # The index exists only while building; callers can mutate records later.
131
+ @membership_index = {}
132
+ records.each { |record| add_record(record, false) }
133
+ ensure
134
+ @membership_index = nil
135
+ end
136
+
123
137
  def ==(other)
124
138
  return false unless other.instance_of?RRSet
125
139
  return false if (other.sigs.length != self.sigs.length)
@@ -137,7 +151,9 @@ class RRSet
137
151
  end
138
152
  # Delete the RR from this RRSet
139
153
  def delete(rr)
140
- @rrs.delete(rr)
154
+ removed = @rrs.delete(rr)
155
+ @num_sigs = @rrs.count { |record| record.type == Types::RRSIG } if removed
156
+ removed
141
157
  end
142
158
  def each
143
159
  @rrs.each do |rr|
@@ -96,7 +96,7 @@ module Dnsruby
96
96
  msg.put_pack("n", @priority)
97
97
  msg.put_pack("n", @weight)
98
98
  msg.put_pack("n", @port)
99
- msg.put_name(@target,canonical)
99
+ msg.put_name(@target, true, canonical)
100
100
  end
101
101
 
102
102
  def self.decode_rdata(msg) #:nodoc: all
@@ -109,4 +109,4 @@ module Dnsruby
109
109
  end
110
110
  end
111
111
  end
112
- end
112
+ end