1package app23import (4 "encoding/csv"5 "errors"6 "strconv"7 "strings"8 "unicode/utf8"9)1011// The wire format is one CSV record.12//13// CSV because it is in the standard library, because it quotes correctly --14// a task called `buy milk, eggs` survives a round trip, which a naive split15// on commas does not -- and because it is writable by hand on a gnokey16// command line. Fields are positional, in the order the verb declares them,17// which is why the schema forbids a required parameter after an optional one18// and why Diff treats a reordering as breaking.19//20// Encode(addTask, "buy milk, eggs") -> "buy milk, eggs" (quoted by csv)21// Encode(transfer, "g1abc…", "42") -> "g1abc…,42"22//23// A verb with no parameters takes an empty payload.2425// Args is a decoded, validated payload. A handler receives one and reads26// fields by name; the app has already checked that every field is present and27// parses as its declared kind, so the accessors do not return errors.28type Args struct {29 verb Verb30 values []string31}3233// Verb returns the name of the verb being called.34func (a *Args) Verb() string { return a.verb.Name }3536// Signature returns the declaration this payload was checked against, which37// is worth putting in a panic message.38func (a *Args) Signature() string { return a.verb.Signature() }3940// Raw returns the decoded fields in declaration order, for a handler that41// would rather loop than name them.42func (a *Args) Raw() []string {43 dup := make([]string, len(a.values))44 copy(dup, a.values)45 return dup46}4748// index returns the position of name and whether a value was supplied.49// It panics if the verb has no such parameter: that is the handler asking for50// something it did not declare, which no payload can fix.51func (a *Args) index(name string, want Kind) (int, bool) {52 for i, p := range a.verb.Params {53 if p.Name != name {54 continue55 }56 if want != "" && p.Kind != want {57 panic(errors.New("app: " + a.verb.Name + ": parameter " + name +58 " is declared " + string(p.Kind) + ", read as " + string(want)))59 }60 return i, i < len(a.values)61 }62 panic(errors.New("app: " + a.verb.Name + " has no parameter " + name +63 "; its signature is " + a.verb.Signature()))64}6566// Has reports whether an optional parameter was supplied.67func (a *Args) Has(name string) bool {68 _, ok := a.index(name, "")69 return ok70}7172// String returns a string parameter, or "" if it was optional and omitted.73func (a *Args) String(name string) string {74 i, ok := a.index(name, KindString)75 if !ok {76 return ""77 }78 return a.values[i]79}8081// Int returns an int parameter, or 0 if it was optional and omitted.82func (a *Args) Int(name string) int64 {83 i, ok := a.index(name, KindInt)84 if !ok {85 return 086 }87 n, _ := strconv.ParseInt(a.values[i], 10, 64) // checked at decode88 return n89}9091// Bool returns a bool parameter, or false if it was optional and omitted.92func (a *Args) Bool(name string) bool {93 i, ok := a.index(name, KindBool)94 if !ok {95 return false96 }97 return a.values[i] == "true"98}99100// Address returns an address parameter, or the zero address if it was101// optional and omitted.102func (a *Args) Address(name string) address {103 i, ok := a.index(name, KindAddress)104 if !ok {105 return ""106 }107 return address(a.values[i])108}109110// Encode builds a payload for v from values given in declaration order,111// validating each against its declared kind. Use it when one realm calls112// another app, so an encoding mistake fails where it was made.113func Encode(v Verb, values ...string) string {114 if err := checkArity(v, len(values)); err != nil {115 panic(err)116 }117 for i, val := range values {118 if err := v.Params[i].Kind.parse(val); err != nil {119 panic(fieldErr(v, i, err))120 }121 }122 if len(values) == 0 {123 return ""124 }125 // A lone empty field encodes to nothing at all -- csv does not quote an126 // empty field, so the record "" and the record with no fields are the127 // same bytes. Quote it explicitly so the payload says which it is.128 if len(values) == 1 && values[0] == "" {129 return `""`130 }131 // Fast path: when nothing needs quoting, a CSV record IS the fields132 // joined by commas, and building a csv.Writer to discover that costs133 // more than the whole rest of the call. See needsQuotes.134 plain := true135 for _, val := range values {136 if needsQuotes(val) {137 plain = false138 break139 }140 }141 if plain {142 return strings.Join(values, ",")143 }144145 var b strings.Builder146 w := csv.NewWriter(&b)147 if err := w.Write(values); err != nil {148 panic(errors.New("app: " + v.Name + ": " + err.Error()))149 }150 w.Flush()151 if err := w.Error(); err != nil {152 panic(errors.New("app: " + v.Name + ": " + err.Error()))153 }154 return strings.TrimRight(b.String(), "\n")155}156157// splitPlain splits a payload on commas in a single pass, reporting false the158// moment it meets a byte that gives commas a different meaning. The one pass159// is the point: bailing out and splitting separately doubles the interpreted160// work over the whole payload.161func splitPlain(payload string) ([]string, bool) {162 out := []string{}163 start := 0164 for i := 0; i < len(payload); i++ {165 switch payload[i] {166 case '"', '\r', '\n':167 return nil, false168 case ',':169 out = append(out, payload[start:i])170 start = i + 1171 }172 }173 return append(out, payload[start:]), true174}175176// needsQuotes mirrors csv.Writer's rule exactly. Diverging from it would mean177// Encode produced something csv.Reader reads back differently, so this is178// copied rather than approximated: a field is quoted when it is the literal179// `\.`, when it contains a comma, quote, CR or LF, or when it begins with a180// space.181func needsQuotes(field string) bool {182 if field == "" {183 return false184 }185 if field == `\.` {186 return true187 }188 switch field[0] {189 case ' ', '\t', '\v', '\f':190 return true191 }192 // A leading non-ASCII space (U+00A0 and friends) is rare enough to hand193 // to the slow path rather than decode a rune for on every call.194 if field[0] >= utf8.RuneSelf {195 return true196 }197 // One pass, for the same reason splitPlain is one pass:198 // strings.ContainsAny is interpreted Gno and costs more over a short199 // field than the csv.Writer it is trying to avoid.200 for i := 0; i < len(field); i++ {201 switch field[i] {202 case ',', '"', '\r', '\n':203 return true204 }205 }206 return false207}208209// checkArity reports whether n values can satisfy v.210func checkArity(v Verb, n int) error {211 req := v.Required()212 if n < req {213 return errors.New("app: " + v.Name + " needs at least " + strconv.Itoa(req) +214 " parameter(s), got " + strconv.Itoa(n) + "; signature is " + v.Signature())215 }216 if n > len(v.Params) {217 return errors.New("app: " + v.Name + " takes at most " + strconv.Itoa(len(v.Params)) +218 " parameter(s), got " + strconv.Itoa(n) + "; signature is " + v.Signature())219 }220 return nil221}222223// fieldErr names the verb, the parameter and the signature, which is what a224// caller needs to fix the payload without reading the handler's source.225func fieldErr(v Verb, i int, err error) error {226 return errors.New("app: " + v.Name + ": parameter " + v.Params[i].Name +227 ": " + err.Error() + "; signature is " + v.Signature())228}229230// decode parses and validates a payload against v.231func decode(v Verb, payload string) (*Args, error) {232 values := []string{}233 if payload == "" {234 // Ambiguous on its own: no fields, or one empty field. The verb235 // decides. A required first parameter means an empty payload is that236 // parameter, empty -- which is what a gnokey caller passing "" for a237 // string means, and Encode spells `""` for the same case.238 if v.Required() >= 1 {239 values = []string{""}240 }241 } else if rec, plain := splitPlain(payload); plain {242 // Fast path: with no quote and no newline anywhere, no field is243 // quoted, so every comma is a separator and every field is literal244 // -- exactly what csv.Reader would return. splitPlain decides that245 // and does the split in one pass, because every string operation246 // here is interpreted Gno: two passes with strings.ContainsAny and247 // strings.Split costs more than the csv parse it replaces.248 values = rec249 } else {250 rec, err := csv.NewReader(strings.NewReader(payload)).Read()251 if err != nil {252 return nil, errors.New("app: " + v.Name + ": malformed payload: " + err.Error())253 }254 values = rec255 }256 if err := checkArity(v, len(values)); err != nil {257 return nil, err258 }259 for i, val := range values {260 if err := v.Params[i].Kind.parse(val); err != nil {261 return nil, fieldErr(v, i, err)262 }263 }264 return &Args{verb: v, values: values}, nil265}266Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.