From 10567f459b63d1337b136eafaabdc040a7f3dff9 Mon Sep 17 00:00:00 2001 From: David Arroyo Date: Mon, 15 Jul 2013 10:16:10 -0400 Subject: [PATCH] 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. --- decode.go | 1 - ndb.go | 104 ++++++++++++++++++----------- read.go | 85 +++++++++++------------ read_test.go | 182 +++++++++++++++++++++++--------------------------- write.go | 131 ++++++++++++++++++++++++++++++++++++ write_test.go | 53 +++++++++++++++ 6 files changed, 375 insertions(+), 181 deletions(-) delete mode 100644 decode.go diff --git a/decode.go b/decode.go deleted file mode 100644 index faea4ff..0000000 --- a/decode.go +++ /dev/null @@ -1 +0,0 @@ -package ndb diff --git a/ndb.go b/ndb.go index 8d1b1b7..f738661 100644 --- a/ndb.go +++ b/ndb.go @@ -22,23 +22,23 @@ package ndb import ( - "reflect" - "bytes" "bufio" - "net/textproto" + "bytes" "fmt" "io" + "net/textproto" + "reflect" "unicode/utf8" ) // A SyntaxError occurs when malformed input, such as an unterminated // 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 // Decode() method type SyntaxError struct { - Data []byte - Offset int64 + Data []byte + Offset int64 Message string } @@ -52,7 +52,7 @@ func (e *TypeError) Error() 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 { return a } @@ -61,24 +61,27 @@ func min(a,b int64) int64 { func (e *SyntaxError) Error() string { start := e.Offset - end := min(e.Offset + 10, int64(len(e.Data))) - - // Make sure we're on utf8 boundaries - for !utf8.RuneStart(e.Data[start]) && start > 0 { - start-- + end := min(e.Offset+10, int64(len(e.Data))) + + if e.Data != nil { + // Make sure we're on utf8 boundaries + 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)) { - end++ - } - - return fmt.Sprintf("%s\n\tat `%s'", e.Message, e.Data[start:end]) + return e.Message } // An Encoder wraps an io.Writer and serializes Go values // into ndb strings. Successive calls to the Encode() method // append lines to the io.Writer. type Encoder struct { - out bufio.Writer + start bool + out io.Writer } // 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 // into the Go value v. Value v must be a pointer. Parse will behave // differently depending on the type of value v points to. -// +// // If v is a slice, Parse will decode all lines from the ndb input // into slice elements. Otherwise, Parse will decode only the first // line. -// +// // 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. -// +// // If v is a struct, Parse will populate struct fields whose names // match the ndb attribute. Struct fields may be annotated with a tag // of the form `ndb:"name"`, where name matches the attribute string // in the ndb input. -// +// // 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 // 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 { d := new(Decoder) d.src = textproto.NewReader(bufio.NewReader(r)) - d.attrs = make(map[string] struct{}, 8) - d.multi = make(map[string] struct{}, 8) - d.finfo = make(map[string] []int, 8) + d.attrs = make(map[string]struct{}, 8) + d.multi = make(map[string]struct{}, 8) + d.finfo = make(map[string][]int, 8) return d } @@ -134,32 +137,32 @@ func NewDecoder(r io.Reader) *Decoder { func (d *Decoder) Decode(v interface{}) error { val := reflect.ValueOf(v) typ := reflect.TypeOf(v) - + if typ.Kind() != reflect.Ptr { return &TypeError{typ} } - + if typ.Elem().Kind() == reflect.Slice { return d.decodeSlice(val) } - p,err := d.getPairs() + p, err := d.getPairs() if err != nil { return err } - + switch typ.Elem().Kind() { default: return &TypeError{val.Type()} case reflect.Map: if val.Elem().IsNil() { val.Elem().Set(reflect.MakeMap(typ.Elem())) - } - return d.saveMap(p,val.Elem()) + } + return d.saveMap(p, val.Elem()) case reflect.Struct: if val.IsNil() { return &TypeError{nil} } - return d.saveStruct(p,val.Elem()) + return d.saveStruct(p, val.Elem()) } 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 // the order of the tuples. 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 @@ -183,16 +191,32 @@ func Emit(v interface{}) ([]byte, error) { // If the value cannot be fully encoded, an error is returned and // no data will be written to the io.Writer. 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 // io.Writer func NewEncoder(w io.Writer) *Encoder { - return nil -} - -// Flush forces all outstanding data in an Encoder to be written to -// its backend io.Writer. -func (e *Encoder) Flush() { + return &Encoder{out: w} } diff --git a/read.go b/read.go index 4f068f2..9db594f 100644 --- a/read.go +++ b/read.go @@ -1,14 +1,14 @@ package ndb import ( - "io" - "reflect" - "net/textproto" - "unicode" - "strconv" "bytes" - "strings" "fmt" + "io" + "net/textproto" + "reflect" + "strconv" + "strings" + "unicode" ) type scanner struct { @@ -24,22 +24,22 @@ func (p pair) String() string { } 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 { - return &SyntaxError { line, offset, "Unterminated quoted string" } + return &SyntaxError{line, offset, "Unterminated quoted string"} } 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 { - return &SyntaxError { line, offset, "Missing white space between tuples" } + return &SyntaxError{line, offset, "Missing white space between tuples"} } func (d *Decoder) getPairs() ([]pair, error) { line, err := d.src.ReadContinuedLineBytes() if err != nil { - return nil,err + return nil, err } d.reset() return d.parseLine(line) @@ -59,7 +59,7 @@ func (d *Decoder) reset() { func (d *Decoder) decodeSlice(val reflect.Value) error { var err error - + if val.Kind() != reflect.Ptr { 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 { kv := reflect.New(val.Type().Key()) - + if d.havemulti { if val.Type().Elem().Kind() != reflect.Slice { return &TypeError{val.Type()} } vv := reflect.New(val.Type().Elem().Elem()) - for _,p := range pairs { + for _, p := range pairs { if err := storeVal(kv, p.attr); err != nil { return err } @@ -101,13 +101,13 @@ func (d *Decoder) saveMap(pairs []pair, val reflect.Value) error { if slot.Kind() == reflect.Invalid { slot = reflect.MakeSlice(val.Type().Elem(), 0, 4) } - + slot = reflect.Append(slot, vv.Elem()) val.SetMapIndex(kv.Elem(), slot) } } else { vv := reflect.New(val.Type().Elem()) - for _,p := range pairs { + for _, p := range pairs { if err := storeVal(kv, p.attr); err != nil { return err } @@ -136,10 +136,10 @@ func (d *Decoder) saveStruct(pairs []pair, val reflect.Value) error { d.finfo[field.Name] = field.Index } } - for _,p := range pairs { - if id,ok := d.finfo[string(p.attr)]; ok { + for _, p := range pairs { + if id, ok := d.finfo[string(p.attr)]; ok { 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 { return &TypeError{f.Type()} } @@ -163,7 +163,7 @@ func storeVal(dst reflect.Value, src []byte) error { } dst = dst.Elem() } - + switch dst.Kind() { default: return &TypeError{dst.Type()} @@ -203,6 +203,7 @@ func storeVal(dst reflect.Value, src []byte) error { } type scanState []int + func (s *scanState) push(n int) { *s = append(*s, n) } @@ -215,7 +216,7 @@ func (s scanState) top() int { func (s *scanState) pop() int { v := s.top() if len(*s) > 0 { - *s = (*s)[0:len(*s)-1] + *s = (*s)[0 : len(*s)-1] } return v } @@ -235,15 +236,15 @@ const ( // by copying Rob Pike's Go lexing talk. func (d *Decoder) parseLine(line []byte) ([]pair, error) { var add pair - var beg,offset int64 + var beg, offset int64 var esc bool - + state := make(scanState, 0, 3) 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 { - return nil,errBadUnicode(line, offset) + return nil, errBadUnicode(line, offset) } switch state.top() { case scanNone: @@ -253,23 +254,23 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) { state.push(scanAttr) beg = offset } else { - return nil,errBadAttr(line, offset) + return nil, errBadAttr(line, offset) } case scanAttr: if unicode.IsSpace(r) { - add.attr = line[beg:offset] + add.attr = line[beg:offset] 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.multi[string(add.attr)] = struct{}{} } else { d.attrs[string(add.attr)] = struct{}{} } - add.attr,add.val,esc = nil,nil,false + add.attr, add.val, esc = nil, nil, false state.pop() } else if r == '=' { 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.multi[string(add.attr)] = struct{}{} } else { @@ -277,14 +278,14 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) { } state.pop() state.push(scanValueStart) - } else if !(unicode.IsLetter(r) || unicode.IsNumber(r)) { - return nil,errBadAttr(line, offset) + } else if !(r == '-' || unicode.IsLetter(r) || unicode.IsNumber(r)) { + return nil, errBadAttr(line, offset) } case scanValueStart: beg = offset state.pop() state.push(scanValue) - + if r == '\'' { state.push(scanQuoteStart) break @@ -298,7 +299,7 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) { add.val = bytes.Replace(add.val, []byte("''"), []byte("'"), -1) } d.pairbuf = append(d.pairbuf, add) - add.attr,add.val = nil,nil + add.attr, add.val = nil, nil } case scanQuoteClose: state.pop() @@ -307,14 +308,14 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) { state.push(scanQuoteValue) } else if unicode.IsSpace(r) { state.pop() - add.val = line[beg:offset-1] + add.val = line[beg : offset-1] if esc { add.val = bytes.Replace(add.val, []byte("''"), []byte("'"), -1) } d.pairbuf = append(d.pairbuf, add) - add.attr,add.val,esc = nil,nil,false + add.attr, add.val, esc = nil, nil, false } else { - return nil,errMissingSpace(line, offset) + return nil, errMissingSpace(line, offset) } case scanQuoteStart: state.pop() @@ -330,17 +331,17 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) { state.pop() state.push(scanQuoteClose) } else if r == '\n' { - return nil,errUnterminated(line, offset) + return nil, errUnterminated(line, offset) } } offset += int64(sz) } switch state.top() { case scanQuoteValue, scanQuoteStart: - return nil,errUnterminated(line, offset) + return nil, errUnterminated(line, offset) case scanAttr: 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.multi[string(add.attr)] = struct{}{} } else { @@ -360,5 +361,5 @@ func (d *Decoder) parseLine(line []byte) ([]pair, error) { } d.pairbuf = append(d.pairbuf, add) } - return d.pairbuf,nil + return d.pairbuf, nil } diff --git a/read_test.go b/read_test.go index 0a9950c..d0a83f8 100644 --- a/read_test.go +++ b/read_test.go @@ -1,72 +1,73 @@ package ndb import ( - "testing" + "bytes" "fmt" + "testing" ) type screenCfg struct { - Title string + Title string Width, Height uint16 - R,G,B,A uint16 + R, G, B, A uint16 } type netCfg struct { - Host string `ndb:"hostname"` - Vlan []int `ndb:"vlan"` - Native int `ndb:"nativevlan"` + Host string `ndb:"host-name"` + Vlan []int `ndb:"vlan"` + Native int `ndb:"native-vlan"` } var multiMap = []struct { - in string - out map[string] []string + in string + out map[string][]string }{ - { + { in: "user=clive user=david user=trenton group=dirty-dozen", - out: map[string] []string { - "user": []string {"clive", "david", "trenton"}, - "group": []string {"dirty-dozen"}, + out: map[string][]string{ + "user": []string{"clive", "david", "trenton"}, + "group": []string{"dirty-dozen"}, }, }, } - + var advancedTests = []struct { - in string + in string out netCfg }{ - { in: "hostname=p2-jbs537 vlan=66 vlan=35 nativevlan=218", - out: netCfg { - Host: "p2-jbs537", - Vlan: []int {66, 35}, - Native: 218, - }, + {in: "host-name=p2-jbs537 vlan=66 vlan=35 native-vlan=218", + out: netCfg{ + Host: "p2-jbs537", + Vlan: []int{66, 35}, + Native: 218, + }, }, } var structTests = []struct { - in string + in string out screenCfg }{ { in: "Title='Hollywood movie' Width=640 Height=400 A=8", - out: screenCfg { - Title: "Hollywood movie", - Width: 640, + out: screenCfg{ + Title: "Hollywood movie", + Width: 640, Height: 400, - A: 8, + A: 8, }, }, } var mapTests = []struct { - in string - out map[string] string + in string + out map[string]string }{ { - in: "ipnet=murray–hill ip=135.104.0.0 ipmask=255.255.0.0", - out: map[string] string { - "ipnet": "murray-hill", - "ip": "135.104.0.0", + in: "ipnet=murray-hill ip=135.104.0.0 ipmask=255.255.0.0", + out: map[string]string{ + "ipnet": "murray-hill", + "ip": "135.104.0.0", "ipmask": "255.255.0.0", }, }, @@ -74,8 +75,8 @@ var mapTests = []struct { func TestStruct(t *testing.T) { var cfg screenCfg - - for _,tt := range structTests { + + for _, tt := range structTests { if err := Parse([]byte(tt.in), &cfg); err != nil { t.Error(err) } else if cfg != tt.out { @@ -85,20 +86,34 @@ func TestStruct(t *testing.T) { } } func TestMap(t *testing.T) { - var net map[string] string - for _,tt := range mapTests { + var net map[string]string + for _, tt := range mapTests { if err := Parse([]byte(tt.in), &net); err != nil { t.Error(err) - } else if fmt.Sprint(net) != fmt.Sprint(tt.out) { - t.Errorf("Got %v, wanted %v", net, tt.out) + } else if !mapEquals(tt.out, net) { + t.Errorf("Got `%v`, wanted `%v`", net, tt.out) } 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) { var net netCfg - for _,tt := range advancedTests { + for _, tt := range advancedTests { if err := Parse([]byte(tt.in), &net); err != nil { t.Error(err) } else if fmt.Sprint(tt.out) != fmt.Sprint(net) { @@ -109,8 +124,8 @@ func TestAdvanced(t *testing.T) { } func TestMultiMap(t *testing.T) { - var m map[string] []string - for _,tt := range multiMap { + var m map[string][]string + for _, tt := range multiMap { if err := Parse([]byte(tt.in), &m); err != nil { t.Error(err) } 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 { - in []byte + in []byte out []pair }{ { in: []byte("key1=val1 key2=val2 key3=val3"), - out: []pair { - {[]byte("key1"),[]byte("val1")}, - {[]byte("key2"),[]byte("val2")}, - {[]byte("key3"),[]byte("val3")}}, + out: []pair{ + {[]byte("key1"), []byte("val1")}, + {[]byte("key2"), []byte("val2")}, + {[]byte("key3"), []byte("val3")}}, }, { in: []byte("title='Some value with spaces' width=340 height=200"), - out: []pair { - {[]byte("title"),[]byte("Some value with spaces")}, - {[]byte("width"),[]byte("340")}, - {[]byte("height"),[]byte("200")}}, + out: []pair{ + {[]byte("title"), []byte("Some value with spaces")}, + {[]byte("width"), []byte("340")}, + {[]byte("height"), []byte("200")}}, }, { in: []byte("title='Dave''s pasta' sq=Davis cost=$$"), - out: []pair { - {[]byte("title"),[]byte("Dave's pasta")}, - {[]byte("sq"),[]byte("Davis")}, - {[]byte("cost"),[]byte("$$")}}, + out: []pair{ + {[]byte("title"), []byte("Dave's pasta")}, + {[]byte("sq"), []byte("Davis")}, + {[]byte("cost"), []byte("$$")}}, }, { in: []byte("action=''bradley key=jay mod=ctrl+alt+shift"), - out: []pair { - {[]byte("action"),[]byte("'bradley")}, - {[]byte("key"),[]byte("jay")}, - {[]byte("mod"),[]byte("ctrl+alt+shift")}}, + out: []pair{ + {[]byte("action"), []byte("'bradley")}, + {[]byte("key"), []byte("jay")}, + {[]byte("mod"), []byte("ctrl+alt+shift")}}, }, { in: []byte("action=reload key='' mod=ctrl+alt+shift"), - out: []pair { - {[]byte("action"),[]byte("reload")}, - {[]byte("key"),[]byte("'")}, - {[]byte("mod"),[]byte("ctrl+alt+shift")}}, + out: []pair{ + {[]byte("action"), []byte("reload")}, + {[]byte("key"), []byte("'")}, + {[]byte("mod"), []byte("ctrl+alt+shift")}}, }, { in: []byte("s='spaces and '' quotes'"), - out: []pair { - {[]byte("s"),[]byte("spaces and ' quotes")}}, + out: []pair{ + {[]byte("s"), []byte("spaces and ' quotes")}}, }, { in: []byte("esc='Use '''' to escape a '''"), - out: []pair { - {[]byte("esc"),[]byte("Use '' to escape a '")}}, + out: []pair{ + {[]byte("esc"), []byte("Use '' to escape a '")}}, }, - } func Test_parsing(t *testing.T) { - for i,tt := range parseTests { + for i, tt := range parseTests { d := NewDecoder(bytes.NewReader(tt.in)) - p,err := d.getPairs() + p, err := d.getPairs() if err != nil { t.Error(err) t.FailNow() } else { for j := range tt.out { - if j > len(p) || !match(p[j],tt.out[j]) { - t.Errorf("%d: getPairs %s => %v, want %v",i, tt.in, p, tt.out) + 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.FailNow() } } diff --git a/write.go b/write.go index faea4ff..cfbd568 100644 --- a/write.go +++ b/write.go @@ -1 +1,132 @@ 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 +} diff --git a/write_test.go b/write_test.go index e69de29..2ec9f24 100644 --- a/write_test.go +++ b/write_test.go @@ -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)) + } + } +}