diff --git a/msg.go b/msg.go index 63656873..7001f6da 100644 --- a/msg.go +++ b/msg.go @@ -398,17 +398,12 @@ 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 - s = append(s, escapeByte(b)...) - } else { - s = append(s, b) - } + } else if b < ' ' || b > '~' { + s = append(s, escapeByte(b)...) + } else { + s = append(s, b) } } s = append(s, '.') diff --git a/types.go b/types.go index ff32c3a1..7776b4f0 100644 --- a/types.go +++ b/types.go @@ -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 - } - if b == '.' { - if dst.Len() != 0 { - dst.WriteByte('.') + // Drop "dangling" incomplete escapes. + if dst.Len() == 0 { + return s[:i] } - i += n - continue + break } - 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 { - if dst.Len() == 0 { - dst.Grow(len(s) * 2) - dst.WriteString(s[:i]) - } - dst.WriteString(escapeByte(b)) + } 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 diff --git a/types_test.go b/types_test.go index 5f3dece0..b3f5583f 100644 --- a/types_test.go +++ b/types_test.go @@ -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) + } } }