Example #1
0
File: config.go Project: qanx/bmad
// Performs heuristics to determine the hostname of the current host.
// Tries os.Hostname(), and if that isn't fully qualified (contains a '.'),
// Fails over to finding the first hostname for the first IP of the host
// that contains a '.'. If none do, fails back to the unqualified hostname.
func hostname() string {
	h, err := os_hostname()
	if err != nil {
		log.Error("Couldn't get hostname for current host: %s", err.Error())
		return "unknown"
	}
	if strings.ContainsRune(h, '.') {
		return h
	}
	addrs, err := net_lookuphost(h)
	if err != nil {
		log.Warn("Couldn't resolve FQDN of host: %s", err.Error())
		return h
	}
	if len(addrs) > 0 {
		names, err := net_lookupaddr(addrs[0])
		if err != nil {
			log.Warn("Couldn't resolve FQDN of host: %s", err.Error())
			return h
		}
		for _, name := range names {
			if strings.ContainsRune(name, '.') {
				return name
			}
		}
	}

	log.Warn("No FQDN resolvable, defaulting to unqualified hostname")
	return h
}
Example #2
0
func (c Config) Validate() error {
	if c.Host == "" {
		return errors.New("host cannot be empty")
	}
	if c.Port <= 0 {
		return fmt.Errorf("invalid port %d", c.Port)
	}
	if c.IdleTimeout < 0 {
		return errors.New("idle timeout must be positive")
	}
	if c.Enabled && c.From == "" {
		return errors.New("must provide a 'from' address")
	}
	// Poor mans email validation, but since emails have a very large domain this is probably good enough
	// to catch user error.
	if c.From != "" && !strings.ContainsRune(c.From, '@') {
		return fmt.Errorf("invalid from email address: %q", c.From)
	}
	for _, t := range c.To {
		if !strings.ContainsRune(t, '@') {
			return fmt.Errorf("invalid to email address: %q", t)
		}
	}
	return nil
}
Example #3
0
func (q MysqlQuoter) quoteAs(parts ...string) string {

	lp := len(parts)

	hasQuote0 := strings.ContainsRune(parts[0], quoteRune)
	hasDot0 := strings.ContainsRune(parts[0], '.')

	switch {
	case lp == 1 && hasQuote0:
		return parts[0] // already quoted
	case lp > 1 && parts[1] == "" && !hasQuote0 && !hasDot0:
		return quote + q.unQuote(parts[0]) + quote // must be quoted
	case lp == 1 && !hasQuote0 && hasDot0:
		return q.splitDotAndQuote(parts[0])
	}

	n := q.splitDotAndQuote(parts[0])

	switch lp {
	case 1:
		return n
	case 2:
		return n + " AS " + quote + q.unQuote(parts[1]) + quote
	default:
		return n + " AS " + quote + q.unQuote(strings.Join(parts[1:], "_")) + quote
	}
}
Example #4
0
func readMapFile(map_file string, hook func(line string, email string)) []Alias {
	f, err := os.Open(map_file)
	if err != nil {
		log.Fatal("could not open map file ", map_file, " due to ", err)
	}
	aliases := make([]Alias, 0)
	scanner := bufio.NewScanner(bufio.NewReader(f))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "#") {
			if hook != nil {
				hook(line, "")
			}
			continue
		}
		fields := strings.Fields(line)
		// there could be a comment after the map entry
		if len(fields) >= 2 && strings.ContainsRune(fields[0], '@') {
			if strings.ContainsRune(fields[0], '@') {
				aliases = append(aliases, Alias{fields[0], fields[1]})
			}
			if hook != nil {
				hook(line, fields[0])
			}
		} else {
			if hook != nil {
				hook(line, "")
			}
		}
	}
	return aliases
}
Example #5
0
// Generates the WHERE part
func (b *Builder) outputWhere() string {
	if b.where == "" {
		return ""
	}

	var ret bytes.Buffer

	ret.WriteString(" WHERE ")

	inside := false
	var insiderune rune
	for _, c := range b.where {
		if strings.ContainsRune("'\"", c) {
			if !inside || insiderune == c {
				inside = !inside
				insiderune = c
			}
		}

		if !inside && strings.ContainsRune("?", c) {
			ret.WriteString(b.processor.NextParam(""))
		} else {
			ret.WriteRune(c)
		}
	}

	return ret.String()
}
Example #6
0
func Test한글(t *testing.T) {
	h := "한글"
	assert.Equal(t, 6, len(h))
	assert.Equal(t, 2, utf8.RuneCountInString(h))
	assert.Equal(t, []string{"한", "글"}, strings.Split(h, ""))
	assert.True(t, strings.Contains(h, "한"))
	assert.True(t, strings.Contains(h, "글"))
	assert.True(t, strings.ContainsRune(h, '한'))
	assert.True(t, strings.ContainsRune(h, '글'))
	runes := []rune(h)
	assert.Equal(t, []int32{54620, 44544}, runes)
	assert.Equal(t, "[]int32", reflect.TypeOf(runes).String())
	assert.Equal(t, "int32", reflect.TypeOf(runes[0]).String())
	r := reflect.ValueOf(runes[0])
	assert.Equal(t, "reflect.Value", reflect.TypeOf(r).String())
	assert.Equal(t, "int32", r.Type().String())
	assert.Equal(t, reflect.Int32, r.Kind())
	for i, c := range h {
		assert.True(t, strings.Contains(h, string(c)))
		assert.True(t, strings.ContainsRune(h, c))
		assert.Equal(t, "int32", reflect.TypeOf(c).String())
		assert.Equal(t, "int32", reflect.TypeOf(rune(c)).String())
		assert.Equal(t, "string", reflect.TypeOf(strconv.QuoteRune(c)).String())
		if 0 == i {
			assert.True(t, 54620 == c)
			assert.Equal(t, "한", string(c))
			assert.Equal(t, "'한'", strconv.QuoteRune(c))
		}
	}
}
Example #7
0
// ContainRune reports whether the Unicode code point r is within s
func ContainsRune(s string, r rune) bool {
	fmt.Println(strings.ContainsRune("seafood", 12))    // false
	fmt.Println(strings.ContainsRune("seafood12", 12))  // false
	fmt.Println(strings.ContainsRune("seafood12", 97))  // true 97 ->a
	fmt.Println(strings.ContainsRune("seafood12", 111)) // true  111->o
	return strings.ContainsRune(s, r)
}
Example #8
0
func (t *Test) validate(d *Document) error {
	if len(t.TestID) == 0 {
		return fmt.Errorf("a test in document has no identifier")
	}
	if t.getEvaluationInterface() == nil {
		return fmt.Errorf("%v: no valid evaluation interface", t.TestID)
	}
	for _, x := range t.If {
		ptr, err := d.getTest(x)
		if err != nil {
			return fmt.Errorf("%v: %v", t.TestID, err)
		}
		if ptr == t {
			return fmt.Errorf("%v: test cannot reference itself", t.TestID)
		}
	}
	// Ensure the tags only contain valid characters
	for _, x := range t.Tags {
		if strings.ContainsRune(x.Key, '"') {
			return fmt.Errorf("%v: test tag key cannot contain quote", t.TestID)
		}
		if strings.ContainsRune(x.Value, '"') {
			return fmt.Errorf("%v: test tag value cannot contain quote", t.TestID)
		}
	}
	return nil
}
Example #9
0
func newMailAddress(w http.ResponseWriter, r *http.Request) (string, error) {
	mail := r.FormValue("mail")
	if len(mail) > 140 || strings.ContainsRune(mail, '\n') ||
		strings.ContainsRune(mail, '\t') {
		return "", errors.New("Illegal mail address.")
	}
	return mail, nil
}
Example #10
0
func TestTransformerQuotesOff(t *testing.T) {
	t.Parallel()
	tname := "double and single quotes 1"
	res := Transform(samples[tname], QuotesOff)
	if strings.ContainsRune(res, 0x22) || strings.ContainsRune(res, 0x27) {
		t.Errorf("Error in transform of %s: quotes are not replaced in the result [[%s]]: original %s\n", tname, res, samples[tname])
	}
}
Example #11
0
func TestDetectCharacterSetInHTML(t *testing.T) {
	msg := readMessage("non-mime-missing-charset.raw")
	mime, err := ParseMIMEBody(msg)
	if err != nil {
		t.Fatalf("Failed to parse non-MIME: %v", err)
	}
	assert.False(t, strings.ContainsRune(mime.Html, 0x80), "HTML body should not have contained a Windows CP1250 Euro Symbol")
	assert.True(t, strings.ContainsRune(mime.Html, 0x20ac), "HTML body should have contained a Unicode Euro Symbol")
}
Example #12
0
func (b *buffer) Word(offset int) Region {
	if offset < 0 {
		offset = 0
	}
	lr := b.FullLine(offset)
	col := offset - lr.Begin()

	line := b.SubstrR(lr)
	if len(line) == 0 {
		return Region{offset, offset}
	}

	seps := "./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}`~?"
	if v, ok := b.Settings().Get("word_separators", seps).(string); ok {
		seps = v
	}
	spacing := " \n\t\r"
	eseps := seps + spacing

	if col >= len(line) {
		col = len(line) - 1
	}
	last := true
	li := 0
	ls := false
	lc := 0
	for i, r := range line {
		cur := strings.ContainsRune(eseps, r)
		cs := r == ' '
		if !cs {
			lc = i
		}
		if last == cur && ls == cs {
			continue
		}
		ls = cs
		r := Region{li, i}
		if r.Contains(col) && i != 0 {
			r.A, r.B = r.A+lr.Begin(), r.B+lr.Begin()
			if !(r.B == offset && last) {
				return r
			}
		}
		li = i
		last = cur
	}
	r := Region{lr.Begin() + li, lr.End()}
	lc += lr.Begin()
	if lc != offset && !strings.ContainsRune(spacing, b.Index(r.A)) {
		r.B = lc
	}
	if r.A == offset && r.B == r.A+1 {
		r.B--
	}
	return r
}
Example #13
0
func (fs *SubFileSystem) File(subPath string) File {
	if strings.HasPrefix(subPath, fs.Prefix()) || strings.HasPrefix(subPath, "..") || strings.ContainsRune(subPath, ':') {
		panic("invalid subPath for SubFileSystem: " + subPath)
	}
	cleanedPath := filepath.Clean(subPath)
	if cleanedPath == "/" || strings.ContainsRune(cleanedPath, ':') {
		panic("invalid subPath for SubFileSystem: " + subPath)
	}
	return fs.Parent.File(filepath.Join(fs.BasePath, cleanedPath))
}
Example #14
0
func Tokenize(code []byte) []Token {
	tokens := make([]Token, 0)
	token := ""
	state := S_DUNNO
	for i := 0; i < len(code); i++ {
		r := rune(code[i])
		cut := false
		if state == S_DUNNO {
			if strings.ContainsRune("0123456789", r) {
				state = S_INTEGER
			} else if strings.ContainsRune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", r) {
				state = S_IDENTIFIER
			} else if strings.ContainsRune("()", r) {
				state = S_PARANTHESES
			} else if strings.ContainsRune("+-*/=^", r) {
				state = S_OP
			} else if strings.ContainsRune(",", r) {
				state = S_COMMA
			}
		} else if state == S_INTEGER {
			if strings.ContainsRune("0123456789", r) {
				state = S_INTEGER
			} else if strings.ContainsRune(".", r) {
				state = S_FLOAT
			} else {
				cut = true
			}
		} else if state == S_FLOAT {
			if strings.ContainsRune("0123456789", r) {
				state = S_FLOAT
			} else {
				cut = true
			}
		} else if state == S_IDENTIFIER {
			if strings.ContainsRune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", r) {
				state = S_IDENTIFIER
			} else {
				cut = true
			}
		} else if state == S_PARANTHESES {
			cut = true
		} else if state == S_OP {
			cut = true
		} else if state == S_COMMA {
			cut = true
		}
		if cut {
			tokens = append(tokens, Token{state, token})
			state = S_DUNNO
			token = ""
			cut = false
			i--
		} else {
			if state != S_DUNNO {
				token += string(r)
			}
		}
	}
	return tokens
}
Example #15
0
func TestTransformerTagsOff(t *testing.T) {
	t.Parallel()
	for _, k := range []string{"tags", "less-than", "greater-than", "script"} {
		tname := k
		v := samples[k]
		res := Transform(v, TagCharsOff)
		if strings.ContainsRune(res, 0x3c) || strings.ContainsRune(res, 0x3e) {
			t.Errorf("Error in transform of %s: no wanted replacement in the result [[%s]] (original: %s)\n", tname, res, v)
		}
	}
}
Example #16
0
func newSecurityQuestion(w http.ResponseWriter, r *http.Request) (string,
	string, error) {
	secquestion := r.FormValue("secquestion")
	secanswer := r.FormValue("secanswer")
	if "" == secquestion || len(secquestion) > 140 ||
		strings.ContainsRune(secquestion, '\n') ||
		strings.ContainsRune(secquestion, '\t') {
		return "", "", errors.New("Illegal security question.")
	} else if "" == secanswer {
		return "", "", errors.New("Illegal security question answer.")
	}
	return secquestion, hashFromPw(secanswer), nil
}
Example #17
0
// TableColumnAlias prefixes all columns with a table name/alias and puts quotes around them.
// If a column name has already been prefixed by a name or an alias it will be ignored.
func (q MysqlQuoter) TableColumnAlias(t string, cols ...string) []string {
	for i, c := range cols {
		switch {
		case strings.ContainsRune(c, quoteRune):
			cols[i] = c
		case strings.ContainsRune(c, '.'):
			cols[i] = q.QuoteAs(c)
		default:
			cols[i] = q.QuoteAs(t + "." + c)
		}
	}
	return cols
}
Example #18
0
func TestDetectCharacterSetInHTML(t *testing.T) {
	msg := readMessage("non-mime-missing-charset.raw")
	mime, err := ParseMIMEBody(msg)
	if err != nil {
		t.Fatal("Failed to parse non-MIME:", err)
	}
	if strings.ContainsRune(mime.HTML, 0x80) {
		t.Error("HTML body should not have contained a Windows CP1250 Euro Symbol")
	}
	if !strings.ContainsRune(mime.HTML, 0x20ac) {
		t.Error("HTML body should have contained a Unicode Euro Symbol")
	}
}
Example #19
0
func bit(s string) uint64 {
	b := uint64(0)
	if strings.ContainsRune(s, 'r') {
		b |= 4
	}
	if strings.ContainsRune(s, 'w') {
		b |= 2
	}
	if strings.ContainsRune(s, 'x') {
		b |= 1
	}
	return b
}
Example #20
0
func parseDSN(dsn string) (addr string, dbName string, user string, passwd string, err error) {
	s1, s2 := split1(dsn, "@")
	user, passwd = split1(s1, ":")
	addr, dbName = split1(s2, "/")
	if !strings.ContainsRune(addr, ':') {
		addr += ":3050"
	}
	if strings.ContainsRune(dbName, '/') {
		dbName = "/" + dbName
	}

	return
}
Example #21
0
func node_style_to_attribute(n *html.Node) {
	style := node_get_attribute(n, "style")
	lines := strings.FieldsFunc(style, func(r rune) bool {
		return strings.ContainsRune(";\n", r)
	})
	for _, line := range lines {
		fields := strings.FieldsFunc(line, func(r rune) bool {
			return strings.ContainsRune(": ", r)
		})
		if len(fields) == 2 {
			node_set_attribute(n, fields[0], fields[1])
		}
	}
}
Example #22
0
// GetCodecForResponding gets the codec to use to respond based on the
// given accept string, the extension provided and whether it has a callback
// or not.
//
// As of now, if hasCallback is true, the JSONP codec will be returned.
// This may be changed if additional callback capable codecs are added.
func (s *WebCodecService) GetCodecForResponding(accept, extension string, hasCallback bool) (codecs.Codec, error) {

	// make sure we have at least one codec
	s.assertCodecs()

	// is there a callback?  If so, look for JSONP
	if hasCallback {
		for _, codec := range s.codecs {
			if codec.ContentType() == constants.ContentTypeJSONP {
				return codec, nil
			}
		}
	}

	if accept != "" {
		// Try the simple case first
		if !(strings.ContainsRune(accept, ',') || strings.ContainsRune(accept, ';')) {
			accept = strings.TrimSpace(accept)
			codec, err := s.getCodecByMimeString(accept)
			if codec != nil {
				return codec, err
			}
		}

		// If this doesn't match the simple case or simple matching
		// failed to find a matching codec, do a full header parse
		orderedAccept, err := OrderAcceptHeader(accept)
		if err != nil {
			return nil, err
		}
		for _, entry := range orderedAccept {
			if codec, err := s.getCodecByContentType(entry.ContentType); err == nil {
				return codec, nil
			}
		}
	}

	for _, codec := range s.codecs {
		if strings.ToLower(codec.FileExtension()) == strings.ToLower(extension) {
			return codec, nil
		} else if hasCallback && codec.CanMarshalWithCallback() {
			return codec, nil
		}
	}

	// return the first installed codec by default
	return s.codecs[0], nil
}
Example #23
0
func (r *jsonRPCRequest) setMethodAndParams(args []string) error {
	switch {
	case len(args) == 0:
		return fmt.Errorf("missing 'method' (first argument)")
	case len(args) == 1:
		r.Method = args[0]
	case len(args) > 1:
		r.Method = args[0]

		if strings.ContainsRune(args[1], '=') {
			// if first argument conatins a '=' i assume
			// a list of key-value pairs, forming a map
			// or an 'object' in javascript terms
			r.Params = kvPairsToMap(args[1:])

		} else if len(args[1]) > 1 && strings.TrimLeft(args[1], " \n\t")[0] == '{' {
			raw := json.RawMessage(args[1])
			if ok, err := isValidJSON(args[1]); !ok {
				return err
			}
			r.Params = &raw
		} else {
			r.Params = args[1:]
		}
	}
	return nil
}
Example #24
0
func nextLogoText() {
	lines := make([]string, len(stevenLogoLines))
	copy(lines, stevenLogoLines)

	rs, _ := resource.OpenAll("minecraft", "texts/splashes.txt")
	for _, r := range rs {
		func() {
			defer r.Close()
			s := bufio.NewScanner(r)
			for s.Scan() {
				line := s.Text()
				if line != "" && !strings.ContainsRune(line, '§') {
					switch line {
					case "Now Java 6!":
						line = "Now Go!"
					case "OpenGL 2.1 (if supported)!":
						line = "OpenGL 3.2!"
					}
					lines = append(lines, line)
				}
			}
		}()
	}

	logoText = lines[r.Intn(len(lines))]
}
Example #25
0
func translate1(codon []byte) byte {
	// if code contains an 'N' -> 'X'
	if strings.ContainsRune(string(codon), 'N') {
		return 'X'
	}
	// otherwise, look up in hash

	var genCode = map[string]byte{
		"ATA": 'I', "ATC": 'I', "ATT": 'I', "ATG": 'M',
		"ACA": 'T', "ACC": 'T', "ACG": 'T', "ACT": 'T',
		"AAC": 'N', "AAT": 'N', "AAA": 'K', "AAG": 'K',
		"AGC": 'S', "AGT": 'S', "AGA": 'R', "AGG": 'R',
		"CTA": 'L', "CTC": 'L', "CTG": 'L', "CTT": 'L',
		"CCA": 'P', "CCC": 'P', "CCG": 'P', "CCT": 'P',
		"CAC": 'H', "CAT": 'H', "CAA": 'Q', "CAG": 'Q',
		"CGA": 'R', "CGC": 'R', "CGG": 'R', "CGT": 'R',
		"GTA": 'V', "GTC": 'V', "GTG": 'V', "GTT": 'V',
		"GCA": 'A', "GCC": 'A', "GCG": 'A', "GCT": 'A',
		"GAC": 'D', "GAT": 'D', "GAA": 'E', "GAG": 'E',
		"GGA": 'G', "GGC": 'G', "GGG": 'G', "GGT": 'G',
		"TCA": 'S', "TCC": 'S', "TCG": 'S', "TCT": 'S',
		"TTC": 'F', "TTT": 'F', "TTA": 'L', "TTG": 'L',
		"TAC": 'Y', "TAT": 'Y', "TAA": '*', "TAG": '*',
		"TGC": 'C', "TGT": 'C', "TGA": '*', "TGG": 'W',
	}
	return genCode[string(codon)]
}
Example #26
0
func pluginCmd(path string) *exec.Cmd {
	cmdPath := ""

	// If the path doesn't contain a separator, look in the same
	// directory as the Terraform executable first.
	if !strings.ContainsRune(path, os.PathSeparator) {
		exePath, err := osext.Executable()
		if err == nil {
			temp := filepath.Join(
				filepath.Dir(exePath),
				filepath.Base(path))

			if _, err := os.Stat(temp); err == nil {
				cmdPath = temp
			}
		}

		// If we still haven't found the executable, look for it
		// in the PATH.
		if v, err := exec.LookPath(path); err == nil {
			cmdPath = v
		}
	}

	// If we still don't have a path, then just set it to the original
	// given path.
	if cmdPath == "" {
		cmdPath = path
	}

	// Build the command to execute the plugin
	return exec.Command(cmdPath)
}
Example #27
0
func (cfg *ServerConfig) SetDefaults() *ServerConfig {
	if cfg.Address == "" {
		log.Panic("Could not init iproto.ClientServer with empty address")
	}

	if cfg.Network == "" {
		/* try to predict kind of network: if we have port separator, than it is tcp :) */
		if strings.ContainsRune(cfg.Address, ':') {
			cfg.Network = "tcp"
		} else {
			cfg.Network = "unix"
		}
	}

	if cfg.Name == "" {
		cfg.Name = cfg.Network + ":" + cfg.Address
	}

	if cfg.Connections <= 0 {
		cfg.Connections = 1
	}

	if cfg.PingInterval == 0 {
		cfg.PingInterval = DefaultPingInterval
	}

	if cfg.DialTimeout == 0 {
		cfg.DialTimeout = 5 * time.Second
	}

	return cfg
}
Example #28
0
// expandEnv expands any $XXX invocations in word.
func (g *Generator) expandEnv(word string) string {
	if !strings.ContainsRune(word, '$') {
		return word
	}
	var buf bytes.Buffer
	var w int
	var r rune
	for i := 0; i < len(word); i += w {
		r, w = utf8.DecodeRuneInString(word[i:])
		if r != '$' {
			buf.WriteRune(r)
			continue
		}
		w += g.identLength(word[i+w:])
		envVar := word[i+1 : i+w]
		var sub string
		switch envVar {
		case "GOARCH":
			sub = runtime.GOARCH
		case "GOOS":
			sub = runtime.GOOS
		case "GOFILE":
			sub = g.file
		case "GOPACKAGE":
			sub = g.pkg
		default:
			sub = os.Getenv(envVar)
		}
		buf.WriteString(sub)
	}
	return buf.String()
}
Example #29
0
// scanString scans a single input string.
func scanString(s string) (str, tail string, err error) {
	if s = skipSpace(s); s == "" {
		return s, s, errors.New("missing string")
	}
	buf := [16]byte{} // small but enough to hold most cases.
	value := buf[:0]
	for s != "" {
		if consume(&s, '\'') {
			i := strings.IndexByte(s, '\'')
			if i == -1 {
				return "", "", errors.New(`unmatched single quote`)
			}
			if i == 0 {
				value = append(value, '\'')
			} else {
				value = append(value, s[:i]...)
			}
			s = s[i+1:]
			continue
		}
		r, sz := utf8.DecodeRuneInString(s)
		if unicode.IsSpace(r) || strings.ContainsRune("&<=#", r) {
			break
		}
		value = append(value, s[:sz]...)
		s = s[sz:]
	}
	return string(value), skipSpace(s), nil
}
Example #30
0
// Subscribe subscribes to a value from the metadata service.
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
// The suffix may contain query parameters.
//
// Subscribe calls fn with the latest metadata value indicated by the provided
// suffix. If the metadata value is deleted, fn is called with the empty string
// and ok false. Subscribe blocks until fn returns a non-nil error or the value
// is deleted. Subscribe returns the error value returned from the last call to
// fn, which may be nil when ok == false.
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
	const failedSubscribeSleep = time.Second * 5

	// First check to see if the metadata value exists at all.
	val, lastETag, err := getETag(suffix)
	if err != nil {
		return err
	}

	if err := fn(val, true); err != nil {
		return err
	}

	ok := true
	if strings.ContainsRune(suffix, '?') {
		suffix += "&wait_for_change=true&last_etag="
	} else {
		suffix += "?wait_for_change=true&last_etag="
	}
	for {
		val, etag, err := getETag(suffix + url.QueryEscape(lastETag))
		if err != nil {
			if _, deleted := err.(NotDefinedError); !deleted {
				time.Sleep(failedSubscribeSleep)
				continue // Retry on other errors.
			}
			ok = false
		}
		lastETag = etag

		if err := fn(val, ok); err != nil || !ok {
			return err
		}
	}
}