// Copyright 2021 CUE Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package textproto import ( "fmt" "strings" "cuelang.org/go/cue" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/errors" "cuelang.org/go/cue/literal" "cuelang.org/go/cue/token" "cuelang.org/go/encoding/protobuf/pbinternal" "cuelang.org/go/internal/core/adt" "cuelang.org/go/internal/value" pbast "github.com/protocolbuffers/txtpbfmt/ast" "github.com/protocolbuffers/txtpbfmt/parser" "github.com/protocolbuffers/txtpbfmt/unquote" ) // Option defines options for the decoder. // There are currently no options. type Option func(*options) type options struct { } // NewDecoder returns a new Decoder func NewDecoder(option ...Option) *Decoder { d := &Decoder{} _ = d.m // work around linter bug. return d } // A Decoder caches conversions of cue.Value between calls to its methods. type Decoder struct { m map[*adt.Vertex]*mapping } type decoder struct { *Decoder // Reset on each call errs errors.Error file *token.File } // Parse parses the given textproto bytes and converts them to a CUE expression, // using schema as the guideline for conversion using the following rules: // // - the @protobuf attribute is optional, but is necessary for: // - interpreting protobuf maps // - using a name different from the CUE name // - fields in the textproto that have no corresponding field in // schema are ignored // // NOTE: the filename is used for associating position information. However, // currently no position information is associated with the text proto because // the position information of github.com/protocolbuffers/txtpbfmt is too // unreliable to be useful. func (d *Decoder) Parse(schema cue.Value, filename string, b []byte) (ast.Expr, error) { dec := decoder{Decoder: d} // dec.errs = nil f := token.NewFile(filename, -1, len(b)) f.SetLinesForContent(b) dec.file = f cfg := parser.Config{} nodes, err := parser.ParseWithConfig(b, cfg) if err != nil { return nil, errors.Newf(token.NoPos, "textproto: %v", err) } m := dec.parseSchema(schema) if dec.errs != nil { return nil, dec.errs } n := dec.decodeMsg(m, nodes) if dec.errs != nil { return nil, dec.errs } return n, nil } // Don't expose until the protobuf APIs settle down. // func (d *decoder) Decode(schema cue.Value, textpbfmt) (cue.Value, error) { // } type mapping struct { children map[string]*fieldInfo } type fieldInfo struct { pbinternal.Info msg *mapping // keytype, for now } func (d *decoder) addErr(err error) { d.errs = errors.Append(d.errs, errors.Promote(err, "textproto")) } func (d *decoder) addErrf(pos pbast.Position, format string, args ...interface{}) { err := errors.Newf(d.protoPos(pos), "textproto: "+format, args...) d.errs = errors.Append(d.errs, err) } func (d *decoder) protoPos(p pbast.Position) token.Pos { return d.file.Pos(int(p.Byte), token.NoRelPos) } // parseSchema walks over a CUE "type", converts it to an internal data // structure that is used for parsing text proto, and writes it to func (d *decoder) parseSchema(schema cue.Value) *mapping { _, v := value.ToInternal(schema) if v == nil { return nil } if d.m == nil { d.m = map[*adt.Vertex]*mapping{} } else if m := d.m[v]; m != nil { return m } m := &mapping{children: map[string]*fieldInfo{}} i, err := schema.Fields() if err != nil { d.addErr(err) return nil } for i.Next() { info, err := pbinternal.FromIter(i) if err != nil { d.addErr(err) continue } var msg *mapping switch info.CompositeType { case pbinternal.Normal: switch info.ValueType { case pbinternal.Message: msg = d.parseSchema(i.Value()) } case pbinternal.List, pbinternal.Map: e, _ := i.Value().Elem() if e.IncompleteKind() == cue.StructKind { msg = d.parseSchema(e) } } m.children[info.Name] = &fieldInfo{ Info: info, msg: msg, } } d.m[v] = m return m } func (d *decoder) decodeMsg(m *mapping, n []*pbast.Node) ast.Expr { st := &ast.StructLit{} var listMap map[string]*ast.ListLit for _, x := range n { if x.Values == nil && x.Children == nil { if cg := addComments(x.PreComments...); cg != nil { ast.SetRelPos(cg, token.NewSection) st.Elts = append(st.Elts, cg) continue } } if m == nil { continue } f, ok := m.children[x.Name] if !ok { continue // ignore unknown fields } var value ast.Expr switch f.CompositeType { default: value = d.decodeValue(f, x) case pbinternal.List: if listMap == nil { listMap = make(map[string]*ast.ListLit) } list := listMap[f.CUEName] if list == nil { list = &ast.ListLit{} listMap[f.CUEName] = list value = list } if len(x.Values) == 1 || f.ValueType == pbinternal.Message { v := d.decodeValue(f, x) if value == nil { if cg := addComments(x.PreComments...); cg != nil { cg.Doc = true ast.AddComment(v, cg) } } if cg := addComments(x.PostValuesComments...); cg != nil { cg.Position = 4 ast.AddComment(v, cg) } list.Elts = append(list.Elts, v) break } var last ast.Expr // Handle [1, 2, 3] for _, v := range x.Values { if v.Value == "" { if cg := addComments(v.PreComments...); cg != nil { if last != nil { cg.Position = 4 ast.AddComment(last, cg) } else { cg.Position = 1 ast.AddComment(list, cg) } } continue } y := *x y.Values = []*pbast.Value{v} last = d.decodeValue(f, &y) list.Elts = append(list.Elts, last) } if cg := addComments(x.PostValuesComments...); cg != nil { if last != nil { cg.Position = 4 ast.AddComment(last, cg) } else { cg.Position = 1 ast.AddComment(list, cg) } } if cg := addComments(x.ClosingBraceComment); cg != nil { cg.Position = 4 ast.AddComment(list, cg) } case pbinternal.Map: // mapValue: { // key: 123 // value: "string" // } if k := len(x.Values); k > 0 { d.addErrf(x.Start, "values not allowed for Message type; found %d", k) } var ( key ast.Label val ast.Expr ) for _, c := range x.Children { if len(c.Values) != 1 { d.addErrf(x.Start, "expected 1 value, found %d", len(c.Values)) continue } s := c.Values[0].Value switch c.Name { case "key": if strings.HasPrefix(s, `"`) { key = &ast.BasicLit{Kind: token.STRING, Value: s} } else { key = ast.NewString(s) } case "value": val = d.decodeValue(f, c) if cg := addComments(x.ClosingBraceComment); cg != nil { cg.Line = true ast.AddComment(val, cg) } default: d.addErrf(c.Start, "unsupported key name %q in map", c.Name) continue } } if key != nil && val != nil { value = ast.NewStruct(key, val) } } if value != nil { var label ast.Label if s := f.CUEName; ast.IsValidIdent(s) { label = ast.NewIdent(s) } else { label = ast.NewString(s) } // TODO: convert line number information. However, position // information in textpbfmt packages is too wonky to be useful f := &ast.Field{ Label: label, Value: value, // Attrs: []*ast.Attribute{{Text: f.attr.}}, } if cg := addComments(x.PreComments...); cg != nil { cg.Doc = true ast.AddComment(f, cg) } st.Elts = append(st.Elts, f) } } return st } func addComments(lines ...string) (cg *ast.CommentGroup) { var a []*ast.Comment for _, c := range lines { if !strings.HasPrefix(c, "#") { continue } a = append(a, &ast.Comment{Text: "//" + c[1:]}) } if a != nil { cg = &ast.CommentGroup{List: a} } return cg } func (d *decoder) decodeValue(f *fieldInfo, n *pbast.Node) (x ast.Expr) { if f.ValueType == pbinternal.Message { if k := len(n.Values); k > 0 { d.addErrf(n.Start, "values not allowed for Message type; found %d", k) } x = d.decodeMsg(f.msg, n.Children) if cg := addComments(n.ClosingBraceComment); cg != nil { cg.Line = true cg.Position = 4 ast.AddComment(x, cg) } return x } if len(n.Values) != 1 { d.addErrf(n.Start, "expected 1 value, found %d", len(n.Values)) return nil } v := n.Values[0] defer func() { if cg := addComments(v.PreComments...); cg != nil { cg.Doc = true ast.AddComment(x, cg) } if cg := addComments(v.InlineComment); cg != nil { cg.Line = true cg.Position = 2 ast.AddComment(x, cg) } }() switch f.ValueType { case pbinternal.String, pbinternal.Bytes: s, err := unquote.Unquote(n) if err != nil { d.addErrf(n.Start, "invalid string or bytes: %v", err) } if f.ValueType == pbinternal.String { s = literal.String.Quote(s) } else { s = literal.Bytes.Quote(s) } return &ast.BasicLit{Kind: token.STRING, Value: s} case pbinternal.Bool: switch v.Value { case "true": return ast.NewBool(true) case "false": default: d.addErrf(n.Start, "invalid bool %s", v.Value) } return ast.NewBool(false) case pbinternal.Int, pbinternal.Float: s := v.Value switch s { case "inf", "nan": // TODO: include message. return &ast.BottomLit{} } var info literal.NumInfo if err := literal.ParseNum(s, &info); err != nil { var x ast.BasicLit if pbinternal.MatchBySymbol(f.Value, s, &x) { return &x } d.addErrf(n.Start, "invalid number %s", s) } if !info.IsInt() { return &ast.BasicLit{Kind: token.FLOAT, Value: s} } return &ast.BasicLit{Kind: token.INT, Value: info.String()} default: panic(fmt.Sprintf("unexpected type %v", f.ValueType)) } }