Be consistent about domain name label character escaping (#1122)

* Improve sprintName tests

* Fix sprintName handling of escaped dots

* Make sprintName consistently drop dangling incomplete escapes

* Be consistent about domain name label character escaping

Fixes #1121

* Replace strings.IndexByte with faster special-purpose function
This commit is contained in:
Richard Gibson 2020-07-06 04:07:56 -04:00 committed by GitHub
parent 064ba4b789
commit 978b9a827a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 59 additions and 37 deletions

9
msg.go
View File

@ -398,19 +398,14 @@ Loop:
return "", lenmsg, ErrLongDomain
}
for _, b := range msg[off : off+c] {
switch b {
case '.', '(', ')', ';', ' ', '@':
fallthrough
case '"', '\\':
if isDomainNameLabelSpecial(b) {
s = append(s, '\\', b)
default:
if b < ' ' || b > '~' { // unprintable, use \DDD
} else if b < ' ' || b > '~' {
s = append(s, escapeByte(b)...)
} else {
s = append(s, b)
}
}
}
s = append(s, '.')
off += c
case 0xC0:

View File

@ -445,45 +445,38 @@ func sprintName(s string) string {
var dst strings.Builder
for i := 0; i < len(s); {
if i+1 < len(s) && s[i] == '\\' && s[i+1] == '.' {
if s[i] == '.' {
if dst.Len() != 0 {
dst.WriteString(s[i : i+2])
dst.WriteByte('.')
}
i += 2
i++
continue
}
b, n := nextByte(s, i)
if n == 0 {
i++
continue
// Drop "dangling" incomplete escapes.
if dst.Len() == 0 {
return s[:i]
}
if b == '.' {
if dst.Len() != 0 {
dst.WriteByte('.')
break
}
i += n
continue
}
switch b {
case ' ', '\'', '@', ';', '(', ')', '"', '\\': // additional chars to escape
if isDomainNameLabelSpecial(b) {
if dst.Len() == 0 {
dst.Grow(len(s) * 2)
dst.WriteString(s[:i])
}
dst.WriteByte('\\')
dst.WriteByte(b)
default:
if ' ' <= b && b <= '~' {
if dst.Len() != 0 {
dst.WriteByte(b)
}
} else {
} else if b < ' ' || b > '~' { // unprintable, use \DDD
if dst.Len() == 0 {
dst.Grow(len(s) * 2)
dst.WriteString(s[:i])
}
dst.WriteString(escapeByte(b))
} else {
if dst.Len() != 0 {
dst.WriteByte(b)
}
}
i += n
@ -585,6 +578,17 @@ func escapeByte(b byte) string {
return escapedByteLarge[int(b)*4 : int(b)*4+4]
}
// isDomainNameLabelSpecial returns true if
// a domain name label byte should be prefixed
// with an escaping backslash.
func isDomainNameLabelSpecial(b byte) bool {
switch b {
case '.', ' ', '\'', '@', ';', '(', ')', '"', '\\':
return true
}
return false
}
func nextByte(s string, offset int) (byte, int) {
if offset >= len(s) {
return 0, 0

View File

@ -74,10 +74,33 @@ func TestSplitN(t *testing.T) {
}
func TestSprintName(t *testing.T) {
got := sprintName("abc\\.def\007\"\127@\255\x05\xef\\")
tests := map[string]string{
// Non-numeric escaping of special printable characters.
" '@;()\"\\..example": `\ \'\@\;\(\)\"\..example`,
"\\032\\039\\064\\059\\040\\041\\034\\046\\092.example": `\ \'\@\;\(\)\"\.\\.example`,
if want := "abc\\.def\\007\\\"W\\@\\173\\005\\239"; got != want {
t.Errorf("expected %q, got %q", want, got)
// Numeric escaping of nonprintable characters.
"\x00\x07\x09\x0a\x1f.\x7f\x80\xad\xef\xff": `\000\007\009\010\031.\127\128\173\239\255`,
"\\000\\007\\009\\010\\031.\\127\\128\\173\\239\\255": `\000\007\009\010\031.\127\128\173\239\255`,
// No escaping of other printable characters, at least after a prior escape.
";[a-zA-Z0-9_]+/*.~": `\;[a-zA-Z0-9_]+/*.~`,
";\\091\\097\\045\\122\\065\\045\\090\\048\\045\\057\\095\\093\\043\\047\\042.\\126": `\;[a-zA-Z0-9_]+/*.~`,
// "\\091\\097\\045\\122\\065\\045\\090\\048\\045\\057\\095\\093\\043\\047\\042.\\126": `[a-zA-Z0-9_]+/*.~`,
// Incomplete "dangling" escapes are dropped regardless of prior escaping.
"a\\": `a`,
";\\": `\;`,
// Escaped dots stay escaped regardless of prior escaping.
"a\\.\\046.\\.\\046": `a\.\..\.\.`,
"a\\046\\..\\046\\.": `a\.\..\.\.`,
}
for input, want := range tests {
got := sprintName(input)
if got != want {
t.Errorf("input %q: expected %q, got %q", input, want, got)
}
}
}