@nictool/dns-resource-record 1.4.0 → 1.6.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.
package/rr/key.js CHANGED
@@ -63,11 +63,24 @@ export default class KEY extends RR {
63
63
  return 25
64
64
  }
65
65
 
66
+ getCanonical() {
67
+ return {
68
+ owner: 'example.com.',
69
+ ttl: 3600,
70
+ class: 'IN',
71
+ type: 'KEY',
72
+ flags: 256,
73
+ protocol: 3,
74
+ algorithm: 5,
75
+ publickey: 'AQPSKAsj8...',
76
+ }
77
+ }
78
+
66
79
  /****** IMPORTERS *******/
67
80
 
68
- fromBind(opts) {
81
+ fromBind({ bindline }) {
69
82
  // test.example.com 3600 IN KEY Flags Protocol Algorithm PublicKey
70
- const [owner, ttl, c, type, flags, protocol, algorithm] = opts.bindline.split(/\s+/)
83
+ const [owner, ttl, c, type, flags, protocol, algorithm] = bindline.split(/\s+/)
71
84
  return new KEY({
72
85
  owner,
73
86
  ttl: parseInt(ttl, 10),
@@ -76,31 +89,27 @@ export default class KEY extends RR {
76
89
  flags: parseInt(flags, 10),
77
90
  protocol: parseInt(protocol, 10),
78
91
  algorithm: parseInt(algorithm, 10),
79
- publickey: opts.bindline.split(/\s+/).slice(7).join(' ').trim(),
92
+ publickey: bindline.split(/\s+/).slice(7).join(' ').trim(),
80
93
  })
81
94
  }
82
95
 
83
- fromTinydns(opts) {
84
- const rd = opts.rd
85
-
96
+ fromTinydns({ tinyline }) {
86
97
  // RDATA format: Flags (6 octal chars) + Protocol (3 octal chars) + Algorithm (3 octal chars) + Public Key (escaped data)
98
+ const [owner, _typeId, rd, ttl, ts, loc] = tinyline.slice(1).split(':')
87
99
  if (rd.length < 12) {
88
100
  this.throwHelp(`KEY: RDATA too short: ${rd}`)
89
101
  }
90
102
 
91
- const flags = TINYDNS.octalToUInt16(rd.substring(0, 6))
92
- const protocol = TINYDNS.octalToUInt8(rd.substring(6, 9))
93
- const algorithm = TINYDNS.octalToUInt8(rd.substring(9, 12))
94
- const publicKeyData = TINYDNS.unescapeOctal(rd.substring(12))
95
-
96
103
  return new KEY({
97
- owner: this.fullyQualify(opts.owner),
98
- ttl: parseInt(opts.ttl, 10),
104
+ owner: this.fullyQualify(owner),
105
+ ttl: parseInt(ttl, 10),
99
106
  type: 'KEY',
100
- flags: flags,
101
- protocol: protocol,
102
- algorithm: algorithm,
103
- publickey: publicKeyData,
107
+ flags: TINYDNS.octalToUInt16(rd.slice(0, 6)),
108
+ protocol: TINYDNS.octalToUInt8(rd.slice(6, 9)),
109
+ algorithm: TINYDNS.octalToUInt8(rd.slice(9, 12)),
110
+ publickey: TINYDNS.unescapeOctal(rd.slice(12)),
111
+ timestamp: ts,
112
+ location: loc?.trim() ?? '',
104
113
  })
105
114
  }
106
115
 
package/rr/kx.js CHANGED
@@ -51,9 +51,25 @@ export default class KX extends RR {
51
51
  }
52
52
 
53
53
  /****** IMPORTERS *******/
54
- fromBind(opts) {
54
+ fromTinydns({ tinyline }) {
55
+ // KX via generic, :fqdn:36:rdata:ttl:timestamp:lo
56
+ const [fqdn, n, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
57
+ if (n != 36) this.throwHelp('KX fromTinydns, invalid n')
58
+
59
+ return new KX({
60
+ owner: this.fullyQualify(fqdn),
61
+ ttl: parseInt(ttl, 10),
62
+ type: 'KX',
63
+ preference: TINYDNS.octalToUInt16(rdata.slice(0, 8)),
64
+ exchanger: TINYDNS.unpackDomainName(rdata.slice(8))[0],
65
+ timestamp: ts,
66
+ location: loc?.trim() ?? '',
67
+ })
68
+ }
69
+
70
+ fromBind({ bindline }) {
55
71
  // test.example.com 3600 IN KX preference exchanger
56
- const [owner, ttl, c, type, preference, exchanger] = opts.bindline.split(/\s+/)
72
+ const [owner, ttl, c, type, preference, exchanger] = bindline.split(/\s+/)
57
73
  return new KX({
58
74
  owner,
59
75
  ttl: parseInt(ttl, 10),
package/rr/loc.js CHANGED
@@ -48,6 +48,16 @@ export default class LOC extends RR {
48
48
  return 29
49
49
  }
50
50
 
51
+ getCanonical() {
52
+ return {
53
+ owner: 'example.com.',
54
+ ttl: 3600,
55
+ class: 'IN',
56
+ type: 'LOC',
57
+ address: '52 22 23.000 N 4 53 32.000 E 10m 100m 10m 2m',
58
+ }
59
+ }
60
+
51
61
  parseLoc(string) {
52
62
  // d1 [m1 [s1]]
53
63
  const dms = '(\\d+)\\s+(?:(\\d+)\\s+)?(?:([\\d.]+)\\s+)?'
@@ -85,22 +95,22 @@ export default class LOC extends RR {
85
95
  }
86
96
 
87
97
  /****** IMPORTERS *******/
88
- fromTinydns(opts) {
98
+ fromTinydns({ tinyline }) {
89
99
  // LOC via generic, :fqdn:n:rdata:ttl:timestamp:lo
90
- const [fqdn, n, rdata, ttl, ts, loc] = opts.tinyline.substring(1).split(':')
100
+ const [fqdn, n, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
91
101
  if (n != 29) this.throwHelp('LOC fromTinydns, invalid n')
92
102
 
93
103
  // divide by 100 is to convert cm to meters
94
104
  const l = {
95
- version: TINYDNS.octalToUInt8(rdata.substring(0, 4)),
96
- size: this.fromExponent(TINYDNS.octalToUInt8(rdata.substring(4, 8))),
105
+ version: TINYDNS.octalToUInt8(rdata.slice(0, 4)),
106
+ size: this.fromExponent(TINYDNS.octalToUInt8(rdata.slice(4, 8))),
97
107
  precision: {
98
- horizontal: this.fromExponent(TINYDNS.octalToUInt8(rdata.substring(8, 12))),
99
- vertical: this.fromExponent(TINYDNS.octalToUInt8(rdata.substring(12, 16))),
108
+ horizontal: this.fromExponent(TINYDNS.octalToUInt8(rdata.slice(8, 12))),
109
+ vertical: this.fromExponent(TINYDNS.octalToUInt8(rdata.slice(12, 16))),
100
110
  },
101
- latitude: this.arcSecToDMS(TINYDNS.octalToUInt32(rdata.substring(16, 32)), 'lat'),
102
- longitude: this.arcSecToDMS(TINYDNS.octalToUInt32(rdata.substring(32, 48)), 'lon'),
103
- altitude: TINYDNS.octalToUInt32(rdata.substring(48, 64)) - REF.ALTITUDE,
111
+ latitude: this.arcSecToDMS(TINYDNS.octalToUInt32(rdata.slice(16, 32)), 'lat'),
112
+ longitude: this.arcSecToDMS(TINYDNS.octalToUInt32(rdata.slice(32, 48)), 'lon'),
113
+ altitude: TINYDNS.octalToUInt32(rdata.slice(48, 64)) - REF.ALTITUDE,
104
114
  }
105
115
 
106
116
  return new LOC({
@@ -109,24 +119,24 @@ export default class LOC extends RR {
109
119
  address: this.toHuman(l),
110
120
  ttl: parseInt(ttl, 10),
111
121
  timestamp: ts,
112
- location: loc !== '' && loc !== '\n' ? loc : '',
122
+ location: loc?.trim() ?? '',
113
123
  })
114
124
  }
115
125
 
116
- fromBind(opts) {
117
- const [owner, ttl, c, type] = opts.bindline.split(/\s+/)
126
+ fromBind({ bindline }) {
127
+ const [owner, ttl, c, type] = bindline.split(/\s+/)
118
128
 
119
129
  return new LOC({
120
130
  owner,
121
131
  ttl: parseInt(ttl, 10),
122
132
  class: c,
123
133
  type: type,
124
- address: opts.bindline.split(/\s+/).slice(4).join(' ').trim(),
134
+ address: bindline.split(/\s+/).slice(4).join(' ').trim(),
125
135
  })
126
136
  }
127
137
 
128
138
  dmsToArcSec(obj) {
129
- let retval = obj.degrees * CONV.deg + (obj.minutes || 0) * CONV.min + (obj.seconds || 0) * CONV.sec
139
+ let retval = obj.degrees * CONV.deg + (obj.minutes ?? 0) * CONV.min + (obj.seconds ?? 0) * CONV.sec
130
140
  switch (obj.hemisphere.toUpperCase()) {
131
141
  case 'W':
132
142
  case 'S':
package/rr/mx.js CHANGED
@@ -29,6 +29,10 @@ export default class MX extends RR {
29
29
  return 'Mail Exchanger'
30
30
  }
31
31
 
32
+ getTags() {
33
+ return ['common']
34
+ }
35
+
32
36
  getRdataFields(arg) {
33
37
  return ['preference', 'exchange']
34
38
  }
@@ -47,16 +51,16 @@ export default class MX extends RR {
47
51
  ttl: 43200,
48
52
  class: 'IN',
49
53
  type: 'MX',
50
- preference: 0,
54
+ preference: 10,
51
55
  exchange: 'mail.example.com.',
52
56
  }
53
57
  }
54
58
 
55
59
  /****** IMPORTERS *******/
56
- fromTinydns(opts) {
60
+ fromTinydns({ tinyline }) {
57
61
  // @fqdn:ip:x:dist:ttl:timestamp:lo
58
62
  // eslint-disable-next-line no-unused-vars
59
- const [owner, ip, x, preference, ttl, ts, loc] = opts.tinyline.substring(1).split(':')
63
+ const [owner, ip, x, preference, ttl, ts, loc] = tinyline.slice(1).split(':')
60
64
 
61
65
  return new MX({
62
66
  type: 'MX',
@@ -65,13 +69,13 @@ export default class MX extends RR {
65
69
  preference: parseInt(preference, 10) || 0,
66
70
  ttl: parseInt(ttl, 10),
67
71
  timestamp: ts,
68
- location: loc !== '' && loc !== '\n' ? loc : '',
72
+ location: loc?.trim() ?? '',
69
73
  })
70
74
  }
71
75
 
72
- fromBind(opts) {
76
+ fromBind({ bindline }) {
73
77
  // test.example.com 3600 IN MX preference exchange
74
- const [owner, ttl, c, type, preference, exchange] = opts.bindline.split(/\s+/)
78
+ const [owner, ttl, c, type, preference, exchange] = bindline.split(/\s+/)
75
79
 
76
80
  return new MX({
77
81
  owner,
@@ -84,6 +88,12 @@ export default class MX extends RR {
84
88
  }
85
89
 
86
90
  /****** EXPORTERS *******/
91
+ getWireRdata() {
92
+ const pref = Buffer.alloc(2)
93
+ pref.writeUInt16BE(this.get('preference'))
94
+ return Buffer.concat([pref, this.wirePackDomain(this.get('exchange'))])
95
+ }
96
+
87
97
  toBind(zone_opts) {
88
98
  return `${this.getPrefix(zone_opts)}\t${this.get('preference')}\t${this.getFQDN('exchange', zone_opts)}\n`
89
99
  }
package/rr/naptr.js CHANGED
@@ -21,13 +21,28 @@ export default class NAPTR extends RR {
21
21
  }
22
22
 
23
23
  getRFCs() {
24
- return [2915, 3403]
24
+ return [2915, 3403, 4848]
25
25
  }
26
26
 
27
27
  getTypeId() {
28
28
  return 35
29
29
  }
30
30
 
31
+ getCanonical() {
32
+ return {
33
+ owner: 'cid.urn.arpa.',
34
+ ttl: 3600,
35
+ class: 'IN',
36
+ type: 'NAPTR',
37
+ order: 100,
38
+ preference: 10,
39
+ flags: 'S',
40
+ service: 'z3950+N2L+N2R',
41
+ regexp: '',
42
+ replacement: 'gatekeeper.example.com.',
43
+ }
44
+ }
45
+
31
46
  /****** Resource record specific setters *******/
32
47
  setOrder(val) {
33
48
  this.is16bitInt('NAPTR', 'order', val)
@@ -62,9 +77,9 @@ export default class NAPTR extends RR {
62
77
  }
63
78
 
64
79
  /****** IMPORTERS *******/
65
- fromTinydns(opts) {
80
+ fromTinydns({ tinyline }) {
66
81
  // NAPTR via generic, :fqdn:n:rdata:ttl:timestamp:lo
67
- const [fqdn, n, rdata, ttl, ts, loc] = opts.tinyline.substring(1).split(':')
82
+ const [fqdn, n, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
68
83
  if (n != 35) this.throwHelp('NAPTR fromTinydns, invalid n')
69
84
 
70
85
  const binRdata = Buffer.from(TINYDNS.octalToChar(rdata), 'binary')
@@ -74,7 +89,7 @@ export default class NAPTR extends RR {
74
89
  owner: this.fullyQualify(fqdn),
75
90
  ttl: parseInt(ttl, 10),
76
91
  timestamp: ts,
77
- location: loc !== '' && loc !== '\n' ? loc : '',
92
+ location: loc?.trim() ?? '',
78
93
  order: binRdata.readUInt16BE(0, 2),
79
94
  preference: binRdata.readUInt16BE(2, 4),
80
95
  }
@@ -102,26 +117,30 @@ export default class NAPTR extends RR {
102
117
  return new NAPTR(rec)
103
118
  }
104
119
 
105
- fromBind(opts) {
106
- const str = opts.bindline
107
- // test.example.com 3600 IN NAPTR order, preference, "flags", "service", "regexp", replacement
108
- const [owner, ttl, c, type, order, preference] = str.split(/\s+/)
109
- const [flags, service, regexp] = str.match(/(?:").*?(?:"\s)/g)
110
- const replacement = str.trim().split(/\s+/).pop()
120
+ fromBind({ bindline }) {
121
+ const regex =
122
+ /^(?<owner>\S+)\s+(?<ttl>\d+)\s+(?<class>\S+)\s+(?<type>NAPTR)\s+(?<order>\d+)\s+(?<preference>\d+)\s+["'](?<flags>[^"']*)["']\s+["'](?<service>[^"']*)["']\s+["'](?<regexp>[^"']*)["']\s+(?<replacement>\S+)$/
123
+
124
+ const match = bindline.trim().match(regex)
111
125
 
112
- const bits = {
113
- owner: owner,
126
+ if (!match) {
127
+ throw new Error(`Invalid NAPTR BIND line: ${bindline}`)
128
+ }
129
+
130
+ const { owner, ttl, type, order, preference, flags, service, regexp, replacement } = match.groups
131
+
132
+ return new NAPTR({
133
+ owner: this.fullyQualify(owner),
114
134
  ttl: parseInt(ttl, 10),
115
- class: c,
116
- type: type,
135
+ class: match.groups.class,
136
+ type,
117
137
  order: parseInt(order, 10),
118
138
  preference: parseInt(preference, 10),
119
- flags: flags.trim().replace(/^['"]|['"]$/g, ''),
120
- service: service.trim().replace(/^['"]|['"]$/g, ''),
121
- regexp: regexp.trim().replace(/^['"]|['"]/g, ''),
122
- replacement: replacement,
123
- }
124
- return new NAPTR(bits)
139
+ flags,
140
+ service,
141
+ regexp,
142
+ replacement,
143
+ })
125
144
  }
126
145
 
127
146
  /****** EXPORTERS *******/
package/rr/ns.js CHANGED
@@ -21,6 +21,10 @@ export default class NS extends RR {
21
21
  return 'Name Server'
22
22
  }
23
23
 
24
+ getTags() {
25
+ return ['common']
26
+ }
27
+
24
28
  getRdataFields(arg) {
25
29
  return ['dname']
26
30
  }
@@ -33,11 +37,21 @@ export default class NS extends RR {
33
37
  return 2
34
38
  }
35
39
 
40
+ getCanonical() {
41
+ return {
42
+ owner: 'example.com.',
43
+ ttl: 3600,
44
+ class: 'IN',
45
+ type: 'NS',
46
+ dname: 'ns1.example.com.',
47
+ }
48
+ }
49
+
36
50
  /****** IMPORTERS *******/
37
- fromTinydns(opts) {
51
+ fromTinydns({ tinyline }) {
38
52
  // &fqdn:ip:x:ttl:timestamp:lo
39
53
  // eslint-disable-next-line no-unused-vars
40
- const [fqdn, ip, dname, ttl, ts, loc] = opts.tinyline.substring(1).split(':')
54
+ const [fqdn, ip, dname, ttl, ts, loc] = tinyline.slice(1).split(':')
41
55
 
42
56
  return new NS({
43
57
  type: 'NS',
@@ -45,13 +59,13 @@ export default class NS extends RR {
45
59
  dname: this.fullyQualify(/\./.test(dname) ? dname : `${dname}.ns.${fqdn}`),
46
60
  ttl: parseInt(ttl, 10),
47
61
  timestamp: ts,
48
- location: loc !== '' && loc !== '\n' ? loc : '',
62
+ location: loc?.trim() ?? '',
49
63
  })
50
64
  }
51
65
 
52
- fromBind(opts) {
66
+ fromBind({ bindline }) {
53
67
  // test.example.com 3600 IN NS dname
54
- const [owner, ttl, c, type, dname] = opts.bindline.split(/\s+/)
68
+ const [owner, ttl, c, type, dname] = bindline.split(/\s+/)
55
69
 
56
70
  return new NS({
57
71
  owner,
@@ -63,6 +77,10 @@ export default class NS extends RR {
63
77
  }
64
78
 
65
79
  /****** EXPORTERS *******/
80
+ getWireRdata() {
81
+ return this.wirePackDomain(this.get('dname'))
82
+ }
83
+
66
84
  toBind(zone_opts) {
67
85
  return `${this.getPrefix(zone_opts)}\t${this.getFQDN('dname', zone_opts)}\n`
68
86
  }
package/rr/nsec.js CHANGED
@@ -29,6 +29,10 @@ export default class NSEC extends RR {
29
29
  return 'Next Secure'
30
30
  }
31
31
 
32
+ getTags() {
33
+ return ['dnssec']
34
+ }
35
+
32
36
  getRdataFields(arg) {
33
37
  return ['next domain', 'type bit maps']
34
38
  }
@@ -41,10 +45,21 @@ export default class NSEC extends RR {
41
45
  return 47
42
46
  }
43
47
 
48
+ getCanonical() {
49
+ return {
50
+ owner: 'alfa.example.com.',
51
+ ttl: 3600,
52
+ class: 'IN',
53
+ type: 'NSEC',
54
+ 'next domain': 'host.example.com.',
55
+ 'type bit maps': 'A MX RRSIG NSEC TYPE1234',
56
+ }
57
+ }
58
+
44
59
  /****** IMPORTERS *******/
45
60
 
46
- fromTinydns(opts) {
47
- const [owner, _typeId, rdata, ttl, ts, loc] = opts.tinyline.substring(1).split(':')
61
+ fromTinydns({ tinyline }) {
62
+ const [owner, _typeId, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
48
63
  const binaryRdata = Buffer.from(TINYDNS.octalToChar(rdata), 'binary')
49
64
  const [nextDomain, _escapedLen, binaryLen] = TINYDNS.unpackDomainName(rdata)
50
65
 
@@ -55,20 +70,20 @@ export default class NSEC extends RR {
55
70
  'next domain': nextDomain,
56
71
  'type bit maps': binaryRdata.slice(binaryLen).toString(),
57
72
  timestamp: ts,
58
- location: loc !== '' && loc !== '\n' ? loc : '',
73
+ location: loc?.trim() ?? '',
59
74
  })
60
75
  }
61
76
 
62
- fromBind(opts) {
77
+ fromBind({ bindline }) {
63
78
  // test.example.com 3600 IN NSEC NextDomain TypeBitMaps
64
- const [owner, ttl, c, type, next] = opts.bindline.split(/\s+/)
79
+ const [owner, ttl, c, type, next] = bindline.split(/\s+/)
65
80
  return new NSEC({
66
81
  owner,
67
82
  ttl: parseInt(ttl, 10),
68
83
  class: c,
69
84
  type: type,
70
85
  'next domain': next,
71
- 'type bit maps': opts.bindline.split(/\s+/).slice(5).filter(removeParens).join(' ').trim(),
86
+ 'type bit maps': bindline.split(/\s+/).slice(5).filter(removeParens).join(' ').trim(),
72
87
  })
73
88
  }
74
89
 
package/rr/nsec3.js CHANGED
@@ -64,6 +64,10 @@ export default class NSEC3 extends RR {
64
64
  return 'Next Secure'
65
65
  }
66
66
 
67
+ getTags() {
68
+ return ['dnssec']
69
+ }
70
+
67
71
  getRdataFields(arg) {
68
72
  return ['hash algorithm', 'flags', 'iterations', 'salt', 'next hashed owner name', 'type bit maps']
69
73
  }
@@ -76,12 +80,27 @@ export default class NSEC3 extends RR {
76
80
  return 50
77
81
  }
78
82
 
83
+ getCanonical() {
84
+ return {
85
+ owner: 'test.example.com.',
86
+ ttl: 3600,
87
+ class: 'IN',
88
+ type: 'NSEC3',
89
+ 'hash algorithm': 1,
90
+ flags: 1,
91
+ iterations: 12,
92
+ salt: 'aabbccdd',
93
+ 'next hashed owner name': '2vptu5timamqttgl4luu9kg21e0aor3s',
94
+ 'type bit maps': 'A RRSIG',
95
+ }
96
+ }
97
+
79
98
  /****** IMPORTERS *******/
80
99
 
81
- fromBind(opts) {
100
+ fromBind({ bindline }) {
82
101
  // test.example.com. 3600 IN NSEC3 1 1 12 aabbccdd (2vptu5timamqttgl4luu9kg21e0aor3s A RRSIG)
83
- const [owner, ttl, c, type, ha, flags, iterations, salt] = opts.bindline.split(/\s+/)
84
- const rdata = opts.bindline.split(/\(|\)/)[1]
102
+ const [owner, ttl, c, type, ha, flags, iterations, salt] = bindline.split(/\s+/)
103
+ const rdata = bindline.split(/\(|\)/)[1]
85
104
 
86
105
  return new NSEC3({
87
106
  owner,
@@ -97,8 +116,8 @@ export default class NSEC3 extends RR {
97
116
  })
98
117
  }
99
118
 
100
- fromTinydns(opts) {
101
- const [fqdn, n, rdata, ttl, ts, loc] = opts.tinyline.substring(1).split(':')
119
+ fromTinydns({ tinyline }) {
120
+ const [fqdn, n, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
102
121
  if (n != 50) this.throwHelp('NSEC3 fromTinydns, invalid n')
103
122
 
104
123
  const bytes = Buffer.from(TINYDNS.octalToChar(rdata), 'binary')
@@ -112,7 +131,7 @@ export default class NSEC3 extends RR {
112
131
  // Salt (variable length based on Salt Length)
113
132
  // Next Hashed Owner Name (variable length)
114
133
  // Type Bit Maps (variable length)
115
- const { salt, nextHashedOwnerName, typeBitMaps } = TINYDNS.parseNSEC3Buffer(bytes)
134
+ const { salt, nextHashedOwnerName, typeBitMaps } = parseNSEC3Buffer(bytes)
116
135
 
117
136
  return new NSEC3({
118
137
  owner: this.fullyQualify(fqdn),
@@ -125,7 +144,7 @@ export default class NSEC3 extends RR {
125
144
  'next hashed owner name': nextHashedOwnerName,
126
145
  'type bit maps': typeBitMaps,
127
146
  timestamp: ts,
128
- location: loc !== '' && loc !== '\n' ? loc : '',
147
+ location: loc?.trim() ?? '',
129
148
  })
130
149
  }
131
150
 
@@ -155,3 +174,48 @@ export default class NSEC3 extends RR {
155
174
  )
156
175
  }
157
176
  }
177
+
178
+ function parseNSEC3Buffer(bytes) {
179
+ // bytes is a Buffer containing the full RDATA binary (hash alg, flags, iterations, then ASCII salt + next-hashed + type bit maps)
180
+ // Start after the first 4 bytes (hash alg, flags, iterations)
181
+ const rest = bytes.slice(4).toString('utf8')
182
+
183
+ // determine expected next hashed owner name length from hash algorithm
184
+ const hashAlgorithm = bytes.readUInt8(0)
185
+ // common mapping: algorithm 1 => SHA-1 => 20 bytes => base32 length 32
186
+ const expectedLen = hashAlgorithm === 1 ? 32 : hashAlgorithm === 2 ? 52 : 32
187
+
188
+ // salt length is ambiguous in this representation; try to find a split where
189
+ // the following segment matches expected base32 length
190
+ let salt = ''
191
+ let nextHashedOwnerName = ''
192
+ let typeBitMaps = ''
193
+
194
+ const maxSl = Math.min(64, rest.length)
195
+ for (let sl = maxSl; sl >= 1; sl--) {
196
+ const candNext = rest.slice(sl, sl + expectedLen)
197
+ if (candNext.length !== expectedLen) continue
198
+ if (!/^[0-9a-z]+$/.test(candNext)) continue
199
+ // candidate looks like a base32 name; accept and treat remainder as type bit maps
200
+ const saltCandidate = rest.slice(0, sl)
201
+ if (!/^[0-9A-Fa-f]+$/.test(saltCandidate)) continue
202
+ salt = saltCandidate
203
+ nextHashedOwnerName = candNext
204
+ typeBitMaps = rest.slice(sl + expectedLen)
205
+ break
206
+ }
207
+
208
+ // fallback: if we couldn't find a split, treat everything up to first non-hex as salt
209
+ if (!nextHashedOwnerName) {
210
+ const saltMatch = rest.match(/^([0-9A-Fa-f]*)/)
211
+ salt = saltMatch ? saltMatch[1] : ''
212
+ nextHashedOwnerName = rest.slice(salt.length)
213
+ typeBitMaps = ''
214
+ }
215
+
216
+ return {
217
+ salt,
218
+ nextHashedOwnerName,
219
+ typeBitMaps,
220
+ }
221
+ }