Co-authored-by: Claude <developers@lethean.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dd25cff835
commit
c122e89f40
3 changed files with 765 additions and 0 deletions
70
pkg/io/sigil/sigil.go
Normal file
70
pkg/io/sigil/sigil.go
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Package sigil provides the Sigil composable transform framework for reversible
|
||||||
|
// and irreversible data transformations.
|
||||||
|
//
|
||||||
|
// Sigils are the core abstraction -- each sigil implements a specific transformation
|
||||||
|
// (encoding, compression, hashing) with a uniform interface. Sigils can be chained
|
||||||
|
// together to create transformation pipelines via Transmute and Untransmute.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// hexSigil, _ := sigil.NewSigil("hex")
|
||||||
|
// base64Sigil, _ := sigil.NewSigil("base64")
|
||||||
|
// encoded, _ := sigil.Transmute(data, []sigil.Sigil{hexSigil, base64Sigil})
|
||||||
|
// decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, base64Sigil})
|
||||||
|
package sigil
|
||||||
|
|
||||||
|
// Sigil defines the interface for a composable data transformer.
|
||||||
|
//
|
||||||
|
// A Sigil represents a single transformation unit that can be applied to byte data.
|
||||||
|
// Sigils may be reversible (encoding, compression) or irreversible (hashing).
|
||||||
|
//
|
||||||
|
// For reversible sigils: Out(In(x)) == x for all valid x
|
||||||
|
// For irreversible sigils: Out returns the input unchanged
|
||||||
|
// For symmetric sigils: In(x) == Out(x)
|
||||||
|
//
|
||||||
|
// Implementations must handle nil input by returning nil without error,
|
||||||
|
// and empty input by returning an empty slice without error.
|
||||||
|
type Sigil interface {
|
||||||
|
// In applies the forward transformation to the data.
|
||||||
|
// For encoding sigils, this encodes the data.
|
||||||
|
// For compression sigils, this compresses the data.
|
||||||
|
// For hash sigils, this computes the digest.
|
||||||
|
In(data []byte) ([]byte, error)
|
||||||
|
|
||||||
|
// Out applies the reverse transformation to the data.
|
||||||
|
// For reversible sigils, this recovers the original data.
|
||||||
|
// For irreversible sigils (e.g., hashing), this returns the input unchanged.
|
||||||
|
Out(data []byte) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transmute applies a series of sigils to data in forward sequence.
|
||||||
|
//
|
||||||
|
// Each sigil's In method is called in order, with the output of one sigil
|
||||||
|
// becoming the input of the next. If any sigil returns an error, Transmute
|
||||||
|
// stops immediately and returns nil with that error.
|
||||||
|
func Transmute(data []byte, sigils []Sigil) ([]byte, error) {
|
||||||
|
var err error
|
||||||
|
for _, s := range sigils {
|
||||||
|
data, err = s.In(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untransmute applies a series of sigils to data in reverse sequence.
|
||||||
|
//
|
||||||
|
// Each sigil's Out method is called in reverse order, unwinding a previous
|
||||||
|
// Transmute operation. If any sigil returns an error, Untransmute stops
|
||||||
|
// immediately and returns nil with that error.
|
||||||
|
func Untransmute(data []byte, sigils []Sigil) ([]byte, error) {
|
||||||
|
var err error
|
||||||
|
for i := len(sigils) - 1; i >= 0; i-- {
|
||||||
|
data, err = sigils[i].Out(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
422
pkg/io/sigil/sigil_test.go
Normal file
422
pkg/io/sigil/sigil_test.go
Normal file
|
|
@ -0,0 +1,422 @@
|
||||||
|
package sigil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ReverseSigil
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestReverseSigil_Good(t *testing.T) {
|
||||||
|
s := &ReverseSigil{}
|
||||||
|
|
||||||
|
out, err := s.In([]byte("hello"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("olleh"), out)
|
||||||
|
|
||||||
|
// Symmetric: Out does the same thing.
|
||||||
|
restored, err := s.Out(out)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("hello"), restored)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReverseSigil_Bad(t *testing.T) {
|
||||||
|
s := &ReverseSigil{}
|
||||||
|
|
||||||
|
// Empty input returns empty.
|
||||||
|
out, err := s.In([]byte{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte{}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReverseSigil_Ugly(t *testing.T) {
|
||||||
|
s := &ReverseSigil{}
|
||||||
|
|
||||||
|
// Nil input returns nil.
|
||||||
|
out, err := s.In(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
|
||||||
|
out, err = s.Out(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HexSigil
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHexSigil_Good(t *testing.T) {
|
||||||
|
s := &HexSigil{}
|
||||||
|
data := []byte("hello world")
|
||||||
|
|
||||||
|
encoded, err := s.In(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte(hex.EncodeToString(data)), encoded)
|
||||||
|
|
||||||
|
decoded, err := s.Out(encoded)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, data, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHexSigil_Bad(t *testing.T) {
|
||||||
|
s := &HexSigil{}
|
||||||
|
|
||||||
|
// Invalid hex input.
|
||||||
|
_, err := s.Out([]byte("zzzz"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Empty input.
|
||||||
|
out, err := s.In([]byte{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte{}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHexSigil_Ugly(t *testing.T) {
|
||||||
|
s := &HexSigil{}
|
||||||
|
|
||||||
|
out, err := s.In(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
|
||||||
|
out, err = s.Out(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Base64Sigil
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestBase64Sigil_Good(t *testing.T) {
|
||||||
|
s := &Base64Sigil{}
|
||||||
|
data := []byte("composable transforms")
|
||||||
|
|
||||||
|
encoded, err := s.In(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString(data)), encoded)
|
||||||
|
|
||||||
|
decoded, err := s.Out(encoded)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, data, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBase64Sigil_Bad(t *testing.T) {
|
||||||
|
s := &Base64Sigil{}
|
||||||
|
|
||||||
|
// Invalid base64 (wrong padding).
|
||||||
|
_, err := s.Out([]byte("!!!"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Empty input.
|
||||||
|
out, err := s.In([]byte{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte{}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBase64Sigil_Ugly(t *testing.T) {
|
||||||
|
s := &Base64Sigil{}
|
||||||
|
|
||||||
|
out, err := s.In(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
|
||||||
|
out, err = s.Out(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// GzipSigil
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestGzipSigil_Good(t *testing.T) {
|
||||||
|
s := &GzipSigil{}
|
||||||
|
data := []byte("the quick brown fox jumps over the lazy dog")
|
||||||
|
|
||||||
|
compressed, err := s.In(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, data, compressed)
|
||||||
|
|
||||||
|
decompressed, err := s.Out(compressed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, data, decompressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGzipSigil_Bad(t *testing.T) {
|
||||||
|
s := &GzipSigil{}
|
||||||
|
|
||||||
|
// Invalid gzip data.
|
||||||
|
_, err := s.Out([]byte("not gzip"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Empty input compresses to a valid gzip stream.
|
||||||
|
compressed, err := s.In([]byte{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, compressed) // gzip header is always present
|
||||||
|
|
||||||
|
decompressed, err := s.Out(compressed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte{}, decompressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGzipSigil_Ugly(t *testing.T) {
|
||||||
|
s := &GzipSigil{}
|
||||||
|
|
||||||
|
out, err := s.In(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
|
||||||
|
out, err = s.Out(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JSONSigil
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestJSONSigil_Good(t *testing.T) {
|
||||||
|
s := &JSONSigil{Indent: false}
|
||||||
|
data := []byte(`{ "key" : "value" }`)
|
||||||
|
|
||||||
|
compacted, err := s.In(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte(`{"key":"value"}`), compacted)
|
||||||
|
|
||||||
|
// Out is passthrough.
|
||||||
|
passthrough, err := s.Out(compacted)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, compacted, passthrough)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSigil_Good_Indent(t *testing.T) {
|
||||||
|
s := &JSONSigil{Indent: true}
|
||||||
|
data := []byte(`{"key":"value"}`)
|
||||||
|
|
||||||
|
indented, err := s.In(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(indented), "\n")
|
||||||
|
assert.Contains(t, string(indented), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSigil_Bad(t *testing.T) {
|
||||||
|
s := &JSONSigil{Indent: false}
|
||||||
|
|
||||||
|
// Invalid JSON.
|
||||||
|
_, err := s.In([]byte("not json"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSigil_Ugly(t *testing.T) {
|
||||||
|
s := &JSONSigil{Indent: false}
|
||||||
|
|
||||||
|
// json.Compact on nil/empty will produce an error (invalid JSON).
|
||||||
|
_, err := s.In(nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Out with nil is passthrough.
|
||||||
|
out, err := s.Out(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HashSigil
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHashSigil_Good(t *testing.T) {
|
||||||
|
data := []byte("hash me")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sigilName string
|
||||||
|
size int
|
||||||
|
}{
|
||||||
|
{"md5", "md5", md5.Size},
|
||||||
|
{"sha1", "sha1", sha1.Size},
|
||||||
|
{"sha256", "sha256", sha256.Size},
|
||||||
|
{"sha512", "sha512", sha512.Size},
|
||||||
|
{"sha224", "sha224", sha256.Size224},
|
||||||
|
{"sha384", "sha384", sha512.Size384},
|
||||||
|
{"sha512-224", "sha512-224", 28},
|
||||||
|
{"sha512-256", "sha512-256", 32},
|
||||||
|
{"sha3-224", "sha3-224", 28},
|
||||||
|
{"sha3-256", "sha3-256", 32},
|
||||||
|
{"sha3-384", "sha3-384", 48},
|
||||||
|
{"sha3-512", "sha3-512", 64},
|
||||||
|
{"ripemd160", "ripemd160", 20},
|
||||||
|
{"blake2s-256", "blake2s-256", 32},
|
||||||
|
{"blake2b-256", "blake2b-256", 32},
|
||||||
|
{"blake2b-384", "blake2b-384", 48},
|
||||||
|
{"blake2b-512", "blake2b-512", 64},
|
||||||
|
{"md4", "md4", 16},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s, err := NewSigil(tt.sigilName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
hashed, err := s.In(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, hashed, tt.size)
|
||||||
|
|
||||||
|
// Out is passthrough.
|
||||||
|
passthrough, err := s.Out(hashed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, hashed, passthrough)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashSigil_Bad(t *testing.T) {
|
||||||
|
// Unsupported hash constant.
|
||||||
|
s := &HashSigil{Hash: 0}
|
||||||
|
_, err := s.In([]byte("data"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashSigil_Ugly(t *testing.T) {
|
||||||
|
// Hashing empty data should still produce a valid digest.
|
||||||
|
s, err := NewSigil("sha256")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
hashed, err := s.In([]byte{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, hashed, sha256.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// NewSigil factory
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestNewSigil_Good(t *testing.T) {
|
||||||
|
names := []string{
|
||||||
|
"reverse", "hex", "base64", "gzip", "json", "json-indent",
|
||||||
|
"md4", "md5", "sha1", "sha224", "sha256", "sha384", "sha512",
|
||||||
|
"ripemd160",
|
||||||
|
"sha3-224", "sha3-256", "sha3-384", "sha3-512",
|
||||||
|
"sha512-224", "sha512-256",
|
||||||
|
"blake2s-256", "blake2b-256", "blake2b-384", "blake2b-512",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
s, err := NewSigil(name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSigil_Bad(t *testing.T) {
|
||||||
|
_, err := NewSigil("nonexistent")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown sigil name")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSigil_Ugly(t *testing.T) {
|
||||||
|
_, err := NewSigil("")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Transmute / Untransmute
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestTransmute_Good(t *testing.T) {
|
||||||
|
data := []byte("round trip")
|
||||||
|
|
||||||
|
hexSigil, err := NewSigil("hex")
|
||||||
|
require.NoError(t, err)
|
||||||
|
base64Sigil, err := NewSigil("base64")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
chain := []Sigil{hexSigil, base64Sigil}
|
||||||
|
|
||||||
|
encoded, err := Transmute(data, chain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, data, encoded)
|
||||||
|
|
||||||
|
decoded, err := Untransmute(encoded, chain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, data, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransmute_Good_MultiSigil(t *testing.T) {
|
||||||
|
data := []byte("multi sigil pipeline test data")
|
||||||
|
|
||||||
|
reverseSigil, err := NewSigil("reverse")
|
||||||
|
require.NoError(t, err)
|
||||||
|
hexSigil, err := NewSigil("hex")
|
||||||
|
require.NoError(t, err)
|
||||||
|
base64Sigil, err := NewSigil("base64")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
chain := []Sigil{reverseSigil, hexSigil, base64Sigil}
|
||||||
|
|
||||||
|
encoded, err := Transmute(data, chain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decoded, err := Untransmute(encoded, chain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, data, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransmute_Good_GzipRoundTrip(t *testing.T) {
|
||||||
|
data := []byte("compress then encode then decode then decompress")
|
||||||
|
|
||||||
|
gzipSigil, err := NewSigil("gzip")
|
||||||
|
require.NoError(t, err)
|
||||||
|
hexSigil, err := NewSigil("hex")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
chain := []Sigil{gzipSigil, hexSigil}
|
||||||
|
|
||||||
|
encoded, err := Transmute(data, chain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decoded, err := Untransmute(encoded, chain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, data, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransmute_Bad(t *testing.T) {
|
||||||
|
// Transmute with a sigil that will fail: hex decode on non-hex input.
|
||||||
|
hexSigil := &HexSigil{}
|
||||||
|
|
||||||
|
// Calling Out (decode) with invalid input via manual chain.
|
||||||
|
_, err := Untransmute([]byte("not-hex!!"), []Sigil{hexSigil})
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransmute_Ugly(t *testing.T) {
|
||||||
|
// Empty sigil chain is a no-op.
|
||||||
|
data := []byte("unchanged")
|
||||||
|
|
||||||
|
result, err := Transmute(data, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, data, result)
|
||||||
|
|
||||||
|
result, err = Untransmute(data, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, data, result)
|
||||||
|
|
||||||
|
// Nil data through a chain.
|
||||||
|
hexSigil, _ := NewSigil("hex")
|
||||||
|
result, err = Transmute(nil, []Sigil{hexSigil})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
273
pkg/io/sigil/sigils.go
Normal file
273
pkg/io/sigil/sigils.go
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
package sigil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto"
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/blake2b"
|
||||||
|
"golang.org/x/crypto/blake2s"
|
||||||
|
"golang.org/x/crypto/md4"
|
||||||
|
"golang.org/x/crypto/ripemd160"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReverseSigil is a symmetric Sigil that reverses the bytes of the payload.
|
||||||
|
// Both In and Out perform the same reversal operation.
|
||||||
|
type ReverseSigil struct{}
|
||||||
|
|
||||||
|
// In reverses the bytes of the data.
|
||||||
|
func (s *ReverseSigil) In(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
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 (symmetric with In).
|
||||||
|
func (s *ReverseSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
return s.In(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HexSigil is a Sigil that encodes/decodes data to/from hexadecimal.
|
||||||
|
// In encodes the data, Out decodes it.
|
||||||
|
type HexSigil struct{}
|
||||||
|
|
||||||
|
// In encodes the data to hexadecimal.
|
||||||
|
func (s *HexSigil) In(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
dst := make([]byte, hex.EncodedLen(len(data)))
|
||||||
|
hex.Encode(dst, data)
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out decodes the data from hexadecimal.
|
||||||
|
func (s *HexSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
dst := make([]byte, hex.DecodedLen(len(data)))
|
||||||
|
_, err := hex.Decode(dst, data)
|
||||||
|
return dst, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64Sigil is a Sigil that encodes/decodes data to/from standard base64.
|
||||||
|
// In encodes the data, Out decodes it.
|
||||||
|
type Base64Sigil struct{}
|
||||||
|
|
||||||
|
// In encodes the data to base64.
|
||||||
|
func (s *Base64Sigil) In(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
dst := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
||||||
|
base64.StdEncoding.Encode(dst, data)
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out decodes the data from base64.
|
||||||
|
func (s *Base64Sigil) Out(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
dst := make([]byte, base64.StdEncoding.DecodedLen(len(data)))
|
||||||
|
n, err := base64.StdEncoding.Decode(dst, data)
|
||||||
|
return dst[:n], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GzipSigil is a Sigil that compresses/decompresses data using gzip.
|
||||||
|
// In compresses the data, Out decompresses it.
|
||||||
|
type GzipSigil struct{}
|
||||||
|
|
||||||
|
// In compresses the data using gzip.
|
||||||
|
func (s *GzipSigil) In(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var b bytes.Buffer
|
||||||
|
gz := gzip.NewWriter(&b)
|
||||||
|
if _, err := gz.Write(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := gz.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out decompresses the data using gzip.
|
||||||
|
func (s *GzipSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
r, err := gzip.NewReader(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
return io.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONSigil is a Sigil that compacts or indents JSON data.
|
||||||
|
// Out is a passthrough (no-op).
|
||||||
|
type JSONSigil struct {
|
||||||
|
Indent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// In compacts or indents the JSON data depending on the Indent field.
|
||||||
|
func (s *JSONSigil) In(data []byte) ([]byte, error) {
|
||||||
|
if s.Indent {
|
||||||
|
var out bytes.Buffer
|
||||||
|
err := json.Indent(&out, data, "", " ")
|
||||||
|
return out.Bytes(), err
|
||||||
|
}
|
||||||
|
var out bytes.Buffer
|
||||||
|
err := json.Compact(&out, data)
|
||||||
|
return out.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out is a passthrough for JSONSigil. The primary use is formatting.
|
||||||
|
func (s *JSONSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashSigil is a Sigil that hashes data using a specified algorithm.
|
||||||
|
// In computes the hash digest, Out is a passthrough.
|
||||||
|
type HashSigil struct {
|
||||||
|
Hash crypto.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHashSigil creates a new HashSigil for the given hash algorithm.
|
||||||
|
func NewHashSigil(h crypto.Hash) *HashSigil {
|
||||||
|
return &HashSigil{Hash: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In hashes the data using the configured algorithm.
|
||||||
|
func (s *HashSigil) In(data []byte) ([]byte, error) {
|
||||||
|
var h io.Writer
|
||||||
|
switch s.Hash {
|
||||||
|
case crypto.MD4:
|
||||||
|
h = md4.New()
|
||||||
|
case crypto.MD5:
|
||||||
|
h = md5.New()
|
||||||
|
case crypto.SHA1:
|
||||||
|
h = sha1.New()
|
||||||
|
case crypto.SHA224:
|
||||||
|
h = sha256.New224()
|
||||||
|
case crypto.SHA256:
|
||||||
|
h = sha256.New()
|
||||||
|
case crypto.SHA384:
|
||||||
|
h = sha512.New384()
|
||||||
|
case crypto.SHA512:
|
||||||
|
h = sha512.New()
|
||||||
|
case crypto.RIPEMD160:
|
||||||
|
h = ripemd160.New()
|
||||||
|
case crypto.SHA3_224:
|
||||||
|
h = sha3.New224()
|
||||||
|
case crypto.SHA3_256:
|
||||||
|
h = sha3.New256()
|
||||||
|
case crypto.SHA3_384:
|
||||||
|
h = sha3.New384()
|
||||||
|
case crypto.SHA3_512:
|
||||||
|
h = sha3.New512()
|
||||||
|
case crypto.SHA512_224:
|
||||||
|
h = sha512.New512_224()
|
||||||
|
case crypto.SHA512_256:
|
||||||
|
h = sha512.New512_256()
|
||||||
|
case crypto.BLAKE2s_256:
|
||||||
|
h, _ = blake2s.New256(nil)
|
||||||
|
case crypto.BLAKE2b_256:
|
||||||
|
h, _ = blake2b.New256(nil)
|
||||||
|
case crypto.BLAKE2b_384:
|
||||||
|
h, _ = blake2b.New384(nil)
|
||||||
|
case crypto.BLAKE2b_512:
|
||||||
|
h, _ = blake2b.New512(nil)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("sigil: hash algorithm not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Write(data)
|
||||||
|
return h.(interface{ Sum([]byte) []byte }).Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out is a passthrough for HashSigil. Hashing is irreversible.
|
||||||
|
func (s *HashSigil) Out(data []byte) ([]byte, error) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigil is a factory function that returns a Sigil based on a string name.
|
||||||
|
// It is the primary way to create Sigil instances.
|
||||||
|
//
|
||||||
|
// Supported names: reverse, hex, base64, gzip, json, json-indent,
|
||||||
|
// md4, md5, sha1, sha224, sha256, sha384, sha512, ripemd160,
|
||||||
|
// sha3-224, sha3-256, sha3-384, sha3-512, sha512-224, sha512-256,
|
||||||
|
// blake2s-256, blake2b-256, blake2b-384, blake2b-512.
|
||||||
|
func NewSigil(name string) (Sigil, error) {
|
||||||
|
switch name {
|
||||||
|
case "reverse":
|
||||||
|
return &ReverseSigil{}, nil
|
||||||
|
case "hex":
|
||||||
|
return &HexSigil{}, nil
|
||||||
|
case "base64":
|
||||||
|
return &Base64Sigil{}, nil
|
||||||
|
case "gzip":
|
||||||
|
return &GzipSigil{}, nil
|
||||||
|
case "json":
|
||||||
|
return &JSONSigil{Indent: false}, nil
|
||||||
|
case "json-indent":
|
||||||
|
return &JSONSigil{Indent: true}, nil
|
||||||
|
case "md4":
|
||||||
|
return NewHashSigil(crypto.MD4), nil
|
||||||
|
case "md5":
|
||||||
|
return NewHashSigil(crypto.MD5), nil
|
||||||
|
case "sha1":
|
||||||
|
return NewHashSigil(crypto.SHA1), nil
|
||||||
|
case "sha224":
|
||||||
|
return NewHashSigil(crypto.SHA224), nil
|
||||||
|
case "sha256":
|
||||||
|
return NewHashSigil(crypto.SHA256), nil
|
||||||
|
case "sha384":
|
||||||
|
return NewHashSigil(crypto.SHA384), nil
|
||||||
|
case "sha512":
|
||||||
|
return NewHashSigil(crypto.SHA512), nil
|
||||||
|
case "ripemd160":
|
||||||
|
return NewHashSigil(crypto.RIPEMD160), nil
|
||||||
|
case "sha3-224":
|
||||||
|
return NewHashSigil(crypto.SHA3_224), nil
|
||||||
|
case "sha3-256":
|
||||||
|
return NewHashSigil(crypto.SHA3_256), nil
|
||||||
|
case "sha3-384":
|
||||||
|
return NewHashSigil(crypto.SHA3_384), nil
|
||||||
|
case "sha3-512":
|
||||||
|
return NewHashSigil(crypto.SHA3_512), nil
|
||||||
|
case "sha512-224":
|
||||||
|
return NewHashSigil(crypto.SHA512_224), nil
|
||||||
|
case "sha512-256":
|
||||||
|
return NewHashSigil(crypto.SHA512_256), nil
|
||||||
|
case "blake2s-256":
|
||||||
|
return NewHashSigil(crypto.BLAKE2s_256), nil
|
||||||
|
case "blake2b-256":
|
||||||
|
return NewHashSigil(crypto.BLAKE2b_256), nil
|
||||||
|
case "blake2b-384":
|
||||||
|
return NewHashSigil(crypto.BLAKE2b_384), nil
|
||||||
|
case "blake2b-512":
|
||||||
|
return NewHashSigil(crypto.BLAKE2b_512), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("sigil: unknown sigil name: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue