@nictool/dns-resource-record 1.6.1 → 1.7.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 (54) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +323 -199
  3. package/dist/dns-rr.min.js +26 -0
  4. package/dist/dns-rr.min.js.map +7 -0
  5. package/index.js +12 -11
  6. package/lib/binary.js +108 -0
  7. package/lib/bind.js +94 -0
  8. package/lib/dns-query.js +67 -0
  9. package/lib/tinydns.js +128 -44
  10. package/lib/wire.js +519 -0
  11. package/package.json +23 -6
  12. package/rr/TEMPLATE.js +124 -0
  13. package/rr/a.js +8 -48
  14. package/rr/aaaa.js +13 -80
  15. package/rr/apl.js +123 -27
  16. package/rr/caa.js +27 -50
  17. package/rr/cert.js +58 -87
  18. package/rr/cname.js +7 -63
  19. package/rr/dhcid.js +20 -30
  20. package/rr/dname.js +9 -36
  21. package/rr/dnskey.js +38 -32
  22. package/rr/ds.js +43 -57
  23. package/rr/hinfo.js +21 -40
  24. package/rr/hip.js +88 -26
  25. package/rr/https.js +28 -40
  26. package/rr/ipseckey.js +97 -18
  27. package/rr/key.js +39 -29
  28. package/rr/kx.js +16 -27
  29. package/rr/loc.js +42 -12
  30. package/rr/mx.js +16 -51
  31. package/rr/naptr.js +58 -33
  32. package/rr/ns.js +8 -36
  33. package/rr/nsec.js +89 -18
  34. package/rr/nsec3.js +62 -26
  35. package/rr/nsec3param.js +56 -53
  36. package/rr/nxt.js +57 -18
  37. package/rr/openpgpkey.js +24 -30
  38. package/rr/ptr.js +7 -47
  39. package/rr/rp.js +17 -32
  40. package/rr/rrsig.js +80 -85
  41. package/rr/sig.js +70 -45
  42. package/rr/smimea.js +33 -33
  43. package/rr/soa.js +31 -49
  44. package/rr/spf.js +10 -17
  45. package/rr/srv.js +22 -62
  46. package/rr/sshfp.js +25 -53
  47. package/rr/svcb.js +27 -40
  48. package/rr/tlsa.js +34 -34
  49. package/rr/tsig.js +87 -24
  50. package/rr/txt.js +40 -59
  51. package/rr/uri.js +19 -29
  52. package/rr/wks.js +154 -22
  53. package/rr.js +290 -109
  54. package/lib/readme.js +0 -84
package/index.js CHANGED
@@ -1,5 +1,3 @@
1
- const typeMap = {}
2
-
3
1
  import RR from './rr.js'
4
2
  import A from './rr/a.js'
5
3
  import AAAA from './rr/aaaa.js'
@@ -42,9 +40,8 @@ import TXT from './rr/txt.js'
42
40
  import URI from './rr/uri.js'
43
41
  import WKS from './rr/wks.js'
44
42
 
