From f36918966727be99bfe9d461059269f36f92058a Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Fri, 6 Mar 2026 10:52:02 +0800 Subject: config: Split files --- config/bom.go | 56 ++++++ config/char.go | 52 ++++++ config/config.go | 158 ----------------- config/entry.go | 25 +++ config/extended_section.go | 76 ++++++++ config/key_value.go | 119 +++++++++++++ config/lookup.go | 45 +++++ config/parser.go | 428 ++------------------------------------------- config/result.go | 68 +++++++ config/section.go | 41 +++++ config/typed.go | 170 ++++++++++++++++++ config/value.go | 111 ++++++++++++ config/value_parse.go | 158 ----------------- 13 files changed, 773 insertions(+), 734 deletions(-) create mode 100644 config/bom.go create mode 100644 config/char.go create mode 100644 config/entry.go create mode 100644 config/extended_section.go create mode 100644 config/key_value.go create mode 100644 config/lookup.go create mode 100644 config/result.go create mode 100644 config/section.go create mode 100644 config/typed.go create mode 100644 config/value.go delete mode 100644 config/value_parse.go (limited to 'config') diff --git a/config/bom.go b/config/bom.go new file mode 100644 index 00000000..48963e7f --- /dev/null +++ b/config/bom.go @@ -0,0 +1,56 @@ +package config + +import ( + "errors" + "io" +) + +func (p *configParser) skipBOM() error { + first, err := p.reader.ReadByte() + if errors.Is(err, io.EOF) { + return nil + } + + if err != nil { + return err + } + + 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 +} diff --git a/config/char.go b/config/char.go new file mode 100644 index 00000000..da52013c --- /dev/null +++ b/config/char.go @@ -0,0 +1,52 @@ +package config + +func (p *configParser) nextChar() (byte, error) { + if p.hasPeeked { + p.hasPeeked = false + + return p.peeked, nil + } + + ch, err := p.reader.ReadByte() + if err != nil { + return 0, err + } + + if ch == '\r' { + next, err := p.reader.ReadByte() + if err == nil && next == '\n' { + ch = '\n' + } else if err == nil { + // Weird but ok + _ = p.reader.UnreadByte() + } + } + + if ch == '\n' { + p.lineNum++ + } + + return ch, nil +} + +func (p *configParser) unreadChar(ch byte) { + p.peeked = ch + + p.hasPeeked = true + if ch == '\n' && p.lineNum > 1 { + p.lineNum-- + } +} + +func (p *configParser) skipToEOL() error { + for { + ch, err := p.nextChar() + if err != nil { + return err + } + + if ch == '\n' { + return nil + } + } +} diff --git a/config/config.go b/config/config.go index 4ff8b21d..5564d940 100644 --- a/config/config.go +++ b/config/config.go @@ -1,14 +1,6 @@ // Package config provides configuration parsing. package config -import ( - "bufio" - "errors" - "fmt" - "io" - "strings" -) - // Config holds all parsed configuration entries from a Git config file. // // A Config preserves the ordering of entries as they appeared in the source. @@ -20,153 +12,3 @@ import ( type Config struct { 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 "= " 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. - Section string - // The subsection name, retaining the exact form parsed from the input. - 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 -} - -// 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() -} - -// 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 { - if strings.EqualFold(entry.Section, section) && - entry.Subsection == subsection && - strings.EqualFold(entry.Key, key) { - return LookupResult{ - Kind: entry.Kind, - Value: entry.Value, - } - } - } - - return LookupResult{Kind: ValueMissing} -} - -// 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 []LookupResult - - for _, entry := range c.entries { - if strings.EqualFold(entry.Section, section) && - entry.Subsection == subsection && - strings.EqualFold(entry.Key, key) { - values = append(values, LookupResult{ - Kind: entry.Kind, - Value: 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 -} diff --git a/config/entry.go b/config/entry.go new file mode 100644 index 00000000..a2a39965 --- /dev/null +++ b/config/entry.go @@ -0,0 +1,25 @@ +package config + +// 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 + // 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 +} + +// 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 +} diff --git a/config/extended_section.go b/config/extended_section.go new file mode 100644 index 00000000..410009e7 --- /dev/null +++ b/config/extended_section.go @@ -0,0 +1,76 @@ +package config + +import ( + "bytes" + "errors" + "fmt" + "strings" +) + +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 !isWhitespace(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.WriteByte(next) + } else { + subsec.WriteByte(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 +} diff --git a/config/key_value.go b/config/key_value.go new file mode 100644 index 00000000..482bfcc7 --- /dev/null +++ b/config/key_value.go @@ -0,0 +1,119 @@ +package config + +import ( + "bytes" + "errors" + "fmt" + "io" +) + +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 errors.Is(err, io.EOF) { + break + } + + if err != nil { + return err + } + + if ch == '=' || ch == '\n' || isSpace(ch) { + p.unreadChar(ch) + + break + } + + if !isKeyChar(ch) { + return fmt.Errorf("invalid character in key: %q", ch) + } + + key.WriteByte(toLower(ch)) + } + + keyStr := key.String() + if len(keyStr) == 0 { + return errors.New("empty key name") + } + + if !isLetter(keyStr[0]) { + return errors.New("key must start with a letter") + } + + for { + ch, err := p.nextChar() + if errors.Is(err, io.EOF) { + cfg.entries = append(cfg.entries, ConfigEntry{ + Section: p.currentSection, + Subsection: p.currentSubsec, + Key: keyStr, + Kind: ValueValueless, + Value: "", + }) + + return nil + } + + if err != nil { + return err + } + + if ch == '\n' { + cfg.entries = append(cfg.entries, ConfigEntry{ + Section: p.currentSection, + Subsection: p.currentSubsec, + Key: keyStr, + Kind: ValueValueless, + Value: "", + }) + + return nil + } + + if ch == '#' || ch == ';' { + err := p.skipToEOL() + if err != nil && !errors.Is(err, io.EOF) { + return err + } + + cfg.entries = append(cfg.entries, ConfigEntry{ + Section: p.currentSection, + Subsection: p.currentSubsec, + Key: keyStr, + Kind: ValueValueless, + Value: "", + }) + + return nil + } + + if ch == '=' { + break + } + + if !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, + Kind: ValueString, + Value: value, + }) + + return nil +} diff --git a/config/lookup.go b/config/lookup.go new file mode 100644 index 00000000..1f3c03fe --- /dev/null +++ b/config/lookup.go @@ -0,0 +1,45 @@ +package config + +import "strings" + +// 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 { + if strings.EqualFold(entry.Section, section) && + entry.Subsection == subsection && + strings.EqualFold(entry.Key, key) { + return LookupResult{ + Kind: entry.Kind, + Value: entry.Value, + } + } + } + + return LookupResult{Kind: ValueMissing} +} + +// 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 []LookupResult + + for _, entry := range c.entries { + if strings.EqualFold(entry.Section, section) && + entry.Subsection == subsection && + strings.EqualFold(entry.Key, key) { + values = append(values, LookupResult{ + Kind: entry.Kind, + Value: entry.Value, + }) + } + } + + return values +} diff --git a/config/parser.go b/config/parser.go index edaf1944..c56a68d5 100644 --- a/config/parser.go +++ b/config/parser.go @@ -2,13 +2,21 @@ package config import ( "bufio" - "bytes" "errors" "fmt" "io" - "strings" ) +// 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() +} + type configParser struct { reader *bufio.Reader lineNum int @@ -78,419 +86,3 @@ func (p *configParser) parse() (*Config, error) { return cfg, nil } - -func (p *configParser) nextChar() (byte, error) { - if p.hasPeeked { - p.hasPeeked = false - - return p.peeked, nil - } - - ch, err := p.reader.ReadByte() - if err != nil { - return 0, err - } - - if ch == '\r' { - next, err := p.reader.ReadByte() - if err == nil && next == '\n' { - ch = '\n' - } else if err == nil { - // Weird but ok - _ = p.reader.UnreadByte() - } - } - - if ch == '\n' { - p.lineNum++ - } - - return ch, nil -} - -func (p *configParser) unreadChar(ch byte) { - p.peeked = ch - - p.hasPeeked = true - if ch == '\n' && p.lineNum > 1 { - p.lineNum-- - } -} - -func (p *configParser) skipBOM() error { - first, err := p.reader.ReadByte() - if errors.Is(err, io.EOF) { - return nil - } - - if err != nil { - return err - } - - 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 -} - -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 isWhitespace(ch) { - return p.parseExtendedSection(&name) - } - - if !isKeyChar(ch) && ch != '.' { - return fmt.Errorf("invalid character in section name: %q", ch) - } - - name.WriteByte(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 !isWhitespace(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.WriteByte(next) - } else { - subsec.WriteByte(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 errors.Is(err, io.EOF) { - break - } - - if err != nil { - return err - } - - if ch == '=' || ch == '\n' || isSpace(ch) { - p.unreadChar(ch) - - break - } - - if !isKeyChar(ch) { - return fmt.Errorf("invalid character in key: %q", ch) - } - - key.WriteByte(toLower(ch)) - } - - keyStr := key.String() - if len(keyStr) == 0 { - return errors.New("empty key name") - } - - if !isLetter(keyStr[0]) { - return errors.New("key must start with a letter") - } - - for { - ch, err := p.nextChar() - if errors.Is(err, io.EOF) { - cfg.entries = append(cfg.entries, ConfigEntry{ - Section: p.currentSection, - Subsection: p.currentSubsec, - Key: keyStr, - Kind: ValueValueless, - Value: "", - }) - - return nil - } - - if err != nil { - return err - } - - if ch == '\n' { - cfg.entries = append(cfg.entries, ConfigEntry{ - Section: p.currentSection, - Subsection: p.currentSubsec, - Key: keyStr, - Kind: ValueValueless, - Value: "", - }) - - return nil - } - - if ch == '#' || ch == ';' { - err := p.skipToEOL() - if err != nil && !errors.Is(err, io.EOF) { - return err - } - - cfg.entries = append(cfg.entries, ConfigEntry{ - Section: p.currentSection, - Subsection: p.currentSubsec, - Key: keyStr, - Kind: ValueValueless, - Value: "", - }) - - return nil - } - - if ch == '=' { - break - } - - if !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, - Kind: ValueString, - Value: value, - }) - - return nil -} - -func (p *configParser) parseValue() (string, error) { - var ( - value bytes.Buffer - inQuote bool - inComment bool - ) - - trimLen := 0 - - for { - ch, err := p.nextChar() - if errors.Is(err, io.EOF) { - if inQuote { - return "", errors.New("unexpected EOF in quoted value") - } - - if trimLen > 0 { - return truncateAtNUL(value.String()[:trimLen]), nil - } - - return truncateAtNUL(value.String()), nil - } - - if err != nil { - return "", err - } - - if ch == '\n' { - if inQuote { - return "", errors.New("newline in quoted value") - } - - if trimLen > 0 { - return truncateAtNUL(value.String()[:trimLen]), nil - } - - return truncateAtNUL(value.String()), nil - } - - if inComment { - continue - } - - if isWhitespace(ch) && !inQuote { - if trimLen == 0 && value.Len() > 0 { - trimLen = value.Len() - } - - if value.Len() > 0 { - value.WriteByte(ch) - } - - continue - } - - if !inQuote && (ch == '#' || ch == ';') { - inComment = true - - continue - } - - if trimLen > 0 { - trimLen = 0 - } - - if ch == '\\' { - next, err := p.nextChar() - if errors.Is(err, io.EOF) { - return "", errors.New("unexpected EOF after backslash") - } - - if err != nil { - return "", err - } - - switch next { - case '\n': - continue - case 'n': - value.WriteByte('\n') - case 't': - value.WriteByte('\t') - case 'b': - value.WriteByte('\b') - case '\\', '"': - value.WriteByte(next) - default: - return "", fmt.Errorf("invalid escape sequence: \\%c", next) - } - - continue - } - - if ch == '"' { - inQuote = !inQuote - - continue - } - - value.WriteByte(ch) - } -} diff --git a/config/result.go b/config/result.go new file mode 100644 index 00000000..1f53edc5 --- /dev/null +++ b/config/result.go @@ -0,0 +1,68 @@ +package config + +import ( + "errors" + "fmt" +) + +// 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) + } +} diff --git a/config/section.go b/config/section.go new file mode 100644 index 00000000..66adf011 --- /dev/null +++ b/config/section.go @@ -0,0 +1,41 @@ +package config + +import ( + "bytes" + "errors" + "fmt" + "strings" +) + +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 isWhitespace(ch) { + return p.parseExtendedSection(&name) + } + + if !isKeyChar(ch) && ch != '.' { + return fmt.Errorf("invalid character in section name: %q", ch) + } + + name.WriteByte(toLower(ch)) + } +} diff --git a/config/typed.go b/config/typed.go new file mode 100644 index 00000000..39eeb767 --- /dev/null +++ b/config/typed.go @@ -0,0 +1,170 @@ +package config + +import ( + "errors" + "fmt" + "math" + "strconv" + "strings" + + "codeberg.org/lindenii/furgit/internal/intconv" +) + +// 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 "= " part. + ValueValueless + // ValueString means the key exists and has an explicit value (possibly ""). + ValueString +) + +func isValidSection(s string) bool { + if len(s) == 0 { + return false + } + + for i := range len(s) { + ch := s[i] + if !isLetter(ch) && !isDigit(ch) && ch != '-' && ch != '.' { + return false + } + } + + return true +} + +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 intconv.Int64ToInt32(n64) +} + +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, maxValue 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 := maxValue + intMin := -maxValue - 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 := range len(value) { + 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 +} diff --git a/config/value.go b/config/value.go new file mode 100644 index 00000000..3ade9c16 --- /dev/null +++ b/config/value.go @@ -0,0 +1,111 @@ +package config + +import ( + "bytes" + "errors" + "fmt" + "io" +) + +func (p *configParser) parseValue() (string, error) { + var ( + value bytes.Buffer + inQuote bool + inComment bool + ) + + trimLen := 0 + + for { + ch, err := p.nextChar() + if errors.Is(err, io.EOF) { + if inQuote { + return "", errors.New("unexpected EOF in quoted value") + } + + if trimLen > 0 { + return truncateAtNUL(value.String()[:trimLen]), nil + } + + return truncateAtNUL(value.String()), nil + } + + if err != nil { + return "", err + } + + if ch == '\n' { + if inQuote { + return "", errors.New("newline in quoted value") + } + + if trimLen > 0 { + return truncateAtNUL(value.String()[:trimLen]), nil + } + + return truncateAtNUL(value.String()), nil + } + + if inComment { + continue + } + + if isWhitespace(ch) && !inQuote { + if trimLen == 0 && value.Len() > 0 { + trimLen = value.Len() + } + + if value.Len() > 0 { + value.WriteByte(ch) + } + + continue + } + + if !inQuote && (ch == '#' || ch == ';') { + inComment = true + + continue + } + + if trimLen > 0 { + trimLen = 0 + } + + if ch == '\\' { + next, err := p.nextChar() + if errors.Is(err, io.EOF) { + return "", errors.New("unexpected EOF after backslash") + } + + if err != nil { + return "", err + } + + switch next { + case '\n': + continue + case 'n': + value.WriteByte('\n') + case 't': + value.WriteByte('\t') + case 'b': + value.WriteByte('\b') + case '\\', '"': + value.WriteByte(next) + default: + return "", fmt.Errorf("invalid escape sequence: \\%c", next) + } + + continue + } + + if ch == '"' { + inQuote = !inQuote + + continue + } + + value.WriteByte(ch) + } +} diff --git a/config/value_parse.go b/config/value_parse.go deleted file mode 100644 index 9659e476..00000000 --- a/config/value_parse.go +++ /dev/null @@ -1,158 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "math" - "strconv" - "strings" - - "codeberg.org/lindenii/furgit/internal/intconv" -) - -func isValidSection(s string) bool { - if len(s) == 0 { - return false - } - - for i := range len(s) { - ch := s[i] - if !isLetter(ch) && !isDigit(ch) && ch != '-' && ch != '.' { - return false - } - } - - return true -} - -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 intconv.Int64ToInt32(n64) -} - -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, maxValue 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 := maxValue - intMin := -maxValue - 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 := range len(value) { - 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 -} -- cgit v1.3.1-10-gc9f91