shithub: furgit

Download patch

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()
 	}
--