ref: c0d1cc442031c8200df4a3a499c07081fab40553
parent: 3a7df704f3a48015c898ea7cb694359f5b326515
author: Runxi Yu <me@runxiyu.org>
date: Sun Mar 1 05:57:26 EST 2026
config: Add fuzz, regression tests, and updates
--- a/config/config.go
+++ b/config/config.go
@@ -7,8 +7,9 @@
"errors"
"fmt"
"io"
+ "math"
+ "strconv"
"strings"
- "unicode"
)
// Config holds all parsed configuration entries from a Git config file.
@@ -23,6 +24,80 @@
entries []ConfigEntry
}
+// ValueKind describes the presence and form of a config value.
+type ValueKind uint8
+
+const (
+ // ValueMissing means the queried key does not exist.
+ ValueMissing ValueKind = iota
+ // ValueValueless means the key exists but has no "= <value>" part.
+ ValueValueless
+ // ValueString means the key exists and has an explicit value (possibly "").
+ ValueString
+)
+
+// LookupResult is a value returned by Lookup/LookupAll.
+type LookupResult struct {+ Kind ValueKind
+ Value string
+}
+
+// String returns the explicit string value.
+func (r LookupResult) String() (string, error) {+ switch r.Kind {+ case ValueMissing:
+ return "", errors.New("missing config value")+ case ValueValueless:
+ return "", errors.New("valueless config key")+ case ValueString:
+ return r.Value, nil
+ default:
+ return "", fmt.Errorf("unknown value kind %d", r.Kind)+ }
+}
+
+// Bool interprets this lookup result using Git config boolean rules.
+func (r LookupResult) Bool() (bool, error) {+ switch r.Kind {+ case ValueMissing:
+ return false, errors.New("missing config value")+ case ValueValueless:
+ return true, nil
+ case ValueString:
+ return parseBool(r.Value)
+ default:
+ return false, fmt.Errorf("unknown value kind %d", r.Kind)+ }
+}
+
+// Int interprets this lookup result as a Git integer value.
+func (r LookupResult) Int() (int, error) {+ switch r.Kind {+ case ValueMissing:
+ return 0, errors.New("missing config value")+ case ValueValueless:
+ return 0, errors.New("valueless config key")+ case ValueString:
+ return parseInt(r.Value)
+ default:
+ return 0, fmt.Errorf("unknown value kind %d", r.Kind)+ }
+}
+
+// Int64 interprets this lookup result as a Git int64 value.
+func (r LookupResult) Int64() (int64, error) {+ switch r.Kind {+ case ValueMissing:
+ return 0, errors.New("missing config value")+ case ValueValueless:
+ return 0, errors.New("valueless config key")+ case ValueString:
+ return parseInt64(r.Value)
+ default:
+ return 0, fmt.Errorf("unknown value kind %d", r.Kind)+ }
+}
+
// ConfigEntry represents a single parsed configuration directive.
type ConfigEntry struct {// The section name in canonical lowercase form.
@@ -31,6 +106,8 @@
Subsection string
// The key name in canonical lowercase form.
Key string
+ // Kind records whether this entry has no value or an explicit value.
+ Kind ValueKind
// The interpreted value of the configuration entry, including unescaped
// characters where appropriate.
Value string
@@ -45,9 +122,9 @@
return parser.parse()
}
-// Get retrieves the first value for a given section, optional subsection, and key.
-// Returns an empty string if not found.
-func (c *Config) Get(section, subsection, key string) string {+// Lookup retrieves the first value for a given section, optional subsection,
+// and key.
+func (c *Config) Lookup(section, subsection, key string) LookupResult {section = strings.ToLower(section)
key = strings.ToLower(key)
for _, entry := range c.entries {@@ -54,22 +131,29 @@
if strings.EqualFold(entry.Section, section) &&
entry.Subsection == subsection &&
strings.EqualFold(entry.Key, key) {- return entry.Value
+ return LookupResult{+ Kind: entry.Kind,
+ Value: entry.Value,
+ }
}
}
- return ""
+ return LookupResult{Kind: ValueMissing}}
-// GetAll retrieves all values for a given section, optional subsection, and key.
-func (c *Config) GetAll(section, subsection, key string) []string {+// LookupAll retrieves all values for a given section, optional subsection,
+// and key.
+func (c *Config) LookupAll(section, subsection, key string) []LookupResult {section = strings.ToLower(section)
key = strings.ToLower(key)
- var values []string
+ var values []LookupResult
for _, entry := range c.entries {if strings.EqualFold(entry.Section, section) &&
entry.Subsection == subsection &&
strings.EqualFold(entry.Key, key) {- values = append(values, entry.Value)
+ values = append(values, LookupResult{+ Kind: entry.Kind,
+ Value: entry.Value,
+ })
}
}
return values
@@ -88,7 +172,7 @@
lineNum int
currentSection string
currentSubsec string
- peeked rune
+ peeked byte
hasPeeked bool
}
@@ -108,8 +192,8 @@
return nil, err
}
- // Skip whitespace and newlines
- if ch == '\n' || unicode.IsSpace(ch) {+ // Skip leading whitespace between entries.
+ if isWhitespace(ch) {continue
}
@@ -130,7 +214,7 @@
}
// Key-value pair
- if unicode.IsLetter(ch) {+ if isLetter(ch) {p.unreadChar(ch)
if err := p.parseKeyValue(cfg); err != nil { return nil, fmt.Errorf("furgit: config: line %d: %w", p.lineNum, err)@@ -144,24 +228,24 @@
return cfg, nil
}
-func (p *configParser) nextChar() (rune, error) {+func (p *configParser) nextChar() (byte, error) { if p.hasPeeked {p.hasPeeked = false
return p.peeked, nil
}
- ch, _, err := p.reader.ReadRune()
+ ch, err := p.reader.ReadByte()
if err != nil {return 0, err
}
if ch == '\r' {- next, _, err := p.reader.ReadRune()
+ next, err := p.reader.ReadByte()
if err == nil && next == '\n' {ch = '\n'
} else if err == nil {// Weird but ok
- _ = p.reader.UnreadRune()
+ _ = p.reader.UnreadByte()
}
}
@@ -172,7 +256,7 @@
return ch, nil
}
-func (p *configParser) unreadChar(ch rune) {+func (p *configParser) unreadChar(ch byte) {p.peeked = ch
p.hasPeeked = true
if ch == '\n' && p.lineNum > 1 {@@ -181,7 +265,7 @@
}
func (p *configParser) skipBOM() error {- first, _, err := p.reader.ReadRune()
+ first, err := p.reader.ReadByte()
if errors.Is(err, io.EOF) {return nil
}
@@ -188,9 +272,33 @@
if err != nil {return err
}
- if first != '\uFEFF' {- _ = p.reader.UnreadRune()
+ if first != 0xef {+ _ = p.reader.UnreadByte()
+ return nil
}
+ second, err := p.reader.ReadByte()
+ if err != nil {+ if errors.Is(err, io.EOF) {+ _ = p.reader.UnreadByte()
+ return nil
+ }
+ return err
+ }
+ third, err := p.reader.ReadByte()
+ if err != nil {+ if errors.Is(err, io.EOF) {+ _ = p.reader.UnreadByte()
+ _ = p.reader.UnreadByte()
+ return nil
+ }
+ return err
+ }
+ if second == 0xbb && third == 0xbf {+ return nil
+ }
+ _ = p.reader.UnreadByte()
+ _ = p.reader.UnreadByte()
+ _ = p.reader.UnreadByte()
return nil
}
@@ -225,7 +333,7 @@
return nil
}
- if unicode.IsSpace(ch) {+ if isWhitespace(ch) {return p.parseExtendedSection(&name)
}
@@ -233,7 +341,7 @@
return fmt.Errorf("invalid character in section name: %q", ch)}
- name.WriteRune(unicode.ToLower(ch))
+ name.WriteByte(toLower(ch))
}
}
@@ -243,7 +351,7 @@
if err != nil { return errors.New("unexpected EOF in section header")}
- if !unicode.IsSpace(ch) {+ if !isWhitespace(ch) { if ch != '"' { return errors.New("expected quote after section name")}
@@ -274,9 +382,9 @@
if next == '\n' { return errors.New("newline after backslash in subsection")}
- subsec.WriteRune(next)
+ subsec.WriteByte(next)
} else {- subsec.WriteRune(ch)
+ subsec.WriteByte(ch)
}
}
@@ -306,11 +414,14 @@
var key bytes.Buffer
for {ch, err := p.nextChar()
+ if errors.Is(err, io.EOF) {+ break
+ }
if err != nil {- return errors.New("unexpected EOF reading key")+ return err
}
- if ch == '=' || ch == '\n' || unicode.IsSpace(ch) {+ if ch == '=' || ch == '\n' || isSpace(ch) {p.unreadChar(ch)
break
}
@@ -319,7 +430,7 @@
return fmt.Errorf("invalid character in key: %q", ch)}
- key.WriteRune(unicode.ToLower(ch))
+ key.WriteByte(toLower(ch))
}
keyStr := key.String()
@@ -326,7 +437,7 @@
if len(keyStr) == 0 { return errors.New("empty key name")}
- if !unicode.IsLetter(rune(keyStr[0])) {+ if !isLetter(keyStr[0]) { return errors.New("key must start with a letter")}
@@ -337,7 +448,8 @@
Section: p.currentSection,
Subsection: p.currentSubsec,
Key: keyStr,
- Value: "true",
+ Kind: ValueValueless,
+ Value: "",
})
return nil
}
@@ -350,7 +462,8 @@
Section: p.currentSection,
Subsection: p.currentSubsec,
Key: keyStr,
- Value: "true",
+ Kind: ValueValueless,
+ Value: "",
})
return nil
}
@@ -363,7 +476,8 @@
Section: p.currentSection,
Subsection: p.currentSubsec,
Key: keyStr,
- Value: "true",
+ Kind: ValueValueless,
+ Value: "",
})
return nil
}
@@ -372,7 +486,7 @@
break
}
- if !unicode.IsSpace(ch) {+ if !isSpace(ch) { return fmt.Errorf("unexpected character after key: %q", ch)}
}
@@ -386,6 +500,7 @@
Section: p.currentSection,
Subsection: p.currentSubsec,
Key: keyStr,
+ Kind: ValueString,
Value: value,
})
@@ -405,9 +520,9 @@
return "", errors.New("unexpected EOF in quoted value")}
if trimLen > 0 {- return value.String()[:trimLen], nil
+ return truncateAtNUL(value.String()[:trimLen]), nil
}
- return value.String(), nil
+ return truncateAtNUL(value.String()), nil
}
if err != nil {return "", err
@@ -418,9 +533,9 @@
return "", errors.New("newline in quoted value")}
if trimLen > 0 {- return value.String()[:trimLen], nil
+ return truncateAtNUL(value.String()[:trimLen]), nil
}
- return value.String(), nil
+ return truncateAtNUL(value.String()), nil
}
if inComment {@@ -427,12 +542,12 @@
continue
}
- if unicode.IsSpace(ch) && !inQuote {+ if isWhitespace(ch) && !inQuote { if trimLen == 0 && value.Len() > 0 {trimLen = value.Len()
}
if value.Len() > 0 {- value.WriteRune(ch)
+ value.WriteByte(ch)
}
continue
}
@@ -459,13 +574,13 @@
case '\n':
continue
case 'n':
- value.WriteRune('\n')+ value.WriteByte('\n')case 't':
- value.WriteRune('\t')+ value.WriteByte('\t')case 'b':
- value.WriteRune('\b')+ value.WriteByte('\b')case '\\', '"':
- value.WriteRune(next)
+ value.WriteByte(next)
default:
return "", fmt.Errorf("invalid escape sequence: \\%c", next)}
@@ -477,7 +592,7 @@
continue
}
- value.WriteRune(ch)
+ value.WriteByte(ch)
}
}
@@ -485,8 +600,9 @@
if len(s) == 0 {return false
}
- for _, ch := range s {- if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '-' && ch != '.' {+ for i := 0; i < len(s); i++ {+ ch := s[i]
+ if !isLetter(ch) && !isDigit(ch) && ch != '-' && ch != '.' {return false
}
}
@@ -493,6 +609,123 @@
return true
}
-func isKeyChar(ch rune) bool {- return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-'
+func isKeyChar(ch byte) bool {+ return isLetter(ch) || isDigit(ch) || ch == '-'
+}
+
+func parseBool(value string) (bool, error) {+ switch {+ case strings.EqualFold(value, "true"),
+ strings.EqualFold(value, "yes"),
+ strings.EqualFold(value, "on"):
+ return true, nil
+ case strings.EqualFold(value, "false"),
+ strings.EqualFold(value, "no"),
+ strings.EqualFold(value, "off"),
+ value == "":
+ return false, nil
+ }
+
+ n, err := parseInt32(value)
+ if err != nil {+ return false, fmt.Errorf("invalid boolean value %q", value)+ }
+ return n != 0, nil
+}
+
+func parseInt32(value string) (int32, error) {+ n64, err := parseInt64WithMax(value, math.MaxInt32)
+ if err != nil {+ return 0, err
+ }
+ return int32(n64), nil
+}
+
+func parseInt(value string) (int, error) {+ n64, err := parseInt64WithMax(value, int64(int(^uint(0)>>1)))
+ if err != nil {+ return 0, err
+ }
+ return int(n64), nil
+}
+
+func parseInt64(value string) (int64, error) {+ return parseInt64WithMax(value, int64(^uint64(0)>>1))
+}
+
+func parseInt64WithMax(value string, max int64) (int64, error) {+ if value == "" {+ return 0, errors.New("empty value")+ }
+
+ trimmed := strings.TrimLeft(value, " \t\n\r\f\v")
+ if trimmed == "" {+ return 0, errors.New("empty value")+ }
+
+ numPart := trimmed
+ factor := int64(1)
+ if last := trimmed[len(trimmed)-1]; last == 'k' || last == 'K' || last == 'm' || last == 'M' || last == 'g' || last == 'G' {+ switch toLower(last) {+ case 'k':
+ factor = 1024
+ case 'm':
+ factor = 1024 * 1024
+ case 'g':
+ factor = 1024 * 1024 * 1024
+ }
+ numPart = trimmed[:len(trimmed)-1]
+ }
+ if numPart == "" {+ return 0, errors.New("missing integer value")+ }
+
+ n, err := strconv.ParseInt(numPart, 0, 64)
+ if err != nil {+ return 0, err
+ }
+
+ intMax := max
+ intMin := -max - 1
+ if n > 0 && n > intMax/factor {+ return 0, errors.New("integer overflow")+ }
+ if n < 0 && n < intMin/factor {+ return 0, errors.New("integer overflow")+ }
+
+ n *= factor
+ return n, nil
+}
+
+func truncateAtNUL(value string) string {+ for i := 0; i < len(value); i++ {+ if value[i] == 0 {+ return value[:i]
+ }
+ }
+ return value
+}
+
+func isSpace(ch byte) bool {+ return ch == ' ' || ch == '\t'
+}
+
+func isWhitespace(ch byte) bool {+ return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\v' || ch == '\f'
+}
+
+func isLetter(ch byte) bool {+ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
+}
+
+func isDigit(ch byte) bool {+ return ch >= '0' && ch <= '9'
+}
+
+func toLower(ch byte) byte {+ if ch >= 'A' && ch <= 'Z' {+ return ch + ('a' - 'A')+ }
+ return ch
}
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1,7 +1,9 @@
package config_test
import (
+ "bytes"
"os"
+ "os/exec"
"path/filepath"
"strings"
"testing"
@@ -25,6 +27,35 @@
return testRepo.Run(t, "config", "--get", key)
}
+func gitConfigGetE(testRepo *testgit.TestRepo, key string) (string, error) {+ //nolint:noctx
+ cmd := exec.Command("git", "config", "--get", key) //#nosec G204+ cmd.Dir = testRepo.Dir()
+ cmd.Env = append(os.Environ(),
+ "GIT_CONFIG_GLOBAL=/dev/null",
+ "GIT_CONFIG_SYSTEM=/dev/null",
+ )
+ out, err := cmd.CombinedOutput()
+ return strings.TrimSpace(string(out)), err
+}
+
+func lookupValue(cfg *config.Config, section, subsection, key string) string {+ result := cfg.Lookup(section, subsection, key)
+ if result.Kind == config.ValueMissing {+ return ""
+ }
+ return result.Value
+}
+
+func lookupAllValues(cfg *config.Config, section, subsection, key string) []string {+ results := cfg.LookupAll(section, subsection, key)
+ values := make([]string, 0, len(results))
+ for _, result := range results {+ values = append(values, result.Value)
+ }
+ return values
+}
+
func TestConfigAgainstGit(t *testing.T) {t.Parallel()
testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper@@ -42,16 +73,16 @@
t.Fatalf("ParseConfig failed: %v", err)}
- if got := cfg.Get("core", "", "bare"); got != "true" {+ if got := lookupValue(cfg, "core", "", "bare"); got != "true" { t.Errorf("core.bare: got %q, want %q", got, "true")}
- if got := cfg.Get("core", "", "filemode"); got != "false" {+ if got := lookupValue(cfg, "core", "", "filemode"); got != "false" { t.Errorf("core.filemode: got %q, want %q", got, "false")}
- if got := cfg.Get("user", "", "name"); got != "Jane Doe" {+ if got := lookupValue(cfg, "user", "", "name"); got != "Jane Doe" { t.Errorf("user.name: got %q, want %q", got, "Jane Doe")}
- if got := cfg.Get("user", "", "email"); got != "jane@example.org" {+ if got := lookupValue(cfg, "user", "", "email"); got != "jane@example.org" { t.Errorf("user.email: got %q, want %q", got, "jane@example.org")}
})
@@ -72,10 +103,10 @@
t.Fatalf("ParseConfig failed: %v", err)}
- if got := cfg.Get("remote", "origin", "url"); got != "https://example.org/repo.git" {+ if got := lookupValue(cfg, "remote", "origin", "url"); got != "https://example.org/repo.git" { t.Errorf("remote.origin.url: got %q, want %q", got, "https://example.org/repo.git")}
- if got := cfg.Get("remote", "origin", "fetch"); got != "+refs/heads/*:refs/remotes/origin/*" {+ if got := lookupValue(cfg, "remote", "origin", "fetch"); got != "+refs/heads/*:refs/remotes/origin/*" { t.Errorf("remote.origin.fetch: got %q, want %q", got, "+refs/heads/*:refs/remotes/origin/*")}
})
@@ -97,7 +128,7 @@
t.Fatalf("ParseConfig failed: %v", err)}
- fetches := cfg.GetAll("remote", "origin", "fetch")+ fetches := lookupAllValues(cfg, "remote", "origin", "fetch")
if len(fetches) != 3 { t.Fatalf("expected 3 fetch values, got %d", len(fetches))}
@@ -133,13 +164,13 @@
t.Fatalf("ParseConfig failed: %v", err)}
- if got := cfg.Get("core", "", "bare"); got != gitVerifyBare {+ if got := lookupValue(cfg, "core", "", "bare"); got != gitVerifyBare { t.Errorf("core.bare: got %q, want %q (from git)", got, gitVerifyBare)}
- if got := cfg.Get("CORE", "", "BARE"); got != gitVerifyBare {+ if got := lookupValue(cfg, "CORE", "", "BARE"); got != gitVerifyBare { t.Errorf("CORE.BARE: got %q, want %q (from git)", got, gitVerifyBare)}
- if got := cfg.Get("core", "", "filemode"); got != gitVerifyFilemode {+ if got := lookupValue(cfg, "core", "", "filemode"); got != gitVerifyFilemode { t.Errorf("core.filemode: got %q, want %q (from git)", got, gitVerifyFilemode)}
})
@@ -173,7 +204,7 @@
}
for _, tt := range tests {- if got := cfg.Get("test", "", tt.key); got != tt.want {+ if got := lookupValue(cfg, "test", "", tt.key); got != tt.want { t.Errorf("test.%s: got %q, want %q (from git)", tt.key, got, tt.want)}
}
@@ -180,6 +211,105 @@
})
}
+func TestConfigLookupKindsAndBool(t *testing.T) {+ t.Parallel()
+ cfgText := "[test]\nnovalue\nempty =\ntruthy = yes\nnumeric = -2\nleadspace = \" 1\"\nleadtab = \"\t-2\"\nksuffix = 1k\nhex = 0x10\nmaxi32 = 2147483647\ntoobig = 2147483648\ntoosmall = -2147483649\nbadnum = \" 2x\"\n"
+ cfg, err := config.ParseConfig(strings.NewReader(cfgText))
+ if err != nil {+ t.Fatalf("ParseConfig failed: %v", err)+ }
+
+ novalue := cfg.Lookup("test", "", "novalue")+ if novalue.Kind != config.ValueValueless {+ t.Fatalf("novalue kind: got %v, want %v", novalue.Kind, config.ValueValueless)+ }
+ novalueBool, err := novalue.Bool()
+ if err != nil || !novalueBool {+ t.Fatalf("novalue bool: got (%v, %v), want (true, nil)", novalueBool, err)+ }
+
+ empty := cfg.Lookup("test", "", "empty")+ if empty.Kind != config.ValueString || empty.Value != "" {+ t.Fatalf("empty: got (%v, %q), want (%v, %q)", empty.Kind, empty.Value, config.ValueString, "")+ }
+ emptyBool, err := empty.Bool()
+ if err != nil || emptyBool {+ t.Fatalf("empty bool: got (%v, %v), want (false, nil)", emptyBool, err)+ }
+
+ truthyBool, err := cfg.Lookup("test", "", "truthy").Bool()+ if err != nil || !truthyBool {+ t.Fatalf("truthy bool: got (%v, %v), want (true, nil)", truthyBool, err)+ }
+ numericBool, err := cfg.Lookup("test", "", "numeric").Bool()+ if err != nil || !numericBool {+ t.Fatalf("numeric bool: got (%v, %v), want (true, nil)", numericBool, err)+ }
+ leadspaceBool, err := cfg.Lookup("test", "", "leadspace").Bool()+ if err != nil || !leadspaceBool {+ t.Fatalf("leadspace bool: got (%v, %v), want (true, nil)", leadspaceBool, err)+ }
+ leadtabBool, err := cfg.Lookup("test", "", "leadtab").Bool()+ if err != nil || !leadtabBool {+ t.Fatalf("leadtab bool: got (%v, %v), want (true, nil)", leadtabBool, err)+ }
+ ksuffixBool, err := cfg.Lookup("test", "", "ksuffix").Bool()+ if err != nil || !ksuffixBool {+ t.Fatalf("ksuffix bool: got (%v, %v), want (true, nil)", ksuffixBool, err)+ }
+ maxi32Bool, err := cfg.Lookup("test", "", "maxi32").Bool()+ if err != nil || !maxi32Bool {+ t.Fatalf("maxi32 bool: got (%v, %v), want (true, nil)", maxi32Bool, err)+ }
+ if _, err := cfg.Lookup("test", "", "toobig").Bool(); err == nil {+ t.Fatal("toobig bool: expected error")+ }
+ if _, err := cfg.Lookup("test", "", "toosmall").Bool(); err == nil {+ t.Fatal("toosmall bool: expected error")+ }
+ if _, err := cfg.Lookup("test", "", "badnum").Bool(); err == nil {+ t.Fatal("badnum bool: expected error")+ }
+
+ if _, err := novalue.String(); err == nil {+ t.Fatal("novalue string: expected error")+ }
+ emptyString, err := empty.String()
+ if err != nil || emptyString != "" {+ t.Fatalf("empty string: got (%q, %v), want (%q, nil)", emptyString, err, "")+ }
+
+ numericInt, err := cfg.Lookup("test", "", "numeric").Int()+ if err != nil || numericInt != -2 {+ t.Fatalf("numeric int: got (%v, %v), want (-2, nil)", numericInt, err)+ }
+ ksuffixInt, err := cfg.Lookup("test", "", "ksuffix").Int()+ if err != nil || ksuffixInt != 1024 {+ t.Fatalf("ksuffix int: got (%v, %v), want (1024, nil)", ksuffixInt, err)+ }
+ hexInt64, err := cfg.Lookup("test", "", "hex").Int64()+ if err != nil || hexInt64 != 16 {+ t.Fatalf("hex int64: got (%v, %v), want (16, nil)", hexInt64, err)+ }
+ if _, err := cfg.Lookup("test", "", "badnum").Int(); err == nil {+ t.Fatal("badnum int: expected error")+ }
+
+ missing := cfg.Lookup("test", "", "missing")+ if missing.Kind != config.ValueMissing {+ t.Fatalf("missing kind: got %v, want %v", missing.Kind, config.ValueMissing)+ }
+ if _, err := missing.Bool(); err == nil {+ t.Fatal("missing bool: expected error")+ }
+ if _, err := missing.Int(); err == nil {+ t.Fatal("missing int: expected error")+ }
+ if _, err := missing.String(); err == nil {+ t.Fatal("missing string: expected error")+ }
+}
+
func TestConfigComplexValuesAgainstGit(t *testing.T) {t.Parallel()
testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper@@ -200,7 +330,7 @@
tests := []string{"spaced", "special", "path", "number"} for _, key := range tests {want := gitConfigGet(t, testRepo, "test."+key)
- if got := cfg.Get("test", "", key); got != want {+ if got := lookupValue(cfg, "test", "", key); got != want { t.Errorf("test.%s: got %q, want %q (from git)", key, got, want)}
}
@@ -282,4 +412,131 @@
}
})
}
+}
+
+func TestConfigEOFAfterKeyAgainstGit(t *testing.T) {+ t.Parallel()
+ testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectid.AlgorithmSHA1, Bare: true})+ cfgPath := filepath.Join(testRepo.Dir(), "config")
+
+ cfgData := []byte("[Core]BAre")+ if err := os.WriteFile(cfgPath, cfgData, 0o600); err != nil {+ t.Fatalf("failed to write config: %v", err)+ }
+
+ gitValue, gitErr := gitConfigGetE(testRepo, "Core.BAre")
+ furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData))
+
+ if (gitErr == nil) != (furErr == nil) {+ t.Fatalf("git: %v\nfur: %v", gitErr, furErr)+ }
+ if furErr != nil {+ return
+ }
+
+ if got := lookupValue(furConfig, "Core", "", "BAre"); got != gitValue {+ t.Fatalf("git: %q\nfur: %q", gitValue, got)+ }
+}
+
+func TestConfigNULValueAgainstGit(t *testing.T) {+ t.Parallel()
+ testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectid.AlgorithmSHA1, Bare: true})+ cfgPath := filepath.Join(testRepo.Dir(), "config")
+
+ cfgData := []byte("[Core]BAre=\x00")+ if err := os.WriteFile(cfgPath, cfgData, 0o600); err != nil {+ t.Fatalf("failed to write config: %v", err)+ }
+
+ gitValue, gitErr := gitConfigGetE(testRepo, "Core.BAre")
+ furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData))
+
+ if (gitErr == nil) != (furErr == nil) {+ t.Fatalf("git: %v\nfur: %v", gitErr, furErr)+ }
+ if furErr != nil {+ return
+ }
+
+ if got := lookupValue(furConfig, "Core", "", "BAre"); got != gitValue {+ t.Fatalf("git: %q\nfur: %q", gitValue, got)+ }
+}
+
+func TestConfigCarriageReturnSeparatorAgainstGit(t *testing.T) {+ t.Parallel()
+ testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectid.AlgorithmSHA1, Bare: true})+ cfgPath := filepath.Join(testRepo.Dir(), "config")
+
+ cfgData := []byte("[Core \"sub\"]\rBAre")+ if err := os.WriteFile(cfgPath, cfgData, 0o600); err != nil {+ t.Fatalf("failed to write config: %v", err)+ }
+
+ gitValue, gitErr := gitConfigGetE(testRepo, "Core.sub.BAre")
+ furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData))
+
+ if (gitErr == nil) != (furErr == nil) {+ t.Fatalf("git: %v\nfur: %v", gitErr, furErr)+ }
+ if furErr != nil {+ return
+ }
+
+ if got := lookupValue(furConfig, "Core", "sub", "BAre"); got != gitValue {+ t.Fatalf("git: %q\nfur: %q", gitValue, got)+ }
+}
+
+func FuzzConfig(f *testing.F) {+ f.Add([]byte("[core]\nbare = true"), "core.bare")+ f.Add([]byte("[core]\nbare = true\n[core/invalid]"), "core.bare")+ f.Add([]byte("[core \"sub\"]\nbare = true"), "core.sub.bare")+
+ testRepo := testgit.NewRepo(f, testgit.RepoOptions{ObjectFormat: objectid.AlgorithmSHA1, Bare: true})+ cfgPath := filepath.Join(testRepo.Dir(), "config")
+
+ f.Fuzz(func(t *testing.T, cfgData []byte, gitKey string) {+ if err := os.WriteFile(cfgPath, cfgData, 0o600); err != nil {+ t.Fatalf("failed to write config: %v", err)+ }
+
+ gitValue, gitErr := gitConfigGetE(testRepo, gitKey)
+ furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData))
+ if furErr == nil && furConfig == nil {+ t.Fatalf("ParseConfig returned nil config with nil error")+ }
+
+ sameErr := (gitErr == nil) == (furErr == nil)
+ if !sameErr {+ if furErr == nil {+ return
+ }
+ t.Fatalf("git: %v\nfur: %v", gitErr, furErr)+ }
+ if furErr == nil {+ parts := strings.SplitN(gitKey, ".", 3)
+ furSection := parts[0]
+ var furSubsection, furKey string
+ switch len(parts) {+ case 1:
+ case 2:
+ furKey = parts[1]
+ case 3:
+ furSubsection = parts[1]
+ furKey = parts[2]
+ default:
+ t.Fatalf("unexpected split(%q): %v", gitKey, parts)+ }
+
+ furValue := lookupValue(furConfig, furSection, furSubsection, furKey)
+ if gitValue != furValue {+ t.Fatalf(
+ "key: %v (%v.%v.%v)\ngit: %q\nfur: %q",
+ gitKey, furSection, furSubsection, furKey, gitValue, furValue,
+ )
+ }
+ }
+ })
}
--- /dev/null
+++ b/config/testdata/fuzz/FuzzConfig/86abac337c758b6b
@@ -1,0 +1,3 @@
+go test fuzz v1
+[]byte("[Core \"sub\"]BAre=\xfe")+string("Core.sub.BAre")--- /dev/null
+++ b/config/testdata/fuzz/FuzzConfig/a76c07b1ae70ed94
@@ -1,0 +1,3 @@
+go test fuzz v1
+[]byte("[Core \"sub\"]\rBAre")+string("Core.sub.BAre")--- /dev/null
+++ b/config/testdata/fuzz/FuzzConfig/c0718ca6bc57e0e2
@@ -1,0 +1,3 @@
+go test fuzz v1
+[]byte("[Core]BAre=\x00")+string("Core.BAre")--- /dev/null
+++ b/config/testdata/fuzz/FuzzConfig/dc6f7dcd8aaa1cf7
@@ -1,0 +1,3 @@
+go test fuzz v1
+[]byte("[Core]BAre")+string("Core.BAre")--- a/repository/repository.go
+++ b/repository/repository.go
@@ -133,7 +133,7 @@
}
func detectObjectAlgorithm(cfg *config.Config) (objectid.Algorithm, error) {- algoName := cfg.Get("extensions", "", "objectformat")+ algoName := cfg.Lookup("extensions", "", "objectformat").Value if algoName == "" {algoName = objectid.AlgorithmSHA1.String()
}
--
⑨