rrs 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0d9733af6339a12ce11ff22d41f27142c86f16b46755c9b12c671f9a851fa6e
4
- data.tar.gz: a3df3377fa1a8f10e93281536fb4ccdcfc1cdd2bf19ab3bf5ce8e2840f575a60
3
+ metadata.gz: 5a851d73be2407a245945dd33ac1e669bd66ccad17f7f073212bf00e8105f358
4
+ data.tar.gz: c5c4d8f7c52bfad9aa0fe4cc85aff98b515f870c146242a5f263f82c10c75fe3
5
5
  SHA512:
6
- metadata.gz: f8ab7a7e7b4d8046101570b92a1ac8e7e07fd55100a3d43f4287b6c48ad317e5f13c07ccd80c743a6cd46a624d21dd82f5f27c993ea90430f79ace1e0b790f21
7
- data.tar.gz: 6e2479f8d855d39afb625f61699501ec4d2d00c7d177d2f950d63dc845c260e0a6a02b6daf37e8f1e5fdb9b73b51357d9e58f735ddc3b35599025674b4688317
6
+ metadata.gz: 789a9f105dbaf0f00c93cd0811e75fbc6956130ede2482bba8720ed207cc14e27d53889824f284dbde05c3131b8e61a69883deaf4dc52d4035684ed8d7a94936
7
+ data.tar.gz: ca30ac41e67c5f55bebdeba2d9c7b28acde254385d838eca61c579a998bb64e2852de7407582f1e74e078c8b694440fdeab77cb33e59a38a0cfd9fbcda241bc6
@@ -1,5 +1,5 @@
1
1
  module RRs
2
- class A
2
+ class IN::A < Resource
3
3
  TypeValue = 1
4
4
  ClassValue = 1
5
5
 
data/lib/rrs/in.rb ADDED
@@ -0,0 +1,4 @@
1
+ module RRs
2
+ module IN
3
+ end
4
+ end
data/lib/rrs/inflector.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative "maps/qtype_map"
1
2
  module RRs
2
3
  class Inflector < Zeitwerk::Inflector
3
4
  def camelize(basename, abspath)
@@ -5,6 +6,10 @@ module RRs
5
6
  "RRs" + super($1, abspath)
6
7
  elsif basename =~ /\Aipv(.*)/
7
8
  "IPv" + super($1, abspath)
9
+ elsif basename =~ /\Ain(.*)/ && basename != "inflector"
10
+ "IN" + super($1, abspath)
11
+ elsif ::RRs::Maps::QTYPE_MAP.values.any? { |v| basename.upcase == v.to_s }
12
+ basename.upcase
8
13
  else
9
14
  super
10
15
  end
