|
@@ -0,0 +1,570 @@
|
|
|
+package coap
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "encoding/binary"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "reflect"
|
|
|
+ "sort"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+)
|
|
|
+
|
|
|
+// COAPType 代表消息类型
|
|
|
+type COAPType uint8
|
|
|
+
|
|
|
+// MaxTokenSize 最大token size
|
|
|
+const MaxTokenSize = 8
|
|
|
+
|
|
|
+const (
|
|
|
+ // CON 需要被确认的请求
|
|
|
+ CON COAPType = 0
|
|
|
+ // NON 不需要被确认的请求
|
|
|
+ NON COAPType = 1
|
|
|
+ // ACK 应答消息,接受到CON消息的响应
|
|
|
+ ACK COAPType = 2
|
|
|
+ // RST 复位消息,当接收者接受到的消息包含一个错误,接受者解析消息或者不再关心发送者发送的内容,那么复位消息将会被发送
|
|
|
+ RST COAPType = 3
|
|
|
+)
|
|
|
+
|
|
|
+// COAPCode 请求方法的类型
|
|
|
+type COAPCode uint8
|
|
|
+
|
|
|
+// request codes
|
|
|
+const (
|
|
|
+ GET COAPCode = iota + 1
|
|
|
+ POST
|
|
|
+ PUT
|
|
|
+ DELETE
|
|
|
+)
|
|
|
+
|
|
|
+// Response codes
|
|
|
+const (
|
|
|
+ Empty COAPCode = 0
|
|
|
+ Created COAPCode = 65
|
|
|
+ Deleted COAPCode = 66
|
|
|
+ Valid COAPCode = 67
|
|
|
+ Changed COAPCode = 68
|
|
|
+ Content COAPCode = 69
|
|
|
+ Continue COAPCode = 95
|
|
|
+ BadRequest COAPCode = 128
|
|
|
+ Unauthorized COAPCode = 129
|
|
|
+ BadOption COAPCode = 130
|
|
|
+ Forbidden COAPCode = 131
|
|
|
+ NotFound COAPCode = 132
|
|
|
+ MethodNotAllowed COAPCode = 133
|
|
|
+ NotAcceptable COAPCode = 134
|
|
|
+ RequestEntityIncomplete COAPCode = 136
|
|
|
+ PreconditionFailed COAPCode = 140
|
|
|
+ RequestEntityTooLarge COAPCode = 141
|
|
|
+ UnsupportedMediaType COAPCode = 143
|
|
|
+ InternalServerError COAPCode = 160
|
|
|
+ NotImplemented COAPCode = 161
|
|
|
+ BadGateway COAPCode = 162
|
|
|
+ ServiceUnavailable COAPCode = 163
|
|
|
+ GatewayTimeout COAPCode = 164
|
|
|
+ ProxyingNotSupported COAPCode = 165
|
|
|
+)
|
|
|
+
|
|
|
+// MediaType 请求消息的媒体类型 对应Content-Format
|
|
|
+type MediaType uint16
|
|
|
+
|
|
|
+// Content formats.
|
|
|
+const (
|
|
|
+ TextPlain MediaType = 0 // text/plain;charset=utf-8
|
|
|
+ AppXML MediaType = 41 // application/xml
|
|
|
+ AppOctets MediaType = 42 // application/octet-stream
|
|
|
+ AppExi MediaType = 47 // application/exi
|
|
|
+ AppJSON MediaType = 50 // application/json
|
|
|
+ AppCBOR MediaType = 60 //application/cbor (RFC 7049)
|
|
|
+)
|
|
|
+
|
|
|
+func (c MediaType) String() string {
|
|
|
+ switch c {
|
|
|
+ case TextPlain:
|
|
|
+ return "text/plain;charset=utf-8"
|
|
|
+ case AppXML:
|
|
|
+ return "application/xml"
|
|
|
+ case AppOctets:
|
|
|
+ return "application/octet-stream"
|
|
|
+ case AppExi:
|
|
|
+ return "application/exi"
|
|
|
+ case AppJSON:
|
|
|
+ return "application/json"
|
|
|
+ case AppCBOR:
|
|
|
+ return "application/cbor (RFC 7049)"
|
|
|
+ }
|
|
|
+ return "Unknown media type: 0x" + strconv.FormatInt(int64(c), 16)
|
|
|
+}
|
|
|
+
|
|
|
+// OptionID Option编号
|
|
|
+type OptionID uint8
|
|
|
+
|
|
|
+// Option IDs.
|
|
|
+const (
|
|
|
+ IfMatch OptionID = 1
|
|
|
+ URIHost OptionID = 3
|
|
|
+ ETag OptionID = 4
|
|
|
+ IfNoneMatch OptionID = 5
|
|
|
+ Observe OptionID = 6
|
|
|
+ URIPort OptionID = 7
|
|
|
+ LocationPath OptionID = 8
|
|
|
+ URIPath OptionID = 11
|
|
|
+ ContentFormat OptionID = 12
|
|
|
+ MaxAge OptionID = 14
|
|
|
+ URIQuery OptionID = 15
|
|
|
+ Accept OptionID = 17
|
|
|
+ LocationQuery OptionID = 20
|
|
|
+ Block2 OptionID = 23
|
|
|
+ Block1 OptionID = 27
|
|
|
+ Size2 OptionID = 28
|
|
|
+ ProxyURI OptionID = 35
|
|
|
+ ProxyScheme OptionID = 39
|
|
|
+ Size1 OptionID = 60
|
|
|
+)
|
|
|
+
|
|
|
+// Option value format (RFC7252 section 3.2)
|
|
|
+type valueFormat uint8
|
|
|
+
|
|
|
+const (
|
|
|
+ valueUnknown valueFormat = iota
|
|
|
+ valueEmpty
|
|
|
+ valueOpaque
|
|
|
+ valueUint
|
|
|
+ valueString
|
|
|
+)
|
|
|
+
|
|
|
+type optionDef struct {
|
|
|
+ valueFormat valueFormat
|
|
|
+ minLen int
|
|
|
+ maxLen int
|
|
|
+}
|
|
|
+
|
|
|
+var coapOptionDefs = map[OptionID]optionDef{
|
|
|
+ IfMatch: optionDef{valueFormat: valueOpaque, minLen: 0, maxLen: 8},
|
|
|
+ URIHost: optionDef{valueFormat: valueString, minLen: 1, maxLen: 255},
|
|
|
+ ETag: optionDef{valueFormat: valueOpaque, minLen: 1, maxLen: 8},
|
|
|
+ IfNoneMatch: optionDef{valueFormat: valueEmpty, minLen: 0, maxLen: 0},
|
|
|
+ Observe: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 3},
|
|
|
+ URIPort: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 2},
|
|
|
+ LocationPath: optionDef{valueFormat: valueString, minLen: 0, maxLen: 255},
|
|
|
+ URIPath: optionDef{valueFormat: valueString, minLen: 0, maxLen: 255},
|
|
|
+ ContentFormat: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 2},
|
|
|
+ MaxAge: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 4},
|
|
|
+ URIQuery: optionDef{valueFormat: valueString, minLen: 0, maxLen: 255},
|
|
|
+ Accept: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 2},
|
|
|
+ LocationQuery: optionDef{valueFormat: valueString, minLen: 0, maxLen: 255},
|
|
|
+ Block2: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 3},
|
|
|
+ Block1: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 3},
|
|
|
+ Size2: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 4},
|
|
|
+ ProxyURI: optionDef{valueFormat: valueString, minLen: 1, maxLen: 1034},
|
|
|
+ ProxyScheme: optionDef{valueFormat: valueString, minLen: 1, maxLen: 255},
|
|
|
+ Size1: optionDef{valueFormat: valueUint, minLen: 0, maxLen: 4},
|
|
|
+}
|
|
|
+
|
|
|
+type option struct {
|
|
|
+ ID OptionID
|
|
|
+ Value interface{}
|
|
|
+}
|
|
|
+
|
|
|
+func encodeInt(v uint32) []byte {
|
|
|
+ switch {
|
|
|
+ case v == 0:
|
|
|
+ return nil
|
|
|
+ case v < 256:
|
|
|
+ return []byte{byte(v)}
|
|
|
+ case v < 65536:
|
|
|
+ rv := []byte{0, 0}
|
|
|
+ binary.BigEndian.PutUint16(rv, uint16(v))
|
|
|
+ return rv
|
|
|
+ case v < 16777216:
|
|
|
+ rv := []byte{0, 0, 0, 0}
|
|
|
+ binary.BigEndian.PutUint32(rv, uint32(v))
|
|
|
+ return rv[1:]
|
|
|
+ default:
|
|
|
+ rv := []byte{0, 0, 0, 0}
|
|
|
+ binary.BigEndian.PutUint32(rv, uint32(v))
|
|
|
+ return rv
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func decodeInt(b []byte) uint32 {
|
|
|
+ tmp := []byte{0, 0, 0, 0}
|
|
|
+ copy(tmp[4-len(b):], b)
|
|
|
+ return binary.BigEndian.Uint32(tmp)
|
|
|
+}
|
|
|
+
|
|
|
+func (o option) toBytes() []byte {
|
|
|
+ var v uint32
|
|
|
+
|
|
|
+ switch i := o.Value.(type) {
|
|
|
+ case string:
|
|
|
+ return []byte(i)
|
|
|
+ case []byte:
|
|
|
+ return i
|
|
|
+ case MediaType:
|
|
|
+ v = uint32(i)
|
|
|
+ case int:
|
|
|
+ v = uint32(i)
|
|
|
+ case int32:
|
|
|
+ v = uint32(i)
|
|
|
+ case uint:
|
|
|
+ v = uint32(i)
|
|
|
+ case uint32:
|
|
|
+ v = i
|
|
|
+ default:
|
|
|
+ panic(fmt.Errorf("invalid type for option %x: %T (%v)",
|
|
|
+ o.ID, o.Value, o.Value))
|
|
|
+ }
|
|
|
+
|
|
|
+ return encodeInt(v)
|
|
|
+}
|
|
|
+
|
|
|
+type options []option
|
|
|
+
|
|
|
+func (o options) Len() int {
|
|
|
+ return len(o)
|
|
|
+}
|
|
|
+
|
|
|
+func (o options) Less(i, j int) bool {
|
|
|
+ if o[i].ID == o[j].ID {
|
|
|
+ return i < j
|
|
|
+ }
|
|
|
+ return o[i].ID < o[j].ID
|
|
|
+}
|
|
|
+
|
|
|
+func (o options) Swap(i, j int) {
|
|
|
+ o[i], o[j] = o[j], o[i]
|
|
|
+}
|
|
|
+
|
|
|
+func (o options) Remove(oid OptionID) options {
|
|
|
+ idx := 0
|
|
|
+ for i := 0; i < len(o); i++ {
|
|
|
+ if o[i].ID != oid {
|
|
|
+ o[idx] = o[i]
|
|
|
+ idx++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return o[:idx]
|
|
|
+}
|
|
|
+
|
|
|
+const (
|
|
|
+ extoptByteCode = 13
|
|
|
+ extoptByteAddend = 13
|
|
|
+ extoptWordCode = 14
|
|
|
+ extoptWordAddend = 269
|
|
|
+ extoptError = 15
|
|
|
+)
|
|
|
+
|
|
|
+// Message interface
|
|
|
+type Message interface {
|
|
|
+ Encode() ([]byte, error)
|
|
|
+ Decode(data []byte) error
|
|
|
+ AllOptions() options
|
|
|
+ Option(opid OptionID) interface{}
|
|
|
+ Path() []string
|
|
|
+ PathString() string
|
|
|
+ SetPath([]string)
|
|
|
+ SetPathString(s string)
|
|
|
+ AddOption(opid OptionID, val interface{})
|
|
|
+ RemoveOption(opid OptionID)
|
|
|
+ IsConfirmable() bool
|
|
|
+ OptionStrings(opid OptionID) []string
|
|
|
+ GetMessageID() uint16
|
|
|
+ GetToken() []byte
|
|
|
+}
|
|
|
+
|
|
|
+// BaseMessage COAP 消息体
|
|
|
+type BaseMessage struct {
|
|
|
+ Type COAPType
|
|
|
+ Code COAPCode
|
|
|
+ MessageID uint16
|
|
|
+ Token []byte
|
|
|
+ Payload []byte
|
|
|
+ Opts options
|
|
|
+}
|
|
|
+
|
|
|
+func (m *BaseMessage) GetToken() []byte {
|
|
|
+ return m.Token
|
|
|
+}
|
|
|
+func (m *BaseMessage) GetMessageID() uint16 {
|
|
|
+ return m.MessageID
|
|
|
+}
|
|
|
+
|
|
|
+func (m *BaseMessage) Encode() ([]byte, error) {
|
|
|
+ tmpbuf := []byte{0, 0}
|
|
|
+ binary.BigEndian.PutUint16(tmpbuf, m.MessageID)
|
|
|
+ buf := bytes.Buffer{}
|
|
|
+ buf.Write([]byte{
|
|
|
+ (1 << 6) | (uint8(m.Type) << 4) | uint8(0xf&len(m.Token)),
|
|
|
+ byte(m.Code),
|
|
|
+ tmpbuf[0], tmpbuf[1],
|
|
|
+ })
|
|
|
+ buf.Write(m.Token)
|
|
|
+ extendOpt := func(opt int) (int, int) {
|
|
|
+ ext := 0
|
|
|
+ if opt >= extoptByteAddend {
|
|
|
+ if opt >= extoptWordAddend {
|
|
|
+ ext = opt - extoptWordAddend
|
|
|
+ opt = extoptWordCode
|
|
|
+ } else {
|
|
|
+ ext = opt - extoptByteAddend
|
|
|
+ opt = extoptByteCode
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return opt, ext
|
|
|
+ }
|
|
|
+
|
|
|
+ writeOptHeader := func(delta, length int) {
|
|
|
+ d, dx := extendOpt(delta)
|
|
|
+ l, lx := extendOpt(length)
|
|
|
+
|
|
|
+ buf.WriteByte(byte(d<<4) | byte(l))
|
|
|
+
|
|
|
+ tmp := []byte{0, 0}
|
|
|
+ writeExt := func(opt, ext int) {
|
|
|
+ switch opt {
|
|
|
+ case extoptByteCode:
|
|
|
+ buf.WriteByte(byte(ext))
|
|
|
+ case extoptWordCode:
|
|
|
+ binary.BigEndian.PutUint16(tmp, uint16(ext))
|
|
|
+ buf.Write(tmp)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ writeExt(d, dx)
|
|
|
+ writeExt(l, lx)
|
|
|
+ }
|
|
|
+
|
|
|
+ sort.Stable(&m.Opts)
|
|
|
+
|
|
|
+ prev := 0
|
|
|
+
|
|
|
+ for _, o := range m.Opts {
|
|
|
+ b := o.toBytes()
|
|
|
+ writeOptHeader(int(o.ID)-prev, len(b))
|
|
|
+ buf.Write(b)
|
|
|
+ prev = int(o.ID)
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(m.Payload) > 0 {
|
|
|
+ buf.Write([]byte{0xff})
|
|
|
+ }
|
|
|
+
|
|
|
+ buf.Write(m.Payload)
|
|
|
+
|
|
|
+ return buf.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+func (m *BaseMessage) Decode(data []byte) error {
|
|
|
+ if len(data) < 4 {
|
|
|
+ return errors.New("short packet")
|
|
|
+ }
|
|
|
+
|
|
|
+ if data[0]>>6 != 1 {
|
|
|
+ return errors.New("invalid version")
|
|
|
+ }
|
|
|
+
|
|
|
+ m.Type = COAPType((data[0] >> 4) & 0x3)
|
|
|
+ tokenLen := int(data[0] & 0xf)
|
|
|
+ if tokenLen > 8 {
|
|
|
+ return ErrInvalidTokenLen
|
|
|
+ }
|
|
|
+
|
|
|
+ m.Code = COAPCode(data[1])
|
|
|
+ m.MessageID = binary.BigEndian.Uint16(data[2:4])
|
|
|
+
|
|
|
+ if tokenLen > 0 {
|
|
|
+ m.Token = make([]byte, tokenLen)
|
|
|
+ }
|
|
|
+ if len(data) < 4+tokenLen {
|
|
|
+ return errors.New("truncated")
|
|
|
+ }
|
|
|
+ copy(m.Token, data[4:4+tokenLen])
|
|
|
+ b := data[4+tokenLen:]
|
|
|
+ prev := 0
|
|
|
+
|
|
|
+ parseExtOpt := func(opt int) (int, error) {
|
|
|
+ switch opt {
|
|
|
+ case extoptByteCode:
|
|
|
+ if len(b) < 1 {
|
|
|
+ return -1, errors.New("truncated")
|
|
|
+ }
|
|
|
+ opt = int(b[0]) + extoptByteAddend
|
|
|
+ b = b[1:]
|
|
|
+ case extoptWordCode:
|
|
|
+ if len(b) < 2 {
|
|
|
+ return -1, errors.New("truncated")
|
|
|
+ }
|
|
|
+ opt = int(binary.BigEndian.Uint16(b[:2])) + extoptWordAddend
|
|
|
+ b = b[2:]
|
|
|
+ }
|
|
|
+ return opt, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ for len(b) > 0 {
|
|
|
+ if b[0] == 0xff {
|
|
|
+ b = b[1:]
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ delta := int(b[0] >> 4)
|
|
|
+ length := int(b[0] & 0x0f)
|
|
|
+
|
|
|
+ if delta == extoptError || length == extoptError {
|
|
|
+ return errors.New("unexpected extended option marker")
|
|
|
+ }
|
|
|
+
|
|
|
+ b = b[1:]
|
|
|
+
|
|
|
+ delta, err := parseExtOpt(delta)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ length, err = parseExtOpt(length)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(b) < length {
|
|
|
+ return errors.New("truncated")
|
|
|
+ }
|
|
|
+
|
|
|
+ oid := OptionID(prev + delta)
|
|
|
+ opval := parseOptionValue(oid, b[:length])
|
|
|
+ b = b[length:]
|
|
|
+ prev = int(oid)
|
|
|
+
|
|
|
+ if opval != nil {
|
|
|
+ m.Opts = append(m.Opts, option{ID: oid, Value: opval})
|
|
|
+ }
|
|
|
+ }
|
|
|
+ m.Payload = b
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// AllOptions get message opts
|
|
|
+func (m *BaseMessage) AllOptions() options {
|
|
|
+ return m.Opts
|
|
|
+}
|
|
|
+
|
|
|
+// IsConfirmable 如果是CON类型的消息类型,返回true
|
|
|
+func (m *BaseMessage) IsConfirmable() bool {
|
|
|
+ return m.Type == CON
|
|
|
+}
|
|
|
+
|
|
|
+// Option get option by id
|
|
|
+func (m *BaseMessage) Option(o OptionID) interface{} {
|
|
|
+ for _, v := range m.Opts {
|
|
|
+ if o == v.ID {
|
|
|
+ return v.Value
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// Options 获取所的option value
|
|
|
+func (m *BaseMessage) Options(o OptionID) []interface{} {
|
|
|
+ var rv []interface{}
|
|
|
+
|
|
|
+ for _, v := range m.Opts {
|
|
|
+ if o == v.ID {
|
|
|
+ rv = append(rv, v.Value)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rv
|
|
|
+}
|
|
|
+
|
|
|
+// OptionStrings get option strings by id
|
|
|
+func (m *BaseMessage) OptionStrings(o OptionID) []string {
|
|
|
+ var rv []string
|
|
|
+ for _, o := range m.Options(o) {
|
|
|
+ rv = append(rv, o.(string))
|
|
|
+ }
|
|
|
+ return rv
|
|
|
+}
|
|
|
+
|
|
|
+// Path 获取URIPath
|
|
|
+func (m *BaseMessage) Path() []string {
|
|
|
+ return m.OptionStrings(URIPath)
|
|
|
+}
|
|
|
+
|
|
|
+// PathString gets a path as a / separated string.
|
|
|
+func (m *BaseMessage) PathString() string {
|
|
|
+ return strings.Join(m.Path(), "/")
|
|
|
+}
|
|
|
+
|
|
|
+// SetPathString sets a path by a / separated string.
|
|
|
+func (m *BaseMessage) SetPathString(s string) {
|
|
|
+ switch s {
|
|
|
+ case "", "/":
|
|
|
+ //root path is not set as option
|
|
|
+ return
|
|
|
+ default:
|
|
|
+ if s[0] == '/' {
|
|
|
+ s = s[1:]
|
|
|
+ }
|
|
|
+ m.SetPath(strings.Split(s, "/"))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+//RemoveOption remove a given opid
|
|
|
+func (m *BaseMessage) RemoveOption(opID OptionID) {
|
|
|
+ m.Opts = m.Opts.Remove(opID)
|
|
|
+}
|
|
|
+
|
|
|
+// AddOption ``
|
|
|
+func (m *BaseMessage) AddOption(opID OptionID, val interface{}) {
|
|
|
+ iv := reflect.ValueOf(val)
|
|
|
+ if (iv.Kind() == reflect.Slice || iv.Kind() == reflect.Array) &&
|
|
|
+ iv.Type().Elem().Kind() == reflect.String {
|
|
|
+ for i := 0; i < iv.Len(); i++ {
|
|
|
+ m.Opts = append(m.Opts, option{opID, iv.Index(i).Interface()})
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ m.Opts = append(m.Opts, option{opID, val})
|
|
|
+}
|
|
|
+
|
|
|
+// SetPath ``
|
|
|
+func (m *BaseMessage) SetPath(s []string) {
|
|
|
+ m.SetOption(URIPath, s)
|
|
|
+}
|
|
|
+
|
|
|
+// SetOption sets an option, discarding any previous value
|
|
|
+func (m *BaseMessage) SetOption(opID OptionID, val interface{}) {
|
|
|
+ m.RemoveOption(opID)
|
|
|
+ m.AddOption(opID, val)
|
|
|
+}
|
|
|
+func parseOptionValue(optionID OptionID, valueBuf []byte) interface{} {
|
|
|
+ def := coapOptionDefs[optionID]
|
|
|
+ if def.valueFormat == valueUnknown {
|
|
|
+ // Skip unrecognized options (RFC7252 section 5.4.1)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ if len(valueBuf) < def.minLen || len(valueBuf) > def.maxLen {
|
|
|
+ // Skip options with illegal value length (RFC7252 section 5.4.3)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ switch def.valueFormat {
|
|
|
+ case valueUint:
|
|
|
+ intValue := decodeInt(valueBuf)
|
|
|
+ if optionID == ContentFormat || optionID == Accept {
|
|
|
+ return MediaType(intValue)
|
|
|
+ } else {
|
|
|
+ return intValue
|
|
|
+ }
|
|
|
+ case valueString:
|
|
|
+ return string(valueBuf)
|
|
|
+ case valueOpaque, valueEmpty:
|
|
|
+ return valueBuf
|
|
|
+ }
|
|
|
+ // Skip unrecognized options (should never be reached)
|
|
|
+ return nil
|
|
|
+}
|
|
|
+func ParseMessage(data []byte) (Message, error) {
|
|
|
+ rv := &BaseMessage{}
|
|
|
+ return rv, rv.Decode(data)
|
|
|
+}
|