use reflect for document parsing
authornsz <nsz@port70.net>
Sun, 13 Nov 2011 18:37:58 +0000 (19:37 +0100)
committernsz <nsz@port70.net>
Sun, 13 Nov 2011 18:37:58 +0000 (19:37 +0100)
document/document.go
document/document_test.go

index 46f41ab..afbe31c 100644 (file)
@@ -6,10 +6,6 @@ package document
 // the signature detached. The format of the key-value pairs are
 // similar to MIME header fields.
 //
-// TODO: allow visually aligned field values
-// TODO: handling repeated fields
-// TODO: representation of a list (references)
-//
 // Example:
 //
 // -----BEGIN PGP SIGNED MESSAGE-----
@@ -25,14 +21,17 @@ package document
 
 import (
        "bytes"
+       "encoding/hex"
+       "fmt"
+       "reflect"
        "strconv"
+       "strings"
+       "time"
 )
 
 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n"
 
-// Non-standard MIME subtype see RFC 2045 and RFC 2046
-// TODO: verify that Content-Transfer-Encoding is not needed
-// TODO: text/epoint.cert would be shorter
+// (non-standard) MIME subtype for epoint documents, see RFC 2045 and RFC 2046
 var ContentType = map[string]string{
        "cert":  "text/plain.epoint.cert; charset=utf-8",
        "draft": "text/plain.epoint.draft; charset=utf-8",
@@ -47,61 +46,53 @@ type ClearSigned struct {
        ArmoredSignature []byte
 }
 
+type Field struct {
+       Key   string
+       Value string
+}
+
 // Draft document represents an obligation transfer order
 type Draft struct {
-       Drawer      string // ID of the payer (signer of the document)
-       Beneficiary string // ID of the payee
-       Amount      int64  // amount transfered
-       // TODO: issuer keys is enough?
+       Drawer       string `marshal:"id"` // ID of the payer (signer of the document)
+       Beneficiary  string `marshal:"id"` // ID of the payee
+       Amount       int64  // amount transfered
        Denomination string
-       // TODO: maturity date is enough if the only constraint is <= now
-       IssueDate    int64
-       MaturityDate int64 // Draft is bounced before this date
-       // TODO: implement limits
-       Notes string // Arbitrary text notes of the drawer
-       // TODO: hack to make signed draft unique (not required for DSA)
-       Nonce string // unique number
-       // TODO: server ID might change, do we need it?
-       Server string // ID of the server (drawee?)
-       //TODO: naming: drawee vs issuer
-       Drawee string // ID of the obligation issuer
-       // TODO: reference cert ids in the draft
+       IssueDate    int64 `marshal:"date" key:"Issue-Date"`
+       // Draft is bounced before this date
+       MaturityDate int64  `marshal:"date" key:"Maturity-Date"`
+       Notes        string // Arbitrary text notes of the drawer
+       Nonce        string // unique number
+       Server       string `marshal:"id"` // ID of the server
+       Drawee       string `marshal:"id"` // ID of the obligation issuer
        // useful if more strict date of issue information is needed
        //References []string
 }
 
-// TODO: cert references: fpr+serial, fpr+certID
-// Certification of obligation after a transfer
+// Obligation certificate after a transfer
 // References previous certificate (if any)
 // and the transfer related other documents
 type Cert struct {
-       Holder           string // ID of the creditor
+       Holder           string `marshal:"id"` // ID of the creditor
        Serial           uint32 // serial number, number of certs of the holder
-       Date             int64  // date of issue
+       Date             int64  `marshal:"date"` // date of issue
        Balance          int64  // current obligation value
        Denomination     string
-       Issuer           string // ID of the obligation issuer (drawee?)
-       LastDebitSerial  uint32 // serial of the last draft cert or 0
-       LastCreditSerial uint32 // serial of the last credit cert or 0
-       // TODO: move to References?
-       LastCert string // ID of the previous cert if any
-       // TODO: determine cert type from diff value only?
-       // (>0: credit cert, <0: debit cert, ==0: special)
-       Difference int64 // difference from previous balance
-       // TODO: enough on the debit side
-       Draft string // draft ID related to the transfer
-       // TODO: credit side, redundant references
-       Drawer       string // ID of the drawer in the transaction
-       DrawerSerial uint32 // serial of the drawer's related debit cert
-       DrawerCert   string // ID of the drawer's related debit cert
-       // TODO: fingerprint?
-       References []string // cert IDs for timestamping the system
+       Issuer           string   `marshal:"id"`                 // ID of the obligation issuer (drawee?)
+       LastDebitSerial  uint32   `key:"Last-Debit-Serial"`      // serial of the last draft cert or 0
+       LastCreditSerial uint32   `key:"Last-Credit-Serial"`     // serial of the last credit cert or 0
+       LastCert         string   `marshal:"id" key:"Last-Cert"` // ID of the previous cert if any
+       Difference       int64    // difference from previous balance
+       Draft            string   `marshal:"id"`                   // draft ID related to the transfer
+       Drawer           string   `marshal:"id"`                   // ID of the drawer in the transaction
+       DrawerSerial     uint32   `key:"Drawer-Serial"`            // serial of the drawer's related debit cert
+       DrawerCert       string   `marshal:"id" key:"Drawer-Cert"` // ID of the drawer's related debit cert
+       References       []string `marshal:"idlist"`               // cert IDs for timestamping the system
 }
 
 func DecodeClearSigned(s []byte) (c *ClearSigned, err error) {
        hash, body, sig := split(s)
        if len(sig) == 0 {
-               // TODO: split errors
+               err = fmt.Errorf("DecodeClearSigned could parse the signed document")
                return
        }
        c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
@@ -122,9 +113,7 @@ func EncodeClearSigned(c *ClearSigned) (data []byte, err error) {
        return
 }
 
-func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
-       // TODO: error
-       fields = make(map[string]string)
+func ParseFields(s []byte) (fields []Field, rest []byte, err error) {
        rest = s
        for len(rest) > 0 {
                var line []byte
@@ -134,153 +123,292 @@ func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
                        break
                }
                i := bytes.Index(line, []byte(": "))
-               // TODO: long lines can be broken up in MIME
                if i < 0 {
-                       return nil, nil, nil
+                       err = fmt.Errorf("ParseFields: missing ': '\n")
+                       break
                }
-               // TODO: repeated fields
-               fields[string(line[:i])] = string(line[i+2:])
+               fields = append(fields, Field{string(line[:i]), string(line[i+2:])})
        }
        return
 }
 
-func ParseBody(s []byte) (t string, fields map[string]string, err error) {
+func ParseBody(s []byte) (doctype string, fields []Field, err error) {
        // parse content type header first
-       mime, s, err := ParseFields(s)
-       if len(mime) != 1 {
+       fs, s, err := ParseFields(s)
+       if err != nil {
                return
        }
+       if len(fs) != 1 || fs[0].Key != "Content-Type" {
+               return "", nil, fmt.Errorf("ParseBody: single Content-Type header was expected\n")
+       }
+       ctype := fs[0].Value
        for k, v := range ContentType {
-               if mime["Content-Type"] == v {
-                       t = k
-                       fields, s, err = ParseFields(s)
-                       if len(s) > 0 {
-                               fields = nil
-                               break
-                       }
-                       return
+               if ctype == v {
+                       doctype = k
+                       break
                }
        }
-       // TODO: error
-       return
-}
-
-/* rendering with reflect
-func render(d interface{}) (s []byte, err error) {
-       a := []string{}
-       v := reflect.ValueOf(d)
-       t := v.Type()
-       n := v.NumField()
-       for i := 0; i < n; i++ {
-               f := t.Field(i)
-               fv := v.Field(i)
-               fs := ""
-               switch fv.Type() {
-               case reflect.String:
-                       fs = fv.String() // TODO: quote, esc (\n..)
-               case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-                       fs = strconv.Itoa64(fv.Int())
-               case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
-                       fs = strconv.Uitoa64(fv.Uint())
-               default:
-                       return // TODO: error
-               }
-               a = append(a, f.Name, ": ", fs, "\n")
+       if doctype == "" {
+               return "", nil, fmt.Errorf("ParseBody: unknown Content-Type: %s", ctype)
+       }
+       fields, s, err = ParseFields(s)
+       if err == nil && len(s) > 0 {
+               err = fmt.Errorf("ParseBody: extra data after fields: %q", s)
        }
-       s = strings.Join(a, "")
        return
 }
-*/
-
-func checkID(s string) (string, error) {
-       return s, nil
-}
 
-func ParseDraft(s []byte) (draft *Draft, err error) {
+func parse(s []byte, doctype string) (v interface{}, err error) {
        t, fields, err := ParseBody(s)
        if err != nil {
                return
        }
-       if t != "draft" {
+       if doctype != t && doctype != "" {
+               err = fmt.Errorf("parse: expected doctype %s; got %s", doctype, t)
+               return
+       }
+       switch t {
+       case "draft":
+               v = new(Draft)
+       case "cert":
+               v = new(Cert)
+       default:
+               err = fmt.Errorf("parse: unkown doc type: %s", t)
                return
        }
+       err = unmarshal(fields, v)
+       return
+}
 
-       draftFields := []string{
-               "Drawer",
-               "Beneficiary",
-               "Amount",
-               "Denomination",
-               "IssueDate",
-               "MaturityDate",
-               "Notes",
-               "Nonce",
-               "Server",
-               "Drawee",
+// TODO: limit errors
+func render(v interface{}) ([]byte, error) {
+       doctype := ""
+       switch v.(type) {
+       case *Draft:
+               doctype = "draft"
+       case *Cert:
+               doctype = "cert"
+       default:
+               panic("reder: unknown type")
+       }
+       s := "Content-Type: " + ContentType[doctype] + "\n\n"
+       fields := marshal(v)
+       for _, f := range fields {
+               s += f.Key + ": " + f.Value + "\n"
        }
-       if len(fields) != len(draftFields) {
+       return []byte(s), nil
+}
+
+func ParseDraft(s []byte) (draft *Draft, err error) {
+       v, err := parse(s, "draft")
+       if err != nil {
                return
        }
-       for _, f := range draftFields {
-               _, ok := fields[f]
-               if !ok {
-                       return
-               }
+       draft = v.(*Draft)
+       return
+}
+
+func RenderDraft(draft *Draft) ([]byte, error) {
+       return render(draft)
+}
+
+func ParseCert(s []byte) (cert *Cert, err error) {
+       v, err := parse(s, "cert")
+       if err != nil {
+               return nil, err
        }
+       cert = v.(*Cert)
+       return
+}
 
-       draft = new(Draft)
-       draft.Drawer, _ = checkID(fields["Drawer"])
-       draft.Beneficiary, _ = checkID(fields["Beneficiary"])
-       draft.Amount, _ = strconv.Atoi64(fields["Amount"])
-       draft.Denomination = fields["Denomination"]
-       draft.IssueDate, _ = strconv.Atoi64(fields["IssueDate"])
-       draft.MaturityDate, _ = strconv.Atoi64(fields["MaturityDate"])
-       draft.Notes = fields["Notes"]
-       draft.Nonce = fields["Nonce"]
-       draft.Server, _ = checkID(fields["Server"])
-       draft.Drawee, _ = checkID(fields["Drawee"])
+func RenderCert(cert *Cert) ([]byte, error) {
+       return render(cert)
+}
 
-       // more checks..
+func formatId(s string) string {
+       return fmt.Sprintf("%040X", s)
+}
 
-       return
+func parseId(s string) (string, error) {
+       dst := make([]byte, 20)
+       if len(s) != 40 {
+               return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
+       }
+       _, err := hex.Decode(dst, []byte(s))
+       return string(dst), err
 }
 
-func RenderDraft(draft *Draft) (data []byte, err error) {
-       s := "Content-Type: " + ContentType["draft"] + "\n"
-       s += "\n"
-       s += "Drawer: " + draft.Drawer + "\n"
-       s += "Beneficiary: " + draft.Beneficiary + "\n"
-       s += "Amount: " + strconv.Itoa64(draft.Amount) + "\n"
-       s += "Denomination: " + draft.Denomination + "\n"
-       s += "IssueDate: " + strconv.Itoa64(draft.IssueDate) + "\n"
-       s += "MaturityDate: " + strconv.Itoa64(draft.MaturityDate) + "\n"
-       s += "Notes: " + draft.Notes + "\n"
-       s += "Nonce: " + draft.Nonce + "\n"
-       s += "Server: " + draft.Server + "\n"
-       s += "Drawee: " + draft.Drawee + "\n"
-       data = []byte(s)
-       return
+func quoteValue(s string) string {
+       return s
 }
 
-func ParseCert(s []byte) (cert Cert, err error) {
-       return
+func unquoteValue(s string) (string, error) {
+       return s, nil
 }
 
-func RenderCert(cert Cert) (s []byte, err error) {
-       return
+func formatDate(i int64) string {
+       return time.SecondsToUTC(i).Format(time.RFC3339)
 }
 
-func splitline(s []byte) (line, rest []byte) {
-       i := bytes.IndexByte(s, '\n')
-       if i < 0 {
-               line = s
-       } else {
-               rest = s[i+1:]
-               if i > 0 && s[i-1] == '\r' {
-                       i--
+func parseDate(s string) (int64, error) {
+       t, err := time.Parse(time.RFC3339, s)
+       if err != nil {
+               return 0, err
+       }
+       return t.Seconds(), nil
+}
+
+func marshal(iv interface{}) []Field {
+       v := reflect.ValueOf(iv)
+       if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
+               panic("unmarshal: input is not a pointer to struct")
+       }
+       v = v.Elem()
+       t := v.Type()
+       n := v.NumField()
+       fields := []Field{}
+       for i := 0; i < n; i++ {
+               ft := t.Field(i)
+               fv := v.Field(i)
+               m := ft.Tag.Get("marshal")
+               k := ft.Tag.Get("key")
+               if k == "" {
+                       k = ft.Name
+               }
+               val := ""
+               switch fv.Kind() {
+               case reflect.String:
+                       switch m {
+                       case "id":
+                               val = formatId(fv.String())
+                       case "":
+                               val = quoteValue(fv.String())
+                       default:
+                               panic("bad string field tag")
+                       }
+               case reflect.Int, reflect.Int32, reflect.Int64:
+                       switch m {
+                       case "date":
+                               val = formatDate(fv.Int())
+                       case "":
+                               val = strconv.Itoa64(fv.Int())
+                       default:
+                               panic("bad int field tag")
+                       }
+               case reflect.Uint, reflect.Uint32, reflect.Uint64:
+                       switch m {
+                       case "":
+                               val = strconv.Uitoa64(fv.Uint())
+                       default:
+                               panic("bad uint field tag")
+                       }
+               case reflect.Slice:
+                       switch m {
+                       case "idlist":
+                               if fv.Type().Elem().Kind() != reflect.String {
+                                       panic("only string slice is supported")
+                               }
+                               k := fv.Len()
+                               for j := 0; j < k; j++ {
+                                       if j > 0 {
+                                               val += " "
+                                       }
+                                       val += formatId(fv.Index(j).String())
+                               }
+                       default:
+                               panic("bad slice field tag")
+                       }
+               default:
+                       panic("bad field type")
                }
-               line = s[:i]
+               fields = append(fields, Field{k, val})
        }
-       return
+       return fields
+}
+
+func unmarshal(fields []Field, iv interface{}) error {
+       v := reflect.ValueOf(iv)
+       if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
+               panic("unmarshal: input is not a pointer to struct")
+       }
+       v = v.Elem()
+       t := v.Type()
+       n := v.NumField()
+       if len(fields) != n {
+               return fmt.Errorf("unmarshal: %s has %d fields, got %d\n", t.Name(), n, len(fields))
+       }
+       for i := 0; i < n; i++ {
+               ft := t.Field(i)
+               fv := v.Field(i)
+               m := ft.Tag.Get("marshal")
+               k := ft.Tag.Get("key")
+               if k == "" {
+                       k = ft.Name
+               }
+               if fields[i].Key != k {
+                       return fmt.Errorf("unmarshal: field %d of %s (%s) is missing\n", i, t.Name(), k)
+               }
+               s := fields[i].Value
+               var err error
+               switch fv.Kind() {
+               case reflect.String:
+                       var val string
+                       switch m {
+                       case "id":
+                               val, err = parseId(s)
+                       case "":
+                               val, err = unquoteValue(s)
+                       default:
+                               panic("bad string field tag")
+                       }
+                       fv.SetString(val)
+               case reflect.Int, reflect.Int32, reflect.Int64:
+                       var val int64
+                       switch m {
+                       case "date":
+                               val, err = parseDate(s)
+                       case "":
+                               val, err = strconv.Atoi64(s)
+                       default:
+                               panic("bad int field tag")
+                       }
+                       fv.SetInt(val)
+               case reflect.Uint, reflect.Uint32, reflect.Uint64:
+                       var val uint64
+                       switch m {
+                       case "":
+                               val, err = strconv.Atoui64(s)
+                       default:
+                               panic("bad uint field tag")
+                       }
+                       fv.SetUint(val)
+               case reflect.Slice:
+                       var val []string
+                       switch m {
+                       case "idlist":
+                               if fv.Type().Elem().Kind() != reflect.String {
+                                       panic("only string slice is supported")
+                               }
+                               ids := strings.Split(s, " ")
+                               val = make([]string, len(ids))
+                               for j := range val {
+                                       val[j], err = parseId(ids[j])
+                                       if err != nil {
+                                               return err
+                                       }
+                               }
+                       default:
+                               panic("bad slice field tag")
+                       }
+                       fv.Set(reflect.ValueOf(val))
+               default:
+                       panic("bad field type")
+               }
+               if err != nil {
+                       return err
+               }
+       }
+       return nil
 }
 
 func getLine(data []byte) (line, rest []byte) {
index 45cac1f..8c3413e 100644 (file)
@@ -59,17 +59,17 @@ sig
 -----END PGP SIGNATURE-----
 `)}
 
-var F = map[string]string{
-       "A": "foo",
-       "B": "bar",
-       "a": "baz",
+var F = []Field{
+       {"A", "foo"},
+       {"B", "bar"},
+       {"a", "baz"},
 }
 
 var testData = []struct {
        D []byte
        C *ClearSigned
        T string
-       F map[string]string
+       F []Field
 }{
        {[]byte(D1), C1, "cert", F},
        {[]byte(D2), C2, "cert", F},
@@ -81,13 +81,16 @@ func eqClearSigned(c1, c2 *ClearSigned) bool {
                string(c1.ArmoredSignature) == string(c2.ArmoredSignature)
 }
 
-func eqFields(f1, f2 map[string]string) bool {
-       for k,v := range f1 {
-               if f2[k] != v {
+func eqFields(f1, f2 []Field) bool {
+       if len(f1) != len(f2) {
+               return false
+       }
+       for i, v := range f1 {
+               if f2[i].Key != v.Key && f2[i].Value != v.Value {
                        return false
                }
        }
-       return len(f1) == len(f2)
+       return true
 }
 
 func TestClearSigned(t *testing.T) {
@@ -131,28 +134,63 @@ func TestParse(t *testing.T) {
 
 const draftBody = `Content-Type: text/plain.epoint.draft; charset=utf-8
 
-Drawer: 000000000000000000000000000000000000000a
-Beneficiary: 000000000000000000000000000000000000000b
+Drawer: 000000000000000000000000000000000000000A
+Beneficiary: 000000000000000000000000000000000000000B
 Amount: 1
 Denomination: half euro
-IssueDate: 2
-MaturityDate: 3
+Issue-Date: 2011-11-13T12:20:35Z
+Maturity-Date: 2011-12-27T09:18:46Z
 Notes: some notes
 Nonce: 42
-Server: 000000000000000000000000000000000000000c
-Drawee: 000000000000000000000000000000000000000d
+Server: 000000000000000000000000000000000000000C
+Drawee: 000000000000000000000000000000000000000D
 `
 
 func TestDraft(t *testing.T) {
        d, err := ParseDraft([]byte(draftBody))
        if err != nil {
-               t.Errorf("parse %q draft failed: %s\n", draftBody, err)
+               t.Errorf("ParseDraft failed: %s\n", err)
+               return
        }
        s, err := RenderDraft(d)
        if err != nil {
                t.Errorf("render %v draft failed: %s\n", d, err)
        }
        if string(s) != draftBody {
-               t.Errorf("parsed %#v, expected %#v\n", d, draftBody)
+               t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", d, draftBody, s)
+       }
+}
+
+const certBody = `Content-Type: text/plain.epoint.cert; charset=utf-8
+
+Holder: 000000000000000000000000000000000000000A
+Serial: 13
+Date: 2011-11-01T10:29:38Z
+Balance: 23
+Denomination: half euro
+Issuer: 000000000000000000000000000000000000000B
+Last-Debit-Serial: 0
+Last-Credit-Serial: 12
+Last-Cert: 000000000000000000000000000000000000000C
+Difference: 1
+Draft: 000000000000000000000000000000000000000D
+Drawer: 000000000000000000000000000000000000000E
+Drawer-Serial: 2
+Drawer-Cert: 000000000000000000000000000000000000000F
+References: 000000000000000000000000000000000000000C 000000000000000000000000000000000000000F
+`
+
+func TestCert(t *testing.T) {
+       c, err := ParseCert([]byte(certBody))
+       if err != nil {
+               t.Errorf("ParseCert failed: %s\n", err)
+               return
+       }
+       s, err := RenderCert(c)
+       if err != nil {
+               t.Errorf("render %v cert failed: %s\n", c, err)
+       }
+       if string(s) != certBody {
+               t.Errorf("parsed %#v\nexpected: %s\ngot: %s\n", c, certBody, s)
        }
 }