aboutsummaryrefslogtreecommitdiff
path: root/internal/proto
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2025-06-04 11:08:19 +0300
committerGitHub <noreply@github.com>2025-06-04 11:08:19 +0300
commit7240595478fedec02e9e47c704976cf56a66d3e8 (patch)
tree300165d4ecf00fed03d33b36863370384cbdc7ce /internal/proto
parent76dc648f33a9f96e68d5c9000032c2b986bd5a3d (diff)
downloadgo-metatrader4-7240595478fedec02e9e47c704976cf56a66d3e8.tar.xz
go-metatrader4-7240595478fedec02e9e47c704976cf56a66d3e8.zip
First version
Diffstat (limited to 'internal/proto')
-rw-r--r--internal/proto/proto.go65
-rw-r--r--internal/proto/proto_test.go39
2 files changed, 104 insertions, 0 deletions
diff --git a/internal/proto/proto.go b/internal/proto/proto.go
new file mode 100644
index 0000000..d86ce75
--- /dev/null
+++ b/internal/proto/proto.go
@@ -0,0 +1,65 @@
+package proto
+
+import (
+ "encoding/base64"
+ "fmt"
+ "sort"
+ "strings"
+ "unicode"
+
+ "golang.org/x/text/encoding/charmap"
+)
+
+// EncodeParams converts params map into a sorted base64-encoded string using Windows-1251 encoding.
+func EncodeParams(params map[string]string) (string, error) {
+ keys := make([]string, 0, len(params))
+ for k := range params {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ var sb strings.Builder
+ for i, k := range keys {
+ if i > 0 {
+ sb.WriteByte('|')
+ }
+ sb.WriteString(k)
+ sb.WriteByte('=')
+ sb.WriteString(params[k])
+ }
+ sb.WriteByte('|')
+
+ enc := charmap.Windows1251.NewEncoder()
+ encoded, err := enc.String(sb.String())
+ if err != nil {
+ return "", fmt.Errorf("encode params: %w", err)
+ }
+ return base64.StdEncoding.EncodeToString([]byte(encoded)), nil
+}
+
+// DecodeResponse decodes base64-encoded Windows-1251 text to UTF-8 and removes control characters.
+func DecodeResponse(data string) (string, error) {
+ raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(data))
+ if err != nil {
+ return "", fmt.Errorf("base64 decode: %w", err)
+ }
+ decoded, err := charmap.Windows1251.NewDecoder().Bytes(raw)
+ if err != nil {
+ return "", fmt.Errorf("decode charset: %w", err)
+ }
+ cleaned := strings.Map(func(r rune) rune {
+ if unicode.IsPrint(r) || r == '\n' || r == '\r' || r == '\t' {
+ return r
+ }
+ return -1
+ }, string(decoded))
+ return cleaned, nil
+}
+
+// BuildRequest returns byte slice representing the command and parameters.
+func BuildRequest(command, encodedParams string, quit bool) []byte {
+ if quit {
+ return []byte(fmt.Sprintf("%s %s\nQUIT\n", command, encodedParams))
+ }
+ return []byte(fmt.Sprintf("%s %s\n", command, encodedParams))
+}
diff --git a/internal/proto/proto_test.go b/internal/proto/proto_test.go
new file mode 100644
index 0000000..0e7e644
--- /dev/null
+++ b/internal/proto/proto_test.go
@@ -0,0 +1,39 @@
+package proto
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestEncodeParamsOrder(t *testing.T) {
+ params := map[string]string{"B": "2", "A": "1"}
+ encoded1, err := EncodeParams(params)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // encode again with different map order
+ encoded2, err := EncodeParams(map[string]string{"A": "1", "B": "2"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if encoded1 != encoded2 {
+ t.Fatalf("expected deterministic encode, got %s vs %s", encoded1, encoded2)
+ }
+}
+
+func TestDecodeResponse(t *testing.T) {
+ // "привет" in Cyrillic
+ original := "привет"
+ params := map[string]string{"MSG": original}
+ enc, err := EncodeParams(params)
+ if err != nil {
+ t.Fatalf("encode params: %v", err)
+ }
+ dec, err := DecodeResponse(enc)
+ if err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+ if !strings.Contains(dec, original) {
+ t.Fatalf("expected to contain %q, got %q", original, dec)
+ }
+}