202 lines
4.5 KiB
Go
202 lines
4.5 KiB
Go
package dnsttclient
|
|
|
|
// Random selection from weighted distributions, and strings for specifying such
|
|
// distributions.
|
|
|
|
import (
|
|
cryptorand "crypto/rand"
|
|
"encoding/binary"
|
|
"fmt"
|
|
mathrand "math/rand"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// parseWeightedList parses a list of text labels with optional numeric weights,
|
|
// and returns parallel slices of weights and labels. If a weight is omitted for
|
|
// a label, the weight is 1.
|
|
//
|
|
// An example weighted list string is "2*apple,orange,10*cookie". This example
|
|
// results in the slices [2, 1, 10] and ["apple", "orange", "cookie"].
|
|
// Bytes may be escaped by backslashes.
|
|
//
|
|
// list ::= entry ("," entry)*
|
|
// entry ::= (weight "*")? label
|
|
func parseWeightedList(s string) ([]uint32, []string, error) {
|
|
const (
|
|
kindEOF = iota
|
|
kindComma
|
|
kindAsterisk
|
|
kindText
|
|
kindError
|
|
)
|
|
type token struct {
|
|
Kind int
|
|
Text string
|
|
}
|
|
|
|
var i int
|
|
// nextToken incrementally consumes s and returns tokens.
|
|
nextToken := func() token {
|
|
if !(i < len(s)) {
|
|
return token{Kind: kindEOF}
|
|
}
|
|
if s[i] == ',' {
|
|
i++
|
|
return token{Kind: kindComma}
|
|
}
|
|
if s[i] == '*' {
|
|
i++
|
|
return token{Kind: kindAsterisk}
|
|
}
|
|
var text strings.Builder
|
|
for i < len(s) && s[i] != ',' && s[i] != '*' {
|
|
if s[i] == '\\' {
|
|
i++
|
|
if !(i < len(s)) {
|
|
return token{Kind: kindError, Text: fmt.Sprintf("%q at end of string", s[i])}
|
|
}
|
|
}
|
|
text.WriteByte(s[i])
|
|
i++
|
|
}
|
|
return token{Kind: kindText, Text: text.String()}
|
|
}
|
|
peekToken := func() token {
|
|
saved := i
|
|
t := nextToken()
|
|
i = saved
|
|
return t
|
|
}
|
|
|
|
const (
|
|
stateBeginEntry = iota
|
|
stateLabel
|
|
stateEndEntry
|
|
stateDone
|
|
stateUnexpected
|
|
)
|
|
|
|
var weights []uint32
|
|
var labels []string
|
|
var weightString, label string
|
|
var t token
|
|
for state := stateBeginEntry; state != stateDone; {
|
|
switch state {
|
|
// Beginning of a new entry (at the beginning of the input or
|
|
// after a comma).
|
|
case stateBeginEntry:
|
|
t = nextToken()
|
|
switch t.Kind {
|
|
case kindText:
|
|
// If the next token is an asterisk, this text
|
|
// represents a weight; otherwise it represents
|
|
// a label (with a weight of "1").
|
|
switch peekToken().Kind {
|
|
case kindAsterisk:
|
|
nextToken() // Consume the asterisk token.
|
|
weightString = t.Text
|
|
state = stateLabel
|
|
default:
|
|
weightString = "1"
|
|
label = t.Text
|
|
state = stateEndEntry
|
|
}
|
|
default:
|
|
state = stateUnexpected
|
|
}
|
|
// weightString is assigned and we have seen an asterisk, now
|
|
// expect a text label.
|
|
case stateLabel:
|
|
t = nextToken()
|
|
switch t.Kind {
|
|
case kindText:
|
|
label = t.Text
|
|
state = stateEndEntry
|
|
default:
|
|
state = stateUnexpected
|
|
}
|
|
// weightString and label are assigned, now emit the entry and
|
|
// expect a comma or EOF.
|
|
case stateEndEntry:
|
|
w, err := strconv.ParseUint(weightString, 10, 32)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
weights = append(weights, uint32(w))
|
|
labels = append(labels, label)
|
|
t = nextToken()
|
|
switch t.Kind {
|
|
case kindEOF:
|
|
state = stateDone
|
|
case kindComma:
|
|
state = stateBeginEntry
|
|
default:
|
|
state = stateUnexpected
|
|
}
|
|
case stateUnexpected:
|
|
if t.Kind == kindError {
|
|
return nil, nil, fmt.Errorf("%s", t.Text)
|
|
} else {
|
|
var ttext string
|
|
switch t.Kind {
|
|
case kindEOF:
|
|
ttext = "end of string"
|
|
case kindComma:
|
|
ttext = "\",\""
|
|
case kindAsterisk:
|
|
ttext = "\"*\""
|
|
case kindText:
|
|
ttext = fmt.Sprintf("%+q", t.Text)
|
|
}
|
|
return nil, nil, fmt.Errorf("unexpected %s", ttext)
|
|
}
|
|
default:
|
|
panic(state)
|
|
}
|
|
}
|
|
|
|
return weights, labels, nil
|
|
}
|
|
|
|
// cryptoSource is a math/rand Source that reads from the crypto/rand Reader.
|
|
// The Seed method does not affect the sequence of numbers returned from the
|
|
// Int63 method.
|
|
type cryptoSource struct{}
|
|
|
|
func (s cryptoSource) Seed(_ int64) {}
|
|
|
|
func (s cryptoSource) Int63() int64 {
|
|
var n int64
|
|
err := binary.Read(cryptorand.Reader, binary.BigEndian, &n)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
n &= (1 << 63) - 1
|
|
return n
|
|
}
|
|
|
|
// sampleWeighted returns the index of a randomly selected element of the
|
|
// weights slice, weighted by the values stored in the slice. Panics if
|
|
// the sum of the weights is zero or does not fit in an int64.
|
|
func sampleWeighted(weights []uint32) int {
|
|
var sum int64 = 0
|
|
for _, w := range weights {
|
|
sum += int64(w)
|
|
if sum < int64(w) {
|
|
panic("weights overflow")
|
|
}
|
|
}
|
|
if sum == 0 {
|
|
panic("total weight is zero")
|
|
}
|
|
r := uint64(mathrand.New(&cryptoSource{}).Int63n(sum))
|
|
for i, w := range weights {
|
|
if r < uint64(w) {
|
|
return i
|
|
}
|
|
r -= uint64(w)
|
|
}
|
|
panic("impossible")
|
|
}
|