ref: 565bee5877265ac4e58f3970bb2a8d1195177537
dir: /config/config.go/
// Package config provides routines to parse Git configuration files.
package config
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strings"
"unicode"
)
// Config holds all parsed configuration entries from a Git config file.
//
// A Config preserves the ordering of entries as they appeared in the source.
//
// Lookups are matched case-insensitively for section and key names, and
// subsections must match exactly.
//
// Includes aren't supported yet; they will be supported in a later revision.
type Config struct {
entries []ConfigEntry
}
// ConfigEntry represents a single parsed configuration directive.
type ConfigEntry struct {
// The section name in canonical lowercase form.
Section string
// The subsection name, retaining the exact form parsed from the input.
Subsection string
// The key name in canonical lowercase form.
Key string
// The interpreted value of the configuration entry, including unescaped
// characters where appropriate.
Value string
}
// ParseConfig reads and parses Git configuration entries from r.
func ParseConfig(r io.Reader) (*Config, error) {
parser := &configParser{
reader: bufio.NewReader(r),
lineNum: 1,
}
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 {
section = strings.ToLower(section)
key = strings.ToLower(key)
for _, entry := range c.entries {
if strings.EqualFold(entry.Section, section) &&
entry.Subsection == subsection &&
strings.EqualFold(entry.Key, key) {
return entry.Value
}
}
return ""
}
// GetAll retrieves all values for a given section, optional subsection, and key.
func (c *Config) GetAll(section, subsection, key string) []string {
section = strings.ToLower(section)
key = strings.ToLower(key)
var values []string
for _, entry := range c.entries {
if strings.EqualFold(entry.Section, section) &&
entry.Subsection == subsection &&
strings.EqualFold(entry.Key, key) {
values = append(values, entry.Value)
}
}
return values
}
// Entries returns a copy of all parsed configuration entries in the order they
// appeared. Modifying the returned slice does not affect the Config.
func (c *Config) Entries() []ConfigEntry {
result := make([]ConfigEntry, len(c.entries))
copy(result, c.entries)
return result
}
type configParser struct {
reader *bufio.Reader
lineNum int
currentSection string
currentSubsec string
peeked rune
hasPeeked bool
}
func (p *configParser) parse() (*Config, error) {
cfg := &Config{}
if err := p.skipBOM(); err != nil {
return nil, err
}
for {
ch, err := p.nextChar()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
// Skip whitespace and newlines
if ch == '\n' || unicode.IsSpace(ch) {
continue
}
// Comments
if ch == '#' || ch == ';' {
if err := p.skipToEOL(); err != nil && err != io.EOF {
return nil, err
}
continue
}
// Section header
if ch == '[' {
if err := p.parseSection(); err != nil {
return nil, fmt.Errorf("furgit: config: line %d: %w", p.lineNum, err)
}
continue
}
// Key-value pair
if unicode.IsLetter(ch) {
p.unreadChar(ch)
if err := p.parseKeyValue(cfg); err != nil {
return nil, fmt.Errorf("furgit: config: line %d: %w", p.lineNum, err)
}
continue
}
return nil, fmt.Errorf("furgit: config: line %d: unexpected character %q", p.lineNum, ch)
}
return cfg, nil
}
func (p *configParser) nextChar() (rune, error) {
if p.hasPeeked {
p.hasPeeked = false
return p.peeked, nil
}
ch, _, err := p.reader.ReadRune()
if err != nil {
return 0, err
}
if ch == '\r' {
next, _, err := p.reader.ReadRune()
if err == nil && next == '\n' {
ch = '\n'
} else if err == nil {
// Weird but ok
_ = p.reader.UnreadRune()
}
}
if ch == '\n' {
p.lineNum++
}
return ch, nil
}
func (p *configParser) unreadChar(ch rune) {
p.peeked = ch
p.hasPeeked = true
if ch == '\n' && p.lineNum > 1 {
p.lineNum--
}
}
func (p *configParser) skipBOM() error {
first, _, err := p.reader.ReadRune()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if first != '\uFEFF' {
_ = p.reader.UnreadRune()
}
return nil
}
func (p *configParser) skipToEOL() error {
for {
ch, err := p.nextChar()
if err != nil {
return err
}
if ch == '\n' {
return nil
}
}
}
func (p *configParser) parseSection() error {
var name bytes.Buffer
for {
ch, err := p.nextChar()
if err != nil {
return errors.New("unexpected EOF in section header")
}
if ch == ']' {
section := name.String()
if !isValidSection(section) {
return fmt.Errorf("invalid section name: %q", section)
}
p.currentSection = strings.ToLower(section)
p.currentSubsec = ""
return nil
}
if unicode.IsSpace(ch) {
return p.parseExtendedSection(&name)
}
if !isKeyChar(ch) && ch != '.' {
return fmt.Errorf("invalid character in section name: %q", ch)
}
name.WriteRune(unicode.ToLower(ch))
}
}
func (p *configParser) parseExtendedSection(sectionName *bytes.Buffer) error {
for {
ch, err := p.nextChar()
if err != nil {
return errors.New("unexpected EOF in section header")
}
if !unicode.IsSpace(ch) {
if ch != '"' {
return errors.New("expected quote after section name")
}
break
}
}
var subsec bytes.Buffer
for {
ch, err := p.nextChar()
if err != nil {
return errors.New("unexpected EOF in subsection")
}
if ch == '\n' {
return errors.New("newline in subsection")
}
if ch == '"' {
break
}
if ch == '\\' {
next, err := p.nextChar()
if err != nil {
return errors.New("unexpected EOF after backslash in subsection")
}
if next == '\n' {
return errors.New("newline after backslash in subsection")
}
subsec.WriteRune(next)
} else {
subsec.WriteRune(ch)
}
}
ch, err := p.nextChar()
if err != nil {
return errors.New("unexpected EOF after subsection")
}
if ch != ']' {
return fmt.Errorf("expected ']' after subsection, got %q", ch)
}
section := sectionName.String()
if !isValidSection(section) {
return fmt.Errorf("invalid section name: %q", section)
}
p.currentSection = strings.ToLower(section)
p.currentSubsec = subsec.String()
return nil
}
func (p *configParser) parseKeyValue(cfg *Config) error {
if p.currentSection == "" {
return errors.New("key-value pair before any section header")
}
var key bytes.Buffer
for {
ch, err := p.nextChar()
if err != nil {
return errors.New("unexpected EOF reading key")
}
if ch == '=' || ch == '\n' || unicode.IsSpace(ch) {
p.unreadChar(ch)
break
}
if !isKeyChar(ch) {
return fmt.Errorf("invalid character in key: %q", ch)
}
key.WriteRune(unicode.ToLower(ch))
}
keyStr := key.String()
if len(keyStr) == 0 {
return errors.New("empty key name")
}
if !unicode.IsLetter(rune(keyStr[0])) {
return errors.New("key must start with a letter")
}
for {
ch, err := p.nextChar()
if err == io.EOF {
cfg.entries = append(cfg.entries, ConfigEntry{
Section: p.currentSection,
Subsection: p.currentSubsec,
Key: keyStr,
Value: "true",
})
return nil
}
if err != nil {
return err
}
if ch == '\n' {
cfg.entries = append(cfg.entries, ConfigEntry{
Section: p.currentSection,
Subsection: p.currentSubsec,
Key: keyStr,
Value: "true",
})
return nil
}
if ch == '#' || ch == ';' {
if err := p.skipToEOL(); err != nil && err != io.EOF {
return err
}
cfg.entries = append(cfg.entries, ConfigEntry{
Section: p.currentSection,
Subsection: p.currentSubsec,
Key: keyStr,
Value: "true",
})
return nil
}
if ch == '=' {
break
}
if !unicode.IsSpace(ch) {
return fmt.Errorf("unexpected character after key: %q", ch)
}
}
value, err := p.parseValue()
if err != nil {
return err
}
cfg.entries = append(cfg.entries, ConfigEntry{
Section: p.currentSection,
Subsection: p.currentSubsec,
Key: keyStr,
Value: value,
})
return nil
}
func (p *configParser) parseValue() (string, error) {
var value bytes.Buffer
var inQuote bool
var inComment bool
trimLen := 0
for {
ch, err := p.nextChar()
if err == io.EOF {
if inQuote {
return "", errors.New("unexpected EOF in quoted value")
}
if trimLen > 0 {
return value.String()[:trimLen], nil
}
return value.String(), nil
}
if err != nil {
return "", err
}
if ch == '\n' {
if inQuote {
return "", errors.New("newline in quoted value")
}
if trimLen > 0 {
return value.String()[:trimLen], nil
}
return value.String(), nil
}
if inComment {
continue
}
if unicode.IsSpace(ch) && !inQuote {
if trimLen == 0 && value.Len() > 0 {
trimLen = value.Len()
}
if value.Len() > 0 {
value.WriteRune(ch)
}
continue
}
if !inQuote && (ch == '#' || ch == ';') {
inComment = true
continue
}
if trimLen > 0 {
trimLen = 0
}
if ch == '\\' {
next, err := p.nextChar()
if err == io.EOF {
return "", errors.New("unexpected EOF after backslash")
}
if err != nil {
return "", err
}
switch next {
case '\n':
continue
case 'n':
value.WriteRune('\n')
case 't':
value.WriteRune('\t')
case 'b':
value.WriteRune('\b')
case '\\', '"':
value.WriteRune(next)
default:
return "", fmt.Errorf("invalid escape sequence: \\%c", next)
}
continue
}
if ch == '"' {
inQuote = !inQuote
continue
}
value.WriteRune(ch)
}
}
func isValidSection(s string) bool {
if len(s) == 0 {
return false
}
for _, ch := range s {
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '-' && ch != '.' {
return false
}
}
return true
}
func isKeyChar(ch rune) bool {
return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-'
}