diff options
author | Runxi Yu <me@runxiyu.org> | 2025-04-06 09:26:46 +0800 |
---|---|---|
committer | Runxi Yu <me@runxiyu.org> | 2025-04-06 09:27:53 +0800 |
commit | da1d8f4e7c332c7109427915e6459b10209cedce (patch) | |
tree | 280b921be3b51f93d82d916b4eaa89387b7102cc /internal/scfg | |
parent | git2c, git2d: Rename cmd1 and cmd2 descriptively (diff) | |
download | forge-0.1.32.tar.gz forge-0.1.32.tar.zst forge-0.1.32.zip |
Move the Go stuff to ./forged/v0.1.32
Diffstat (limited to 'internal/scfg')
-rw-r--r-- | internal/scfg/.golangci.yaml | 25 | ||||
-rw-r--r-- | internal/scfg/reader.go | 157 | ||||
-rw-r--r-- | internal/scfg/scfg.go | 59 | ||||
-rw-r--r-- | internal/scfg/struct.go | 82 | ||||
-rw-r--r-- | internal/scfg/unmarshal.go | 375 | ||||
-rw-r--r-- | internal/scfg/writer.go | 112 |
6 files changed, 0 insertions, 810 deletions
diff --git a/internal/scfg/.golangci.yaml b/internal/scfg/.golangci.yaml deleted file mode 100644 index 7db9793..0000000 --- a/internal/scfg/.golangci.yaml +++ /dev/null @@ -1,25 +0,0 @@ -linters: - enable-all: true - disable: - - perfsprint - - wsl - - varnamelen - - nlreturn - - exhaustruct - - wrapcheck - - lll - - exhaustive - - intrange - - godox - - nestif - - err113 - - staticcheck - - errorlint - - cyclop - - nonamedreturns - - funlen - - gochecknoglobals - -issues: - max-issues-per-linter: 0 - max-same-issues: 0 diff --git a/internal/scfg/reader.go b/internal/scfg/reader.go deleted file mode 100644 index 6a2bedc..0000000 --- a/internal/scfg/reader.go +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr> - -package scfg - -import ( - "bufio" - "fmt" - "io" - "os" - "strings" -) - -// This limits the max block nesting depth to prevent stack overflows. -const maxNestingDepth = 1000 - -// Load loads a configuration file. -func Load(path string) (Block, error) { - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - - return Read(f) -} - -// Read parses a configuration file from an io.Reader. -func Read(r io.Reader) (Block, error) { - scanner := bufio.NewScanner(r) - - dec := decoder{scanner: scanner} - block, closingBrace, err := dec.readBlock() - if err != nil { - return nil, err - } else if closingBrace { - return nil, fmt.Errorf("line %v: unexpected '}'", dec.lineno) - } - - return block, scanner.Err() -} - -type decoder struct { - scanner *bufio.Scanner - lineno int - blockDepth int -} - -// readBlock reads a block. closingBrace is true if parsing stopped on '}' -// (otherwise, it stopped on Scanner.Scan). -func (dec *decoder) readBlock() (block Block, closingBrace bool, err error) { - dec.blockDepth++ - defer func() { - dec.blockDepth-- - }() - - if dec.blockDepth >= maxNestingDepth { - return nil, false, fmt.Errorf("exceeded max block depth") - } - - for dec.scanner.Scan() { - dec.lineno++ - - l := dec.scanner.Text() - words, err := splitWords(l) - if err != nil { - return nil, false, fmt.Errorf("line %v: %v", dec.lineno, err) - } else if len(words) == 0 { - continue - } - - if len(words) == 1 && l[len(l)-1] == '}' { - closingBrace = true - break - } - - var d *Directive - if words[len(words)-1] == "{" && l[len(l)-1] == '{' { - words = words[:len(words)-1] - - var name string - params := words - if len(words) > 0 { - name, params = words[0], words[1:] - } - - startLineno := dec.lineno - childBlock, childClosingBrace, err := dec.readBlock() - if err != nil { - return nil, false, err - } else if !childClosingBrace { - return nil, false, fmt.Errorf("line %v: unterminated block", startLineno) - } - - // Allows callers to tell apart "no block" and "empty block" - if childBlock == nil { - childBlock = Block{} - } - - d = &Directive{Name: name, Params: params, Children: childBlock, lineno: dec.lineno} - } else { - d = &Directive{Name: words[0], Params: words[1:], lineno: dec.lineno} - } - block = append(block, d) - } - - return block, closingBrace, nil -} - -func splitWords(l string) ([]string, error) { - var ( - words []string - sb strings.Builder - escape bool - quote rune - wantWSP bool - ) - for _, ch := range l { - switch { - case escape: - sb.WriteRune(ch) - escape = false - case wantWSP && (ch != ' ' && ch != '\t'): - return words, fmt.Errorf("atom not allowed after quoted string") - case ch == '\\': - escape = true - case quote != 0 && ch == quote: - quote = 0 - wantWSP = true - if sb.Len() == 0 { - words = append(words, "") - } - case quote == 0 && len(words) == 0 && sb.Len() == 0 && ch == '#': - return nil, nil - case quote == 0 && (ch == '\'' || ch == '"'): - if sb.Len() > 0 { - return words, fmt.Errorf("quoted string not allowed after atom") - } - quote = ch - case quote == 0 && (ch == ' ' || ch == '\t'): - if sb.Len() > 0 { - words = append(words, sb.String()) - } - sb.Reset() - wantWSP = false - default: - sb.WriteRune(ch) - } - } - if quote != 0 { - return words, fmt.Errorf("unterminated quoted string") - } - if sb.Len() > 0 { - words = append(words, sb.String()) - } - return words, nil -} diff --git a/internal/scfg/scfg.go b/internal/scfg/scfg.go deleted file mode 100644 index 4533e63..0000000 --- a/internal/scfg/scfg.go +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr> - -// Package scfg parses and formats configuration files. -// Note that this fork of scfg behaves differently from upstream scfg. -package scfg - -import ( - "fmt" -) - -// Block is a list of directives. -type Block []*Directive - -// GetAll returns a list of directives with the provided name. -func (blk Block) GetAll(name string) []*Directive { - l := make([]*Directive, 0, len(blk)) - for _, child := range blk { - if child.Name == name { - l = append(l, child) - } - } - return l -} - -// Get returns the first directive with the provided name. -func (blk Block) Get(name string) *Directive { - for _, child := range blk { - if child.Name == name { - return child - } - } - return nil -} - -// Directive is a configuration directive. -type Directive struct { - Name string - Params []string - - Children Block - - lineno int -} - -// ParseParams extracts parameters from the directive. It errors out if the -// user hasn't provided enough parameters. -func (d *Directive) ParseParams(params ...*string) error { - if len(d.Params) < len(params) { - return fmt.Errorf("directive %q: want %v params, got %v", d.Name, len(params), len(d.Params)) - } - for i, ptr := range params { - if ptr == nil { - continue - } - *ptr = d.Params[i] - } - return nil -} diff --git a/internal/scfg/struct.go b/internal/scfg/struct.go deleted file mode 100644 index 98ec943..0000000 --- a/internal/scfg/struct.go +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr> - -package scfg - -import ( - "fmt" - "reflect" - "strings" - "sync" -) - -// structInfo contains scfg metadata for structs. -type structInfo struct { - param int // index of field storing parameters - children map[string]int // indices of fields storing child directives -} - -var ( - structCacheMutex sync.Mutex - structCache = make(map[reflect.Type]*structInfo) -) - -func getStructInfo(t reflect.Type) (*structInfo, error) { - structCacheMutex.Lock() - defer structCacheMutex.Unlock() - - if info := structCache[t]; info != nil { - return info, nil - } - - info := &structInfo{ - param: -1, - children: make(map[string]int), - } - - for i := 0; i < t.NumField(); i++ { - f := t.Field(i) - if f.Anonymous { - return nil, fmt.Errorf("scfg: anonymous struct fields are not supported") - } else if !f.IsExported() { - continue - } - - tag := f.Tag.Get("scfg") - parts := strings.Split(tag, ",") - k, options := parts[0], parts[1:] - if k == "-" { - continue - } else if k == "" { - k = f.Name - } - - isParam := false - for _, opt := range options { - switch opt { - case "param": - isParam = true - default: - return nil, fmt.Errorf("scfg: invalid option %q in struct tag", opt) - } - } - - if isParam { - if info.param >= 0 { - return nil, fmt.Errorf("scfg: param option specified multiple times in struct tag in %v", t) - } - if parts[0] != "" { - return nil, fmt.Errorf("scfg: name must be empty when param option is specified in struct tag in %v", t) - } - info.param = i - } else { - if _, ok := info.children[k]; ok { - return nil, fmt.Errorf("scfg: key %q specified multiple times in struct tag in %v", k, t) - } - info.children[k] = i - } - } - - structCache[t] = info - return info, nil -} diff --git a/internal/scfg/unmarshal.go b/internal/scfg/unmarshal.go deleted file mode 100644 index e9e1a52..0000000 --- a/internal/scfg/unmarshal.go +++ /dev/null @@ -1,375 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr> -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> - -package scfg - -import ( - "encoding" - "fmt" - "io" - "reflect" - "strconv" -) - -// Decoder reads and decodes an scfg document from an input stream. -type Decoder struct { - r io.Reader - unknownDirectives []*Directive -} - -// NewDecoder returns a new decoder which reads from r. -func NewDecoder(r io.Reader) *Decoder { - return &Decoder{r: r} -} - -// UnknownDirectives returns a slice of all unknown directives encountered -// during Decode. -func (dec *Decoder) UnknownDirectives() []*Directive { - return dec.unknownDirectives -} - -// Decode reads scfg document from the input and stores it in the value pointed -// to by v. -// -// If v is nil or not a pointer, Decode returns an error. -// -// Blocks can be unmarshaled to: -// -// - Maps. Each directive is unmarshaled into a map entry. The map key must -// be a string. -// - Structs. Each directive is unmarshaled into a struct field. -// -// Duplicate directives are not allowed, unless the struct field or map value -// is a slice of values representing a directive: structs or maps. -// -// Directives can be unmarshaled to: -// -// - Maps. The children block is unmarshaled into the map. Parameters are not -// allowed. -// - Structs. The children block is unmarshaled into the struct. Parameters -// are allowed if one of the struct fields contains the "param" option in -// its tag. -// - Slices. Parameters are unmarshaled into the slice. Children blocks are -// not allowed. -// - Arrays. Parameters are unmarshaled into the array. The number of -// parameters must match exactly the length of the array. Children blocks -// are not allowed. -// - Strings, booleans, integers, floating-point values, values implementing -// encoding.TextUnmarshaler. Only a single parameter is allowed and is -// unmarshaled into the value. Children blocks are not allowed. -// -// The decoding of each struct field can be customized by the format string -// stored under the "scfg" key in the struct field's tag. The tag contains the -// name of the field possibly followed by a comma-separated list of options. -// The name may be empty in order to specify options without overriding the -// default field name. As a special case, if the field name is "-", the field -// is ignored. The "param" option specifies that directive parameters are -// stored in this field (the name must be empty). -func (dec *Decoder) Decode(v interface{}) error { - block, err := Read(dec.r) - if err != nil { - return err - } - - rv := reflect.ValueOf(v) - if rv.Kind() != reflect.Ptr || rv.IsNil() { - return fmt.Errorf("scfg: invalid value for unmarshaling") - } - - return dec.unmarshalBlock(block, rv) -} - -func (dec *Decoder) unmarshalBlock(block Block, v reflect.Value) error { - v = unwrapPointers(v) - t := v.Type() - - dirsByName := make(map[string][]*Directive, len(block)) - for _, dir := range block { - dirsByName[dir.Name] = append(dirsByName[dir.Name], dir) - } - - switch v.Kind() { - case reflect.Map: - if t.Key().Kind() != reflect.String { - return fmt.Errorf("scfg: map key type must be string") - } - if v.IsNil() { - v.Set(reflect.MakeMap(t)) - } else if v.Len() > 0 { - clearMap(v) - } - - for name, dirs := range dirsByName { - mv := reflect.New(t.Elem()).Elem() - if err := dec.unmarshalDirectiveList(dirs, mv); err != nil { - return err - } - v.SetMapIndex(reflect.ValueOf(name), mv) - } - - case reflect.Struct: - si, err := getStructInfo(t) - if err != nil { - return err - } - - seen := make(map[int]bool) - - for name, dirs := range dirsByName { - fieldIndex, ok := si.children[name] - if !ok { - dec.unknownDirectives = append(dec.unknownDirectives, dirs...) - continue - } - fv := v.Field(fieldIndex) - if err := dec.unmarshalDirectiveList(dirs, fv); err != nil { - return err - } - seen[fieldIndex] = true - } - - for name, fieldIndex := range si.children { - if fieldIndex == si.param { - continue - } - if _, ok := seen[fieldIndex]; !ok { - return fmt.Errorf("scfg: missing required directive %q", name) - } - } - - default: - return fmt.Errorf("scfg: unsupported type for unmarshaling blocks: %v", t) - } - - return nil -} - -func (dec *Decoder) unmarshalDirectiveList(dirs []*Directive, v reflect.Value) error { - v = unwrapPointers(v) - t := v.Type() - - if v.Kind() != reflect.Slice || !isDirectiveType(t.Elem()) { - if len(dirs) > 1 { - return newUnmarshalDirectiveError(dirs[1], "directive must not be specified more than once") - } - return dec.unmarshalDirective(dirs[0], v) - } - - sv := reflect.MakeSlice(t, len(dirs), len(dirs)) - for i, dir := range dirs { - if err := dec.unmarshalDirective(dir, sv.Index(i)); err != nil { - return err - } - } - v.Set(sv) - return nil -} - -// isDirectiveType checks whether a type can only be unmarshaled as a -// directive, not as a parameter. Accepting too many types here would result in -// ambiguities, see: -// https://lists.sr.ht/~emersion/public-inbox/%3C20230629132458.152205-1-contact%40emersion.fr%3E#%3Ch4Y2peS_YBqY3ar4XlmPDPiNBFpYGns3EBYUx3_6zWEhV2o8_-fBQveRujGADWYhVVCucHBEryFGoPtpC3d3mQ-x10pWnFogfprbQTSvtxc=@emersion.fr%3E -func isDirectiveType(t reflect.Type) bool { - for t.Kind() == reflect.Ptr { - t = t.Elem() - } - - textUnmarshalerType := reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() - if reflect.PtrTo(t).Implements(textUnmarshalerType) { - return false - } - - switch t.Kind() { - case reflect.Struct, reflect.Map: - return true - default: - return false - } -} - -func (dec *Decoder) unmarshalDirective(dir *Directive, v reflect.Value) error { - v = unwrapPointers(v) - t := v.Type() - - if v.CanAddr() { - if _, ok := v.Addr().Interface().(encoding.TextUnmarshaler); ok { - if len(dir.Children) != 0 { - return newUnmarshalDirectiveError(dir, "directive requires zero children") - } - return unmarshalParamList(dir, v) - } - } - - switch v.Kind() { - case reflect.Map: - if len(dir.Params) > 0 { - return newUnmarshalDirectiveError(dir, "directive requires zero parameters") - } - if err := dec.unmarshalBlock(dir.Children, v); err != nil { - return err - } - case reflect.Struct: - si, err := getStructInfo(t) - if err != nil { - return err - } - - if si.param >= 0 { - if err := unmarshalParamList(dir, v.Field(si.param)); err != nil { - return err - } - } else { - if len(dir.Params) > 0 { - return newUnmarshalDirectiveError(dir, "directive requires zero parameters") - } - } - - if err := dec.unmarshalBlock(dir.Children, v); err != nil { - return err - } - default: - if len(dir.Children) != 0 { - return newUnmarshalDirectiveError(dir, "directive requires zero children") - } - if err := unmarshalParamList(dir, v); err != nil { - return err - } - } - return nil -} - -func unmarshalParamList(dir *Directive, v reflect.Value) error { - switch v.Kind() { - case reflect.Slice: - t := v.Type() - sv := reflect.MakeSlice(t, len(dir.Params), len(dir.Params)) - for i, param := range dir.Params { - if err := unmarshalParam(param, sv.Index(i)); err != nil { - return newUnmarshalParamError(dir, i, err) - } - } - v.Set(sv) - case reflect.Array: - if len(dir.Params) != v.Len() { - return newUnmarshalDirectiveError(dir, fmt.Sprintf("directive requires exactly %v parameters", v.Len())) - } - for i, param := range dir.Params { - if err := unmarshalParam(param, v.Index(i)); err != nil { - return newUnmarshalParamError(dir, i, err) - } - } - default: - if len(dir.Params) != 1 { - return newUnmarshalDirectiveError(dir, "directive requires exactly one parameter") - } - if err := unmarshalParam(dir.Params[0], v); err != nil { - return newUnmarshalParamError(dir, 0, err) - } - } - - return nil -} - -func unmarshalParam(param string, v reflect.Value) error { - v = unwrapPointers(v) - t := v.Type() - - // TODO: improve our logic following: - // https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/encoding/json/decode.go;drc=b9b8cecbfc72168ca03ad586cc2ed52b0e8db409;l=421 - if v.CanAddr() { - if v, ok := v.Addr().Interface().(encoding.TextUnmarshaler); ok { - return v.UnmarshalText([]byte(param)) - } - } - - switch v.Kind() { - case reflect.String: - v.Set(reflect.ValueOf(param)) - case reflect.Bool: - switch param { - case "true": - v.Set(reflect.ValueOf(true)) - case "false": - v.Set(reflect.ValueOf(false)) - default: - return fmt.Errorf("invalid bool parameter %q", param) - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(param, 10, t.Bits()) - if err != nil { - return fmt.Errorf("invalid %v parameter: %v", t, err) - } - v.Set(reflect.ValueOf(i).Convert(t)) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u, err := strconv.ParseUint(param, 10, t.Bits()) - if err != nil { - return fmt.Errorf("invalid %v parameter: %v", t, err) - } - v.Set(reflect.ValueOf(u).Convert(t)) - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(param, t.Bits()) - if err != nil { - return fmt.Errorf("invalid %v parameter: %v", t, err) - } - v.Set(reflect.ValueOf(f).Convert(t)) - default: - return fmt.Errorf("unsupported type for unmarshaling parameter: %v", t) - } - - return nil -} - -func unwrapPointers(v reflect.Value) reflect.Value { - for v.Kind() == reflect.Ptr { - if v.IsNil() { - v.Set(reflect.New(v.Type().Elem())) - } - v = v.Elem() - } - return v -} - -func clearMap(v reflect.Value) { - for _, k := range v.MapKeys() { - v.SetMapIndex(k, reflect.Value{}) - } -} - -type unmarshalDirectiveError struct { - lineno int - name string - msg string -} - -func newUnmarshalDirectiveError(dir *Directive, msg string) *unmarshalDirectiveError { - return &unmarshalDirectiveError{ - name: dir.Name, - lineno: dir.lineno, - msg: msg, - } -} - -func (err *unmarshalDirectiveError) Error() string { - return fmt.Sprintf("line %v, directive %q: %v", err.lineno, err.name, err.msg) -} - -type unmarshalParamError struct { - lineno int - directive string - paramIndex int - err error -} - -func newUnmarshalParamError(dir *Directive, paramIndex int, err error) *unmarshalParamError { - return &unmarshalParamError{ - directive: dir.Name, - lineno: dir.lineno, - paramIndex: paramIndex, - err: err, - } -} - -func (err *unmarshalParamError) Error() string { - return fmt.Sprintf("line %v, directive %q, parameter %v: %v", err.lineno, err.directive, err.paramIndex+1, err.err) -} diff --git a/internal/scfg/writer.go b/internal/scfg/writer.go deleted file mode 100644 index 02a07fe..0000000 --- a/internal/scfg/writer.go +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr> - -package scfg - -import ( - "errors" - "io" - "strings" -) - -var errDirEmptyName = errors.New("scfg: directive with empty name") - -// Write writes a parsed configuration to the provided io.Writer. -func Write(w io.Writer, blk Block) error { - enc := newEncoder(w) - err := enc.encodeBlock(blk) - return err -} - -// encoder write SCFG directives to an output stream. -type encoder struct { - w io.Writer - lvl int - err error -} - -// newEncoder returns a new encoder that writes to w. -func newEncoder(w io.Writer) *encoder { - return &encoder{w: w} -} - -func (enc *encoder) push() { - enc.lvl++ -} - -func (enc *encoder) pop() { - enc.lvl-- -} - -func (enc *encoder) writeIndent() { - for i := 0; i < enc.lvl; i++ { - enc.write([]byte("\t")) - } -} - -func (enc *encoder) write(p []byte) { - if enc.err != nil { - return - } - _, enc.err = enc.w.Write(p) -} - -func (enc *encoder) encodeBlock(blk Block) error { - for _, dir := range blk { - if err := enc.encodeDir(*dir); err != nil { - return err - } - } - return enc.err -} - -func (enc *encoder) encodeDir(dir Directive) error { - if enc.err != nil { - return enc.err - } - - if dir.Name == "" { - enc.err = errDirEmptyName - return enc.err - } - - enc.writeIndent() - enc.write([]byte(maybeQuote(dir.Name))) - for _, p := range dir.Params { - enc.write([]byte(" ")) - enc.write([]byte(maybeQuote(p))) - } - - if len(dir.Children) > 0 { - enc.write([]byte(" {\n")) - enc.push() - if err := enc.encodeBlock(dir.Children); err != nil { - return err - } - enc.pop() - - enc.writeIndent() - enc.write([]byte("}")) - } - enc.write([]byte("\n")) - - return enc.err -} - -const specialChars = "\"\\\r\n'{} \t" - -func maybeQuote(s string) string { - if s == "" || strings.ContainsAny(s, specialChars) { - var sb strings.Builder - sb.WriteByte('"') - for _, ch := range s { - if strings.ContainsRune(`"\`, ch) { - sb.WriteByte('\\') - } - sb.WriteRune(ch) - } - sb.WriteByte('"') - return sb.String() - } - return s -} |