blob: 1525ead1ef30eece7abb3d9100a61c6b683eba92 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mdns
import (
"encoding/base32"
"errors"
"regexp"
"sort"
"strings"
"v.io/v23/discovery"
)
const (
// Limit the maximum large txt records to the maximum total txt records size.
maxLargeTxtRecordLen = maxTotalTxtRecordsLen
// The index of large txt records ranges from '0' to '~' as defined in
// reLargeTxtRecord.
maxNumLargeTxtRecords = '~' - '0' + 1
)
var (
// The key of encoded large txt records is "_x<i><j>", where 'i' and 'j' are
// one byte indices. This should be enough with the current limit on the
// advertisement size and the number of attributes or attachments.
reLargeTxtRecord = regexp.MustCompile("^" + attrLargeTxtPrefix + "[0-~][0-~]=")
)
// encodeAdId encodes the given advertisement id to a valid host name by using
// "Extended Hex Alphabet" defined in RFC 4648. This removes any padding characters.
func encodeAdId(id *discovery.AdId) string {
return strings.TrimRight(base32.HexEncoding.EncodeToString(id[:]), "=")
}
// decodeAdId decodes the given host name to the advertisement id.
func decodeAdId(hostname string, id *discovery.AdId) error {
// Add padding characters if needed.
if p := len(hostname) % 8; p > 0 {
hostname += strings.Repeat("=", 8-p)
}
decoded, err := base32.HexEncoding.DecodeString(hostname)
if err != nil {
return err
}
if len(decoded) != len(id) {
return errors.New("invalid hostname")
}
copy(id[:], decoded)
return nil
}
// maybeSplitTxtRecord slices a txt record into multiple records if it is larger than 255 bytes.
func maybeSplitTxtRecord(txt string, xseq int) ([]string, int) {
n := len(txt)
if n <= maxTxtRecordLen {
return []string{txt}, xseq
}
// The overhead of a large txt record is 5 bytes -
// "_x<i><j>=", where 'i' and 'j' are one byte.
const overhead = 5
splitted := make([]string, 0, (n+maxTxtRecordLen-overhead-1)/(maxTxtRecordLen-overhead))
var buf [maxTxtRecordLen]byte
copy(buf[:], attrLargeTxtPrefix)
for i, off := 0, 0; off < n; i++ {
buf[2] = byte(xseq + '0')
buf[3] = byte(i + '0')
buf[4] = '='
c := copy(buf[5:], txt[off:])
splitted = append(splitted, string(buf[:5+c]))
off += c
}
return splitted, xseq + 1
}
// maybeJoinTxtRecords joins the splitted large txt records.
func maybeJoinTxtRecords(txts []string) ([]string, error) {
joined, splitted := make([]string, 0, len(txts)), make([]string, 0, len(txts))
for _, txt := range txts {
switch {
case strings.HasPrefix(txt, attrLargeTxtPrefix):
if !reLargeTxtRecord.MatchString(txt) {
return nil, errors.New("invalid large txt record")
}
splitted = append(splitted, txt)
default:
joined = append(joined, txt)
}
}
if len(splitted) == 0 {
return joined, nil
}
sort.Strings(splitted)
var buf [maxLargeTxtRecordLen]byte
xseq, off := -1, 0
for _, txt := range splitted {
i := int(txt[2] - '0')
if i > xseq {
if off > 0 {
// A new large txt record started.
joined = append(joined, string(buf[:off]))
}
xseq = i
off = 0
}
c := copy(buf[off:], txt[5:])
off += c
}
joined = append(joined, string(buf[:off]))
return joined, nil
}