Treat circular references and malformed expressions as errors. Add Set() method for setting values. Renamed GetDefault() to GetString(). Added GetFloat, GetInt, GetUint and GetBool convenience methods.
diff --git a/doc.go b/doc.go index a11d46b..c140a35 100644 --- a/doc.go +++ b/doc.go
@@ -2,14 +2,50 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// goproperties provides functions for reading -// ISO-8859-1 (Java) and UTF-8 encoded .properties files and has -// support for Spring-like property expansion. +// goproperties provides functions for reading and writing +// ISO-8859-1 and UTF-8 encoded .properties files and has +// support for recursive property expansion. // +// Java properties files are ISO-8859-1 encoded and use Unicode +// literals for characters outside the ISO character set. Unicode +// literals can be used in UTF-8 encoded properties files but +// aren't necessary. +// +// All of the different key/value delimiters " :=" are supported +// as well as the comment characters '!' and '#' and multi-line +// values. +// +// ! this is a comment +// # and so is this +// +// # the following expressions are equal +// key value +// key=value +// key:value // key = value -// # key2 = value +// key : value +// key = val\ +// ue +// +// Property expansion is recursive and circular references +// and malformed expressions are not allowed and cause an +// error. +// +// # standard property +// key = value +// +// # property expansion: key2 = value // key2 = ${key} // +// # recursive property expansion: key3 = value +// key3 = ${key2} +// +// # circular reference (error) +// key = ${key} +// +// # malformed expression (error) +// key = ${ke +// // The default property expansion format is ${key} but can be // changed by setting different pre- and postfix values on the // Properties object. @@ -18,20 +54,7 @@ // p.Prefix = "#[" // p.Postfix = "]#" // -// Property expansion is recursive and circular references are not allowed. -// If a circular reference is detected an error is logged and the -// unexpanded value is returned. -// -// # Circular reference -// key = ${key} -// -// # Malformed expression -// key = ${ke -// -// When writing properties to a writer currently only ISO-8859-1 encoding -// is supported. -// -// See one of the following links for a description of the properties +// The following documents provide a description of the properties // file format. // // http://en.wikipedia.org/wiki/.properties
diff --git a/example_test.go b/example_test.go index 18081aa..14811d5 100644 --- a/example_test.go +++ b/example_test.go
@@ -31,9 +31,9 @@ // UTF-8 value with unicode character ⌘ and umlaut ä } -func Example_Properties_GetDefault() { +func Example_Properties_GetString() { p, _ := Load([]byte("key=value"), ISO_8859_1) - v := p.GetDefault("another key", "default value") + v := p.GetString("another key", "default value") fmt.Println(v) // Output: // default value @@ -57,7 +57,7 @@ } // Get a key with a default value - v := p.GetDefault("does not exist", "some value") + v := p.GetString("does not exist", "some value") fmt.Println(v) // Dump the expanded key/value pairs of the Properties
diff --git a/lex.go b/lex.go index 562b85b..b1e71bf 100644 --- a/lex.go +++ b/lex.go
@@ -322,7 +322,7 @@ return l.scanUnicodeLiteral() case isEOF(r): - return fmt.Errorf("premature EOF") + return fmt.Errorf("Premature EOF") // silently drop the escape character and append the rune as is default: @@ -339,7 +339,7 @@ for i := 0; i < 4; i++ { d[i] = l.next() if d[i] == eof || !strings.ContainsRune("0123456789abcdefABCDEF", d[i]) { - return fmt.Errorf("invalid unicode literal") + return fmt.Errorf("Invalid unicode literal") } }
diff --git a/load.go b/load.go index b77da41..a0fad7f 100644 --- a/load.go +++ b/load.go
@@ -50,7 +50,12 @@ ) func loadBuf(buf []byte, enc Encoding) (*Properties, error) { - return parse(convert(buf, enc)) + p, err := parse(convert(buf, enc)) + if err != nil { + return nil, err + } + + return p, p.check() } func loadFiles(filenames []string, enc Encoding, ignoreMissing bool) (*Properties, error) { @@ -96,6 +101,6 @@ } return string(runes) default: - panic(fmt.Sprintf("unsupported encoding %v", enc)) + panic(fmt.Sprintf("Unsupported encoding %v", enc)) } }
diff --git a/properties.go b/properties.go index 4b3b5d1..f87af49 100644 --- a/properties.go +++ b/properties.go
@@ -7,7 +7,7 @@ import ( "fmt" "io" - "log" + "strconv" "strings" "unicode/utf8" ) @@ -39,22 +39,75 @@ expanded, err := p.expand(v) - // if there is an error then this is a format exception which we just log - // and return the input unchanged. + // we guarantee that the expanded value is free of + // circular references and malformed expressions + // so we panic if we still get an error here. if err != nil { - log.Printf("%s in %q", err, key+" = "+v) - return v, true + panic(fmt.Errorf("%s in %q", err, key+" = "+v)) } return expanded, true } -// GetDefault returns the expanded value for the given key if exists or the default value otherwise. -func (p *Properties) GetDefault(key, defaultValue string) (value string) { +// GetBool checks if the expanded value is one of '1', 'yes', +// 'true' or 'on' if the key exists. The comparison is case-insensitive. +// If the key does not exist the default value is returned. +func (p *Properties) GetBool(key string, def bool) bool { + if v, ok := p.Get(key); ok { + v = strings.ToLower(v) + return v == "1" || v == "true" || v == "yes" || v == "on" + } + return def +} + +// GetFloat parses the expanded value as a float64 if the key exists. +// If key does not exist or the value cannot be parsed the default +// value is returned. +func (p *Properties) GetFloat(key string, def float64) float64 { + if v, ok := p.Get(key); ok { + n, err := strconv.ParseFloat(v, 64) + if err != nil { + return def + } + return n + } + return def +} + +// GetInt parses the expanded value as an int if the key exists. +// If key does not exist or the value cannot be parsed the default +// value is returned. +func (p *Properties) GetInt(key string, def int64) int64 { + if v, ok := p.Get(key); ok { + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return def + } + return n + } + return def +} + +// GetUint parses the expanded value as an uint64 if the key exists. +// If key does not exist or the value cannot be parsed the default +// value is returned. +func (p *Properties) GetUint(key string, def uint64) uint64 { + if v, ok := p.Get(key); ok { + n, err := strconv.ParseUint(v, 10, 64) + if err != nil { + return def + } + return n + } + return def +} + +// GetString returns the expanded value for the given key if exists or the default value otherwise. +func (p *Properties) GetString(key, def string) string { if v, ok := p.Get(key); ok { return v } - return defaultValue + return def } // Len returns the number of keys. @@ -62,6 +115,23 @@ return len(p.m) } +// Set sets the property key to the corresponding value. +// If a value for key existed before then ok is true and prev +// contains the previous value. If the value contains a +// circular reference or a malformed expression then +// an error is returned. +func (p *Properties) Set(key, value string) (prev string, ok bool, err error) { + // TODO(frank): Check for invalid unicode literals since this is currently done in the lexer. + _, err = p.expand(value) + if err != nil { + return "", false, err + } + + v, ok := p.Get(key) + p.m[key] = value + return v, ok, nil +} + // Dump returns a string of all unexpanded 'key = value' pairs. func (p *Properties) Dump() string { var s string @@ -81,11 +151,11 @@ return s } -// Write writes all unexpanded 'key = value' pairs as ISO-8859-1 to the given writer. -func (p *Properties) Write(w io.Writer) (int, error) { +// Write writes all unexpanded 'key = value' pairs to the given writer. +func (p *Properties) Write(w io.Writer, enc Encoding) (int, error) { total := 0 for key, value := range p.m { - s := fmt.Sprintf("%s = %s\n", encode(key, " :"), encode(value, "")) + s := fmt.Sprintf("%s = %s\n", encode(key, " :", enc), encode(value, "", enc)) n, err := w.Write([]byte(s)) if err != nil { return total, err @@ -95,6 +165,19 @@ return total, nil } +// ---------------------------------------------------------------------------- + +// check expands all values and returns an error if a circular reference or +// a malformed expression was found. +func (p *Properties) check() error { + for _, value := range p.m { + if _, err := p.expand(value); err != nil { + return err + } + } + return nil +} + // expand recursively expands expressions of '(prefix)key(postfix)' to their corresponding values. // The function keeps track of the keys that were already expanded and stops if it // detects a circular reference or a malformed expression. @@ -136,7 +219,28 @@ } // encode encodes a UTF-8 string to ISO-8859-1 and escapes some characters. -func encode(s string, special string) string { +func encode(s string, special string, enc Encoding) string { + switch enc { + case UTF8: + return encodeUtf8(s, special) + case ISO_8859_1: + return encodeIso(s, special) + default: + panic(fmt.Sprintf("Unsupported encoding %v", enc)) + } +} + +func encodeUtf8(s string, special string) string { + v := "" + for pos := 0; pos < len(s); { + r, w := utf8.DecodeRuneInString(s[pos:]) + pos += w + v += escape(r, special) + } + return v +} + +func encodeIso(s string, special string) string { var r rune var w int var v string
diff --git a/properties_test.go b/properties_test.go index 0522007..36950e3 100644 --- a/properties_test.go +++ b/properties_test.go
@@ -83,33 +83,41 @@ // expansion tests {"key=value\nkey2=${key}", "key", "value", "key2", "value"}, {"key=value\nkey2=${key}\nkey3=${key2}", "key", "value", "key2", "value", "key3", "value"}, - - // circular references - {"key=${key}", "key", "${key}"}, - {"key1=${key2}\nkey2=${key1}", "key1", "${key2}", "key2", "${key1}"}, - - // malformed expressions - {"key=${ke", "key", "${ke"}, - {"key=valu${ke", "key", "valu${ke"}, } // define error test cases in the form of // {"input", "expected error message"} var errorTests = [][]string{ - {"key\\u1 = value", "invalid unicode literal"}, - {"key\\u12 = value", "invalid unicode literal"}, - {"key\\u123 = value", "invalid unicode literal"}, - {"key\\u123g = value", "invalid unicode literal"}, - {"key\\u123", "invalid unicode literal"}, + // unicode literals + {"key\\u1 = value", "Invalid unicode literal"}, + {"key\\u12 = value", "Invalid unicode literal"}, + {"key\\u123 = value", "Invalid unicode literal"}, + {"key\\u123g = value", "Invalid unicode literal"}, + {"key\\u123", "Invalid unicode literal"}, + + // circular references + {"key=${key}", "Circular reference"}, + {"key1=${key2}\nkey2=${key1}", "Circular reference"}, + + // malformed expressions + {"key=${ke", "Malformed expression"}, + {"key=valu${ke", "Malformed expression"}, } // define write encoding test cases in the form of -// {"input", "expected output after write"} +// {"input", "expected output after write", ["UTF-8", "ISO-8859-1"]} var writeTests = [][]string{ - {"key = value", "key = value\n"}, - {"key = value \\\n continued", "key = value continued\n"}, - {"key⌘ = value", "key\\u2318 = value\n"}, - {"ke\\ \\:y = value", "ke\\ \\:y = value\n"}, + // ISO-8859-1 tests + {"key = value", "key = value\n", "ISO-8859-1"}, + {"key = value \\\n continued", "key = value continued\n", "ISO-8859-1"}, + {"key⌘ = value", "key\\u2318 = value\n", "ISO-8859-1"}, + {"ke\\ \\:y = value", "ke\\ \\:y = value\n", "ISO-8859-1"}, + + // UTF-8 tests + {"key = value", "key = value\n", "UTF-8"}, + {"key = value \\\n continued", "key = value continued\n", "UTF-8"}, + {"key⌘ = value⌘", "key⌘ = value⌘\n", "UTF-8"}, + {"ke\\ \\:y = value", "ke\\ \\:y = value\n", "UTF-8"}, } // Benchmarks the decoder by creating a property file with 1000 key/value pairs. @@ -149,11 +157,17 @@ // Test write encoding. func (l *TestSuite) TestWrite(c *C) { for _, test := range writeTests { - input, output := test[0], test[1] + input, output, enc := test[0], test[1], test[2] p, err := parse(input) buf := new(bytes.Buffer) - n, err := p.Write(buf) + var n int + switch enc { + case "UTF-8": + n, err = p.Write(buf, UTF8) + case "ISO-8859-1": + n, err = p.Write(buf, ISO_8859_1) + } c.Assert(err, IsNil) s := string(buf.Bytes()) c.Assert(n, Equals, len(output), Commentf("input=%q expected=%q obtained=%q", input, output, s))