@nictool/dns-resource-record 1.6.1 → 1.8.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 (55) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +356 -199
  3. package/dist/dns-rr.cjs +7050 -0
  4. package/dist/dns-rr.min.js +26 -0
  5. package/dist/dns-rr.min.js.map +7 -0
  6. package/index.js +12 -11
  7. package/lib/binary.js +108 -0
  8. package/lib/bind.js +96 -0
  9. package/lib/dns-query.js +67 -0
  10. package/lib/tinydns.js +128 -44
  11. package/lib/wire.js +629 -0
  12. package/package.json +28 -8
  13. package/rr/TEMPLATE.js +124 -0
  14. package/rr/a.js +8 -48
  15. package/rr/aaaa.js +13 -80
  16. package/rr/apl.js +123 -27
  17. package/rr/caa.js +27 -50
  18. package/rr/cert.js +60 -81
  19. package/rr/cname.js +8 -59
  20. package/rr/dhcid.js +20 -30
  21. package/rr/dname.js +10 -32
  22. package/rr/dnskey.js +38 -32
  23. package/rr/ds.js +59 -54
  24. package/rr/hinfo.js +21 -40
  25. package/rr/hip.js +88 -26
  26. package/rr/https.js +28 -40
  27. package/rr/ipseckey.js +97 -18
  28. package/rr/key.js +39 -29
  29. package/rr/kx.js +16 -27
  30. package/rr/loc.js +42 -12
  31. package/rr/mx.js +17 -48
  32. package/rr/naptr.js +58 -33
  33. package/rr/ns.js +8 -36
  34. package/rr/nsec.js +89 -18
  35. package/rr/nsec3.js +62 -26
  36. package/rr/nsec3param.js +56 -53
  37. package/rr/nxt.js +57 -18
  38. package/rr/openpgpkey.js +24 -30
  39. package/rr/ptr.js +7 -47
  40. package/rr/rp.js +17 -32
  41. package/rr/rrsig.js +108 -75
  42. package/rr/sig.js +70 -45
  43. package/rr/smimea.js +33 -33
  44. package/rr/soa.js +31 -49
  45. package/rr/spf.js +15 -18
  46. package/rr/srv.js +28 -51
  47. package/rr/sshfp.js +35 -48
  48. package/rr/svcb.js +27 -40
  49. package/rr/tlsa.js +34 -34
  50. package/rr/tsig.js +87 -24
  51. package/rr/txt.js +77 -68
  52. package/rr/uri.js +19 -29
  53. package/rr/wks.js +154 -22
  54. package/rr.js +290 -109
  55. package/lib/readme.js +0 -84
package/rr/tsig.js CHANGED
@@ -1,7 +1,14 @@
1
1
  import RR from '../rr.js'
2
2
  import * as TINYDNS from '../lib/tinydns.js'
3
+ import * as BINARY from '../lib/binary.js'
4
+ import * as WIRE from '../lib/wire.js'
3
5
 