data/lib/rrs/label.rb ADDED
@@ -0,0 +1,39 @@
1
+ module RRs
2
+ module Label # :nodoc:
3
+ def self.split(arg)
4
+ labels = []
5
+ arg.scan(/[^\.]+/) {labels << Str.new($&)}
6
+ return labels
7
+ end
8
+
9
+ class Str # :nodoc:
10
+ def initialize(string)
11
+ @string = string
12
+ # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343]
13
+ # This assumes @string is given in ASCII compatible encoding.
14
+ @downcase = string.b.downcase
15
+ end
16
+ attr_reader :string, :downcase
17
+
18
+ def to_s
19
+ return @string
20
+ end
21
+
22
+ def inspect
23
+ return "#<#{self.class} #{self}>"
24
+ end
25
+
26
+ def ==(other)
27
+ return self.class == other.class && @downcase == other.downcase
28
+ end
29
+
30
+ def eql?(other)
31
+ return self == other
32
+ end
33
+
34
+ def hash
35
+ return @downcase.hash
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ module RRs
2
+ module Maps
3
+ QCLASS_MAP = Ractor.make_shareable({
4
+ 1 => :IN,
5
+ 2 => :CS,
6
+ 3 => :CH,
7
+ 4 => :HS,
8
+ 255 => :*
9
+ }.freeze)
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ module RRs
2
+ module Maps
3
+ QTYPE_MAP = Ractor.make_shareable({
4
+ 1 => :A,
5
+ 2 => :NS,
6
+ 3 => :MD,
7
+ 4 => :MF,
8
+ 5 => :CNAME,
9
+ 6 => :SOA,
10
+ 7 => :MB,
11
+ 8 => :MG,
12
+ 9 => :MR,
13
+ 10 => :NULL,
14
+ 11 => :WKS,
15
+ 12 => :PTR,
16
+ 13 => :HINFO,
17
+ 14 => :MINFO,
18
+ 15 => :MX,
19
+ 16 => :TXT,
20
+ 28 => :AAAA,
21
+ 41 => :OPT,
22
+ 252 => :AXFR,
23
+ 253 => :MAILB,
24
+ 254 => :MAILA,
25
+ 255 => :*
26
+ }.freeze)
27
+ end
28
+ end
data/lib/rrs/maps.rb ADDED
@@ -0,0 +1,6 @@
1
+ require_relative "maps/qclass_map"
2
+ require_relative "maps/qtype_map"
3
+ module RRs
4
+ module Maps
5
+ end
6
+ end
@@ -0,0 +1,352 @@
1
+ module RRs
2
+ class Message
3
+ @@identifier = -1
4
+
5
+ def initialize(id = (@@identifier += 1) & 0xffff)
6
+ @id = id
7
+ @qr = 0
8
+ @opcode = 0
9
+ @aa = 0
10
+ @tc = 0
11
+ @rd = 0 # recursion desired
12
+ @ra = 0 # recursion available
13
+ @rcode = 0
14
+ @question = []
15
+ @answer = []
16
+ @authority = []
17
+ @additional = []
18
+ end
19
+
20
+ attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :rcode
21
+ attr_reader :question, :answer, :authority, :additional
22
+
23
+ def ==(other)
24
+ return @id == other.id &&
25
+ @qr == other.qr &&
26
+ @opcode == other.opcode &&
27
+ @aa == other.aa &&
28
+ @tc == other.tc &&
29
+ @rd == other.rd &&
30
+ @ra == other.ra &&
31
+ @rcode == other.rcode &&
32
+ @question == other.question &&
33
+ @answer == other.answer &&
34
+ @authority == other.authority &&
35
+ @additional == other.additional
36
+ end
37
+
38
+ def add_question(name, typeclass)
39
+ @question << [Name.create(name), typeclass]
40
+ end
41
+
42
+ def each_question
43
+ @question.each {|name, typeclass|
44
+ yield name, typeclass
45
+ }
46
+ end
47
+
48
+ def add_answer(name, ttl, data)
49
+ @answer << [Name.create(name), ttl, data]
50
+ end
51
+
52
+ def each_answer
53
+ @answer.each {|name, ttl, data|
54
+ yield name, ttl, data
55
+ }
56
+ end
57
+
58
+ def add_authority(name, ttl, data)
59
+ @authority << [Name.create(name), ttl, data]
60
+ end
61
+
62
+ def each_authority
63
+ @authority.each {|name, ttl, data|
64
+ yield name, ttl, data
65
+ }
66
+ end
67
+
68
+ def add_additional(name, ttl, data)
69
+ @additional << [Name.create(name), ttl, data]
70
+ end
71
+
72
+ def each_additional
73
+ @additional.each {|name, ttl, data|
74
+ yield name, ttl, data
75
+ }
76
+ end
77
+
78
+ def each_resource
79
+ each_answer {|name, ttl, data| yield name, ttl, data}
80
+ each_authority {|name, ttl, data| yield name, ttl, data}
81
+ each_additional {|name, ttl, data| yield name, ttl, data}
82
+ end
83
+
84
+ def encode
85
+ return MessageEncoder.new {|msg|
86
+ msg.put_pack('nnnnnn',
87
+ @id,
88
+ (@qr & 1) << 15 |
89
+ (@opcode & 15) << 11 |
90
+ (@aa & 1) << 10 |
91
+ (@tc & 1) << 9 |
92
+ (@rd & 1) << 8 |
93
+ (@ra & 1) << 7 |
94
+ (@rcode & 15),
95
+ @question.length,
96
+ @answer.length,
97
+ @authority.length,
98
+ @additional.length)
99
+ @question.each {|q|
100
+ name, typeclass = q
101
+ msg.put_name(name)
102
+ msg.put_pack('nn', typeclass::TypeValue, typeclass::ClassValue)
103
+ }
104
+ [@answer, @authority, @additional].each {|rr|
105
+ rr.each {|r|
106
+ name, ttl, data = r
107
+ msg.put_name(name)
108
+ msg.put_pack('nnN', data.class::TypeValue, data.class::ClassValue, ttl)
109
+ msg.put_length16 {data.encode_rdata(msg)}
110
+ }
111
+ }
112
+ }.to_s
113
+ end
114
+
115
+ class MessageEncoder # :nodoc:
116
+ def initialize
117
+ @data = ''.dup
118
+ @names = {}
119
+ yield self
120
+ end
121
+
122
+ def to_s
123
+ return @data
124
+ end
125
+
126
+ def put_bytes(d)
127
+ @data << d
128
+ end
129
+
130
+ def put_pack(template, *d)
131
+ @data << d.pack(template)
132
+ end
133
+
134
+ def put_length16
135
+ length_index = @data.length
136
+ @data << "\0\0"
137
+ data_start = @data.length
138
+ yield
139
+ data_end = @data.length
140
+ @data[length_index, 2] = [data_end - data_start].pack("n")
141
+ end
142
+
143
+ def put_string(d)
144
+ self.put_pack("C", d.length)
145
+ @data << d
146
+ end
147
+
148
+ def put_string_list(ds)
149
+ ds.each {|d|
150
+ self.put_string(d)
151
+ }
152
+ end
153
+
154
+ def put_name(d, compress: true)
155
+ put_labels(d.to_a, compress: compress)
156
+ end
157
+
158
+ def put_labels(d, compress: true)
159
+ d.each_index {|i|
160
+ domain = d[i..-1]
161
+ if compress && idx = @names[domain]
162
+ self.put_pack("n", 0xc000 | idx)
163
+ return
164
+ else
165
+ if @data.length < 0x4000
166
+ @names[domain] = @data.length
167
+ end
168
+ self.put_label(d[i])
169
+ end
170
+ }
171
+ @data << "\0"
172
+ end
173
+
174
+ def put_label(d)
175
+ self.put_string(d.to_s)
176
+ end
177
+ end
178
+
179
+ def Message.decode(m)
180
+ o = Message.new(0)
181
+ MessageDecoder.new(m) {|msg|
182
+ id, flag, qdcount, ancount, nscount, arcount =
183
+ msg.get_unpack('nnnnnn')
184
+ o.id = id
185
+ o.tc = (flag >> 9) & 1
186
+ o.rcode = flag & 15
187
+ return o unless o.tc.zero?
188
+
189
+ o.qr = (flag >> 15) & 1
190
+ o.opcode = (flag >> 11) & 15
191
+ o.aa = (flag >> 10) & 1
192
+ o.rd = (flag >> 8) & 1
193
+ o.ra = (flag >> 7) & 1
194
+ (1..qdcount).each {
195
+ name, typeclass = msg.get_question
196
+ o.add_question(name, typeclass)
197
+ }
198
+ (1..ancount).each {
199
+ name, ttl, data = msg.get_rr
200
+ o.add_answer(name, ttl, data)
201
+ }
202
+ (1..nscount).each {
203
+ name, ttl, data = msg.get_rr
204
+ o.add_authority(name, ttl, data)
205
+ }
206
+ (1..arcount).each {
207
+ name, ttl, data = msg.get_rr
208
+ o.add_additional(name, ttl, data)
209
+ }
210
+ }
211
+ return o
212
+ end
213
+
214
+ class MessageDecoder # :nodoc:
215
+ def initialize(data)
216
+ @data = data
217
+ @index = 0
218
+ @limit = data.bytesize
219
+ yield self
220
+ end
221
+
222
+ def inspect
223
+ "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>"
224
+ end
225
+
226
+ def get_length16
227
+ len, = self.get_unpack('n')
228
+ save_limit = @limit
229
+ @limit = @index + len
230
+ d = yield(len)
231
+ if @index < @limit
232
+ raise DecodeError.new("junk exists")
233
+ elsif @limit < @index
234
+ raise DecodeError.new("limit exceeded")
235
+ end
236
+ @limit = save_limit
237
+ return d
238
+ end
239
+
240
+ def get_bytes(len = @limit - @index)
241
+ raise DecodeError.new("limit exceeded") if @limit < @index + len
242
+ d = @data.byteslice(@index, len)
243
+ @index += len
244
+ return d
245
+ end
246
+
247
+ def get_unpack(template)
248
+ len = 0
249
+ template.each_byte {|byte|
250
+ byte = "%c" % byte
251
+ case byte
252
+ when ?c, ?C
253
+ len += 1
254
+ when ?n
255
+ len += 2
256
+ when ?N
257
+ len += 4
258
+ else
259
+ raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'")
260
+ end
261
+ }
262
+ raise DecodeError.new("limit exceeded") if @limit < @index + len
263
+ arr = @data.unpack("@#{@index}#{template}")
264
+ @index += len
265
+ return arr
266
+ end
267
+
268
+ def get_string
269
+ raise DecodeError.new("limit exceeded") if @limit <= @index
270
+ len = @data.getbyte(@index)
271
+ raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len
272
+ d = @data.byteslice(@index + 1, len)
273
+ @index += 1 + len
274
+ return d
275
+ end
276
+
277
+ def get_string_list
278
+ strings = []
279
+ while @index < @limit
280
+ strings << self.get_string
281
+ end
282
+ strings
283
+ end
284
+
285
+ def get_list
286
+ [].tap do |values|
287
+ while @index < @limit
288
+ values << yield
289
+ end
290
+ end
291
+ end
292
+
293
+ def get_name
294
+ return Name.new(self.get_labels)
295
+ end
296
+
297
+ def get_labels
298
+ prev_index = @index
299
+ save_index = nil
300
+ d = []
301
+ while true
302
+ raise DecodeError.new("limit exceeded") if @limit <= @index
303
+ case @data.getbyte(@index)
304
+ when 0
305
+ @index += 1
306
+ if save_index
307
+ @index = save_index
308
+ end
309
+ return d
310
+ when 192..255
311
+ idx = self.get_unpack('n')[0] & 0x3fff
312
+ if prev_index <= idx
313
+ raise DecodeError.new("non-backward name pointer")
314
+ end
315
+ prev_index = idx
316
+ if !save_index
317
+ save_index = @index
318
+ end
319
+ @index = idx
320
+ else
321
+ d << self.get_label
322
+ end
323
+ end
324
+ end
325
+
326
+ def get_label
327
+ return Label::Str.new(self.get_string)
328
+ end
329
+
330
+ def get_question
331
+ name = self.get_name
332
+ type, klass = self.get_unpack("nn")
333
+ return name, Resource.get_class(type, klass)
334
+ end
335
+
336
+ def get_rr
337
+ name = self.get_name
338
+ type, klass, ttl = self.get_unpack('nnN')
339
+ typeclass = Resource.get_class(type, klass)
340
+ res = self.get_length16 do
341
+ begin
342
+ typeclass.decode_rdata self
343
+ rescue => e
344
+ raise DecodeError, e.message, e.backtrace
345
+ end
346
+ end
347
+ res.instance_variable_set :@ttl, ttl
348
+ return name, ttl, res
349
+ end
350
+ end
351
+ end
352
+ end
data/lib/rrs/name.rb ADDED
@@ -0,0 +1,109 @@
1
+ module RRs
2
+ ##
3
+ # A representation of a DNS name.
4
+
5
+ class Name
6
+
7
+ ##
8
+ # Creates a new DNS name from +arg+. +arg+ can be:
9
+ #
10
+ # Name:: returns +arg+.
11
+ # String:: Creates a new Name.
12
+
13
+ def self.create(arg)
14
+ case arg
15
+ when Name
16
+ return arg
17
+ when String
18
+ return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false)
19
+ else
20
+ raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}")
21
+ end
22
+ end
23
+
24
+ def initialize(labels, absolute=true) # :nodoc:
25
+ labels = labels.map {|label|
26
+ case label
27
+ when String then Label::Str.new(label)
28
+ when Label::Str then label
29
+ else
30
+ raise ArgumentError, "unexpected label: #{label.inspect}"
31
+ end
32
+ }
33
+ @labels = labels
34
+ @absolute = absolute
35
+ end
36
+
37
+ def inspect # :nodoc:
38
+ "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>"
39
+ end
40
+
41
+ ##
42
+ # True if this name is absolute.
43
+
44
+ def absolute?
45
+ return @absolute
46
+ end
47
+
48
+ def ==(other) # :nodoc:
49
+ return false unless Name === other
50
+ return false unless @absolute == other.absolute?
51
+ return @labels == other.to_a
52
+ end
53
+
54
+ alias eql? == # :nodoc:
55
+
56
+ ##
57
+ # Returns true if +other+ is a subdomain.
58
+ #
59
+ # Example:
60
+ #
61
+ # domain = Resolv::DNS::Name.create("y.z")
62
+ # p Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true
63
+ # p Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true
64
+ # p Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false
65
+ # p Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false
66
+ # p Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false
67
+ # p Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false
68
+ #
69
+
70
+ def subdomain_of?(other)
71
+ raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other
72
+ return false if @absolute != other.absolute?
73
+ other_len = other.length
74
+ return false if @labels.length <= other_len
75
+ return @labels[-other_len, other_len] == other.to_a
76
+ end
77
+
78
+ def hash # :nodoc:
79
+ return @labels.hash ^ @absolute.hash
80
+ end
81
+
82
+ def to_a # :nodoc:
83
+ return @labels
84
+ end
85
+
86
+ def length # :nodoc:
87
+ return @labels.length
88
+ end
89
+
90
+ def [](i) # :nodoc:
91
+ return @labels[i]
92
+ end
93
+
94
+ ##
95
+ # returns the domain name as a string.
96
+ #
97
+ # The domain name doesn't have a trailing dot even if the name object is
98
+ # absolute.
99
+ #
100
+ # Example:
101
+ #
102
+ # p Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z"
103
+ # p Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z"
104
+
105
+ def to_s
106
+ return @labels.join('.')
107
+ end
108
+ end
109
+ end
data/lib/rrs/opt.rb ADDED
@@ -0,0 +1,16 @@
1
+ module RRs
2
+ class OPT < Resource
3
+ attr_reader :data
4
+ def initialize(data)
5
+ @data = data
6
+ end
7
+
8
+ def encode_rdata(msg)
9
+ msg.put_bytes(data)
10
+ end
11
+
12
+ def self.decode_rdata(msg)
13
+ return self.new(msg.get_bytes)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,51 @@
1
+ class RRs::Resource
2
+ ##
3
+ # Remaining Time To Live for this Resource.
4
+
5
+ attr_reader :ttl
6
+
7
+ def encode_rdata(msg) # :nodoc:
8
+ raise NotImplementedError.new
9
+ end
10
+
11
+ def self.decode_rdata(msg) # :nodoc:
12
+ raise NotImplementedError.new
13
+ end
14
+
15
+ def ==(other) # :nodoc:
16
+ return false unless self.class == other.class
17
+ s_ivars = self.instance_variables
18
+ s_ivars.sort!
19
+ s_ivars.delete :@ttl
20
+ o_ivars = other.instance_variables
21
+ o_ivars.sort!
22
+ o_ivars.delete :@ttl
23
+ return s_ivars == o_ivars &&
24
+ s_ivars.collect {|name| self.instance_variable_get name} ==
25
+ o_ivars.collect {|name| other.instance_variable_get name}
26
+ end
27
+
28
+ def eql?(other) # :nodoc:
29
+ return self == other
30
+ end
31
+
32
+ def hash # :nodoc:
33
+ h = 0
34
+ vars = self.instance_variables
35
+ vars.delete :@ttl
36
+ vars.each {|name|
37
+ h ^= self.instance_variable_get(name).hash
38
+ }
39
+ return h
40
+ end
41
+
42
+ def self.get_class(type_value, class_value) # :nodoc:
43
+ path = []
44
+ if class_value
45
+ path.push(::RRs::Maps::QCLASS_MAP[class_value.to_i].to_s) if ::RRs::Maps::QCLASS_MAP[class_value.to_i]
46
+ end
47
+ path.push(::RRs::Maps::QTYPE_MAP[type_value.to_i].to_s)
48
+ RRs.const_get(path.join("::"))
49
+ end
50
+
51
+ end
data/lib/rrs.rb CHANGED
@@ -1,15 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
  require "zeitwerk"