45
- export default RR
46
-
47
- export {
43
+ const typeMap = {}
44
+ const classes = [
48
45
  A,
49
46
  AAAA,
50
47
  APL,
@@ -85,10 +82,15 @@ export {
85
82
  TXT,
86
83
  URI,
87
84
  WKS,
88
- typeMap,
85
+ ]
86
+
87
+ for (const c of classes) {
88
+ const id = c.getTypeId()
89
+ typeMap[id] = c.typeName
90
+ typeMap[c.typeName] = id
89
91
  }
90
92
 
91
- for (const c of [
93
+ export {
92
94
  A,
93
95
  AAAA,
94
96
  APL,
@@ -129,8 +131,7 @@ for (const c of [
129
131
  TXT,
130
132
  URI,
131
133
  WKS,
132
- ]) {
133
- const id = new c(null).getTypeId()
134
- typeMap[id] = c.name
135
- typeMap[c.name] = id
134
+ typeMap,
136
135
  }
136
+
137
+ export default RR
package/lib/binary.js ADDED
@@ -0,0 +1,108 @@
1
+ export function hexToBytes(hex) {
2
+ return Uint8Array.from({ length: hex.length / 2 }, (_, i) => parseInt(hex.slice(i * 2, i * 2 + 2), 16))
3
+ }
4
+
5
+ export function bytesToHex(bytes) {
6
+ return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
7
+ }
8
+
9
+ export function bytesToBase64(bytes) {
10
+ return btoa([...bytes].map((b) => String.fromCharCode(b)).join(''))
11
+ }
12
+
13
+ export function base64ToBytes(b64) {
14
+ return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))
15
+ }
16
+
17
+ // DNS type name → numeric type ID mapping (subset covering known RR types)
18
+ export const DNS_TYPE_IDS = {
19
+ A: 1,
20
+ NS: 2,
21
+ MD: 3,
22
+ MF: 4,
23
+ CNAME: 5,
24
+ SOA: 6,
25
+ MB: 7,
26
+ MG: 8,
27
+ MR: 9,
28
+ NULL: 10,
29
+ WKS: 11,
30
+ PTR: 12,
31
+ HINFO: 13,
32
+ MINFO: 14,
33
+ MX: 15,
34
+ TXT: 16,
35
+ RP: 17,
36
+ AFSDB: 18,
37
+ X25: 19,
38
+ ISDN: 20,
39
+ RT: 21,
40
+ NSAP: 22,
41
+ NSAP_PTR: 23,
42
+ SIG: 24,
43
+ KEY: 25,
44
+ PX: 26,
45
+ GPOS: 27,
46
+ AAAA: 28,
47
+ LOC: 29,
48
+ NXT: 30,
49
+ EID: 31,
50
+ NIMLOC: 32,
51
+ SRV: 33,
52
+ ATMA: 34,
53
+ NAPTR: 35,
54
+ KX: 36,
55
+ CERT: 37,
56
+ A6: 38,
57
+ DNAME: 39,
58
+ SINK: 40,
59
+ OPT: 41,
60
+ APL: 42,
61
+ DS: 43,
62
+ SSHFP: 44,
63
+ IPSECKEY: 45,
64
+ RRSIG: 46,
65
+ NSEC: 47,
66
+ DNSKEY: 48,
67
+ DHCID: 49,
68
+ NSEC3: 50,
69
+ NSEC3PARAM: 51,
70
+ TLSA: 52,
71
+ SMIMEA: 53,
72
+ HIP: 55,
73
+ NINFO: 56,
74
+ RKEY: 57,
75
+ TALINK: 58,
76
+ CDS: 59,
77
+ CDNSKEY: 60,
78
+ OPENPGPKEY: 61,
79
+ CSYNC: 62,
80
+ ZONEMD: 63,
81
+ SVCB: 64,
82
+ HTTPS: 65,
83
+ SPF: 99,
84
+ UINFO: 100,
85
+ UID: 101,
86
+ GID: 102,
87
+ UNSPEC: 103,
88
+ NID: 104,
89
+ L32: 105,
90
+ L64: 106,
91
+ LP: 107,
92
+ EUI48: 108,
93
+ EUI64: 109,
94
+ TKEY: 249,
95
+ TSIG: 250,
96
+ IXFR: 251,
97
+ AXFR: 252,
98
+ MAILB: 253,
99
+ MAILA: 254,
100
+ ANY: 255,
101
+ URI: 256,
102
+ CAA: 257,
103
+ AVC: 258,
104
+ DOA: 259,
105
+ AMTRELAY: 260,
106
+ TA: 32768,
107
+ DLV: 32769,
108
+ }
package/lib/bind.js ADDED
@@ -0,0 +1,94 @@
1
+ export function parseBindLine(line, RRClasses) {
2
+ const res = {
3
+ class: 'IN',
4
+ type: '',
5
+ rdata: [],
6
+ }
7
+
8
+ // 1. Strip comments (not inside quotes)
9
+ let cleanLine = ''
10
+ let inQuote = false
11
+ for (let i = 0; i < line.length; i++) {
12
+ const c = line[i]
13
+ if (c === '"') inQuote = !inQuote
14
+ if (c === ';' && !inQuote) break
15
+ cleanLine += c
16
+ }
17
+ cleanLine = cleanLine.trim()
18
+ if (!cleanLine) return null
19
+
20
+ // 2. Tokenize, respecting quoted strings
21
+ const tokens = cleanLine.match(/(".*?"|\S+)/g) || []
22
+ if (tokens.length < 1) return null
23
+
24
+ // 3. Owner handling
25
+ if (!/^\s/.test(line)) {
26
+ res.owner = tokens.shift()
27
+ }
28
+
29
+ // 4. Extract TTL, Class, and Type
30
+ while (tokens.length > 0) {
31
+ const token = tokens[0].toUpperCase()
32
+
33
+ if (RRClasses[token]) {
34
+ res.class = tokens.shift().toUpperCase()
35
+ continue
36
+ }
37
+
38
+ if (/^\d+$/.test(token)) {
39
+ res.ttl = parseInt(tokens.shift(), 10)
40
+ continue
41
+ }
42
+
43
+ // If it's not a Class or TTL, it must be the RR Type (A, MX, etc.)
44
+ res.type = tokens.shift().toUpperCase()
45
+ break
46
+ }
47
+
48
+ // 5. Remaining tokens are RDATA
49
+ res.rdata = tokens
50
+
51
+ return res
52
+ }
53
+
54
+ export function fromBind(rrInstance, opts) {
55
+ const { owner, ttl, cls, rdata } = opts
56
+ const result = {
57
+ owner,
58
+ ttl,
59
+ class: cls,
60
+ type: rrInstance.constructor.typeName,
61
+ }
62
+
63
+ const fields = rrInstance.getFields('rdata')
64
+ for (let i = 0; i < fields.length; i++) {
65
+ const isLastField = i === fields.length - 1
66
+ if (isLastField && rrInstance.isQuotedField(fields[i])) {
67
+ // Collect all remaining tokens for TXT/etc
68
+ const val = rdata
69
+ .slice(i)
70
+ .map((s) => s.replace(/^"|"$/g, ''))
71
+ .join('')
72
+ result[fields[i]] = val
73
+ break
74
+ }
75
+
76
+ let val = rdata[i]
77
+ if (rrInstance.isQuotedField(fields[i])) {
78
+ val = val?.replace(/^"|"$/g, '')
79
+ } else if (/^\d+$/.test(val)) {
80
+ val = parseInt(val, 10)
81
+ }
82
+ result[fields[i]] = val
83
+ }
84
+
85
+ return new rrInstance.constructor(result)
86
+ }
87
+
88
+ export function toBind(rrInstance, zone_opts) {
89
+ const fields = rrInstance.getFields('rdata')
90
+ const rdata = fields
91
+ .map((f) => (rrInstance.isFqdnField(f) ? rrInstance.getFQDN(f, zone_opts) : rrInstance.getQuoted(f)))
92
+ .join('\t')
93
+ return `${rrInstance.getPrefix(zone_opts)}\t${rdata}\n`
94
+ }
@@ -0,0 +1,67 @@
1
+ import { createSocket } from 'node:dgram'
2
+ import { createConnection } from 'node:net'
3
+
4
+ export function dnsQueryUDP(host, port, queryBuf, timeout = 5000) {
5
+ return new Promise((resolve, reject) => {
6
+ const socket = createSocket('udp4')
7
+ const timer = setTimeout(() => {
8
+ socket.close()
9
+ reject(new Error(`DNS query timed out (${host}:${port})`))
10
+ }, timeout)
11
+ socket.on('message', (msg) => {
12
+ clearTimeout(timer)
13
+ socket.close()
14
+ resolve(msg)
15
+ })
16
+ socket.on('error', (err) => {
17
+ clearTimeout(timer)
18
+ socket.close()
19
+ reject(err)
20
+ })
21
+ socket.send(queryBuf, port, host, (err) => {
22
+ if (err) {
23
+ clearTimeout(timer)
24
+ socket.close()
25
+ reject(err)
26
+ }
27
+ })
28
+ })
29
+ }
30
+
31
+ export function dnsQueryTCP(host, port, queryBuf, timeout = 5000) {
32
+ return new Promise((resolve, reject) => {
33
+ const conn = createConnection({ host, port })
34
+ const timer = setTimeout(() => {
35
+ conn.destroy()
36
+ reject(new Error(`DNS TCP query timed out (${host}:${port})`))
37
+ }, timeout)
38
+ let received = Buffer.alloc(0)
39
+ conn.on('connect', () => {
40
+ const len = Buffer.alloc(2)
41
+ len.writeUInt16BE(queryBuf.length)
42
+ conn.write(Buffer.concat([len, queryBuf]))
43
+ })
44
+ conn.on('data', (chunk) => {
45
+ received = Buffer.concat([received, chunk])
46
+ if (received.length >= 2) {
47
+ const msgLen = received.readUInt16BE(0)
48
+ if (received.length >= 2 + msgLen) {
49
+ clearTimeout(timer)
50
+ conn.destroy()
51
+ resolve(received.slice(2, 2 + msgLen))
52
+ }
53
+ }
54
+ })
55
+ conn.on('error', (err) => {
56
+ clearTimeout(timer)
57
+ reject(err)
58
+ })
59
+ })
60
+ }
61
+
62
+ export async function dnsQuery(host, port, queryBuf, timeout = 5000) {
63
+ const udpResult = await dnsQueryUDP(host, port, queryBuf, timeout)
64
+ const tc = (udpResult.readUInt16BE(2) >> 9) & 1
65
+ if (tc) return dnsQueryTCP(host, port, queryBuf, timeout)
66
+ return udpResult
67
+ }
package/lib/tinydns.js CHANGED
@@ -18,6 +18,84 @@ export const SPECIAL_CHARS = {
18
18
  }
19
19
 
20
20
  const octalRe = new RegExp(/\\(?:[1-7][0-7]{0,2}|[0-7]{2,3})/, 'g')
21
+ const textDecoder = new TextDecoder()
22
+
23
+ export function parseFields(tinyline, rdataCount) {
24
+ const parts = tinyline.slice(1).split(':')
25
+ const owner = parts[0]
26
+ const rdata = parts.slice(1, 1 + rdataCount)
27
+ const [ttl, timestamp, location] = parts.slice(1 + rdataCount)
28
+ return { owner, rdata, ttl, timestamp, location: location?.trim() ?? '' }
29
+ }
30
+
31
+ export function parseGenericLine(tinyline) {
32
+ // Generic form: :owner:typeId:rdata:ttl:timestamp:location
33
+ const [owner, typeId, rdata, ttl, timestamp, location] = tinyline.slice(1).split(':')
34
+ return { owner, typeId, rdata, ttl: parseInt(ttl, 10), timestamp, location: location?.trim() ?? '' }
35
+ }
36
+
37
+ export function octalRdataToBytes(rdata) {
38
+ return Uint8Array.from(octalToChar(rdata), (c) => c.charCodeAt(0))
39
+ }
40
+
41
+ export function parseSvcbLikeRdata(rdata, recordType) {
42
+ if (rdata.length < 6) {
43
+ throw new Error(`${recordType}: RDATA too short: ${rdata}`)
44
+ }
45
+
46
+ const binary = octalRdataToBytes(rdata)
47
+ const priority = (binary[0] << 8) | binary[1]
48
+
49
+ let pos = 2
50
+ const labels = []
51
+ while (true) {
52
+ const len = binary[pos]
53
+ pos += 1
54
+ if (len === 0) break
55
+ labels.push(textDecoder.decode(binary.subarray(pos, pos + len)))
56
+ pos += len
57
+ }
58
+
59
+ return {
60
+ priority,
61
+ targetName: `${labels.join('.')}.`,
62
+ params: textDecoder.decode(binary.subarray(pos)),
63
+ }
64
+ }
65
+
66
+ export function to(rrInstance) {
67
+ if (rrInstance.constructor.tinydnsType) {
68
+ const fields = rrInstance.getFields('rdata')
69
+ const rdata = fields
70
+ .map((f) => {
71
+ if (rrInstance.isFqdnField(f)) return rrInstance.getTinyFQDN(f)
72
+ return rrInstance.get(f)
73
+ })
74
+ .join(':')
75
+ return `${rrInstance.constructor.tinydnsType}${rrInstance.getTinyFQDN('owner')}:${rdata}:${rrInstance.getTinydnsPostamble()}\n`
76
+ }
77
+ return rrInstance.getTinydnsGeneric(bytesToOctalString(rrInstance.getWireRdata()))
78
+ }
79
+
80
+ export function fromGeneric(rrInstance, { tinyline }) {
81
+ const fields = rrInstance.getFields('rdata')
82
+ const { owner, rdata, ttl, timestamp, location } = parseFields(tinyline, fields.length)
83
+
84
+ const result = {
85
+ owner: rrInstance.fullyQualify(owner),
86
+ type: rrInstance.constructor.typeName,
87
+ ttl: parseInt(ttl, 10),
88
+ timestamp: timestamp,
89
+ location: location,
90
+ }
91
+
92
+ for (let i = 0; i < fields.length; i++) {
93
+ const val = rdata[i]
94
+ result[fields[i]] = rrInstance.isFqdnField(fields[i]) ? rrInstance.fullyQualify(val) : val
95
+ }
96
+
97
+ return new rrInstance.constructor(result)
98
+ }
21
99
 
22
100
  export function escapeOctal(re, str) {
23
101
  let escaped = ''
@@ -28,7 +106,7 @@ export function escapeOctal(re, str) {
28
106
  }
29
107
 
30
108
  export function unescapeOctal(str) {
31
- return this.octalToChar(str)
109
+ return octalToChar(str)
32
110
  }
33
111
 
34
112
  export function octalToChar(str) {
@@ -46,47 +124,38 @@ export function octalToHex(str) {
46
124
  }
47
125
 
48
126
  export function octalToUInt8(str) {
49
- const b = Buffer.alloc(1)
50
- b.writeUInt8(parseInt(str.slice(1, 4), 8), 0)
51
- return b.readUInt8()
127
+ return parseInt(str.slice(1, 4), 8) & 0xff
52
128
  }
53
129
 
54
130
  export function octalToUInt16(str) {
55
- const b = Buffer.alloc(2)
56
- b.writeUInt8(parseInt(str.slice(1, 4), 8), 0)
57
- b.writeUInt8(parseInt(str.slice(5, 8), 8), 1)
58
- return b.readUInt16BE()
131
+ return (parseInt(str.slice(1, 4), 8) << 8) | parseInt(str.slice(5, 8), 8)
59
132
  }
60
133
 
61
134
  export function octalToUInt32(str) {
62
- const b = Buffer.alloc(4)
63
- b.writeUInt8(parseInt(str.slice(1, 4), 8), 0)
64
- b.writeUInt8(parseInt(str.slice(5, 8), 8), 1)
65
- b.writeUInt8(parseInt(str.slice(9, 12), 8), 2)
66
- b.writeUInt8(parseInt(str.slice(13, 16), 8), 3)
67
- return b.readUInt32BE()
135
+ const b0 = parseInt(str.slice(1, 4), 8)
136
+ const b1 = parseInt(str.slice(5, 8), 8)
137
+ const b2 = parseInt(str.slice(9, 12), 8)
138
+ const b3 = parseInt(str.slice(13, 16), 8)
139
+ return ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) >>> 0
68
140
  }
69
141
 
70
142
  export function packString(str) {
71
143
  return str
72
144
  .match(/(.{1,255})/g)
73
- .map((s) => {
74
- const len = Buffer.alloc(1)
75
- len.writeUInt8(s.length)
76
- return `${UInt8toOctal(len.readUInt8(0))}${s}`
77
- })
145
+ .map((s) => `${UInt8toOctal(s.length)}${s}`)
78
146
  .join('')
79
147
  }
80
148
 
81
149
  export function unpackString(str) {
82
- const asBuf = Buffer.from(octalToChar(str.toString()))
150
+ const asBuf = Uint8Array.from(octalToChar(str.toString()), (c) => c.charCodeAt(0))
151
+ const dec = new TextDecoder()
83
152
  const res = []
84
153
  let pos = 0
85
154
  let len
86
- while ((len = asBuf.readUInt8(pos))) {
155
+ while ((len = asBuf[pos])) {
87
156
  // encoded length byte
88
157
  pos++
89
- res.push(asBuf.slice(pos, pos + len).toString())
158
+ res.push(dec.decode(asBuf.subarray(pos, pos + len)))
90
159
  pos = +(pos + len)
91
160
  if (pos >= asBuf.length) break
92
161
  }
@@ -102,9 +171,7 @@ export function packDomainName(fqdn) {
102
171
  fqdn.split('.').forEach((label) => {
103
172
  if (label === undefined || !label.length) return
104
173
 
105
- const len = Buffer.alloc(1)
106
- len.writeUInt8(label.length)
107
- packed += UInt8toOctal(len.readUInt8(0))
174
+ packed += UInt8toOctal(label.length)
108
175
 
109
176
  packed += escapeOctal(labelRegEx, label)
110
177
  })
@@ -163,28 +230,26 @@ export function charToOctal(c) {
163
230
  }
164
231
 
165
232
  export function UInt8toOctal(n) {
166
- if (n > 255) throw new Error('UInt8toOctal does not work on numbers > 255')
233
+ if (n > 255) {
234
+ throw new Error(
235
+ `UInt8toOctal: value ${n} exceeds 255 — tinydns encoders require Latin-1/byte input (code points <= 0xFF)`,
236
+ )
237
+ }
167
238
 
168
239
  return `\\${parseInt(n, 10).toString(8).padStart(3, 0)}`
169
240
  }
170
241
 
171
242
  export function UInt16toOctal(n) {
172
- let r = ''
173
- const pri = Buffer.alloc(2)
174
- pri.writeUInt16BE(n)
175
- r += UInt8toOctal(pri.readUInt8(0))
176
- r += UInt8toOctal(pri.readUInt8(1))
177
- return r
243
+ return UInt8toOctal((n >>> 8) & 0xff) + UInt8toOctal(n & 0xff)
178
244
  }
179
245
 
180
246
  export function UInt32toOctal(n) {
181
- let r = ''
182
- const pri = Buffer.alloc(4)
183
- pri.writeUInt32BE(n)
184
- for (let i = 0; i < 4; i++) {
185
- r += UInt8toOctal(pri.readUInt8(i))
186
- }
187
- return r
247
+ return (
248
+ UInt8toOctal((n >>> 24) & 0xff) +
249
+ UInt8toOctal((n >>> 16) & 0xff) +
250
+ UInt8toOctal((n >>> 8) & 0xff) +
251
+ UInt8toOctal(n & 0xff)
252
+ )
188
253
  }
189
254
 
190
255
  export function ipv4toOctal(ip) {
@@ -196,8 +261,20 @@ export function octalToIPv4(str) {
196
261
  return [24, 16, 8, 0].map((n) => (asInt >> n) & 0xff).join('.')
197
262
  }
198
263
 
264
+ export function expandIPv6(val, delimiter = ':') {
265
+ const colons = val.match(/:/g)
266
+ if (colons?.length < 7) {
267
+ val = val.replace(/::/, ':'.repeat(9 - colons.length))
268
+ }
269
+ return val
270
+ .split(':')
271
+ .map((s) => s.padStart(4, '0'))
272
+ .join(delimiter)
273
+ .toLowerCase()
274
+ }
275
+
199
276
  export function ipv6toOctal(ip) {
200
- return packHex(ip.replace(/:/g, ''))
277
+ return packHex(expandIPv6(ip, ''))
201
278
  }
202
279
 
203
280
  export function octalToIPv6(str) {
@@ -207,14 +284,21 @@ export function octalToIPv6(str) {
207
284
  }
208
285
 
209
286
  export function base64toOctal(str) {
210
- const bytes = Buffer.from(str, 'base64')
287
+ const binary = atob(str)
211
288
  let escaped = ''
212
- for (const b of bytes) {
213
- escaped += /[A-Za-z0-9\-.]/.test(String.fromCharCode(b)) ? String.fromCharCode(b) : UInt8toOctal(b)
289
+ for (let i = 0; i < binary.length; i++) {
290
+ const b = binary.charCodeAt(i)
291
+ escaped += /[A-Za-z0-9\-.]/.test(binary[i]) ? binary[i] : UInt8toOctal(b)
214
292
  }
215
293
  return escaped
216
294
  }
217
295
 
218
296
  export function octalToBase64(str) {
219
- return Buffer.from(octalToChar(str), 'binary').toString('base64')
297
+ return btoa(octalToChar(str))
298
+ }
299
+
300
+ export function bytesToOctalString(bytes) {
301
+ let result = ''
302
+ for (const b of bytes) result += UInt8toOctal(b)
303
+ return result
220
304
  }