diff options
Diffstat (limited to 'config')
| -rw-r--r-- | config/bom.go | 56 | ||||
| -rw-r--r-- | config/char.go | 52 | ||||
| -rw-r--r-- | config/config.go | 158 | ||||
| -rw-r--r-- | config/entry.go | 25 | ||||
| -rw-r--r-- | config/extended_section.go | 76 | ||||
| -rw-r--r-- | config/key_value.go | 119 | ||||
| -rw-r--r-- | config/lookup.go | 45 | ||||
| -rw-r--r-- | config/parser.go | 428 | ||||
| -rw-r--r-- | config/result.go | 68 | ||||
| -rw-r--r-- | config/section.go | 41 | ||||
| -rw-r--r-- | config/typed.go (renamed from config/value_parse.go) | 12 | ||||
| -rw-r--r-- | config/value.go | 111 |
12 files changed, 615 insertions, 576 deletions
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 "= <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. - 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/value_parse.go b/config/typed.go index 9659e476..39eeb767 100644 --- a/config/value_parse.go +++ b/config/typed.go @@ -10,6 +10,18 @@ import ( "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 "= <value>" 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 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) + } +} |
