Skip to content

Commit 00d0072

Browse files
Fix counting of escape sequences when splitting TXT strings
`endingToTxtSlice`, used by TXT, SPF and a few other types, parses a string such as `"hello world"` from an RR's content in a zone file. These strings are limited to 255 characters, and `endingToTxtSlice` automatically splits them if they're longer than that. However, it didn't count the length correctly: escape sequences such as `\\` or `\123` were counted as multiple characters (2 and 4 respectively in these examples), but they should only count as one character because they represent a single byte in wire format (which is where this 255 character limit comes from). This commit fixes that.
1 parent 2230854 commit 00d0072

File tree

3 files changed

+101
-25
lines changed

3 files changed

+101
-25
lines changed

parse_test.go

+31-8
Original file line numberDiff line numberDiff line change
@@ -1098,18 +1098,41 @@ func TestTXT(t *testing.T) {
10981098
}
10991099
}
11001100

1101-
// Test TXT record with chunk larger than 255 bytes, they should be split up, by the parser
1102-
s := ""
1103-
for i := 0; i < 255; i++ {
1104-
s += "a"
1101+
// Test TXT record with string larger than 255 bytes that should be split
1102+
// up by the parser. Add some escape sequences too to ensure their length
1103+
// is counted correctly.
1104+
s := `"\;\\\120` + strings.Repeat("a", 255) + `b"`
1105+
rr, err = NewRR(`test.local. 60 IN TXT ` + s)
1106+
if err != nil {
1107+
t.Error("failed to parse empty-string TXT record", err)
1108+
}
1109+
if rr.(*TXT).Txt[1] != "aaab" {
1110+
t.Errorf("Txt should have two strings, last one must be 'aaab', but is %s", rr.(*TXT).Txt[1])
11051111
}
1106-
s += "b"
1107-
rr, err = NewRR(`test.local. 60 IN TXT "` + s + `"`)
1112+
rrContent := strings.Replace(rr.String(), rr.Header().String(), "", 1)
1113+
expectedRRContent := `";\\x` + strings.Repeat("a", 252) + `" "aaab"`
1114+
if expectedRRContent != rrContent {
1115+
t.Errorf("Expected TXT RR content to be %#q but got %#q", expectedRRContent, rrContent)
1116+
}
1117+
1118+
// Test TXT record that is already split up into strings of len <= 255.
1119+
s = fmt.Sprintf(
1120+
"%q %q %q %q %q %q",
1121+
strings.Repeat(`a`, 255),
1122+
strings.Repeat("b", 255),
1123+
strings.Repeat("c", 255),
1124+
strings.Repeat("d", 0),
1125+
strings.Repeat("e", 1),
1126+
strings.Repeat("f", 123),
1127+
)
1128+
rr, err = NewRR(`test.local. 60 IN TXT ` + s)
11081129
if err != nil {
11091130
t.Error("failed to parse empty-string TXT record", err)
11101131
}
1111-
if rr.(*TXT).Txt[1] != "b" {
1112-
t.Errorf("Txt should have two chunk, last one my be 'b', but is %s", rr.(*TXT).Txt[1])
1132+
rrContent = strings.Replace(rr.String(), rr.Header().String(), "", 1)
1133+
expectedRRContent = s // same as input
1134+
if expectedRRContent != rrContent {
1135+
t.Errorf("Expected TXT RR content to be %#q but got %#q", expectedRRContent, rrContent)
11131136
}
11141137
}
11151138

scan_rr.go

+42-17
Original file line numberDiff line numberDiff line change
@@ -51,25 +51,21 @@ func endingToTxtSlice(c *zlexer, errstr string) ([]string, *ParseError) {
5151
switch l.value {
5252
case zString:
5353
empty = false
54-
if len(l.token) > 255 {
55-
// split up tokens that are larger than 255 into 255-chunks
56-
sx := []string{}
57-
p, i := 0, 255
58-
for {
59-
if i <= len(l.token) {
60-
sx = append(sx, l.token[p:i])
61-
} else {
62-
sx = append(sx, l.token[p:])
63-
break
64-
65-
}
66-
p, i = p+255, i+255
54+
// split up tokens that are larger than 255 into 255-chunks
55+
sx := []string{}
56+
p := 0
57+
for {
58+
i := escapedStringOffset(l.token[p:], 255)
59+
if i != -1 && p+i != len(l.token) {
60+
sx = append(sx, l.token[p:p+i])
61+
} else {
62+
sx = append(sx, l.token[p:])
63+
break
64+
6765
}
68-
s = append(s, sx...)
69-
break
66+
p += i
7067
}
71-
72-
s = append(s, l.token)
68+
s = append(s, sx...)
7369
case zBlank:
7470
if quote {
7571
// zBlank can only be seen in between txt parts.
@@ -1920,3 +1916,32 @@ func (rr *APL) parse(c *zlexer, o string) *ParseError {
19201916
rr.Prefixes = prefixes
19211917
return nil
19221918
}
1919+
1920+
// escapedStringOffset finds the offset within a string (which may contain escape
1921+
// sequences) that corresponds to a certain byte offset. If the input offset is
1922+
// out of bounds, -1 is returned.
1923+
func escapedStringOffset(s string, byteOffset int) int {
1924+
if byteOffset == 0 {
1925+
return 0
1926+
}
1927+
1928+
offset := 0
1929+
for i := 0; i < len(s); i++ {
1930+
offset += 1
1931+
1932+
// Skip escape sequences
1933+
if s[i] != '\\' {
1934+
// Not an escape sequence; nothing to do.
1935+
} else if isDDD(s[i+1:]) {
1936+
i += 3
1937+
} else {
1938+
i++
1939+
}
1940+
1941+
if offset >= byteOffset {
1942+
return i + 1
1943+
}
1944+
}
1945+
1946+
return -1
1947+
}

scan_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,31 @@ func BenchmarkZoneParser(b *testing.B) {
427427
}
428428
}
429429
}
430+
431+
func TestEscapedStringOffset(t *testing.T) {
432+
var cases = []struct {
433+
input string
434+
inputOffset int
435+
expectedOffset int
436+
}{
437+
{"simple string with no escape sequences", 20, 20},
438+
{"simple string with no escape sequences", 500, -1},
439+
{`\;\088\\\;\120\\`, 0, 0},
440+
{`\;\088\\\;\120\\`, 1, 2},
441+
{`\;\088\\\;\120\\`, 2, 6},
442+
{`\;\088\\\;\120\\`, 3, 8},
443+
{`\;\088\\\;\120\\`, 4, 10},
444+
{`\;\088\\\;\120\\`, 5, 14},
445+
{`\;\088\\\;\120\\`, 6, 16},
446+
{`\;\088\\\;\120\\`, 7, -1},
447+
}
448+
for i, test := range cases {
449+
outputOffset := escapedStringOffset(test.input, test.inputOffset)
450+
if outputOffset != test.expectedOffset {
451+
t.Errorf(
452+
"Test %d (input %#q offset %d) returned offset %d but expected %d",
453+
i, test.input, test.inputOffset, outputOffset, test.expectedOffset,
454+
)
455+
}
456+
}
457+
}

0 commit comments

Comments
 (0)