proto: allow text proto map keys to be omitted or order swapped
diff --git a/proto/equal.go b/proto/equal.go index cafb99f..8b16f95 100644 --- a/proto/equal.go +++ b/proto/equal.go
@@ -191,6 +191,13 @@ } return true case reflect.Ptr: + // Maps may have nil values in them, so check for nil. + if v1.IsNil() && v2.IsNil() { + return true + } + if v1.IsNil() != v2.IsNil() { + return false + } return equalAny(v1.Elem(), v2.Elem(), prop) case reflect.Slice: if v1.Type().Elem().Kind() == reflect.Uint8 {
diff --git a/proto/text_parser.go b/proto/text_parser.go index 8fa9f3a..0b8c59f 100644 --- a/proto/text_parser.go +++ b/proto/text_parser.go
@@ -602,8 +602,9 @@ // The map entry should be this sequence of tokens: // < key : KEY value : VALUE > - // Technically the "key" and "value" could come in any order, - // but in practice they won't. + // However, implementations may omit key or value, and technically + // we should support them in any order. See b/28924776 for a time + // this went wrong. tok := p.next() var terminator string @@ -615,32 +616,39 @@ default: return p.errorf("expected '{' or '<', found %q", tok.value) } - if err := p.consumeToken("key"); err != nil { - return err - } - if err := p.consumeToken(":"); err != nil { - return err - } - if err := p.readAny(key, props.mkeyprop); err != nil { - return err - } - if err := p.consumeOptionalSeparator(); err != nil { - return err - } - if err := p.consumeToken("value"); err != nil { - return err - } - if err := p.checkForColon(props.mvalprop, dst.Type().Elem()); err != nil { - return err - } - if err := p.readAny(val, props.mvalprop); err != nil { - return err - } - if err := p.consumeOptionalSeparator(); err != nil { - return err - } - if err := p.consumeToken(terminator); err != nil { - return err + for { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value == terminator { + break + } + switch tok.value { + case "key": + if err := p.consumeToken(":"); err != nil { + return err + } + if err := p.readAny(key, props.mkeyprop); err != nil { + return err + } + if err := p.consumeOptionalSeparator(); err != nil { + return err + } + case "value": + if err := p.checkForColon(props.mvalprop, dst.Type().Elem()); err != nil { + return err + } + if err := p.readAny(val, props.mvalprop); err != nil { + return err + } + if err := p.consumeOptionalSeparator(); err != nil { + return err + } + default: + p.back() + return p.errorf(`expected "key", "value", or %q, found %q`, terminator, tok.value) + } } dst.SetMapIndex(key, val)
diff --git a/proto/text_parser_test.go b/proto/text_parser_test.go index 32c4117..f25d5b0 100644 --- a/proto/text_parser_test.go +++ b/proto/text_parser_test.go
@@ -508,7 +508,10 @@ const in = `name_mapping:<key:1234 value:"Feist"> name_mapping:<key:1 value:"Beatles">` + `msg_mapping:<key:-4, value:<f: 2.0>,>` + // separating commas are okay `msg_mapping<key:-2 value<f: 4.0>>` + // no colon after "value" - `byte_mapping:<key:true value:"so be it">` + `msg_mapping:<value:<f: 5.0>>` + // omitted key + `msg_mapping:<key:1>` + // omitted value + `byte_mapping:<key:true value:"so be it">` + + `byte_mapping:<>` // omitted key and value want := &MessageWithMap{ NameMapping: map[int32]string{ 1: "Beatles", @@ -517,9 +520,12 @@ MsgMapping: map[int64]*FloatingPoint{ -4: {F: Float64(2.0)}, -2: {F: Float64(4.0)}, + 0: {F: Float64(5.0)}, + 1: nil, }, ByteMapping: map[bool][]byte{ - true: []byte("so be it"), + false: nil, + true: []byte("so be it"), }, } if err := UnmarshalText(in, m); err != nil {