// -----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"
"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
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 {
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 {
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
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)
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
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 {
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())
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())
default:
panic("bad field type " + key + " " + fieldtype[key])
}
+ setval:
doc.Fields[key] = val
doc.Order = append(doc.Order, key)
}
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
}
// 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
}
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
}
}
func parseDate(s string) (int64, error) {
+ // TODO: fractional seconds?
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return 0, err
}
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)