From 01ae5ecb514db3b0a1aaf9235e331b671bd64f66 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sun, 16 Nov 2025 00:00:00 +0000 Subject: Move config to its own package --- config.go | 485 -------------------------------------------------------------- 1 file changed, 485 deletions(-) delete mode 100644 config.go (limited to 'config.go') diff --git a/config.go b/config.go deleted file mode 100644 index b60a5f9d..00000000 --- a/config.go +++ /dev/null @@ -1,485 +0,0 @@ -package furgit - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "io" - "strings" - "unicode" -) - -// Config represents a parsed Git configuration. -type Config struct { - entries []ConfigEntry -} - -// ConfigEntry represents a single configuration key-value pair. -type ConfigEntry struct { - Section string - Subsection string - Key string - Value string -} - -// ParseConfig parses a Git configuration from a reader. -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 all configuration entries. -func (c *Config) Entries() []ConfigEntry { - result := make([]ConfigEntry, len(c.entries)) - copy(result, c.entries) - return result -} - -// configParser implements Git config file parsing using character-based reading. -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 == '-' -} -- cgit v1.3.1-10-gc9f91