package codes
import (
"errors"
"strconv"
)
var codeToString = map[Code]string{
Empty: "Empty",
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE",
Created: "Created",
Deleted: "Deleted",
Valid: "Valid",
Changed: "Changed",
Content: "Content",
BadRequest: "BadRequest",
Unauthorized: "Unauthorized",
BadOption: "BadOption",
Forbidden: "Forbidden",
NotFound: "NotFound",
MethodNotAllowed: "MethodNotAllowed",
NotAcceptable: "NotAcceptable",
PreconditionFailed: "PreconditionFailed",
RequestEntityTooLarge: "RequestEntityTooLarge",
UnsupportedMediaType: "UnsupportedMediaType",
TooManyRequests: "TooManyRequests",
InternalServerError: "InternalServerError",
NotImplemented: "NotImplemented",
BadGateway: "BadGateway",
ServiceUnavailable: "ServiceUnavailable",
GatewayTimeout: "GatewayTimeout",
ProxyingNotSupported: "ProxyingNotSupported",
CSM: "Capabilities and Settings Messages",
Ping: "Ping",
Pong: "Pong",
Release: "Release",
Abort: "Abort",
}
func (c Code) String() string {
val, ok := codeToString[c]
if ok {
return val
}
return "Code(" + strconv.FormatInt(int64(c), 10) + ")"
}
func ToCode(v string) (Code, error) {
for key, val := range codeToString {
if v == val {
return key, nil
}
}
return 0, errors.New("not found")
}
package codes
import (
"errors"
"fmt"
"strconv"
"github.com/plgd-dev/go-coap/v3/pkg/math"
)
// A Code is an unsigned 16-bit coap code as defined in the coap spec.
type Code uint16
// Request Codes
const (
GET Code = 1
POST Code = 2
PUT Code = 3
DELETE Code = 4
)
// Response Codes
const (
Empty Code = 0
Created Code = 65
Deleted Code = 66
Valid Code = 67
Changed Code = 68
Content Code = 69
Continue Code = 95
BadRequest Code = 128
Unauthorized Code = 129
BadOption Code = 130
Forbidden Code = 131
NotFound Code = 132
MethodNotAllowed Code = 133
NotAcceptable Code = 134
RequestEntityIncomplete Code = 136
PreconditionFailed Code = 140
RequestEntityTooLarge Code = 141
UnsupportedMediaType Code = 143
TooManyRequests Code = 157
InternalServerError Code = 160
NotImplemented Code = 161
BadGateway Code = 162
ServiceUnavailable Code = 163
GatewayTimeout Code = 164
ProxyingNotSupported Code = 165
)
// Signaling Codes for TCP
const (
CSM Code = 225
Ping Code = 226
Pong Code = 227
Release Code = 228
Abort Code = 229
)
const _maxCode = 255
var _maxCodeLen int
var strToCode = map[string]Code{
`"GET"`: GET,
`"POST"`: POST,
`"PUT"`: PUT,
`"DELETE"`: DELETE,
`"Created"`: Created,
`"Deleted"`: Deleted,
`"Valid"`: Valid,
`"Changed"`: Changed,
`"Content"`: Content,
`"BadRequest"`: BadRequest,
`"Unauthorized"`: Unauthorized,
`"BadOption"`: BadOption,
`"Forbidden"`: Forbidden,
`"NotFound"`: NotFound,
`"MethodNotAllowed"`: MethodNotAllowed,
`"NotAcceptable"`: NotAcceptable,
`"PreconditionFailed"`: PreconditionFailed,
`"RequestEntityTooLarge"`: RequestEntityTooLarge,
`"UnsupportedMediaType"`: UnsupportedMediaType,
`"TooManyRequests"`: TooManyRequests,
`"InternalServerError"`: InternalServerError,
`"NotImplemented"`: NotImplemented,
`"BadGateway"`: BadGateway,
`"ServiceUnavailable"`: ServiceUnavailable,
`"GatewayTimeout"`: GatewayTimeout,
`"ProxyingNotSupported"`: ProxyingNotSupported,
`"Capabilities and Settings Messages"`: CSM,
`"Ping"`: Ping,
`"Pong"`: Pong,
`"Release"`: Release,
`"Abort"`: Abort,
}
func getMaxCodeLen() int {
// max uint32 as string binary representation: "0b" + 32 digits
maxLen := 34
for k := range strToCode {
kLen := len(k)
if kLen > maxLen {
maxLen = kLen
}
}
return maxLen
}
func init() {
_maxCodeLen = getMaxCodeLen()
}
// UnmarshalJSON unmarshals b into the Code.
func (c *Code) UnmarshalJSON(b []byte) error {
// From json.Unmarshaler: By convention, to approximate the behavior of
// Unmarshal itself, Unmarshalers implement UnmarshalJSON([]byte("null")) as
// a no-op.
if string(b) == "null" {
return nil
}
if c == nil {
return errors.New("nil receiver passed to UnmarshalJSON")
}
if len(b) > _maxCodeLen {
return fmt.Errorf("invalid code: input too large(length=%d)", len(b))
}
if ci, err := strconv.ParseUint(string(b), 10, 32); err == nil {
if ci >= _maxCode {
return fmt.Errorf("invalid code: %q", ci)
}
*c = math.CastTo[Code](ci)
return nil
}
if jc, ok := strToCode[string(b)]; ok {
*c = jc
return nil
}
return fmt.Errorf("invalid code: %v", b)
}
package message
import (
"encoding/binary"
"github.com/plgd-dev/go-coap/v3/pkg/math"
)
func EncodeUint32(buf []byte, value uint32) (int, error) {
switch {
case value == 0:
return 0, nil
case value <= max1ByteNumber:
if len(buf) < 1 {
return 1, ErrTooSmall
}
buf[0] = byte(value)
return 1, nil
case value <= max2ByteNumber:
if len(buf) < 2 {
return 2, ErrTooSmall
}
binary.BigEndian.PutUint16(buf, math.CastTo[uint16](value))
return 2, nil
case value <= max3ByteNumber:
if len(buf) < 3 {
return 3, ErrTooSmall
}
rv := make([]byte, 4)
binary.BigEndian.PutUint32(rv, value)
copy(buf, rv[1:])
return 3, nil
default:
if len(buf) < 4 {
return 4, ErrTooSmall
}
binary.BigEndian.PutUint32(buf, value)
return 4, nil
}
}
func DecodeUint32(buf []byte) (uint32, int, error) {
if len(buf) > 4 {
buf = buf[:4]
}
tmp := []byte{0, 0, 0, 0}
copy(tmp[4-len(buf):], buf)
value := binary.BigEndian.Uint32(tmp)
return value, len(buf), nil
}
package message
import (
"encoding/binary"
"errors"
"hash/crc64"
"io"
)
// GetETag calculates ETag from payload via CRC64
func GetETag(r io.ReadSeeker) ([]byte, error) {
if r == nil {
return make([]byte, 8), nil
}
c64 := crc64.New(crc64.MakeTable(crc64.ISO))
orig, err := r.Seek(0, io.SeekCurrent)
if err != nil {
return nil, err
}
_, err = r.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
buf := make([]byte, 4096)
for {
bufR := buf
n, errR := r.Read(bufR)
if errors.Is(errR, io.EOF) {
break
}
if errR != nil {
return nil, errR
}
bufR = bufR[:n]
c64.Write(bufR)
}
_, err = r.Seek(orig, io.SeekStart)
if err != nil {
return nil, err
}
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, c64.Sum64())
return b, nil
}
package message
import (
"crypto/rand"
"encoding/hex"
"hash/crc64"
)
type Token []byte
func (t Token) String() string {
return hex.EncodeToString(t)
}
func (t Token) Hash() uint64 {
return crc64.Checksum(t, crc64.MakeTable(crc64.ISO))
}
// GetToken generates a random token by a given length
func GetToken() (Token, error) {
b := make(Token, 8)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}
package message
import (
"crypto/rand"
"encoding/binary"
"math"
"sync/atomic"
"time"
pkgMath "github.com/plgd-dev/go-coap/v3/pkg/math"
pkgRand "github.com/plgd-dev/go-coap/v3/pkg/rand"
)
var weakRng = pkgRand.NewRand(time.Now().UnixNano())
var msgID = uint32(RandMID())
// GetMID generates a message id for UDP. (0 <= mid <= 65535)
func GetMID() int32 {
return int32(pkgMath.CastTo[uint16](atomic.AddUint32(&msgID, 1)))
}
func RandMID() int32 {
b := make([]byte, 4)
_, err := rand.Read(b)
if err != nil {
// fallback to cryptographically insecure pseudo-random generator
return int32(pkgMath.CastTo[uint16](weakRng.Uint32() >> 16))
}
return int32(pkgMath.CastTo[uint16](binary.BigEndian.Uint32(b)))
}
// ValidateMID validates a message id for UDP. (0 <= mid <= 65535)
func ValidateMID(mid int32) bool {
return mid >= 0 && mid <= math.MaxUint16
}
package message
import (
"fmt"
"github.com/plgd-dev/go-coap/v3/message/codes"
)
// MaxTokenSize maximum of token size that can be used in message
const MaxTokenSize = 8
type Message struct {
Token Token
Options Options
Code codes.Code
Payload []byte
// For DTLS and UDP messages
MessageID int32 // uint16 is valid, all other values are invalid, -1 is used for unset
Type Type // uint8 is valid, all other values are invalid, -1 is used for unset
}
func (r *Message) String() string {
if r == nil {
return "nil"
}
buf := fmt.Sprintf("Code: %v, Token: %v", r.Code, r.Token)
path, err := r.Options.Path()
if err == nil {
buf = fmt.Sprintf("%s, Path: %v", buf, path)
}
cf, err := r.Options.ContentFormat()
if err == nil {
buf = fmt.Sprintf("%s, ContentFormat: %v", buf, cf)
}
queries, err := r.Options.Queries()
if err == nil {
buf = fmt.Sprintf("%s, Queries: %+v", buf, queries)
}
if ValidateType(r.Type) {
buf = fmt.Sprintf("%s, Type: %v", buf, r.Type)
}
if ValidateMID(r.MessageID) {
buf = fmt.Sprintf("%s, MessageID: %v", buf, r.MessageID)
}
if len(r.Payload) > 0 {
buf = fmt.Sprintf("%s, PayloadLen: %v", buf, len(r.Payload))
}
return buf
}
// IsPing returns true if the message is a ping.
func (r *Message) IsPing(isTCP bool) bool {
if isTCP {
return r.Code == codes.Ping
}
return r.Code == codes.Empty && r.Type == Confirmable && len(r.Token) == 0 && len(r.Options) == 0 && len(r.Payload) == 0
}
package message
import (
"encoding/binary"
"errors"
"strconv"
"github.com/plgd-dev/go-coap/v3/pkg/math"
"golang.org/x/exp/constraints"
)
const (
max1ByteNumber = uint32(^uint8(0))
max2ByteNumber = uint32(^uint16(0))
max3ByteNumber = uint32(0xffffff)
)
const (
ExtendOptionByteCode = 13
ExtendOptionByteAddend = 13
ExtendOptionWordCode = 14
ExtendOptionWordAddend = 269
ExtendOptionError = 15
)
// OptionID identifies an option in a message.
type OptionID uint16
/*
+-----+----+---+---+---+----------------+--------+--------+---------+
| No. | C | U | N | R | Name | Format | Length | Default |
+-----+----+---+---+---+----------------+--------+--------+---------+
| 1 | x | | | x | If-Match | opaque | 0-8 | (none) |
| 3 | x | x | - | | Uri-Host | string | 1-255 | (see |
| | | | | | | | | below) |
| 4 | | | | x | ETag | opaque | 1-8 | (none) |
| 5 | x | | | | If-None-Match | empty | 0 | (none) |
| 7 | x | x | - | | Uri-Port | uint | 0-2 | (see |
| | | | | | | | | below) |
| 8 | | | | x | Location-Path | string | 0-255 | (none) |
| 11 | x | x | - | x | Uri-Path | string | 0-255 | (none) |
| 12 | | | | | Content-Format | uint | 0-2 | (none) |
| 14 | | x | - | | Max-Age | uint | 0-4 | 60 |
| 15 | x | x | - | x | Uri-Query | string | 0-255 | (none) |
| 17 | x | | | | Accept | uint | 0-2 | (none) |
| 20 | | | | x | Location-Query | string | 0-255 | (none) |
| 23 | x | x | - | - | Block2 | uint | 0-3 | (none) |
| 27 | x | x | - | - | Block1 | uint | 0-3 | (none) |
| 28 | | | x | | Size2 | uint | 0-4 | (none) |
| 35 | x | x | - | | Proxy-Uri | string | 1-1034 | (none) |
| 39 | x | x | - | | Proxy-Scheme | string | 1-255 | (none) |
| 60 | | | x | | Size1 | uint | 0-4 | (none) |
+-----+----+---+---+---+----------------+--------+--------+---------+
C=Critical, U=Unsafe, N=NoCacheKey, R=Repeatable
*/
// 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
NoResponse OptionID = 258
)
var optionIDToString = map[OptionID]string{
IfMatch: "IfMatch",
URIHost: "URIHost",
ETag: "ETag",
IfNoneMatch: "IfNoneMatch",
Observe: "Observe",
URIPort: "URIPort",
LocationPath: "LocationPath",
URIPath: "URIPath",
ContentFormat: "ContentFormat",
MaxAge: "MaxAge",
URIQuery: "URIQuery",
Accept: "Accept",
LocationQuery: "LocationQuery",
Block2: "Block2",
Block1: "Block1",
Size2: "Size2",
ProxyURI: "ProxyURI",
ProxyScheme: "ProxyScheme",
Size1: "Size1",
NoResponse: "NoResponse",
}
func (o OptionID) String() string {
str, ok := optionIDToString[o]
if !ok {
return "Option(" + strconv.FormatInt(int64(o), 10) + ")"
}
return str
}
func ToOptionID(v string) (OptionID, error) {
for key, val := range optionIDToString {
if val == v {
return key, nil
}
}
return 0, errors.New("not found")
}
// Option value format (RFC7252 section 3.2)
type ValueFormat uint8
const (
ValueUnknown ValueFormat = iota
ValueEmpty
ValueOpaque
ValueUint
ValueString
)
type OptionDef struct {
MinLen uint32
MaxLen uint32
ValueFormat ValueFormat
}
var CoapOptionDefs = map[OptionID]OptionDef{
IfMatch: {ValueFormat: ValueOpaque, MinLen: 0, MaxLen: 8},
URIHost: {ValueFormat: ValueString, MinLen: 1, MaxLen: 255},
ETag: {ValueFormat: ValueOpaque, MinLen: 1, MaxLen: 8},
IfNoneMatch: {ValueFormat: ValueEmpty, MinLen: 0, MaxLen: 0},
Observe: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 3},
URIPort: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 2},
LocationPath: {ValueFormat: ValueString, MinLen: 0, MaxLen: 255},
URIPath: {ValueFormat: ValueString, MinLen: 0, MaxLen: 255},
ContentFormat: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 2},
MaxAge: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 4},
URIQuery: {ValueFormat: ValueString, MinLen: 0, MaxLen: 255},
Accept: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 2},
LocationQuery: {ValueFormat: ValueString, MinLen: 0, MaxLen: 255},
Block2: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 3},
Block1: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 3},
Size2: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 4},
ProxyURI: {ValueFormat: ValueString, MinLen: 1, MaxLen: 1034},
ProxyScheme: {ValueFormat: ValueString, MinLen: 1, MaxLen: 255},
Size1: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 4},
NoResponse: {ValueFormat: ValueUint, MinLen: 0, MaxLen: 1},
}
// MediaType specifies the content format of a message.
type MediaType uint16
// Content formats.
var (
TextPlain MediaType // text/plain; charset=utf-8
AppCoseEncrypt0 MediaType = 16 // application/cose; cose-type="cose-encrypt0" (RFC 8152)
AppCoseMac0 MediaType = 17 // application/cose; cose-type="cose-mac0" (RFC 8152)
AppCoseSign1 MediaType = 18 // application/cose; cose-type="cose-sign1" (RFC 8152)
AppLinkFormat MediaType = 40 // application/link-format
AppXML MediaType = 41 // application/xml
AppOctets MediaType = 42 // application/octet-stream
AppExi MediaType = 47 // application/exi
AppJSON MediaType = 50 // application/json
AppJSONPatch MediaType = 51 // application/json-patch+json (RFC6902)
AppJSONMergePatch MediaType = 52 // application/merge-patch+json (RFC7396)
AppCBOR MediaType = 60 // application/cbor (RFC 7049)
AppCWT MediaType = 61 // application/cwt
AppCoseEncrypt MediaType = 96 // application/cose; cose-type="cose-encrypt" (RFC 8152)
AppCoseMac MediaType = 97 // application/cose; cose-type="cose-mac" (RFC 8152)
AppCoseSign MediaType = 98 // application/cose; cose-type="cose-sign" (RFC 8152)
AppCoseKey MediaType = 101 // application/cose-key (RFC 8152)
AppCoseKeySet MediaType = 102 // application/cose-key-set (RFC 8152)
AppSenmlJSON MediaType = 110 // application/senml+json
AppSenmlCbor MediaType = 112 // application/senml+cbor
AppCoapGroup MediaType = 256 // coap-group+json (RFC 7390)
AppSenmlEtchJSON MediaType = 320 // application/senml-etch+json
AppSenmlEtchCbor MediaType = 322 // application/senml-etch+cbor
AppOcfCbor MediaType = 10000 // application/vnd.ocf+cbor
AppLwm2mTLV MediaType = 11542 // application/vnd.oma.lwm2m+tlv
AppLwm2mJSON MediaType = 11543 // application/vnd.oma.lwm2m+json
AppLwm2mCbor MediaType = 11544 // application/vnd.oma.lwm2m+cbor
)
var mediaTypeToString = map[MediaType]string{
TextPlain: "text/plain; charset=utf-8",
AppCoseEncrypt0: "application/cose; cose-type=\"cose-encrypt0\"",
AppCoseMac0: "application/cose; cose-type=\"cose-mac0\"",
AppCoseSign1: "application/cose; cose-type=\"cose-sign1\"",
AppLinkFormat: "application/link-format",
AppXML: "application/xml",
AppOctets: "application/octet-stream",
AppExi: "application/exi",
AppJSON: "application/json",
AppJSONPatch: "application/json-patch+json",
AppJSONMergePatch: "application/merge-patch+json",
AppCBOR: "application/cbor",
AppCWT: "application/cwt",
AppCoseEncrypt: "application/cose; cose-type=\"cose-encrypt\"",
AppCoseMac: "application/cose; cose-type=\"cose-mac\"",
AppCoseSign: "application/cose; cose-type=\"cose-sign\"",
AppCoseKey: "application/cose-key",
AppCoseKeySet: "application/cose-key-set",
AppSenmlJSON: "application/senml+json",
AppSenmlCbor: "application/senml+cbor",
AppCoapGroup: "coap-group+json",
AppSenmlEtchJSON: "application/senml-etch+json",
AppSenmlEtchCbor: "application/senml-etch+cbor",
AppOcfCbor: "application/vnd.ocf+cbor",
AppLwm2mTLV: "application/vnd.oma.lwm2m+tlv",
AppLwm2mJSON: "application/vnd.oma.lwm2m+json",
AppLwm2mCbor: "application/vnd.oma.lwm2m+cbor",
}
func (c MediaType) String() string {
str, ok := mediaTypeToString[c]
if !ok {
return "MediaType(" + strconv.FormatInt(int64(c), 10) + ")"
}
return str
}
func ToMediaType(v string) (MediaType, error) {
for key, val := range mediaTypeToString {
if val == v {
return key, nil
}
}
return 0, errors.New("not found")
}
func MediaTypeFromNumber[T constraints.Integer](v T) (MediaType, error) {
mt, err := math.SafeCastTo[MediaType](v)
if err != nil {
return MediaType(0), err
}
_, ok := mediaTypeToString[mt]
if !ok {
return MediaType(0), errors.New("invalid value")
}
return mt, nil
}
func extendOpt(opt int) (int, int) {
ext := 0
if opt >= ExtendOptionByteAddend {
if opt >= ExtendOptionWordAddend {
ext = opt - ExtendOptionWordAddend
opt = ExtendOptionWordCode
} else {
ext = opt - ExtendOptionByteAddend
opt = ExtendOptionByteCode
}
}
return opt, ext
}
// VerifyOptLen checks whether valueLen is within (min, max) length limits for given option.
func VerifyOptLen(optID OptionID, valueLen int) bool {
def := CoapOptionDefs[optID]
if valueLen < int(def.MinLen) || valueLen > int(def.MaxLen) {
return false
}
return true
}
func marshalOptionHeaderExt(buf []byte, opt, ext int) (int, error) {
switch opt {
case ExtendOptionByteCode:
if len(buf) > 0 {
buf[0] = byte(ext)
return 1, nil
}
return 1, ErrTooSmall
case ExtendOptionWordCode:
if len(buf) > 1 {
binary.BigEndian.PutUint16(buf, math.CastTo[uint16](ext))
return 2, nil
}
return 2, ErrTooSmall
}
return 0, nil
}
func marshalOptionHeader(buf []byte, delta, length int) (int, error) {
size := 0
d, dx := extendOpt(delta)
l, lx := extendOpt(length)
if len(buf) > 0 {
buf[0] = byte(d<<4) | byte(l)
size++
} else {
buf = nil
size++
}
var lenBuf int
var err error
if buf == nil {
lenBuf, err = marshalOptionHeaderExt(nil, d, dx)
} else {
lenBuf, err = marshalOptionHeaderExt(buf[size:], d, dx)
}
switch {
case err == nil:
case errors.Is(err, ErrTooSmall):
buf = nil
default:
return -1, err
}
size += lenBuf
if buf == nil {
lenBuf, err = marshalOptionHeaderExt(nil, l, lx)
} else {
lenBuf, err = marshalOptionHeaderExt(buf[size:], l, lx)
}
switch {
case err == nil:
case errors.Is(err, ErrTooSmall):
buf = nil
default:
return -1, err
}
size += lenBuf
if buf == nil {
return size, ErrTooSmall
}
return size, nil
}
type Option struct {
Value []byte
ID OptionID
}
func (o Option) MarshalValue(buf []byte) (int, error) {
if len(buf) < len(o.Value) {
return len(o.Value), ErrTooSmall
}
copy(buf, o.Value)
return len(o.Value), nil
}
func (o *Option) UnmarshalValue(buf []byte) (int, error) {
o.Value = buf
return len(buf), nil
}
func (o Option) Marshal(buf []byte, previousID OptionID) (int, error) {
/*
0 1 2 3 4 5 6 7
+---------------+---------------+
| | |
| Option Delta | Option Length | 1 byte
| | |
+---------------+---------------+
\ \
/ Option Delta / 0-2 bytes
\ (extended) \
+-------------------------------+
\ \
/ Option Length / 0-2 bytes
\ (extended) \
+-------------------------------+
\ \
/ /
\ \
/ Option Value / 0 or more bytes
\ \
/ /
\ \
+-------------------------------+
*/
delta := int(o.ID) - int(previousID)
lenBuf, err := o.MarshalValue(nil)
switch {
case err == nil, errors.Is(err, ErrTooSmall):
default:
return -1, err
}
// header marshal
lenBuf, err = marshalOptionHeader(buf, delta, lenBuf)
switch {
case err == nil:
case errors.Is(err, ErrTooSmall):
buf = nil
default:
return -1, err
}
length := lenBuf
if buf == nil {
lenBuf, err = o.MarshalValue(nil)
} else {
lenBuf, err = o.MarshalValue(buf[length:])
}
switch {
case err == nil:
case errors.Is(err, ErrTooSmall):
buf = nil
default:
return -1, err
}
length += lenBuf
if buf == nil {
return length, ErrTooSmall
}
return length, nil
}
func parseExtOpt(data []byte, opt int) (int, int, error) {
processed := 0
switch opt {
case ExtendOptionByteCode:
if len(data) < 1 {
return 0, -1, ErrOptionTruncated
}
opt = int(data[0]) + ExtendOptionByteAddend
processed = 1
case ExtendOptionWordCode:
if len(data) < 2 {
return 0, -1, ErrOptionTruncated
}
opt = int(binary.BigEndian.Uint16(data[:2])) + ExtendOptionWordAddend
processed = 2
}
return processed, opt, nil
}
func (o *Option) Unmarshal(data []byte, optionDefs map[OptionID]OptionDef, optionID OptionID) (int, error) {
if def, ok := optionDefs[optionID]; ok {
if def.ValueFormat == ValueUnknown {
// Skip unrecognized options (RFC7252 section 5.4.1)
return len(data), nil
}
dataLen := math.CastTo[uint32](len(data))
if dataLen < def.MinLen || dataLen > def.MaxLen {
// Skip options with illegal value length (RFC7252 section 5.4.3)
return len(data), nil
}
}
o.ID = optionID
proc, err := o.UnmarshalValue(data)
if err != nil {
return -1, err
}
return proc, err
}
package message
import (
"errors"
"fmt"
"strings"
"github.com/plgd-dev/go-coap/v3/pkg/math"
)
// Options Container of COAP Options, It must be always sort'ed after modification.
type Options []Option
const maxPathValue = 255
// GetPathBufferSize gets the size of the buffer required to store path in URI-Path options.
//
// If the path cannot be stored an error is returned.
func GetPathBufferSize(path string) (int, error) {
size := 0
for start := 0; start < len(path); {
subPath := path[start:]
segmentSize := strings.Index(subPath, "/")
if segmentSize == 0 {
start++
continue
}
if segmentSize < 0 {
segmentSize = len(subPath)
}
if segmentSize > maxPathValue {
return -1, ErrInvalidValueLength
}
size += segmentSize
start += segmentSize + 1
}
return size, nil
}
func setPath(options Options, optionID OptionID, buf []byte, path string) (Options, int, error) {
if len(path) == 0 {
return options, 0, nil
}
o := options.Remove(optionID)
if path[0] == '/' {
path = path[1:]
}
requiredSize, err := GetPathBufferSize(path)
if err != nil {
return options, -1, err
}
if requiredSize > len(buf) {
return options, -1, ErrTooSmall
}
encoded := 0
for start := 0; start < len(path); {
subPath := path[start:]
end := strings.Index(subPath, "/")
if end == 0 {
start++
continue
}
if end < 0 {
end = len(subPath)
}
data := buf[encoded:]
var enc int
var err error
o, enc, err = o.AddString(data, optionID, subPath[:end])
if err != nil {
return o, -1, err
}
encoded += enc
start += end + 1
}
return o, encoded, nil
}
// SetPath splits path by '/' to URIPath options and copies it to buffer.
//
// Returns modified options, number of used buf bytes and error if occurs.
//
// @note the url encoded into URIHost, URIPort, URIPath is expected to be
// absolute (https://www.rfc-editor.org/rfc/rfc7252.txt)
func (options Options) SetPath(buf []byte, path string) (Options, int, error) {
return setPath(options, URIPath, buf, path)
}
// SetLocationPath splits path by '/' to LocationPath options and copies it to buffer.
//
// Returns modified options, number of used buf bytes and error if occurs.
//
// @note the url encoded into LocationPath is expected to be
// absolute (https://www.rfc-editor.org/rfc/rfc7252.txt)
func (options Options) SetLocationPath(buf []byte, path string) (Options, int, error) {
return setPath(options, LocationPath, buf, path)
}
func (options Options) path(buf []byte, id OptionID) (int, error) {
firstIdx, lastIdx, err := options.Find(id)
if err != nil {
return -1, err
}
var needed int
for i := firstIdx; i < lastIdx; i++ {
needed += len(options[i].Value)
needed++
}
if len(buf) < needed {
return needed, ErrTooSmall
}
for i := firstIdx; i < lastIdx; i++ {
buf[0] = '/'
buf = buf[1:]
copy(buf, options[i].Value)
buf = buf[len(options[i].Value):]
}
return needed, nil
}
// Path joins URIPath options by '/' to the buf.
//
// Returns number of used buf bytes or error when occurs.
func (options Options) Path() (string, error) {
buf := make([]byte, 32)
m, err := options.path(buf, URIPath)
if errors.Is(err, ErrTooSmall) {
buf = append(buf, make([]byte, m)...)
m, err = options.path(buf, URIPath)
}
if err != nil {
return "", err
}
buf = buf[:m]
return string(buf), nil
}
// LocationPath joins Location-Path options by '/' to the buf.
//
// Returns number of used buf bytes or error when occurs.
func (options Options) LocationPath() (string, error) {
buf := make([]byte, 32)
m, err := options.path(buf, LocationPath)
if errors.Is(err, ErrTooSmall) {
buf = append(buf, make([]byte, m)...)
m, err = options.path(buf, LocationPath)
}
if err != nil {
return "", err
}
buf = buf[:m]
return string(buf), nil
}
// SetString replaces/stores string option to options.
//
// Returns modified options, number of used buf bytes and error if occurs.
func (options Options) SetString(buf []byte, id OptionID, str string) (Options, int, error) {
data := []byte(str)
return options.SetBytes(buf, id, data)
}
// AddString appends string option to options.
//
// Returns modified options, number of used buf bytes and error if occurs.
func (options Options) AddString(buf []byte, id OptionID, str string) (Options, int, error) {
data := []byte(str)
return options.AddBytes(buf, id, data)
}
// HasOption returns true is option exist in options.
func (options Options) HasOption(id OptionID) bool {
_, _, err := options.Find(id)
return err == nil
}
// GetUint32s gets all options with same id.
func (options Options) GetUint32s(id OptionID, r []uint32) (int, error) {
firstIdx, lastIdx, err := options.Find(id)
if err != nil {
return 0, err
}
if len(r) < lastIdx-firstIdx {
return lastIdx - firstIdx, ErrTooSmall
}
var idx int
for i := firstIdx; i <= lastIdx; i++ {
val, _, err := DecodeUint32(options[i].Value)
if err == nil {
r[idx] = val
idx++
}
}
return idx, nil
}
// GetUint32 gets the uin32 value of the first option with the given ID.
func (options Options) GetUint32(id OptionID) (uint32, error) {
firstIdx, _, err := options.Find(id)
if err != nil {
return 0, err
}
val, _, err := DecodeUint32(options[firstIdx].Value)
return val, err
}
// ContentFormat gets the content format of body.
func (options Options) ContentFormat() (MediaType, error) {
v, err := options.GetUint32(ContentFormat)
return math.CastTo[MediaType](v), err
}
// GetString gets the string value of the first option with the given ID.
func (options Options) GetString(id OptionID) (string, error) {
firstIdx, _, err := options.Find(id)
if err != nil {
return "", err
}
return string(options[firstIdx].Value), nil
}
// GetStrings gets string array of all options with the given id.
func (options Options) GetStrings(id OptionID, r []string) (int, error) {
firstIdx, lastIdx, err := options.Find(id)
if err != nil {
return 0, err
}
if len(r) < lastIdx-firstIdx {
return lastIdx - firstIdx, ErrTooSmall
}
var idx int
for i := firstIdx; i < lastIdx; i++ {
r[idx] = string(options[i].Value)
idx++
}
return idx, nil
}
// Queries gets URIQuery parameters.
func (options Options) Queries() ([]string, error) {
q := make([]string, 4)
n, err := options.GetStrings(URIQuery, q)
if errors.Is(err, ErrTooSmall) {
q = append(q, make([]string, n-len(q))...)
n, err = options.GetStrings(URIQuery, q)
}
if err != nil {
return nil, err
}
return q[:n], nil
}
// SetBytes replaces/stores bytes of a option to options.
//
// Returns modified options, number of used buf bytes and error if occurs.
func (options Options) SetBytes(buf []byte, id OptionID, data []byte) (Options, int, error) {
if len(buf) < len(data) {
return options, len(data), ErrTooSmall
}
if id == URIPath && len(data) > maxPathValue {
return options, -1, ErrInvalidValueLength
}
copy(buf, data)
return options.Set(Option{ID: id, Value: buf[:len(data)]}), len(data), nil
}
// AddBytes appends bytes of a option to options.
//
// Returns modified options, number of used buf bytes and error if occurs.
func (options Options) AddBytes(buf []byte, id OptionID, data []byte) (Options, int, error) {
if len(buf) < len(data) {
return options, len(data), ErrTooSmall
}
if id == URIPath && len(data) > maxPathValue {
return options, -1, ErrInvalidValueLength
}
copy(buf, data)
return options.Add(Option{ID: id, Value: buf[:len(data)]}), len(data), nil
}
// GetBytes gets bytes of the first option with given id.
func (options Options) GetBytes(id OptionID) ([]byte, error) {
firstIdx, _, err := options.Find(id)
if err != nil {
return nil, err
}
return options[firstIdx].Value, nil
}
// GetBytess gets array of bytes of all options with the same id.
func (options Options) GetBytess(id OptionID, r [][]byte) (int, error) {
firstIdx, lastIdx, err := options.Find(id)
if err != nil {
return 0, err
}
if len(r) < lastIdx-firstIdx {
return lastIdx - firstIdx, ErrTooSmall
}
var idx int
for i := firstIdx; i < lastIdx; i++ {
r[idx] = options[i].Value
idx++
}
return idx, nil
}
// AddUint32 appends uint32 option to options.
//
// Returns modified options, number of used buf bytes and error if occurs.
func (options Options) AddUint32(buf []byte, id OptionID, value uint32) (Options, int, error) {
enc, err := EncodeUint32(buf, value)
if err != nil {
return options, enc, err
}
o := options.Add(Option{ID: id, Value: buf[:enc]})
return o, enc, err
}
// SetUint32 replaces/stores uint32 option to options.
//
// Returns modified options, number of used buf bytes and error if occurs.
func (options Options) SetUint32(buf []byte, id OptionID, value uint32) (Options, int, error) {
enc, err := EncodeUint32(buf, value)
if err != nil {
return options, enc, err
}
o := options.Set(Option{ID: id, Value: buf[:enc]})
return o, enc, err
}
// SetContentFormat sets ContentFormat option.
func (options Options) SetContentFormat(buf []byte, contentFormat MediaType) (Options, int, error) {
return options.SetUint32(buf, ContentFormat, uint32(contentFormat))
}
// SetObserve sets Observe option.
func (options Options) SetObserve(buf []byte, observe uint32) (Options, int, error) {
return options.SetUint32(buf, Observe, observe)
}
// Observe gets Observe option.
func (options Options) Observe() (uint32, error) {
return options.GetUint32(Observe)
}
// SetAccept sets accept option.
func (options Options) SetAccept(buf []byte, contentFormat MediaType) (Options, int, error) {
return options.SetUint32(buf, Accept, uint32(contentFormat))
}
// Accept gets accept option.
func (options Options) Accept() (MediaType, error) {
v, err := options.GetUint32(Accept)
return math.CastTo[MediaType](v), err
}
// Find returns range of type options. First number is index and second number is index of next option type.
func (options Options) Find(id OptionID) (int, int, error) {
idxPre, idxPost := options.findPosition(id)
if idxPre == -1 && idxPost == 0 {
return -1, -1, ErrOptionNotFound
}
if idxPre == len(options)-1 && idxPost == -1 {
return -1, -1, ErrOptionNotFound
}
if idxPre < idxPost && idxPost-idxPre == 1 {
return -1, -1, ErrOptionNotFound
}
idxPre++
if idxPost < 0 {
idxPost = len(options)
}
return idxPre, idxPost, nil
}
// findPosition returns opened interval, -1 at means minIdx insert at 0, -1 maxIdx at maxIdx means append.
func (options Options) findPosition(id OptionID) (minIdx int, maxIdx int) {
if len(options) == 0 {
return -1, 0
}
pivot := 0
maxIdx = len(options)
minIdx = 0
for {
switch {
case id == options[pivot].ID || (maxIdx-minIdx)/2 == 0:
for maxIdx = pivot; maxIdx < len(options) && options[maxIdx].ID <= id; {
maxIdx++
}
if maxIdx == len(options) {
maxIdx = -1
}
for minIdx = pivot; minIdx >= 0 && options[minIdx].ID >= id; {
minIdx--
}
return minIdx, maxIdx
case id < options[pivot].ID:
maxIdx = pivot
pivot = maxIdx - (maxIdx-minIdx)/2
case id > options[pivot].ID:
minIdx = pivot
pivot = minIdx + (maxIdx-minIdx)/2
}
}
}
// Set replaces/stores option at options.
//
// Returns modified options.
func (options Options) Set(opt Option) Options {
idxPre, idxPost := options.findPosition(opt.ID)
if idxPre == -1 && idxPost == -1 {
// append
options = append(options[:0], opt)
return options
}
var insertPosition int
var updateTo int
var updateFrom int
optsLength := len(options)
switch {
case idxPre == -1 && idxPost >= 0:
insertPosition = 0
updateTo = 1
updateFrom = idxPost
case idxPre == idxPost:
insertPosition = idxPre
updateFrom = idxPre
updateTo = idxPre + 1
case idxPre >= 0:
insertPosition = idxPre + 1
updateTo = idxPre + 2
updateFrom = idxPost
if updateFrom < 0 {
updateFrom = len(options)
}
if updateTo == updateFrom {
options[insertPosition] = opt
return options
}
}
if len(options) == cap(options) {
options = append(options, Option{})
} else {
options = options[:len(options)+1]
}
// replace + move
updateIdx := updateTo
if updateFrom < updateTo {
for i := optsLength; i > updateFrom; i-- {
options[i] = options[i-1]
updateIdx++
}
} else {
for i := updateFrom; i < optsLength; i++ {
options[updateIdx] = options[i]
updateIdx++
}
}
options[insertPosition] = opt
options = options[:updateIdx]
return options
}
// Add appends option to options.
func (options Options) Add(opt Option) Options {
_, idxPost := options.findPosition(opt.ID)
if idxPost == -1 {
idxPost = len(options)
}
if len(options) == cap(options) {
options = append(options, Option{})
} else {
options = options[:len(options)+1]
}
for i := len(options) - 1; i > idxPost; i-- {
options[i] = options[i-1]
}
options[idxPost] = opt
return options
}
// Remove removes all options with ID.
func (options Options) Remove(id OptionID) Options {
idxPre, idxPost, err := options.Find(id)
if err != nil {
return options
}
updateIdx := idxPre
for i := idxPost; i < len(options); i++ {
options[updateIdx] = options[i]
updateIdx++
}
length := len(options) - (idxPost - idxPre)
options = options[:length]
return options
}
// Marshal marshals options to buf.
//
// Returns the number of used buf bytes.
func (options Options) Marshal(buf []byte) (int, error) {
previousID := OptionID(0)
length := 0
for _, o := range options {
// return coap.error but calculate length
if length > len(buf) {
buf = nil
}
var optionLength int
var err error
if buf != nil {
optionLength, err = o.Marshal(buf[length:], previousID)
} else {
optionLength, err = o.Marshal(nil, previousID)
}
previousID = o.ID
switch {
case err == nil:
case errors.Is(err, ErrTooSmall):
buf = nil
default:
return -1, err
}
length += optionLength
}
if buf == nil {
return length, ErrTooSmall
}
return length, nil
}
// Unmarshal unmarshals data bytes to options and returns the number of consumed bytes.
func (options *Options) Unmarshal(data []byte, optionDefs map[OptionID]OptionDef) (int, error) {
prev := 0
processed := 0
for len(data) > 0 {
if data[0] == 0xff {
processed++
break
}
delta := int(data[0] >> 4)
length := int(data[0] & 0x0f)
if delta == ExtendOptionError || length == ExtendOptionError {
return -1, ErrOptionUnexpectedExtendMarker
}
data = data[1:]
processed++
proc, delta, err := parseExtOpt(data, delta)
if err != nil {
return -1, err
}
processed += proc
data = data[proc:]
proc, length, err = parseExtOpt(data, length)
if err != nil {
return -1, err
}
processed += proc
data = data[proc:]
if len(data) < length {
return -1, ErrOptionTruncated
}
option := Option{}
oid, err := math.SafeCastTo[OptionID](prev + delta)
if err != nil {
return -1, fmt.Errorf("%w: %w", ErrOptionNotFound, err)
}
proc, err = option.Unmarshal(data[:length], optionDefs, oid)
if err != nil {
return -1, err
}
if cap(*options) == len(*options) {
return -1, ErrOptionsTooSmall
}
if option.ID != 0 {
(*options) = append(*options, option)
}
processed += proc
data = data[proc:]
prev = int(oid)
}
return processed, nil
}
// ResetOptionsTo resets options to in options.
//
// Returns modified options, number of used buf bytes and error if occurs.
func (options Options) ResetOptionsTo(buf []byte, in Options) (Options, int, error) {
opts := options[:0]
used := 0
for idx, o := range in {
if len(buf) < len(o.Value) {
for i := idx; i < len(in); i++ {
used += len(in[i].Value)
}
return options, used, ErrTooSmall
}
copy(buf, o.Value)
used += len(o.Value)
opts = opts.Add(Option{
ID: o.ID,
Value: buf[:len(o.Value)],
})
buf = buf[len(o.Value):]
}
return opts, used, nil
}
// Clone create duplicates of options.
func (options Options) Clone() (Options, error) {
opts := make(Options, 0, len(options))
buf := make([]byte, 64)
opts, used, err := opts.ResetOptionsTo(buf, options)
if errors.Is(err, ErrTooSmall) {
buf = append(buf, make([]byte, used-len(buf))...)
opts, _, err = opts.ResetOptionsTo(buf, options)
}
if err != nil {
return nil, err
}
return opts, nil
}
package message
import (
"math"
"strconv"
)
// Type represents the message type.
// It's only part of CoAP UDP messages.
// Reliable transports like TCP do not have a type.
type Type int16
const (
// Used for unset
Unset Type = -1
// Confirmable messages require acknowledgements.
Confirmable Type = 0
// NonConfirmable messages do not require acknowledgements.
NonConfirmable Type = 1
// Acknowledgement is a message indicating a response to confirmable message.
Acknowledgement Type = 2
// Reset indicates a permanent negative acknowledgement.
Reset Type = 3
)
var typeToString = map[Type]string{
Unset: "Unset",
Confirmable: "Confirmable",
NonConfirmable: "NonConfirmable",
Acknowledgement: "Acknowledgement",
Reset: "Reset",
}
func (t Type) String() string {
val, ok := typeToString[t]
if ok {
return val
}
return "Type(" + strconv.FormatInt(int64(t), 10) + ")"
}
// ValidateType validates the type for UDP. (0 <= typ <= 255)
func ValidateType(typ Type) bool {
return typ >= 0 && typ <= math.MaxUint8
}
package coder
import (
"encoding/binary"
"errors"
"github.com/plgd-dev/go-coap/v3/message"
"github.com/plgd-dev/go-coap/v3/message/codes"
"github.com/plgd-dev/go-coap/v3/pkg/math"
)
var DefaultCoder = new(Coder)
const (
MessageLength13Base = 13
MessageLength14Base = 269
MessageLength15Base = 65805
messageMaxLen = 0x7fff0000 // Large number that works in 32-bit builds
)
type Coder struct{}
type MessageHeader struct {
Token []byte
Length uint32
MessageLength uint32
Code codes.Code
}
func (c *Coder) Size(m message.Message) (int, error) {
size, err := c.Encode(m, nil)
if errors.Is(err, message.ErrTooSmall) {
err = nil
}
return size, err
}
func getHeader(messageLength int) (uint8, []byte) {
if messageLength < MessageLength13Base {
return math.CastTo[uint8](messageLength), nil
}
if messageLength < MessageLength14Base {
extLen := messageLength - MessageLength13Base
extLenBytes := []byte{math.CastTo[uint8](extLen)}
return 13, extLenBytes
}
if messageLength < MessageLength15Base {
extLen := messageLength - MessageLength14Base
extLenBytes := make([]byte, 2)
binary.BigEndian.PutUint16(extLenBytes, math.CastTo[uint16](extLen))
return 14, extLenBytes
}
if messageLength < messageMaxLen {
extLen := messageLength - MessageLength15Base
extLenBytes := make([]byte, 4)
binary.BigEndian.PutUint32(extLenBytes, math.CastTo[uint32](extLen))
return 15, extLenBytes
}
return 0, nil
}
func (c *Coder) Encode(m message.Message, buf []byte) (int, error) {
/*
A CoAP Message message lomessage.OKs like:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Len | TKL | Extended Length ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Code | TKL bytes ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 1 1 1 1 1 1 1| Payload (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The size of the Extended Length field is inferred from the value of the
Len field as follows:
| Len value | Extended Length size | Total length |
+------------+-----------------------+---------------------------+
| 0-12 | 0 | Len |
| 13 | 1 | Extended Length + 13 |
| 14 | 2 | Extended Length + 269 |
| 15 | 4 | Extended Length + 65805 |
*/
if len(m.Token) > message.MaxTokenSize {
return -1, message.ErrInvalidTokenLen
}
payloadLen := len(m.Payload)
if payloadLen > 0 {
// for separator 0xff
payloadLen++
}
optionsLen, err := m.Options.Marshal(nil)
if !errors.Is(err, message.ErrTooSmall) {
return -1, err
}
bufLen := payloadLen + optionsLen
lenNib, extLenBytes := getHeader(bufLen)
var hdr [1 + 4 + message.MaxTokenSize + 1]byte
hdrLen := 1 + len(extLenBytes) + len(m.Token) + 1
hdrOff := 0
copyToHdr := func(offset int, data []byte) int {
if len(data) > 0 {
copy(hdr[hdrOff:hdrOff+len(data)], data)
offset += len(data)
}
return offset
}
// Length and TKL nibbles.
hdr[hdrOff] = math.CastTo[uint8](len(m.Token)) | (lenNib << 4)
hdrOff++
// Extended length, if present.
hdrOff = copyToHdr(hdrOff, extLenBytes)
// Code.
hdr[hdrOff] = byte(m.Code)
hdrOff++
// Token.
copyToHdr(hdrOff, m.Token)
bufLen += hdrLen
if len(buf) < bufLen {
return bufLen, message.ErrTooSmall
}
copy(buf, hdr[:hdrLen])
optionsLen, err = m.Options.Marshal(buf[hdrLen:])
switch {
case err == nil:
case errors.Is(err, message.ErrTooSmall):
return bufLen, err
default:
return -1, err
}
if len(m.Payload) > 0 {
copy(buf[hdrLen+optionsLen:], []byte{0xff})
copy(buf[hdrLen+optionsLen+1:], m.Payload)
}
return bufLen, nil
}
func (c *Coder) DecodeHeader(data []byte, h *MessageHeader) (int, error) {
hdrOff := uint32(0)
if len(data) == 0 {
return -1, message.ErrShortRead
}
firstByte := data[0]
data = data[1:]
hdrOff++
lenNib := (firstByte & 0xf0) >> 4
tkl := firstByte & 0x0f
var opLen int
switch {
case lenNib < MessageLength13Base:
opLen = int(lenNib)
case lenNib == 13:
if len(data) < 1 {
return -1, message.ErrShortRead
}
extLen := data[0]
data = data[1:]
hdrOff++
opLen = MessageLength13Base + int(extLen)
case lenNib == 14:
if len(data) < 2 {
return -1, message.ErrShortRead
}
extLen := binary.BigEndian.Uint16(data)
data = data[2:]
hdrOff += 2
opLen = MessageLength14Base + int(extLen)
case lenNib == 15:
if len(data) < 4 {
return -1, message.ErrShortRead
}
extLen := binary.BigEndian.Uint32(data)
data = data[4:]
hdrOff += 4
opLen = MessageLength15Base + int(extLen)
}
h.MessageLength = hdrOff + 1 + uint32(tkl) + math.CastTo[uint32](opLen)
if len(data) < 1 {
return -1, message.ErrShortRead
}
h.Code = codes.Code(data[0])
data = data[1:]
hdrOff++
if len(data) < int(tkl) {
return -1, message.ErrShortRead
}
if tkl > 0 {
h.Token = data[:tkl]
}
hdrOff += uint32(tkl)
h.Length = hdrOff
return int(h.Length), nil
}
func (c *Coder) DecodeWithHeader(data []byte, header MessageHeader, m *message.Message) (int, error) {
optionDefs := message.CoapOptionDefs
processed := header.Length
switch header.Code {
case codes.CSM:
optionDefs = message.TCPSignalCSMOptionDefs
case codes.Ping, codes.Pong:
optionDefs = message.TCPSignalPingPongOptionDefs
case codes.Release:
optionDefs = message.TCPSignalReleaseOptionDefs
case codes.Abort:
optionDefs = message.TCPSignalAbortOptionDefs
}
proc, err := m.Options.Unmarshal(data, optionDefs)
if err != nil {
return -1, err
}
data = data[proc:]
processed += math.CastTo[uint32](proc)
if len(data) > 0 {
m.Payload = data
}
processed += math.CastTo[uint32](len(data))
m.Code = header.Code
m.Token = header.Token
return int(processed), nil
}
func (c *Coder) Decode(data []byte, m *message.Message) (int, error) {
var header MessageHeader
_, err := c.DecodeHeader(data, &header)
if err != nil {
return -1, err
}
if math.CastTo[uint32](len(data)) < header.MessageLength {
return -1, message.ErrShortRead
}
return c.DecodeWithHeader(data[header.Length:], header, m)
}
package coder
import (
"encoding/binary"
"errors"
"fmt"
"github.com/plgd-dev/go-coap/v3/message"
"github.com/plgd-dev/go-coap/v3/message/codes"
"github.com/plgd-dev/go-coap/v3/pkg/math"
)
var DefaultCoder = new(Coder)
type Coder struct{}
func (c *Coder) Size(m message.Message) (int, error) {
if len(m.Token) > message.MaxTokenSize {
return -1, message.ErrInvalidTokenLen
}
size := 4 + len(m.Token)
payloadLen := len(m.Payload)
optionsLen, err := m.Options.Marshal(nil)
if !errors.Is(err, message.ErrTooSmall) {
return -1, err
}
if payloadLen > 0 {
// for separator 0xff
payloadLen++
}
size += payloadLen + optionsLen
return size, nil
}
func (c *Coder) Encode(m message.Message, buf []byte) (int, error) {
/*
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Ver| T | TKL | Code | Message ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Token (if any, TKL bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 1 1 1 1 1 1 1| Payload (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
if !message.ValidateMID(m.MessageID) {
return -1, fmt.Errorf("invalid MessageID(%v)", m.MessageID)
}
if !message.ValidateType(m.Type) {
return -1, fmt.Errorf("invalid Type(%v)", m.Type)
}
size, err := c.Size(m)
if err != nil {
return -1, err
}
if len(buf) < size {
return size, message.ErrTooSmall
}
tmpbuf := []byte{0, 0}
// safe: checked by message.ValidateMID above
binary.BigEndian.PutUint16(tmpbuf, math.CastTo[uint16](m.MessageID))
buf[0] = (1 << 6) | byte(m.Type)<<4 | byte(0xf&len(m.Token))
buf[1] = byte(m.Code)
buf[2] = tmpbuf[0]
buf[3] = tmpbuf[1]
buf = buf[4:]
if len(m.Token) > message.MaxTokenSize {
return -1, message.ErrInvalidTokenLen
}
copy(buf, m.Token)
buf = buf[len(m.Token):]
optionsLen, err := m.Options.Marshal(buf)
switch {
case err == nil:
case errors.Is(err, message.ErrTooSmall):
return size, err
default:
return -1, err
}
buf = buf[optionsLen:]
if len(m.Payload) > 0 {
buf[0] = 0xff
buf = buf[1:]
}
copy(buf, m.Payload)
return size, nil
}
func (c *Coder) Decode(data []byte, m *message.Message) (int, error) {
size := len(data)
if size < 4 {
return -1, ErrMessageTruncated
}
if data[0]>>6 != 1 {
return -1, ErrMessageInvalidVersion
}
typ := message.Type((data[0] >> 4) & 0x3)
tokenLen := int(data[0] & 0xf)
if tokenLen > 8 {
return -1, message.ErrInvalidTokenLen
}
code := codes.Code(data[1])
messageID := binary.BigEndian.Uint16(data[2:4])
data = data[4:]
if len(data) < tokenLen {
return -1, ErrMessageTruncated
}
token := data[:tokenLen]
if len(token) == 0 {
token = nil
}
data = data[tokenLen:]
optionDefs := message.CoapOptionDefs
proc, err := m.Options.Unmarshal(data, optionDefs)
if err != nil {
return -1, err
}
data = data[proc:]
if len(data) == 0 {
data = nil
}
m.Payload = data
m.Code = code
m.Token = token
m.Type = typ
m.MessageID = int32(messageID)
return size, nil
}