diff options
| author | Shulhan <ms@kilabit.info> | 2018-05-10 20:49:02 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2018-05-10 20:49:02 +0700 |
| commit | 488e6c32348fb84636dd69d46c151fffcfe7ee37 (patch) | |
| tree | 928837e7a53baf3f85a4f5060cafacedc6b9062e /lib | |
| parent | e6ef580dc8134019cffa94810907b81cb5caa19a (diff) | |
| download | pakakeh.go-488e6c32348fb84636dd69d46c151fffcfe7ee37.tar.xz | |
Refactor parser using bytes.Reader
Previous benchmark result (22dcd07 Move buffer to reader),
BenchmarkParse-2 500 19534400 ns/op 4656335 B/op 81163 allocs/op
New benchmark result,
BenchmarkParse-2 20000 71120 ns/op 35368 B/op 549 allocs/op
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/ini/ini.go | 52 | ||||
| -rw-r--r-- | lib/ini/ini_test.go | 62 | ||||
| -rw-r--r-- | lib/ini/parsedline.go | 22 | ||||
| -rw-r--r-- | lib/ini/reader.go | 860 | ||||
| -rw-r--r-- | lib/ini/reader_bench_test.go | 8 | ||||
| -rw-r--r-- | lib/ini/reader_test.go | 344 | ||||
| -rw-r--r-- | lib/ini/section.go | 85 | ||||
| -rw-r--r-- | lib/ini/testdata/input.ini | 14 | ||||
| -rw-r--r-- | lib/ini/testdata/var_without_section.ini | 2 | ||||
| -rw-r--r-- | lib/ini/variable.go | 62 | ||||
| -rw-r--r-- | lib/test/test.go | 7 |
11 files changed, 836 insertions, 682 deletions
diff --git a/lib/ini/ini.go b/lib/ini/ini.go index 5c068ff3..3fbbac36 100644 --- a/lib/ini/ini.go +++ b/lib/ini/ini.go @@ -19,12 +19,14 @@ // (S.2.0) A section begins with the name of the section in square brackets. // (S.2.1) A section continues until the next section begins. // (S.2.2) Section name are case-insensitive. -// (S.2.3) Section name only allow alphanumeric characters, `-` and `.`. -// (S.2.4) Section can be further divided into subsections. -// (S.2.5) Section headers cannot span multiple lines. -// (S.2.6) You can have `[section]` if you have `[section "subsection"]`, but +// (S.2.3) Variable name must start with an alphabetic character, no +// whitespace before name or after '['. +// (S.2.4) Section name only allow alphanumeric characters, `-` and `.`. +// (S.2.5) Section can be further divided into subsections. +// (S.2.6) Section headers cannot span multiple lines. +// (S.2.7) You can have `[section]` if you have `[section "subsection"]`, but // you don’t need to. -// (S.2.7) All the other lines (and the remainder of the line after the +// (S.2.8) All the other lines (and the remainder of the line after the // section header) are recognized as setting variables, in the form // `name = value`. // @@ -138,34 +140,10 @@ func (in *Ini) Save(filename string) (err error) { // func (in *Ini) Write(w io.Writer) (err error) { for x := 0; x < len(in.secs); x++ { - switch in.secs[x].m { - case sectionModeNormal: - _, err = fmt.Fprintf(w, "[%s]\n", in.secs[x].name) - - case sectionModeSub: - _, err = fmt.Fprintf(w, "[%s \"%s\"]\n", in.secs[x].name, - in.secs[x].subName) - - } - if err != nil { - return - } + fmt.Fprint(w, in.secs[x]) for _, v := range in.secs[x].vars { - switch v.m { - case varModeNewline: - _, err = fmt.Fprintln(w) - - case varModeComment: - _, err = fmt.Fprintf(w, "%s\n", v.c) - - case varModeNormal: - _, err = fmt.Fprintf(w, "\t%s = %s%s\n", v.k, - v.v, v.c) - } - if err != nil { - return - } + fmt.Fprint(w, v) } } @@ -180,6 +158,16 @@ func (in *Ini) Reset() { in.secs = nil } +func (in *Ini) addSection(sec *section) { + if sec == nil { + return + } + if len(sec.secName) > 0 { + sec.secLower = bytes.ToLower(sec.secName) + } + in.secs = append(in.secs, sec) +} + // // Get return the last key on section and/or subsection (if not empty). // @@ -202,7 +190,7 @@ func (in *Ini) Get(section, subsection, key string) (val []byte, ok bool) { bkey := []byte(key) for ; x >= 0; x-- { - if !bytes.Equal(in.secs[x].name, bsec) { + if !bytes.Equal(in.secs[x].secLower, bsec) { continue } diff --git a/lib/ini/ini_test.go b/lib/ini/ini_test.go index 5850cdf9..5ec5f0c4 100644 --- a/lib/ini/ini_test.go +++ b/lib/ini/ini_test.go @@ -15,7 +15,7 @@ var ( inputIni *Ini ) -func TestOpenAndSave(t *testing.T) { +func TestOpen(t *testing.T) { cases := []struct { desc string inFile string @@ -36,15 +36,48 @@ func TestOpenAndSave(t *testing.T) { for _, c := range cases { t.Logf("%+v", c) + in, err := Open(c.inFile) + if err != nil { + test.Assert(t, "error", c.expErr, err.Error(), true) + continue + } + + err = in.Save(c.inFile + ".save") + if err != nil { + t.Fatal(err) + } + } +} + +func TestSave(t *testing.T) { + cases := []struct { + desc string + inFile string + expErr string + }{{ + desc: "With no file", + expErr: "open : no such file or directory", + }, { + desc: "With variable without section", + inFile: testdataVarWithoutSection, + expErr: "variable without section, line 7 at testdata/var_without_section.ini", + }, { + desc: "With valid file", + inFile: "testdata/input.ini", + }} + + for _, c := range cases { + t.Logf("%+v", c) + ini, err := Open(c.inFile) if err != nil { - test.Assert(t, c.expErr, err.Error(), true) + test.Assert(t, "error", c.expErr, err.Error(), true) continue } err = ini.Save(c.inFile + ".save") if err != nil { - test.Assert(t, c.expErr, err.Error(), true) + test.Assert(t, "error", c.expErr, err.Error(), true) } } } @@ -107,11 +140,11 @@ func TestGet(t *testing.T) { got, ok = inputIni.Get(c.sec, c.sub, c.key) if !ok { - test.Assert(t, c.expOk, ok, true) + test.Assert(t, "ok", c.expOk, ok, true) continue } - test.Assert(t, c.expVal, got, true) + test.Assert(t, "value", c.expVal, got, true) } } @@ -131,7 +164,7 @@ func TestGetInputIni(t *testing.T) { "autocrlf", }, expVals: []string{ - "false", + "true", "default-proxy", "less -R", "nvim", @@ -270,9 +303,9 @@ func TestGetInputIni(t *testing.T) { "codereview pending", "codereview submit", "codereview sync", - `--no-pager log --graph --date=format:'%Y-%m-%d' --pretty=format:'%C(auto,dim)%ad %<(7,trunc) %an %Creset%m %h %s %Cgreen%d%Creset' --exclude=*/production --exclude=*/dev-* --all -n 20`, - `!git stash -u && git fetch origin && git rebase origin/master && git stash pop && git --no-pager log --graph --decorate --pretty=oneline --abbrev-commit origin/master~1..HEAD`, - `!git stash -u && git fetch origin && git rebase origin/production && git stash pop && git --no-pager log --graph --decorate --pretty=oneline --abbrev-commit origin/production~1..HEAD`, + `!git --no-pager log --graph --date=format:'%Y-%m-%d' --pretty=format:'%C(auto,dim)%ad %<(7,trunc) %an %Creset%m %h %s %Cgreen%d%Creset' --exclude=*/production --exclude=*/dev-* --all -n 20`, + `!git stash -u && git fetch origin && git rebase origin/master && git stash pop && git --no-pager log --graph --decorate --pretty=oneline --abbrev-commit origin/master~1..HEAD`, + `!git stash -u && git fetch origin && git rebase origin/production && git stash pop && git --no-pager log --graph --decorate --pretty=oneline --abbrev-commit origin/production~1..HEAD`, }, }, { sec: "url", @@ -291,6 +324,8 @@ func TestGetInputIni(t *testing.T) { ) for _, c := range cases { + t.Log(c) + if debug >= debugL2 { t.Logf("Section header: [%s %s]", c.sec, c.sub) t.Logf(">>> keys: %s", c.keys) @@ -298,17 +333,16 @@ func TestGetInputIni(t *testing.T) { } for x, k := range c.keys { - if debug >= debugL2 { - t.Logf("Get: %s", k) - } + t.Log(" Get:", k) + got, ok = inputIni.Get(c.sec, c.sub, k) if !ok { t.Logf("Get: %s > %s > %s", c.sec, c.sub, k) - test.Assert(t, true, ok, true) + test.Assert(t, "ok", true, ok, true) t.FailNow() } - test.Assert(t, c.expVals[x], string(got), true) + test.Assert(t, "value", []byte(c.expVals[x]), got, true) } } } diff --git a/lib/ini/parsedline.go b/lib/ini/parsedline.go deleted file mode 100644 index bba7a839..00000000 --- a/lib/ini/parsedline.go +++ /dev/null @@ -1,22 +0,0 @@ -package ini - -type lineMode uint - -const ( - lineModeNewline lineMode = 1 << iota - lineModeComment - lineModeSection - lineModeSubsection - lineModeVar - lineModeVarMulti -) - -// -// parsedLine define the single line, where `m` contain mode of line, `n` -// contain line number, and `v` contain the content of line itself. -// -type parsedLine struct { - m lineMode - n int - v []byte -} diff --git a/lib/ini/reader.go b/lib/ini/reader.go index 66c55832..7b112af3 100644 --- a/lib/ini/reader.go +++ b/lib/ini/reader.go @@ -2,58 +2,86 @@ package ini import ( "bytes" + "errors" "fmt" + "io" "io/ioutil" - "log" "unicode" ) const ( tokBackslash = '\\' + tokDot = '.' + tokDoubleQuote = '"' + tokEqual = '=' tokHash = '#' + tokHyphen = '-' + tokNewLine = '\n' + tokPercent = '%' tokSecEnd = ']' tokSecStart = '[' tokSemiColon = ';' - tokDoubleQuote = '"' + tokSpace = ' ' + tokTab = '\t' ) var ( - errBadConfig = "bad config line %d at %s" + errBadConfig = errors.New("bad config line %d at %s") errVarNoSection = "variable without section, line %d at %s" - errVarNameInvalid = "invalid variable name, line %d at %s" - errValueInvalid = "invalid value, line %d at %s" + errVarNameInvalid = errors.New("invalid variable name, line %d at %s") + errValueInvalid = errors.New("invalid value, line %d at %s") - sepSubsection = []byte{' '} - sepNewline = []byte{'\n'} - sepVar = []byte{'='} + fmtStr = []byte{'%', 's'} + escPercent = []byte{'%', '%'} ) // // Reader define the INI file reader. // type Reader struct { - filename string - lines []parsedLine - sec *section - buf bytes.Buffer - bufSpaces bytes.Buffer - bufCom bytes.Buffer + br *bytes.Reader + b byte + r rune + lineNum int + filename string + _var *variable + sec *section + buf bytes.Buffer + bufComment bytes.Buffer + bufFormat bytes.Buffer + bufSpaces bytes.Buffer } // // NewReader create, initialize, and return new reader. // func NewReader() (reader *Reader) { - reader = &Reader{} - reader.reset() + reader = &Reader{ + br: bytes.NewReader(nil), + } + reader.reset(nil) return } -func (reader *Reader) reset() { +// +// reset all reader attributes, excluding filename. +// +func (reader *Reader) reset(src []byte) { + reader.br.Reset(src) + reader.b = 0 + reader.r = 0 + reader.lineNum = 0 + reader._var = &variable{ + mode: varModeEmpty, + } reader.sec = §ion{ - m: sectionModeNone, + mode: varModeEmpty, } + reader.buf.Reset() + reader.bufComment.Reset() + reader.bufFormat.Reset() + reader.bufSpaces.Reset() } // @@ -77,552 +105,528 @@ func (reader *Reader) ParseFile(in *Ini, filename string) (err error) { // // nolint: gocyclo func (reader *Reader) Parse(in *Ini, src []byte) (err error) { - var ok bool - in.Reset() - reader.reset() + reader.reset(src) - err = reader.normalized(src) - if err != nil { - return - } - - for x := 0; x < len(reader.lines); x++ { - switch reader.lines[x].m { - case lineModeNewline: - reader.sec.pushVar(varModeNewline, nil, nil, nil) - - case lineModeComment: - reader.sec.pushVar(varModeComment, nil, nil, - reader.lines[x].v) - - case lineModeVar: - // S.4.0 variable must belong to section - if reader.sec.m == sectionModeNone { - err = fmt.Errorf(errVarNoSection, x+1, + for { + err = reader.parse() + if err != nil { + if err != io.EOF { + return fmt.Errorf(err.Error(), reader.lineNum, reader.filename) - return } + break + } - err = reader.parseVar(reader.lines[x].v, x) + if debug >= debugL1 { + fmt.Print(reader._var) + } + + reader._var.lineNum = reader.lineNum + reader.lineNum++ - case lineModeVarMulti: - // S.4.0 variable must belong to section - if reader.sec.m == sectionModeNone { - err = fmt.Errorf(errVarNoSection, x+1, + if reader._var.mode&varModeSingle == varModeSingle || + reader._var.mode&varModeValue == varModeValue || + reader._var.mode&varModeMulti == varModeMulti { + if reader.sec.mode == varModeEmpty { + return fmt.Errorf(errVarNoSection, + reader.lineNum, reader.filename) - return } + } - x, err = reader.parseMultilineVar(x) - x-- + if reader._var.mode&varModeSection == varModeSection || + reader._var.mode&varModeSubsection == varModeSubsection { - case lineModeSection: - in.secs = append(in.secs, reader.sec) - reader.sec = §ion{ - m: sectionModeNormal, - } - ok = reader.parseSection(reader.lines[x].v, x, true) - if !ok { - err = fmt.Errorf(errBadConfig, x, - reader.filename) - } + in.addSection(reader.sec) - case lineModeSubsection: - in.secs = append(in.secs, reader.sec) - reader.sec = §ion{ - m: sectionModeSub, - } - ok = reader.parseSubsection(reader.lines[x].v, x) - if !ok { - err = fmt.Errorf(errBadConfig, x, - reader.filename) + reader.sec = (*section)(reader._var) + reader._var = &variable{ + mode: varModeEmpty, } + continue } + reader.sec.addVariable(reader._var) + + reader._var = &variable{ + mode: varModeEmpty, + } + } + + if debug >= debugL1 { + fmt.Println(reader._var) + } + + reader.sec.addVariable(reader._var) + in.addSection(reader.sec) + + reader._var = nil + reader.sec = nil + + err = nil + return +} + +func (reader *Reader) parse() (err error) { + reader.bufFormat.Reset() + + for { + reader.b, err = reader.br.ReadByte() if err != nil { + break + } + if reader.b == tokNewLine { + reader.bufFormat.WriteByte(reader.b) + reader._var.format = append(reader._var.format, reader.bufFormat.Bytes()...) + return + } + if reader.b == tokSpace || reader.b == tokTab { + reader.bufFormat.WriteByte(reader.b) + continue + } + if reader.b == tokHash || reader.b == tokSemiColon { + _ = reader.br.UnreadByte() + err = reader.parseComment() return } + if reader.b == tokSecStart { + err = reader.parseSectionHeader() + break + } + _ = reader.br.UnreadByte() + return reader.parseVariable() } - in.secs = append(in.secs, reader.sec) - reader.sec = §ion{ - m: sectionModeNormal, + return +} + +func (reader *Reader) parseComment() (err error) { + reader.bufComment.Reset() + + reader._var.mode |= varModeComment + + reader.bufFormat.Write(fmtStr) + + for { + reader.b, err = reader.br.ReadByte() + if err != nil { + break + } + if reader.b == tokNewLine { + reader.bufFormat.WriteByte(reader.b) + break + } + _ = reader.bufComment.WriteByte(reader.b) } + reader._var.format = append(reader._var.format, reader.bufFormat.Bytes()...) + reader._var.others = append(reader._var.others, reader.bufComment.Bytes()...) + return } -// -// normalized will split source by lines. -// // nolint: gocyclo -func (reader *Reader) normalized(src []byte) (err error) { - // (0) - multi := false - lines := bytes.Split(src, sepNewline) +func (reader *Reader) parseSectionHeader() (err error) { + reader.buf.Reset() - for x := 0; x < len(lines); x++ { - orgLine := lines[x] - line := bytes.TrimSpace(orgLine) + reader._var.mode = varModeSection + reader.bufFormat.WriteByte(tokSecStart) - if len(line) == 0 { - multi = false - reader.addLine(lineModeNewline, x, nil) - continue - } + reader.r, _, err = reader.br.ReadRune() + if err != nil { + return errBadConfig + } - b0 := line[0] - blast := line[len(line)-1] + if !unicode.IsLetter(reader.r) { + return errBadConfig + } - if multi { - reader.addLine(lineModeVarMulti, x, line) + reader.bufFormat.Write(fmtStr) + reader.buf.WriteRune(reader.r) - if blast != tokBackslash { - multi = false - } - continue + for { + reader.r, _, err = reader.br.ReadRune() + if err != nil { + return errBadConfig } - - if b0 == tokHash || b0 == tokSemiColon { - reader.addLine(lineModeComment, x, orgLine) - continue + if reader.r == tokSpace || reader.r == tokTab { + break } + if reader.r == tokSecEnd { + reader.bufFormat.WriteRune(reader.r) - if b0 == tokSecStart { - if blast != tokSecEnd { - err = fmt.Errorf(errBadConfig, x+1, - reader.filename) - return - } + reader._var.secName = append(reader._var.secName, reader.buf.Bytes()...) - reader.addLine(lineModeSection, x, line) - continue + return reader.parsePossibleComment() } - - if blast != tokBackslash { - reader.addLine(lineModeVar, x, line) + if unicode.IsLetter(reader.r) || unicode.IsDigit(reader.r) || reader.r == tokHyphen || reader.r == tokDot { + reader.buf.WriteRune(reader.r) continue } - reader.addLine(lineModeVarMulti, x, line) - multi = true + return errBadConfig } - if debug >= debugL2 { - for _, line := range reader.lines { - fmt.Printf("%1d %4d %s\n", line.m, line.n, line.v) - } - } + reader.bufFormat.WriteRune(reader.r) + reader._var.secName = append(reader._var.secName, reader.buf.Bytes()...) - return + return reader.parseSubsection() } // -// addLine add line `in` to list of lines in reader. -// -// (1) If line mode is section, -// (1.1) If it's contain space, change their mode to subsection. +// (0) Skip white-spaces // -func (reader *Reader) addLine(mode lineMode, num int, in []byte) { - // (1) - if mode == lineModeSection { - itHaveSub := bytes.Index(in, sepSubsection) - if itHaveSub > 0 { - mode = lineModeSubsection - } - } +// nolint: gocyclo +func (reader *Reader) parseSubsection() (err error) { + reader.buf.Reset() - line := parsedLine{ - m: mode, - n: num, - v: in, - } + reader._var.mode |= varModeSubsection - reader.lines = append(reader.lines, line) -} + // (0) + for { + reader.b, err = reader.br.ReadByte() + if err != nil { + return errBadConfig + } + if reader.b == tokSpace || reader.b == tokTab { + reader.bufFormat.WriteByte(reader.b) + continue + } + if reader.b != tokDoubleQuote { + return errBadConfig + } + break + } -func (reader *Reader) parseMultilineVar(start int) (end int, err error) { - var ( - lastIdx int - blast byte - ) + reader.bufFormat.WriteByte(reader.b) // == tokDoubleQuote + reader.bufFormat.Write(fmtStr) - reader.buf.Reset() + var esc bool + var end bool - for end = start; end < len(reader.lines); end++ { - if reader.lines[end].m != lineModeVarMulti { - break + for { + reader.b, err = reader.br.ReadByte() + if err != nil { + return errBadConfig } - - lastIdx = len(reader.lines[end].v) - 1 - blast = reader.lines[end].v[lastIdx] - if blast == tokBackslash { - reader.buf.Write(reader.lines[end].v[0:lastIdx]) - } else { - reader.buf.Write(reader.lines[end].v) + if end { + if reader.b == tokSecEnd { + reader.bufFormat.WriteByte(reader.b) + break + } + return errBadConfig + } + if esc { + reader.buf.WriteByte(reader.b) + esc = false + continue } + if reader.b == tokBackslash { + esc = true + continue + } + if reader.b == tokDoubleQuote { + reader.bufFormat.WriteByte(reader.b) + end = true + continue + } + reader.buf.WriteByte(reader.b) } - err = reader.parseVar(reader.buf.Bytes(), start) + reader._var.subName = append(reader._var.subName, reader.buf.Bytes()...) - return + return reader.parsePossibleComment() } // -// parseVar will split line at line number `num` into key and value -// using `=` as separator. -// -// (S.5.4) Variable name without value is a short-hand to set the value to the -// boolean "true". +// parsePossibleComment will check only for whitespace and comment start +// character. // -func (reader *Reader) parseVar(line []byte, num int) (err error) { - var v, comment []byte - - kv := bytes.SplitN(line, sepVar, 2) - - k, ok := reader.parseVarName(kv[0]) - if !ok { - err = fmt.Errorf(errVarNameInvalid, num, reader.filename) - return - } - - // (S.5.4) - if len(kv) == 1 { - v = varValueTrue - } else { - v, comment, ok = reader.parseVarValue(kv[1]) - if !ok { - err = fmt.Errorf(errValueInvalid, num, reader.filename) +func (reader *Reader) parsePossibleComment() (err error) { + for { + reader.b, err = reader.br.ReadByte() + if err != nil { return } + if reader.b == tokNewLine { + reader.bufFormat.WriteByte(reader.b) + break + } + if reader.b == tokSpace || reader.b == tokTab { + reader.bufFormat.WriteByte(reader.b) + continue + } + if reader.b == tokHash || reader.b == tokSemiColon { + _ = reader.br.UnreadByte() + err = reader.parseComment() + return + } + return errBadConfig } - reader.sec.pushVar(varModeNormal, k, v, comment) + reader._var.format = append(reader._var.format, reader.bufFormat.Bytes()...) return } -// -// parseVarName will parse variable name from input bytes as defined in rules -// S.5. -// -func (reader *Reader) parseVarName(in []byte) (out []byte, ok bool) { - in = bytes.ToLower(bytes.TrimSpace(in)) +// nolint: gocyclo +func (reader *Reader) parseVariable() (err error) { + reader.buf.Reset() - if len(in) == 0 { - return + reader.r, _, err = reader.br.ReadRune() + if err != nil { + return errVarNameInvalid } - x := 0 - rr := bytes.Runes(in) - - if !unicode.IsLetter(rr[x]) { - return + if !unicode.IsLetter(reader.r) { + return errVarNameInvalid } - reader.buf.Reset() - reader.buf.WriteRune(rr[x]) + reader.bufFormat.Write(fmtStr) + reader.buf.WriteRune(reader.r) - for x++; x < len(rr); x++ { - if rr[x] == '-' { - reader.buf.WriteRune(rr[x]) - continue + for { + reader.r, _, err = reader.br.ReadRune() + if err != nil { + break + } + if reader.r == tokNewLine { + reader.bufFormat.WriteRune(reader.r) + break } - if unicode.IsLetter(rr[x]) || unicode.IsDigit(rr[x]) { - reader.buf.WriteRune(rr[x]) + if unicode.IsLetter(reader.r) || unicode.IsDigit(reader.r) || reader.r == tokHyphen { + reader.buf.WriteRune(reader.r) continue } + if reader.r == tokHash || reader.r == tokSemiColon { + _ = reader.br.UnreadRune() - return + reader._var.mode = varModeSingle + reader._var.key = append(reader._var.key, reader.buf.Bytes()...) + reader._var.value = varValueTrue + + err = reader.parseComment() + return + } + if unicode.IsSpace(reader.r) { + reader.bufFormat.WriteRune(reader.r) + + reader._var.mode = varModeSingle + reader._var.key = append(reader._var.key, reader.buf.Bytes()...) + + return reader.parsePossibleValue() + } + if reader.r == tokEqual { + reader.bufFormat.WriteRune(reader.r) + + reader._var.mode = varModeSingle + reader._var.key = append(reader._var.key, reader.buf.Bytes()...) + + return reader.parseVarValue() + } + return errVarNameInvalid } - out = append(out, reader.buf.Bytes()...) - ok = true + reader._var.mode = varModeSingle + reader._var.format = append(reader._var.format, reader.bufFormat.Bytes()...) + reader._var.key = append(reader._var.key, reader.buf.Bytes()...) + reader._var.value = varValueTrue return } // -// parseVarValue will parse variable value as defined in rules S.6. +// parsePossibleValue will check if the next character after space is comment +// or `=`. // -// (0) Check for double-quote on the first rune. -// -// (1) If rune is space ' ' or tab '\t', -// (1.1) If `quoted`, write to buffer -// (1.2) If not `quoted`, write to whitespaces buffer, to be used later. -// -// (2) If rune is double-quote, reset quoted state, do not append the -// quoted character. -// -// (3) If next rune is '#', -// (3.1) If we are on double-quoted, add it to buffer. -// (3.2) If we are not on double-quoted, the rest of must be comment +func (reader *Reader) parsePossibleValue() (err error) { + for { + reader.b, err = reader.br.ReadByte() + if err != nil { + break + } + if reader.b == tokNewLine { + reader.bufFormat.WriteByte(reader.b) + break + } + if reader.b == tokSpace || reader.b == tokTab { + reader.bufFormat.WriteByte(reader.b) + continue + } + if reader.b == tokHash || reader.b == tokSemiColon { + _ = reader.br.UnreadByte() + reader._var.value = varValueTrue + return reader.parseComment() + } + if reader.b == tokEqual { + reader.bufFormat.WriteByte(reader.b) + return reader.parseVarValue() + } + return errVarNameInvalid + } + + reader._var.mode = varModeSingle + reader._var.format = append(reader._var.format, reader.bufFormat.Bytes()...) + reader._var.value = varValueTrue + + return +} + // -// (4) If `esc` is true, check if next rune is valid escaped character, -// otherwise return. +// At this point we found `=` on source, and we expect the rest of source will +// be variable value. // -// (5) If next rune is '\', -// (5.1) If `quoted` is true, set `esc` to true and continue to the next rune. -// (5.3) If not `quoted`, return immediately. +// (0) Consume leading white-spaces. // // nolint: gocyclo -func (reader *Reader) parseVarValue(in []byte) (value, comment []byte, ok bool) { - in = bytes.TrimSpace(in) +func (reader *Reader) parseVarValue() (err error) { + reader.buf.Reset() + reader.bufSpaces.Reset() - // S.6.0 - if len(in) == 0 { - value = varValueTrue - ok = true - return + // (0) + for { + reader.b, err = reader.br.ReadByte() + if err != nil { + reader._var.format = append(reader._var.format, reader.bufFormat.Bytes()...) + reader._var.value = varValueTrue + return + } + if reader.b == tokSpace || reader.b == tokTab { + reader.bufFormat.WriteByte(reader.b) + continue + } + if reader.b == tokHash || reader.b == tokSemiColon { + _ = reader.br.UnreadByte() + reader._var.value = varValueTrue + return reader.parseComment() + } + if reader.b == tokNewLine { + reader.bufFormat.WriteByte(reader.b) + reader._var.format = append(reader._var.format, reader.bufFormat.Bytes()...) + reader._var.value = varValueTrue + return + } + break } + reader._var.mode = varModeValue + _ = reader.br.UnreadByte() + var ( quoted bool esc bool - x int ) - rr := bytes.Runes(in) + for { + reader.b, err = reader.br.ReadByte() + if err != nil { + break + } - // (0) - if rr[x] == tokDoubleQuote { - quoted = true - x++ - } + if esc { + if reader.b == tokNewLine { + reader._var.mode = varModeMulti - reader.buf.Reset() - reader.bufSpaces.Reset() - reader.bufCom.Reset() + reader.valueCommit(true) - for ; x < len(rr); x++ { - if rr[x] == ' ' || rr[x] == '\t' { - if quoted { - _, _ = reader.buf.WriteRune(rr[x]) - continue - } - if reader.buf.Len() > 0 { - reader.bufSpaces.WriteRune(rr[x]) - } - continue - } + reader.bufFormat.WriteByte(tokNewLine) - // (2) - if rr[x] == tokDoubleQuote { - if esc { - if reader.bufSpaces.Len() > 0 { - _, _ = reader.buf.Write(reader.bufSpaces.Bytes()) - reader.bufSpaces.Reset() - } - _, _ = reader.buf.WriteRune('"') + reader.lineNum++ esc = false continue } - if quoted { - if esc { - _, _ = reader.buf.WriteRune('"') - esc = false - continue - } - if reader.bufSpaces.Len() > 0 { - _, _ = reader.buf.Write(reader.bufSpaces.Bytes()) - reader.bufSpaces.Reset() - } - quoted = false - continue - } - quoted = true - continue - } - - // (3) - if rr[x] == tokHash || rr[x] == tokSemiColon { - if quoted { - if reader.bufSpaces.Len() > 0 { - _, _ = reader.buf.Write(reader.bufSpaces.Bytes()) - reader.bufSpaces.Reset() - } - _, _ = reader.buf.WriteRune(rr[x]) - continue - } - - if reader.bufSpaces.Len() > 0 { - _, _ = reader.bufCom.Write(reader.bufSpaces.Bytes()) - reader.bufSpaces.Reset() - } - reader.bufCom.WriteString(string(rr[x:])) - goto out - } - - // (4) - if esc { - if rr[x] == 'n' || rr[x] == 't' || rr[x] == 'b' { - _, _ = reader.buf.WriteRune(tokBackslash) - _, _ = reader.buf.WriteRune(rr[x]) + if reader.b == tokBackslash || reader.b == tokDoubleQuote { + reader.valueWriteByte(reader.b) esc = false continue } - if rr[x] == '\\' { - _, _ = reader.buf.WriteRune(rr[x]) + if reader.b == 'b' || reader.b == 'n' || reader.b == 't' { + reader.valueWriteByte(tokBackslash) + reader.bufFormat.WriteByte(reader.b) + reader.buf.WriteByte(reader.b) esc = false continue } - return + return errValueInvalid } - - // (5) - if rr[x] == tokBackslash { + if reader.b == tokSpace || reader.b == tokTab { if quoted { - if reader.bufSpaces.Len() > 0 { - _, _ = reader.buf.Write(reader.bufSpaces.Bytes()) - reader.bufSpaces.Reset() - } - esc = true + reader.valueWriteByte(reader.b) continue } + reader.bufFormat.WriteByte(reader.b) + reader.bufSpaces.WriteByte(reader.b) + continue + } + if reader.b == tokBackslash { + reader.bufFormat.WriteByte(reader.b) esc = true continue } - - if reader.bufSpaces.Len() > 0 { - _, _ = reader.buf.Write(reader.bufSpaces.Bytes()) - reader.bufSpaces.Reset() + if reader.b == tokDoubleQuote { + reader.bufFormat.WriteByte(reader.b) + if quoted { + quoted = false + } else { + quoted = true + } + continue } - _, _ = reader.buf.WriteRune(rr[x]) - } - - if quoted || esc { - return - } -out: - value = append(value, reader.buf.Bytes()...) - comment = append(comment, reader.bufCom.Bytes()...) - ok = true - - return -} - -// -// parseSection will parse section name from line. Line is assumed to be a -// valid section, which is started with '[' and end with ']'. -// -// (0) Remove '[' and ']' -// (1) Section name must start with alphabetic character. -// (2) Section name must be alphanumeric, '-', or '.'. -// -func (reader *Reader) parseSection(line []byte, num int, trim bool) (ok bool) { - // (0) - if trim { - line = bytes.TrimSpace(line[1 : len(line)-1]) - } - if len(line) == 0 { - return - } - - line = bytes.ToLower(line) - x := 0 - runes := bytes.Runes(line) - - if !unicode.IsLetter(runes[x]) { - return - } + if reader.b == tokNewLine { + reader.bufFormat.WriteByte(reader.b) + break + } + if reader.b == tokHash || reader.b == tokSemiColon { + if quoted { + reader.valueWriteByte(reader.b) + continue + } - reader.buf.Reset() + reader.valueCommit(false) - for ; x < len(runes); x++ { - if runes[x] == '-' || runes[x] == '.' { - reader.buf.WriteRune(runes[x]) - continue - } - if unicode.IsLetter(runes[x]) || unicode.IsDigit(runes[x]) { - reader.buf.WriteRune(runes[x]) - continue + _ = reader.br.UnreadByte() + err = reader.parseComment() + return } + reader.valueWriteByte(reader.b) + } - return + if quoted { + return errValueInvalid } - ok = true + reader.valueCommit(false) - reader.sec.name = nil - reader.sec.name = append(reader.sec.name, reader.buf.Bytes()...) + reader._var.format = append(reader._var.format, reader.bufFormat.Bytes()...) return } -// -// parseSubsection will parse section name and subsection name from line. Line -// is assumed to be a valid section, which is started with '[' and end with -// ']'. -// -// (0) Remove '[' and ']' -// (1) Section and subsection is separated by single space ' '. -// (2) Subsection name enclosed by double-quote. -// (3) Subsection can contains only the following escape character: '\' and -// '"', other than that will be appended without '\' character. -// -// nolint: gocyclo -func (reader *Reader) parseSubsection(line []byte, num int) (ok bool) { - // (0) - line = bytes.TrimSpace(line[1 : len(line)-1]) - if len(line) == 0 { - return - } - - // (1) - names := bytes.SplitN(line, sepSubsection, 2) - ok = reader.parseSection(names[0], num, false) - if !ok { - return - } +func (reader *Reader) valueCommit(withSpaces bool) { + val := make([]byte, 0) + val = append(val, reader.buf.Bytes()...) - if debug >= debugL2 { - log.Printf(">>> subsection names: %s", names) + if withSpaces { + val = append(val, reader.bufSpaces.Bytes()...) } - // (2) - bfirst := names[1][0] - lastIdx := len(names[1]) - 1 - blast := names[1][lastIdx] - if bfirst != tokDoubleQuote || blast != tokDoubleQuote { - return - } - - var ( - esc bool - runes = bytes.Runes(names[1][1:lastIdx]) - ) - - if debug >= debugL2 { - log.Printf(">>> subsection name: %s", string(runes)) - } + reader._var.value = append(reader._var.value, val...) reader.buf.Reset() + reader.bufSpaces.Reset() +} - for x := 0; x < len(runes); x++ { - // (3) - if esc { - reader.buf.WriteRune(runes[x]) - esc = false - continue - } - if runes[x] == tokBackslash { - esc = true - continue - } - if runes[x] == tokDoubleQuote { - return - } - reader.buf.WriteRune(runes[x]) +func (reader *Reader) valueWriteByte(b byte) { + if reader.bufSpaces.Len() > 0 { + reader.buf.Write(reader.bufSpaces.Bytes()) + reader.bufSpaces.Reset() } - if esc { - return + if b == tokPercent { + reader.bufFormat.Write(escPercent) + } else { + reader.bufFormat.WriteByte(b) } - - reader.sec.subName = nil - reader.sec.subName = append(reader.sec.subName, reader.buf.Bytes()...) - ok = true - - return + reader.buf.WriteByte(b) } diff --git a/lib/ini/reader_bench_test.go b/lib/ini/reader_bench_test.go index 02ea1f12..ba27e12f 100644 --- a/lib/ini/reader_bench_test.go +++ b/lib/ini/reader_bench_test.go @@ -7,12 +7,14 @@ import ( // // 999f056 With bytes.Buffer in functions -// BenchmarkParse-2 300 17007586 ns/op 6361586 B/op 78712 allocs/op/ +// BenchmarkParse-2 300 17007586 ns/op 6361586 B/op 78712 allocs/op/ // // 22dcd07 Move buffer to reader -// BenchmarkParse-2 500 19534400 ns/op 4656335 B/op 81163 allocs/op +// BenchmarkParse-2 500 19534400 ns/op 4656335 B/op 81163 allocs/op +// +// Refactor parser to use bytes.Reader +// BenchmarkParse-2 20000 71120 ns/op 35368 B/op 549 allocs/op // - func BenchmarkParse(b *testing.B) { in := &Ini{} reader := NewReader() diff --git a/lib/ini/reader_test.go b/lib/ini/reader_test.go index 4c075b90..28275e90 100644 --- a/lib/ini/reader_test.go +++ b/lib/ini/reader_test.go @@ -1,179 +1,287 @@ package ini import ( + "io" "testing" "github.com/shuLhan/share/lib/test" ) -func TestParseVarName(t *testing.T) { +func TestParseVariable(t *testing.T) { cases := []struct { - desc string - in []byte - exp []byte - expOK bool + desc string + in []byte + expErr error + expMode varMode + expFormat []byte + expComment []byte + expKey []byte + expValue []byte }{{ - desc: "Empty", + desc: "Empty", + expErr: errVarNameInvalid, }, { desc: "Empty with space", in: []byte(" "), + expErr: errVarNameInvalid, }, { - desc: "Digit at start", - in: []byte("0name"), + desc: "Digit at start", + in: []byte("0name"), + expErr: errVarNameInvalid, }, { - desc: "Digit at end", - in: []byte("name0"), - exp: []byte("name0"), - expOK: true, + desc: "Digit at end", + in: []byte("name0"), + expErr: io.EOF, + expMode: varModeSingle, + expFormat: []byte("%s"), + expKey: []byte("name0"), + expValue: varValueTrue, }, { - desc: "Digit at middle", - in: []byte("na0me"), - exp: []byte("na0me"), - expOK: true, + desc: "Digit at middle", + in: []byte("na0me"), + expErr: io.EOF, + expMode: varModeSingle, + expFormat: []byte("%s"), + expKey: []byte("na0me"), + expValue: varValueTrue, }, { - desc: "Hyphen at start", - in: []byte("-name"), + desc: "Hyphen at start", + in: []byte("-name"), + expErr: errVarNameInvalid, }, { - desc: "Hyphen at end", - in: []byte("name-"), - exp: []byte("name-"), - expOK: true, + desc: "Hyphen at end", + in: []byte("name-"), + expErr: io.EOF, + expMode: varModeSingle, + expFormat: []byte("%s"), + expKey: []byte("name-"), + expValue: varValueTrue, }, { - desc: "hyphen at middle", - in: []byte("na-me"), - exp: []byte("na-me"), - expOK: true, + desc: "hyphen at middle", + in: []byte("na-me"), + expErr: io.EOF, + expMode: varModeSingle, + expFormat: []byte("%s"), + expKey: []byte("na-me"), + expValue: varValueTrue, }, { - desc: "Non alnumhyp at start", - in: []byte("!name"), + desc: "Non alnumhyp at start", + in: []byte("!name"), + expErr: errVarNameInvalid, }, { - desc: "Non alnumhyp at end", - in: []byte("name!"), + desc: "Non alnumhyp at end", + in: []byte("name!"), + expErr: errVarNameInvalid, }, { - desc: "Non alnumhyp at middle", - in: []byte("na!me"), + desc: "Non alnumhyp at middle", + in: []byte("na!me"), + expErr: errVarNameInvalid, }, { - desc: "With escaped char \\", - in: []byte(`na\me`), + desc: "With escaped char \\", + in: []byte(`na\me`), + expErr: errVarNameInvalid, + }, { + desc: "With comment #1", + in: []byte(`name; comment`), + expErr: io.EOF, + expMode: varModeSingle | varModeComment, + expKey: []byte("name"), + expComment: []byte("; comment"), + expFormat: []byte("%s%s"), + expValue: varValueTrue, + }, { + desc: "With comment #2", + in: []byte(`name ; comment`), + expErr: io.EOF, + expMode: varModeSingle | varModeComment, + expKey: []byte("name"), + expComment: []byte("; comment"), + expFormat: []byte("%s %s"), + expValue: varValueTrue, + }, { + desc: "With empty value #1", + in: []byte(`name=`), + expErr: io.EOF, + expMode: varModeSingle, + expKey: []byte("name"), + expFormat: []byte("%s="), + expValue: varValueTrue, + }, { + desc: "With empty value #2", + in: []byte(`name =`), + expErr: io.EOF, + expMode: varModeSingle, + expKey: []byte("name"), + expFormat: []byte("%s ="), + expValue: varValueTrue, + }, { + desc: "With empty value and comment", + in: []byte(`name =# a comment`), + expErr: io.EOF, + expMode: varModeSingle | varModeComment, + expKey: []byte("name"), + expFormat: []byte("%s =%s"), + expComment: []byte("# a comment"), + expValue: varValueTrue, }} - reader := &Reader{} + reader := NewReader() for _, c := range cases { - t.Log(c.desc) + t.Log(c) + reader.reset(c.in) - got, ok := reader.parseVarName(c.in) - if !ok { - test.Assert(t, c.expOK, ok, true) + err := reader.parseVariable() + if err != nil { + test.Assert(t, "error", c.expErr, err, true) + if err != io.EOF { + continue + } } - test.Assert(t, c.exp, got, true) + test.Assert(t, "mode", c.expMode, reader._var.mode, true) + test.Assert(t, "format", c.expFormat, reader._var.format, true) + test.Assert(t, "key", c.expKey, reader._var.key, true) + test.Assert(t, "value", c.expValue, reader._var.value, true) + test.Assert(t, "comment", c.expComment, reader._var.others, true) } } func TestParseVarValue(t *testing.T) { cases := []struct { - desc string - in []byte - expval []byte - expcom []byte - expok bool + desc string + in []byte + expErr error + expFormat []byte + expValue []byte + expComment []byte }{{ - desc: `Empty input`, - expval: varValueTrue, - expok: true, + desc: `Empty input`, + expErr: io.EOF, + expValue: varValueTrue, + }, { + desc: `Input with spaces`, + in: []byte(` `), + expErr: io.EOF, + expFormat: []byte(` `), + expValue: varValueTrue, }, { - desc: `Input with spaces`, - in: []byte(` `), - expval: varValueTrue, - expok: true, + desc: `Input with tab`, + in: []byte(` `), + expErr: io.EOF, + expFormat: []byte(` `), + expValue: varValueTrue, }, { - desc: `Double quoted with spaces`, - in: []byte(`" "`), - expval: []byte(` `), - expok: true, + desc: `Input with newline`, + in: []byte(` +`), + expErr: nil, + expFormat: []byte(` +`), + expValue: varValueTrue, }, { - desc: `Double quote at start only`, - in: []byte(`"\\ value`), - expok: false, + desc: `Double quoted with spaces`, + in: []byte(`" "`), + expErr: io.EOF, + expFormat: []byte(`" "`), + expValue: []byte(" "), }, { - desc: `Double quote at end only`, - in: []byte(`\\ value "`), - expok: false, + desc: `Double quote at start only`, + in: []byte(`"\\ value`), + expErr: errValueInvalid, }, { - desc: `Double quoted at start only`, - in: []byte(`"\\" value`), - expval: []byte(`\ value`), - expok: true, + desc: `Double quote at end only`, + in: []byte(`\\ value "`), + expErr: errValueInvalid, }, { - desc: `Double quoted at end only`, - in: []byte(`value "\""`), - expval: []byte(`value "`), - expok: true, + desc: `Double quoted at start only`, + in: []byte(`"\\" value`), + expErr: io.EOF, + expFormat: []byte(`"\\" value`), + expValue: []byte(`\ value`), }, { - desc: `Double quoted at start and end`, - in: []byte(`"\\" value "\""`), - expval: []byte(`\ value "`), - expok: true, + desc: `Double quoted at end only`, + in: []byte(`value "\""`), + expErr: io.EOF, + expFormat: []byte(`value "\""`), + expValue: []byte(`value "`), }, { - desc: `With comment #`, - in: []byte(`value # comment`), - expval: []byte(`value`), - expcom: []byte(` # comment`), - expok: true, + desc: `Double quoted at start and end`, + in: []byte(`"\\" value "\""`), + expErr: io.EOF, + expFormat: []byte(`"\\" value "\""`), + expValue: []byte(`\ value "`), }, { - desc: `With comment ;`, - in: []byte(`value ; comment`), - expval: []byte(`value`), - expcom: []byte(` ; comment`), - expok: true, + desc: `With comment #`, + in: []byte(`value # comment`), + expErr: io.EOF, + expFormat: []byte(`value %s`), + expValue: []byte("value"), + expComment: []byte("# comment"), }, { - desc: `With comment # inside double-quote`, - in: []byte(`"value # comment"`), - expval: []byte(`value # comment`), - expok: true, + desc: `With comment ;`, + in: []byte(`value ; comment`), + expErr: io.EOF, + expFormat: []byte("value %s"), + expValue: []byte("value"), + expComment: []byte("; comment"), }, { - desc: `With comment ; inside double-quote`, - in: []byte(`"value ; comment"`), - expval: []byte(`value ; comment`), - expok: true, + desc: `With comment # inside double-quote`, + in: []byte(`"value # comment"`), + expErr: io.EOF, + expFormat: []byte(`"value # comment"`), + expValue: []byte(`value # comment`), }, { - desc: `Double quote and comment #1`, - in: []byte(`val" "#ue`), - expval: []byte(`val `), - expcom: []byte(`#ue`), - expok: true, + desc: `With comment ; inside double-quote`, + in: []byte(`"value ; comment"`), + expErr: io.EOF, + expFormat: []byte(`"value ; comment"`), + expValue: []byte(`value ; comment`), }, { - desc: `Double quote and comment #2`, - in: []byte(`val" " #ue`), - expval: []byte(`val `), - expcom: []byte(` #ue`), - expok: true, + desc: `Double quote and comment #1`, + in: []byte(`val" "#ue`), + expErr: io.EOF, + expFormat: []byte(`val" "%s`), + expValue: []byte(`val `), + expComment: []byte(`#ue`), }, { - desc: `Double quote and comment #3`, - in: []byte(`val " " #ue`), - expval: []byte(`val `), - expcom: []byte(` #ue`), - expok: true, + desc: `Double quote and comment #2`, + in: []byte(`val" " #ue`), + expErr: io.EOF, + expFormat: []byte(`val" " %s`), + expValue: []byte(`val `), + expComment: []byte(`#ue`), }, { - desc: `Escaped chars`, - in: []byte(`value \"escaped\" here`), - expval: []byte(`value "escaped" here`), - expok: true, + desc: `Double quote and comment #3`, + in: []byte(`val " " #ue`), + expErr: io.EOF, + expFormat: []byte(`val " " %s`), + expValue: []byte(`val `), + expComment: []byte(`#ue`), + }, { + desc: `Escaped chars`, + in: []byte(`value \"escaped\" here`), + expErr: io.EOF, + expFormat: []byte(`value \"escaped\" here`), + expValue: []byte(`value "escaped" here`), }} - reader := &Reader{} - + reader := NewReader() for _, c := range cases { t.Log(c.desc) + reader.reset(c.in) - gotval, gotcom, ok := reader.parseVarValue(c.in) - if !ok { - test.Assert(t, c.expok, ok, true) + err := reader.parseVarValue() + if err != nil { + test.Assert(t, "error", c.expErr, err, true) + if err != io.EOF { + continue + } } - test.Assert(t, c.expval, gotval, true) - test.Assert(t, c.expcom, gotcom, true) + test.Assert(t, "format", c.expFormat, reader._var.format, true) + test.Assert(t, "value", c.expValue, reader._var.value, true) + test.Assert(t, "comment", c.expComment, reader._var.others, true) } } diff --git a/lib/ini/section.go b/lib/ini/section.go index 65a276f1..22bace78 100644 --- a/lib/ini/section.go +++ b/lib/ini/section.go @@ -5,47 +5,7 @@ import ( "fmt" ) -type sectionMode uint - -const ( - sectionModeNone sectionMode = 1 << iota - sectionModeNormal - sectionModeSub -) - -type section struct { - m sectionMode - name []byte - subName []byte - vars []*variable -} - -// -// pushVar will push new variable to list if no key exist or replace existing -// value if it's exist. -// -func (sec *section) pushVar(mode varMode, k, v, comment []byte) { - switch mode { - case varModeNewline: - sec.vars = append(sec.vars, varNewline) - - case varModeComment: - sec.vars = append(sec.vars, &variable{ - m: mode, - k: nil, - v: nil, - c: comment, - }) - - case varModeNormal: - sec.vars = append(sec.vars, &variable{ - m: mode, - k: k, - v: v, - c: comment, - }) - } -} +type section variable // // Get will return the last key's value. @@ -60,21 +20,48 @@ func (sec *section) Get(key []byte) (val []byte, ok bool) { for ; x >= 0; x-- { if debug >= debugL2 { - fmt.Printf("sec: %s, var: %s %s\n", sec.name, - string(sec.vars[x].k), - string(sec.vars[x].v)) - } - if sec.vars[x].m != varModeNormal { - continue + fmt.Printf("sec: %s, var: %s %s\n", sec.secName, + string(sec.vars[x].key), + string(sec.vars[x].value)) } - if !bytes.Equal(sec.vars[x].k, key) { + if !bytes.Equal(sec.vars[x].keyLower, key) { continue } - val = sec.vars[x].v + val = sec.vars[x].value ok = true break } return } + +func (sec *section) addVariable(v *variable) { + if v == nil { + return + } + + v.keyLower = bytes.ToLower(v.key) + sec.vars = append(sec.vars, v) +} + +// +// String return formatted INI section header. +// nolint: gas +func (sec *section) String() string { + var buf bytes.Buffer + format := string(sec.format) + + switch sec.mode { + case varModeSection: + _, _ = fmt.Fprintf(&buf, format, sec.secName) + case varModeSection | varModeComment: + _, _ = fmt.Fprintf(&buf, format, sec.secName, sec.others) + case varModeSection | varModeSubsection: + _, _ = fmt.Fprintf(&buf, format, sec.secName, sec.subName) + case varModeSection | varModeSubsection | varModeComment: + _, _ = fmt.Fprintf(&buf, format, sec.secName, sec.subName, sec.others) + } + + return buf.String() +} diff --git a/lib/ini/testdata/input.ini b/lib/ini/testdata/input.ini index 88028f93..68524030 100644 --- a/lib/ini/testdata/input.ini +++ b/lib/ini/testdata/input.ini @@ -64,7 +64,7 @@ pager = less -R editor = nvim autocrlf = false - filemode = false + filemode = true [gui] fontui = -family \"xos4 Terminus\" -size 10 -weight normal -slant roman -underline 0 -overstrike 0 fontdiff = -family \"xos4 Terminus\" -size 10 -weight normal -slant roman -underline 0 -overstrike 0 @@ -82,7 +82,7 @@ pending = codereview pending submit = codereview submit sync = codereview sync - tree = --no-pager log --graph \ + tree = !git --no-pager log --graph \ --date=format:'%Y-%m-%d' \ --pretty=format:'%C(auto,dim)%ad %<(7,trunc) %an %Creset%m %h %s %Cgreen%d%Creset' \ --exclude="*/production" \ @@ -106,5 +106,13 @@ name = Shulhan email = ms@kilabit.info -[url "git@github.com:"] +[url "git@github.com:"] # Replace HTTP URL with git+ssh insteadOf = https://github.com/ + +[last] + valid0 + valid1 = + valid2 = # comment + valid3 = \ + ; comment + valid4 = diff --git a/lib/ini/testdata/var_without_section.ini b/lib/ini/testdata/var_without_section.ini index 51bb1d02..465bee31 100644 --- a/lib/ini/testdata/var_without_section.ini +++ b/lib/ini/testdata/var_without_section.ini @@ -10,5 +10,3 @@ key = value [core] ; Don't trust file modes filemode = false - - diff --git a/lib/ini/variable.go b/lib/ini/variable.go index a206bd31..aeb88b1d 100644 --- a/lib/ini/variable.go +++ b/lib/ini/variable.go @@ -1,21 +1,67 @@ package ini +import ( + "bytes" + "fmt" +) + type varMode uint +const varModeEmpty varMode = 0 + const ( - varModeNewline varMode = 1 << iota - varModeComment - varModeNormal + varModeComment varMode = 1 << iota + varModeSection // 2 + varModeSubsection // 4 + varModeSingle // 8 + varModeValue // 16 + varModeMulti // 32 ) var ( - varNewline = &variable{m: varModeNewline, k: nil, v: nil, c: nil} varValueTrue = []byte("true") ) type variable struct { - m varMode - k []byte - v []byte - c []byte + mode varMode + lineNum int + format []byte + secName []byte + subName []byte + key []byte + value []byte + others []byte + secLower []byte + keyLower []byte + vars []*variable +} + +// +// String return formatted INI variable. +// +// nolint: gocyclo, gas +func (v *variable) String() string { + var buf bytes.Buffer + format := string(v.format) + + switch v.mode { + case varModeEmpty: + _, _ = fmt.Fprintf(&buf, format) + case varModeComment: + _, _ = fmt.Fprintf(&buf, format, v.others) + case varModeSingle: + _, _ = fmt.Fprintf(&buf, format, v.key) + case varModeSingle | varModeComment: + _, _ = fmt.Fprintf(&buf, format, v.key, v.others) + case varModeValue: + _, _ = fmt.Fprintf(&buf, format, v.key) + case varModeValue | varModeComment: + _, _ = fmt.Fprintf(&buf, format, v.key, v.others) + case varModeMulti: + _, _ = fmt.Fprintf(&buf, format, v.key) + case varModeMulti | varModeComment: + _, _ = fmt.Fprintf(&buf, format, v.key, v.others) + } + + return buf.String() } diff --git a/lib/test/test.go b/lib/test/test.go index 0a71e34d..a937f3a9 100644 --- a/lib/test/test.go +++ b/lib/test/test.go @@ -53,9 +53,10 @@ func Assert(t *testing.T, name string, exp, got interface{}, equal bool) { printStackTrace(t) - t.Fatalf("\n"+ - ">>> Expecting %s '%+v'\n"+ - " got '%+v'\n", name, exp, got) + t.Fatalf(">>> Expecting %s,\n"+ + "'%+v'\n"+ + " got,\n"+ + "'%+v'\n", name, exp, got) os.Exit(1) } } |
