diff --git a/arguments.go b/arguments.go index 4125d86..1660917 100644 --- a/arguments.go +++ b/arguments.go @@ -1,52 +1,52 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "os" - "strings" -) - -// Arguments reads properties from the command line arguments. -// Property arguments are expected to have a common prefix and use key=value -// format. Other arguments are ignored. -// -// For example, the command: -// cmd -a -1 -z --prop.1=a --prop.2=b --prop.3 --log=debug -// with a prefix of '--prop.' would have properties "1"="a", "2"="b", and -// "3"="". -type Arguments struct { - // Prefix provides the common prefix to use when looking for property - // arguments. If not set, the default of '--' will be used. - Prefix string -} - -// Ensure that Arguments implements PropertyGetter -var _ PropertyGetter = &Arguments{} - -// Get retrieves the value of a property from the command line arguments. If -// the property does not exist, an empty string will be returned. The bool -// return value indicates whether the property was found. -func (a *Arguments) Get(key string) (string, bool) { - prefix := a.Prefix - if prefix == "" { - prefix = "--" - } - prefix = prefix + key + "=" - for _, val := range os.Args { - if strings.HasPrefix(val, prefix) { - return val[len(prefix):], true - } - } - return "", false -} - -// GetDefault retrieves the value of a property from the command line arguments. -// If the property does not exist, then the default value will be returned. -func (e *Arguments) GetDefault(key, defVal string) string { - v, ok := e.Get(key) - if !ok { - return defVal - } - return v -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "os" + "strings" +) + +// Arguments reads properties from the command line arguments. +// Property arguments are expected to have a common prefix and use key=value +// format. Other arguments are ignored. +// +// For example, the command: +// cmd -a -1 -z --prop.1=a --prop.2=b --prop.3 --log=debug +// with a prefix of '--prop.' would have properties "1"="a", "2"="b", and +// "3"="". +type Arguments struct { + // Prefix provides the common prefix to use when looking for property + // arguments. If not set, the default of '--' will be used. + Prefix string +} + +// Ensure that Arguments implements PropertyGetter +var _ PropertyGetter = &Arguments{} + +// Get retrieves the value of a property from the command line arguments. If +// the property does not exist, an empty string will be returned. The bool +// return value indicates whether the property was found. +func (a *Arguments) Get(key string) (string, bool) { + prefix := a.Prefix + if prefix == "" { + prefix = "--" + } + prefix = prefix + key + "=" + for _, val := range os.Args { + if strings.HasPrefix(val, prefix) { + return val[len(prefix):], true + } + } + return "", false +} + +// GetDefault retrieves the value of a property from the command line arguments. +// If the property does not exist, then the default value will be returned. +func (e *Arguments) GetDefault(key, defVal string) string { + v, ok := e.Get(key) + if !ok { + return defVal + } + return v +} diff --git a/arguments_test.go b/arguments_test.go index 24ee799..dbcfa8c 100644 --- a/arguments_test.go +++ b/arguments_test.go @@ -1,60 +1,60 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "os" - "testing" -) - -func TestArgumentsGet(t *testing.T) { - a := &Arguments{} - os.Args = make([]string, 0) - os.Args = append(os.Args, "prog") - os.Args = append(os.Args, "--props.test.val1=abc") - os.Args = append(os.Args, "--props.test.val2=") - - val, ok := a.Get("props.test.val1") - if !ok || val != "abc" { - t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) - } - - val, ok = a.Get("props.test.val2") - if !ok || val != "" { - t.Errorf("want: true, ''; got: %t, '%s'", ok, val) - } - - val, ok = a.Get("props.test.val3") - if ok || val != "" { - t.Errorf("want: false, ''; got %t, '%s'", ok, val) - } - - a.Prefix = "--props." - val, ok = a.Get("test.val1") - if !ok || val != "abc" { - t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) - } -} - -func TestArgumentsGetDefault(t *testing.T) { - a := &Arguments{Prefix: "--props."} - os.Args = make([]string, 0) - os.Args = append(os.Args, "prog") - os.Args = append(os.Args, "--props.test.val1=abc") - os.Args = append(os.Args, "--props.test.val2=") - - val := a.GetDefault("test.val1", "zzz") - if val != "abc" { - t.Errorf("want: 'abc'; got: '%s'", val) - } - - val = a.GetDefault("test.val2", "def") - if val != "" { - t.Errorf("want: ''; got: '%s'", val) - } - - val = a.GetDefault("test.val3", "ghi") - if val != "ghi" { - t.Errorf("want: 'ghi'; got: '%s'", val) - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "os" + "testing" +) + +func TestArgumentsGet(t *testing.T) { + a := &Arguments{} + os.Args = make([]string, 0) + os.Args = append(os.Args, "prog") + os.Args = append(os.Args, "--props.test.val1=abc") + os.Args = append(os.Args, "--props.test.val2=") + + val, ok := a.Get("props.test.val1") + if !ok || val != "abc" { + t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) + } + + val, ok = a.Get("props.test.val2") + if !ok || val != "" { + t.Errorf("want: true, ''; got: %t, '%s'", ok, val) + } + + val, ok = a.Get("props.test.val3") + if ok || val != "" { + t.Errorf("want: false, ''; got %t, '%s'", ok, val) + } + + a.Prefix = "--props." + val, ok = a.Get("test.val1") + if !ok || val != "abc" { + t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) + } +} + +func TestArgumentsGetDefault(t *testing.T) { + a := &Arguments{Prefix: "--props."} + os.Args = make([]string, 0) + os.Args = append(os.Args, "prog") + os.Args = append(os.Args, "--props.test.val1=abc") + os.Args = append(os.Args, "--props.test.val2=") + + val := a.GetDefault("test.val1", "zzz") + if val != "abc" { + t.Errorf("want: 'abc'; got: '%s'", val) + } + + val = a.GetDefault("test.val2", "def") + if val != "" { + t.Errorf("want: ''; got: '%s'", val) + } + + val = a.GetDefault("test.val3", "ghi") + if val != "ghi" { + t.Errorf("want: 'ghi'; got: '%s'", val) + } +} diff --git a/combined.go b/combined.go index 3766e20..4b73dea 100644 --- a/combined.go +++ b/combined.go @@ -1,41 +1,41 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -// Combined provides property value lookups across multiple sources. -type Combined struct { - // The property sources to use for lookup in priority order. The first - // source to have a value for a property will be used. - Sources []PropertyGetter -} - -// Ensure that Combined implements PropertyGetter -var _ PropertyGetter = &Combined{} - -// Get retrieves the value of a property from the source list. If the source -// list is empty or none of the sources has the property, an empty string will -// be returned. The bool return value indicates whether the property was found. -func (c *Combined) Get(key string) (string, bool) { - if c.Sources == nil { - return "", false - } - for _, l := range c.Sources { - val, ok := l.Get(key) - if ok { - return val, true - } - } - return "", false -} - -// GetDefault retrieves the value of a property from the source list. If the -// source list is empty or none of the sources has the property, then the -// default value will be returned. -func (c *Combined) GetDefault(key string, defVal string) string { - val, ok := c.Get(key) - if ok { - return val - } else { - return defVal - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +// Combined provides property value lookups across multiple sources. +type Combined struct { + // The property sources to use for lookup in priority order. The first + // source to have a value for a property will be used. + Sources []PropertyGetter +} + +// Ensure that Combined implements PropertyGetter +var _ PropertyGetter = &Combined{} + +// Get retrieves the value of a property from the source list. If the source +// list is empty or none of the sources has the property, an empty string will +// be returned. The bool return value indicates whether the property was found. +func (c *Combined) Get(key string) (string, bool) { + if c.Sources == nil { + return "", false + } + for _, l := range c.Sources { + val, ok := l.Get(key) + if ok { + return val, true + } + } + return "", false +} + +// GetDefault retrieves the value of a property from the source list. If the +// source list is empty or none of the sources has the property, then the +// default value will be returned. +func (c *Combined) GetDefault(key string, defVal string) string { + val, ok := c.Get(key) + if ok { + return val + } else { + return defVal + } +} diff --git a/combined_test.go b/combined_test.go index e617813..9e93ebb 100644 --- a/combined_test.go +++ b/combined_test.go @@ -1,54 +1,54 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "testing" -) - -func TestCombined(t *testing.T) { - c := &Combined{} - val, ok := c.Get("non.init") - if ok || val != "" { - t.Errorf("want: false, ''; got: %t, '%s'", ok, val) - } - - p1 := NewProperties() - p1.Set("props.test.val1", "abc") - p1.Set("props.test.val2", "") - p2 := NewProperties() - p2.Set("props.test.val1", "def") - p2.Set("props.test.val2", "") - p2.Set("props.test.val3", "ghi") - c.Sources = []PropertyGetter{p1, p2} - - val, ok = c.Get("props.test.val1") - if !ok || val != "abc" { - t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) - } - - val, ok = c.Get("props.test.val2") - if !ok || val != "" { - t.Errorf("want: true, ''; got: %t, '%s'", ok, val) - } - - val, ok = c.Get("props.test.val3") - if !ok || val != "ghi" { - t.Errorf("want: true, 'ghi'; got %t, '%s'", ok, val) - } - - val, ok = c.Get("props.test.val4") - if ok || val != "" { - t.Errorf("want: false, ''; got %t, '%s'", ok, val) - } - - val = c.GetDefault("props.test.val1", "other") - if val != "abc" { - t.Errorf("want: 'abc'; got '%s'", val) - } - - val = c.GetDefault("props.test.val4", "other") - if val != "other" { - t.Errorf("want: 'other'; got: '%s'", val) - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "testing" +) + +func TestCombined(t *testing.T) { + c := &Combined{} + val, ok := c.Get("non.init") + if ok || val != "" { + t.Errorf("want: false, ''; got: %t, '%s'", ok, val) + } + + p1 := NewProperties() + p1.Set("props.test.val1", "abc") + p1.Set("props.test.val2", "") + p2 := NewProperties() + p2.Set("props.test.val1", "def") + p2.Set("props.test.val2", "") + p2.Set("props.test.val3", "ghi") + c.Sources = []PropertyGetter{p1, p2} + + val, ok = c.Get("props.test.val1") + if !ok || val != "abc" { + t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) + } + + val, ok = c.Get("props.test.val2") + if !ok || val != "" { + t.Errorf("want: true, ''; got: %t, '%s'", ok, val) + } + + val, ok = c.Get("props.test.val3") + if !ok || val != "ghi" { + t.Errorf("want: true, 'ghi'; got %t, '%s'", ok, val) + } + + val, ok = c.Get("props.test.val4") + if ok || val != "" { + t.Errorf("want: false, ''; got %t, '%s'", ok, val) + } + + val = c.GetDefault("props.test.val1", "other") + if val != "abc" { + t.Errorf("want: 'abc'; got '%s'", val) + } + + val = c.GetDefault("props.test.val4", "other") + if val != "other" { + t.Errorf("want: 'other'; got: '%s'", val) + } +} diff --git a/configuration.go b/configuration.go index d4fe082..1766da8 100644 --- a/configuration.go +++ b/configuration.go @@ -1,440 +1,440 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "fmt" - "io/fs" - "math" - "regexp" - "strconv" - "strings" - "time" -) - -const ( - // EncryptNone represents a value that has not yet been encrypted - EncryptNone = "[enc:0]" - // EncryptAESGCM represents a value that has been encryped with AES-GCM - EncryptAESGCM = "[enc:1]" - - // EncryptDefault represents the default encryption algorithm - EncryptDefault = EncryptAESGCM -) - -// sizePattern provides the expression used for matching size values -var sizePattern = regexp.MustCompile("^([0-9.]+)\\s{0,1}([a-zA-Z]*)$") - -// Configuration represents an application's configuration parameters provided -// by properties. -// -// It can be created directly or through NewConfiguration which provides -// configuration by convention. -type Configuration struct { - // Props provides the configuration values to retrieve and/or parse. - Props PropertyGetter - - // DateFormat provides the format string to use when parsing dates as - // defined in the time package. If blank, the default 2006-01-02 is used. - DateFormat string - // StrictBool determines whether bool parsing is strict or not. When true, - // only "true" and "false" values are considered valid. When false, - // additional "boolean-like" values are accepted such as 0 and 1. See - // ParseBool for details. - StrictBool bool -} - -// NewConfiguration creates a Configuration using common conventions. -// -// The returned Configuration uses an Expander to return properties in the -// following priority order: -// 1. Command line arguments -// 2. Environment variables -// 3. -.properties for the provided prefix and profiles -// values (in order) -// 4. .properties for the provided prefix value -// The first matching property value found will be returned. -// -// An error will be returned if one of the property files could not be read or -// parsed. -func NewConfiguration(fileSys fs.StatFS, prefix string, profiles ...string) (*Configuration, error) { - c := &Combined{} - c.Sources = make([]PropertyGetter, 0) - c.Sources = append(c.Sources, &Arguments{}) - c.Sources = append(c.Sources, &Environment{Normalize: true}) - - for _, profile := range profiles { - filename := prefix + "-" + profile + ".properties" - stat, err := fileSys.Stat(filename) - if err == nil && !stat.IsDir() { - p := NewProperties() - f, err := fileSys.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - err = p.Load(f) - if err != nil { - return nil, err - } - c.Sources = append(c.Sources, p) - } - } - - filename := prefix + ".properties" - stat, err := fileSys.Stat(filename) - if err == nil && !stat.IsDir() { - p := NewProperties() - f, err := fileSys.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - err = p.Load(f) - if err != nil { - return nil, err - } - c.Sources = append(c.Sources, p) - } - return &Configuration{Props: NewExpander(c)}, nil -} - -// Get retrieves the value of a property. If the property does not exist, an -// empty string will be returned. The bool return value indicates whether -// the property was found. -func (c *Configuration) Get(key string) (string, bool) { - return c.Props.Get(key) -} - -// GetDefault retrieves the value of a property. If the property does not -// exist, then the default value will be returned. -func (c *Configuration) GetDefault(key, defVal string) string { - return c.Props.GetDefault(key, defVal) -} - -// ParseInt converts a property value to an int. If the property does not exist, -// then the default value will be returned with a nil error. If the property -// value could not be parsed, then an error and the default value will be -// returned. -func (c *Configuration) ParseInt(key string, defVal int) (int, error) { - val, ok := c.Props.Get(key) - if ok { - var err error - result, err := strconv.Atoi(val) - if err != nil { - return defVal, fmt.Errorf("invalid int value %s=%s [%w]", key, val, err) - } - return result, nil - } else { - return defVal, nil - } -} - -// ParseFloat converts a property value to a float64. If the property does not -// exist, then the default value will be returned with a nil error. If the -// property value could not be parsed, then an error and the default value will -// be returned. -func (c *Configuration) ParseFloat(key string, defVal float64) (float64, error) { - val, ok := c.Props.Get(key) - if ok { - var err error - result, err := strconv.ParseFloat(val, 64) - if err != nil { - return defVal, fmt.Errorf("invalid float value %s=%s [%w]", key, val, err) - } - return result, nil - } else { - return defVal, nil - } -} - -// ParseByteSize converts a property value in byte size format to uint64. If -// the property does not exist, then the default value will be returned with a -// nil error. If the property value could not be parsed, then an error and the -// default value will be returned. -// -// The format supported is " " where is a numeric value -// (whole number or decimal) and is a byte size unit as listed below. -// The and space between and are optional. -// -// The supported suffixes are: -// (none) - not modified (x 1) -// k - kilobytes (x 1000) -// Ki - kibibytes (x 1024) -// M - megabyte (x 1000^2) -// Mi - mebibyte (x 1024^2) -// G - gigabyte (x 1000^3) -// Gi - gibibyte (x 1024^3) -// T - terabyte (x 1000^4) -// Ti - tebibyte (x 1024^4) -// P - petabyte (x 1000^5) -// Pi - pebibyte (x 1024^5) -// E - exabyte (x 1000^6) -// Ei - exbibyte (x 1024^6) -func (c *Configuration) ParseByteSize(key string, defVal uint64) (uint64, error) { - val, ok := c.Props.Get(key) - if ok { - match := sizePattern.FindAllStringSubmatch(val, -1) - if match == nil || len(match) != 1 || len(match[0]) != 3 { - return defVal, fmt.Errorf("invalid size value %s=%s", key, val) - } - if strings.Index(match[0][1], ".") < 0 { - num, err := strconv.ParseUint(match[0][1], 10, 64) - if err != nil { - return defVal, fmt.Errorf("invalid size value %s=%s [%w]", key, val, err) - } - mult := c.byteSizeMult(match[0][2]) - if mult == 0 { - return defVal, fmt.Errorf("invalid size value %s=%s (unknown suffix)", key, val) - } - return num * mult, nil - } else { - num, err := strconv.ParseFloat(match[0][1], 64) - if err != nil { - return defVal, fmt.Errorf("invalid size value %s=%s [%w]", key, val, err) - } - mult := c.byteSizeMult(match[0][2]) - if mult == 0 { - return defVal, fmt.Errorf("invalid size value %s=%s (unknown suffix)", key, val) - } - return uint64(math.Round(num * float64(mult))), nil - } - } else { - return defVal, nil - } -} - -// byteSizeMult determines the multiplier to be used for the given unit. -func (c *Configuration) byteSizeMult(suffix string) uint64 { - switch suffix { - case "": - return 1 - case "k": - return 1000 - case "Ki": - return 1 << 10 - case "M": - return 1_000_000 - case "Mi": - return 1 << 20 - case "G": - return 1_000_000_000 - case "Gi": - return 1 << 30 - case "T": - return 1_000_000_000_000 - case "Ti": - return 1 << 40 - case "P": - return 1_000_000_000_000_000 - case "Pi": - return 1 << 50 - case "E": - return 1_000_000_000_000_000_000 - case "Ei": - return 1 << 60 - default: - return 0 - } -} - -// ParseSize converts a property value with a metric size suffix to float64. If -// the property does not exist, then the default value will be returned with a -// nil error. If the property value could not be parsed, then an error and the -// default value will be returned. -// -// The format supported is " " where is a numeric value -// (whole number or decimal) and is a size unit as listed below. -// The and the space between and are optional. -// -// The supported suffixes are: -// Y - yotta (10^24) -// Z - zetta (10^21) -// E - exa (10^18) -// P - peta (10^15) -// T - tera (10^12) -// G - giga (10^9) -// M - mega (10^6) -// k - kilo (10^3) -// h - hecto (10^2) -// da - deca (10^1) -// (none) - not modified (x 1) -// d - deci (10^-1) -// c - centi (10^-2) -// m - milli (10^-3) -// u - micro (10^-6) -// n - nano (10^-9) -// p - pico (10^-12) -// f - femto (10^-15) -// a - atto (10^-18) -// z - zepto (10^-21) -// y - yocto (10^-23) -func (c *Configuration) ParseSize(key string, defVal float64) (float64, error) { - val, ok := c.Props.Get(key) - if ok { - match := sizePattern.FindAllStringSubmatch(val, -1) - if match == nil || len(match) != 1 || len(match[0]) != 3 { - return defVal, fmt.Errorf("invalid size value %s=%s", key, val) - } - num, err := strconv.ParseFloat(match[0][1], 64) - if err != nil { - return defVal, fmt.Errorf("invalid size value %s=%s [%w]", key, val, err) - } - mult := c.sizeMult(match[0][2]) - if mult == 0 { - return defVal, fmt.Errorf("invalid size value %s=%s (unknown suffix)", key, val) - } - return num * mult, nil - } else { - return defVal, nil - } -} - -// sizeMult determines the multiplier to be used for the given unit. -func (c *Configuration) sizeMult(suffix string) float64 { - switch suffix { - case "Y": - return 1e24 - case "Z": - return 1e21 - case "E": - return 1e18 - case "P": - return 1e15 - case "T": - return 1e12 - case "G": - return 1e9 - case "M": - return 1e6 - case "k": - return 1e3 - case "h": - return 1e2 - case "da": - return 1e1 - case "": - return 1 - case "d": - return 1e-1 - case "c": - return 1e-2 - case "m": - return 1e-3 - case "u": - return 1e-6 - case "n": - return 1e-9 - case "p": - return 1e-12 - case "f": - return 1e-15 - case "a": - return 1e-18 - case "z": - return 1e-21 - case "y": - return 1e-24 - default: - return 0 - } -} - -// ParseBool converts a property value to a bool. If the property does not -// exist, then the default value will be returned with a nil error. If the -// property value could not be parsed, then an error and the default value will -// be returned. -// -// If the StrictBool setting is true, then only "true" and "false" values are -// able to be converted. -// -// If StrictBool is false (the default), then the following values are -// converted: -// true, t, yes, y, 1, on -> true -// false, f, no, n, 0, off -> false -func (c *Configuration) ParseBool(key string, defVal bool) (bool, error) { - val, ok := c.Props.Get(key) - if ok { - if c.StrictBool { - if val == "true" { - return true, nil - } else if val == "false" { - return false, nil - } else { - return defVal, fmt.Errorf("invalid bool value %s=%s", key, val) - } - } else { - val = strings.ToLower(val) - if val == "true" || val == "t" || val == "yes" || val == "y" || val == "1" || val == "on" { - return true, nil - } else if val == "false" || val == "f" || val == "no" || val == "n" || val == "0" || val == "off" { - return false, nil - } else { - return defVal, fmt.Errorf("invalid bool value %s=%s", key, val) - } - } - } else { - return defVal, nil - } -} - -// ParseDuration converts a property value to a Duration. If the property does -// not exist, then the default value will be returned with a nil error. If the -// property value could not be parsed, then an error and the default value will -// be returned. -// -// The format used is the same as time.ParseDuration. -func (c *Configuration) ParseDuration(key string, defVal time.Duration) (time.Duration, error) { - val, ok := c.Props.Get(key) - if ok { - var err error - result, err := time.ParseDuration(val) - if err != nil { - return defVal, fmt.Errorf("invalid duration value %s=%s [%w]", key, val, err) - } - return result, nil - } else { - return defVal, nil - } -} - -// ParseDate converts a property value to a Time. If the property does not -// exist, then the default value will be returned with a nil error. If the -// property value could not be parsed, then an error and the default value will -// be returned. -// -// The format used is provided by the DateFormat setting and follows the format -// defined in time.Layout. If none is set, the default of 2006-01-02 is used. -func (c *Configuration) ParseDate(key string, defVal time.Time) (time.Time, error) { - val, ok := c.Props.Get(key) - if ok { - layout := c.DateFormat - if layout == "" { - layout = "2006-01-02" - } - result, err := time.Parse(layout, val) - if err != nil { - return defVal, fmt.Errorf("invalid date value %s=%s [%w]", key, val, err) - } - return result, nil - } else { - return defVal, nil - } -} - -// Decrypt returns the plaintext value of a property encrypted with the Encrypt -// function. If the property does not exist, then the default value will be -// returned with a nil error. If the property value could not be decrypted, -// then an error and the default value will be returned. -func (c *Configuration) Decrypt(password string, key string, defVal string) (string, error) { - val, ok := c.Props.Get(key) - if ok { - dec, err := Decrypt(password, val) - if err != nil { - return defVal, fmt.Errorf("invalid encrypted value for %s [%w]", val, err) - } - return dec, nil - } else { - return defVal, nil - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "fmt" + "io/fs" + "math" + "regexp" + "strconv" + "strings" + "time" +) + +const ( + // EncryptNone represents a value that has not yet been encrypted + EncryptNone = "[enc:0]" + // EncryptAESGCM represents a value that has been encryped with AES-GCM + EncryptAESGCM = "[enc:1]" + + // EncryptDefault represents the default encryption algorithm + EncryptDefault = EncryptAESGCM +) + +// sizePattern provides the expression used for matching size values +var sizePattern = regexp.MustCompile("^([0-9.]+)\\s{0,1}([a-zA-Z]*)$") + +// Configuration represents an application's configuration parameters provided +// by properties. +// +// It can be created directly or through NewConfiguration which provides +// configuration by convention. +type Configuration struct { + // Props provides the configuration values to retrieve and/or parse. + Props PropertyGetter + + // DateFormat provides the format string to use when parsing dates as + // defined in the time package. If blank, the default 2006-01-02 is used. + DateFormat string + // StrictBool determines whether bool parsing is strict or not. When true, + // only "true" and "false" values are considered valid. When false, + // additional "boolean-like" values are accepted such as 0 and 1. See + // ParseBool for details. + StrictBool bool +} + +// NewConfiguration creates a Configuration using common conventions. +// +// The returned Configuration uses an Expander to return properties in the +// following priority order: +// 1. Command line arguments +// 2. Environment variables +// 3. -.properties for the provided prefix and profiles +// values (in order) +// 4. .properties for the provided prefix value +// The first matching property value found will be returned. +// +// An error will be returned if one of the property files could not be read or +// parsed. +func NewConfiguration(fileSys fs.StatFS, prefix string, profiles ...string) (*Configuration, error) { + c := &Combined{} + c.Sources = make([]PropertyGetter, 0) + c.Sources = append(c.Sources, &Arguments{}) + c.Sources = append(c.Sources, &Environment{Normalize: true}) + + for _, profile := range profiles { + filename := prefix + "-" + profile + ".properties" + stat, err := fileSys.Stat(filename) + if err == nil && !stat.IsDir() { + p := NewProperties() + f, err := fileSys.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + err = p.Load(f) + if err != nil { + return nil, err + } + c.Sources = append(c.Sources, p) + } + } + + filename := prefix + ".properties" + stat, err := fileSys.Stat(filename) + if err == nil && !stat.IsDir() { + p := NewProperties() + f, err := fileSys.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + err = p.Load(f) + if err != nil { + return nil, err + } + c.Sources = append(c.Sources, p) + } + return &Configuration{Props: NewExpander(c)}, nil +} + +// Get retrieves the value of a property. If the property does not exist, an +// empty string will be returned. The bool return value indicates whether +// the property was found. +func (c *Configuration) Get(key string) (string, bool) { + return c.Props.Get(key) +} + +// GetDefault retrieves the value of a property. If the property does not +// exist, then the default value will be returned. +func (c *Configuration) GetDefault(key, defVal string) string { + return c.Props.GetDefault(key, defVal) +} + +// ParseInt converts a property value to an int. If the property does not exist, +// then the default value will be returned with a nil error. If the property +// value could not be parsed, then an error and the default value will be +// returned. +func (c *Configuration) ParseInt(key string, defVal int) (int, error) { + val, ok := c.Props.Get(key) + if ok { + var err error + result, err := strconv.Atoi(val) + if err != nil { + return defVal, fmt.Errorf("invalid int value %s=%s [%w]", key, val, err) + } + return result, nil + } else { + return defVal, nil + } +} + +// ParseFloat converts a property value to a float64. If the property does not +// exist, then the default value will be returned with a nil error. If the +// property value could not be parsed, then an error and the default value will +// be returned. +func (c *Configuration) ParseFloat(key string, defVal float64) (float64, error) { + val, ok := c.Props.Get(key) + if ok { + var err error + result, err := strconv.ParseFloat(val, 64) + if err != nil { + return defVal, fmt.Errorf("invalid float value %s=%s [%w]", key, val, err) + } + return result, nil + } else { + return defVal, nil + } +} + +// ParseByteSize converts a property value in byte size format to uint64. If +// the property does not exist, then the default value will be returned with a +// nil error. If the property value could not be parsed, then an error and the +// default value will be returned. +// +// The format supported is " " where is a numeric value +// (whole number or decimal) and is a byte size unit as listed below. +// The and space between and are optional. +// +// The supported suffixes are: +// (none) - not modified (x 1) +// k - kilobytes (x 1000) +// Ki - kibibytes (x 1024) +// M - megabyte (x 1000^2) +// Mi - mebibyte (x 1024^2) +// G - gigabyte (x 1000^3) +// Gi - gibibyte (x 1024^3) +// T - terabyte (x 1000^4) +// Ti - tebibyte (x 1024^4) +// P - petabyte (x 1000^5) +// Pi - pebibyte (x 1024^5) +// E - exabyte (x 1000^6) +// Ei - exbibyte (x 1024^6) +func (c *Configuration) ParseByteSize(key string, defVal uint64) (uint64, error) { + val, ok := c.Props.Get(key) + if ok { + match := sizePattern.FindAllStringSubmatch(val, -1) + if match == nil || len(match) != 1 || len(match[0]) != 3 { + return defVal, fmt.Errorf("invalid size value %s=%s", key, val) + } + if strings.Index(match[0][1], ".") < 0 { + num, err := strconv.ParseUint(match[0][1], 10, 64) + if err != nil { + return defVal, fmt.Errorf("invalid size value %s=%s [%w]", key, val, err) + } + mult := c.byteSizeMult(match[0][2]) + if mult == 0 { + return defVal, fmt.Errorf("invalid size value %s=%s (unknown suffix)", key, val) + } + return num * mult, nil + } else { + num, err := strconv.ParseFloat(match[0][1], 64) + if err != nil { + return defVal, fmt.Errorf("invalid size value %s=%s [%w]", key, val, err) + } + mult := c.byteSizeMult(match[0][2]) + if mult == 0 { + return defVal, fmt.Errorf("invalid size value %s=%s (unknown suffix)", key, val) + } + return uint64(math.Round(num * float64(mult))), nil + } + } else { + return defVal, nil + } +} + +// byteSizeMult determines the multiplier to be used for the given unit. +func (c *Configuration) byteSizeMult(suffix string) uint64 { + switch suffix { + case "": + return 1 + case "k": + return 1000 + case "Ki": + return 1 << 10 + case "M": + return 1_000_000 + case "Mi": + return 1 << 20 + case "G": + return 1_000_000_000 + case "Gi": + return 1 << 30 + case "T": + return 1_000_000_000_000 + case "Ti": + return 1 << 40 + case "P": + return 1_000_000_000_000_000 + case "Pi": + return 1 << 50 + case "E": + return 1_000_000_000_000_000_000 + case "Ei": + return 1 << 60 + default: + return 0 + } +} + +// ParseSize converts a property value with a metric size suffix to float64. If +// the property does not exist, then the default value will be returned with a +// nil error. If the property value could not be parsed, then an error and the +// default value will be returned. +// +// The format supported is " " where is a numeric value +// (whole number or decimal) and is a size unit as listed below. +// The and the space between and are optional. +// +// The supported suffixes are: +// Y - yotta (10^24) +// Z - zetta (10^21) +// E - exa (10^18) +// P - peta (10^15) +// T - tera (10^12) +// G - giga (10^9) +// M - mega (10^6) +// k - kilo (10^3) +// h - hecto (10^2) +// da - deca (10^1) +// (none) - not modified (x 1) +// d - deci (10^-1) +// c - centi (10^-2) +// m - milli (10^-3) +// u - micro (10^-6) +// n - nano (10^-9) +// p - pico (10^-12) +// f - femto (10^-15) +// a - atto (10^-18) +// z - zepto (10^-21) +// y - yocto (10^-23) +func (c *Configuration) ParseSize(key string, defVal float64) (float64, error) { + val, ok := c.Props.Get(key) + if ok { + match := sizePattern.FindAllStringSubmatch(val, -1) + if match == nil || len(match) != 1 || len(match[0]) != 3 { + return defVal, fmt.Errorf("invalid size value %s=%s", key, val) + } + num, err := strconv.ParseFloat(match[0][1], 64) + if err != nil { + return defVal, fmt.Errorf("invalid size value %s=%s [%w]", key, val, err) + } + mult := c.sizeMult(match[0][2]) + if mult == 0 { + return defVal, fmt.Errorf("invalid size value %s=%s (unknown suffix)", key, val) + } + return num * mult, nil + } else { + return defVal, nil + } +} + +// sizeMult determines the multiplier to be used for the given unit. +func (c *Configuration) sizeMult(suffix string) float64 { + switch suffix { + case "Y": + return 1e24 + case "Z": + return 1e21 + case "E": + return 1e18 + case "P": + return 1e15 + case "T": + return 1e12 + case "G": + return 1e9 + case "M": + return 1e6 + case "k": + return 1e3 + case "h": + return 1e2 + case "da": + return 1e1 + case "": + return 1 + case "d": + return 1e-1 + case "c": + return 1e-2 + case "m": + return 1e-3 + case "u": + return 1e-6 + case "n": + return 1e-9 + case "p": + return 1e-12 + case "f": + return 1e-15 + case "a": + return 1e-18 + case "z": + return 1e-21 + case "y": + return 1e-24 + default: + return 0 + } +} + +// ParseBool converts a property value to a bool. If the property does not +// exist, then the default value will be returned with a nil error. If the +// property value could not be parsed, then an error and the default value will +// be returned. +// +// If the StrictBool setting is true, then only "true" and "false" values are +// able to be converted. +// +// If StrictBool is false (the default), then the following values are +// converted: +// true, t, yes, y, 1, on -> true +// false, f, no, n, 0, off -> false +func (c *Configuration) ParseBool(key string, defVal bool) (bool, error) { + val, ok := c.Props.Get(key) + if ok { + if c.StrictBool { + if val == "true" { + return true, nil + } else if val == "false" { + return false, nil + } else { + return defVal, fmt.Errorf("invalid bool value %s=%s", key, val) + } + } else { + val = strings.ToLower(val) + if val == "true" || val == "t" || val == "yes" || val == "y" || val == "1" || val == "on" { + return true, nil + } else if val == "false" || val == "f" || val == "no" || val == "n" || val == "0" || val == "off" { + return false, nil + } else { + return defVal, fmt.Errorf("invalid bool value %s=%s", key, val) + } + } + } else { + return defVal, nil + } +} + +// ParseDuration converts a property value to a Duration. If the property does +// not exist, then the default value will be returned with a nil error. If the +// property value could not be parsed, then an error and the default value will +// be returned. +// +// The format used is the same as time.ParseDuration. +func (c *Configuration) ParseDuration(key string, defVal time.Duration) (time.Duration, error) { + val, ok := c.Props.Get(key) + if ok { + var err error + result, err := time.ParseDuration(val) + if err != nil { + return defVal, fmt.Errorf("invalid duration value %s=%s [%w]", key, val, err) + } + return result, nil + } else { + return defVal, nil + } +} + +// ParseDate converts a property value to a Time. If the property does not +// exist, then the default value will be returned with a nil error. If the +// property value could not be parsed, then an error and the default value will +// be returned. +// +// The format used is provided by the DateFormat setting and follows the format +// defined in time.Layout. If none is set, the default of 2006-01-02 is used. +func (c *Configuration) ParseDate(key string, defVal time.Time) (time.Time, error) { + val, ok := c.Props.Get(key) + if ok { + layout := c.DateFormat + if layout == "" { + layout = "2006-01-02" + } + result, err := time.Parse(layout, val) + if err != nil { + return defVal, fmt.Errorf("invalid date value %s=%s [%w]", key, val, err) + } + return result, nil + } else { + return defVal, nil + } +} + +// Decrypt returns the plaintext value of a property encrypted with the Encrypt +// function. If the property does not exist, then the default value will be +// returned with a nil error. If the property value could not be decrypted, +// then an error and the default value will be returned. +func (c *Configuration) Decrypt(password string, key string, defVal string) (string, error) { + val, ok := c.Props.Get(key) + if ok { + dec, err := Decrypt(password, val) + if err != nil { + return defVal, fmt.Errorf("invalid encrypted value for %s [%w]", val, err) + } + return dec, nil + } else { + return defVal, nil + } +} diff --git a/configuration_test.go b/configuration_test.go index 334a1c6..5ade75c 100644 --- a/configuration_test.go +++ b/configuration_test.go @@ -1,559 +1,559 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "errors" - "io/fs" - "math" - "testing" - "testing/fstest" - "time" -) - -var memFs = make(fstest.MapFS) - -func init() { - memFs["testapp.properties"] = &fstest.MapFile{ - Data: []byte(` -key1=abc -key2=def -key3=ghi -keyexp=${key2} -`), - } - memFs["testapp-test.properties"] = &fstest.MapFile{ - Data: []byte(` -key2=123 -key3= -`), - } - memFs["testapp-prod.properties"] = &fstest.MapFile{ - Data: []byte(` -key2=456 -`), - } -} - -func TestNewConfigurationNoProfile(t *testing.T) { - c, err := NewConfiguration(memFs, "testapp") - if err != nil { - t.Errorf("got error: %v", err) - } - - val, _ := c.Get("key1") - if val != "abc" { - t.Errorf("want: 'abc'; got: '%s'", val) - } - - val, _ = c.Get("key2") - if val != "def" { - t.Errorf("want: 'def'; got: '%s'", val) - } - - val, _ = c.Get("key3") - if val != "ghi" { - t.Errorf("want: 'ghi'; got '%s'", val) - } - - val, _ = c.Get("key4") - if val != "" { - t.Errorf("want: ''; got: '%s'", val) - } - - val, _ = c.Get("keyexp") - if val != "def" { - t.Errorf("want: 'def'; got: '%s'", val) - } - - val = c.GetDefault("keyexp", "none") - if val != "def" { - t.Errorf("want: 'def'; got: '%s'", val) - } - - val = c.GetDefault("keynone", "none") - if val != "none" { - t.Errorf("want: 'none'; got: '%s'", val) - } -} - -func TestNewConfigurationProfile(t *testing.T) { - c, err := NewConfiguration(memFs, "testapp", "prod", "test", "bad") - if err != nil { - t.Errorf("got error: %v", err) - } - - val, _ := c.Get("key1") - if val != "abc" { - t.Errorf("want: 'abc'; got: '%s'", val) - } - - val, _ = c.Get("key2") - if val != "456" { - t.Errorf("want: '456'; got: '%s'", val) - } - - val, _ = c.Get("key3") - if val != "" { - t.Errorf("want: ''; got '%s'", val) - } - - val, _ = c.Get("key4") - if val != "" { - t.Errorf("want: ''; got: '%s'", val) - } - - val, _ = c.Get("keyexp") - if val != "456" { - t.Errorf("want: '456'; got '%s'", val) - } -} - -type badFileInfo struct{} - -func (b *badFileInfo) Name() string { return "file" } -func (b *badFileInfo) Size() int64 { return 1024 } -func (b *badFileInfo) Mode() fs.FileMode { return 0 } -func (b *badFileInfo) ModTime() time.Time { return time.Now() } -func (b *badFileInfo) IsDir() bool { return false } -func (b *badFileInfo) Sys() interface{} { return nil } - -type badFile struct{} - -func (b *badFile) Stat() (fs.FileInfo, error) { return &badFileInfo{}, nil } -func (b *badFile) Read([]byte) (int, error) { return 0, errors.New("bad file read") } -func (b *badFile) Close() error { return nil } - -type badFs struct{} - -func (b *badFs) Open(name string) (fs.File, error) { - if name == "bad-read.properties" { - return &badFile{}, nil - } - return nil, errors.New("bad file") -} - -func (b *badFs) Stat(name string) (fs.FileInfo, error) { - return &badFileInfo{}, nil -} - -func TestNewConfigurationBadFile(t *testing.T) { - _, err := NewConfiguration(&badFs{}, "bad", "open", "read") - if err == nil { - t.Errorf("want err; got none") - } - - _, err = NewConfiguration(&badFs{}, "bad", "read") - if err == nil { - t.Errorf("want err; got none") - } - - _, err = NewConfiguration(&badFs{}, "bad-open") - if err == nil { - t.Errorf("want err; got none") - } - - _, err = NewConfiguration(&badFs{}, "bad-read") - if err == nil { - t.Errorf("want err; got none") - } -} - -func TestParseInt(t *testing.T) { - p := NewProperties() - c := &Configuration{Props: NewExpander(p)} - - p.Set("bad", "abc") - p.Set("badextra", "42x") - p.Set("badfloat", "42.123") - p.Set("good", "42") - tests := []struct { - key string - defVal int - want int - wantErr bool - }{ - {"none", 123, 123, false}, - {"bad", 123, 123, true}, - {"badextra", 123, 123, true}, - {"badfloat", 123, 123, true}, - {"good", 123, 42, false}, - } - - for i, test := range tests { - got, gotErr := c.ParseInt(test.key, test.defVal) - if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { - t.Errorf("[%d-%s] got: %d, gotErr: %v; want: %d, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) - } - } -} - -func TestParseFloat(t *testing.T) { - p := NewProperties() - c := &Configuration{Props: NewExpander(p)} - - p.Set("bad", "abc") - p.Set("badextra", "42x") - p.Set("good", "42.123") - tests := []struct { - key string - defVal float64 - want float64 - wantErr bool - }{ - {"none", 123, 123, false}, - {"bad", 123, 123, true}, - {"badextra", 123, 123, true}, - {"good", 123, 42.123, false}, - } - - for i, test := range tests { - got, gotErr := c.ParseFloat(test.key, test.defVal) - if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { - t.Errorf("[%d-%s] got: %f, gotErr: %v; want: %f, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) - } - } -} - -func TestParseByteSize(t *testing.T) { - p := NewProperties() - c := &Configuration{Props: NewExpander(p)} - - p.Set("bad1", "abc") - p.Set("bad2", "42x") - p.Set("bad3", "42_M") - p.Set("bad4", "42M 50k") - p.Set("bad5", "999999999999999999999999999999Ei") - p.Set("bad6", "42...5Ti") - p.Set("bad7", "42.5x") - p.Set("goodint", "42") - p.Set("goodfloat", "42.5") - p.Set("goodk", "720k") - p.Set("goodKi", "720 Ki") - p.Set("goodm", "1.44M") - p.Set("goodmi", "1.44 Mi") - p.Set("goodg", "1.21G") - p.Set("goodgi", "1.21 Gi") - p.Set("goodt", "15T") - p.Set("goodti", "15 Ti") - p.Set("goodp", "20P") - p.Set("goodpi", "20 Pi") - p.Set("goode", "15E") - p.Set("goodei", "15 Ei") - - tests := []struct { - key string - defVal uint64 - want uint64 - wantErr bool - }{ - {"none", 123, 123, false}, - {"bad1", 123, 123, true}, - {"bad2", 123, 123, true}, - {"bad3", 123, 123, true}, - {"bad4", 123, 123, true}, - {"bad5", 123, 123, true}, - {"bad6", 123, 123, true}, - {"bad7", 123, 123, true}, - {"goodint", 123, 42, false}, - {"goodfloat", 123, 43, false}, - {"goodk", 123, 720_000, false}, - {"goodKi", 123, 737_280, false}, - {"goodm", 123, 1_440_000, false}, - {"goodmi", 123, 1_509_949, false}, - {"goodg", 123, 1_210_000_000, false}, - {"goodgi", 123, 1_299_227_607, false}, - {"goodt", 123, 15_000_000_000_000, false}, - {"goodti", 123, 16_492_674_416_640, false}, - {"goodp", 123, 20_000_000_000_000_000, false}, - {"goodpi", 123, 22_517_998_136_852_480, false}, - {"goode", 123, 15_000_000_000_000_000_000, false}, - {"goodei", 123, 17_293_822_569_102_704_640, false}, - } - - for i, test := range tests { - got, gotErr := c.ParseByteSize(test.key, test.defVal) - if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { - t.Errorf("[%d-%s] got: %d, gotErr: %v; want: %d, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) - } - } -} - -func TestParseSize(t *testing.T) { - p := NewProperties() - c := &Configuration{Props: NewExpander(p)} - - p.Set("bad1", "abc") - p.Set("bad2", "42x") - p.Set("bad3", "42_M") - p.Set("bad4", "42M 50k") - p.Set("bad5", "1e9k") - p.Set("bad6", "42...5T") - p.Set("bad7", "42.5x") - p.Set("goodint", "42") - p.Set("goodfloat", "42.5") - p.Set("goodY", "1.23Y") - p.Set("goodZ", "1.23 Z") - p.Set("goodE", "1.23E") - p.Set("goodP", "1.23 P") - p.Set("goodT", "1.23T") - p.Set("goodG", "1.23 G") - p.Set("goodM", "1.23M") - p.Set("goodk", "1.23 k") - p.Set("goodh", "1.23h") - p.Set("goodda", "1.23 da") - p.Set("goodd", "1.23d") - p.Set("goodc", "1.23 c") - p.Set("goodm", "1.23m") - p.Set("goodu", "1.23 u") - p.Set("goodn", "1.23n") - p.Set("goodp", "1.23 p") - p.Set("goodf", "1.23f") - p.Set("gooda", "1.23 a") - p.Set("goodz", "1.23z") - p.Set("goody", "1.23 y") - - tests := []struct { - key string - defVal float64 - want float64 - wantErr bool - }{ - {"none", 123, 123, false}, - {"bad1", 123, 123, true}, - {"bad2", 123, 123, true}, - {"bad3", 123, 123, true}, - {"bad4", 123, 123, true}, - {"bad5", 123, 123, true}, - {"bad6", 123, 123, true}, - {"bad7", 123, 123, true}, - {"goodint", 123, 42, false}, - {"goodfloat", 123, 42.5, false}, - {"goodY", 123, 1.23e24, false}, - {"goodZ", 123, 1.23e21, false}, - {"goodE", 123, 1.23e18, false}, - {"goodP", 123, 1.23e15, false}, - {"goodT", 123, 1.23e12, false}, - {"goodG", 123, 1.23e9, false}, - {"goodM", 123, 1.23e6, false}, - {"goodk", 123, 1.23e3, false}, - {"goodh", 123, 1.23e2, false}, - {"goodda", 123, 1.23e1, false}, - {"goodd", 123, 1.23e-1, false}, - {"goodc", 123, 1.23e-2, false}, - {"goodm", 123, 1.23e-3, false}, - {"goodu", 123, 1.23e-6, false}, - {"goodn", 123, 1.23e-9, false}, - {"goodp", 123, 1.23e-12, false}, - {"goodf", 123, 1.23e-15, false}, - {"gooda", 123, 1.23e-18, false}, - {"goodz", 123, 1.23e-21, false}, - {"goody", 123, 1.23e-24, false}, - } - - for i, test := range tests { - got, gotErr := c.ParseSize(test.key, test.defVal) - if math.Abs(got-test.want) > 0.0001 || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { - t.Errorf("[%d-%s] got: %e, gotErr: %v; want: %e, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) - } - } -} - -func TestParseBool(t *testing.T) { - p := NewProperties() - c := &Configuration{Props: NewExpander(p)} - - p.Set("bad", "abc") - p.Set("badextra", "truex") - p.Set("goodtrue", "true") - p.Set("goodfalse", "false") - - p.Set("bad", "def") - p.Set("badextra", "12") - p.Set("good1", "true") - p.Set("good2", "t") - p.Set("good3", "yes") - p.Set("good4", "y") - p.Set("good5", "1") - p.Set("good6", "on") - p.Set("good7", "false") - p.Set("good8", "f") - p.Set("good9", "no") - p.Set("good10", "n") - p.Set("good11", "0") - p.Set("good12", "off") - - tests := []struct { - key string - defVal bool - want bool - wantErr bool - }{ - {"none", true, true, false}, - {"bad", true, true, true}, - {"badextra", true, true, true}, - {"goodtrue", false, true, false}, - {"goodfalse", true, false, false}, - - {"bad", true, true, true}, - {"badextra", true, true, true}, - {"good1", false, true, false}, - {"good2", false, true, false}, - {"good3", false, true, false}, - {"good4", false, true, false}, - {"good5", false, true, false}, - {"good6", false, true, false}, - {"good7", true, false, false}, - {"good8", true, false, false}, - {"good9", true, false, false}, - {"good10", true, false, false}, - {"good11", true, false, false}, - {"good12", true, false, false}, - } - - for i, test := range tests { - c.StrictBool = i < 5 - got, gotErr := c.ParseBool(test.key, test.defVal) - if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { - t.Errorf("[%d-%s] got: %t, gotErr: %v; want: %t, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) - } - } -} - -func TestParseDuration(t *testing.T) { - p := NewProperties() - c := &Configuration{Props: NewExpander(p)} - - p.Set("bad", "abc") - p.Set("badextra", "42x") - p.Set("badfloat", "42.123") - p.Set("good", "42h") - tests := []struct { - key string - defVal time.Duration - want time.Duration - wantErr bool - }{ - {"none", time.Duration(123), time.Duration(123), false}, - {"bad", time.Duration(123), time.Duration(123), true}, - {"badextra", time.Duration(123), time.Duration(123), true}, - {"badfloat", time.Duration(123), time.Duration(123), true}, - {"good", time.Duration(123), 42 * time.Hour, false}, - } - - for i, test := range tests { - got, gotErr := c.ParseDuration(test.key, test.defVal) - if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { - t.Errorf("[%d-%s] got: %d, gotErr: %v; want: %d, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) - } - } -} - -func TestParseDate(t *testing.T) { - p := NewProperties() - c := &Configuration{Props: NewExpander(p)} - - p.Set("bad", "abc") - p.Set("badextra", "2000-01-01x") - p.Set("badformat", "99-12-31") - p.Set("good", "2000-01-01") - p.Set("goodformat", "2001.05.04 12:30") - tests := []struct { - key string - defVal time.Time - want time.Time - wantErr bool - }{ - {"none", time.Time{}, time.Time{}, false}, - {"bad", time.Time{}, time.Time{}, true}, - {"badextra", time.Time{}, time.Time{}, true}, - {"badformat", time.Time{}, time.Time{}, true}, - {"good", time.Time{}, time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), false}, - {"goodformat", time.Time{}, time.Date(2001, 5, 4, 12, 30, 0, 0, time.UTC), false}, - } - - for i, test := range tests { - if test.key == "goodformat" { - c.DateFormat = "2006.01.02 15:04" - } else { - c.DateFormat = "" - } - got, gotErr := c.ParseDate(test.key, test.defVal) - if !got.Equal(test.want) || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { - t.Errorf("[%d-%s] got: %v, gotErr: %v; want: %v, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) - } - } -} - -func TestConfigDecrypt(t *testing.T) { - p := NewProperties() - c := &Configuration{Props: p} - - p.Set("novalue", "[enc:0]") - p.Set("notencrypted", "[enc:0]plaintext") - p.Set("aesgcm.none", "[enc:1]") - p.Set("aesgcm.badbase64", "[enc:1]$$$$") - p.Set("aesgcm.good", "[enc:1]qzp-F5Cw1G5n9c7D5LFw4NzanvZzdQzONRPlM3JpLLCO6swpBQ==") - p.Set("aesgcm.good256", "[enc:1]x-f72AdaTiZfRvX_6kekHpd4xUj1JmMFwbwiADbMZXgJjGpJNg==") - p.Set("badalg", "[enc:x]abcd") - p.Set("noalg", "abcd") - - badpass := "123" - pass := "1234567890123456" - pass256 := "12345678901234567890123456789012" - - val, err := c.Decrypt(badpass, "none", "default") - if val != "default" || err != nil { - t.Errorf("want: 'default', err == nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(badpass, "novalue", "default") - if val != "" || err != nil { - t.Errorf("want: '', err == nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(badpass, "notencrypted", "default") - if val != "plaintext" || err != nil { - t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(badpass, "aesgcm.good", "default") - if val != "default" || err == nil { - t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(pass, "aesgcm.none", "default") - if val != "default" || err == nil { - t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(pass, "aesgcm.badbase64", "default") - if val != "default" || err == nil { - t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(pass, "aesgcm.good", "default") - if val != "plaintext" || err != nil { - t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(pass, "aesgcm.good256", "default") - if val != "default" || err == nil { - t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(pass256, "aesgcm.good256", "default") - if val != "plaintext" || err != nil { - t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(pass, "badalg", "default") - if val != "default" || err == nil { - t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) - } - - val, err = c.Decrypt(pass, "noalg", "default") - if val != "default" || err == nil { - t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "errors" + "io/fs" + "math" + "testing" + "testing/fstest" + "time" +) + +var memFs = make(fstest.MapFS) + +func init() { + memFs["testapp.properties"] = &fstest.MapFile{ + Data: []byte(` +key1=abc +key2=def +key3=ghi +keyexp=${key2} +`), + } + memFs["testapp-test.properties"] = &fstest.MapFile{ + Data: []byte(` +key2=123 +key3= +`), + } + memFs["testapp-prod.properties"] = &fstest.MapFile{ + Data: []byte(` +key2=456 +`), + } +} + +func TestNewConfigurationNoProfile(t *testing.T) { + c, err := NewConfiguration(memFs, "testapp") + if err != nil { + t.Errorf("got error: %v", err) + } + + val, _ := c.Get("key1") + if val != "abc" { + t.Errorf("want: 'abc'; got: '%s'", val) + } + + val, _ = c.Get("key2") + if val != "def" { + t.Errorf("want: 'def'; got: '%s'", val) + } + + val, _ = c.Get("key3") + if val != "ghi" { + t.Errorf("want: 'ghi'; got '%s'", val) + } + + val, _ = c.Get("key4") + if val != "" { + t.Errorf("want: ''; got: '%s'", val) + } + + val, _ = c.Get("keyexp") + if val != "def" { + t.Errorf("want: 'def'; got: '%s'", val) + } + + val = c.GetDefault("keyexp", "none") + if val != "def" { + t.Errorf("want: 'def'; got: '%s'", val) + } + + val = c.GetDefault("keynone", "none") + if val != "none" { + t.Errorf("want: 'none'; got: '%s'", val) + } +} + +func TestNewConfigurationProfile(t *testing.T) { + c, err := NewConfiguration(memFs, "testapp", "prod", "test", "bad") + if err != nil { + t.Errorf("got error: %v", err) + } + + val, _ := c.Get("key1") + if val != "abc" { + t.Errorf("want: 'abc'; got: '%s'", val) + } + + val, _ = c.Get("key2") + if val != "456" { + t.Errorf("want: '456'; got: '%s'", val) + } + + val, _ = c.Get("key3") + if val != "" { + t.Errorf("want: ''; got '%s'", val) + } + + val, _ = c.Get("key4") + if val != "" { + t.Errorf("want: ''; got: '%s'", val) + } + + val, _ = c.Get("keyexp") + if val != "456" { + t.Errorf("want: '456'; got '%s'", val) + } +} + +type badFileInfo struct{} + +func (b *badFileInfo) Name() string { return "file" } +func (b *badFileInfo) Size() int64 { return 1024 } +func (b *badFileInfo) Mode() fs.FileMode { return 0 } +func (b *badFileInfo) ModTime() time.Time { return time.Now() } +func (b *badFileInfo) IsDir() bool { return false } +func (b *badFileInfo) Sys() interface{} { return nil } + +type badFile struct{} + +func (b *badFile) Stat() (fs.FileInfo, error) { return &badFileInfo{}, nil } +func (b *badFile) Read([]byte) (int, error) { return 0, errors.New("bad file read") } +func (b *badFile) Close() error { return nil } + +type badFs struct{} + +func (b *badFs) Open(name string) (fs.File, error) { + if name == "bad-read.properties" { + return &badFile{}, nil + } + return nil, errors.New("bad file") +} + +func (b *badFs) Stat(name string) (fs.FileInfo, error) { + return &badFileInfo{}, nil +} + +func TestNewConfigurationBadFile(t *testing.T) { + _, err := NewConfiguration(&badFs{}, "bad", "open", "read") + if err == nil { + t.Errorf("want err; got none") + } + + _, err = NewConfiguration(&badFs{}, "bad", "read") + if err == nil { + t.Errorf("want err; got none") + } + + _, err = NewConfiguration(&badFs{}, "bad-open") + if err == nil { + t.Errorf("want err; got none") + } + + _, err = NewConfiguration(&badFs{}, "bad-read") + if err == nil { + t.Errorf("want err; got none") + } +} + +func TestParseInt(t *testing.T) { + p := NewProperties() + c := &Configuration{Props: NewExpander(p)} + + p.Set("bad", "abc") + p.Set("badextra", "42x") + p.Set("badfloat", "42.123") + p.Set("good", "42") + tests := []struct { + key string + defVal int + want int + wantErr bool + }{ + {"none", 123, 123, false}, + {"bad", 123, 123, true}, + {"badextra", 123, 123, true}, + {"badfloat", 123, 123, true}, + {"good", 123, 42, false}, + } + + for i, test := range tests { + got, gotErr := c.ParseInt(test.key, test.defVal) + if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { + t.Errorf("[%d-%s] got: %d, gotErr: %v; want: %d, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) + } + } +} + +func TestParseFloat(t *testing.T) { + p := NewProperties() + c := &Configuration{Props: NewExpander(p)} + + p.Set("bad", "abc") + p.Set("badextra", "42x") + p.Set("good", "42.123") + tests := []struct { + key string + defVal float64 + want float64 + wantErr bool + }{ + {"none", 123, 123, false}, + {"bad", 123, 123, true}, + {"badextra", 123, 123, true}, + {"good", 123, 42.123, false}, + } + + for i, test := range tests { + got, gotErr := c.ParseFloat(test.key, test.defVal) + if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { + t.Errorf("[%d-%s] got: %f, gotErr: %v; want: %f, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) + } + } +} + +func TestParseByteSize(t *testing.T) { + p := NewProperties() + c := &Configuration{Props: NewExpander(p)} + + p.Set("bad1", "abc") + p.Set("bad2", "42x") + p.Set("bad3", "42_M") + p.Set("bad4", "42M 50k") + p.Set("bad5", "999999999999999999999999999999Ei") + p.Set("bad6", "42...5Ti") + p.Set("bad7", "42.5x") + p.Set("goodint", "42") + p.Set("goodfloat", "42.5") + p.Set("goodk", "720k") + p.Set("goodKi", "720 Ki") + p.Set("goodm", "1.44M") + p.Set("goodmi", "1.44 Mi") + p.Set("goodg", "1.21G") + p.Set("goodgi", "1.21 Gi") + p.Set("goodt", "15T") + p.Set("goodti", "15 Ti") + p.Set("goodp", "20P") + p.Set("goodpi", "20 Pi") + p.Set("goode", "15E") + p.Set("goodei", "15 Ei") + + tests := []struct { + key string + defVal uint64 + want uint64 + wantErr bool + }{ + {"none", 123, 123, false}, + {"bad1", 123, 123, true}, + {"bad2", 123, 123, true}, + {"bad3", 123, 123, true}, + {"bad4", 123, 123, true}, + {"bad5", 123, 123, true}, + {"bad6", 123, 123, true}, + {"bad7", 123, 123, true}, + {"goodint", 123, 42, false}, + {"goodfloat", 123, 43, false}, + {"goodk", 123, 720_000, false}, + {"goodKi", 123, 737_280, false}, + {"goodm", 123, 1_440_000, false}, + {"goodmi", 123, 1_509_949, false}, + {"goodg", 123, 1_210_000_000, false}, + {"goodgi", 123, 1_299_227_607, false}, + {"goodt", 123, 15_000_000_000_000, false}, + {"goodti", 123, 16_492_674_416_640, false}, + {"goodp", 123, 20_000_000_000_000_000, false}, + {"goodpi", 123, 22_517_998_136_852_480, false}, + {"goode", 123, 15_000_000_000_000_000_000, false}, + {"goodei", 123, 17_293_822_569_102_704_640, false}, + } + + for i, test := range tests { + got, gotErr := c.ParseByteSize(test.key, test.defVal) + if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { + t.Errorf("[%d-%s] got: %d, gotErr: %v; want: %d, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) + } + } +} + +func TestParseSize(t *testing.T) { + p := NewProperties() + c := &Configuration{Props: NewExpander(p)} + + p.Set("bad1", "abc") + p.Set("bad2", "42x") + p.Set("bad3", "42_M") + p.Set("bad4", "42M 50k") + p.Set("bad5", "1e9k") + p.Set("bad6", "42...5T") + p.Set("bad7", "42.5x") + p.Set("goodint", "42") + p.Set("goodfloat", "42.5") + p.Set("goodY", "1.23Y") + p.Set("goodZ", "1.23 Z") + p.Set("goodE", "1.23E") + p.Set("goodP", "1.23 P") + p.Set("goodT", "1.23T") + p.Set("goodG", "1.23 G") + p.Set("goodM", "1.23M") + p.Set("goodk", "1.23 k") + p.Set("goodh", "1.23h") + p.Set("goodda", "1.23 da") + p.Set("goodd", "1.23d") + p.Set("goodc", "1.23 c") + p.Set("goodm", "1.23m") + p.Set("goodu", "1.23 u") + p.Set("goodn", "1.23n") + p.Set("goodp", "1.23 p") + p.Set("goodf", "1.23f") + p.Set("gooda", "1.23 a") + p.Set("goodz", "1.23z") + p.Set("goody", "1.23 y") + + tests := []struct { + key string + defVal float64 + want float64 + wantErr bool + }{ + {"none", 123, 123, false}, + {"bad1", 123, 123, true}, + {"bad2", 123, 123, true}, + {"bad3", 123, 123, true}, + {"bad4", 123, 123, true}, + {"bad5", 123, 123, true}, + {"bad6", 123, 123, true}, + {"bad7", 123, 123, true}, + {"goodint", 123, 42, false}, + {"goodfloat", 123, 42.5, false}, + {"goodY", 123, 1.23e24, false}, + {"goodZ", 123, 1.23e21, false}, + {"goodE", 123, 1.23e18, false}, + {"goodP", 123, 1.23e15, false}, + {"goodT", 123, 1.23e12, false}, + {"goodG", 123, 1.23e9, false}, + {"goodM", 123, 1.23e6, false}, + {"goodk", 123, 1.23e3, false}, + {"goodh", 123, 1.23e2, false}, + {"goodda", 123, 1.23e1, false}, + {"goodd", 123, 1.23e-1, false}, + {"goodc", 123, 1.23e-2, false}, + {"goodm", 123, 1.23e-3, false}, + {"goodu", 123, 1.23e-6, false}, + {"goodn", 123, 1.23e-9, false}, + {"goodp", 123, 1.23e-12, false}, + {"goodf", 123, 1.23e-15, false}, + {"gooda", 123, 1.23e-18, false}, + {"goodz", 123, 1.23e-21, false}, + {"goody", 123, 1.23e-24, false}, + } + + for i, test := range tests { + got, gotErr := c.ParseSize(test.key, test.defVal) + if math.Abs(got-test.want) > 0.0001 || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { + t.Errorf("[%d-%s] got: %e, gotErr: %v; want: %e, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) + } + } +} + +func TestParseBool(t *testing.T) { + p := NewProperties() + c := &Configuration{Props: NewExpander(p)} + + p.Set("bad", "abc") + p.Set("badextra", "truex") + p.Set("goodtrue", "true") + p.Set("goodfalse", "false") + + p.Set("bad", "def") + p.Set("badextra", "12") + p.Set("good1", "true") + p.Set("good2", "t") + p.Set("good3", "yes") + p.Set("good4", "y") + p.Set("good5", "1") + p.Set("good6", "on") + p.Set("good7", "false") + p.Set("good8", "f") + p.Set("good9", "no") + p.Set("good10", "n") + p.Set("good11", "0") + p.Set("good12", "off") + + tests := []struct { + key string + defVal bool + want bool + wantErr bool + }{ + {"none", true, true, false}, + {"bad", true, true, true}, + {"badextra", true, true, true}, + {"goodtrue", false, true, false}, + {"goodfalse", true, false, false}, + + {"bad", true, true, true}, + {"badextra", true, true, true}, + {"good1", false, true, false}, + {"good2", false, true, false}, + {"good3", false, true, false}, + {"good4", false, true, false}, + {"good5", false, true, false}, + {"good6", false, true, false}, + {"good7", true, false, false}, + {"good8", true, false, false}, + {"good9", true, false, false}, + {"good10", true, false, false}, + {"good11", true, false, false}, + {"good12", true, false, false}, + } + + for i, test := range tests { + c.StrictBool = i < 5 + got, gotErr := c.ParseBool(test.key, test.defVal) + if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { + t.Errorf("[%d-%s] got: %t, gotErr: %v; want: %t, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) + } + } +} + +func TestParseDuration(t *testing.T) { + p := NewProperties() + c := &Configuration{Props: NewExpander(p)} + + p.Set("bad", "abc") + p.Set("badextra", "42x") + p.Set("badfloat", "42.123") + p.Set("good", "42h") + tests := []struct { + key string + defVal time.Duration + want time.Duration + wantErr bool + }{ + {"none", time.Duration(123), time.Duration(123), false}, + {"bad", time.Duration(123), time.Duration(123), true}, + {"badextra", time.Duration(123), time.Duration(123), true}, + {"badfloat", time.Duration(123), time.Duration(123), true}, + {"good", time.Duration(123), 42 * time.Hour, false}, + } + + for i, test := range tests { + got, gotErr := c.ParseDuration(test.key, test.defVal) + if got != test.want || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { + t.Errorf("[%d-%s] got: %d, gotErr: %v; want: %d, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) + } + } +} + +func TestParseDate(t *testing.T) { + p := NewProperties() + c := &Configuration{Props: NewExpander(p)} + + p.Set("bad", "abc") + p.Set("badextra", "2000-01-01x") + p.Set("badformat", "99-12-31") + p.Set("good", "2000-01-01") + p.Set("goodformat", "2001.05.04 12:30") + tests := []struct { + key string + defVal time.Time + want time.Time + wantErr bool + }{ + {"none", time.Time{}, time.Time{}, false}, + {"bad", time.Time{}, time.Time{}, true}, + {"badextra", time.Time{}, time.Time{}, true}, + {"badformat", time.Time{}, time.Time{}, true}, + {"good", time.Time{}, time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), false}, + {"goodformat", time.Time{}, time.Date(2001, 5, 4, 12, 30, 0, 0, time.UTC), false}, + } + + for i, test := range tests { + if test.key == "goodformat" { + c.DateFormat = "2006.01.02 15:04" + } else { + c.DateFormat = "" + } + got, gotErr := c.ParseDate(test.key, test.defVal) + if !got.Equal(test.want) || (test.wantErr && gotErr == nil) || (!test.wantErr && gotErr != nil) { + t.Errorf("[%d-%s] got: %v, gotErr: %v; want: %v, wantErr: %t", i, test.key, got, gotErr, test.want, test.wantErr) + } + } +} + +func TestConfigDecrypt(t *testing.T) { + p := NewProperties() + c := &Configuration{Props: p} + + p.Set("novalue", "[enc:0]") + p.Set("notencrypted", "[enc:0]plaintext") + p.Set("aesgcm.none", "[enc:1]") + p.Set("aesgcm.badbase64", "[enc:1]$$$$") + p.Set("aesgcm.good", "[enc:1]qzp-F5Cw1G5n9c7D5LFw4NzanvZzdQzONRPlM3JpLLCO6swpBQ==") + p.Set("aesgcm.good256", "[enc:1]x-f72AdaTiZfRvX_6kekHpd4xUj1JmMFwbwiADbMZXgJjGpJNg==") + p.Set("badalg", "[enc:x]abcd") + p.Set("noalg", "abcd") + + badpass := "123" + pass := "1234567890123456" + pass256 := "12345678901234567890123456789012" + + val, err := c.Decrypt(badpass, "none", "default") + if val != "default" || err != nil { + t.Errorf("want: 'default', err == nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(badpass, "novalue", "default") + if val != "" || err != nil { + t.Errorf("want: '', err == nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(badpass, "notencrypted", "default") + if val != "plaintext" || err != nil { + t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(badpass, "aesgcm.good", "default") + if val != "default" || err == nil { + t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(pass, "aesgcm.none", "default") + if val != "default" || err == nil { + t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(pass, "aesgcm.badbase64", "default") + if val != "default" || err == nil { + t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(pass, "aesgcm.good", "default") + if val != "plaintext" || err != nil { + t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(pass, "aesgcm.good256", "default") + if val != "default" || err == nil { + t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(pass256, "aesgcm.good256", "default") + if val != "plaintext" || err != nil { + t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(pass, "badalg", "default") + if val != "default" || err == nil { + t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) + } + + val, err = c.Decrypt(pass, "noalg", "default") + if val != "default" || err == nil { + t.Errorf("want: 'default', err != nil; got: %s, %v", val, err) + } +} diff --git a/encryption.go b/encryption.go index bfdfca9..0b10407 100644 --- a/encryption.go +++ b/encryption.go @@ -1,79 +1,79 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "fmt" - "strings" -) - -// Decrypt returns the plaintext value of a property encrypted with the Encrypt -// function. If the property does not exist, then the default value will be -// returned with a nil error. If the property value could not be decrypted, -// then an error and the default value will be returned. -func Decrypt(password string, val string) (string, error) { - if strings.Index(val, "]") < 0 { - return "", fmt.Errorf("missing algorithm") - } - alg := val[0 : strings.Index(val, "]")+1] - if alg == EncryptNone { - return val[len(alg):], nil - } - - enc, err := base64.URLEncoding.DecodeString(val[len(alg):]) - if err != nil { - return "", err - } - switch alg { - case EncryptAESGCM: - block, err := aes.NewCipher([]byte(password)) - if err != nil { - return "", err - } - // AES guarantees correct block size - gcm, _ := cipher.NewGCM(block) - nonceSize := gcm.NonceSize() - if len(enc) < nonceSize+1 { - return "", fmt.Errorf("encrypted value too small") - } - nonce, enc2 := enc[:nonceSize], enc[nonceSize:] - dec, err := gcm.Open(nil, nonce, enc2, nil) - if err != nil { - return "", err - } - return string(dec), nil - default: - return "", fmt.Errorf("unknown algorithm") - } -} - -// Encrypt returns the value encrypted with the provided algorithm in base64 -// format. If encryption fails, an empty string and error are returned. -func Encrypt(alg, password, value string) (string, error) { - switch alg { - case EncryptNone: - return EncryptNone + value, nil - case EncryptAESGCM: - block, err := aes.NewCipher([]byte(password)) - if err != nil { - return "", fmt.Errorf("unable to init aes encryption [%w]", err) - } - // AES guarantees correct block size - gcm, _ := cipher.NewGCM(block) - nonce := make([]byte, gcm.NonceSize()) - _, err = rand.Read(nonce) - if err != nil { - return "", fmt.Errorf("uanble to create nonce: [%w]", err) - } - - enc := gcm.Seal(nonce, nonce, []byte(value), nil) - result := base64.URLEncoding.EncodeToString(enc) - return EncryptAESGCM + result, nil - default: - return "", fmt.Errorf("unknown algorithm %s", alg) - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "strings" +) + +// Decrypt returns the plaintext value of a property encrypted with the Encrypt +// function. If the property does not exist, then the default value will be +// returned with a nil error. If the property value could not be decrypted, +// then an error and the default value will be returned. +func Decrypt(password string, val string) (string, error) { + if strings.Index(val, "]") < 0 { + return "", fmt.Errorf("missing algorithm") + } + alg := val[0 : strings.Index(val, "]")+1] + if alg == EncryptNone { + return val[len(alg):], nil + } + + enc, err := base64.URLEncoding.DecodeString(val[len(alg):]) + if err != nil { + return "", err + } + switch alg { + case EncryptAESGCM: + block, err := aes.NewCipher([]byte(password)) + if err != nil { + return "", err + } + // AES guarantees correct block size + gcm, _ := cipher.NewGCM(block) + nonceSize := gcm.NonceSize() + if len(enc) < nonceSize+1 { + return "", fmt.Errorf("encrypted value too small") + } + nonce, enc2 := enc[:nonceSize], enc[nonceSize:] + dec, err := gcm.Open(nil, nonce, enc2, nil) + if err != nil { + return "", err + } + return string(dec), nil + default: + return "", fmt.Errorf("unknown algorithm") + } +} + +// Encrypt returns the value encrypted with the provided algorithm in base64 +// format. If encryption fails, an empty string and error are returned. +func Encrypt(alg, password, value string) (string, error) { + switch alg { + case EncryptNone: + return EncryptNone + value, nil + case EncryptAESGCM: + block, err := aes.NewCipher([]byte(password)) + if err != nil { + return "", fmt.Errorf("unable to init aes encryption [%w]", err) + } + // AES guarantees correct block size + gcm, _ := cipher.NewGCM(block) + nonce := make([]byte, gcm.NonceSize()) + _, err = rand.Read(nonce) + if err != nil { + return "", fmt.Errorf("uanble to create nonce: [%w]", err) + } + + enc := gcm.Seal(nonce, nonce, []byte(value), nil) + result := base64.URLEncoding.EncodeToString(enc) + return EncryptAESGCM + result, nil + default: + return "", fmt.Errorf("unknown algorithm %s", alg) + } +} diff --git a/encryption_test.go b/encryption_test.go index a48c8aa..55c44d8 100644 --- a/encryption_test.go +++ b/encryption_test.go @@ -1,100 +1,100 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "crypto/rand" - "errors" - "strings" - "testing" -) - -type badReader struct{} - -func (b *badReader) Read(p []byte) (n int, err error) { return 0, errors.New("bad read") } - -func TestEncrypt(t *testing.T) { - val, err := Encrypt("xyz", "password", "plaintext") - if val != "" || err == nil { - t.Errorf("want: '', err != nil; got: %s, %v", val, err) - } - - val, err = Encrypt(EncryptNone, "password", "plaintext") - if val != "[enc:0]plaintext" || err != nil { - t.Errorf("want: '[enc:0]plaintext', err == nil; got: %s, %v", val, err) - } - - val, err = Encrypt(EncryptAESGCM, "small", "plaintext") - if val != "" || err == nil { - t.Errorf("want: '', err != nil; got: %s, %v", val, err) - } - - val, err = Encrypt(EncryptAESGCM, "1234567890123456", "plaintext") - if len(val) < 58 || !strings.HasPrefix(val, "[enc:1]") || err != nil { - t.Errorf("want: '[enc:1]...', err == nil; got: %s, %v", val, err) - } - - oldReader := rand.Reader - rand.Reader = &badReader{} - val, err = Encrypt(EncryptAESGCM, "1234567890123456", "plaintext") - if val != "" || err == nil { - t.Errorf("want: '', err != nil; got: %s, %v", val, err) - } - rand.Reader = oldReader -} - -func TestDecrypt(t *testing.T) { - badpass := "123" - pass := "1234567890123456" - pass256 := "12345678901234567890123456789012" - - val, err := Decrypt(badpass, "[enc:0]") - if val != "" || err != nil { - t.Errorf("want: '', err == nil; got: %s, %v", val, err) - } - - val, err = Decrypt(badpass, "[enc:0]plaintext") - if val != "plaintext" || err != nil { - t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) - } - - val, err = Decrypt(badpass, "[enc:1]qzp-F5Cw1G5n9c7D5LFw4NzanvZzdQzONRPlM3JpLLCO6swpBQ==") - if val != "" || err == nil { - t.Errorf("want: '', err != nil; got: %s, %v", val, err) - } - - val, err = Decrypt(pass, "[enc:1]") - if val != "" || err == nil { - t.Errorf("want: '', err != nil; got: %s, %v", val, err) - } - - val, err = Decrypt(pass, "[enc:1]$$$$") - if val != "" || err == nil { - t.Errorf("want: '', err != nil; got: %s, %v", val, err) - } - - val, err = Decrypt(pass, "[enc:1]qzp-F5Cw1G5n9c7D5LFw4NzanvZzdQzONRPlM3JpLLCO6swpBQ==") - if val != "plaintext" || err != nil { - t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) - } - - val, err = Decrypt(pass, "[enc:1]x-f72AdaTiZfRvX_6kekHpd4xUj1JmMFwbwiADbMZXgJjGpJNg==") - if val != "" || err == nil { - t.Errorf("want: '', err != nil; got: %s, %v", val, err) - } - - val, err = Decrypt(pass256, "[enc:1]x-f72AdaTiZfRvX_6kekHpd4xUj1JmMFwbwiADbMZXgJjGpJNg==") - if val != "plaintext" || err != nil { - t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) - } - - val, err = Decrypt(pass, "[enc:x]abcd") - if val != "" || err == nil { - t.Errorf("want: '', err != nil; got: %s, %v", val, err) - } - - val, err = Decrypt(pass, "abcd") - if val != "" || err == nil { - t.Errorf("want: '', err != nil; got: %s, %v", val, err) - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "crypto/rand" + "errors" + "strings" + "testing" +) + +type badReader struct{} + +func (b *badReader) Read(p []byte) (n int, err error) { return 0, errors.New("bad read") } + +func TestEncrypt(t *testing.T) { + val, err := Encrypt("xyz", "password", "plaintext") + if val != "" || err == nil { + t.Errorf("want: '', err != nil; got: %s, %v", val, err) + } + + val, err = Encrypt(EncryptNone, "password", "plaintext") + if val != "[enc:0]plaintext" || err != nil { + t.Errorf("want: '[enc:0]plaintext', err == nil; got: %s, %v", val, err) + } + + val, err = Encrypt(EncryptAESGCM, "small", "plaintext") + if val != "" || err == nil { + t.Errorf("want: '', err != nil; got: %s, %v", val, err) + } + + val, err = Encrypt(EncryptAESGCM, "1234567890123456", "plaintext") + if len(val) < 58 || !strings.HasPrefix(val, "[enc:1]") || err != nil { + t.Errorf("want: '[enc:1]...', err == nil; got: %s, %v", val, err) + } + + oldReader := rand.Reader + rand.Reader = &badReader{} + val, err = Encrypt(EncryptAESGCM, "1234567890123456", "plaintext") + if val != "" || err == nil { + t.Errorf("want: '', err != nil; got: %s, %v", val, err) + } + rand.Reader = oldReader +} + +func TestDecrypt(t *testing.T) { + badpass := "123" + pass := "1234567890123456" + pass256 := "12345678901234567890123456789012" + + val, err := Decrypt(badpass, "[enc:0]") + if val != "" || err != nil { + t.Errorf("want: '', err == nil; got: %s, %v", val, err) + } + + val, err = Decrypt(badpass, "[enc:0]plaintext") + if val != "plaintext" || err != nil { + t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) + } + + val, err = Decrypt(badpass, "[enc:1]qzp-F5Cw1G5n9c7D5LFw4NzanvZzdQzONRPlM3JpLLCO6swpBQ==") + if val != "" || err == nil { + t.Errorf("want: '', err != nil; got: %s, %v", val, err) + } + + val, err = Decrypt(pass, "[enc:1]") + if val != "" || err == nil { + t.Errorf("want: '', err != nil; got: %s, %v", val, err) + } + + val, err = Decrypt(pass, "[enc:1]$$$$") + if val != "" || err == nil { + t.Errorf("want: '', err != nil; got: %s, %v", val, err) + } + + val, err = Decrypt(pass, "[enc:1]qzp-F5Cw1G5n9c7D5LFw4NzanvZzdQzONRPlM3JpLLCO6swpBQ==") + if val != "plaintext" || err != nil { + t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) + } + + val, err = Decrypt(pass, "[enc:1]x-f72AdaTiZfRvX_6kekHpd4xUj1JmMFwbwiADbMZXgJjGpJNg==") + if val != "" || err == nil { + t.Errorf("want: '', err != nil; got: %s, %v", val, err) + } + + val, err = Decrypt(pass256, "[enc:1]x-f72AdaTiZfRvX_6kekHpd4xUj1JmMFwbwiADbMZXgJjGpJNg==") + if val != "plaintext" || err != nil { + t.Errorf("want: 'plaintext', err == nil; got: %s, %v", val, err) + } + + val, err = Decrypt(pass, "[enc:x]abcd") + if val != "" || err == nil { + t.Errorf("want: '', err != nil; got: %s, %v", val, err) + } + + val, err = Decrypt(pass, "abcd") + if val != "" || err == nil { + t.Errorf("want: '', err != nil; got: %s, %v", val, err) + } +} diff --git a/environment.go b/environment.go index e1091cb..148bf94 100644 --- a/environment.go +++ b/environment.go @@ -1,62 +1,62 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "os" - "strings" -) - -// Environment reads properties from the OS environment. -type Environment struct { - // Normalize indicates that key values should be converted to POSIX-style - // environment variable names. - // - // If true, key values passed to Get and GetDefault will be converted to: - // - Uppercase - // - Alphanumeric (as per ASCII) - // - All non-alphanumeric characters replaced with underscore '_' - // - // For example, 'foo.bar.baz' would become 'FOO_BAR_BAZ' and - // '$my-test#val_1' would become '_MY_TEST_VAL_1'. - Normalize bool -} - -// Ensure that Environment implements PropertyGetter -var _ PropertyGetter = &Environment{} - -// Get retrieves the value of a property from the environment. If the env var -// does not exist, an empty string will be returned. The bool return value -// indicates whether the property was found. -func (e *Environment) Get(key string) (string, bool) { - if e.Normalize { - envKey := strings.Map(normalizeEnv, key) - return os.LookupEnv(envKey) - } else { - return os.LookupEnv(key) - } -} - -// GetDefault retrieves the value of a property from the environment. If the -// env var does not exist, then the default value will be returned. -func (e *Environment) GetDefault(key, defVal string) string { - v, ok := e.Get(key) - if !ok { - return defVal - } - return v -} - -// normalizeEnv converts a rune into a suitable replacement for an environment -// variable name. -func normalizeEnv(r rune) rune { - if r >= 'a' && r <= 'z' { - return r - 32 - } else if r >= 'A' && r <= 'Z' { - return r - } else if r >= '0' && r <= '9' { - return r - } else { - return '_' - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "os" + "strings" +) + +// Environment reads properties from the OS environment. +type Environment struct { + // Normalize indicates that key values should be converted to POSIX-style + // environment variable names. + // + // If true, key values passed to Get and GetDefault will be converted to: + // - Uppercase + // - Alphanumeric (as per ASCII) + // - All non-alphanumeric characters replaced with underscore '_' + // + // For example, 'foo.bar.baz' would become 'FOO_BAR_BAZ' and + // '$my-test#val_1' would become '_MY_TEST_VAL_1'. + Normalize bool +} + +// Ensure that Environment implements PropertyGetter +var _ PropertyGetter = &Environment{} + +// Get retrieves the value of a property from the environment. If the env var +// does not exist, an empty string will be returned. The bool return value +// indicates whether the property was found. +func (e *Environment) Get(key string) (string, bool) { + if e.Normalize { + envKey := strings.Map(normalizeEnv, key) + return os.LookupEnv(envKey) + } else { + return os.LookupEnv(key) + } +} + +// GetDefault retrieves the value of a property from the environment. If the +// env var does not exist, then the default value will be returned. +func (e *Environment) GetDefault(key, defVal string) string { + v, ok := e.Get(key) + if !ok { + return defVal + } + return v +} + +// normalizeEnv converts a rune into a suitable replacement for an environment +// variable name. +func normalizeEnv(r rune) rune { + if r >= 'a' && r <= 'z' { + return r - 32 + } else if r >= 'A' && r <= 'Z' { + return r + } else if r >= '0' && r <= '9' { + return r + } else { + return '_' + } +} diff --git a/environment_test.go b/environment_test.go index 98387d0..83b0684 100644 --- a/environment_test.go +++ b/environment_test.go @@ -1,58 +1,58 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "os" - "testing" -) - -func TestEnvironmentGet(t *testing.T) { - e := &Environment{} - - os.Setenv("PROPS_TEST_VAL1", "abc") - os.Setenv("PROPS_TEST_VAL2", "") - - val, ok := e.Get("PROPS_TEST_VAL1") - if !ok || val != "abc" { - t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) - } - - val, ok = e.Get("PROPS_TEST_VAL2") - if !ok || val != "" { - t.Errorf("want: true, ''; got: %t, '%s'", ok, val) - } - - val, ok = e.Get("PROPS_TEST_VAL3") - if ok || val != "" { - t.Errorf("want: false, ''; got %t, '%s'", ok, val) - } - - e.Normalize = true - val, ok = e.Get("props.TEST:val1") - if !ok || val != "abc" { - t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) - } -} - -func TestEnvironmentGetDefault(t *testing.T) { - e := &Environment{} - - os.Setenv("PROPS_TEST_VAL1", "abc") - os.Setenv("PROPS_TEST_VAL2", "") - - val := e.GetDefault("PROPS_TEST_VAL1", "zzz") - if val != "abc" { - t.Errorf("want: 'abc'; got: '%s'", val) - } - - val = e.GetDefault("PROPS_TEST_VAL2", "def") - if val != "" { - t.Errorf("want: ''; got: '%s'", val) - } - - val = e.GetDefault("PROPS_TEST_VAL3", "ghi") - if val != "ghi" { - t.Errorf("want: 'ghi'; got: '%s'", val) - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "os" + "testing" +) + +func TestEnvironmentGet(t *testing.T) { + e := &Environment{} + + os.Setenv("PROPS_TEST_VAL1", "abc") + os.Setenv("PROPS_TEST_VAL2", "") + + val, ok := e.Get("PROPS_TEST_VAL1") + if !ok || val != "abc" { + t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) + } + + val, ok = e.Get("PROPS_TEST_VAL2") + if !ok || val != "" { + t.Errorf("want: true, ''; got: %t, '%s'", ok, val) + } + + val, ok = e.Get("PROPS_TEST_VAL3") + if ok || val != "" { + t.Errorf("want: false, ''; got %t, '%s'", ok, val) + } + + e.Normalize = true + val, ok = e.Get("props.TEST:val1") + if !ok || val != "abc" { + t.Errorf("want: true, 'abc'; got: %t, '%s'", ok, val) + } +} + +func TestEnvironmentGetDefault(t *testing.T) { + e := &Environment{} + + os.Setenv("PROPS_TEST_VAL1", "abc") + os.Setenv("PROPS_TEST_VAL2", "") + + val := e.GetDefault("PROPS_TEST_VAL1", "zzz") + if val != "abc" { + t.Errorf("want: 'abc'; got: '%s'", val) + } + + val = e.GetDefault("PROPS_TEST_VAL2", "def") + if val != "" { + t.Errorf("want: ''; got: '%s'", val) + } + + val = e.GetDefault("PROPS_TEST_VAL3", "ghi") + if val != "ghi" { + t.Errorf("want: 'ghi'; got: '%s'", val) + } +} diff --git a/scanner_test.go b/scanner_test.go index 8c332fe..4651a9e 100644 --- a/scanner_test.go +++ b/scanner_test.go @@ -1,23 +1,23 @@ -// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). - -package props - -import ( - "bytes" - "testing" -) - -func TestFinishUtfEscape_BadHexChar(t *testing.T) { - s := &scanner{ - p: &Properties{}, - } - s.current = &s.value - s.utfUnits = make([]bytes.Buffer, 1) - s.utfUnits[0].WriteRune('z') - s.finishUtfEscape() - - r, _, _ := s.current.ReadRune() - if r != '\uFFFD' { - t.Errorf("want: \uFFFD; got: %x", r) - } -} +// (c) 2022 Rick Arnold. Licensed under the BSD license (see LICENSE). + +package props + +import ( + "bytes" + "testing" +) + +func TestFinishUtfEscape_BadHexChar(t *testing.T) { + s := &scanner{ + p: &Properties{}, + } + s.current = &s.value + s.utfUnits = make([]bytes.Buffer, 1) + s.utfUnits[0].WriteRune('z') + s.finishUtfEscape() + + r, _, _ := s.current.ReadRune() + if r != '\uFFFD' { + t.Errorf("want: \uFFFD; got: %x", r) + } +}