3
3
  require_relative "rrs/inflector"
4
+ require_relative "rrs/maps"
4
5
 
5
6
  loader = Zeitwerk::Loader.for_gem
6
7
  loader.inflector = RRs::Inflector.new
8
+ loader.ignore("#{__dir__}/rrs/maps")
7
9
  loader.setup
8
10
 
9
11
  module RRs
10
- VERSION = "0.1.0"
12
+ VERSION = "0.2.0"
11
13
 
12
14
  class Error < StandardError; end
15
+ class DecodeError < StandardError; end
16
+ class EncodeError < StandardError; end
13
17
  end
14
18
 
15
19
  loader.eager_load
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rrs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - reesericci
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-17 00:00:00.000000000 Z
11
+ date: 2024-08-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: "RRs is a set of self-contained DNS primitives that do not share state,
14
14
  which makes them suitable for use in Ractors. \n\nThis is a contrast to the Resolv::DNS
@@ -23,10 +23,19 @@ files:
23
23
  - README.md
24
24
  - Rakefile
25
25
  - lib/rrs.rb
26
- - lib/rrs/a.rb
26
+ - lib/rrs/in.rb
27
+ - lib/rrs/in/a.rb
27
28
  - lib/rrs/inflector.rb
28
29
  - lib/rrs/ipv4.rb
29
30
  - lib/rrs/ipv6.rb
31
+ - lib/rrs/label.rb
32
+ - lib/rrs/maps.rb
33
+ - lib/rrs/maps/qclass_map.rb
34
+ - lib/rrs/maps/qtype_map.rb
35
+ - lib/rrs/message.rb
36
+ - lib/rrs/name.rb
37
+ - lib/rrs/opt.rb
38
+ - lib/rrs/resource.rb
30
39
  - sig/rrs.rbs
31
40
  homepage: https://codeberg.org/reesericci/rrs
32
41
  licenses: []