Feature complete. I may change the names, and error-handling needs to be more robust. I feel pretty good about resisting bad input, but not so good about saving to or writing from complex data structures, or maps with interface{} elements.

This commit is contained in:
David Arroyo 2013-07-15 10:16:10 -04:00
parent a19bd721cd
commit 10567f459b
6 changed files with 375 additions and 181 deletions

View File

@ -1 +0,0 @@
package ndb

104
ndb.go
View File

@ -22,23 +22,23 @@
package ndb package ndb
import ( import (
"reflect"
"bytes"
"bufio" "bufio"
"net/textproto" "bytes"
"fmt" "fmt"
"io" "io"
"net/textproto"
"reflect"
"unicode/utf8" "unicode/utf8"
) )
// A SyntaxError occurs when malformed input, such as an unterminated // A SyntaxError occurs when malformed input, such as an unterminated
// quoted string, is received. It contains the UTF-8 encoded line that // quoted string, is received. It contains the UTF-8 encoded line that
// was being read and the position of the first byte that caused the // was being read and the position of the first byte that caused the
// syntax error. Data may only be valid until the next call to the // syntax error. Data may only be valid until the next call to the
// Decode() method // Decode() method
type SyntaxError struct { type SyntaxError struct {
Data []byte Data []byte
Offset int64 Offset int64
Message string Message string
} }
@ -52,7 +52,7 @@ func (e *TypeError) Error() string {
return fmt.Sprintf("Invalid type %s or nil pointer", e.Type.String()) return fmt.Sprintf("Invalid type %s or nil pointer", e.Type.String())
} }
func min(a,b int64) int64 { func min(a, b int64) int64 {
if a < b { if a < b {
return a return a
} }
@ -61,24 +61,27 @@ func min(a,b int64) int64 {
func (e *SyntaxError) Error() string { func (e *SyntaxError) Error() string {
start := e.Offset start := e.Offset
end := min(e.Offset + 10, int64(len(e.Data))) end := min(e.Offset+10, int64(len(e.Data)))
// Make sure we're on utf8 boundaries if e.Data != nil {
for !utf8.RuneStart(e.Data[start]) && start > 0 { // Make sure we're on utf8 boundaries
start-- for !utf8.RuneStart(e.Data[start]) && start > 0 {
start--
}
for !utf8.Valid(e.Data[start:end]) && end < int64(len(e.Data)) {
end++
}
return fmt.Sprintf("%s\n\tat `%s'", e.Message, e.Data[start:end])
} }
for !utf8.Valid(e.Data[start:end]) && end < int64(len(e.Data)) { return e.Message
end++
}
return fmt.Sprintf("%s\n\tat `%s'", e.Message, e.Data[start:end])
} }
// An Encoder wraps an io.Writer and serializes Go values // An Encoder wraps an io.Writer and serializes Go values
// into ndb strings. Successive calls to the Encode() method // into ndb strings. Successive calls to the Encode() method
// append lines to the io.Writer. // append lines to the io.Writer.
type Encoder struct { type Encoder struct {
out bufio.Writer start bool
out io.Writer
} }
// A decoder wraps an io.Reader and decodes successive ndb strings // A decoder wraps an io.Reader and decodes successive ndb strings
@ -95,19 +98,19 @@ type Decoder struct {
// The Parse function reads an entire ndb string and unmarshals it // The Parse function reads an entire ndb string and unmarshals it
// into the Go value v. Value v must be a pointer. Parse will behave // into the Go value v. Value v must be a pointer. Parse will behave
// differently depending on the type of value v points to. // differently depending on the type of value v points to.
// //
// If v is a slice, Parse will decode all lines from the ndb input // If v is a slice, Parse will decode all lines from the ndb input
// into slice elements. Otherwise, Parse will decode only the first // into slice elements. Otherwise, Parse will decode only the first
// line. // line.
// //
// If v is a map, Parse will populate v with key/value pairs, where // If v is a map, Parse will populate v with key/value pairs, where
// value is decoded according to the concrete types of the map. // value is decoded according to the concrete types of the map.
// //
// If v is a struct, Parse will populate struct fields whose names // If v is a struct, Parse will populate struct fields whose names
// match the ndb attribute. Struct fields may be annotated with a tag // match the ndb attribute. Struct fields may be annotated with a tag
// of the form `ndb:"name"`, where name matches the attribute string // of the form `ndb:"name"`, where name matches the attribute string
// in the ndb input. // in the ndb input.
// //
// Struct fields or map keys that do not match the ndb input are left // Struct fields or map keys that do not match the ndb input are left
// unmodified. Ndb attributes that do not match any struct fields are // unmodified. Ndb attributes that do not match any struct fields are
// silently dropped. If an ndb string cannot be converted to the // silently dropped. If an ndb string cannot be converted to the
@ -123,9 +126,9 @@ func Parse(data []byte, v interface{}) error {
func NewDecoder(r io.Reader) *Decoder { func NewDecoder(r io.Reader) *Decoder {
d := new(Decoder) d := new(Decoder)
d.src = textproto.NewReader(bufio.NewReader(r)) d.src = textproto.NewReader(bufio.NewReader(r))
d.attrs = make(map[string] struct{}, 8) d.attrs = make(map[string]struct{}, 8)
d.multi = make(map[string] struct{}, 8) d.multi = make(map[string]struct{}, 8)
d.finfo = make(map[string] []int, 8) d.finfo = make(map[string][]int, 8)
return d return d
} }
@ -134,32 +137,32 @@ func NewDecoder(r io.Reader) *Decoder {
func (d *Decoder) Decode(v interface{}) error { func (d *Decoder) Decode(v interface{}) error {
val := reflect.ValueOf(v) val := reflect.ValueOf(v)
typ := reflect.TypeOf(v) typ := reflect.TypeOf(v)
if typ.Kind() != reflect.Ptr { if typ.Kind() != reflect.Ptr {
return &TypeError{typ} return &TypeError{typ}
} }
if typ.Elem().Kind() == reflect.Slice { if typ.Elem().Kind() == reflect.Slice {
return d.decodeSlice(val) return d.decodeSlice(val)
} }
p,err := d.getPairs() p, err := d.getPairs()
if err != nil { if err != nil {
return err return err
} }
switch typ.Elem().Kind() { switch typ.Elem().Kind() {
default: default:
return &TypeError{val.Type()} return &TypeError{val.Type()}
case reflect.Map: case reflect.Map:
if val.Elem().IsNil() { if val.Elem().IsNil() {
val.Elem().Set(reflect.MakeMap(typ.Elem())) val.Elem().Set(reflect.MakeMap(typ.Elem()))
} }
return d.saveMap(p,val.Elem()) return d.saveMap(p, val.Elem())
case reflect.Struct: case reflect.Struct:
if val.IsNil() { if val.IsNil() {
return &TypeError{nil} return &TypeError{nil}
} }
return d.saveStruct(p,val.Elem()) return d.saveStruct(p, val.Elem())
} }
return nil return nil
} }
@ -174,7 +177,12 @@ func (d *Decoder) Decode(v interface{}) error {
// valid ndb strings, an error is returned. No guarantee is made about // valid ndb strings, an error is returned. No guarantee is made about
// the order of the tuples. // the order of the tuples.
func Emit(v interface{}) ([]byte, error) { func Emit(v interface{}) ([]byte, error) {
return nil,nil var buf bytes.Buffer
e := NewEncoder(&buf)
if err := e.Encode(v); err != nil {
return nil, err
}
return buf.Bytes(), nil
} }
// The Encode method will write the ndb encoding of the Go value v // The Encode method will write the ndb encoding of the Go value v
@ -183,16 +191,32 @@ func Emit(v interface{}) ([]byte, error) {
// If the value cannot be fully encoded, an error is returned and // If the value cannot be fully encoded, an error is returned and
// no data will be written to the io.Writer. // no data will be written to the io.Writer.
func (e *Encoder) Encode(v interface{}) error { func (e *Encoder) Encode(v interface{}) error {
return nil val := reflect.ValueOf(v)
// Drill down to the concrete value
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return &TypeError{nil}
} else {
val = val.Elem()
}
}
defer func() {
e.start = false
}()
switch val.Kind() {
case reflect.Slice:
return e.encodeSlice(val)
case reflect.Struct:
return e.encodeStruct(val)
case reflect.Map:
return e.encodeMap(val)
default:
return &TypeError{val.Type()}
}
} }
// NewEncoder returns an Encoder that writes ndb output to an // NewEncoder returns an Encoder that writes ndb output to an
// io.Writer // io.Writer
func NewEncoder(w io.Writer) *Encoder { func NewEncoder(w io.Writer) *Encoder {
return nil return &Encoder{out: w}
}
// Flush forces all outstanding data in an Encoder to be written to
// its backend io.Writer.
func (e *Encoder) Flush() {
} }

85
read.go
View File

@ -1,14 +1,14 @@
package ndb package ndb
import ( import (
"io"
"reflect"
"net/textproto"
"unicode"
"strconv"
"bytes" "bytes"
"strings"
"fmt" "fmt"
"io"
"net/textproto"
"reflect"
"strconv"
"strings"
"unicode"
) )
type scanner struct { type scanner struct {
@ -24,22 +24,22 @@ func (p pair) String() string {
} }
func errBadAttr(line []byte, offset int64) error { func errBadAttr(line []byte, offset int64) error {
return &SyntaxError { line, offset, "Invalid attribute name" } return &SyntaxError{line, offset, "Invalid attribute name"}
} }
func errUnterminated(line []byte, offset int64) error { func errUnterminated(line []byte, offset int64) error {
return &SyntaxError { line, offset, "Unterminated quoted string" } return &SyntaxError{line, offset, "Unterminated quoted string"}
} }
func errBadUnicode(line []byte, offset int64) error { func errBadUnicode(line []byte, offset int64) error {
return &SyntaxError { line, offset, "Invalid UTF8 input" } return &SyntaxError{line, offset, "Invalid UTF8 input"}
} }
func errMissingSpace(line []byte, offset int64) error { func errMissingSpace(line []byte, offset int64) error {
return &SyntaxError { line, offset, "Missing white space between tuples" } return &SyntaxError{line, offset, "Missing white space between tuples"}
} }
func (d *Decoder) getPairs() ([]pair, error) { func (d *Decoder) getPairs() ([]pair, error) {
line, err := d.src.ReadContinuedLineBytes() line, err := d.src.ReadContinuedLineBytes()
if err != nil { if err != nil {
return nil,err return nil, err
} }
d.reset() d.reset()
return d.parseLine(line) return d.parseLine(line)
@ -59,7 +59,7 @@ func (d *Decoder) reset() {
func (d *Decoder) decodeSlice(val reflect.Value) error { func (d *Decoder) decodeSlice(val reflect.Value) error {
var err error var err error
if val.Kind() != reflect.Ptr { if val.Kind() != reflect.Ptr {
return &TypeError{val.Type()} return &TypeError{val.Type()}
} }
@ -84,13 +84,13 @@ func (d *Decoder) decodeSlice(val reflect.Value) error {
func (d *Decoder) saveMap(pairs []pair, val reflect.Value) error { func (d *Decoder) saveMap(pairs []pair, val reflect.Value) error {
kv := reflect.New(val.Type().Key()) kv := reflect.New(val.Type().Key())
if d.havemulti { if d.havemulti {
if val.Type().Elem().Kind() != reflect.Slice { if val.Type().Elem().Kind() != reflect.Slice {
return &TypeError{val.Type()} return &TypeError{val.Type()}
} }
vv := reflect.New(val.Type().Elem().Elem()) vv := reflect.New(val.Type().Elem().Elem())
for _,p := range pairs { for _, p := range pairs {
if err := storeVal(kv, p.attr); err != nil { if err := storeVal(kv, p.attr); err != nil {
return err return err
} }
@ -101,13 +101,13 @@ func (d *Decoder) saveMap(pairs []pair, val reflect.Value) error {
if slot.Kind() == reflect.Invalid { if slot.Kind() == reflect.Invalid {
slot = reflect.MakeSlice(val.Type().Elem(), 0, 4) slot = reflect.MakeSlice(val.Type().Elem(), 0, 4)
} }
slot = reflect.Append(slot, vv.Elem()) slot = reflect.Append(slot, vv.Elem())
val.SetMapIndex(kv.Elem(), slot) val.SetMapIndex(kv.Elem(), slot)
} }
} else { } else {
vv := reflect.New(val.Type().Elem()) vv := reflect.New(val.Type().Elem())
for _,p := range pairs { for _, p := range pairs {
if err := storeVal(kv, p.attr); err != nil { if err := storeVal(kv, p.attr); err != nil {
return err return err
} }
@ -136,10 +136,10 @@ func (d *Decoder) saveStruct(pairs []pair, val reflect.Value) error {
d.finfo[field.Name] = field.Index d.finfo[field.Name] = field.Index
} }
} }
for _,p := range pairs { for _, p := range pairs {
if id,ok := d.finfo[string(p.attr)]; ok { if id, ok := d.finfo[string(p.attr)]; ok {
f := val.FieldByIndex(id) f := val.FieldByIndex(id)
if _,ok := d.multi[string(p.attr)]; ok { if _, ok := d.multi[string(p.attr)]; ok {
if f.Kind() != reflect.Slice { if f.Kind() != reflect.Slice {
return &TypeError{f.Type()} return &TypeError{f.Type()}
} }
@ -163,7 +163,7 @@ func storeVal(dst reflect.Value, src []byte) error {
} }
dst = dst.Elem() dst = dst.Elem()
} }
switch dst.Kind() { switch dst.Kind() {
default: default:
return &TypeError{dst.Type()} return &TypeError{dst.Type()}
@ -203,6 +203,7 @@ func storeVal(dst reflect.Value, src []byte) error {
} }
type scanState []int type scanState []int
func (s *scanState) push(n int) { func (s *scanState) push(n int) {
*s = append(*s, n) *s = append(*s, n)
} }
@ -215,7 +216,7 @@ func (s scanState) top() int {
func (s *scanState) pop() int { func (s *scanState) pop() int {
v := s.top() v := s.top()
if len(*s) > 0 { if len(*s) > 0 {
*s = (*s)[0:len(*s)-1] *s = (*s)[0 : len(*s)-1]
} }
return v return v
} }
@ -235,15 +236,15 @@ const (
// by copying Rob Pike's Go lexing talk. // by copying Rob Pike's Go lexing talk.
func (d *Decoder) parseLine(line []byte) ([]pair, error) { func (d *Decoder) parseLine(line []byte) ([]pair, error) {
var add pair var add pair
var beg,offset int64 var beg, offset int64
var esc bool var esc bool
state := make(scanState, 0, 3) state := make(scanState, 0, 3)
buf := bytes.NewReader(line) buf := bytes.NewReader(line)
for r,sz,err := buf.ReadRune(); err == nil; r,sz,err = buf.ReadRune() { for r, sz, err := buf.ReadRune(); err == nil; r, sz, err = buf.ReadRune() {
if r == 0xFFFD && sz == 1 { if r == 0xFFFD && sz == 1 {
return nil,errBadUnicode(line, offset) return nil, errBadUnicode(line, offset)
} }
switch state.top() { switch state.top() {
case scanNone: case scanNone:
@ -253,23 +254,23 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) {
state.push(scanAttr) state.push(scanAttr)
beg = offset beg = offset
} else { } else {
return nil,errBadAttr(line, offset) return nil, errBadAttr(line, offset)
} }
case scanAttr: case scanAttr:
if unicode.IsSpace(r) { if unicode.IsSpace(r) {
add.attr = line[beg:offset] add.attr = line[beg:offset]
d.pairbuf = append(d.pairbuf, add) d.pairbuf = append(d.pairbuf, add)
if _,ok := d.attrs[string(add.attr)]; ok { if _, ok := d.attrs[string(add.attr)]; ok {
d.havemulti = true d.havemulti = true
d.multi[string(add.attr)] = struct{}{} d.multi[string(add.attr)] = struct{}{}
} else { } else {
d.attrs[string(add.attr)] = struct{}{} d.attrs[string(add.attr)] = struct{}{}
} }
add.attr,add.val,esc = nil,nil,false add.attr, add.val, esc = nil, nil, false
state.pop() state.pop()
} else if r == '=' { } else if r == '=' {
add.attr = line[beg:offset] add.attr = line[beg:offset]
if _,ok := d.attrs[string(add.attr)]; ok { if _, ok := d.attrs[string(add.attr)]; ok {
d.havemulti = true d.havemulti = true
d.multi[string(add.attr)] = struct{}{} d.multi[string(add.attr)] = struct{}{}
} else { } else {
@ -277,14 +278,14 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) {
} }
state.pop() state.pop()
state.push(scanValueStart) state.push(scanValueStart)
} else if !(unicode.IsLetter(r) || unicode.IsNumber(r)) { } else if !(r == '-' || unicode.IsLetter(r) || unicode.IsNumber(r)) {
return nil,errBadAttr(line, offset) return nil, errBadAttr(line, offset)
} }
case scanValueStart: case scanValueStart:
beg = offset beg = offset
state.pop() state.pop()
state.push(scanValue) state.push(scanValue)
if r == '\'' { if r == '\'' {
state.push(scanQuoteStart) state.push(scanQuoteStart)
break break
@ -298,7 +299,7 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) {
add.val = bytes.Replace(add.val, []byte("''"), []byte("'"), -1) add.val = bytes.Replace(add.val, []byte("''"), []byte("'"), -1)
} }
d.pairbuf = append(d.pairbuf, add) d.pairbuf = append(d.pairbuf, add)
add.attr,add.val = nil,nil add.attr, add.val = nil, nil
} }
case scanQuoteClose: case scanQuoteClose:
state.pop() state.pop()
@ -307,14 +308,14 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) {
state.push(scanQuoteValue) state.push(scanQuoteValue)
} else if unicode.IsSpace(r) { } else if unicode.IsSpace(r) {
state.pop() state.pop()
add.val = line[beg:offset-1] add.val = line[beg : offset-1]
if esc { if esc {
add.val = bytes.Replace(add.val, []byte("''"), []byte("'"), -1) add.val = bytes.Replace(add.val, []byte("''"), []byte("'"), -1)
} }
d.pairbuf = append(d.pairbuf, add) d.pairbuf = append(d.pairbuf, add)
add.attr,add.val,esc = nil,nil,false add.attr, add.val, esc = nil, nil, false
} else { } else {
return nil,errMissingSpace(line, offset) return nil, errMissingSpace(line, offset)
} }
case scanQuoteStart: case scanQuoteStart:
state.pop() state.pop()
@ -330,17 +331,17 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) {
state.pop() state.pop()
state.push(scanQuoteClose) state.push(scanQuoteClose)
} else if r == '\n' { } else if r == '\n' {
return nil,errUnterminated(line, offset) return nil, errUnterminated(line, offset)
} }
} }
offset += int64(sz) offset += int64(sz)
} }
switch state.top() { switch state.top() {
case scanQuoteValue, scanQuoteStart: case scanQuoteValue, scanQuoteStart:
return nil,errUnterminated(line, offset) return nil, errUnterminated(line, offset)
case scanAttr: case scanAttr:
add.attr = line[beg:offset] add.attr = line[beg:offset]
if _,ok := d.attrs[string(add.attr)]; ok { if _, ok := d.attrs[string(add.attr)]; ok {
d.havemulti = true d.havemulti = true
d.multi[string(add.attr)] = struct{}{} d.multi[string(add.attr)] = struct{}{}
} else { } else {
@ -360,5 +361,5 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) {
} }
d.pairbuf = append(d.pairbuf, add) d.pairbuf = append(d.pairbuf, add)
} }
return d.pairbuf,nil return d.pairbuf, nil
} }

View File

@ -1,72 +1,73 @@
package ndb package ndb
import ( import (
"testing" "bytes"
"fmt" "fmt"
"testing"
) )
type screenCfg struct { type screenCfg struct {
Title string Title string
Width, Height uint16 Width, Height uint16
R,G,B,A uint16 R, G, B, A uint16
} }
type netCfg struct { type netCfg struct {
Host string `ndb:"hostname"` Host string `ndb:"host-name"`
Vlan []int `ndb:"vlan"` Vlan []int `ndb:"vlan"`
Native int `ndb:"nativevlan"` Native int `ndb:"native-vlan"`
} }
var multiMap = []struct { var multiMap = []struct {
in string in string
out map[string] []string out map[string][]string
}{ }{
{ {
in: "user=clive user=david user=trenton group=dirty-dozen", in: "user=clive user=david user=trenton group=dirty-dozen",
out: map[string] []string { out: map[string][]string{
"user": []string {"clive", "david", "trenton"}, "user": []string{"clive", "david", "trenton"},
"group": []string {"dirty-dozen"}, "group": []string{"dirty-dozen"},
}, },
}, },
} }
var advancedTests = []struct { var advancedTests = []struct {
in string in string
out netCfg out netCfg
}{ }{
{ in: "hostname=p2-jbs537 vlan=66 vlan=35 nativevlan=218", {in: "host-name=p2-jbs537 vlan=66 vlan=35 native-vlan=218",
out: netCfg { out: netCfg{
Host: "p2-jbs537", Host: "p2-jbs537",
Vlan: []int {66, 35}, Vlan: []int{66, 35},
Native: 218, Native: 218,
}, },
}, },
} }
var structTests = []struct { var structTests = []struct {
in string in string
out screenCfg out screenCfg
}{ }{
{ {
in: "Title='Hollywood movie' Width=640 Height=400 A=8", in: "Title='Hollywood movie' Width=640 Height=400 A=8",
out: screenCfg { out: screenCfg{
Title: "Hollywood movie", Title: "Hollywood movie",
Width: 640, Width: 640,
Height: 400, Height: 400,
A: 8, A: 8,
}, },
}, },
} }
var mapTests = []struct { var mapTests = []struct {
in string in string
out map[string] string out map[string]string
}{ }{
{ {
in: "ipnet=murrayhill ip=135.104.0.0 ipmask=255.255.0.0", in: "ipnet=murray-hill ip=135.104.0.0 ipmask=255.255.0.0",
out: map[string] string { out: map[string]string{
"ipnet": "murray-hill", "ipnet": "murray-hill",
"ip": "135.104.0.0", "ip": "135.104.0.0",
"ipmask": "255.255.0.0", "ipmask": "255.255.0.0",
}, },
}, },
@ -74,8 +75,8 @@ var mapTests = []struct {
func TestStruct(t *testing.T) { func TestStruct(t *testing.T) {
var cfg screenCfg var cfg screenCfg
for _,tt := range structTests { for _, tt := range structTests {
if err := Parse([]byte(tt.in), &cfg); err != nil { if err := Parse([]byte(tt.in), &cfg); err != nil {
t.Error(err) t.Error(err)
} else if cfg != tt.out { } else if cfg != tt.out {
@ -85,20 +86,34 @@ func TestStruct(t *testing.T) {
} }
} }
func TestMap(t *testing.T) { func TestMap(t *testing.T) {
var net map[string] string var net map[string]string
for _,tt := range mapTests { for _, tt := range mapTests {
if err := Parse([]byte(tt.in), &net); err != nil { if err := Parse([]byte(tt.in), &net); err != nil {
t.Error(err) t.Error(err)
} else if fmt.Sprint(net) != fmt.Sprint(tt.out) { } else if !mapEquals(tt.out, net) {
t.Errorf("Got %v, wanted %v", net, tt.out) t.Errorf("Got `%v`, wanted `%v`", net, tt.out)
} }
t.Logf("%s == %v", tt.in, net) t.Logf("%s == %v", tt.in, net)
} }
} }
func mapEquals(m1, m2 map[string] string) bool {
for k := range m1 {
if m1[k] != m2[k] {
return false
}
}
for k := range m2 {
if m1[k] != m2[k] {
return false
}
}
return true
}
func TestAdvanced(t *testing.T) { func TestAdvanced(t *testing.T) {
var net netCfg var net netCfg
for _,tt := range advancedTests { for _, tt := range advancedTests {
if err := Parse([]byte(tt.in), &net); err != nil { if err := Parse([]byte(tt.in), &net); err != nil {
t.Error(err) t.Error(err)
} else if fmt.Sprint(tt.out) != fmt.Sprint(net) { } else if fmt.Sprint(tt.out) != fmt.Sprint(net) {
@ -109,8 +124,8 @@ func TestAdvanced(t *testing.T) {
} }
func TestMultiMap(t *testing.T) { func TestMultiMap(t *testing.T) {
var m map[string] []string var m map[string][]string
for _,tt := range multiMap { for _, tt := range multiMap {
if err := Parse([]byte(tt.in), &m); err != nil { if err := Parse([]byte(tt.in), &m); err != nil {
t.Error(err) t.Error(err)
} else if fmt.Sprint(tt.out) != fmt.Sprint(m) { } else if fmt.Sprint(tt.out) != fmt.Sprint(m) {
@ -120,97 +135,68 @@ func TestMultiMap(t *testing.T) {
} }
} }
func netEqual(t *testing.T, n1, n2 netCfg) bool {
if len(n1.Vlan) != len(n2.Vlan) {
return false
}
for i := range n1.Vlan {
if n1.Vlan[i] != n2.Vlan[i] {
return false
}
}
return n1.Host == n2.Host && n1.Native == n2.Native
}
func mapEqual(t *testing.T, m1, m2 map[string] string) bool {
for k := range m1 {
if m1[k] != m2[k] {
t.Logf("%v != %v", m1[k], m2[k])
return false
}
}
return true
}
package ndb
import (
"testing"
"bytes"
)
var parseTests = []struct { var parseTests = []struct {
in []byte in []byte
out []pair out []pair
}{ }{
{ {
in: []byte("key1=val1 key2=val2 key3=val3"), in: []byte("key1=val1 key2=val2 key3=val3"),
out: []pair { out: []pair{
{[]byte("key1"),[]byte("val1")}, {[]byte("key1"), []byte("val1")},
{[]byte("key2"),[]byte("val2")}, {[]byte("key2"), []byte("val2")},
{[]byte("key3"),[]byte("val3")}}, {[]byte("key3"), []byte("val3")}},
}, },
{ {
in: []byte("title='Some value with spaces' width=340 height=200"), in: []byte("title='Some value with spaces' width=340 height=200"),
out: []pair { out: []pair{
{[]byte("title"),[]byte("Some value with spaces")}, {[]byte("title"), []byte("Some value with spaces")},
{[]byte("width"),[]byte("340")}, {[]byte("width"), []byte("340")},
{[]byte("height"),[]byte("200")}}, {[]byte("height"), []byte("200")}},
}, },
{ {
in: []byte("title='Dave''s pasta' sq=Davis cost=$$"), in: []byte("title='Dave''s pasta' sq=Davis cost=$$"),
out: []pair { out: []pair{
{[]byte("title"),[]byte("Dave's pasta")}, {[]byte("title"), []byte("Dave's pasta")},
{[]byte("sq"),[]byte("Davis")}, {[]byte("sq"), []byte("Davis")},
{[]byte("cost"),[]byte("$$")}}, {[]byte("cost"), []byte("$$")}},
}, },
{ {
in: []byte("action=''bradley key=jay mod=ctrl+alt+shift"), in: []byte("action=''bradley key=jay mod=ctrl+alt+shift"),
out: []pair { out: []pair{
{[]byte("action"),[]byte("'bradley")}, {[]byte("action"), []byte("'bradley")},
{[]byte("key"),[]byte("jay")}, {[]byte("key"), []byte("jay")},
{[]byte("mod"),[]byte("ctrl+alt+shift")}}, {[]byte("mod"), []byte("ctrl+alt+shift")}},
}, },
{ {
in: []byte("action=reload key='' mod=ctrl+alt+shift"), in: []byte("action=reload key='' mod=ctrl+alt+shift"),
out: []pair { out: []pair{
{[]byte("action"),[]byte("reload")}, {[]byte("action"), []byte("reload")},
{[]byte("key"),[]byte("'")}, {[]byte("key"), []byte("'")},
{[]byte("mod"),[]byte("ctrl+alt+shift")}}, {[]byte("mod"), []byte("ctrl+alt+shift")}},
}, },
{ {
in: []byte("s='spaces and '' quotes'"), in: []byte("s='spaces and '' quotes'"),
out: []pair { out: []pair{
{[]byte("s"),[]byte("spaces and ' quotes")}}, {[]byte("s"), []byte("spaces and ' quotes")}},
}, },
{ {
in: []byte("esc='Use '''' to escape a '''"), in: []byte("esc='Use '''' to escape a '''"),
out: []pair { out: []pair{
{[]byte("esc"),[]byte("Use '' to escape a '")}}, {[]byte("esc"), []byte("Use '' to escape a '")}},
}, },
} }
func Test_parsing(t *testing.T) { func Test_parsing(t *testing.T) {
for i,tt := range parseTests { for i, tt := range parseTests {
d := NewDecoder(bytes.NewReader(tt.in)) d := NewDecoder(bytes.NewReader(tt.in))
p,err := d.getPairs() p, err := d.getPairs()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
t.FailNow() t.FailNow()
} else { } else {
for j := range tt.out { for j := range tt.out {
if j > len(p) || !match(p[j],tt.out[j]) { if j > len(p) || !match(p[j], tt.out[j]) {
t.Errorf("%d: getPairs %s => %v, want %v",i, tt.in, p, tt.out) t.Errorf("%d: getPairs %s => %v, want %v", i, tt.in, p, tt.out)
t.FailNow() t.FailNow()
} }
} }

131
write.go
View File

@ -1 +1,132 @@
package ndb package ndb
import (
"bytes"
"fmt"
"reflect"
"unicode"
"unicode/utf8"
)
func (e *Encoder) encodeSlice(val reflect.Value) error {
for i := 0; i < val.Len(); i++ {
e.Encode(val.Index(i).Interface())
}
return nil
}
func (e *Encoder) encodeStruct(val reflect.Value) error {
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
ft := typ.Field(i)
attr := ft.Name
if tag := ft.Tag.Get("ndb"); tag != "" {
attr = tag
}
err := e.writeTuple(attr, val.Field(i))
if err != nil {
return err
}
}
return nil
}
func (e *Encoder) encodeMap(val reflect.Value) error {
for _, k := range val.MapKeys() {
v := val.MapIndex(k)
if err := e.writeTuple(k.Interface(), v); err != nil {
return err
}
}
return nil
}
func (e *Encoder) writeTuple(k interface{}, v reflect.Value) error {
var values reflect.Value
var attrBuf, valBuf bytes.Buffer
fmt.Fprint(&attrBuf, k)
attr := attrBuf.Bytes()
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
sliceType := reflect.SliceOf(v.Type())
pv := reflect.New(sliceType)
pv.Elem().Set(reflect.MakeSlice(sliceType, 0, 1))
pv.Elem().Set(reflect.Append(pv.Elem(), v))
values = pv.Elem()
} else {
values = v
}
for i := 0; i < values.Len(); i++ {
fmt.Fprint(&valBuf, values.Index(i).Interface())
val := valBuf.Bytes()
if e.start {
if _, err := e.out.Write([]byte{' '}); err != nil {
return err
}
} else {
e.start = true
}
if !validAttr(attr) {
return &SyntaxError{nil, 0, fmt.Sprintf("Invalid attribute %s", attr)}
}
if !validVal(val) {
return &SyntaxError{nil, 0, fmt.Sprintf("Invalid value %s", val)}
}
if bytes.IndexByte(val, '\'') != -1 {
val = bytes.Replace(val, []byte{'\''}, []byte{'\'', '\''}, -1)
}
if _, err := e.out.Write(attr); err != nil {
return err
}
if _, err := e.out.Write([]byte{'='}); err != nil {
return err
}
x := bytes.IndexFunc(val, func(r rune) bool {
return unicode.IsSpace(r)
})
if x != -1 {
if _, err := e.out.Write([]byte{'\''}); err != nil {
return err
}
}
if _, err := e.out.Write(val); err != nil {
return err
}
if x != -1 {
if _, err := e.out.Write([]byte{'\''}); err != nil {
return err
}
}
valBuf.Reset()
}
return nil
}
func validAttr(attr []byte) bool {
if !utf8.Valid(attr) {
return false
}
x := bytes.IndexFunc(attr, func(r rune) bool {
switch {
case r == '\'':
return true
case unicode.IsSpace(r):
return true
}
return !unicode.IsLetter(r) &&
!unicode.IsNumber(r) &&
r != '-'
})
return x == -1
}
func validVal(val []byte) bool {
if !utf8.Valid(val) {
return false
}
return bytes.IndexByte(val, '\n') == -1
}

View File

@ -0,0 +1,53 @@
package ndb
import (
"testing"
)
var structWriteTests = []struct {
in netCfg
out string
}{
{
netCfg{"p2-jbs239", []int{64, 52, 100}, 666},
"host-name=p2-jbs239 vlan=64 vlan=52 vlan=100 native-vlan=666",
},
{
netCfg{"p2-cass304", []int{55, 10}, 1},
"host-name=p2-cass304 vlan=55 vlan=10 native-vlan=1",
},
}
var mapWriteTests = []struct {
in map[string] string
out string
}{
{
map[string] string {"user": "jenkins", "group": "jenkins"},
"user=jenkins group=jenkins",
},
}
func TestStructWrite(t *testing.T) {
for _, tt := range structWriteTests {
if b, err := Emit(tt.in); err != nil {
t.Error(err)
} else if string(b) != tt.out {
t.Errorf("Wanted %s, got %s", tt.out, string(b))
} else {
t.Logf("%v => %s", tt.in, string(b))
}
}
}
func TestMapWrite(t *testing.T) {
for _, tt := range mapWriteTests {
if b, err := Emit(tt.in); err != nil {
t.Error(err)
} else if string(b) != tt.out {
t.Errorf("Wanted %s, got %s", tt.out, string(b))
} else {
t.Logf("%v => %s", tt.in, string(b))
}
}
}