262 lines
7.8 KiB
Go
262 lines
7.8 KiB
Go
// Package trix implements the TRIX binary container format (RFC-0002).
|
|
//
|
|
// The .trix format is a generic, protocol-agnostic container for storing
|
|
// arbitrary binary payloads alongside structured JSON metadata. It consists of:
|
|
//
|
|
// [Magic Number (4)] [Version (1)] [Header Length (4)] [JSON Header] [Payload]
|
|
//
|
|
// Key features:
|
|
// - Custom 4-byte magic number for application-specific identification
|
|
// - Extensible JSON header for metadata (content type, checksums, timestamps)
|
|
// - Optional integrity verification via configurable checksum algorithms
|
|
// - Integration with the Sigil transformation framework for encoding/compression
|
|
//
|
|
// Example usage:
|
|
//
|
|
// container := &trix.Trix{
|
|
// Header: map[string]interface{}{"content_type": "text/plain"},
|
|
// Payload: []byte("Hello, World!"),
|
|
// }
|
|
// encoded, _ := trix.Encode(container, "MYAP", nil)
|
|
package trix
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/Snider/Enchantrix/pkg/crypt"
|
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
|
)
|
|
|
|
const (
|
|
// Version is the current version of the .trix file format.
|
|
// See RFC-0002 for version history and compatibility notes.
|
|
Version = 2
|
|
// MaxHeaderSize is the maximum allowed size for the header (16 MB).
|
|
// This limit prevents denial-of-service attacks via large header allocations.
|
|
MaxHeaderSize = 16 * 1024 * 1024 // 16 MB
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidMagicNumber is returned when the magic number is incorrect.
|
|
ErrInvalidMagicNumber = errors.New("trix: invalid magic number")
|
|
// ErrInvalidVersion is returned when the version is incorrect.
|
|
ErrInvalidVersion = errors.New("trix: invalid version")
|
|
// ErrMagicNumberLength is returned when the magic number is not 4 bytes long.
|
|
ErrMagicNumberLength = errors.New("trix: magic number must be 4 bytes long")
|
|
// ErrNilSigil is returned when a sigil is nil.
|
|
ErrNilSigil = errors.New("trix: sigil cannot be nil")
|
|
// ErrChecksumMismatch is returned when the checksum does not match.
|
|
ErrChecksumMismatch = errors.New("trix: checksum mismatch")
|
|
// ErrHeaderTooLarge is returned when the header size exceeds the maximum allowed.
|
|
ErrHeaderTooLarge = errors.New("trix: header size exceeds maximum allowed")
|
|
)
|
|
|
|
// Trix represents a .trix container with header metadata and binary payload.
|
|
//
|
|
// The Header field holds arbitrary JSON-serializable metadata. Common fields include:
|
|
// - content_type: MIME type of the original payload
|
|
// - created_at: ISO 8601 timestamp
|
|
// - encryption_algorithm: Algorithm used for encryption (if applicable)
|
|
// - checksum: Hex-encoded integrity checksum (auto-populated if ChecksumAlgo is set)
|
|
//
|
|
// The InSigils and OutSigils fields specify transformation pipelines:
|
|
// - InSigils: Applied during Pack() in order (e.g., ["gzip", "base64"])
|
|
// - OutSigils: Applied during Unpack() in reverse order (defaults to InSigils)
|
|
type Trix struct {
|
|
// Header contains JSON-serializable metadata about the payload.
|
|
Header map[string]interface{}
|
|
// Payload is the binary data stored in the container.
|
|
Payload []byte
|
|
// InSigils lists sigil names to apply during Pack (forward transformation).
|
|
InSigils []string `json:"-"`
|
|
// OutSigils lists sigil names to apply during Unpack (reverse transformation).
|
|
// If empty, InSigils is used in reverse order.
|
|
OutSigils []string `json:"-"`
|
|
// ChecksumAlgo specifies the hash algorithm for integrity verification.
|
|
// If set, a checksum is computed and stored in the header during Encode.
|
|
ChecksumAlgo crypt.HashType `json:"-"`
|
|
}
|
|
|
|
// Encode serializes a Trix struct into the .trix binary format.
|
|
// It returns the encoded data as a byte slice.
|
|
func Encode(trix *Trix, magicNumber string, w io.Writer) ([]byte, error) {
|
|
if len(magicNumber) != 4 {
|
|
return nil, ErrMagicNumberLength
|
|
}
|
|
|
|
// Calculate and add checksum if an algorithm is specified
|
|
if trix.ChecksumAlgo != "" {
|
|
checksum := crypt.NewService().Hash(trix.ChecksumAlgo, string(trix.Payload))
|
|
trix.Header["checksum"] = checksum
|
|
trix.Header["checksum_algo"] = string(trix.ChecksumAlgo)
|
|
}
|
|
|
|
headerBytes, err := json.Marshal(trix.Header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
headerLength := uint32(len(headerBytes))
|
|
|
|
// If no writer is provided, use an internal buffer.
|
|
// This maintains the original function signature's behavior of returning the byte slice.
|
|
var buf *bytes.Buffer
|
|
writer := w
|
|
if writer == nil {
|
|
buf = new(bytes.Buffer)
|
|
writer = buf
|
|
}
|
|
|
|
// Write Magic Number
|
|
if _, err := io.WriteString(writer, magicNumber); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write Version
|
|
if _, err := writer.Write([]byte{byte(Version)}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write Header Length
|
|
if err := binary.Write(writer, binary.BigEndian, headerLength); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write JSON Header
|
|
if _, err := writer.Write(headerBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write Payload
|
|
if _, err := writer.Write(trix.Payload); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If we used our internal buffer, return its bytes.
|
|
if buf != nil {
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// If an external writer was used, we can't return the bytes.
|
|
// The caller is responsible for the writer.
|
|
return nil, nil
|
|
}
|
|
|
|
// Decode deserializes the .trix binary format into a Trix struct.
|
|
// It returns the decoded Trix struct.
|
|
// Note: Sigils are not stored in the format and must be re-attached by the caller.
|
|
func Decode(data []byte, magicNumber string, r io.Reader) (*Trix, error) {
|
|
if len(magicNumber) != 4 {
|
|
return nil, ErrMagicNumberLength
|
|
}
|
|
|
|
var reader io.Reader
|
|
if r != nil {
|
|
reader = r
|
|
} else {
|
|
reader = bytes.NewReader(data)
|
|
}
|
|
|
|
// Read and Verify Magic Number
|
|
magic := make([]byte, 4)
|
|
if _, err := io.ReadFull(reader, magic); err != nil {
|
|
return nil, err
|
|
}
|
|
if string(magic) != magicNumber {
|
|
return nil, fmt.Errorf("%w: expected %s, got %s", ErrInvalidMagicNumber, magicNumber, string(magic))
|
|
}
|
|
|
|
// Read and Verify Version
|
|
versionByte := make([]byte, 1)
|
|
if _, err := io.ReadFull(reader, versionByte); err != nil {
|
|
return nil, err
|
|
}
|
|
if versionByte[0] != Version {
|
|
return nil, ErrInvalidVersion
|
|
}
|
|
|
|
// Read Header Length
|
|
var headerLength uint32
|
|
if err := binary.Read(reader, binary.BigEndian, &headerLength); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sanity check the header length to prevent massive allocations.
|
|
if headerLength > MaxHeaderSize {
|
|
return nil, ErrHeaderTooLarge
|
|
}
|
|
|
|
// Read JSON Header
|
|
headerBytes := make([]byte, headerLength)
|
|
if _, err := io.ReadFull(reader, headerBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
var header map[string]interface{}
|
|
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read Payload
|
|
payload, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify checksum if it exists in the header
|
|
if checksum, ok := header["checksum"].(string); ok {
|
|
algo, ok := header["checksum_algo"].(string)
|
|
if !ok {
|
|
return nil, errors.New("trix: checksum algorithm not found in header")
|
|
}
|
|
expectedChecksum := crypt.NewService().Hash(crypt.HashType(algo), string(payload))
|
|
if checksum != expectedChecksum {
|
|
return nil, ErrChecksumMismatch
|
|
}
|
|
}
|
|
|
|
return &Trix{
|
|
Header: header,
|
|
Payload: payload,
|
|
}, nil
|
|
}
|
|
|
|
// Pack applies the In method of all attached sigils to the payload.
|
|
// It modifies the Trix struct in place.
|
|
func (t *Trix) Pack() error {
|
|
for _, sigilName := range t.InSigils {
|
|
sigil, err := enchantrix.NewSigil(sigilName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.Payload, err = sigil.In(t.Payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unpack applies the Out method of all sigils in reverse order.
|
|
// It modifies the Trix struct in place.
|
|
func (t *Trix) Unpack() error {
|
|
sigilNames := t.OutSigils
|
|
if len(sigilNames) == 0 {
|
|
sigilNames = t.InSigils
|
|
}
|
|
for i := len(sigilNames) - 1; i >= 0; i-- {
|
|
sigilName := sigilNames[i]
|
|
sigil, err := enchantrix.NewSigil(sigilName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.Payload, err = sigil.Out(t.Payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|