From 0f67afa98dad4de872810235afb541fe2dcfe5c4 Mon Sep 17 00:00:00 2001 From: nsz Date: Sun, 13 Nov 2011 19:37:58 +0100 Subject: [PATCH] use reflect for document parsing --- document/document.go | 436 ++++++++++++++++++++++++-------------- document/document_test.go | 72 +++++-- 2 files changed, 337 insertions(+), 171 deletions(-) diff --git a/document/document.go b/document/document.go index 46f41ab..afbe31c 100644 --- a/document/document.go +++ b/document/document.go @@ -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) { diff --git a/document/document_test.go b/document/document_test.go index 45cac1f..8c3413e 100644 --- a/document/document_test.go +++ b/document/document_test.go @@ -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) } } -- 2.20.1