From ee24c0227c0dacbab9a257e5f2851ea6b2bf9677 Mon Sep 17 00:00:00 2001 From: Link Dupont Date: Tue, 17 Sep 2019 12:06:29 -0400 Subject: [PATCH] Add parser for decoding --- parse.go | 161 ++++++++++++++++++++++++++++++++++++++++++++ parse_test.go | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 parse.go create mode 100644 parse_test.go diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..9e564d2 --- /dev/null +++ b/parse.go @@ -0,0 +1,161 @@ +package ini + +import ( + "fmt" +) + +type errParse struct { + line int + col int + s string +} + +func (e *errParse) Error() string { + return fmt.Sprintf("parse:%v:%v: %v", e.line, e.col, e.s) +} + +type section struct { + name string + props map[string]property +} + +type property struct { + key string + val []string +} + +type parser struct { + ast map[string]section + l *lexer + tok token +} + +func newParser(data []byte) *parser { + p := parser{ + ast: map[string]section{ + "": section{ + name: "", + props: map[string]property{}, + }, + }, + l: lex(string(data)), + } + return &p +} + +func (p *parser) nextToken() { + p.tok = p.l.nextToken() +} + +func (p *parser) parse() error { + for { + if p.tok.typ == tokenEOF { + return nil + } + p.nextToken() + switch p.tok.typ { + case tokenEOF: + return nil + case tokenError: + return &errParse{p.l.line, p.l.col, p.tok.val} + case tokenSection: + var sec section + sec, ok := p.ast[p.tok.val] + if !ok { + sec = section{ + name: p.tok.val, + props: map[string]property{}, + } + } + if err := p.parseSection(&sec); err != nil { + return err + } + p.ast[sec.name] = sec + case tokenKey: + var prop property + prop, ok := p.ast[""].props[p.tok.val] + if !ok { + prop = property{ + key: p.tok.val, + val: []string{}, + } + } + if err := p.parseProperty(&prop); err != nil { + return err + } + p.ast[""].props[prop.key] = prop + } + } +} + +func (p *parser) parseSection(out *section) error { + name := p.tok.val + + if name != out.name { + panic(fmt.Sprintf("section name mismatch: expected '%v', got '%v'", name, out.name)) + } + + for { + if p.tok.typ == tokenEOF { + return nil + } + p.nextToken() + switch p.tok.typ { + case tokenEOF: + return nil + case tokenError: + return &errParse{p.l.line, p.l.col, p.tok.val} + case tokenKey: + var prop property + prop, ok := out.props[p.tok.val] + if !ok { + prop = property{ + key: p.tok.val, + val: []string{}, + } + } + if err := p.parseProperty(&prop); err != nil { + return err + } + out.props[prop.key] = prop + case tokenSection: + var sec section + sec, ok := p.ast[p.tok.val] + if !ok { + sec = section{ + name: p.tok.val, + props: map[string]property{}, + } + } + if err := p.parseSection(&sec); err != nil { + return err + } + p.ast[sec.name] = sec + default: + return nil + } + } +} + +func (p *parser) parseProperty(out *property) error { + key := p.tok.val + + p.nextToken() + if p.tok.typ != tokenAssignment { + return &errParse{p.l.line, p.l.col, p.tok.val} + } + + p.nextToken() + if p.tok.typ != tokenText { + return &errParse{p.l.line, p.l.col, p.tok.val} + } + val := p.tok.val + + if key != out.key { + panic(fmt.Sprintf("property key mismatch: expected '%v', got '%v'", key, out.key)) + } + + out.val = append(out.val, val) + + return nil +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..66feb82 --- /dev/null +++ b/parse_test.go @@ -0,0 +1,180 @@ +package ini + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParse(t *testing.T) { + tests := []struct { + input string + want map[string]section + }{ + { + input: "version=1.2.3\n\n[user]\nname=root\nshell=/bin/bash", + want: map[string]section{ + "": section{ + name: "", + props: map[string]property{ + "version": property{ + key: "version", + val: []string{"1.2.3"}, + }, + }, + }, + "user": section{ + name: "user", + props: map[string]property{ + "name": property{ + key: "name", + val: []string{"root"}, + }, + "shell": property{ + key: "shell", + val: []string{"/bin/bash"}, + }, + }, + }, + }, + }, + { + input: ` +version=1.2.3 + +[owner] +name=John Doe +organization=Acme Widgets Inc. + +[database] +server=192.0.2.62 +port=143 +file="payroll.dat"`, + want: map[string]section{ + "": section{ + name: "", + props: map[string]property{ + "version": property{ + key: "version", + val: []string{"1.2.3"}, + }, + }, + }, + "owner": section{ + name: "owner", + props: map[string]property{ + "name": property{ + key: "name", + val: []string{"John Doe"}, + }, + "organization": property{ + key: "organization", + val: []string{"Acme Widgets Inc."}, + }, + }, + }, + "database": section{ + name: "database", + props: map[string]property{ + "server": property{ + key: "server", + val: []string{"192.0.2.62"}, + }, + "port": property{ + key: "port", + val: []string{"143"}, + }, + "file": property{ + key: "file", + val: []string{`"payroll.dat"`}, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + p := newParser([]byte(test.input)) + err := p.parse() + if err != nil { + t.Fatal(err) + } + if !cmp.Equal(p.ast, test.want, cmp.Options{cmp.AllowUnexported(section{}, property{})}) { + t.Fatalf("%v != %v", p.ast, test.want) + } + } +} + +func TestParseProp(t *testing.T) { + tests := []struct { + input string + want property + }{ + { + input: "shell=/bin/bash", + want: property{ + key: "shell", + val: []string{ + "/bin/bash", + }, + }, + }, + } + + for _, test := range tests { + p := newParser([]byte(test.input)) + p.nextToken() + got := property{ + key: p.tok.val, + val: []string{}, + } + err := p.parseProperty(&got) + if err != nil { + t.Fatal(err) + } + if !cmp.Equal(got, test.want, cmp.Options{cmp.AllowUnexported(property{})}) { + t.Errorf("%v != %v", got, test.want) + } + } +} + +func TestParseSection(t *testing.T) { + tests := []struct { + input string + want section + }{ + { + input: "[user]\nname=root\nshell=/bin/bash", + want: section{ + name: "user", + props: map[string]property{ + "name": property{ + key: "name", + val: []string{"root"}, + }, + "shell": property{ + key: "shell", + val: []string{"/bin/bash"}, + }, + }, + }, + }, + } + + for _, test := range tests { + p := newParser([]byte(test.input)) + p.nextToken() + got := section{ + name: p.tok.val, + props: map[string]property{}, + } + err := p.parseSection(&got) + if err != nil { + t.Fatal(err) + } + if !cmp.Equal(got, test.want, cmp.Options{cmp.AllowUnexported(property{}, section{})}) { + t.Errorf("%v != %v", got, test.want) + } + } +}