v3 candidate
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user