aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2018-05-10 20:49:02 +0700
committerShulhan <ms@kilabit.info>2018-05-10 20:49:02 +0700
commit488e6c32348fb84636dd69d46c151fffcfe7ee37 (patch)
tree928837e7a53baf3f85a4f5060cafacedc6b9062e /lib
parente6ef580dc8134019cffa94810907b81cb5caa19a (diff)
downloadpakakeh.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.go52
-rw-r--r--lib/ini/ini_test.go62
-rw-r--r--lib/ini/parsedline.go22
-rw-r--r--lib/ini/reader.go860
-rw-r--r--lib/ini/reader_bench_test.go8
-rw-r--r--lib/ini/reader_test.go344
-rw-r--r--lib/ini/section.go85
-rw-r--r--lib/ini/testdata/input.ini14
-rw-r--r--lib/ini/testdata/var_without_section.ini2
-rw-r--r--lib/ini/variable.go62
-rw-r--r--lib/test/test.go7
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 = &section{
- 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 = &section{
- 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 = &section{
- 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 = &section{
- 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)
}
}