首次推送
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Command represents a parsed or built TS3 command line.
|
||||
type Command struct {
|
||||
Params map[string]string
|
||||
Name string
|
||||
}
|
||||
|
||||
// Build returns the string representation of the command.
|
||||
func (c *Command) Build() string {
|
||||
return BuildCommand(c.Name, c.Params)
|
||||
}
|
||||
|
||||
var escaper = strings.NewReplacer(
|
||||
"\\", "\\\\",
|
||||
"/", "\\/",
|
||||
" ", "\\s",
|
||||
"|", "\\p",
|
||||
"\a", "\\a",
|
||||
"\b", "\\b",
|
||||
"\f", "\\f",
|
||||
"\n", "\\n",
|
||||
"\r", "\\r",
|
||||
"\t", "\\t",
|
||||
"\v", "\\v",
|
||||
)
|
||||
|
||||
func Escape(s string) string {
|
||||
return escaper.Replace(s)
|
||||
}
|
||||
|
||||
func BuildCommand(cmd string, params map[string]string) string {
|
||||
var res strings.Builder
|
||||
res.WriteString(Escape(cmd))
|
||||
for k, v := range params {
|
||||
res.WriteByte(' ')
|
||||
res.WriteString(k)
|
||||
res.WriteByte('=')
|
||||
res.WriteString(Escape(v))
|
||||
}
|
||||
|
||||
return res.String()
|
||||
}
|
||||
|
||||
func BuildCommandOrdered(cmd string, params [][2]string) string {
|
||||
var res strings.Builder
|
||||
res.WriteString(Escape(cmd))
|
||||
for _, kv := range params {
|
||||
res.WriteByte(' ')
|
||||
res.WriteString(kv[0])
|
||||
res.WriteByte('=')
|
||||
res.WriteString(Escape(kv[1]))
|
||||
}
|
||||
|
||||
return res.String()
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseCommand(s string) *Command {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
startIndex := 0
|
||||
for i := range len(s) {
|
||||
if s[i] >= 32 && s[i] <= 126 {
|
||||
startIndex = i
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if startIndex > 0 {
|
||||
s = s[startIndex:]
|
||||
}
|
||||
|
||||
parts := strings.Split(s, " ")
|
||||
if len(parts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := &Command{
|
||||
Params: make(map[string]string),
|
||||
}
|
||||
|
||||
// TS3 list responses (clientlist, channellist) arrive as nameless rows of
|
||||
// key=value pairs. Only treat the first token as a command name when it is
|
||||
// NOT a key=value pair; otherwise every token is a parameter.
|
||||
start := 0
|
||||
if !strings.Contains(parts[0], "=") {
|
||||
cmd.Name = parts[0]
|
||||
start = 1
|
||||
}
|
||||
|
||||
for _, p := range parts[start:] {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(p, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
cmd.Params[Unescape(kv[0])] = Unescape(kv[1])
|
||||
} else {
|
||||
cmd.Params[Unescape(p)] = ""
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var unescaper = strings.NewReplacer(
|
||||
"\\\\", "\\",
|
||||
"\\/", "/",
|
||||
"\\s", " ",
|
||||
"\\p", "|",
|
||||
"\\a", "\a",
|
||||
"\\b", "\b",
|
||||
"\\f", "\f",
|
||||
"\\n", "\n",
|
||||
"\\r", "\r",
|
||||
"\\t", "\t",
|
||||
"\\v", "\v",
|
||||
)
|
||||
|
||||
func Unescape(s string) string {
|
||||
return unescaper.Replace(s)
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package commands_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/honeybbq/teamspeak-go/commands"
|
||||
)
|
||||
|
||||
const (
|
||||
cmdClientlist = "clientlist"
|
||||
strHelloWorld = "hello world"
|
||||
)
|
||||
|
||||
func TestEscapeSpecialChars(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"hello", "hello"},
|
||||
{strHelloWorld, "hello\\sworld"},
|
||||
{"a\\b", "a\\\\b"},
|
||||
{"a/b", "a\\/b"},
|
||||
{"a|b", "a\\pb"},
|
||||
{"a\ab", "a\\ab"},
|
||||
{"a\bb", "a\\bb"},
|
||||
{"a\fb", "a\\fb"},
|
||||
{"a\nb", "a\\nb"},
|
||||
{"a\rb", "a\\rb"},
|
||||
{"a\tb", "a\\tb"},
|
||||
{"a\vb", "a\\vb"},
|
||||
{"back\\slash /slash |pipe", "back\\\\slash\\s\\/slash\\s\\ppipe"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := commands.Escape(tt.input)
|
||||
if got != tt.expected {
|
||||
t.Errorf("Escape(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnescapeSpecialChars(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"hello", "hello"},
|
||||
{"hello\\sworld", strHelloWorld},
|
||||
{"a\\\\b", "a\\b"},
|
||||
{"a\\/b", "a/b"},
|
||||
{"a\\pb", "a|b"},
|
||||
{"a\\ab", "a\ab"},
|
||||
{"a\\bb", "a\bb"},
|
||||
{"a\\fb", "a\fb"},
|
||||
{"a\\nb", "a\nb"},
|
||||
{"a\\rb", "a\rb"},
|
||||
{"a\\tb", "a\tb"},
|
||||
{"a\\vb", "a\vb"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := commands.Unescape(tt.input)
|
||||
if got != tt.expected {
|
||||
t.Errorf("Unescape(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeUnescapeRoundtrip(t *testing.T) {
|
||||
inputs := []string{
|
||||
strHelloWorld,
|
||||
"back\\slash",
|
||||
"pipe|val",
|
||||
"slash/val",
|
||||
"all\\ \t\n\r",
|
||||
"",
|
||||
"plain text",
|
||||
"mix\\ed /chars|here",
|
||||
}
|
||||
for _, input := range inputs {
|
||||
got := commands.Unescape(commands.Escape(input))
|
||||
if got != input {
|
||||
t.Errorf("Unescape(Escape(%q)) = %q", input, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommandEmpty(t *testing.T) {
|
||||
if commands.ParseCommand("") != nil {
|
||||
t.Error("expected nil for empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommandNameOnly(t *testing.T) {
|
||||
cmd := commands.ParseCommand(cmdClientlist)
|
||||
if cmd == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if cmd.Name != cmdClientlist {
|
||||
t.Errorf("Name = %q, want %q", cmd.Name, cmdClientlist)
|
||||
}
|
||||
if len(cmd.Params) != 0 {
|
||||
t.Errorf("expected 0 params, got %d", len(cmd.Params))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommandWithParams(t *testing.T) {
|
||||
cmd := commands.ParseCommand("notifytextmessage targetmode=3 msg=hello\\sworld invokerid=42")
|
||||
if cmd == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if cmd.Name != "notifytextmessage" {
|
||||
t.Errorf("Name = %q", cmd.Name)
|
||||
}
|
||||
if cmd.Params["targetmode"] != "3" {
|
||||
t.Errorf("targetmode = %q", cmd.Params["targetmode"])
|
||||
}
|
||||
if cmd.Params["msg"] != strHelloWorld {
|
||||
t.Errorf("msg = %q", cmd.Params["msg"])
|
||||
}
|
||||
if cmd.Params["invokerid"] != "42" {
|
||||
t.Errorf("invokerid = %q", cmd.Params["invokerid"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommandFlagParam(t *testing.T) {
|
||||
cmd := commands.ParseCommand("serverinfo -virtualserver_flag")
|
||||
if cmd == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if _, ok := cmd.Params["-virtualserver_flag"]; !ok {
|
||||
t.Error("expected flag param to exist")
|
||||
}
|
||||
if cmd.Params["-virtualserver_flag"] != "" {
|
||||
t.Errorf("flag param value should be empty, got %q", cmd.Params["-virtualserver_flag"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommandNamelessDataRow(t *testing.T) {
|
||||
// TS3 list responses (clientlist/channellist) arrive as nameless rows
|
||||
// beginning with a key=value pair. The first field must NOT be dropped as
|
||||
// a command name, otherwise the leading field (e.g. clid) is lost.
|
||||
cmd := commands.ParseCommand("clid=5761 cid=1 client_nickname=30k")
|
||||
if cmd == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if cmd.Name != "" {
|
||||
t.Errorf("Name = %q, want empty for nameless data row", cmd.Name)
|
||||
}
|
||||
if cmd.Params["clid"] != "5761" {
|
||||
t.Errorf("clid = %q, want 5761 (leading field must not be dropped)", cmd.Params["clid"])
|
||||
}
|
||||
if cmd.Params["cid"] != "1" {
|
||||
t.Errorf("cid = %q, want 1", cmd.Params["cid"])
|
||||
}
|
||||
if cmd.Params["client_nickname"] != "30k" {
|
||||
t.Errorf("client_nickname = %q, want 30k", cmd.Params["client_nickname"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommandLeadingNonPrintable(t *testing.T) {
|
||||
input := "\x00\x01\x02" + cmdClientlist + " clid=1"
|
||||
cmd := commands.ParseCommand(input)
|
||||
if cmd == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if cmd.Name != cmdClientlist {
|
||||
t.Errorf("Name = %q, want %s", cmd.Name, cmdClientlist)
|
||||
}
|
||||
if cmd.Params["clid"] != "1" {
|
||||
t.Errorf("clid = %q", cmd.Params["clid"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommandEscapedValues(t *testing.T) {
|
||||
cmd := commands.ParseCommand("test key=hello\\sworld pipe=a\\pb slash=a\\/b")
|
||||
if cmd == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if cmd.Params["key"] != strHelloWorld {
|
||||
t.Errorf("key = %q", cmd.Params["key"])
|
||||
}
|
||||
if cmd.Params["pipe"] != "a|b" {
|
||||
t.Errorf("pipe = %q", cmd.Params["pipe"])
|
||||
}
|
||||
if cmd.Params["slash"] != "a/b" {
|
||||
t.Errorf("slash = %q", cmd.Params["slash"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCommandNoParams(t *testing.T) {
|
||||
result := commands.BuildCommand(cmdClientlist, nil)
|
||||
if result != cmdClientlist {
|
||||
t.Errorf("BuildCommand = %q, want %q", result, cmdClientlist)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCommandEscapesValues(t *testing.T) {
|
||||
result := commands.BuildCommand("sendtextmessage", map[string]string{
|
||||
"msg": strHelloWorld,
|
||||
})
|
||||
if !strings.Contains(result, "msg=hello\\sworld") {
|
||||
t.Errorf("BuildCommand missing escaped value: %q", result)
|
||||
}
|
||||
if !strings.HasPrefix(result, "sendtextmessage") {
|
||||
t.Errorf("BuildCommand missing command name: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCommandOrderedPreservesOrder(t *testing.T) {
|
||||
result := commands.BuildCommandOrdered("clientinitiv", [][2]string{
|
||||
{"alpha", "aaa"},
|
||||
{"omega", "bbb"},
|
||||
{"ot", "1"},
|
||||
{"ip", ""},
|
||||
})
|
||||
expected := "clientinitiv alpha=aaa omega=bbb ot=1 ip="
|
||||
if result != expected {
|
||||
t.Errorf("BuildCommandOrdered = %q, want %q", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCommandOrderedEscapesValues(t *testing.T) {
|
||||
result := commands.BuildCommandOrdered("cmd", [][2]string{
|
||||
{"key", "a b|c"},
|
||||
})
|
||||
if !strings.Contains(result, "key=a\\sb\\pc") {
|
||||
t.Errorf("BuildCommandOrdered missing escaped: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandBuildMethod(t *testing.T) {
|
||||
cmd := &commands.Command{
|
||||
Name: "sendtextmessage",
|
||||
Params: map[string]string{"msg": "hi"},
|
||||
}
|
||||
result := cmd.Build()
|
||||
if !strings.HasPrefix(result, "sendtextmessage") {
|
||||
t.Errorf("Build() = %q, missing command name", result)
|
||||
}
|
||||
if !strings.Contains(result, "msg=hi") {
|
||||
t.Errorf("Build() = %q, missing param", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBuildRoundtrip(t *testing.T) {
|
||||
original := commands.BuildCommandOrdered("test", [][2]string{
|
||||
{"key1", "val1"},
|
||||
{"key2", strHelloWorld},
|
||||
{"key3", "pipe|val"},
|
||||
{"key4", "back\\slash"},
|
||||
})
|
||||
cmd := commands.ParseCommand(original)
|
||||
if cmd == nil {
|
||||
t.Fatal("ParseCommand returned nil")
|
||||
}
|
||||
if cmd.Name != "test" {
|
||||
t.Errorf("Name = %q", cmd.Name)
|
||||
}
|
||||
if cmd.Params["key1"] != "val1" {
|
||||
t.Errorf("key1 = %q", cmd.Params["key1"])
|
||||
}
|
||||
if cmd.Params["key2"] != strHelloWorld {
|
||||
t.Errorf("key2 = %q", cmd.Params["key2"])
|
||||
}
|
||||
if cmd.Params["key3"] != "pipe|val" {
|
||||
t.Errorf("key3 = %q", cmd.Params["key3"])
|
||||
}
|
||||
if cmd.Params["key4"] != "back\\slash" {
|
||||
t.Errorf("key4 = %q", cmd.Params["key4"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user