main
1package gcfg
2
3import (
4 "fmt"
5 "io"
6 "os"
7 "strings"
8
9 "gopkg.in/warnings.v0"
10
11 "github.com/go-git/gcfg/scanner"
12 "github.com/go-git/gcfg/token"
13)
14
15var unescape = map[rune]rune{'\\': '\\', '"': '"', 'n': '\n', 't': '\t', 'b': '\b', '\n': '\n'}
16
17// no error: invalid literals should be caught by scanner
18func unquote(s string) string {
19 u, q, esc := make([]rune, 0, len(s)), false, false
20 for _, c := range s {
21 if esc {
22 uc, ok := unescape[c]
23 switch {
24 case ok:
25 u = append(u, uc)
26 fallthrough
27 case !q && c == '\n':
28 esc = false
29 continue
30 }
31 panic("invalid escape sequence")
32 }
33 switch c {
34 case '"':
35 q = !q
36 case '\\':
37 esc = true
38 default:
39 u = append(u, c)
40 }
41 }
42 if q {
43 panic("missing end quote")
44 }
45 if esc {
46 panic("invalid escape sequence")
47 }
48 return string(u)
49}
50
51func read(c *warnings.Collector, callback func(string, string, string, string, bool) error,
52 fset *token.FileSet, file *token.File, src []byte) error {
53 //
54 var s scanner.Scanner
55 var errs scanner.ErrorList
56 s.Init(file, src, func(p token.Position, m string) { errs.Add(p, m) }, 0)
57 sect, sectsub := "", ""
58 pos, tok, lit := s.Scan()
59 errfn := func(msg string) error {
60 return fmt.Errorf("%s: %s", fset.Position(pos), msg)
61 }
62 for {
63 if errs.Len() > 0 {
64 if err := c.Collect(errs.Err()); err != nil {
65 return err
66 }
67 }
68 switch tok {
69 case token.EOF:
70 return nil
71 case token.EOL, token.COMMENT:
72 pos, tok, lit = s.Scan()
73 case token.LBRACK:
74 pos, tok, lit = s.Scan()
75 if errs.Len() > 0 {
76 if err := c.Collect(errs.Err()); err != nil {
77 return err
78 }
79 }
80 if tok != token.IDENT {
81 if err := c.Collect(errfn("expected section name")); err != nil {
82 return err
83 }
84 }
85 sect, sectsub = lit, ""
86 pos, tok, lit = s.Scan()
87 if errs.Len() > 0 {
88 if err := c.Collect(errs.Err()); err != nil {
89 return err
90 }
91 }
92 if tok == token.STRING {
93 sectsub = unquote(lit)
94 if sectsub == "" {
95 if err := c.Collect(errfn("empty subsection name")); err != nil {
96 return err
97 }
98 }
99 pos, tok, lit = s.Scan()
100 if errs.Len() > 0 {
101 if err := c.Collect(errs.Err()); err != nil {
102 return err
103 }
104 }
105 }
106 if tok != token.RBRACK {
107 if sectsub == "" {
108 if err := c.Collect(errfn("expected subsection name or right bracket")); err != nil {
109 return err
110 }
111 }
112 if err := c.Collect(errfn("expected right bracket")); err != nil {
113 return err
114 }
115 }
116 pos, tok, lit = s.Scan()
117 if tok != token.EOL && tok != token.EOF && tok != token.COMMENT {
118 if err := c.Collect(errfn("expected EOL, EOF, or comment")); err != nil {
119 return err
120 }
121 }
122 // If a section/subsection header was found, ensure a
123 // container object is created, even if there are no
124 // variables further down.
125 err := c.Collect(callback(sect, sectsub, "", "", true))
126 if err != nil {
127 return err
128 }
129 case token.IDENT:
130 if sect == "" {
131 if err := c.Collect(errfn("expected section header")); err != nil {
132 return err
133 }
134 }
135 n := lit
136 pos, tok, lit = s.Scan()
137 if errs.Len() > 0 {
138 return errs.Err()
139 }
140 blank, v := tok == token.EOF || tok == token.EOL || tok == token.COMMENT, ""
141 if !blank {
142 if tok != token.ASSIGN {
143 if err := c.Collect(errfn("expected '='")); err != nil {
144 return err
145 }
146 }
147 pos, tok, lit = s.Scan()
148 if errs.Len() > 0 {
149 if err := c.Collect(errs.Err()); err != nil {
150 return err
151 }
152 }
153 if tok != token.STRING {
154 if err := c.Collect(errfn("expected value")); err != nil {
155 return err
156 }
157 }
158 v = unquote(lit)
159 pos, tok, lit = s.Scan()
160 if errs.Len() > 0 {
161 if err := c.Collect(errs.Err()); err != nil {
162 return err
163 }
164 }
165 if tok != token.EOL && tok != token.EOF && tok != token.COMMENT {
166 if err := c.Collect(errfn("expected EOL, EOF, or comment")); err != nil {
167 return err
168 }
169 }
170 }
171 err := c.Collect(callback(sect, sectsub, n, v, blank))
172 if err != nil {
173 return err
174 }
175 default:
176 if sect == "" {
177 if err := c.Collect(errfn("expected section header")); err != nil {
178 return err
179 }
180 }
181 if err := c.Collect(errfn("expected section header or variable declaration")); err != nil {
182 return err
183 }
184 }
185 }
186 panic("never reached")
187}
188
189func readInto(config interface{}, fset *token.FileSet, file *token.File,
190 src []byte) error {
191 //
192 c := warnings.NewCollector(isFatal)
193 firstPassCallback := func(s string, ss string, k string, v string, bv bool) error {
194 return set(c, config, s, ss, k, v, bv, false)
195 }
196 err := read(c, firstPassCallback, fset, file, src)
197 if err != nil {
198 return err
199 }
200 secondPassCallback := func(s string, ss string, k string, v string, bv bool) error {
201 return set(c, config, s, ss, k, v, bv, true)
202 }
203 err = read(c, secondPassCallback, fset, file, src)
204 if err != nil {
205 return err
206 }
207 return c.Done()
208}
209
210// ReadWithCallback reads gcfg formatted data from reader and calls
211// callback with each section and option found.
212//
213// Callback is called with section, subsection, option key, option value
214// and blank value flag as arguments.
215//
216// When a section is found, callback is called with nil subsection, option key
217// and option value.
218//
219// When a subsection is found, callback is called with nil option key and
220// option value.
221//
222// If blank value flag is true, it means that the value was not set for an option
223// (as opposed to set to empty string).
224//
225// If callback returns an error, ReadWithCallback terminates with an error too.
226func ReadWithCallback(reader io.Reader, callback func(string, string, string, string, bool) error) error {
227 src, err := io.ReadAll(reader)
228 if err != nil {
229 return err
230 }
231
232 fset := token.NewFileSet()
233 file := fset.AddFile("", fset.Base(), len(src))
234 c := warnings.NewCollector(isFatal)
235
236 return read(c, callback, fset, file, src)
237}
238
239// ReadInto reads gcfg formatted data from reader and sets the values into the
240// corresponding fields in config.
241func ReadInto(config interface{}, reader io.Reader) error {
242 src, err := io.ReadAll(reader)
243 if err != nil {
244 return err
245 }
246 fset := token.NewFileSet()
247 file := fset.AddFile("", fset.Base(), len(src))
248 return readInto(config, fset, file, src)
249}
250
251// ReadStringInto reads gcfg formatted data from str and sets the values into
252// the corresponding fields in config.
253func ReadStringInto(config interface{}, str string) error {
254 r := strings.NewReader(str)
255 return ReadInto(config, r)
256}
257
258// ReadFileInto reads gcfg formatted data from the file filename and sets the
259// values into the corresponding fields in config.
260func ReadFileInto(config interface{}, filename string) error {
261 f, err := os.Open(filename)
262 if err != nil {
263 return err
264 }
265 defer f.Close()
266 src, err := io.ReadAll(f)
267 if err != nil {
268 return err
269 }
270 fset := token.NewFileSet()
271 file := fset.AddFile(filename, fset.Base(), len(src))
272 return readInto(config, fset, file, src)
273}