Files
cgpcli/encode.go
2026-02-19 13:03:07 +03:00

380 lines
8.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// # Data Serialization (Encoder)
//
// The Encoding section provides methods for converting Go data structures into
// the CommuniGate Pro CLI protocol format. It ensures that all commands,
// keywords, and settings are formatted correctly according to the server's
// unique grammar rules.
//
// Key capabilities include:
// - Smart String Quoting: automatically determines if a string is "safe"
// to send as an unquoted token or if it requires escaping and double quotes.
// - Unit Transformation: converts numeric values into CommuniGate Pro CLI size units
// (K, M, G, T) and durations into time units (s, m, h, d) automatically
// based on the context (key name).
// - Native Binary Support: efficiently encodes byte slices into
// Base64-wrapped [data blocks] required by the server for file transfers
// and certificates.
// - Temporal Handling: maps Go's [time.Time] to CGP's #T, #TPAST, and
// #TFUTURE syntax, specifically managing the UTC offset to match the
// server's internal time representation.
// - Recursive Structures: deep serialization of maps ({...}) and
// slices ((...)) with support for nested custom types.
//
// This encoder is the core communication bridge, responsible for
// constructing every command and data structure sent to the server.
package cgpcli
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"net"
"strconv"
"time"
)
type (
cgpDateLegacy time.Time
cgpDuration time.Duration
cgpIPSB net.IP
cgpIPSBSA []net.IP
cgpSize int64
cgpForceString string
)
type encodeWriter interface {
io.Writer
WriteByte(byte) error
WriteString(string) (int, error)
}
type cgpWriterTo interface {
writeCGP(w encodeWriter) error
}
var cgpSafeTable = [256]byte{
'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '6': 1, '7': 1, '8': 1, '9': 1,
'A': 1, 'B': 1, 'C': 1, 'D': 1, 'E': 1, 'F': 1, 'G': 1, 'H': 1, 'I': 1, 'J': 1,
'K': 1, 'L': 1, 'M': 1, 'N': 1, 'O': 1, 'P': 1, 'Q': 1, 'R': 1, 'S': 1, 'T': 1,
'U': 1, 'V': 1, 'W': 1, 'X': 1, 'Y': 1, 'Z': 1,
'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1, 'f': 1, 'g': 1, 'h': 1, 'i': 1, 'j': 1,
'k': 1, 'l': 1, 'm': 1, 'n': 1, 'o': 1, 'p': 1, 'q': 1, 'r': 1, 's': 1, 't': 1,
'u': 1, 'v': 1, 'w': 1, 'x': 1, 'y': 1, 'z': 1,
'_': 1, '@': 1, '.': 1, '-': 1,
}
func formatCGPDuration(d time.Duration) string {
switch d {
case -1:
return "never"
case 0:
return "immediately"
}
totalSeconds := int64(d.Seconds())
// Дни (24h)
if totalSeconds%(24*3600) == 0 {
return strconv.FormatInt(totalSeconds/(24*3600), 10) + "d"
}
// Часы
if totalSeconds%3600 == 0 {
return strconv.FormatInt(totalSeconds/3600, 10) + "h"
}
// Минуты
if totalSeconds%60 == 0 {
return strconv.FormatInt(totalSeconds/60, 10) + "m"
}
// Секунды (по умолчанию)
return strconv.FormatInt(totalSeconds, 10) + "s"
}
// isCGPSafeString проверяет, можно ли передать строку без кавычек.
// В CGP это буквенно-цифровые строки без спецсимволов.
func isCGPSafeString(s string) bool {
if s == "" {
return false
}
for i := 0; i < len(s); i++ {
if cgpSafeTable[s[i]] == 0 {
return false
}
}
return true
}
var cgpSizeUnits = []struct {
suffix string
value int64
}{
{"T", 1024 * 1024 * 1024 * 1024},
{"G", 1024 * 1024 * 1024},
{"M", 1024 * 1024},
{"K", 1024},
}
// formatCGPSize преобразует байты в формат CGP (1K, 5M, 10G) или "unlimited".
func formatCGPSize(v int64) string {
if v == -1 {
return "unlimited"
}
if v == 0 {
return "0"
}
for _, u := range cgpSizeUnits {
if v%u.value == 0 {
return strconv.FormatInt(v/u.value, 10) + u.suffix
}
}
return strconv.FormatInt(v, 10)
}
// MarshalTo serializes v directly into a buffered writer. This is the
// preferred function for high-performance operations or large data sets
// to avoid intermediate string allocations.
//
// Parameters:
// - w: a pointer to a *bufio.Writer where the serialized data will be written.
// - v: the data structure to be serialized.
//
// Returns:
// - error: an error if writing to the buffer fails or if the data is invalid.
func MarshalTo(w *bufio.Writer, v any) error {
return marshal(w, v, "")
}
// MarshalToString returns the CGP-formatted string representation of v.
//
// Parameters:
// - v: the data to be serialized. Can be any basic Go type (string, int,
// bool, time.Time), a map[string]any, or a slice.
//
// Returns:
// - string: a string ready to be sent to the CommuniGate Pro CLI.
// - error: an error if the data structure contains types that cannot be serialized.
func MarshalToString(v any) (string, error) {
var b bytes.Buffer
err := marshal(&b, v, "")
return b.String(), err
}
func marshal(w encodeWriter, v any, keyName string) (err error) {
// 1. Прямая запись (самый быстрый путь для кастомных типов)
if m, ok := v.(cgpWriterTo); ok {
return m.writeCGP(w)
}
// 2. Трансформация исключений (меняет тип на cgp*)
if keyName != "" {
v = transformWrite(keyName, v)
}
switch val := v.(type) {
case cgpDateLegacy:
tm := time.Time(val)
if tm.IsZero() {
_, err = w.WriteString("#NULL#")
} else {
w.WriteByte('"')
w.WriteString(tm.UTC().Format(cgpInternalTimeLayout))
err = w.WriteByte('"')
}
return
case cgpIPSB:
w.WriteByte('"')
w.WriteByte('[')
w.WriteString(net.IP(val).String())
w.WriteByte(']')
err = w.WriteByte('"')
return
case cgpIPSBSA:
w.WriteByte('"')
for i, ip := range val {
if i > 0 {
w.WriteString(", ")
}
w.WriteByte('[')
w.WriteString(ip.String())
w.WriteByte(']')
}
err = w.WriteByte('"')
return
case cgpSize:
_, err = w.WriteString(formatCGPSize(int64(val)))
return
case cgpDuration:
_, err = w.WriteString(formatCGPDuration(time.Duration(val)))
return
case Atom:
_, err = w.WriteString(string(val))
return
case DataBlock:
w.WriteByte('[')
w.Write(val)
err = w.WriteByte(']')
return
case []byte:
w.WriteByte('[')
enc := base64.NewEncoder(base64.StdEncoding, w)
enc.Write(val)
enc.Close()
err = w.WriteByte(']')
return
// --- Строки ---
case string:
if isCGPSafeString(val) {
_, err = w.WriteString(val)
return
}
if err = w.WriteByte('"'); err != nil {
return
}
if _, err = w.WriteString(replaceSpecChars(val)); err != nil {
return
}
return w.WriteByte('"')
// --- Числа ---
case int, int32, int64:
return writeInt64(w, toInt64(val))
// --- Время ---
case time.Time:
if val.IsZero() {
_, err = w.WriteString("#NULL#")
return
}
if val.Equal(TimePast) {
_, err = w.WriteString("#TPAST")
return
}
if val.Equal(TimeFuture) {
_, err = w.WriteString("#TFUTURE")
return
}
w.WriteString("#T")
_, err = w.WriteString(val.UTC().Format(cgpInternalTimeLayout))
return
case time.Duration:
_, err = w.WriteString(formatCGPDuration(val))
return
// --- Словари (Maps) ---
case map[string]any:
return marshalMap(w, val)
case map[string]string:
return marshalMap(w, val)
// --- Списки (Slices) ---
case []any:
w.WriteByte('(')
for i, item := range val {
if i > 0 {
w.WriteByte(',')
}
marshal(w, item, "")
}
return w.WriteByte(')')
case []string:
w.WriteByte('(')
for i, s := range val {
if i > 0 {
w.WriteByte(',')
}
marshal(w, s, "")
}
return w.WriteByte(')')
// --- Остальное ---
case bool:
if val {
_, err = w.WriteString("YES")
return
}
_, err = w.WriteString("NO")
return
case nil:
_, err = w.WriteString("#NULL#")
return
default:
strVal := fmt.Sprint(val)
if isCGPSafeString(strVal) {
_, err = w.WriteString(strVal)
return
}
if err = w.WriteByte('"'); err != nil {
return
}
if _, err = w.WriteString(replaceSpecChars(strVal)); err != nil {
return
}
return w.WriteByte('"')
}
}
func marshalMap[V any](w encodeWriter, m map[string]V) error {
w.WriteByte('{')
for k, v := range m {
if isCGPSafeString(k) {
w.WriteString(k)
} else {
w.WriteByte('"')
w.WriteString(replaceSpecChars(k))
w.WriteByte('"')
}
w.WriteByte('=')
marshal(w, v, k)
w.WriteByte(';')
}
return w.WriteByte('}')
}
func writeInt64(w encodeWriter, i int64) error {
if i == 0 {
return w.WriteByte('0')
}
if i < 0 {
if err := w.WriteByte('-'); err != nil {
return err
}
} else {
i = -i
}
var buf [20]byte
pos := len(buf)
for i != 0 {
pos--
buf[pos] = byte('0' - (i % 10))
i /= 10
}
for ; pos < len(buf); pos++ {
if err := w.WriteByte(buf[pos]); err != nil {
return err
}
}
return nil
}