Skip to content

Commit 20d47e2

Browse files
committed
fix: TXT decode preserves string-boundary info
1 parent cc8dd46 commit 20d47e2

5 files changed

Lines changed: 39 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
44

55
### Unreleased
66

7+
### [3.0.0] - 2026-05-26
8+
9+
- **BREAKING**, TXT `data` is now always an array of strings
10+
- fix(packet): TXT decode preserves character-string boundaries (RFC 1035 §3.3.14)
11+
712
### [2.4.0] - 2026-05-26
813

914
- feat(ESM): dual published with ESM support

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dns2",
3-
"version": "2.4.0",
3+
"version": "3.0.0",
44
"description": "A DNS Server and Client Implementation in Pure JavaScript with no dependencies.",
55
"main": "index.js",
66
"types": "ts/index.d.ts",

packet.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -768,22 +768,24 @@ Packet.Resource.PTR = Packet.Resource.CNAME = {
768768
* @docs https://tools.ietf.org/html/rfc1035#section-3.3.14
769769
*/
770770
Packet.Resource.SPF = Packet.Resource.TXT = {
771+
// RFC 1035 §3.3.14: TXT RDATA is one or more length-prefixed
772+
// <character-string> items. Preserve those boundaries by returning an
773+
// array — joining them silently corrupts SPF/DKIM and other multi-string
774+
// records whose semantics depend on segmentation.
771775
decode: function (reader, length) {
772-
const parts = [];
776+
const strings = [];
773777
let bytesRead = 0;
774-
let chunkLength;
775-
776778
while (bytesRead < length) {
777-
chunkLength = reader.read(8); // text length
779+
const chunkLength = reader.read(8);
778780
bytesRead++;
779-
780-
while (chunkLength--) {
781-
parts.push(reader.read(8));
781+
const bytes = [];
782+
for (let i = 0; i < chunkLength; i++) {
783+
bytes.push(reader.read(8));
782784
bytesRead++;
783785
}
786+
strings.push(Buffer.from(bytes).toString('utf8'));
784787
}
785-
786-
this.data = Buffer.from(parts).toString('utf8');
788+
this.data = strings;
787789
return this;
788790
},
789791
encode: function (record, writer) {

test/packet.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,8 @@ test('Packet#encode', function () {
317317
type: Packet.TYPE.TXT,
318318
class: Packet.CLASS.IN,
319319
ttl: 300,
320-
data: '#v=spf1 include:_spf.google.com ~all',
320+
// TXT data is an array of <character-string> items (RFC 1035 §3.3.14).
321+
data: ['#v=spf1 include:_spf.google.com ~all'],
321322
});
322323

323324
assert.deepEqual(Packet.parse(response.toBuffer()), response);
@@ -343,10 +344,7 @@ test('Packet#encode array of character strings', function () {
343344
data: dkim,
344345
});
345346

346-
assert.equal(
347-
Packet.parse(response.toBuffer()).answers[0].data,
348-
dkim.join(''),
349-
);
347+
assert.deepEqual(Packet.parse(response.toBuffer()).answers[0].data, dkim);
350348
});
351349

352350
test('EDNS.ECS#encode', function () {
@@ -511,9 +509,9 @@ test('Resource#TXT round-trip single string', function () {
511509
type: Packet.TYPE.TXT,
512510
class: Packet.CLASS.IN,
513511
ttl: 300,
514-
data: 'hello world',
512+
data: 'hello world', // encoder normalizes string → [string]
515513
});
516-
assert.equal(out.data, 'hello world');
514+
assert.deepEqual(out.data, ['hello world']);
517515
});
518516

519517
test('Resource#TXT round-trip with utf-8', function () {
@@ -524,7 +522,20 @@ test('Resource#TXT round-trip with utf-8', function () {
524522
ttl: 300,
525523
data: 'café résumé 日本',
526524
});
527-
assert.equal(out.data, 'café résumé 日本');
525+
assert.deepEqual(out.data, ['café résumé 日本']);
526+
});
527+
528+
test('Resource#TXT preserves character-string boundaries', function () {
529+
// SPF/DKIM-style multi-string TXT records must not be merged on decode.
530+
const chunks = ['part-one ', 'part-two ', 'part-three'];
531+
const out = roundTripAnswer({
532+
name: 'multi.example',
533+
type: Packet.TYPE.TXT,
534+
class: Packet.CLASS.IN,
535+
ttl: 60,
536+
data: chunks,
537+
});
538+
assert.deepEqual(out.data, chunks);
528539
});
529540

530541
test('Resource#SOA round-trip', function () {

test/server.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ test('server/udp-tcp#simple-request-async-response', async () => {
151151
const tcpClient = TCPClient({ dns: '127.0.0.1', port: servers.tcp.port });
152152
const udpClient = UDPClient({ dns: '127.0.0.1', port: servers.udp.port });
153153
const expected = [
154-
{ name: 'test.com', ttl: 300, type: 16, class: 1, data: 'Hello World' },
154+
{ name: 'test.com', ttl: 300, type: 16, class: 1, data: ['Hello World'] },
155155
];
156156
assert.deepEqual((await tcpClient('test.com')).answers, expected);
157157
assert.deepEqual((await udpClient('test.com')).answers, expected);
@@ -456,7 +456,7 @@ test('server/doh#POST end-to-end', async () => {
456456
type: Packet.TYPE.TXT,
457457
class: Packet.CLASS.IN,
458458
ttl: 60,
459-
data: 'post-ok',
459+
data: ['post-ok'],
460460
});
461461
send(response);
462462
});
@@ -499,7 +499,7 @@ test('server/doh#POST end-to-end', async () => {
499499
});
500500
const parsed = Packet.parse(body);
501501
assert.equal(parsed.answers.length, 1);
502-
assert.equal(parsed.answers[0].data, 'post-ok');
502+
assert.deepEqual(parsed.answers[0].data, ['post-ok']);
503503
server.close();
504504
});
505505

0 commit comments

Comments
 (0)