go-q3net/message.go

213 lines
4.4 KiB
Go

package quake
import (
"bytes"
"io"
"strings"
)
type Message struct {
Header *Header
Name string
FragmentOffset uint32
FragmentLength uint32
Data []byte
}
func UnmarshalMessage(data []byte) (*Message, error) {
// Is there even a full header?
if len(data) < 4 {
return nil, ErrCorruptedMessage
}
header, err := UnmarshalHeader(data[0:4])
if err != nil {
return nil, err
}
// netchan UDP packets are structured differently
if !header.IsOOB() {
/*
Original commentary from https://github.com/id-Software/Quake-III-Arena/blob/master/code/qcommon/net_chan.c#L30-L33:
packet header
-------------
4 outgoing sequence. high bit will be set if this is a fragmented message
[2 qport (only for client to server)]
[2 fragment start byte]
[2 fragment length. if < FRAGMENT_SIZE, this is the last fragment]
if the sequence number is -1, the packet should be handled as an out-of-band
message instead of as part of a netcon.
All fragments will have the same sequence numbers.
The qport field is a workaround for bad address translating routers that
sometimes remap the client's source port on a packet during gameplay.
If the base part of the net address matches and the qport matches, then the
channel matches even if the IP port differs. The IP port should be updated
to the new value before sending out any replies.
*/
// TODO - if packet is from client to server then 16 bits client's port number here
if header.Fragmented {
// TODO
// 16 bits fragment offset
// 16 bits fragment length
}
}
splitPos := bytes.IndexAny(data[4:], " \n\r\t\x00\\")
if splitPos < 0 {
splitPos = len(data) - 4
}
commandName := string(data[4 : 4+splitPos])
//log.Printf("Got message with command name %q", commandName)
extra := []byte(nil)
if data[4+splitPos] == 92 {
splitPos--
}
if len(data) > 4+splitPos {
extra = data[4+splitPos+1:]
}
for len(extra) > 0 && extra[len(extra)-1] == 0 {
extra = extra[0 : len(extra)-1]
}
return &Message{
Name: commandName,
Data: extra,
Header: header,
}, nil
}
func (m *Message) SetArguments(argv []string) {
m.Data = make([]byte, 0)
for _, arg := range argv {
if strings.Contains(arg, " ") {
arg = "\"" + strings.Replace(arg, "\"", "\\\"", -1) + "\""
}
if len(m.Data) > 0 {
// separate with space
m.Data = append(m.Data, 0x20)
}
// add argument in binary form
m.Data = append(m.Data, []byte(arg)...)
}
//m.Data = append(m.Data, 0x0A)
}
func (m *Message) GetArguments() []string {
// All matches, extract arguments
buffer := m.Data
argv := []string{}
for len(buffer) > 0 {
c := rune(buffer[0])
switch c {
case '"', '\'':
// quoted string
searchPos := 1
for {
pos := bytes.IndexByte(buffer[searchPos:], byte(c))
if pos < 0 {
// found quote start without a matching end
return nil
}
pos += searchPos
if rune(buffer[pos-1]) != '\\' {
// found end of quoted string
searchPos = pos
break
}
// that's an escaped quote char, skip that
searchPos = pos + 1
}
// append what's inside the quotes to the arguments list
arg := string(buffer[1:searchPos])
arg = strings.Replace(arg, "\\"+string(c), string(c), -1)
argv = append(argv, arg)
if searchPos+1 < len(buffer) {
buffer = buffer[searchPos+1:]
} else {
buffer = []byte{}
}
case ' ', '\t', '\n', '\r':
buffer = buffer[1:]
default:
// search for next space
pos := bytes.IndexByte(buffer, 0x20)
if pos < 0 {
pos = bytes.IndexByte(buffer, 0x0A)
}
if pos < 0 {
pos = bytes.IndexByte(buffer, 0x00)
}
if pos < 0 {
pos = len(buffer)
}
arg := string(buffer[0:pos])
argv = append(argv, arg)
if pos+1 < len(buffer) {
buffer = buffer[pos+1:]
} else {
buffer = []byte{}
}
}
}
return argv
}
func (m *Message) Marshal(w io.Writer) error {
// Buffer
buf := new(bytes.Buffer)
// Header
if _, err := buf.Write(m.Header.Marshal()); err != nil {
return err
}
// Command name
if _, err := buf.Write([]byte(m.Name)); err != nil {
return err
}
// Data
if m.Data != nil && len(m.Data) > 0 {
if _, err := buf.Write([]byte{0x20}); err != nil {
return err
}
}
if _, err := buf.Write(m.Data); err != nil {
return err
}
// Separator, IW code expects this to be there, however our
// implementation doesn't require it.
buf.Write([]byte{0x00})
// And now write as one message
w.Write(buf.Bytes())
return nil
}