diff --git a/src/restic/options/options.go b/src/restic/options/options.go new file mode 100644 index 000000000..635216041 --- /dev/null +++ b/src/restic/options/options.go @@ -0,0 +1,61 @@ +package options + +import ( + "restic/errors" + "strings" +) + +// Options holds options in the form key=value. +type Options map[string]string + +// splitKeyValue splits at the first equals (=) sign. +func splitKeyValue(s string) (key string, value string) { + data := strings.SplitN(s, "=", 2) + key = strings.ToLower(strings.TrimSpace(data[0])) + if len(data) == 1 { + // no equals sign is treated as the empty value + return key, "" + } + + return key, strings.TrimSpace(data[1]) +} + +// Parse takes a slice of key=value pairs and returns an Options type. +// The key may include namespaces, separated by dots. Example: "foo.bar=value". +// Keys are converted to lower-case. +func Parse(in []string) (Options, error) { + opts := make(Options, len(in)) + + for _, opt := range in { + key, value := splitKeyValue(opt) + + if key == "" { + return Options{}, errors.Fatalf("empty key is not a valid option") + } + opts[key] = value + } + + return opts, nil +} + +// Extract returns an Options type with all keys in namespace ns, which is +// also stripped from the keys. ns must end with a dot. +func (o Options) Extract(ns string) Options { + l := len(ns) + if ns[l-1] != '.' { + ns += "." + l++ + } + + opts := make(Options) + + for k, v := range o { + if !strings.HasPrefix(k, ns) { + continue + } + + opts[k[l:]] = v + } + + return opts +} diff --git a/src/restic/options/options_test.go b/src/restic/options/options_test.go new file mode 100644 index 000000000..feead1c89 --- /dev/null +++ b/src/restic/options/options_test.go @@ -0,0 +1,107 @@ +package options + +import ( + "fmt" + "reflect" + "testing" +) + +var optsTests = []struct { + input []string + output Options +}{ + { + []string{"foo=bar", "bar=baz ", "k="}, + Options{ + "foo": "bar", + "bar": "baz", + "k": "", + }, + }, + { + []string{"Foo=23", "baR", "k=thing with spaces"}, + Options{ + "foo": "23", + "bar": "", + "k": "thing with spaces", + }, + }, + { + []string{"k=thing with spaces", "k2=more spaces = not evil"}, + Options{ + "k": "thing with spaces", + "k2": "more spaces = not evil", + }, + }, +} + +func TestParseOptions(t *testing.T) { + for i, test := range optsTests { + t.Run(fmt.Sprintf("test-%v", i), func(t *testing.T) { + opts, err := Parse(test.input) + if err != nil { + t.Fatalf("unable to parse options: %v", err) + } + + if !reflect.DeepEqual(opts, test.output) { + t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.output, opts) + } + }) + } +} + +var invalidOptsTests = []struct { + input []string + err string +}{ + { + []string{"=bar", "bar=baz", "k="}, + "empty key is not a valid option", + }, +} + +func TestParseInvalidOptions(t *testing.T) { + for _, test := range invalidOptsTests { + t.Run(test.err, func(t *testing.T) { + _, err := Parse(test.input) + if err == nil { + t.Fatalf("expected error (%v) not found, err is nil", test.err) + } + + if err.Error() != test.err { + t.Fatalf("expected error %q, got %q", test.err, err.Error()) + } + }) + } +} + +var extractTests = []struct { + input Options + ns string + output Options +}{ + { + input: Options{ + "foo.bar:": "baz", + "s3.timeout": "10s", + "sftp.timeout": "5s", + "global": "foobar", + }, + ns: "s3", + output: Options{ + "timeout": "10s", + }, + }, +} + +func TestOptionsExtract(t *testing.T) { + for _, test := range extractTests { + t.Run(test.ns, func(t *testing.T) { + opts := test.input.Extract(test.ns) + + if !reflect.DeepEqual(opts, test.output) { + t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.output, opts) + } + }) + } +}