aboutsummaryrefslogtreecommitdiff
path: root/forged/internal/scfg
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-04-06 09:26:46 +0800
committerRunxi Yu <me@runxiyu.org>2025-04-06 09:27:53 +0800
commitda1d8f4e7c332c7109427915e6459b10209cedce (patch)
tree280b921be3b51f93d82d916b4eaa89387b7102cc /forged/internal/scfg
parentgit2c, git2d: Rename cmd1 and cmd2 descriptively (diff)
downloadforge-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 'forged/internal/scfg')
-rw-r--r--forged/internal/scfg/.golangci.yaml25
-rw-r--r--forged/internal/scfg/reader.go157
-rw-r--r--forged/internal/scfg/scfg.go59
-rw-r--r--forged/internal/scfg/struct.go82
-rw-r--r--forged/internal/scfg/unmarshal.go375
-rw-r--r--forged/internal/scfg/writer.go112
6 files changed, 810 insertions, 0 deletions
diff --git a/forged/internal/scfg/.golangci.yaml b/forged/internal/scfg/.golangci.yaml
new file mode 100644
index 0000000..7db9793
--- /dev/null
+++ b/forged/internal/scfg/.golangci.yaml
@@ -0,0 +1,25 @@
+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/forged/internal/scfg/reader.go b/forged/internal/scfg/reader.go
new file mode 100644
index 0000000..6a2bedc
--- /dev/null
+++ b/forged/internal/scfg/reader.go
@@ -0,0 +1,157 @@
+// 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/forged/internal/scfg/scfg.go b/forged/internal/scfg/scfg.go
new file mode 100644
index 0000000..4533e63
--- /dev/null
+++ b/forged/internal/scfg/scfg.go
@@ -0,0 +1,59 @@
+// 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/forged/internal/scfg/struct.go b/forged/internal/scfg/struct.go
new file mode 100644
index 0000000..98ec943
--- /dev/null
+++ b/forged/internal/scfg/struct.go
@@ -0,0 +1,82 @@
+// 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/forged/internal/scfg/unmarshal.go b/forged/internal/scfg/unmarshal.go
new file mode 100644
index 0000000..e9e1a52
--- /dev/null
+++ b/forged/internal/scfg/unmarshal.go
@@ -0,0 +1,375 @@
+// 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/forged/internal/scfg/writer.go b/forged/internal/scfg/writer.go
new file mode 100644
index 0000000..02a07fe
--- /dev/null
+++ b/forged/internal/scfg/writer.go
@@ -0,0 +1,112 @@
+// 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
+}