From 25295dd0de43846145740bc588548774fe3c75f3 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Sat, 19 Nov 2022 13:50:34 -0800 Subject: [PATCH] Allow quoted keys for --git-config This allows keys to contain literal ':' which would previously confuse the parser. --- cmd/git-sync/main.go | 91 ++++++++++++++++++++++++++------------- cmd/git-sync/main_test.go | 70 ++++++++++++++++++++++++------ 2 files changed, 118 insertions(+), 43 deletions(-) diff --git a/cmd/git-sync/main.go b/cmd/git-sync/main.go index 5a718c4..ff71b49 100644 --- a/cmd/git-sync/main.go +++ b/cmd/git-sync/main.go @@ -127,7 +127,7 @@ var flAskPassURL = flag.String("askpass-url", envString("GIT_ASKPASS_URL", ""), var flGitCmd = flag.String("git", envString("GIT_SYNC_GIT", "git"), "the git command to run (subject to PATH search, mostly for testing)") var flGitConfig = flag.String("git-config", envString("GIT_SYNC_GIT_CONFIG", ""), - "additional git config options in 'key1:val1,key2:val2' format") + "additional git config options in 'section.var1:val1,\"section.sub.var2\":\"val2\"' format") var flGitGC = flag.String("git-gc", envString("GIT_SYNC_GIT_GC", "auto"), "git garbage collection behavior: one of 'auto', 'always', 'aggressive', or 'off'") @@ -1293,9 +1293,19 @@ func parseGitConfigs(configsFlag string) ([]keyVal, error) { if r, ok := <-ch; !ok { break } else { - cur.key, err = parseGitConfigKey(r, ch) - if err != nil { - return nil, err + // This can accumulate things that git doesn't allow, but we'll + // just let git handle it, rather than try to pre-validate to their + // spec. + if r == '"' { + cur.key, err = parseGitConfigQKey(ch) + if err != nil { + return nil, err + } + } else { + cur.key, err = parseGitConfigKey(r, ch) + if err != nil { + return nil, err + } } } @@ -1322,6 +1332,23 @@ func parseGitConfigs(configsFlag string) ([]keyVal, error) { return result, nil } +func parseGitConfigQKey(ch <-chan rune) (string, error) { + str, err := parseQString(ch) + if err != nil { + return "", err + } + + // The next character must be a colon. + r, ok := <-ch + if !ok { + return "", fmt.Errorf("unexpected end of key: %q", str) + } + if r != ':' { + return "", fmt.Errorf("unexpected character after quoted key: %q%c", str, r) + } + return str, nil +} + func parseGitConfigKey(r rune, ch <-chan rune) (string, error) { buf := make([]rune, 0, 64) buf = append(buf, r) @@ -1331,9 +1358,6 @@ func parseGitConfigKey(r rune, ch <-chan rune) (string, error) { case r == ':': return string(buf), nil default: - // This can accumulate things that git doesn't allow, but we'll - // just let git handle it, rather than try to pre-validate to their - // spec. buf = append(buf, r) } } @@ -1341,30 +1365,17 @@ func parseGitConfigKey(r rune, ch <-chan rune) (string, error) { } func parseGitConfigQVal(ch <-chan rune) (string, error) { - buf := make([]rune, 0, 64) - - for r := range ch { - switch r { - case '\\': - if e, err := unescape(ch); err != nil { - return "", err - } else { - buf = append(buf, e) - } - case '"': - // Once we have a closing quote, the next must be either a comma or - // end-of-string. This helps reset the state for the next key, if - // there is one. - r, ok := <-ch - if ok && r != ',' { - return "", fmt.Errorf("unexpected trailing character '%c'", r) - } - return string(buf), nil - default: - buf = append(buf, r) - } + str, err := parseQString(ch) + if err != nil { + return "", err } - return "", fmt.Errorf("unexpected end of value: %q", string(buf)) + + // If there is a next character, it must be a comma. + r, ok := <-ch + if ok && r != ',' { + return "", fmt.Errorf("unexpected character after quoted value %q%c", str, r) + } + return str, nil } func parseGitConfigVal(r rune, ch <-chan rune) (string, error) { @@ -1389,6 +1400,26 @@ func parseGitConfigVal(r rune, ch <-chan rune) (string, error) { return string(buf), nil } +func parseQString(ch <-chan rune) (string, error) { + buf := make([]rune, 0, 64) + + for r := range ch { + switch r { + case '\\': + if e, err := unescape(ch); err != nil { + return "", err + } else { + buf = append(buf, e) + } + case '"': + return string(buf), nil + default: + buf = append(buf, r) + } + } + return "", fmt.Errorf("unexpected end of quoted string: %q", string(buf)) +} + // unescape processes most of the documented escapes that git config supports. func unescape(ch <-chan rune) (rune, error) { r, ok := <-ch diff --git a/cmd/git-sync/main_test.go b/cmd/git-sync/main_test.go index 1f06650..543143e 100644 --- a/cmd/git-sync/main_test.go +++ b/cmd/git-sync/main_test.go @@ -114,26 +114,66 @@ func TestParseGitConfigs(t *testing.T) { name: "one-pair", input: `k:v`, expect: []keyVal{keyVal{"k", "v"}}, + }, { + name: "one-pair-qkey", + input: `"k":v`, + expect: []keyVal{keyVal{"k", "v"}}, }, { name: "one-pair-qval", input: `k:"v"`, expect: []keyVal{keyVal{"k", "v"}}, + }, { + name: "one-pair-qkey-qval", + input: `"k":"v"`, + expect: []keyVal{keyVal{"k", "v"}}, + }, { + name: "multi-pair", + input: `k1:v1,"k2":v2,k3:"v3","k4":"v4"`, + expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}, {"k4", "v4"}}, }, { name: "garbage", input: `abc123`, fail: true, }, { - name: "invalid-val", - input: `k:v\xv`, - fail: true, + name: "key-section-var", + input: `sect.var:v`, + expect: []keyVal{keyVal{"sect.var", "v"}}, }, { - name: "invalid-qval", - input: `k:"v\xv"`, - fail: true, + name: "key-section-subsection-var", + input: `sect.sub.var:v`, + expect: []keyVal{keyVal{"sect.sub.var", "v"}}, }, { - name: "two-pair", - input: `k1:v1,k2:v2`, - expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}}, + name: "key-subsection-with-space", + input: `k.sect.sub section:v`, + expect: []keyVal{keyVal{"k.sect.sub section", "v"}}, + }, { + name: "key-subsection-with-escape", + input: `k.sect.sub\tsection:v`, + expect: []keyVal{keyVal{"k.sect.sub\\tsection", "v"}}, + }, { + name: "key-subsection-with-comma", + input: `k.sect.sub,section:v`, + expect: []keyVal{keyVal{"k.sect.sub,section", "v"}}, + }, { + name: "qkey-subsection-with-space", + input: `"k.sect.sub section":v`, + expect: []keyVal{keyVal{"k.sect.sub section", "v"}}, + }, { + name: "qkey-subsection-with-escapes", + input: `"k.sect.sub\t\n\\section":v`, + expect: []keyVal{keyVal{"k.sect.sub\t\n\\section", "v"}}, + }, { + name: "qkey-subsection-with-comma", + input: `"k.sect.sub,section":v`, + expect: []keyVal{keyVal{"k.sect.sub,section", "v"}}, + }, { + name: "qkey-subsection-with-colon", + input: `"k.sect.sub:section":v`, + expect: []keyVal{keyVal{"k.sect.sub:section", "v"}}, + }, { + name: "invalid-qkey", + input: `"k\xk":v"`, + fail: true, }, { name: "val-spaces", input: `k1:v 1,k2:v 2`, @@ -155,17 +195,21 @@ func TestParseGitConfigs(t *testing.T) { input: `k1:"v1",k2:"v2",`, expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}}, }, { - name: "val-escapes", + name: "val-with-escapes", input: `k1:v\n\t\\\"\,1`, expect: []keyVal{{"k1", "v\n\t\\\",1"}}, }, { - name: "qval-escapes", + name: "qval-with-escapes", input: `k1:"v\n\t\\\"\,1"`, expect: []keyVal{{"k1", "v\n\t\\\",1"}}, }, { - name: "qval-comma", + name: "qval-with-comma", input: `k1:"v,1"`, expect: []keyVal{{"k1", "v,1"}}, + }, { + name: "qkey-missing-close", + input: `"k1:v1`, + fail: true, }, { name: "qval-missing-close", input: `k1:"v1`, @@ -182,7 +226,7 @@ func TestParseGitConfigs(t *testing.T) { t.Errorf("unexpected success") } if !reflect.DeepEqual(kvs, tc.expect) { - t.Errorf("bad result: expected %v, got %v", tc.expect, kvs) + t.Errorf("bad result:\n\texpected: %#v\n\t got: %#v", tc.expect, kvs) } }) }