Merge pull request #818 from tmthrgd/comp-opt

Improve PackDomainName performance
This commit is contained in:
Miek Gieben 2018-11-28 18:53:23 +00:00 committed by GitHub
commit 091d66a39f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 133 additions and 78 deletions

186
msg.go
View File

@ -198,139 +198,167 @@ func packDomainName(s string, msg []byte, off int, compression map[string]int, c
if msg != nil {
lenmsg = len(msg)
}
ls := len(s)
if ls == 0 { // Ok, for instance when dealing with update RR without any rdata.
return off, 0, nil
}
// If not fully qualified, error out, but only if msg == nil #ugly
switch {
case msg == nil:
if s[ls-1] != '.' {
s += "."
ls++
}
case msg != nil:
if s[ls-1] != '.' {
// If not fully qualified, error out, but only if msg != nil #ugly
if s[ls-1] != '.' {
if msg != nil {
return lenmsg, 0, ErrFqdn
}
s += "."
ls++
}
// Each dot ends a segment of the name.
// We trade each dot byte for a length byte.
// Except for escaped dots (\.), which are normal dots.
// There is also a trailing zero.
// Compression
nameoffset := -1
pointer := -1
// Emit sequence of counted strings, chopping at dots.
begin := 0
bs := []byte(s)
roBs, bsFresh, escapedDot := s, true, false
var (
begin int
bs []byte
wasDot bool
)
loop:
for i := 0; i < ls; i++ {
if bs[i] == '\\' {
for j := i; j < ls-1; j++ {
bs[j] = bs[j+1]
}
ls--
var c byte
if bs == nil {
c = s[i]
} else {
c = bs[i]
}
switch c {
case '\\':
if off+1 > lenmsg {
return lenmsg, labels, ErrBuf
}
// check for \DDD
if i+2 < ls && isDigit(bs[i]) && isDigit(bs[i+1]) && isDigit(bs[i+2]) {
bs[i] = dddToByte(bs[i:])
for j := i + 1; j < ls-2; j++ {
bs[j] = bs[j+2]
}
ls -= 2
}
escapedDot = bs[i] == '.'
bsFresh = false
continue
}
if bs[i] == '.' {
if i > 0 && bs[i-1] == '.' && !escapedDot {
if bs == nil {
bs = []byte(s)
}
// check for \DDD
if i+3 < ls && isDigit(bs[i+1]) && isDigit(bs[i+2]) && isDigit(bs[i+3]) {
bs[i] = dddToByte(bs[i+1:])
copy(bs[i+1:ls-3], bs[i+4:])
ls -= 3
} else {
copy(bs[i:ls-1], bs[i+1:])
ls--
}
wasDot = false
case '.':
if wasDot {
// two dots back to back is not legal
return lenmsg, labels, ErrRdata
}
if i-begin >= 1<<6 { // top two bits of length must be clear
wasDot = true
labelLen := i - begin
if labelLen >= 1<<6 { // top two bits of length must be clear
return lenmsg, labels, ErrRdata
}
// off can already (we're in a loop) be bigger than len(msg)
// this happens when a name isn't fully qualified
if off+1 > lenmsg {
if off+1+labelLen > lenmsg {
return lenmsg, labels, ErrBuf
}
if msg != nil {
msg[off] = byte(i - begin)
}
offset := off
off++
for j := begin; j < i; j++ {
if off+1 > lenmsg {
return lenmsg, labels, ErrBuf
}
if msg != nil {
msg[off] = bs[j]
}
off++
}
if compress && !bsFresh {
roBs = string(bs)
bsFresh = true
}
// Don't try to compress '.'
// We should only compress when compress it true, but we should also still pick
// We should only compress when compress is true, but we should also still pick
// up names that can be used for *future* compression(s).
if compression != nil && roBs[begin:] != "." {
if p, ok := compression[roBs[begin:]]; !ok {
// Only offsets smaller than this can be used.
if offset < maxCompressionOffset {
compression[roBs[begin:]] = offset
}
if compression != nil && !isRootLabel(s, bs, begin, ls) {
var (
p int
ok bool
)
if bs == nil {
p, ok = compression[s[begin:]]
} else {
p, ok = compression[string(bs[begin:ls])]
}
if ok {
// The first hit is the longest matching dname
// keep the pointer offset we get back and store
// the offset of the current name, because that's
// where we need to insert the pointer later
// If compress is true, we're allowed to compress this dname
if pointer == -1 && compress {
pointer = p // Where to point to
nameoffset = offset // Where to point from
break
if compress {
pointer = p // Where to point to
break loop
}
} else if off < maxCompressionOffset {
// Only offsets smaller than maxCompressionOffset can be used.
if bs == nil {
compression[s[begin:]] = off
} else {
compression[string(bs[begin:ls])] = off
}
}
}
// The following is covered by the length check above.
if msg != nil {
msg[off] = byte(labelLen)
if bs == nil {
copy(msg[off+1:], s[begin:i])
} else {
copy(msg[off+1:], bs[begin:i])
}
}
off += 1 + labelLen
labels++
begin = i + 1
default:
wasDot = false
}
escapedDot = false
}
// Root label is special
if len(bs) == 1 && bs[0] == '.' {
if isRootLabel(s, bs, 0, ls) {
return off, labels, nil
}
// If we did compression and we find something add the pointer here
if pointer != -1 {
// Clear the msg buffer after the pointer location, otherwise
// packDataNsec writes the wrong data to msg.
tainted := msg[nameoffset:off]
for i := range tainted {
tainted[i] = 0
}
// We have two bytes (14 bits) to put the pointer in
// if msg == nil, we will never do compression
binary.BigEndian.PutUint16(msg[nameoffset:], uint16(pointer^0xC000))
off = nameoffset + 1
goto End
binary.BigEndian.PutUint16(msg[off:], uint16(pointer^0xC000))
return off + 2, labels, nil
}
if msg != nil && off < len(msg) {
if msg != nil && off < lenmsg {
msg[off] = 0
}
End:
off++
return off, labels, nil
return off + 1, labels, nil
}
// isRootLabel returns whether s or bs, from off to end, is the root
// label ".".
//
// If bs is nil, s will be checked, otherwise bs will be checked.
func isRootLabel(s string, bs []byte, off, end int) bool {
if bs == nil {
return s[off:end] == "."
}
return end-off == 1 && bs[off] == '.'
}
// Unpack a domain name.
@ -539,10 +567,12 @@ func unpackTxt(msg []byte, off0 int) (ss []string, off int, err error) {
func isDigit(b byte) bool { return b >= '0' && b <= '9' }
func dddToByte(s []byte) byte {
_ = s[2] // bounds check hint to compiler; see golang.org/issue/14808
return byte((s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0'))
}
func dddStringToByte(s string) byte {
_ = s[2] // bounds check hint to compiler; see golang.org/issue/14808
return byte((s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0'))
}

View File

@ -213,6 +213,31 @@ func TestUnpackDomainName(t *testing.T) {
}
}
func TestPackDomainNameCompressionMap(t *testing.T) {
msg := make([]byte, 256)
for _, compress := range []bool{true, false} {
compression := make(map[string]int)
_, err := PackDomainName(`www\.this.is.\131an.example.org.`, msg, 0, compression, compress)
if err != nil {
t.Fatalf("PackDomainName failed: %v", err)
}
for _, dname := range []string{
`www.this.is.\131an.example.org.`,
`is.\131an.example.org.`,
"\x83an.example.org.",
`example.org.`,
`org.`,
} {
if _, ok := compression[dname]; !ok {
t.Errorf("expected to find %q in compression map", dname)
}
}
}
}
func TestPackDomainNameNSECTypeBitmap(t *testing.T) {
ownername := "some-very-long-ownername.com."
msg := &Msg{