safing-structures/dsd/dsd.go
2025-02-21 10:53:32 +01:00

160 lines
4 KiB
Go

package dsd
// dynamic structured data
// check here for some benchmarks: https://github.com/alecthomas/go_serialization_benchmarks
import (
"encoding/json"
"errors"
"fmt"
"io"
"github.com/fxamacker/cbor/v2"
"github.com/ghodss/yaml"
"github.com/vmihailenco/msgpack/v5"
"github.com/safing/structures/varint"
)
// Load loads an dsd structured data blob into the given interface.
func Load(data []byte, t interface{}) (format uint8, err error) {
format, read, err := loadFormat(data)
if err != nil {
return 0, err
}
_, ok := ValidateSerializationFormat(format)
if ok {
return format, LoadAsFormat(data[read:], format, t)
}
return DecompressAndLoad(data[read:], format, t)
}
// LoadAsFormat loads a data blob into the interface using the specified format.
func LoadAsFormat(data []byte, format uint8, t interface{}) (err error) {
switch format {
case RAW:
return ErrIsRaw
case JSON:
err = json.Unmarshal(data, t)
if err != nil {
return fmt.Errorf("dsd: failed to unpack json: %w, data: %s", err, safeFirst16Bytes(data))
}
return nil
case YAML:
err = yaml.Unmarshal(data, t)
if err != nil {
return fmt.Errorf("dsd: failed to unpack yaml: %w, data: %s", err, safeFirst16Bytes(data))
}
return nil
case CBOR:
err = cbor.Unmarshal(data, t)
if err != nil {
return fmt.Errorf("dsd: failed to unpack cbor: %w, data: %s", err, safeFirst16Bytes(data))
}
return nil
case MsgPack:
err = msgpack.Unmarshal(data, t)
if err != nil {
return fmt.Errorf("dsd: failed to unpack msgpack: %w, data: %s", err, safeFirst16Bytes(data))
}
return nil
case GenCode:
genCodeStruct, ok := t.(GenCodeCompatible)
if !ok {
return errors.New("dsd: gencode is not supported by the given data structure")
}
_, err = genCodeStruct.GenCodeUnmarshal(data)
if err != nil {
return fmt.Errorf("dsd: failed to unpack gencode: %w, data: %s", err, safeFirst16Bytes(data))
}
return nil
default:
return ErrIncompatibleFormat
}
}
func loadFormat(data []byte) (format uint8, read int, err error) {
format, read, err = varint.Unpack8(data)
if err != nil {
return 0, 0, err
}
if len(data) <= read {
return 0, 0, io.ErrUnexpectedEOF
}
return format, read, nil
}
// Dump stores the interface as a dsd formatted data structure.
func Dump(t interface{}, format uint8) ([]byte, error) {
return DumpIndent(t, format, "")
}
// DumpIndent stores the interface as a dsd formatted data structure with indentation, if available.
func DumpIndent(t interface{}, format uint8, indent string) ([]byte, error) {
data, err := DumpWithoutIdentifier(t, format, indent)
if err != nil {
return nil, err
}
// TODO: Find a better way to do this.
return append(varint.Pack8(format), data...), nil
}
// DumpWithoutIdentifier stores the interface as a data structure, without format identifier, but with indentation, if specified and available.
func DumpWithoutIdentifier(t interface{}, format uint8, indent string) ([]byte, error) {
format, ok := ValidateSerializationFormat(format)
if !ok {
return nil, ErrIncompatibleFormat
}
var data []byte
var err error
switch format {
case RAW:
var ok bool
data, ok = t.([]byte)
if !ok {
return nil, ErrIncompatibleFormat
}
case JSON:
// TODO: use SetEscapeHTML(false)
if indent != "" {
data, err = json.MarshalIndent(t, "", indent)
} else {
data, err = json.Marshal(t)
}
if err != nil {
return nil, err
}
case YAML:
data, err = yaml.Marshal(t)
if err != nil {
return nil, err
}
case CBOR:
data, err = cbor.Marshal(t)
if err != nil {
return nil, err
}
case MsgPack:
data, err = msgpack.Marshal(t)
if err != nil {
return nil, err
}
case GenCode:
genCodeStruct, ok := t.(GenCodeCompatible)
if !ok {
return nil, errors.New("dsd: gencode is not supported by the given data structure")
}
data, err = genCodeStruct.GenCodeMarshal(nil)
if err != nil {
return nil, fmt.Errorf("dsd: failed to pack gencode struct: %w", err)
}
default:
return nil, ErrIncompatibleFormat
}
return data, nil
}