X-Git-Url: http://nsz.repo.hu/git/?a=blobdiff_plain;f=document%2Fdocument.go;fp=document%2Fdocument.go;h=1b87bf6eee211c78d671e092243ec92013915f40;hb=7abbc4cf090bacfd8c4bb56f70e5ad1dff810b6a;hp=c410f61833f7c675b8c1c46b4525d5a9e9a87dd3;hpb=75502b9220b35cdc4fe15977f8aeb794535bcd35;p=epoint diff --git a/document/document.go b/document/document.go index c410f61..1b87bf6 100644 --- a/document/document.go +++ b/document/document.go @@ -26,10 +26,11 @@ // -----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)