Files
nexus/internal/leviathan/transforms/transforms.go
T
2026-06-16 11:51:07 +01:00

238 lines
5.5 KiB
Go

// Package transforms exposes chainable transform primitives. Each
// function takes an input []byte and options, and returns an output
// or an error. They are pure and do not mutate input.
package transforms
import (
"bytes"
"compress/flate"
"compress/gzip"
"compress/zlib"
"encoding/base64"
"encoding/hex"
"errors"
"io"
"net/url"
)
const MaxInflateOutput = 64 << 20
// HexEncode returns ASCII hex of src.
func HexEncode(src []byte) ([]byte, error) {
return []byte(hex.EncodeToString(src)), nil
}
// HexDecode parses ASCII hex into bytes. Whitespace is ignored.
func HexDecode(src []byte) ([]byte, error) {
stripped := stripHex(src)
out := make([]byte, len(stripped)/2)
_, err := hex.Decode(out, stripped)
return out, err
}
func stripHex(s []byte) []byte {
out := make([]byte, 0, len(s))
for _, b := range s {
if b == ' ' || b == '\n' || b == '\r' || b == '\t' || b == ',' || b == ':' {
continue
}
if b == '0' && len(out) == 0 {
continue
}
out = append(out, b)
}
return out
}
// Base64Encode returns the standard Base64 of src.
func Base64Encode(src []byte) ([]byte, error) {
return []byte(base64.StdEncoding.EncodeToString(src)), nil
}
// Base64Decode accepts standard or URL-safe Base64, with or without padding.
func Base64Decode(src []byte) ([]byte, error) {
s := string(bytes.TrimSpace(src))
attempts := []*base64.Encoding{
base64.StdEncoding,
base64.URLEncoding,
base64.RawStdEncoding,
base64.RawURLEncoding,
}
for _, enc := range attempts {
if b, err := enc.DecodeString(s); err == nil {
return b, nil
}
}
return nil, errors.New("not valid base64")
}
// URLEncode applies percent-encoding.
func URLEncode(src []byte) ([]byte, error) {
return []byte(url.QueryEscape(string(src))), nil
}
// URLDecode reverses percent-encoding.
func URLDecode(src []byte) ([]byte, error) {
v, err := url.QueryUnescape(string(src))
if err != nil {
return nil, err
}
return []byte(v), nil
}
// XOR returns src^key, cycling key.
func XOR(src, key []byte) ([]byte, error) {
if len(key) == 0 {
return nil, errors.New("empty key")
}
out := make([]byte, len(src))
for i := range src {
out[i] = src[i] ^ key[i%len(key)]
}
return out, nil
}
// InflateGzip, InflateZlib, InflateRaw attempt to decompress.
func InflateGzip(src []byte) ([]byte, error) {
r, err := gzip.NewReader(bytes.NewReader(src))
if err != nil {
return nil, err
}
defer r.Close()
return readLimitedInflate(r)
}
func InflateZlib(src []byte) ([]byte, error) {
r, err := zlib.NewReader(bytes.NewReader(src))
if err != nil {
return nil, err
}
defer r.Close()
return readLimitedInflate(r)
}
func InflateRaw(src []byte) ([]byte, error) {
r := flate.NewReader(bytes.NewReader(src))
defer r.Close()
return readLimitedInflate(r)
}
func readLimitedInflate(r io.Reader) ([]byte, error) {
lr := io.LimitReader(r, MaxInflateOutput+1)
out, err := io.ReadAll(lr)
if err != nil {
return nil, err
}
if len(out) > MaxInflateOutput {
return nil, errors.New("decompressed output exceeds 64 MiB limit")
}
return out, nil
}
// TryInflate attempts gzip, zlib, and raw deflate in order.
func TryInflate(src []byte) (out []byte, kind string, err error) {
if b, e := InflateGzip(src); e == nil {
return b, "gzip", nil
}
if b, e := InflateZlib(src); e == nil {
return b, "zlib", nil
}
if b, e := InflateRaw(src); e == nil {
return b, "raw", nil
}
return nil, "", errors.New("no decompression succeeded")
}
// Step is one entry in a transform pipeline.
type Step struct {
Op string // hex.encode, hex.decode, base64.encode, base64.decode,
// xor, inflate, url.encode, url.decode, caesar, rot13, atbash,
// vigenere.enc, vigenere.dec
Arg string // ASCII parameter (e.g. key for XOR/Vigenere, shift for Caesar)
}
// ApplyPipeline runs steps left-to-right. Intermediate results are
// returned for inspection.
func ApplyPipeline(input []byte, steps []Step) ([][]byte, error) {
results := make([][]byte, 0, len(steps)+1)
cur := append([]byte(nil), input...)
results = append(results, cur)
for _, s := range steps {
out, err := Apply(cur, s)
if err != nil {
return results, err
}
cur = out
results = append(results, cur)
}
return results, nil
}
// Apply one step.
func Apply(in []byte, s Step) ([]byte, error) {
switch s.Op {
case "hex.encode":
return HexEncode(in)
case "hex.decode":
return HexDecode(in)
case "base64.encode":
return Base64Encode(in)
case "base64.decode":
return Base64Decode(in)
case "url.encode":
return URLEncode(in)
case "url.decode":
return URLDecode(in)
case "xor":
key, err := parseKey(s.Arg)
if err != nil {
return nil, err
}
return XOR(in, key)
case "inflate":
switch s.Arg {
case "gzip":
return InflateGzip(in)
case "zlib":
return InflateZlib(in)
case "raw":
return InflateRaw(in)
default:
out, _, err := TryInflate(in)
return out, err
}
case "caesar":
n := 13
if s.Arg != "" {
var v int
for _, c := range s.Arg {
if c >= '0' && c <= '9' {
v = v*10 + int(c-'0')
}
}
n = v
}
return Caesar(in, n), nil
case "rot13":
return Caesar(in, 13), nil
case "atbash":
return Atbash(in), nil
case "vigenere.enc":
return Vigenere(in, []byte(s.Arg), true), nil
case "vigenere.dec":
return Vigenere(in, []byte(s.Arg), false), nil
}
return nil, errors.New("unknown op: " + s.Op)
}
// parseKey accepts either literal string (ASCII) or hex-prefixed "hex:..".
func parseKey(arg string) ([]byte, error) {
if len(arg) > 4 && arg[:4] == "hex:" {
return hex.DecodeString(arg[4:])
}
if arg == "" {
return nil, errors.New("empty key")
}
return []byte(arg), nil
}