initial solution for optional fields
authornsz <nsz@port70.net>
Wed, 23 Nov 2011 17:02:38 +0000 (18:02 +0100)
committernsz <nsz@port70.net>
Wed, 23 Nov 2011 17:02:38 +0000 (18:02 +0100)
document/document.go

index c410f61..1b87bf6 100644 (file)
 // -----END PGP SIGNATURE-----
 package document
 
-// TODO: error types
-// TODO: optional fields
-// TODO: fields of notice (last notice, serial)
-// TODO: space vs not to space
+// TODO: error wrapper (so reporting to user or creating bounce cert is simple)
+// TODO: optional fields: exact semantics ("" vs "-" vs nil)
+// TODO: trailing space handling in ParseFields
+// TODO: fields of notice (last notice, serial, failure notice,..)
+// TODO: limits and cert type specific input validation
 
 import (
        "bytes"
@@ -45,6 +46,15 @@ import (
        "time"
 )
 
+// limits
+const (
+       MaxFields             = 20
+       MaxLineLength         = 160  // 1 sha512 + 1 key (without \n)
+       MaxValueLength        = 1300 // 20 sha256 space separated (without \n)
+       MaxNonceLength        = 20
+       MaxDenominationLength = 100
+)
+
 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----"
 
 // MIME type for epoint documents, see RFC 4288
@@ -115,17 +125,17 @@ type Draft struct {
        Denomination string
        Issuer       string
        AuthorizedBy string
-       MaturityDate int64  // optional
-       ExpiryDate   int64  // optional
-       Nonce        string // optional
-       Notes        string // optional
+       MaturityDate *int64  // optional
+       ExpiryDate   *int64  // optional
+       Nonce        *string // optional
+       Notes        *string // optional
 }
 
 type Notice struct {
        Date         int64
        AuthorizedBy string
-       Notes        string
-       References   []string
+       Notes        *string  // optional
+       References   []string // may be empty (startup notice)
 }
 
 type DebitCert struct {
@@ -138,11 +148,11 @@ type DebitCert struct {
        Difference       int64
        Draft            string
        AuthorizedBy     string
-       Notes            string   // optional
-       LastDebitSerial  int64    // 0 if none
-       LastCreditSerial int64    // 0 if none
-       LastCert         string   // ? if serial == 1
-       References       []string // may be empty
+       Notes            *string // optional
+       LastDebitSerial  int64   // 0 if none
+       LastCreditSerial int64   // 0 if none
+       LastCert         *string // ? if serial == 1
+       References       []string
 }
 
 type CreditCert struct {
@@ -157,24 +167,22 @@ type CreditCert struct {
        Drawer           string
        DebitCert        string
        AuthorizedBy     string
-       Notes            string   // optional
-       LastDebitSerial  int64    // 0 if none
-       LastCreditSerial int64    // 0 if none
-       LastCert         string   // ? if serial == 1
-       References       []string // may be empty
+       Notes            *string // optional
+       LastDebitSerial  int64   // 0 if none
+       LastCreditSerial int64   // 0 if none
+       LastCert         *string // ? if serial == 1
+       References       []string
 }
 
 type BounceCert struct {
        Drawer       string
        Draft        string
-       LastCert     string // optional
-       Balance      int64  // 0 if none
-       Denomination string
-       Issuer       string
+       LastCert     *string // optional
+       Balance      int64   // 0 if none
        Date         int64
        AuthorizedBy string
-       Notes        string   // optional
-       References   []string // may be empty
+       Notes        *string // optional
+       References   []string
 }
 
 // parse an epoint document without checking the signature and format details
@@ -421,6 +429,7 @@ func ParseStruct(doc *Document) (iv interface{}, err error) {
        v := reflect.ValueOf(iv).Elem()
        t := v.Type()
        n := v.NumField()
+       nokey := 0
        for i := 0; i < n; i++ {
                ft := t.Field(i)
                fv := v.Field(i)
@@ -431,8 +440,22 @@ func ParseStruct(doc *Document) (iv interface{}, err error) {
                seen[key] = true
                s, ok := doc.Fields[key]
                if !ok {
+                       if fv.Kind() == reflect.Ptr {
+                               // missing optional key: leave the pointer as nil
+                               nokey++
+                               continue
+                       }
                        return nil, fmt.Errorf("ParseStruct: field %s of %s is missing\n", key, t.Name())
                }
+               if fv.Kind() == reflect.Ptr {
+                       if s == "" || s == "-" {
+                               // TODO
+                               // empty optional key: same as missing
+                               continue
+                       }
+                       fv.Set(reflect.New(fv.Type().Elem()))
+                       fv = fv.Elem()
+               }
                switch fieldtype[key] {
                case "id":
                        var val string
@@ -451,6 +474,7 @@ func ParseStruct(doc *Document) (iv interface{}, err error) {
                        val, err = parseDate(s)
                        fv.SetInt(val)
                case "ids":
+                       // TODO: empty slice?
                        ids := strings.Split(s, " ")
                        val := make([]string, len(ids))
                        for j, id := range ids {
@@ -467,7 +491,7 @@ func ParseStruct(doc *Document) (iv interface{}, err error) {
                        return
                }
        }
-       if len(doc.Fields) != n {
+       if len(doc.Fields)+nokey != n {
                for k := range doc.Fields {
                        if !seen[k] {
                                err = fmt.Errorf("ParseStruct: unknown field %s in %s", k, t.Name())
@@ -498,6 +522,14 @@ func FormatStruct(iv interface{}) (doc *Document, err error) {
                        key = ft.Name
                }
                val := ""
+               if fv.Kind() == reflect.Ptr {
+                       if fv.IsNil() {
+                               // keep empty optional fields but mark them
+                               val = "-"
+                               goto setval
+                       }
+                       fv = fv.Elem()
+               }
                switch fieldtype[key] {
                case "id":
                        val = formatId(fv.String())
@@ -518,6 +550,7 @@ func FormatStruct(iv interface{}) (doc *Document, err error) {
                default:
                        panic("bad field type " + key + " " + fieldtype[key])
                }
+       setval:
                doc.Fields[key] = val
                doc.Order = append(doc.Order, key)
        }
@@ -527,35 +560,49 @@ func FormatStruct(iv interface{}) (doc *Document, err error) {
 func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
        rest = s
        fields = make(map[string]string)
-       lastkey := ""
+       key := ""
+       // \n is optional after the last field and an extra \n is allowed as well
        for len(rest) > 0 {
                var line []byte
                line, rest = getLine(rest)
-               // empty line after the parsed fields is consumed
+               // empty line after the last field is consumed
                if len(line) == 0 {
                        break
                }
-               // TODO: empty line: " \n"
-               if line[0] == ' ' {
-                       if lastkey == "" {
-                               err = fmt.Errorf("ParseFields: expected a field, not ' '")
-                               return
-                       }
-                       fields[lastkey] += string(line)
+               if line[0] == ' ' && key != "" {
+                       // "Key: v1\n v2\n" is equivalent to "Key: v1 v2\n"
+                       fields[key] += string(line)
                        continue
                }
-               // TODO: empty value: "Key: \n"
-               i := bytes.Index(line, []byte(": "))
+               if line[0] < 'A' || line[0] > 'Z' {
+                       err = fmt.Errorf("ParseFields: field name must start with an upper-case ascii letter")
+                       return
+               }
+               i := bytes.IndexByte(line, ':')
                if i < 0 {
-                       err = fmt.Errorf("ParseFields: missing ': '")
-                       break
+                       err = fmt.Errorf("ParseFields: missing ':'")
+                       return
                }
-               lastkey = string(line[:i])
-               if _, ok := fields[lastkey]; ok {
+               key = string(line[:i])
+               if _, ok := fields[key]; ok {
                        err = fmt.Errorf("ParseFields: repeated fields are not allowed")
                        return
                }
-               fields[lastkey] = string(line[i+2:])
+               fields[key] = string(line[i+1:])
+       }
+       for key, v := range fields {
+               // either a single space follows ':' or the value is empty
+               // good: "Key:\n", "Key:\n value\n", "Key: value\n", "Key: v1\n v2\n"
+               // bad: "Key:value\n", "Key: \nvalue\n"
+               // bad but not checked here: "Key: \n", "Key: value \n", "Key:\n \n value\n"
+               if len(v) == 0 {
+                       continue
+               }
+               if v[0] != ' ' {
+                       err = fmt.Errorf("ParseFields: ':' is not followed by ' '")
+                       return
+               }
+               fields[key] = v[1:]
        }
        return
 }
@@ -563,9 +610,8 @@ func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
 // TODO: limit errors
 
 func parseId(s string) (string, error) {
-       if len(s) != 40 {
-               return "", fmt.Errorf("parseId: expected 40 characters; got %d", len(s))
-       }
+       // check if hex decodable
+       // TODO: length check
        dst := make([]byte, len(s)/2)
        _, err := hex.Decode(dst, []byte(s))
        return s, err
@@ -576,8 +622,8 @@ func formatId(s string) string {
 }
 
 func parseString(s string) (string, error) {
-       if len(s) > 140 {
-               return "", fmt.Errorf("parseString: 140 chars limit is exceeded")
+       if len(s) > MaxValueLength {
+               return "", fmt.Errorf("parseString: length limit is exceeded")
        }
        return s, nil
 }
@@ -587,6 +633,7 @@ func formatString(s string) string {
 }
 
 func parseDate(s string) (int64, error) {
+       // TODO: fractional seconds?
        t, err := time.Parse(time.RFC3339, s)
        if err != nil {
                return 0, err
@@ -599,7 +646,7 @@ func formatDate(i int64) string {
 }
 
 func getLine(data []byte) (line, rest []byte) {
-       i := bytes.Index(data, []byte{'\n'})
+       i := bytes.IndexByte(data, '\n')
        j := i + 1
        if i < 0 {
                i = len(data)