4
6
  export default class TSIG extends RR {
7
+ static typeName = 'TSIG'
8
+ static typeId = 250
9
+ static RFCs = [2845, 8945]
10
+ static rdataFields = ['algorithm name', 'time signed', 'fudge', 'mac', 'original id', 'error', 'other']
11
+
5
12
  constructor(opts) {
6
13
  super(opts)
7
14
  if (opts === null) return
@@ -13,18 +20,6 @@ export default class TSIG extends RR {
13
20
  return 'Transaction Signature'
14
21
  }
15
22
 
16
- getRdataFields(arg) {
17
- return ['algorithm name', 'time signed', 'fudge', 'mac', 'original id', 'error', 'other']
18
- }
19
-
20
- getRFCs() {
21
- return [2845, 8945]
22
- }
23
-
24
- getTypeId() {
25
- return 250
26
- }
27
-
28
23
  getCanonical() {
29
24
  return {
30
25
  owner: 'test.example.',
@@ -114,22 +109,23 @@ export default class TSIG extends RR {
114
109
  const algUnpacked = TINYDNS.unpackDomainName(rdata)
115
110
  const algBinaryLen = algUnpacked[2]
116
111
 
117
- const bytes = Buffer.from(TINYDNS.octalToChar(rdata), 'binary')
112
+ const bytes = Uint8Array.from(TINYDNS.octalToChar(rdata), (c) => c.charCodeAt(0))
113
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
118
114
  let bpos = algBinaryLen
119
115
 
120
- const timeSigned = bytes.readUInt32BE(bpos)
116
+ const timeSigned = dv.getUint32(bpos)
121
117
  bpos += 4
122
- const fudge = bytes.readUInt16BE(bpos)
118
+ const fudge = dv.getUint16(bpos)
123
119
  bpos += 2
124
- const macSize = bytes.readUInt16BE(bpos)
120
+ const macSize = dv.getUint16(bpos)
125
121
  bpos += 2
126
- const mac = macSize > 0 ? bytes.slice(bpos, bpos + macSize).toString('hex') : ''
122
+ const mac = macSize > 0 ? BINARY.bytesToHex(bytes.subarray(bpos, bpos + macSize)) : ''
127
123
  bpos += macSize
128
- const originalId = bytes.readUInt16BE(bpos)
124
+ const originalId = dv.getUint16(bpos)
129
125
  bpos += 2
130
- const error = bytes.readUInt16BE(bpos)
126
+ const error = dv.getUint16(bpos)
131
127
  bpos += 2
132
- const other = bpos < bytes.length ? bytes.slice(bpos).toString() : ''
128
+ const other = bpos < bytes.length ? new TextDecoder().decode(bytes.subarray(bpos)) : ''
133
129
 
134
130
  return new TSIG({
135
131
  owner: this.fullyQualify(owner),
@@ -148,6 +144,38 @@ export default class TSIG extends RR {
148
144
  })
149
145
  }
150
146
 
147
+ fromWire({ owner, cls, ttl, rdata }) {
148
+ const { fqdn: algorithmName, end } = this.wireUnpackDomain(rdata, 0)
149
+ const dv = new DataView(rdata.buffer, rdata.byteOffset)
150
+ let pos = end
151
+ const timeSigned = dv.getUint32(pos)
152
+ pos += 4
153
+ const fudge = dv.getUint16(pos)
154
+ pos += 2
155
+ const macSize = dv.getUint16(pos)
156
+ pos += 2
157
+ const mac = macSize > 0 ? BINARY.bytesToHex(rdata.subarray(pos, pos + macSize)) : ''
158
+ pos += macSize
159
+ const originalId = dv.getUint16(pos)
160
+ pos += 2
161
+ const error = dv.getUint16(pos)
162
+ pos += 2
163
+ const other = pos < rdata.length ? new TextDecoder().decode(rdata.subarray(pos)) : ''
164
+ return new TSIG({
165
+ owner,
166
+ ttl: 0,
167
+ class: 'ANY',
168
+ type: 'TSIG',
169
+ 'algorithm name': algorithmName,
170
+ 'time signed': timeSigned,
171
+ fudge,
172
+ mac,
173
+ 'original id': originalId,
174
+ error,
175
+ other,
176
+ })
177
+ }
178
+
151
179
  /****** EXPORTERS *******/
152
180
  toBind(zone_opts) {
153
181
  const mac = this.get('mac') ?? ''
@@ -172,19 +200,54 @@ export default class TSIG extends RR {
172
200
  )
173
201
  }
174
202
 
203
+ getWireRdata() {
204
+ const algWire = WIRE.wirePackDomain(this.get('algorithm name') || '')
205
+ const mac = this.get('mac') ?? ''
206
+ const macBytes = mac.length > 0 ? BINARY.hexToBytes(mac) : new Uint8Array()
207
+ const other = this.get('other') ?? ''
208
+ const otherBytes = other.length > 0 ? new TextEncoder().encode(other) : new Uint8Array()
209
+
210
+ const bytes = new Uint8Array(algWire.length + 4 + 2 + 2 + macBytes.length + 2 + 2 + otherBytes.length)
211
+ const dv = new DataView(bytes.buffer, bytes.byteOffset)
212
+ let pos = 0
213
+
214
+ bytes.set(algWire, pos)
215
+ pos += algWire.length
216
+ dv.setUint32(pos, this.get('time signed') ?? 0)
217
+ pos += 4
218
+ dv.setUint16(pos, this.get('fudge') ?? 0)
219
+ pos += 2
220
+ dv.setUint16(pos, macBytes.length)
221
+ pos += 2
222
+ if (macBytes.length > 0) {
223
+ bytes.set(macBytes, pos)
224
+ pos += macBytes.length
225
+ }
226
+ dv.setUint16(pos, this.get('original id') ?? 0)
227
+ pos += 2
228
+ dv.setUint16(pos, this.get('error') ?? 0)
229
+ pos += 2
230
+ if (otherBytes.length > 0) bytes.set(otherBytes, pos)
231
+
232
+ return bytes
233
+ }
234
+
175
235
  toTinydns() {
176
- const dataRe = new RegExp(/[\r\n\t:\\/]/, 'g')
177
236
  const alg = this.get('algorithm name') || ''
237
+ const mac = this.get('mac') ?? ''
238
+ const macByteLen = mac.length > 0 ? mac.length / 2 : 0
178
239
 
179
240
  return this.getTinydnsGeneric(
180
241
  TINYDNS.packDomainName(alg) +
181
242
  TINYDNS.UInt32toOctal(this.get('time signed') ?? 0) +
182
243
  TINYDNS.UInt16toOctal(this.get('fudge')) +
183
- TINYDNS.UInt16toOctal(this.get('mac size') ?? 0) +
184
- (this.get('mac size') > 0 ? TINYDNS.escapeOctal(dataRe, this.get('mac')) : '') +
244
+ TINYDNS.UInt16toOctal(macByteLen) +
245
+ (macByteLen > 0 ? TINYDNS.packHex(mac) : '') +
185
246
  TINYDNS.UInt16toOctal(this.get('original id') ?? 0) +
186
247
  TINYDNS.UInt16toOctal(this.get('error') ?? 0) +
187
- (this.get('other').length > 0 ? TINYDNS.escapeOctal(dataRe, this.get('other')) : ''),
248
+ (this.get('other').length > 0
249
+ ? TINYDNS.escapeOctal(new RegExp(/[\r\n\t:\\/]/, 'g'), this.get('other'))
250
+ : ''),
188
251
  )
189
252
  }
190
253
  }
package/rr/txt.js CHANGED
@@ -3,6 +3,13 @@ import RR from '../rr.js'
3
3
  import * as TINYDNS from '../lib/tinydns.js'
4
4
 
5
5
  export default class TXT extends RR {
6
+ static typeName = 'TXT'
7
+ static typeId = 16
8
+ static RFCs = [1035, 4408, 7208, 6376]
9
+ static tinydnsType = "'"
10
+ static rdataFields = [['data', 'charstrs']]
11
+ static tags = ['common']
12
+
6
13
  constructor(opts) {
7
14
  super(opts)
8
15
  }
@@ -16,22 +23,6 @@ export default class TXT extends RR {
16
23
  return 'Text'
17
24
  }
18
25
 
19
- getTags() {
20
- return ['common']
21
- }
22
-
23
- getRdataFields(arg) {
24
- return ['data']
25
- }
26
-
27
- getRFCs() {
28
- return [1035, 4408, 7208, 6376]
29
- }
30
-
31
- getTypeId() {
32
- return 16
33
- }
34
-
35
26
  getCanonical() {
36
27
  return {
37
28
  owner: 'example.com.',
@@ -71,35 +62,20 @@ export default class TXT extends RR {
71
62
  if (n != 16) this.throwHelp('TXT fromTinydns, invalid n')
72
63
 
73
64
  rdata = TINYDNS.octalToChar(rdata)
74
- let s = ''
75
- let len = rdata[0].charCodeAt(0)
76
- let pos = 1
65
+ // Walk RFC 1035 §3.3.14 len-prefixed <character-string> segments.
66
+ const parts = []
67
+ let pos = 0
77
68
  while (pos < rdata.length) {
78
- s += rdata.slice(pos, +(len + pos))
79
- pos = len + pos
80
- len = rdata.charCodeAt(pos + 1)
69
+ const len = rdata.charCodeAt(pos)
70
+ pos += 1
71
+ if (pos + len > rdata.length) {
72
+ this.throwHelp('TXT fromTinydnsGeneric: truncated character-string in rdata')
73
+ }
74
+ parts.push(rdata.slice(pos, pos + len))
75
+ pos += len
81
76
  }
82
- return [fqdn, s, ttl, ts, loc]
83
- }
84
-
85
- fromBind({ bindline }) {
86
- // test.example.com 3600 IN TXT "..."
87
- const regex = /^(?<owner>\S{1,255})\s+(?<ttl>\d{1,10})\s+(?<cls>IN)\s+(?<type>\w{3})\s+(?<rdata>\S.*)$/i
88
- const match = bindline.trim().match(regex)
89
- if (!match) this.throwHelp(`unable to parse TXT: ${bindline}`)
90
-
91
- const { owner, ttl, cls, type, rdata } = match.groups
92
-
93
- return new this.constructor({
94
- owner,
95
- ttl: parseInt(ttl, 10),
96
- class: cls,
97
- type: type.toUpperCase(),
98
- data: rdata
99
- .match(/"([^"]+?)"/g)
100
- .map((s) => s.replace(/^"|"$/g, ''))
101
- .join(''),
102
- })
77
+ const data = parts.length > 1 ? parts : (parts[0] ?? '')
78
+ return [fqdn, data, ttl, ts, loc]
103
79
  }
104
80
 
105
81
  /****** EXPORTERS *******/
@@ -113,8 +89,31 @@ export default class TXT extends RR {
113
89
  }
114
90
 
115
91
  getWireRdata() {
116
- let data = this.get('data')
117
- if (Array.isArray(data)) data = data.join('')
92
+ // RFC 1035 §3.3.14: TXT rdata is one or more <character-string>s, each up
93
+ // to 255 bytes. An array preserves explicit boundaries between strings;
94
+ // each element MUST be <= 255 UTF-8 bytes, otherwise we would silently
95
+ // split it and the boundary the caller asked us to preserve would be lost.
96
+ // A single string is auto-chunked at 255-byte UTF-8 boundaries.
97
+ const data = this.get('data')
98
+ if (Array.isArray(data)) {
99
+ const enc = new TextEncoder()
100
+ const buffers = data.map((s, i) => {
101
+ if (enc.encode(s).length > 255) {
102
+ this.throwHelp(
103
+ `TXT: array element ${i} exceeds 255 bytes; split it yourself or pass a single string to auto-chunk`,
104
+ )
105
+ }
106
+ return packStringWire(s)
107
+ })
108
+ const total = buffers.reduce((n, b) => n + b.length, 0)
109
+ const out = new Uint8Array(total)
110
+ let off = 0
111
+ for (const b of buffers) {
112
+ out.set(b, off)
113
+ off += b.length
114
+ }
115
+ return out
116
+ }
118
117
  return packStringWire(data)
119
118
  }
120
119
 
@@ -127,38 +126,48 @@ export default class TXT extends RR {
127
126
  }
128
127
 
129
128
  function asQuotedStrings(data) {
130
- // BIND croaks when any string in the TXT RR data is longer than 255
129
+ // RFC 1035 character-strings are 255 bytes max; chunk by UTF-8 bytes,
130
+ // not JS chars, so non-ASCII TXT data doesn't overflow the 255-byte limit.
131
+ const enc = new TextEncoder()
132
+
131
133
  if (Array.isArray(data)) {
132
- let hasTooLong = false
133
- for (const str of data) {
134
- if (str.length > 255) hasTooLong = true
135
- }
136
- return hasTooLong
137
- ? data
138
- .join('')
139
- .match(/(.{1,255})/g)
140
- .join('" "')
141
- : data.join('" "')
134
+ const anyTooLong = data.some((s) => enc.encode(s).length > 255)
135
+ if (!anyTooLong) return data.join('" "')
136
+ return chunkByBytes(data.join(''), 255).join('" "')
142
137
  }
143
138
 
144
- if (data.length > 255) {
145
- return data.match(/(.{1,255})/g).join('" "')
146
- }
139
+ if (enc.encode(data).length <= 255) return data
140
+ return chunkByBytes(data, 255).join('" "')
141
+ }
147
142
 
148
- return data
143
+ function chunkByBytes(str, maxBytes) {
144
+ const bytes = new TextEncoder().encode(str)
145
+ const dec = new TextDecoder()
146
+ const chunks = []
147
+ let start = 0
148
+ while (start < bytes.length) {
149
+ let end = Math.min(start + maxBytes, bytes.length)
150
+ // back up to a UTF-8 codepoint boundary so decode() returns whole chars
151
+ while (end < bytes.length && (bytes[end] & 0xc0) === 0x80) end--
152
+ chunks.push(dec.decode(bytes.subarray(start, end)))
153
+ start = end
154
+ }
155
+ return chunks
149
156
  }
150
157
 
151
158
  function packStringWire(str) {
152
- const parts = str.match(/(.{1,255})/g)
153
- let len = 0
154
- for (const part of parts) len += part.length + 1
159
+ const encoded = new TextEncoder().encode(str)
160
+ if (encoded.length === 0) return new Uint8Array([0])
161
+
162
+ const chunks = []
163
+ for (let i = 0; i < encoded.length; i += 255) chunks.push(encoded.subarray(i, i + 255))
155
164
 
156
- const buf = Buffer.allocUnsafe(len)
165
+ const buf = new Uint8Array(encoded.length + chunks.length)
157
166
  let offset = 0
158
- for (const part of parts) {
159
- buf.writeUInt8(part.length, offset++)
160
- buf.write(part, offset, 'ascii')
161
- offset += part.length
167
+ for (const chunk of chunks) {
168
+ buf[offset++] = chunk.length
169
+ buf.set(chunk, offset)
170
+ offset += chunk.length
162
171
  }
163
172
  return buf
164
173
  }
package/rr/uri.js CHANGED
@@ -3,6 +3,15 @@ import RR from '../rr.js'
3
3
  import * as TINYDNS from '../lib/tinydns.js'
4
4
 
5
5
  export default class URI extends RR {
6
+ static typeName = 'URI'
7
+ static typeId = 256
8
+ static RFCs = [7553]
9
+ static rdataFields = [
10
+ ['priority', 'u16'],
11
+ ['weight', 'u16'],
12
+ ['target', 'qstr'],
13
+ ]
14
+
6
15
  constructor(opts) {
7
16
  super(opts)
8
17
  }
@@ -44,37 +53,11 @@ export default class URI extends RR {
44
53
  })
45
54
  }
46
55
 
47
- fromBind({ bindline }) {
48
- // test.example.com 3600 IN URI priority, weight, target
49
- const [owner, ttl, c, type, priority, weight, target] = bindline.split(/\s+/)
50
- return new URI({
51
- class: c,
52
- type: type,
53
- owner,
54
- priority: parseInt(priority, 10),
55
- weight: parseInt(weight, 10),
56
- target: target.replace(/^"|"$/g, ''),
57
- ttl: parseInt(ttl, 10),
58
- })
59
- }
60
-
61
56
  /****** MISC *******/
62
57
  getDescription() {
63
58
  return 'URI'
64
59
  }
65
60
 
66
- getRdataFields(arg) {
67
- return ['priority', 'weight', 'target']
68
- }
69
-
70
- getRFCs() {
71
- return [7553]
72
- }
73
-
74
- getTypeId() {
75
- return 256
76
- }
77
-
78
61
  getCanonical() {
79
62
  return {
80
63
  owner: 'www.example.com.',
@@ -87,11 +70,18 @@ export default class URI extends RR {
87
70
  }
88
71
  }
89
72
 
90
- getQuotedFields() {
91
- return ['target']
73
+ /****** EXPORTERS *******/
74
+
75
+ getWireRdata() {
76
+ const target = new TextEncoder().encode(this.get('target'))
77
+ const result = new Uint8Array(4 + target.length)
78
+ const dv = new DataView(result.buffer)
79
+ dv.setUint16(0, this.get('priority'))
80
+ dv.setUint16(2, this.get('weight'))
81
+ result.set(target, 4)
82
+ return result
92
83
  }
93
84
 
94
- /****** EXPORTERS *******/
95
85
  toTinydns() {
96
86
  const dataRe = new RegExp(/[\r\n\t:\\/]/, 'g')
97
87
  let rdata = ''
package/rr/wks.js CHANGED
@@ -2,7 +2,102 @@ import RR from '../rr.js'
2
2
 
3
3
  import * as TINYDNS from '../lib/tinydns.js'
4
4
 
5
+ const WELL_KNOWN_PORTS = {
6
+ echo: 7,
7
+ discard: 9,
8
+ systat: 11,
9
+ daytime: 13,
10
+ netstat: 15,
11
+ ftp_data: 20,
12
+ ftp: 21,
13
+ ssh: 22,
14
+ telnet: 23,
15
+ smtp: 25,
16
+ time: 37,
17
+ rlp: 39,
18
+ nameserver: 42,
19
+ nicname: 43,
20
+ domain: 53,
21
+ mtp: 57,
22
+ bootps: 67,
23
+ bootpc: 68,
24
+ tftp: 69,
25
+ gopher: 70,
26
+ rje: 77,
27
+ finger: 79,
28
+ http: 80,
29
+ link: 87,
30
+ supdup: 95,
31
+ hostnames: 101,
32
+ iso_tsap: 102,
33
+ csnet_ns: 105,
34
+ pop_2: 109,
35
+ pop3: 110,
36
+ sunrpc: 111,
37
+ auth: 113,
38
+ sftp: 115,
39
+ uucp_path: 117,
40
+ nntp: 119,
41
+ ntp: 123,
42
+ netbios_ns: 137,
43
+ netbios_dgm: 138,
44
+ netbios_ssn: 139,
45
+ imap: 143,
46
+ sql_net: 150,
47
+ snmp: 161,
48
+ snmp_trap: 162,
49
+ cmip_man: 163,
50
+ cmip_agent: 164,
51
+ xdmcp: 177,
52
+ nextstep: 178,
53
+ bgp: 179,
54
+ prospero: 191,
55
+ irc: 194,
56
+ smux: 199,
57
+ at_rtmp: 201,
58
+ at_nbp: 202,
59
+ at_echo: 204,
60
+ at_zis: 206,
61
+ qmtp: 209,
62
+ z3950: 210,
63
+ ipx: 213,
64
+ imap3: 220,
65
+ ulistproc: 372,
66
+ https: 443,
67
+ snpp: 444,
68
+ microsoft_ds: 445,
69
+ kpasswd: 464,
70
+ urd: 465,
71
+ saft: 487,
72
+ isakmp: 500,
73
+ exec: 512,
74
+ biff: 512,
75
+ login: 513,
76
+ who: 513,
77
+ cmd: 514,
78
+ syslog: 514,
79
+ printer: 515,
80
+ talk: 517,
81
+ ntalk: 518,
82
+ route: 520,
83
+ timed: 525,
84
+ tempo: 526,
85
+ courier: 530,
86
+ netnews: 532,
87
+ netwall: 533,
88
+ uucp: 540,
89
+ remotefs: 556,
90
+ nntps: 563,
91
+ ldap: 389,
92
+ }
93
+
5
94
  export default class WKS extends RR {
95
+ static typeName = 'WKS'
96
+ static typeId = 11
97
+ static RFCs = [883, 1035]
98
+ static rdataFields = ['address', 'protocol', 'bit map']
99
+ static tags = ['obsolete']
100
+
6
101
  constructor(opts) {
7
102
  super(opts)
8
103
  }
@@ -29,22 +124,6 @@ export default class WKS extends RR {
29
124
  return 'Well Known Service'
30
125
  }
31
126
 
32
- getTags() {
33
- return ['obsolete']
34
- }
35
-
36
- getRdataFields(arg) {
37
- return ['address', 'protocol', 'bit map']
38
- }
39
-
40
- getRFCs() {
41
- return [883, 1035]
42
- }
43
-
44
- getTypeId() {
45
- return 11
46
- }
47
-
48
127
  getCanonical() {
49
128
  return {
50
129
  owner: 'host.example.com.',
@@ -77,14 +156,12 @@ export default class WKS extends RR {
77
156
  fromTinydns({ tinyline }) {
78
157
  const [owner, _typeId, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
79
158
 
80
- const binary = Buffer.from(TINYDNS.octalToChar(rdata), 'binary')
81
- const address = [binary.readUInt8(0), binary.readUInt8(1), binary.readUInt8(2), binary.readUInt8(3)].join(
82
- '.',
83
- )
84
- const protoNum = binary.readUInt8(4)
159
+ const binary = Uint8Array.from(TINYDNS.octalToChar(rdata), (c) => c.charCodeAt(0))
160
+ const address = [binary[0], binary[1], binary[2], binary[3]].join('.')
161
+ const protoNum = binary[4]
85
162
  const protoMap = { 6: 'TCP', 17: 'UDP' }
86
163
  const protocol = protoMap[protoNum] ?? protoNum
87
- const bitmap = binary.slice(5).toString()
164
+ const bitmap = new TextDecoder().decode(binary.subarray(5))
88
165
 
89
166
  return new WKS({
90
167
  owner: this.fullyQualify(owner),
@@ -98,6 +175,33 @@ export default class WKS extends RR {
98
175
  })
99
176
  }
100
177
 
178
+ fromWire({ owner, cls, ttl, rdata }) {
179
+ const address = [...rdata.subarray(0, 4)].join('.')
180
+ const protoNum = rdata[4]
181
+ const protoMap = { 6: 'TCP', 17: 'UDP' }
182
+ const protocol = protoMap[protoNum] ?? String(protoNum)
183
+ const PORT_NAMES = Object.fromEntries(Object.entries(WELL_KNOWN_PORTS).map(([k, v]) => [v, k]))
184
+ const bitmap = rdata.subarray(5)
185
+ const ports = []
186
+ for (let i = 0; i < bitmap.length; i++) {
187
+ for (let bit = 0; bit < 8; bit++) {
188
+ if (bitmap[i] & (0x80 >> bit)) {
189
+ const port = i * 8 + bit
190
+ ports.push(PORT_NAMES[port] ?? String(port))
191
+ }
192
+ }
193
+ }
194
+ return new WKS({
195
+ owner,
196
+ ttl,
197
+ class: cls,
198
+ type: 'WKS',
199
+ address,
200
+ protocol,
201
+ 'bit map': ports.join(' '),
202
+ })
203
+ }
204
+
101
205
  /****** EXPORTERS *******/
102
206
 
103
207
  toTinydns() {
@@ -111,4 +215,32 @@ export default class WKS extends RR {
111
215
  TINYDNS.escapeOctal(dataRe, this.get('bit map')),
112
216
  )
113
217
  }
218
+
219
+ getWireRdata() {
220
+ const protoMap = { TCP: 6, UDP: 17, 6: 6, 17: 17 }
221
+ const addrBytes = this.get('address').split('.').map(Number)
222
+ const protoNum = protoMap[this.get('protocol')]
223
+
224
+ const portNums = this.get('bit map')
225
+ .trim()
226
+ .split(/\s+/)
227
+ .map((s) => {
228
+ if (/^\d+$/.test(s)) return parseInt(s, 10)
229
+ return WELL_KNOWN_PORTS[s.toLowerCase()]
230
+ })
231
+ .filter((p) => p !== undefined)
232
+
233
+ if (portNums.length === 0) return new Uint8Array([...addrBytes, protoNum])
234
+
235
+ const maxPort = Math.max(...portNums)
236
+ const bitmapLen = Math.floor(maxPort / 8) + 1
237
+ const bitmap = new Uint8Array(bitmapLen)
238
+ for (const port of portNums) bitmap[Math.floor(port / 8)] |= 0x80 >> (port % 8)
239
+
240
+ const result = new Uint8Array(5 + bitmapLen)
241
+ result.set(addrBytes)
242
+ result[4] = protoNum
243
+ result.set(bitmap, 5)
244
+ return result
245
+ }
114
246
  }