719 lines
15 KiB
Go
719 lines
15 KiB
Go
// # Data Deserialization (Parser)
|
|
//
|
|
// The Decoding section contains the core logic for parsing raw responses from
|
|
// the CommuniGate Pro CLI. It transforms the server's unique data format
|
|
// into native Go structures, handling the complexities of the protocol's
|
|
// grammar with high efficiency.
|
|
//
|
|
// Key capabilities include:
|
|
// - High-Performance Parsing: uses a state-based [decodeState] with a
|
|
// custom character table to scan data with minimal overhead.
|
|
// - String Interning: implements a key cache to reuse common dictionary
|
|
// keys (like "Account", "Status", "Domain"), significantly reducing
|
|
// memory allocations during large data transfers.
|
|
// - CGP-Specific Types: native support for unquoted tokens, #I[address]
|
|
// blocks, Base64-encoded [data blocks], and specialized temporal
|
|
// constants (#TPAST, #TFUTURE).
|
|
// - Structural Mapping: handles recursive dictionaries ({...}),
|
|
// arrays ((...)), and even embedded XML or tagged objects.
|
|
// - Type Transformation: automatically converts CommuniGate Pro CLI strings like "10M" or
|
|
// "1h" into meaningful numeric bytes or [time.Duration] values via
|
|
// internal helper functions.
|
|
//
|
|
// This parser is designed to be robust, handling potentially malformed or
|
|
// unusually large responses without compromising system stability.
|
|
package cgpcli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
type decodeState struct {
|
|
data []byte
|
|
len int
|
|
span int
|
|
scratch bytes.Buffer
|
|
keyCache map[string]string
|
|
}
|
|
|
|
var (
|
|
binYesUpper = []byte("YES")
|
|
binYesLower = []byte("yes")
|
|
binNoUpper = []byte("NO")
|
|
binNoLower = []byte("no")
|
|
)
|
|
|
|
const (
|
|
isCgpSpace uint8 = 1 << 0
|
|
isCgpDelimiter uint8 = 1 << 1
|
|
isCgpOpen uint8 = 1 << 2 // {
|
|
isCgpClose uint8 = 1 << 3 // }
|
|
isCgpSemi uint8 = 1 << 4 // ;
|
|
isCgpParenOpen uint8 = 1 << 5 // (
|
|
isCgpParenClose uint8 = 1 << 6 // )
|
|
isCgpComma uint8 = 1 << 7 // ,
|
|
)
|
|
|
|
var cgpCharTable [256]uint8
|
|
|
|
func init() {
|
|
for _, c := range " \t\r\n" {
|
|
cgpCharTable[c] |= isCgpSpace
|
|
}
|
|
for _, c := range " \t\r\n=;,)}" {
|
|
cgpCharTable[c] |= isCgpDelimiter
|
|
}
|
|
// Специфические флаги для структуры словаря
|
|
cgpCharTable['{'] |= isCgpOpen
|
|
cgpCharTable['}'] |= isCgpClose
|
|
cgpCharTable[';'] |= isCgpSemi
|
|
cgpCharTable['('] |= isCgpParenOpen
|
|
cgpCharTable[')'] |= isCgpParenClose
|
|
cgpCharTable[','] |= isCgpComma
|
|
}
|
|
|
|
func (d *decodeState) init(data []byte) *decodeState {
|
|
d.data = data
|
|
d.len = len(data)
|
|
d.span = 0
|
|
d.scratch.Grow(4096)
|
|
if d.keyCache == nil {
|
|
d.keyCache = make(map[string]string, 256)
|
|
}
|
|
return d
|
|
}
|
|
|
|
func isSpace(c byte) bool {
|
|
return cgpCharTable[c]&isCgpSpace != 0
|
|
}
|
|
|
|
func (d *decodeState) skipSpaces() {
|
|
for d.span < d.len && (cgpCharTable[d.data[d.span]]&isCgpSpace != 0) {
|
|
d.span++
|
|
}
|
|
}
|
|
|
|
// Unmarshal parses the CGP-formatted data and stores the result in the
|
|
// value pointed to by v.
|
|
//
|
|
// Parameters:
|
|
// - data: a byte slice containing the raw response from the CommuniGate Pro CLI.
|
|
// This can be a simple word, a dictionary, an array or a complex nested types.
|
|
// - v: a non-nil pointer to the destination variable. If v is a pointer
|
|
// to an interface{}, Unmarshal will populate it with map[string]any,
|
|
// []any, int64, bool, or string, depending on the source data.
|
|
//
|
|
// Returns:
|
|
// - error: an error if v is not a pointer, is nil, or if the data cannot be
|
|
// correctly mapped to the target type.
|
|
func Unmarshal(data []byte, v any) error {
|
|
var d decodeState
|
|
d.init(data)
|
|
|
|
rv := reflect.ValueOf(v)
|
|
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
|
return fmt.Errorf("cgpcli: Unmarshal expects non-nil pointer")
|
|
}
|
|
|
|
result := d.valueInterface(false)
|
|
target := reflect.ValueOf(v).Elem()
|
|
|
|
if result == nil {
|
|
if target.CanSet() {
|
|
target.Set(reflect.Zero(target.Type()))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
val := reflect.ValueOf(result)
|
|
if val.Type().AssignableTo(target.Type()) {
|
|
target.Set(val)
|
|
} else if val.Type().ConvertibleTo(target.Type()) {
|
|
target.Set(val.Convert(target.Type()))
|
|
} else {
|
|
return fmt.Errorf("cgpcli: cannot assign %T to %v", result, target.Type())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *decodeState) valueInterface(asString bool) any {
|
|
d.skipSpaces()
|
|
if d.span >= d.len {
|
|
return nil
|
|
}
|
|
switch d.data[d.span] {
|
|
case '{':
|
|
d.span++
|
|
return d.dictionaryInterface()
|
|
case '(':
|
|
d.span++
|
|
return d.arrayInterface()
|
|
case '<':
|
|
d.span++
|
|
return d.xmlInterface()
|
|
default:
|
|
return d.wordInterface(asString)
|
|
}
|
|
}
|
|
|
|
func (d *decodeState) dictionaryInterface() any {
|
|
count := 0
|
|
depth := 1
|
|
ts := d.span
|
|
data := d.data
|
|
|
|
for ts < len(data) {
|
|
f := cgpCharTable[data[ts]]
|
|
if f != 0 {
|
|
if f&isCgpOpen != 0 {
|
|
depth++
|
|
} else if f&isCgpClose != 0 {
|
|
depth--
|
|
if depth == 0 {
|
|
break
|
|
}
|
|
} else if f&isCgpSemi != 0 && depth == 1 {
|
|
count++
|
|
}
|
|
}
|
|
ts++
|
|
}
|
|
|
|
res := make(map[string]any, count)
|
|
|
|
for d.span < d.len {
|
|
d.skipSpaces()
|
|
if d.span >= d.len || d.data[d.span] == '}' {
|
|
d.span++
|
|
break
|
|
}
|
|
|
|
var key string
|
|
start := d.span
|
|
if d.data[d.span] != '"' {
|
|
for d.span < d.len && cgpCharTable[d.data[d.span]]&isCgpDelimiter == 0 {
|
|
d.span++
|
|
}
|
|
|
|
keyBytes := d.data[start:d.span]
|
|
|
|
var ok bool
|
|
if key, ok = d.keyCache[string(keyBytes)]; !ok {
|
|
key = string(keyBytes)
|
|
d.keyCache[key] = key
|
|
}
|
|
} else {
|
|
kRaw := d.wordInterface(true)
|
|
key, _ = kRaw.(string)
|
|
if cached, ok := d.keyCache[key]; ok {
|
|
key = cached
|
|
} else {
|
|
d.keyCache[key] = key
|
|
}
|
|
}
|
|
|
|
d.skipSpaces()
|
|
if d.span < d.len && d.data[d.span] == '=' {
|
|
d.span++
|
|
val := d.valueInterface(false)
|
|
res[key] = transformRead(key, val)
|
|
}
|
|
|
|
d.skipSpaces()
|
|
if d.span < d.len && d.data[d.span] == ';' {
|
|
d.span++
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (d *decodeState) arrayInterface() any {
|
|
commas := 0
|
|
depth := 1
|
|
ts := d.span
|
|
data := d.data
|
|
empty := true
|
|
|
|
for ts < len(data) {
|
|
f := cgpCharTable[data[ts]]
|
|
if f != 0 {
|
|
if f&isCgpParenOpen != 0 {
|
|
depth++
|
|
} else if f&isCgpParenClose != 0 {
|
|
depth--
|
|
if depth == 0 {
|
|
break
|
|
}
|
|
} else if f&isCgpComma != 0 && depth == 1 {
|
|
commas++
|
|
}
|
|
}
|
|
if f&isCgpSpace == 0 {
|
|
empty = false
|
|
}
|
|
ts++
|
|
}
|
|
|
|
capacity := 0
|
|
if !empty {
|
|
capacity = commas + 1
|
|
}
|
|
res := make([]any, 0, capacity)
|
|
|
|
for d.span < d.len {
|
|
d.skipSpaces()
|
|
if d.span >= d.len || d.data[d.span] == ')' {
|
|
d.span++
|
|
break
|
|
}
|
|
|
|
res = append(res, d.valueInterface(false))
|
|
|
|
d.skipSpaces()
|
|
if d.span < d.len && d.data[d.span] == ',' {
|
|
d.span++
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (d *decodeState) wordInterface(asString bool) any {
|
|
d.skipSpaces()
|
|
if d.span >= d.len {
|
|
return ""
|
|
}
|
|
|
|
curr := d.data[d.span]
|
|
|
|
// 1. Data Blocks [base64]
|
|
if curr == '[' {
|
|
d.span++
|
|
return d.readCgpBlock('[', ']')
|
|
}
|
|
|
|
// 2. IP Addresses #I[...]
|
|
if curr == '#' && d.span+2 < d.len && d.data[d.span+1] == 'I' && d.data[d.span+2] == '[' {
|
|
d.span += 3
|
|
ipData := d.readCgpBlock('[', ']')
|
|
res := CGPIP{IP: net.ParseIP(string(ipData))}
|
|
if d.span < d.len && d.data[d.span] == ':' {
|
|
d.span++
|
|
start := d.span
|
|
for d.span < d.len && d.data[d.span] >= '0' && d.data[d.span] <= '9' {
|
|
d.span++
|
|
}
|
|
res.Port, _ = strconv.Atoi(string(d.data[start:d.span]))
|
|
}
|
|
return res
|
|
}
|
|
|
|
// 3. Fast Path (Unquoted tokens: keys, numbers, constants)
|
|
if curr != '"' && curr != '#' {
|
|
start := d.span
|
|
for d.span < d.len {
|
|
if cgpCharTable[d.data[d.span]]&isCgpDelimiter != 0 {
|
|
break
|
|
}
|
|
d.span++
|
|
}
|
|
|
|
subSlice := d.data[start:d.span]
|
|
|
|
// Если нам не принудительно нужна строка (как для ключей мап)
|
|
if !asString {
|
|
// Проверка на YES/NO через bytes.Equal
|
|
if val, ok := tryCgpBool(subSlice); ok {
|
|
return val
|
|
}
|
|
// Проверка на числа через unsafe string
|
|
if val, ok := tryParseInt(subSlice); ok {
|
|
return val
|
|
}
|
|
}
|
|
|
|
// Поиск в кеше строк (интернирование)
|
|
if s, ok := d.keyCache[string(subSlice)]; ok {
|
|
return s
|
|
}
|
|
|
|
// Единственная точка аллокации для новых уникальных строк/ключей
|
|
raw := string(subSlice)
|
|
d.keyCache[raw] = raw
|
|
return raw
|
|
}
|
|
|
|
// 4. Slow Path (Quoted, Objects #(..), Tags)
|
|
d.scratch.Reset()
|
|
isQuoted := curr == '"'
|
|
isTagged := curr == '#'
|
|
isObject := false
|
|
|
|
if isQuoted {
|
|
for d.span < d.len && d.data[d.span] == '"' {
|
|
d.span++
|
|
for d.span < d.len {
|
|
ch := d.data[d.span]
|
|
if ch == '\\' {
|
|
if d.span+1 < d.len {
|
|
next := d.data[d.span+1]
|
|
if next >= '0' && next <= '7' && d.span+3 < d.len {
|
|
octStr := string(d.data[d.span+1 : d.span+4])
|
|
if val, err := strconv.ParseInt(octStr, 8, 32); err == nil {
|
|
d.scratch.WriteByte(byte(val))
|
|
d.span += 4
|
|
continue
|
|
}
|
|
}
|
|
switch next {
|
|
case 'e':
|
|
d.scratch.WriteByte('\n')
|
|
case 't':
|
|
d.scratch.WriteByte('\t')
|
|
case '\\':
|
|
d.scratch.WriteByte('\\')
|
|
case '"':
|
|
d.scratch.WriteByte('"')
|
|
default:
|
|
d.scratch.WriteByte(next)
|
|
}
|
|
d.span += 2
|
|
continue
|
|
}
|
|
}
|
|
if ch == '"' {
|
|
d.span++
|
|
if d.span < d.len && d.data[d.span] == '"' {
|
|
d.scratch.WriteByte('"')
|
|
d.span++
|
|
continue
|
|
}
|
|
d.skipSpaces()
|
|
break
|
|
}
|
|
d.scratch.WriteByte(ch)
|
|
d.span++
|
|
}
|
|
}
|
|
} else if isTagged {
|
|
if bytes.HasPrefix(d.data[d.span:], []byte("#(")) {
|
|
isObject = true
|
|
} else {
|
|
d.scratch.WriteByte('#')
|
|
d.span++
|
|
}
|
|
for d.span < d.len {
|
|
ch := d.data[d.span]
|
|
if isObject {
|
|
d.scratch.WriteByte(ch)
|
|
d.span++
|
|
if ch == ')' {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
if isSpace(ch) || ch == '=' || ch == ';' || ch == ',' || ch == ')' || ch == '}' {
|
|
break
|
|
}
|
|
d.scratch.WriteByte(ch)
|
|
d.span++
|
|
}
|
|
}
|
|
|
|
raw := d.scratch.String()
|
|
if asString || isQuoted {
|
|
return raw
|
|
}
|
|
|
|
if isTagged && !isObject {
|
|
if raw == "#NULL#" {
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(raw, "#T") {
|
|
switch raw {
|
|
case "#TPAST":
|
|
return TimePast
|
|
case "#TFUTURE":
|
|
return TimeFuture
|
|
default:
|
|
if t, err := time.Parse(cgpInternalTimeLayout, raw[2:]); err == nil {
|
|
return t
|
|
}
|
|
}
|
|
}
|
|
return parseCGPNumber(raw)
|
|
}
|
|
|
|
if val, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
|
return val
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func (d *decodeState) xmlInterface() any {
|
|
startSpan := d.span - 1
|
|
depth := 1
|
|
inString := false
|
|
var quoteChar byte
|
|
|
|
for d.span < d.len {
|
|
ch := d.data[d.span]
|
|
if inString {
|
|
if ch == quoteChar {
|
|
inString = false
|
|
}
|
|
d.span++
|
|
continue
|
|
}
|
|
if ch == '"' || ch == '\'' {
|
|
inString = true
|
|
quoteChar = ch
|
|
d.span++
|
|
continue
|
|
}
|
|
if ch == '<' && d.span+1 < d.len {
|
|
if d.data[d.span+1] == '/' {
|
|
depth--
|
|
} else if d.data[d.span+1] != '!' && d.data[d.span+1] != '?' {
|
|
depth++
|
|
}
|
|
} else if ch == '>' {
|
|
if d.span > 0 && d.data[d.span-1] == '/' {
|
|
depth--
|
|
}
|
|
if depth == 0 {
|
|
d.span++
|
|
break
|
|
}
|
|
}
|
|
d.span++
|
|
}
|
|
return string(d.data[startSpan:d.span])
|
|
}
|
|
|
|
func parseCGPDuration(s string) time.Duration {
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
switch strings.ToLower(s) {
|
|
case "never":
|
|
return -1
|
|
case "immediately":
|
|
return 0
|
|
}
|
|
|
|
lastIdx := len(s) - 1
|
|
multiplier := time.Second
|
|
numPart := s
|
|
hasSuffix := true
|
|
|
|
switch s[lastIdx] {
|
|
case 's', 'S':
|
|
multiplier = time.Second
|
|
case 'm', 'M':
|
|
multiplier = time.Minute
|
|
case 'h', 'H':
|
|
multiplier = time.Hour
|
|
case 'd', 'D':
|
|
multiplier = time.Hour * 24
|
|
default:
|
|
hasSuffix = false
|
|
}
|
|
|
|
if hasSuffix {
|
|
numPart = s[:lastIdx]
|
|
}
|
|
|
|
val, err := strconv.ParseInt(numPart, 10, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
return time.Duration(val) * multiplier
|
|
}
|
|
|
|
func parseCGPNumber(raw string) any {
|
|
if len(raw) < 2 {
|
|
return raw
|
|
}
|
|
s := raw[1:] // Отрезаем '#'
|
|
|
|
base := 10
|
|
start := 0
|
|
if s[0] == '-' {
|
|
start = 1
|
|
}
|
|
|
|
if len(s[start:]) >= 2 && s[start] == '0' {
|
|
switch s[start+1] {
|
|
case 'x':
|
|
base = 16
|
|
case 'o':
|
|
base = 8
|
|
case 'b':
|
|
base = 2
|
|
}
|
|
if base != 10 {
|
|
// Убираем индикатор базы (0x, 0o, 0b) для strconv
|
|
toParse := s[:start] + s[start+2:]
|
|
if val, err := strconv.ParseInt(toParse, base, 64); err == nil {
|
|
return val
|
|
}
|
|
}
|
|
}
|
|
|
|
if val, err := strconv.ParseInt(s, 10, 64); err == nil {
|
|
return val
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func parseCGPSize(s string) any {
|
|
if s == "unlimited" {
|
|
return int64(-1)
|
|
}
|
|
s = strings.Trim(s, "\"")
|
|
if len(s) == 0 {
|
|
return s
|
|
}
|
|
multiplier := int64(1)
|
|
last := s[len(s)-1]
|
|
hasSuffix := true
|
|
switch last {
|
|
case 'K', 'k':
|
|
multiplier = 1024
|
|
case 'M', 'm':
|
|
multiplier = 1024 * 1024
|
|
case 'G', 'g':
|
|
multiplier = 1024 * 1024 * 1024
|
|
case 'T', 't':
|
|
multiplier = 1024 * 1024 * 1024 * 1024
|
|
default:
|
|
hasSuffix = false
|
|
}
|
|
numPart := s
|
|
if hasSuffix {
|
|
numPart = s[:len(s)-1]
|
|
}
|
|
if val, err := strconv.ParseInt(numPart, 10, 64); err == nil {
|
|
return val * multiplier
|
|
}
|
|
return s
|
|
}
|
|
|
|
func parseIPSB(v any) net.IP {
|
|
s, ok := v.(string)
|
|
if !ok || s == "" {
|
|
return nil
|
|
}
|
|
s = strings.Trim(s, "[] ")
|
|
return net.ParseIP(s)
|
|
}
|
|
|
|
var ipsbsaReplacer = strings.NewReplacer("[", "", "]", "")
|
|
|
|
func parseIPSBSA(s string) []net.IP {
|
|
if s == "" {
|
|
return []net.IP{}
|
|
}
|
|
s = ipsbsaReplacer.Replace(s)
|
|
parts := strings.Split(s, ",")
|
|
res := make([]net.IP, 0, len(parts))
|
|
for _, p := range parts {
|
|
trimmed := strings.TrimSpace(p)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if ip := net.ParseIP(trimmed); ip != nil {
|
|
res = append(res, ip)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (d *decodeState) readCgpBlock(open, close byte) []byte {
|
|
tempSpan := d.span
|
|
totalB64Len := 0
|
|
for {
|
|
for tempSpan < d.len && d.data[tempSpan] != close {
|
|
totalB64Len++
|
|
tempSpan++
|
|
}
|
|
if tempSpan < d.len {
|
|
tempSpan++
|
|
}
|
|
for tempSpan < d.len && isSpace(d.data[tempSpan]) {
|
|
tempSpan++
|
|
}
|
|
if tempSpan < d.len && d.data[tempSpan] == open {
|
|
tempSpan++
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
result := make([]byte, 0, base64.StdEncoding.DecodedLen(totalB64Len))
|
|
for d.span < d.len {
|
|
d.scratch.Reset()
|
|
for d.span < d.len && d.data[d.span] != close {
|
|
d.scratch.WriteByte(d.data[d.span])
|
|
d.span++
|
|
}
|
|
if d.span < d.len {
|
|
d.span++
|
|
}
|
|
|
|
if d.scratch.Len() > 0 {
|
|
src := d.scratch.Bytes()
|
|
start := len(result)
|
|
n := base64.StdEncoding.DecodedLen(len(src))
|
|
result = result[:start+n]
|
|
actualLen, err := base64.StdEncoding.Decode(result[start:], src)
|
|
if err == nil {
|
|
result = result[:start+actualLen]
|
|
} else {
|
|
result = result[:start]
|
|
result = append(result, src...)
|
|
}
|
|
}
|
|
d.skipSpaces()
|
|
if d.span < d.len && d.data[d.span] == open {
|
|
d.span++
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return result
|
|
}
|
|
|
|
func tryCgpBool(b []byte) (any, bool) {
|
|
switch len(b) {
|
|
case 2:
|
|
if bytes.Equal(b, binNoUpper) || bytes.Equal(b, binNoLower) {
|
|
return false, true
|
|
}
|
|
case 3:
|
|
if bytes.Equal(b, binYesUpper) || bytes.Equal(b, binYesLower) {
|
|
return true, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func tryParseInt(b []byte) (any, bool) {
|
|
if len(b) > 0 && ((b[0] >= '0' && b[0] <= '9') || b[0] == '-') {
|
|
s := unsafe.String(unsafe.SliceData(b), len(b))
|
|
if val, err := strconv.ParseInt(s, 10, 64); err == nil {
|
|
return val, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|