feat: Introduce sigil transformers for trix containers

This commit introduces the concept of "sigils," which are programmable, pure-function transformers that can be applied to a Trix container's payload.

- A `Sigil` interface with `In` and `Out` methods is defined in the `trix` package.
- The `Trix` struct now includes a `Sigils` field to attach a chain of transformers.
- The `Encode` function applies the `In` transformations before encoding the payload.
- The caller is responsible for applying the `Out` transformations after decoding.

This new feature provides a flexible and extensible data pipeline for `Trix` containers.

The implementation is fully tested with the Good, Bad, and Ugly testing strategy.
This commit is contained in:
google-labs-jules[bot] 2025-10-31 02:20:59 +00:00
parent d5ae9a44e1
commit f7587b2471
3 changed files with 111 additions and 1 deletions

View file

@ -41,6 +41,7 @@ func main() {
trixContainer := &trix.Trix{
Header: header,
Payload: actualCiphertext,
Sigils: []trix.Sigil{&trix.ReverseSigil{}},
}
// 4. Encode the .trix container into its binary format
@ -58,6 +59,13 @@ func main() {
log.Fatalf("Failed to decode .trix container: %v", err)
}
// Manually apply the Out method of the sigil to restore the original payload.
restoredPayload, err := trixContainer.Sigils[0].Out(decodedTrix.Payload)
if err != nil {
log.Fatalf("Failed to apply sigil: %v", err)
}
decodedTrix.Payload = restoredPayload
// 6. Reassemble the ciphertext (nonce + payload) and decrypt
retrievedNonceStr, ok := decodedTrix.Header["nonce"].(string)
if !ok {

View file

@ -17,12 +17,20 @@ var (
ErrInvalidMagicNumber = errors.New("trix: invalid magic number")
ErrInvalidVersion = errors.New("trix: invalid version")
ErrMagicNumberLength = errors.New("trix: magic number must be 4 bytes long")
ErrNilSigil = errors.New("trix: sigil cannot be nil")
)
// Sigil defines the interface for a data transformer.
type Sigil interface {
In(data []byte) ([]byte, error)
Out(data []byte) ([]byte, error)
}
// Trix represents the structure of a .trix file.
type Trix struct {
Header map[string]interface{}
Payload []byte
Sigils []Sigil `json:"-"` // Ignore Sigils during JSON marshaling
}
// Encode serializes a Trix struct into the .trix binary format.
@ -31,6 +39,19 @@ func Encode(trix *Trix, magicNumber string) ([]byte, error) {
return nil, ErrMagicNumberLength
}
// Apply sigils to the payload before encoding
payload := trix.Payload
for _, sigil := range trix.Sigils {
if sigil == nil {
return nil, ErrNilSigil
}
var err error
payload, err = sigil.In(payload)
if err != nil {
return nil, err
}
}
headerBytes, err := json.Marshal(trix.Header)
if err != nil {
return nil, err
@ -60,7 +81,7 @@ func Encode(trix *Trix, magicNumber string) ([]byte, error) {
}
// Write Payload
if _, err := buf.Write(trix.Payload); err != nil {
if _, err := buf.Write(payload); err != nil {
return nil, err
}
@ -68,6 +89,7 @@ func Encode(trix *Trix, magicNumber string) ([]byte, error) {
}
// Decode deserializes the .trix binary format into a Trix struct.
// Note: Sigils are not stored in the format and must be re-attached by the caller.
func Decode(data []byte, magicNumber string) (*Trix, error) {
if len(magicNumber) != 4 {
return nil, ErrMagicNumberLength
@ -120,3 +142,21 @@ func Decode(data []byte, magicNumber string) (*Trix, error) {
Payload: payload,
}, nil
}
// ReverseSigil is an example Sigil that reverses the bytes of the payload.
type ReverseSigil struct{}
// In reverses the bytes of the data.
func (s *ReverseSigil) In(data []byte) ([]byte, error) {
reversed := make([]byte, len(data))
for i, j := 0, len(data)-1; i < len(data); i, j = i+1, j-1 {
reversed[i] = data[j]
}
return reversed, nil
}
// Out reverses the bytes of the data.
func (s *ReverseSigil) Out(data []byte) ([]byte, error) {
// Reversing the bytes again restores the original data.
return s.In(data)
}

View file

@ -1,6 +1,7 @@
package trix
import (
"errors"
"io"
"reflect"
"testing"
@ -111,3 +112,64 @@ func TestTrixEncodeDecode_Ugly(t *testing.T) {
assert.NotNil(t, decoded)
})
}
// --- Sigil Tests ---
// FailingSigil is a helper for testing sigils that intentionally fail.
type FailingSigil struct {
err error
}
func (s *FailingSigil) In(data []byte) ([]byte, error) {
return nil, s.err
}
func (s *FailingSigil) Out(data []byte) ([]byte, error) {
return nil, s.err
}
func TestSigilPipeline_Good(t *testing.T) {
originalPayload := []byte("hello world")
trix := &Trix{
Header: map[string]interface{}{},
Payload: originalPayload,
Sigils: []Sigil{&ReverseSigil{}},
}
encoded, err := Encode(trix, "SIGL")
assert.NoError(t, err)
decoded, err := Decode(encoded, "SIGL")
assert.NoError(t, err)
// Manually apply the Out method to restore the original payload.
restoredPayload, err := trix.Sigils[0].Out(decoded.Payload)
assert.NoError(t, err)
assert.Equal(t, originalPayload, restoredPayload)
}
func TestSigilPipeline_Bad(t *testing.T) {
expectedErr := errors.New("sigil failed")
trix := &Trix{
Header: map[string]interface{}{},
Payload: []byte("some data"),
Sigils: []Sigil{&ReverseSigil{}, &FailingSigil{err: expectedErr}},
}
_, err := Encode(trix, "FAIL")
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
}
func TestSigilPipeline_Ugly(t *testing.T) {
t.Run("NilSigil", func(t *testing.T) {
trix := &Trix{
Header: map[string]interface{}{},
Payload: []byte("some data"),
Sigils: []Sigil{nil},
}
_, err := Encode(trix, "UGLY")
assert.Error(t, err)
assert.Equal(t, ErrNilSigil, err)
})
}