1// Copyright 2018 Frank Schroeder. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package properties
6
7// BUG(frank): Set() does not check for invalid unicode literals since this is currently handled by the lexer.
8// BUG(frank): Write() does not allow to configure the newline character. Therefore, on Windows LF is used.
9
10import (
11	"bytes"
12	"fmt"
13	"io"
14	"log"
15	"os"
16	"regexp"
17	"sort"
18	"strconv"
19	"strings"
20	"time"
21	"unicode/utf8"
22)
23
24const maxExpansionDepth = 64
25
26// ErrorHandlerFunc defines the type of function which handles failures
27// of the MustXXX() functions. An error handler function must exit
28// the application after handling the error.
29type ErrorHandlerFunc func(error)
30
31// ErrorHandler is the function which handles failures of the MustXXX()
32// functions. The default is LogFatalHandler.
33var ErrorHandler ErrorHandlerFunc = LogFatalHandler
34
35// LogHandlerFunc defines the function prototype for logging errors.
36type LogHandlerFunc func(fmt string, args ...interface{})
37
38// LogPrintf defines a log handler which uses log.Printf.
39var LogPrintf LogHandlerFunc = log.Printf
40
41// LogFatalHandler handles the error by logging a fatal error and exiting.
42func LogFatalHandler(err error) {
43	log.Fatal(err)
44}
45
46// PanicHandler handles the error by panicking.
47func PanicHandler(err error) {
48	panic(err)
49}
50
51// -----------------------------------------------------------------------------
52
53// A Properties contains the key/value pairs from the properties input.
54// All values are stored in unexpanded form and are expanded at runtime
55type Properties struct {
56	// Pre-/Postfix for property expansion.
57	Prefix  string
58	Postfix string
59
60	// DisableExpansion controls the expansion of properties on Get()
61	// and the check for circular references on Set(). When set to
62	// true Properties behaves like a simple key/value store and does
63	// not check for circular references on Get() or on Set().
64	DisableExpansion bool
65
66	// Stores the key/value pairs
67	m map[string]string
68
69	// Stores the comments per key.
70	c map[string][]string
71
72	// Stores the keys in order of appearance.
73	k []string
74
75	// WriteSeparator specifies the separator of key and value while writing the properties.
76	WriteSeparator string
77}
78
79// NewProperties creates a new Properties struct with the default
80// configuration for "${key}" expressions.
81func NewProperties() *Properties {
82	return &Properties{
83		Prefix:  "${",
84		Postfix: "}",
85		m:       map[string]string{},
86		c:       map[string][]string{},
87		k:       []string{},
88	}
89}
90
91// Load reads a buffer into the given Properties struct.
92func (p *Properties) Load(buf []byte, enc Encoding) error {
93	l := &Loader{Encoding: enc, DisableExpansion: p.DisableExpansion}
94	newProperties, err := l.LoadBytes(buf)
95	if err != nil {
96		return err
97	}
98	p.Merge(newProperties)
99	return nil
100}
101
102// Get returns the expanded value for the given key if exists.
103// Otherwise, ok is false.
104func (p *Properties) Get(key string) (value string, ok bool) {
105	v, ok := p.m[key]
106	if p.DisableExpansion {
107		return v, ok
108	}
109	if !ok {
110		return "", false
111	}
112
113	expanded, err := p.expand(key, v)
114
115	// we guarantee that the expanded value is free of
116	// circular references and malformed expressions
117	// so we panic if we still get an error here.
118	if err != nil {
119		ErrorHandler(err)
120	}
121
122	return expanded, true
123}
124
125// MustGet returns the expanded value for the given key if exists.
126// Otherwise, it panics.
127func (p *Properties) MustGet(key string) string {
128	if v, ok := p.Get(key); ok {
129		return v
130	}
131	ErrorHandler(invalidKeyError(key))
132	panic("ErrorHandler should exit")
133}
134
135// ----------------------------------------------------------------------------
136
137// ClearComments removes the comments for all keys.
138func (p *Properties) ClearComments() {
139	p.c = map[string][]string{}
140}
141
142// ----------------------------------------------------------------------------
143
144// GetComment returns the last comment before the given key or an empty string.
145func (p *Properties) GetComment(key string) string {
146	comments, ok := p.c[key]
147	if !ok || len(comments) == 0 {
148		return ""
149	}
150	return comments[len(comments)-1]
151}
152
153// ----------------------------------------------------------------------------
154
155// GetComments returns all comments that appeared before the given key or nil.
156func (p *Properties) GetComments(key string) []string {
157	if comments, ok := p.c[key]; ok {
158		return comments
159	}
160	return nil
161}
162
163// ----------------------------------------------------------------------------
164
165// SetComment sets the comment for the key.
166func (p *Properties) SetComment(key, comment string) {
167	p.c[key] = []string{comment}
168}
169
170// ----------------------------------------------------------------------------
171
172// SetComments sets the comments for the key. If the comments are nil then
173// all comments for this key are deleted.
174func (p *Properties) SetComments(key string, comments []string) {
175	if comments == nil {
176		delete(p.c, key)
177		return
178	}
179	p.c[key] = comments
180}
181
182// ----------------------------------------------------------------------------
183
184// GetBool checks if the expanded value is one of '1', 'yes',
185// 'true' or 'on' if the key exists. The comparison is case-insensitive.
186// If the key does not exist the default value is returned.
187func (p *Properties) GetBool(key string, def bool) bool {
188	v, err := p.getBool(key)
189	if err != nil {
190		return def
191	}
192	return v
193}
194
195// MustGetBool checks if the expanded value is one of '1', 'yes',
196// 'true' or 'on' if the key exists. The comparison is case-insensitive.
197// If the key does not exist the function panics.
198func (p *Properties) MustGetBool(key string) bool {
199	v, err := p.getBool(key)
200	if err != nil {
201		ErrorHandler(err)
202	}
203	return v
204}
205
206func (p *Properties) getBool(key string) (value bool, err error) {
207	if v, ok := p.Get(key); ok {
208		return boolVal(v), nil
209	}
210	return false, invalidKeyError(key)
211}
212
213func boolVal(v string) bool {
214	v = strings.ToLower(v)
215	return v == "1" || v == "true" || v == "yes" || v == "on"
216}
217
218// ----------------------------------------------------------------------------
219
220// GetDuration parses the expanded value as an time.Duration (in ns) if the
221// key exists. If key does not exist or the value cannot be parsed the default
222// value is returned. In almost all cases you want to use GetParsedDuration().
223func (p *Properties) GetDuration(key string, def time.Duration) time.Duration {
224	v, err := p.getInt64(key)
225	if err != nil {
226		return def
227	}
228	return time.Duration(v)
229}
230
231// MustGetDuration parses the expanded value as an time.Duration (in ns) if
232// the key exists. If key does not exist or the value cannot be parsed the
233// function panics. In almost all cases you want to use MustGetParsedDuration().
234func (p *Properties) MustGetDuration(key string) time.Duration {
235	v, err := p.getInt64(key)
236	if err != nil {
237		ErrorHandler(err)
238	}
239	return time.Duration(v)
240}
241
242// ----------------------------------------------------------------------------
243
244// GetParsedDuration parses the expanded value with time.ParseDuration() if the key exists.
245// If key does not exist or the value cannot be parsed the default
246// value is returned.
247func (p *Properties) GetParsedDuration(key string, def time.Duration) time.Duration {
248	s, ok := p.Get(key)
249	if !ok {
250		return def
251	}
252	v, err := time.ParseDuration(s)
253	if err != nil {
254		return def
255	}
256	return v
257}
258
259// MustGetParsedDuration parses the expanded value with time.ParseDuration() if the key exists.
260// If key does not exist or the value cannot be parsed the function panics.
261func (p *Properties) MustGetParsedDuration(key string) time.Duration {
262	s, ok := p.Get(key)
263	if !ok {
264		ErrorHandler(invalidKeyError(key))
265	}
266	v, err := time.ParseDuration(s)
267	if err != nil {
268		ErrorHandler(err)
269	}
270	return v
271}
272
273// ----------------------------------------------------------------------------
274
275// GetFloat64 parses the expanded value as a float64 if the key exists.
276// If key does not exist or the value cannot be parsed the default
277// value is returned.
278func (p *Properties) GetFloat64(key string, def float64) float64 {
279	v, err := p.getFloat64(key)
280	if err != nil {
281		return def
282	}
283	return v
284}
285
286// MustGetFloat64 parses the expanded value as a float64 if the key exists.
287// If key does not exist or the value cannot be parsed the function panics.
288func (p *Properties) MustGetFloat64(key string) float64 {
289	v, err := p.getFloat64(key)
290	if err != nil {
291		ErrorHandler(err)
292	}
293	return v
294}
295
296func (p *Properties) getFloat64(key string) (value float64, err error) {
297	if v, ok := p.Get(key); ok {
298		value, err = strconv.ParseFloat(v, 64)
299		if err != nil {
300			return 0, err
301		}
302		return value, nil
303	}
304	return 0, invalidKeyError(key)
305}
306
307// ----------------------------------------------------------------------------
308
309// GetInt parses the expanded value as an int if the key exists.
310// If key does not exist or the value cannot be parsed the default
311// value is returned. If the value does not fit into an int the
312// function panics with an out of range error.
313func (p *Properties) GetInt(key string, def int) int {
314	v, err := p.getInt64(key)
315	if err != nil {
316		return def
317	}
318	return intRangeCheck(key, v)
319}
320
321// MustGetInt parses the expanded value as an int if the key exists.
322// If key does not exist or the value cannot be parsed the function panics.
323// If the value does not fit into an int the function panics with
324// an out of range error.
325func (p *Properties) MustGetInt(key string) int {
326	v, err := p.getInt64(key)
327	if err != nil {
328		ErrorHandler(err)
329	}
330	return intRangeCheck(key, v)
331}
332
333// ----------------------------------------------------------------------------
334
335// GetInt64 parses the expanded value as an int64 if the key exists.
336// If key does not exist or the value cannot be parsed the default
337// value is returned.
338func (p *Properties) GetInt64(key string, def int64) int64 {
339	v, err := p.getInt64(key)
340	if err != nil {
341		return def
342	}
343	return v
344}
345
346// MustGetInt64 parses the expanded value as an int if the key exists.
347// If key does not exist or the value cannot be parsed the function panics.
348func (p *Properties) MustGetInt64(key string) int64 {
349	v, err := p.getInt64(key)
350	if err != nil {
351		ErrorHandler(err)
352	}
353	return v
354}
355
356func (p *Properties) getInt64(key string) (value int64, err error) {
357	if v, ok := p.Get(key); ok {
358		value, err = strconv.ParseInt(v, 10, 64)
359		if err != nil {
360			return 0, err
361		}
362		return value, nil
363	}
364	return 0, invalidKeyError(key)
365}
366
367// ----------------------------------------------------------------------------
368
369// GetUint parses the expanded value as an uint if the key exists.
370// If key does not exist or the value cannot be parsed the default
371// value is returned. If the value does not fit into an int the
372// function panics with an out of range error.
373func (p *Properties) GetUint(key string, def uint) uint {
374	v, err := p.getUint64(key)
375	if err != nil {
376		return def
377	}
378	return uintRangeCheck(key, v)
379}
380
381// MustGetUint parses the expanded value as an int if the key exists.
382// If key does not exist or the value cannot be parsed the function panics.
383// If the value does not fit into an int the function panics with
384// an out of range error.
385func (p *Properties) MustGetUint(key string) uint {
386	v, err := p.getUint64(key)
387	if err != nil {
388		ErrorHandler(err)
389	}
390	return uintRangeCheck(key, v)
391}
392
393// ----------------------------------------------------------------------------
394
395// GetUint64 parses the expanded value as an uint64 if the key exists.
396// If key does not exist or the value cannot be parsed the default
397// value is returned.
398func (p *Properties) GetUint64(key string, def uint64) uint64 {
399	v, err := p.getUint64(key)
400	if err != nil {
401		return def
402	}
403	return v
404}
405
406// MustGetUint64 parses the expanded value as an int if the key exists.
407// If key does not exist or the value cannot be parsed the function panics.
408func (p *Properties) MustGetUint64(key string) uint64 {
409	v, err := p.getUint64(key)
410	if err != nil {
411		ErrorHandler(err)
412	}
413	return v
414}
415
416func (p *Properties) getUint64(key string) (value uint64, err error) {
417	if v, ok := p.Get(key); ok {
418		value, err = strconv.ParseUint(v, 10, 64)
419		if err != nil {
420			return 0, err
421		}
422		return value, nil
423	}
424	return 0, invalidKeyError(key)
425}
426
427// ----------------------------------------------------------------------------
428
429// GetString returns the expanded value for the given key if exists or
430// the default value otherwise.
431func (p *Properties) GetString(key, def string) string {
432	if v, ok := p.Get(key); ok {
433		return v
434	}
435	return def
436}
437
438// MustGetString returns the expanded value for the given key if exists or
439// panics otherwise.
440func (p *Properties) MustGetString(key string) string {
441	if v, ok := p.Get(key); ok {
442		return v
443	}
444	ErrorHandler(invalidKeyError(key))
445	panic("ErrorHandler should exit")
446}
447
448// ----------------------------------------------------------------------------
449
450// Filter returns a new properties object which contains all properties
451// for which the key matches the pattern.
452func (p *Properties) Filter(pattern string) (*Properties, error) {
453	re, err := regexp.Compile(pattern)
454	if err != nil {
455		return nil, err
456	}
457
458	return p.FilterRegexp(re), nil
459}
460
461// FilterRegexp returns a new properties object which contains all properties
462// for which the key matches the regular expression.
463func (p *Properties) FilterRegexp(re *regexp.Regexp) *Properties {
464	pp := NewProperties()
465	for _, k := range p.k {
466		if re.MatchString(k) {
467			// TODO(fs): we are ignoring the error which flags a circular reference.
468			// TODO(fs): since we are just copying a subset of keys this cannot happen (fingers crossed)
469			pp.Set(k, p.m[k])
470		}
471	}
472	return pp
473}
474
475// FilterPrefix returns a new properties object with a subset of all keys
476// with the given prefix.
477func (p *Properties) FilterPrefix(prefix string) *Properties {
478	pp := NewProperties()
479	for _, k := range p.k {
480		if strings.HasPrefix(k, prefix) {
481			// TODO(fs): we are ignoring the error which flags a circular reference.
482			// TODO(fs): since we are just copying a subset of keys this cannot happen (fingers crossed)
483			pp.Set(k, p.m[k])
484		}
485	}
486	return pp
487}
488
489// FilterStripPrefix returns a new properties object with a subset of all keys
490// with the given prefix and the prefix removed from the keys.
491func (p *Properties) FilterStripPrefix(prefix string) *Properties {
492	pp := NewProperties()
493	n := len(prefix)
494	for _, k := range p.k {
495		if len(k) > len(prefix) && strings.HasPrefix(k, prefix) {
496			// TODO(fs): we are ignoring the error which flags a circular reference.
497			// TODO(fs): since we are modifying keys I am not entirely sure whether we can create a circular reference
498			// TODO(fs): this function should probably return an error but the signature is fixed
499			pp.Set(k[n:], p.m[k])
500		}
501	}
502	return pp
503}
504
505// Len returns the number of keys.
506func (p *Properties) Len() int {
507	return len(p.m)
508}
509
510// Keys returns all keys in the same order as in the input.
511func (p *Properties) Keys() []string {
512	keys := make([]string, len(p.k))
513	copy(keys, p.k)
514	return keys
515}
516
517// Set sets the property key to the corresponding value.
518// If a value for key existed before then ok is true and prev
519// contains the previous value. If the value contains a
520// circular reference or a malformed expression then
521// an error is returned.
522// An empty key is silently ignored.
523func (p *Properties) Set(key, value string) (prev string, ok bool, err error) {
524	if key == "" {
525		return "", false, nil
526	}
527
528	// if expansion is disabled we allow circular references
529	if p.DisableExpansion {
530		prev, ok = p.Get(key)
531		p.m[key] = value
532		if !ok {
533			p.k = append(p.k, key)
534		}
535		return prev, ok, nil
536	}
537
538	// to check for a circular reference we temporarily need
539	// to set the new value. If there is an error then revert
540	// to the previous state. Only if all tests are successful
541	// then we add the key to the p.k list.
542	prev, ok = p.Get(key)
543	p.m[key] = value
544
545	// now check for a circular reference
546	_, err = p.expand(key, value)
547	if err != nil {
548
549		// revert to the previous state
550		if ok {
551			p.m[key] = prev
552		} else {
553			delete(p.m, key)
554		}
555
556		return "", false, err
557	}
558
559	if !ok {
560		p.k = append(p.k, key)
561	}
562
563	return prev, ok, nil
564}
565
566// SetValue sets property key to the default string value
567// as defined by fmt.Sprintf("%v").
568func (p *Properties) SetValue(key string, value interface{}) error {
569	_, _, err := p.Set(key, fmt.Sprintf("%v", value))
570	return err
571}
572
573// MustSet sets the property key to the corresponding value.
574// If a value for key existed before then ok is true and prev
575// contains the previous value. An empty key is silently ignored.
576func (p *Properties) MustSet(key, value string) (prev string, ok bool) {
577	prev, ok, err := p.Set(key, value)
578	if err != nil {
579		ErrorHandler(err)
580	}
581	return prev, ok
582}
583
584// String returns a string of all expanded 'key = value' pairs.
585func (p *Properties) String() string {
586	var s string
587	for _, key := range p.k {
588		value, _ := p.Get(key)
589		s = fmt.Sprintf("%s%s = %s\n", s, key, value)
590	}
591	return s
592}
593
594// Sort sorts the properties keys in alphabetical order.
595// This is helpfully before writing the properties.
596func (p *Properties) Sort() {
597	sort.Strings(p.k)
598}
599
600// Write writes all unexpanded 'key = value' pairs to the given writer.
601// Write returns the number of bytes written and any write error encountered.
602func (p *Properties) Write(w io.Writer, enc Encoding) (n int, err error) {
603	return p.WriteComment(w, "", enc)
604}
605
606// WriteComment writes all unexpanced 'key = value' pairs to the given writer.
607// If prefix is not empty then comments are written with a blank line and the
608// given prefix. The prefix should be either "# " or "! " to be compatible with
609// the properties file format. Otherwise, the properties parser will not be
610// able to read the file back in. It returns the number of bytes written and
611// any write error encountered.
612func (p *Properties) WriteComment(w io.Writer, prefix string, enc Encoding) (n int, err error) {
613	var x int
614
615	for _, key := range p.k {
616		value := p.m[key]
617
618		if prefix != "" {
619			if comments, ok := p.c[key]; ok {
620				// don't print comments if they are all empty
621				allEmpty := true
622				for _, c := range comments {
623					if c != "" {
624						allEmpty = false
625						break
626					}
627				}
628
629				if !allEmpty {
630					// add a blank line between entries but not at the top
631					if len(comments) > 0 && n > 0 {
632						x, err = fmt.Fprintln(w)
633						if err != nil {
634							return
635						}
636						n += x
637					}
638
639					for _, c := range comments {
640						x, err = fmt.Fprintf(w, "%s%s\n", prefix, c)
641						if err != nil {
642							return
643						}
644						n += x
645					}
646				}
647			}
648		}
649		sep := " = "
650		if p.WriteSeparator != "" {
651			sep = p.WriteSeparator
652		}
653		x, err = fmt.Fprintf(w, "%s%s%s\n", encode(key, " :", enc), sep, encode(value, "", enc))
654		if err != nil {
655			return
656		}
657		n += x
658	}
659	return
660}
661
662// Map returns a copy of the properties as a map.
663func (p *Properties) Map() map[string]string {
664	m := make(map[string]string)
665	for k, v := range p.m {
666		m[k] = v
667	}
668	return m
669}
670
671// FilterFunc returns a copy of the properties which includes the values which passed all filters.
672func (p *Properties) FilterFunc(filters ...func(k, v string) bool) *Properties {
673	pp := NewProperties()
674outer:
675	for k, v := range p.m {
676		for _, f := range filters {
677			if !f(k, v) {
678				continue outer
679			}
680			pp.Set(k, v)
681		}
682	}
683	return pp
684}
685
686// ----------------------------------------------------------------------------
687
688// Delete removes the key and its comments.
689func (p *Properties) Delete(key string) {
690	delete(p.m, key)
691	delete(p.c, key)
692	newKeys := []string{}
693	for _, k := range p.k {
694		if k != key {
695			newKeys = append(newKeys, k)
696		}
697	}
698	p.k = newKeys
699}
700
701// Merge merges properties, comments and keys from other *Properties into p
702func (p *Properties) Merge(other *Properties) {
703	for k, v := range other.m {
704		p.m[k] = v
705	}
706	for k, v := range other.c {
707		p.c[k] = v
708	}
709
710outer:
711	for _, otherKey := range other.k {
712		for _, key := range p.k {
713			if otherKey == key {
714				continue outer
715			}
716		}
717		p.k = append(p.k, otherKey)
718	}
719}
720
721// ----------------------------------------------------------------------------
722
723// check expands all values and returns an error if a circular reference or
724// a malformed expression was found.
725func (p *Properties) check() error {
726	for key, value := range p.m {
727		if _, err := p.expand(key, value); err != nil {
728			return err
729		}
730	}
731	return nil
732}
733
734func (p *Properties) expand(key, input string) (string, error) {
735	// no pre/postfix -> nothing to expand
736	if p.Prefix == "" && p.Postfix == "" {
737		return input, nil
738	}
739
740	return expand(input, []string{key}, p.Prefix, p.Postfix, p.m)
741}
742
743// expand recursively expands expressions of '(prefix)key(postfix)' to their corresponding values.
744// The function keeps track of the keys that were already expanded and stops if it
745// detects a circular reference or a malformed expression of the form '(prefix)key'.
746func expand(s string, keys []string, prefix, postfix string, values map[string]string) (string, error) {
747	if len(keys) > maxExpansionDepth {
748		return "", fmt.Errorf("expansion too deep")
749	}
750
751	for {
752		start := strings.Index(s, prefix)
753		if start == -1 {
754			return s, nil
755		}
756
757		keyStart := start + len(prefix)
758		keyLen := strings.Index(s[keyStart:], postfix)
759		if keyLen == -1 {
760			return "", fmt.Errorf("malformed expression")
761		}
762
763		end := keyStart + keyLen + len(postfix) - 1
764		key := s[keyStart : keyStart+keyLen]
765
766		// fmt.Printf("s:%q pp:%q start:%d end:%d keyStart:%d keyLen:%d key:%q\n", s, prefix + "..." + postfix, start, end, keyStart, keyLen, key)
767
768		for _, k := range keys {
769			if key == k {
770				var b bytes.Buffer
771				b.WriteString("circular reference in:\n")
772				for _, k1 := range keys {
773					fmt.Fprintf(&b, "%s=%s\n", k1, values[k1])
774				}
775				return "", fmt.Errorf(b.String())
776			}
777		}
778
779		val, ok := values[key]
780		if !ok {
781			val = os.Getenv(key)
782		}
783		new_val, err := expand(val, append(keys, key), prefix, postfix, values)
784		if err != nil {
785			return "", err
786		}
787		s = s[:start] + new_val + s[end+1:]
788	}
789	return s, nil
790}
791
792// encode encodes a UTF-8 string to ISO-8859-1 and escapes some characters.
793func encode(s string, special string, enc Encoding) string {
794	switch enc {
795	case UTF8:
796		return encodeUtf8(s, special)
797	case ISO_8859_1:
798		return encodeIso(s, special)
799	default:
800		panic(fmt.Sprintf("unsupported encoding %v", enc))
801	}
802}
803
804func encodeUtf8(s string, special string) string {
805	v := ""
806	for pos := 0; pos < len(s); {
807		r, w := utf8.DecodeRuneInString(s[pos:])
808		pos += w
809		v += escape(r, special)
810	}
811	return v
812}
813
814func encodeIso(s string, special string) string {
815	var r rune
816	var w int
817	var v string
818	for pos := 0; pos < len(s); {
819		switch r, w = utf8.DecodeRuneInString(s[pos:]); {
820		case r < 1<<8: // single byte rune -> escape special chars only
821			v += escape(r, special)
822		case r < 1<<16: // two byte rune -> unicode literal
823			v += fmt.Sprintf("\\u%04x", r)
824		default: // more than two bytes per rune -> can't encode
825			v += "?"
826		}
827		pos += w
828	}
829	return v
830}
831
832func escape(r rune, special string) string {
833	switch r {
834	case '\f':
835		return "\\f"
836	case '\n':
837		return "\\n"
838	case '\r':
839		return "\\r"
840	case '\t':
841		return "\\t"
842	case '\\':
843		return "\\\\"
844	default:
845		if strings.ContainsRune(special, r) {
846			return "\\" + string(r)
847		}
848		return string(r)
849	}
850}
851
852func invalidKeyError(key string) error {
853	return fmt.Errorf("unknown property: %s", key)
854}
855