encoding/protobuf/textproto: add decoder implementation

Note that this is incredibly buggy. Not much I can do,
as there just doesn't seem to be any good textproto parser
for Go, and this one is the recommended "gold standard".

Issue #5

Change-Id: Ieab0910dc4ea6072d9dc50e2947d8a7fb33ba7ef
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9369
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/protobuf/pbinternal/attribute.go b/encoding/protobuf/pbinternal/attribute.go
new file mode 100644
index 0000000..73fb349
--- /dev/null
+++ b/encoding/protobuf/pbinternal/attribute.go
@@ -0,0 +1,142 @@
+// 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 pbinternal
+
+import (
+	"strings"
+
+	"cuelang.org/go/cue"
+)
+
+type CompositeType int
+
+const (
+	Normal CompositeType = iota
+	List
+	Map
+)
+
+type ValueType int
+
+const (
+	Unknown ValueType = iota
+	Message
+	Int
+	Float
+	String
+	Bytes
+	Bool
+)
+
+type Info struct {
+	Name    string
+	CUEName string
+	Attr    cue.Attribute
+	Value   cue.Value
+
+	CompositeType CompositeType
+	ValueType     ValueType
+	Type          string
+
+	// For maps only
+	KeyType       ValueType // only for maps
+	KeyTypeString string
+}
+
+func FromIter(i *cue.Iterator) (info Info, err error) {
+	return FromValue(i.Label(), i.Value())
+}
+
+func FromValue(name string, v cue.Value) (info Info, err error) {
+	a := v.Attribute("protobuf")
+
+	info.Name = name
+	info.CUEName = name
+
+	if a.Err() == nil {
+		info.Attr = a
+
+		s, ok, err := a.Lookup(1, "name")
+		if err != nil {
+			return info, err
+		}
+		if ok {
+			info.Name = strings.TrimSpace(s)
+		}
+
+		info.Type, err = a.String(1)
+		if err != nil {
+			return info, err
+		}
+	}
+
+	switch v.IncompleteKind() {
+	case cue.ListKind:
+		info.CompositeType = List
+		e, _ := v.Elem()
+		if e.Exists() {
+			v = e
+		} else {
+			for i, _ := v.List(); i.Next(); {
+				v = i.Value()
+				break
+			}
+		}
+
+	case cue.StructKind:
+		if strings.HasPrefix(info.Type, "map[") {
+			a := strings.SplitN(info.Type[len("map["):], ",", 2)
+			info.KeyTypeString = strings.TrimSpace(a[0])
+			switch info.KeyTypeString {
+			case "string":
+				info.KeyType = String
+			case "bytes":
+				info.KeyType = Bytes
+			case "double", "float":
+				info.KeyType = Float
+			case "bool":
+				info.KeyType = Bool
+			default:
+				info.KeyType = Int // Assuming
+			}
+			info.CompositeType = Map
+			v, _ = v.Elem()
+		}
+	}
+
+	info.Value = v
+
+	switch v.IncompleteKind() {
+	case cue.StructKind:
+		info.ValueType = Message
+
+	case cue.StringKind:
+		info.ValueType = String
+
+	case cue.BytesKind:
+		info.ValueType = Bytes
+
+	case cue.BoolKind:
+		info.ValueType = Bool
+
+	case cue.IntKind:
+		info.ValueType = Int
+
+	case cue.FloatKind, cue.NumberKind:
+		info.ValueType = Float
+	}
+
+	return info, nil
+}
diff --git a/encoding/protobuf/textproto/decoder.go b/encoding/protobuf/textproto/decoder.go
new file mode 100644
index 0000000..18681f7
--- /dev/null
+++ b/encoding/protobuf/textproto/decoder.go
@@ -0,0 +1,443 @@
+// 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, 0, 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))
+	}
+}
diff --git a/encoding/protobuf/textproto/decoder_test.go b/encoding/protobuf/textproto/decoder_test.go
new file mode 100644
index 0000000..4e5dd56
--- /dev/null
+++ b/encoding/protobuf/textproto/decoder_test.go
@@ -0,0 +1,80 @@
+// 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_test
+
+import (
+	"strings"
+	"testing"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast/astutil"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/format"
+	"cuelang.org/go/encoding/protobuf/textproto"
+	"cuelang.org/go/internal/cuetest"
+	"cuelang.org/go/internal/cuetxtar"
+)
+
+func TestParse(t *testing.T) {
+	test := cuetxtar.TxTarTest{
+		Root:   "./testdata/decoder",
+		Name:   "decode",
+		Update: cuetest.UpdateGoldenFiles,
+	}
+
+	r := cue.Runtime{}
+
+	d := textproto.NewDecoder()
+
+	test.Run(t, func(t *cuetxtar.Test) {
+		// TODO: use high-level API.
+
+		var schema cue.Value
+		var filename string
+		var b []byte
+
+		for _, f := range t.Archive.Files {
+			switch {
+			case strings.HasSuffix(f.Name, ".cue"):
+				inst, err := r.Compile(f.Name, f.Data)
+				if err != nil {
+					t.WriteErrors(errors.Promote(err, "test"))
+					return
+				}
+				schema = inst.Value()
+
+			case strings.HasSuffix(f.Name, ".textproto"):
+				filename = f.Name
+				b = f.Data
+			}
+		}
+
+		x, err := d.Parse(schema, filename, b)
+		if err != nil {
+			t.WriteErrors(errors.Promote(err, "test"))
+			return
+		}
+
+		f, err := astutil.ToFile(x)
+		if err != nil {
+			t.WriteErrors(errors.Promote(err, "test"))
+		}
+		b, err = format.Node(f)
+		if err != nil {
+			t.WriteErrors(errors.Promote(err, "test"))
+		}
+		_, _ = t.Write(b)
+	})
+}
diff --git a/encoding/protobuf/textproto/doc.go b/encoding/protobuf/textproto/doc.go
new file mode 100644
index 0000000..75fb6e5
--- /dev/null
+++ b/encoding/protobuf/textproto/doc.go
@@ -0,0 +1,27 @@
+// 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 converts text protobuffer files to and from CUE.
+//
+// Note that textproto is an unofficial standard and that there are no
+// specifications: the recommended packages are the de facto standard for the
+// relevant programming languages and the recommended implementations may vary
+// considerably between them.
+//
+// Also, the standard text proto parsing libraries are rather buggy. Please
+// verify that a parsing issues is not related these libraries before filing
+// bugs with CUE.
+//
+// API Status: DRAFT: API may change without notice.
+package textproto
diff --git a/encoding/protobuf/textproto/testdata/decoder/comments.txtar b/encoding/protobuf/textproto/testdata/decoder/comments.txtar
new file mode 100644
index 0000000..f4fbc74
--- /dev/null
+++ b/encoding/protobuf/textproto/testdata/decoder/comments.txtar
@@ -0,0 +1,116 @@
+// TODO: there are many missing comments, but these really are consequences
+// of the buggy textpbfmt library.
+
+-- foo.cue --
+a: string
+b: [...int]
+c: [...int32]
+m: [...#Msg]
+#Msg: {
+    x: string
+    y: string
+}
+
+-- input.textproto --
+# file comment
+
+# doc comment a
+a: "dsfadsafsaf" # line comment
+
+# floating comment a-b
+
+# doc comment b
+b: [
+  # pre-elem comment
+
+  # doc elem 0
+  123, # elem 0 line comment
+  # trailing elem 0
+
+  # inbetween comment 1
+
+  # inbetween comment 2
+
+  # doc elem 1
+  456 # elem 1 line comment
+  # trailing elem 1
+
+  # final floating 1
+
+  # final floating 2
+]
+# floating end
+
+c: 2342134 # line elem 0
+c: 2342135 # line elem 1
+# inbetween elems
+c: 2342136 # line elem 2
+# after list c
+
+# floating
+
+m {
+    x: "sdfff" # inner line comment
+    y: "q\"qq\\q\n"
+    # after last value
+} # after elem line
+ # after elem separate
+m {
+    x: "   sdfff2  \321\202\320\265\321\201\321\202 "
+    y: "q\tqq<>q2&\001\377"
+} # after list line
+# after list
+
+# floating end
+
+-- out/decode --
+// file comment
+
+// doc comment a
+a: "dsfadsafsaf" // line comment
+
+// floating comment a-b
+
+// doc comment b
+b: [
+	// pre-elem comment
+	123, // elem 0 line comment
+
+	// trailing elem 0
+	// inbetween comment 2
+	456, // elem 1 line comment
+
+	// trailing elem 1
+
+	// final floating 2
+]
+
+// floating end
+
+c: [2342134, // line elem 0
+		2342135, // line elem 1
+		// inbetween elems
+		2342136, // line elem 2
+]
+
+// after list c
+
+// floating
+
+m: [{
+	x: "sdfff" // inner line comment
+	y: "q\"qq\\q\n"
+
+	// after last value
+
+}, // after elem line
+	// after elem separate
+	{
+		x: "   sdfff2  тест "
+		y: "q\tqq<>q2&\u0001�"
+	}, // after list line
+]
+
+// after list
+
+// floating end
diff --git a/encoding/protobuf/textproto/testdata/decoder/enums.txtar b/encoding/protobuf/textproto/testdata/decoder/enums.txtar
new file mode 100644
index 0000000..032249b
--- /dev/null
+++ b/encoding/protobuf/textproto/testdata/decoder/enums.txtar
@@ -0,0 +1,23 @@
+-- foo.cue --
+#MyEnum:
+    #Val1 |
+    #Val2 |
+    #Val3
+
+#Val1: 1
+#Val2: 2
+#Val3: 3
+
+a0: #MyEnum
+a1: [...#MyEnum]
+
+-- input.textproto --
+a0: Val1
+a1: Val1
+a1: Val2
+a1: Val3
+a1: 2
+
+-- out/decode --
+a0: 1
+a1: [1, 2, 3, 2]
diff --git a/encoding/protobuf/textproto/testdata/decoder/errors.txtar b/encoding/protobuf/textproto/testdata/decoder/errors.txtar
new file mode 100644
index 0000000..3f173d4
--- /dev/null
+++ b/encoding/protobuf/textproto/testdata/decoder/errors.txtar
@@ -0,0 +1,12 @@
+# The error in input.textproto (wrong comment style) is going undetected.
+# This is a protobuf bug. Can't do much about it.
+-- errors.cue --
+a: int
+
+-- input.textproto --
+
+// Silent nights
+a: 1
+
+-- out/decode --
+
diff --git a/encoding/protobuf/textproto/testdata/decoder/list.txtar b/encoding/protobuf/textproto/testdata/decoder/list.txtar
new file mode 100644
index 0000000..5a0969f
--- /dev/null
+++ b/encoding/protobuf/textproto/testdata/decoder/list.txtar
@@ -0,0 +1,47 @@
+// test of non-standard list
+
+// TODO: there are many missing comments, but these really are consequences
+// of the buggy textpbfmt library.
+
+-- list.cue --
+empty1: [...int]
+empty2: [...int]
+
+int1: [...int]
+int2: [...int]
+int3: [...int]
+
+string1: [...string]
+
+float1: [...number]
+
+
+-- input.textproto --
+empty1: []
+empty2: [ # foo
+]
+
+int1: [1, 2]
+int2: [1 2]  # omitting commas okay
+int3: [
+    1 # omitting comma okay
+    2
+]
+
+string1: [
+    "a",  # omitting comma NOT supported
+    "b"
+]
+
+float1: [ 1e+2 1. 0]
+-- out/decode --
+empty1: []
+empty2: [ // foo
+]
+int1: [1, 2]
+int2: [1, 2] // omitting commas okay
+int3: [1, // omitting comma okay
+		2]
+string1: ["a", // omitting comma NOT supported
+		"b"]
+float1: [1e+2, 1.0, 0]
diff --git a/encoding/protobuf/textproto/testdata/decoder/map.txtar b/encoding/protobuf/textproto/testdata/decoder/map.txtar
new file mode 100644
index 0000000..0dbf2b5
--- /dev/null
+++ b/encoding/protobuf/textproto/testdata/decoder/map.txtar
@@ -0,0 +1,51 @@
+-- map.cue --
+map: {[string]: int} @protobuf(1,map[string]int)
+
+
+implicit: [string]: string
+
+intMap: {[string]: int} @protobuf(1,map[int]int)
+
+-- input.textproto --
+map: {
+    key: "foo"
+    value: 2
+}
+map: {
+    key: "bar"
+    value: 3
+}
+
+implicit: {
+    key: "foo"
+    value: 2
+}
+implicit: {
+    key: "bar"
+    value: 3
+}
+
+intMap: {
+    key:   100
+    value: 2
+}
+intMap: {
+    key:   102
+    value: 3
+}
+
+-- out/decode --
+map: {
+	"foo": 2
+}
+map: {
+	"bar": 3
+}
+implicit: {}
+implicit: {}
+intMap: {
+	"100": 2
+}
+intMap: {
+	"102": 3
+}
diff --git a/encoding/protobuf/textproto/testdata/decoder/scalar.txtar b/encoding/protobuf/textproto/testdata/decoder/scalar.txtar
new file mode 100644
index 0000000..e39c3f8
--- /dev/null
+++ b/encoding/protobuf/textproto/testdata/decoder/scalar.txtar
@@ -0,0 +1,19 @@
+-- map.cue --
+inf: number
+nan: number
+
+t: bool
+f: bool
+
+
+-- input.textproto --
+inf: inf
+nan: nan
+t: true
+f: false
+
+-- out/decode --
+inf: _|_
+nan: _|_
+t:   true
+f:   false
diff --git a/encoding/protobuf/textproto/testdata/decoder/simple.txtar b/encoding/protobuf/textproto/testdata/decoder/simple.txtar
new file mode 100644
index 0000000..fb4645e
--- /dev/null
+++ b/encoding/protobuf/textproto/testdata/decoder/simple.txtar
@@ -0,0 +1,63 @@
+// From: https://stackoverflow.com/questions/18873924/what-does-the-protobuf-text-format-look-like
+-- foo.cue --
+#MyEnum: "Default" | "Variant1" | "Variant100"
+
+f1:       string
+f2:       int64
+fa:       [...uint64]
+fb:       [...int32]
+fc:       [...number]
+pairs:    [...#Pair]
+bbbb:     bytes // optional
+
+//    extensions 100 to max;
+
+#Pair: {
+    key:   string
+    value: string
+}
+
+-- input.textproto --
+f1: "dsfadsafsaf"
+f2: 234 # value comment
+
+fa: 2342134
+fa: 2342135
+fa: 2342136
+# Mix of list and single elements.
+fb: [ -2342134, -2342135, -2342136 ]
+fb: -1000
+
+fc: 4
+fc: 7
+fc: -12
+fc: 4
+fc: 7
+fc: -3
+fc: 4
+fc: 7
+fc: 0
+pairs {
+    key: "sdfff"
+    value: "q\"qq\\q\n"
+}
+pairs {
+    key: "   sdfff2  \321\202\320\265\321\201\321\202 "
+    value: "q\tqq<>q2&\001\377"
+}
+bbbb: "\000\001\002\377\376\375"
+-- out/decode --
+f1: "dsfadsafsaf"
+f2: 234 // value comment
+fa: [2342134, 2342135, 2342136]
+// Mix of list and single elements.
+fb: [-2342134, -2342135, -2342136, -1000]
+fc: [4, 7, -12, 4, 7, -3, 4, 7, 0]
+pairs: [{
+	key:   "sdfff"
+	value: "q\"qq\\q\n"
+}, {
+	key:   "   sdfff2  тест "
+	value: "q\tqq<>q2&\u0001�"
+}]
+bbbb: '\x00\x01\x02\xff\xfe\xfd'
diff --git a/go.mod b/go.mod
index d838caf..81af465 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@
 	github.com/lib/pq v1.0.0 // indirect
 	github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de
 	github.com/pkg/errors v0.8.1 // indirect
+	github.com/protocolbuffers/txtpbfmt v0.0.0-20201118171849-f6a6b3f636fc
 	github.com/rogpeppe/go-internal v1.8.0
 	github.com/spf13/cobra v1.0.0
 	github.com/spf13/pflag v1.0.3
diff --git a/go.sum b/go.sum
index ef6affb..de875ad 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,7 @@
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -91,6 +92,8 @@
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/protocolbuffers/txtpbfmt v0.0.0-20201118171849-f6a6b3f636fc h1:gSVONBi2HWMFXCa9jFdYvYk7IwW/mTLxWOF7rXS4LO0=
+github.com/protocolbuffers/txtpbfmt v0.0.0-20201118171849-f6a6b3f636fc/go.mod h1:KbKfKPy2I6ecOIGA9apfheFv14+P3RSmmQvshofQyMY=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
diff --git a/internal/attrs_test.go b/internal/attrs_test.go
index 8256ab3..38ba0d6 100644
--- a/internal/attrs_test.go
+++ b/internal/attrs_test.go
@@ -48,11 +48,11 @@
 		in:  `foo,bar,baz`,
 		out: "[{foo 0} {bar 0} {baz 0}]",
 	}, {
-		in:  `1,"map<int,string>"`,
-		out: "[{1 0} {map<int,string> 0}]",
+		in:  `1,map[int]string`,
+		out: "[{1 0} {map[int]string 0}]",
 	}, {
-		in:  `1, "map<int,string>"`,
-		out: "[{1 0} {map<int,string> 0}]",
+		in:  `1,map[int]string`,
+		out: "[{1 0} {map[int]string 0}]",
 	}, {
 		in:  `bar=str`,
 		out: "[{bar=str 3}]",