Documentation
¶
Overview ¶
Package cgpdata implements a parser and serializer for the CommuniGate Pro internal Data Format, as specified at https://doc.communigatepro.ru/development/Data.html.
The format describes a small, closed set of object kinds: Strings (atoms and quoted strings), DataBlocks (Base64-encoded binary data), Numbers (64-bit signed integers), Time Stamps, IP Addresses, a Null object, ordered Arrays, and Dictionaries. This package represents each kind as a distinct Go type implementing the Value interface: Null, String, DataBlock, Number, TimeStamp, IPAddress, Array, and Dictionary.
Embedded XML objects (vCards, iCalendar, SDP, and similar structures the format allows to appear as raw XML) are not semantically decoded. They are preserved verbatim as XML, an opaque string holding the exact source text of the XML document.
Parsing ¶
Parse parses a single object from a byte slice. Decoder parses a sequence of objects from an io.Reader, similar to encoding/json. A Decoder reads its entire input into memory (bounded by Options.MaxSize) before decoding the first object, since the format allows embedded XML documents whose extent can only be determined by tokenizing them.
ParseWith and NewDecoderWith accept Options: a ParseMode selecting between the strict multi-line and single-line grammars and a mode compatible with the CommuniGate Pro server's actual observed behavior, plus limits on input size and nesting depth.
In every mode, a parsed String is guaranteed to be valid UTF-8 and to contain no NUL byte.
Serializing ¶
Marshal renders a Value as compact, single-line text. Use Encoder with Encoder.SetIndent, or MarshalIndent, to spread Array and Dictionary elements across multiple indented lines.
All three validate the value with Validate first, and refuse to encode a value whose text would be syntactically invalid or would parse back to a different value: whenever they succeed, parsing the text they wrote yields a value Equal to the one they were given.
Example (EmbeddedXML) ¶
Embedded XML objects are preserved verbatim as opaque text rather than being parsed into a structured Go type.
package main
import (
"fmt"
cgpdata "github.com/gmyzovsky/go-cgp-data"
)
func main() {
v, err := cgpdata.ParseString(`{Contact=<vCard><NAME><VALUE>Bjorn Jensen</VALUE></NAME></vCard>;}`)
if err != nil {
fmt.Println("error:", err)
return
}
dict := v.(cgpdata.Dictionary)
contact, _ := dict.Get("Contact")
fmt.Println(contact.(cgpdata.XML))
}
Output: <vCard><NAME><VALUE>Bjorn Jensen</VALUE></NAME></vCard>
Index ¶
- Constants
- func Equal(a, b Value) bool
- func Marshal(v Value) ([]byte, error)
- func MarshalIndent(v Value, prefix, indent string) ([]byte, error)
- func Validate(v Value) error
- type Array
- type DataBlock
- type Decoder
- type DictEntry
- type Dictionary
- type Encoder
- type IPAddress
- type Null
- type Number
- type Options
- type ParseMode
- type String
- type SyntaxError
- type TimeSpecial
- type TimeStamp
- type Value
- type XML
Examples ¶
Constants ¶
const ( // DefaultMaxDepth bounds the combined nesting depth of Arrays, // Dictionaries and embedded XML elements accepted by the parser // and by [Validate]. DefaultMaxDepth = 200 // DefaultMaxSize bounds the input size, in bytes, that a [Decoder] // reads into memory. DefaultMaxSize = 64 << 20 // 64 MiB )
Default limits, applied when the corresponding Options field is zero.
Variables ¶
This section is empty.
Functions ¶
func Equal ¶
Equal reports whether a and b represent the same CommuniGate Pro data object. Unlike reflect.DeepEqual, it compares TimeStamp values by the instant in time they represent (via time.Time.Equal) rather than by field-for-field equality of the underlying time.Time, and it compares Array and Dictionary elements recursively with Equal.
Dictionaries are compared as unordered sets of key/value pairs: two Dictionaries with the same pairs in a different order are equal. To compare the order-preserving textual representation instead, compare the Marshal outputs.
Cyclic values are handled the way reflect.DeepEqual handles them: a pair of values already being compared further up the call stack is assumed equal rather than recursed into, so Equal always terminates.
func Marshal ¶
Marshal returns the compact, single-line CommuniGate Pro textual representation of v.
Marshal validates v with Validate first and refuses to encode a value that would produce syntactically invalid text or text that parses back to a different value: whenever Marshal succeeds, parsing its output yields a value Equal to v.
Example ¶
package main
import (
"fmt"
cgpdata "github.com/gmyzovsky/go-cgp-data"
)
func main() {
v := cgpdata.Dictionary{
{Key: "Name", Value: cgpdata.String("Alice")},
{Key: "Age", Value: cgpdata.Number(30)},
}
data, err := cgpdata.Marshal(v)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(string(data))
}
Output: {Name=Alice; Age=#30; }
func MarshalIndent ¶
MarshalIndent is like Marshal, but formats the output with the given prefix and indent applied at each nesting level of Arrays and Dictionaries, similar to encoding/json.MarshalIndent. Both prefix and indent must consist only of whitespace (space, tab, CR, LF), so that the output remains parseable; anything else is an error.
func Validate ¶ added in v0.2.0
Validate reports whether v is a well-formed CommuniGate Pro data object that can be marshaled into text that parses back to an equal value. It checks, recursively:
- every String, including Dictionary keys, is valid UTF-8 and contains no NUL byte;
- Dictionary keys are unique;
- a DataBlock is not empty (the grammar has no representation for an empty one);
- a TimeStamp is a sentinel, or carries a non-zero UTC time with whole-second precision and a year in the normative 1970-2038 range;
- an IPAddress has a valid address without an IPv6 zone and a port in the range -1 (no port) to 65535;
- an XML value is exactly one well-formed XML document starting with '<';
- Arrays and Dictionaries nest no deeper than DefaultMaxDepth and contain no cycles.
Marshal, MarshalIndent and Encoder.Encode call Validate before writing anything, so they can never emit text that is syntactically invalid or that changes meaning when parsed back.
Types ¶
type Array ¶
type Array []Value
Array is an ordered sequence of Values, represented in text as a comma-separated, parenthesized list.
type DataBlock ¶
type DataBlock []byte
DataBlock is a block of arbitrary binary data, represented in text as Base64 enclosed in square brackets, e.g. "[HcqHfHI=]".
type Decoder ¶
type Decoder struct {
// contains filtered or unexported fields
}
Decoder reads a sequence of CommuniGate Pro data objects from an input stream, similar to encoding/json.Decoder.
A Decoder reads its entire input into memory (up to Options.MaxSize bytes) the first time Decode or More is called, rather than truly streaming it, because an embedded XML object's extent can only be found by tokenizing it and the format has no length prefix to bound that lookahead otherwise.
func NewDecoder ¶
NewDecoder returns a new Decoder that reads from r using the zero Options.
func NewDecoderWith ¶ added in v0.2.0
NewDecoderWith returns a new Decoder that reads from r according to opts.
func (*Decoder) Decode ¶
Decode reads the next CommuniGate Pro object from the input stream and returns its Value representation. It returns io.EOF when there are no more objects to read. Any other error is sticky: every subsequent Decode call returns it again.
func (*Decoder) Err ¶ added in v0.2.0
Err returns the first error the Decoder encountered, or nil. It never returns io.EOF: a clean end of input is not an error. Err reports, in particular, the errors that made Decoder.More return false, which More itself cannot distinguish from the end of the input.
func (*Decoder) InputOffset ¶
InputOffset returns the byte offset of the Decoder's current position within its input.
func (*Decoder) More ¶
More reports whether there is at least one more object to decode. It returns false both at a clean end of input and when the Decoder has failed (for example, because reading the input failed or an unterminated comment was found); after a loop conditioned on More, call Decoder.Err to distinguish the two.
type DictEntry ¶
DictEntry is a single key/value pair within a Dictionary.
type Dictionary ¶
type Dictionary []DictEntry
Dictionary is an ordered sequence of key/value pairs, represented in text as a sequence of "key=value;" pairs enclosed in curly braces. Keys are case-sensitive and unique within a Dictionary; Parse reports an error if a key repeats.
Dictionary is a slice rather than a map so that the insertion order preserved by the textual format survives a parse/marshal round trip.
func (Dictionary) Get ¶
func (d Dictionary) Get(key string) (Value, bool)
Get returns the value associated with key and reports whether key was present.
func (Dictionary) Keys ¶
func (d Dictionary) Keys() []string
Keys returns the Dictionary's keys, in order.
func (Dictionary) Set ¶
func (d Dictionary) Set(key string, v Value) Dictionary
Set returns a copy of d with key set to v, replacing the value of an existing entry with that key or appending a new entry if key is not already present.
type Encoder ¶
type Encoder struct {
// contains filtered or unexported fields
}
Encoder writes CommuniGate Pro textual object representations to an output stream.
func NewEncoder ¶
NewEncoder returns a new Encoder that writes to w.
func (*Encoder) Encode ¶
Encode writes the textual representation of v, followed by a newline. Like Marshal, it validates v with Validate first and writes nothing if v is not a well-formed value or if a non- whitespace prefix/indent was configured with Encoder.SetIndent.
func (*Encoder) SetIndent ¶
SetIndent configures the Encoder to format each subsequent [Encode] call with the given prefix and per-level indent, spreading Array and Dictionary elements across multiple lines. The zero value (no prefix, no indent) produces compact single-line output. Both prefix and indent must consist only of whitespace (space, tab, CR, LF), so that the output remains parseable; Encoder.Encode fails otherwise.
type IPAddress ¶
type IPAddress struct {
Addr netip.Addr
// Port is the associated port number, or -1 if the object has no
// port, e.g. "#I[10.0.44.55]" without a trailing ":port".
Port int
}
IPAddress is a CommuniGate Pro IP Address object: an IPv4 or IPv6 address with an optional port number.
A well-formed IPAddress - the only kind Validate accepts and Marshal encodes - has a valid Addr without an IPv6 zone (the format cannot represent zones) and a Port in the range -1 (no port) to 65535.
type Null ¶
type Null struct{}
Null represents the CommuniGate Pro #NULL# object, used to denote the absence of any other object.
type Number ¶
type Number int64
Number is a 64-bit signed integer object. Its text form starts with "#", an optional "-", and decimal digits, or a 0x/0o/0b base indicator followed by digits of that base. Marshal always emits decimal.
type Options ¶ added in v0.2.0
type Options struct {
// Mode selects the grammar variant.
Mode ParseMode
// MaxDepth bounds the combined nesting depth of Arrays,
// Dictionaries and embedded XML elements. Zero (or a negative
// value) means DefaultMaxDepth.
MaxDepth int
// MaxSize bounds, in bytes, how much input a Decoder reads into
// memory before parsing; a Decoder whose input is longer fails
// rather than truncating it. Zero (or a negative value) means
// DefaultMaxSize. ParseWith ignores MaxSize, since its input is
// already in memory.
MaxSize int64
}
Options configures parsing for ParseWith and NewDecoderWith. The zero value selects StrictMultiLine, DefaultMaxDepth and DefaultMaxSize.
type ParseMode ¶ added in v0.2.0
type ParseMode int
ParseMode selects the grammar variant used when parsing.
The CommuniGate Pro Data Format has two normative textual representations - a single-line form used in CLI/API responses and a multi-line form used in settings and other text files - and, beyond both, the CommuniGate Pro server accepts a number of extensions and deviations from the formal grammar. No single grammar can be strict and server-compatible at the same time, so the mode is explicit.
const ( // StrictMultiLine parses the multi-line text-file form of the // format: comments are recognized, and a string value is a // sequence of one or more consecutive string tokens - atoms and // quoted strings in any combination - that concatenate into a // single value, the grammar's way of splitting a long string // across lines. This is the default mode. StrictMultiLine ParseMode = iota // StrictSingleLine parses the single-line form used in CLI/API // responses: comments are not recognized, strings do not // concatenate, and the only whitespace between tokens is the // space character. StrictSingleLine // ServerCompatible parses input the way the CommuniGate Pro server // does, accepting its known deviations from the formal grammar: a // UTF-8 BOM in front of any object, nested ones included, is // skipped, though not one in front of a dictionary key, which is // read as a string rather than as an object; Base64 data is decoded // leniently (characters outside the alphabet are skipped, "=" ends // the block, a truncated group still contributes the byte it had // begun, and an empty data block is allowed); the additional // escapes \b, \f, \/, \' and \uXXXX are recognized; numbers may use // a leading "+" and uppercase 0X/0O/0B base prefixes; // application-specific object references (#(name:address)) parse as // [Null]; a repeated dictionary key replaces the earlier value // instead of failing; and the fields of a Time Stamp are digit runs // of any width whose seconds, and whose time of day as a whole, may // be left out, an empty run counting as zero. // // IP addresses follow the server's own address reader rather than // the normative grammar: the square brackets are optional, an IPv4 // octet is a run of decimal digits below 256 (so "010.000.000.001" // is 10.0.0.1), an IPv6 group is a run of hexadecimal digits below // 0x10000 of any length, blanks may surround the separators, and an // IPv6 address may also be written in the dotted notation // v6.x.x.x.x.x.x.x.x.v6. // // Time Stamps also follow the server's value model rather than the // normative year range. The server counts seconds from the start of // 1970, so a date it cannot count that way is not an error but one // of the two sentinels: [TimeStampPast] below the epoch, including // the epoch second itself, which is the server's own remote-past // marker, and [TimeStampFuture] from year 2100 on. That mapping // happens before the calendar fields are checked, so an impossible // day or month on such a date is not diagnosed. A year below 100 is // a two-digit year, mapped into 1970-2069. // // It also mirrors the server's restrictions that the normative // multi-line grammar does not have: an atom never concatenates // with an adjacent quoted string (only quoted strings concatenate // with each other). // // Even in this mode, strings must be valid UTF-8, must never // contain NUL, and may not contain unescaped control characters; // that part of the package's string contract is not relaxed. // // The mode reproduces how the server reads the multi-line form. It // does not reproduce one observed difference in how the server // reads the single-line form of CLI/API traffic: there a Time Stamp // whose third character after "#T" is a digit is taken to be // written in the compact calendar form (YYYYMMDD with an optional // Thhmmss), so "#T1-1-5" is rejected there while "#T01-01-69" is // not. // // Note that this mode can produce values outside the normative // format - an empty DataBlock, a TimeStamp between 2039 and 2099 - // which [Marshal] will refuse to encode, since their output could // not be parsed back by the strict grammar. ServerCompatible )
type String ¶
type String string
String is a CommuniGate Pro string. In text it is represented either as an atom (a run of letters, digits, and the punctuation ". - _ @", plus any non-ASCII UTF-8 character) or as a quoted string with backslash escapes. Parse unescapes quoted strings; Marshal prefers the atom form when the value's content allows it, and falls back to a quoted string otherwise.
type SyntaxError ¶
type SyntaxError struct {
Offset int64
Line int
Column int
// contains filtered or unexported fields
}
SyntaxError reports a malformed CommuniGate Pro data object, including the byte offset and the 1-based line/column at which the problem was detected. Lines are terminated by LF, CRLF, or a lone CR, each counting as a single line break; Column counts Unicode characters, not bytes.
func (*SyntaxError) Error ¶
func (e *SyntaxError) Error() string
type TimeSpecial ¶
type TimeSpecial int
TimeSpecial identifies one of the two sentinel Time Stamp values, #TPAST and #TFUTURE, that do not carry a concrete time.
const ( // TimeNone marks a TimeStamp that carries a concrete Time value. TimeNone TimeSpecial = iota // TimePast is the CommuniGate Pro "remote past" sentinel, #TPAST. TimePast // TimeFuture is the CommuniGate Pro "remote future" sentinel, #TFUTURE. TimeFuture )
type TimeStamp ¶
type TimeStamp struct {
// Time holds the represented instant, always in UTC. It is the
// zero time.Time when Special is not TimeNone.
Time time.Time
// Special identifies a sentinel value; TimeNone for a concrete Time.
Special TimeSpecial
}
TimeStamp is a CommuniGate Pro Time Stamp object: a GMT time value, or one of the two sentinel values representing the remote past or the remote future.
A well-formed TimeStamp - the only kind Validate accepts and Marshal encodes - is either a sentinel (Special is TimePast or TimeFuture, Time is the zero time.Time), or a concrete time with whole-second precision and a year within the normative grammar's 1970-2038 range. Construct values with NewTimeStamp, TimeStampPast and TimeStampFuture rather than with struct literals: the zero TimeStamp is not a valid value.
The textual format itself gives no special meaning to boundary dates, so the strict parse modes read the start of the Unix epoch (#T01-01-1970_00:00:00) as an ordinary time value. The server does not: it counts seconds from that instant and reserves the count zero for the remote past, so ServerCompatible mode reads that timestamp, and every earlier one, as TimeStampPast.
func NewTimeStamp ¶
NewTimeStamp returns a TimeStamp for the given instant, converted to UTC (CommuniGate Pro Time Stamps always represent GMT time) and truncated to whole seconds, since the textual format cannot represent sub-second precision.
func TimeStampFuture ¶
func TimeStampFuture() TimeStamp
TimeStampFuture returns the "remote future" sentinel Time Stamp (#TFUTURE).
func TimeStampPast ¶
func TimeStampPast() TimeStamp
TimeStampPast returns the "remote past" sentinel Time Stamp (#TPAST).
type Value ¶
type Value interface {
// contains filtered or unexported methods
}
Value is implemented by every CommuniGate Pro data object recognized by this package: Null, String, DataBlock, Number, TimeStamp, IPAddress, Array, Dictionary, and XML.
The interface is sealed - only types defined in this package implement it - so a type switch over a Value covering those nine kinds is exhaustive.
func Parse ¶
Parse parses a single CommuniGate Pro data object from data and returns its Value representation. Parse fails if data contains anything other than optional surrounding whitespace/comments plus exactly one object. It is equivalent to ParseWith with the zero Options: StrictMultiLine mode and DefaultMaxDepth.
Example ¶
package main
import (
"fmt"
cgpdata "github.com/gmyzovsky/go-cgp-data"
)
func main() {
v, err := cgpdata.ParseString(`{Name="Alice"; Age=#30; Tags=(admin, ops);}`)
if err != nil {
fmt.Println("error:", err)
return
}
dict := v.(cgpdata.Dictionary)
name, _ := dict.Get("Name")
fmt.Println(name)
}
Output: Alice
func ParseString ¶
ParseString is a convenience wrapper around Parse for callers that already have the input as a string.