chrly/internal/db/serializer.go
2024-02-01 07:58:26 +01:00

137 lines
3.5 KiB
Go

package db
import (
"bytes"
"compress/zlib"
"io"
"strings"
"github.com/valyala/fastjson"
)
type ProfileSerializer interface {
Serialize(profile *Profile) ([]byte, error)
Deserialize(value []byte) (*Profile, error)
}
func NewJsonSerializer() *JsonSerializer {
return &JsonSerializer{
parserPool: &fastjson.ParserPool{},
}
}
type JsonSerializer struct {
parserPool *fastjson.ParserPool
}
// Reasons for manual JSON serialization:
// 1. The Profile must be pure and must not contain tags.
// 2. Without tags it's impossible to apply omitempty during serialization.
// 3. Without omitempty we significantly inflate the storage size, which is critical for large deployments.
// Since the JSON structure in this case is very simple, it's very easy to write a manual serialization,
// achieving all constraints above.
func (s *JsonSerializer) Serialize(profile *Profile) ([]byte, error) {
var builder strings.Builder
// Prepare for the worst case (e.g. long username, long textures links, long Mojang textures and signature)
// to prevent additional memory allocations during serialization
builder.Grow(1536)
builder.WriteString(`{"uuid":"`)
builder.WriteString(profile.Uuid)
builder.WriteString(`","username":"`)
builder.WriteString(profile.Username)
builder.WriteString(`"`)
if profile.SkinUrl != "" {
builder.WriteString(`,"skinUrl":"`)
builder.WriteString(profile.SkinUrl)
builder.WriteString(`"`)
if profile.SkinModel != "" {
builder.WriteString(`,"skinModel":"`)
builder.WriteString(profile.SkinModel)
builder.WriteString(`"`)
}
}
if profile.CapeUrl != "" {
builder.WriteString(`,"capeUrl":"`)
builder.WriteString(profile.CapeUrl)
builder.WriteString(`"`)
}
if profile.MojangTextures != "" {
builder.WriteString(`,"mojangTextures":"`)
builder.WriteString(profile.MojangTextures)
builder.WriteString(`","mojangSignature":"`)
builder.WriteString(profile.MojangSignature)
builder.WriteString(`"`)
}
builder.WriteString("}")
return []byte(builder.String()), nil
}
func (s *JsonSerializer) Deserialize(value []byte) (*Profile, error) {
parser := s.parserPool.Get()
defer s.parserPool.Put(parser)
v, err := parser.ParseBytes(value)
if err != nil {
return nil, err
}
profile := &Profile{
Uuid: string(v.GetStringBytes("uuid")),
Username: string(v.GetStringBytes("username")),
SkinUrl: string(v.GetStringBytes("skinUrl")),
SkinModel: string(v.GetStringBytes("skinModel")),
CapeUrl: string(v.GetStringBytes("capeUrl")),
MojangTextures: string(v.GetStringBytes("mojangTextures")),
MojangSignature: string(v.GetStringBytes("mojangSignature")),
}
return profile, nil
}
func NewZlibEncoder(serializer ProfileSerializer) *ZlibEncoder {
return &ZlibEncoder{serializer}
}
type ZlibEncoder struct {
serializer ProfileSerializer
}
func (s *ZlibEncoder) Serialize(profile *Profile) ([]byte, error) {
serialized, err := s.serializer.Serialize(profile)
if err != nil {
return nil, err
}
var buff bytes.Buffer
writer := zlib.NewWriter(&buff)
_, err = writer.Write(serialized)
if err != nil {
return nil, err
}
_ = writer.Close()
return buff.Bytes(), nil
}
func (s *ZlibEncoder) Deserialize(value []byte) (*Profile, error) {
buff := bytes.NewReader(value)
reader, err := zlib.NewReader(buff)
if err != nil {
return nil, err
}
resultBuffer := new(bytes.Buffer)
_, err = io.Copy(resultBuffer, reader)
if err != nil {
return nil, err
}
_ = reader.Close()
return s.serializer.Deserialize(resultBuffer.Bytes())
}