690 lines
16 KiB
Go
690 lines
16 KiB
Go
/*
|
|
|--------------------------------------------------------------------------
|
|
| Description
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Name:
|
|
| - Codec
|
|
|
|
|
| Purpose:
|
|
| - Provide unified encoding and decoding capabilities using Base8,
|
|
| Base10, Base16, Base32, Base64, and Sqids with centralized
|
|
| validation, operation dispatching, transformation handling,
|
|
| and standardized result generation.
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Instruction
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Guideline:
|
|
| - Use the utility to encode input data using the selected codec format.
|
|
| - Use the utility to decode input data using the selected codec format.
|
|
| - Configure the codec format using base8, base10, base16, base32,
|
|
| base64, or sqids.
|
|
| - Provide input data as a string or []byte.
|
|
| - Configure charset and salt transformations when required.
|
|
| - Configure lower or upper output casing when required.
|
|
| - Configure minimum output length for Sqids encoding.
|
|
| - Configure length for deterministic Sqids decoding.
|
|
|
|
|
| Example:
|
|
| - ObrimCodec("encode", map[string]any{
|
|
| "format": "base64",
|
|
| "data": "hello",
|
|
| })
|
|
|
|
|
| - ObrimCodec("decode", map[string]any{
|
|
| "format": "base64",
|
|
| "data": "aGVsbG8=",
|
|
| })
|
|
|
|
|
| - ObrimCodec("encode", map[string]any{
|
|
| "format": "sqids",
|
|
| "data": "12345",
|
|
| "charset": "abcdefghijklmnopqrstuvwxyz",
|
|
| "salt": "example",
|
|
| "length": 8,
|
|
| })
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Credit
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Contributor:
|
|
| - Rajon Ahmed
|
|
| - Blockonite
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
package codec
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base32"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"github.com/sqids/sqids-go"
|
|
)
|
|
|
|
const (
|
|
obrimCodecTypeEncode = "encode"
|
|
obrimCodecTypeDecode = "decode"
|
|
|
|
obrimCodecFormatBase8 = "base8"
|
|
obrimCodecFormatBase10 = "base10"
|
|
obrimCodecFormatBase16 = "base16"
|
|
obrimCodecFormatBase32 = "base32"
|
|
obrimCodecFormatBase64 = "base64"
|
|
obrimCodecFormatSqids = "sqids"
|
|
|
|
obrimCodecCaseLower = "lower"
|
|
obrimCodecCaseUpper = "upper"
|
|
|
|
obrimCodecSuccessEncoded = "SUCCESS_ENCODED"
|
|
obrimCodecSuccessDecoded = "SUCCESS_DECODED"
|
|
|
|
obrimCodecFailureInvalidType = "FAILURE_INVALID_TYPE"
|
|
obrimCodecFailureInvalidConfig = "FAILURE_INVALID_CONFIG"
|
|
obrimCodecFailureInvalidFormat = "FAILURE_INVALID_FORMAT"
|
|
obrimCodecFailureInvalidData = "FAILURE_INVALID_DATA"
|
|
obrimCodecFailureInvalidCharset = "FAILURE_INVALID_CHARSET"
|
|
obrimCodecFailureInvalidSalt = "FAILURE_INVALID_SALT"
|
|
obrimCodecFailureInvalidCase = "FAILURE_INVALID_CASE"
|
|
obrimCodecFailureInvalidLength = "FAILURE_INVALID_LENGTH"
|
|
obrimCodecFailureEncoding = "FAILURE_ENCODING"
|
|
obrimCodecFailureDecoding = "FAILURE_DECODING"
|
|
obrimCodecFailureTransformation = "FAILURE_TRANSFORMATION"
|
|
obrimCodecFailureTransformationReversal = "FAILURE_TRANSFORMATION_REVERSAL"
|
|
obrimCodecFailureUnsupportedData = "FAILURE_UNSUPPORTED_DATA"
|
|
obrimCodecFailureSqids = "FAILURE_SQIDS"
|
|
)
|
|
|
|
type obrimCodecConfig struct {
|
|
format string
|
|
data []byte
|
|
charset string
|
|
salt string
|
|
caseRule string
|
|
length int
|
|
}
|
|
|
|
type obrimCodecResult struct {
|
|
operation string
|
|
format string
|
|
value string
|
|
}
|
|
|
|
type obrimCodecOutput struct {
|
|
Status bool `json:"status"`
|
|
Code string `json:"code"`
|
|
Payload *obrimCodecResult `json:"payload"`
|
|
}
|
|
|
|
func ObrimCodec(typ string, config map[string]any) map[string]any {
|
|
normalized, code := obrimCodecValidateInput(typ, config)
|
|
if code != "" {
|
|
return obrimCodecBuildOutput(false, code, nil)
|
|
}
|
|
|
|
value, code := obrimCodecRouteRequest(typ, normalized)
|
|
if code != "" {
|
|
return obrimCodecBuildOutput(false, code, nil)
|
|
}
|
|
|
|
if typ == obrimCodecTypeEncode {
|
|
return obrimCodecBuildOutput(
|
|
true,
|
|
obrimCodecSuccessEncoded,
|
|
&obrimCodecResult{
|
|
operation: typ,
|
|
format: normalized.format,
|
|
value: value,
|
|
},
|
|
)
|
|
}
|
|
|
|
return obrimCodecBuildOutput(
|
|
true,
|
|
obrimCodecSuccessDecoded,
|
|
&obrimCodecResult{
|
|
operation: typ,
|
|
format: normalized.format,
|
|
value: value,
|
|
},
|
|
)
|
|
}
|
|
|
|
func obrimCodecValidateInput(typ string, config map[string]any) (obrimCodecConfig, string) {
|
|
var result obrimCodecConfig
|
|
|
|
switch typ {
|
|
case obrimCodecTypeEncode, obrimCodecTypeDecode:
|
|
default:
|
|
return result, obrimCodecFailureInvalidType
|
|
}
|
|
|
|
formatValue, exists := config["format"]
|
|
if !exists {
|
|
return result, obrimCodecFailureInvalidConfig
|
|
}
|
|
|
|
format, ok := formatValue.(string)
|
|
if !ok || format == "" {
|
|
return result, obrimCodecFailureInvalidFormat
|
|
}
|
|
|
|
switch format {
|
|
case obrimCodecFormatBase8,
|
|
obrimCodecFormatBase10,
|
|
obrimCodecFormatBase16,
|
|
obrimCodecFormatBase32,
|
|
obrimCodecFormatBase64,
|
|
obrimCodecFormatSqids:
|
|
default:
|
|
return result, obrimCodecFailureInvalidFormat
|
|
}
|
|
|
|
dataValue, exists := config["data"]
|
|
if !exists {
|
|
return result, obrimCodecFailureInvalidData
|
|
}
|
|
|
|
switch value := dataValue.(type) {
|
|
case string:
|
|
if value == "" {
|
|
return result, obrimCodecFailureInvalidData
|
|
}
|
|
result.data = []byte(value)
|
|
case []byte:
|
|
if len(value) == 0 {
|
|
return result, obrimCodecFailureInvalidData
|
|
}
|
|
result.data = append([]byte(nil), value...)
|
|
default:
|
|
return result, obrimCodecFailureUnsupportedData
|
|
}
|
|
|
|
result.format = format
|
|
|
|
if charsetValue, exists := config["charset"]; exists {
|
|
charset, ok := charsetValue.(string)
|
|
if !ok || charset == "" {
|
|
return result, obrimCodecFailureInvalidCharset
|
|
}
|
|
result.charset = charset
|
|
}
|
|
|
|
if saltValue, exists := config["salt"]; exists {
|
|
salt, ok := saltValue.(string)
|
|
if !ok || salt == "" {
|
|
return result, obrimCodecFailureInvalidSalt
|
|
}
|
|
result.salt = salt
|
|
}
|
|
|
|
if caseValue, exists := config["case"]; exists {
|
|
caseRule, ok := caseValue.(string)
|
|
if !ok {
|
|
return result, obrimCodecFailureInvalidCase
|
|
}
|
|
|
|
switch caseRule {
|
|
case obrimCodecCaseLower, obrimCodecCaseUpper:
|
|
result.caseRule = caseRule
|
|
default:
|
|
return result, obrimCodecFailureInvalidCase
|
|
}
|
|
}
|
|
|
|
if lengthValue, exists := config["length"]; exists {
|
|
switch length := lengthValue.(type) {
|
|
case int:
|
|
result.length = length
|
|
case int8:
|
|
result.length = int(length)
|
|
case int16:
|
|
result.length = int(length)
|
|
case int32:
|
|
result.length = int(length)
|
|
case int64:
|
|
result.length = int(length)
|
|
case uint:
|
|
result.length = int(length)
|
|
case uint8:
|
|
result.length = int(length)
|
|
case uint16:
|
|
result.length = int(length)
|
|
case uint32:
|
|
result.length = int(length)
|
|
case uint64:
|
|
if uint64(int(length)) != length {
|
|
return result, obrimCodecFailureInvalidLength
|
|
}
|
|
result.length = int(length)
|
|
case float64:
|
|
if length != float64(int(length)) {
|
|
return result, obrimCodecFailureInvalidLength
|
|
}
|
|
result.length = int(length)
|
|
default:
|
|
return result, obrimCodecFailureInvalidLength
|
|
}
|
|
|
|
if result.length < 0 {
|
|
return result, obrimCodecFailureInvalidLength
|
|
}
|
|
}
|
|
|
|
if format == obrimCodecFormatSqids && result.length == 0 {
|
|
result.length = 0
|
|
}
|
|
|
|
return result, ""
|
|
}
|
|
|
|
func obrimCodecRouteRequest(typ string, config obrimCodecConfig) (string, string) {
|
|
switch typ {
|
|
case obrimCodecTypeEncode:
|
|
return obrimCodecEncode(config)
|
|
case obrimCodecTypeDecode:
|
|
return obrimCodecDecode(config)
|
|
default:
|
|
return "", obrimCodecFailureInvalidType
|
|
}
|
|
}
|
|
|
|
func obrimCodecBuildOutput(status bool, code string, payload *obrimCodecResult) map[string]any {
|
|
if !status {
|
|
return map[string]any{
|
|
"status": false,
|
|
"code": code,
|
|
"payload": nil,
|
|
}
|
|
}
|
|
|
|
return map[string]any{
|
|
"status": true,
|
|
"code": code,
|
|
"payload": map[string]any{
|
|
"operation": payload.operation,
|
|
"format": payload.format,
|
|
"value": payload.value,
|
|
},
|
|
}
|
|
}
|
|
|
|
func obrimCodecEncode(config obrimCodecConfig) (string, string) {
|
|
data, code := obrimCodecApplyEncodeTransformations(config.data, config)
|
|
if code != "" {
|
|
return "", code
|
|
}
|
|
|
|
var (
|
|
value string
|
|
err error
|
|
)
|
|
|
|
switch config.format {
|
|
case obrimCodecFormatBase8:
|
|
value, err = obrimCodecEncodeBase8(data)
|
|
case obrimCodecFormatBase10:
|
|
value, err = obrimCodecEncodeBase10(data)
|
|
case obrimCodecFormatBase16:
|
|
value, err = obrimCodecEncodeBase16(data)
|
|
case obrimCodecFormatBase32:
|
|
value, err = obrimCodecEncodeBase32(data)
|
|
case obrimCodecFormatBase64:
|
|
value, err = obrimCodecEncodeBase64(data)
|
|
case obrimCodecFormatSqids:
|
|
value, err = obrimCodecEncodeSqids(data, config)
|
|
default:
|
|
return "", obrimCodecFailureInvalidFormat
|
|
}
|
|
|
|
if err != nil {
|
|
if config.format == obrimCodecFormatSqids {
|
|
return "", obrimCodecFailureSqids
|
|
}
|
|
return "", obrimCodecFailureEncoding
|
|
}
|
|
|
|
value = obrimCodecApplyCase(value, config.caseRule)
|
|
return value, ""
|
|
}
|
|
|
|
func obrimCodecDecode(config obrimCodecConfig) (string, string) {
|
|
data := append([]byte(nil), config.data...)
|
|
|
|
var err error
|
|
var value []byte
|
|
|
|
switch config.format {
|
|
case obrimCodecFormatBase8:
|
|
value, err = obrimCodecDecodeBase8(string(data))
|
|
case obrimCodecFormatBase10:
|
|
value, err = obrimCodecDecodeBase10(string(data))
|
|
case obrimCodecFormatBase16:
|
|
value, err = obrimCodecDecodeBase16(string(data))
|
|
case obrimCodecFormatBase32:
|
|
value, err = obrimCodecDecodeBase32(string(data))
|
|
case obrimCodecFormatBase64:
|
|
value, err = obrimCodecDecodeBase64(string(data))
|
|
case obrimCodecFormatSqids:
|
|
value, err = obrimCodecDecodeSqids(string(data), config)
|
|
default:
|
|
return "", obrimCodecFailureInvalidFormat
|
|
}
|
|
|
|
if err != nil {
|
|
if config.format == obrimCodecFormatSqids {
|
|
return "", obrimCodecFailureSqids
|
|
}
|
|
return "", obrimCodecFailureDecoding
|
|
}
|
|
|
|
value, err = obrimCodecApplyDecodeTransformations(value, config)
|
|
if err != nil {
|
|
return "", obrimCodecFailureTransformationReversal
|
|
}
|
|
|
|
return string(value), ""
|
|
}
|
|
|
|
func obrimCodecEncodeBase8(data []byte) (string, error) {
|
|
if len(data) == 0 {
|
|
return "", fmt.Errorf("empty data")
|
|
}
|
|
|
|
number := new(big.Int).SetBytes(data)
|
|
return number.Text(8), nil
|
|
}
|
|
|
|
func obrimCodecEncodeBase10(data []byte) (string, error) {
|
|
if len(data) == 0 {
|
|
return "", fmt.Errorf("empty data")
|
|
}
|
|
|
|
number := new(big.Int).SetBytes(data)
|
|
return number.Text(10), nil
|
|
}
|
|
|
|
func obrimCodecEncodeBase16(data []byte) (string, error) {
|
|
if len(data) == 0 {
|
|
return "", fmt.Errorf("empty data")
|
|
}
|
|
|
|
return hex.EncodeToString(data), nil
|
|
}
|
|
|
|
func obrimCodecEncodeBase32(data []byte) (string, error) {
|
|
if len(data) == 0 {
|
|
return "", fmt.Errorf("empty data")
|
|
}
|
|
|
|
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(data), nil
|
|
}
|
|
|
|
func obrimCodecEncodeBase64(data []byte) (string, error) {
|
|
if len(data) == 0 {
|
|
return "", fmt.Errorf("empty data")
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(data), nil
|
|
}
|
|
|
|
func obrimCodecEncodeSqids(data []byte, config obrimCodecConfig) (string, error) {
|
|
if len(data) == 0 {
|
|
return "", fmt.Errorf("empty data")
|
|
}
|
|
|
|
number := new(big.Int).SetBytes(data)
|
|
|
|
if !number.IsUint64() {
|
|
return "", fmt.Errorf("sqids input exceeds uint64")
|
|
}
|
|
|
|
options := sqids.Options{}
|
|
|
|
if config.charset != "" {
|
|
options.Alphabet = config.charset
|
|
}
|
|
|
|
if config.length > 0 {
|
|
options.MinLength = config.length
|
|
}
|
|
|
|
encoder, err := sqids.New(options)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return encoder.Encode([]uint64{number.Uint64()}), nil
|
|
}
|
|
|
|
func obrimCodecDecodeBase8(data string) ([]byte, error) {
|
|
data = strings.TrimSpace(data)
|
|
|
|
if data == "" {
|
|
return nil, fmt.Errorf("empty data")
|
|
}
|
|
|
|
number := new(big.Int)
|
|
if _, ok := number.SetString(data, 8); !ok {
|
|
return nil, fmt.Errorf("invalid base8 value")
|
|
}
|
|
|
|
return obrimCodecBigIntBytes(number), nil
|
|
}
|
|
|
|
func obrimCodecDecodeBase10(data string) ([]byte, error) {
|
|
data = strings.TrimSpace(data)
|
|
|
|
if data == "" {
|
|
return nil, fmt.Errorf("empty data")
|
|
}
|
|
|
|
number := new(big.Int)
|
|
if _, ok := number.SetString(data, 10); !ok {
|
|
return nil, fmt.Errorf("invalid base10 value")
|
|
}
|
|
|
|
return obrimCodecBigIntBytes(number), nil
|
|
}
|
|
|
|
func obrimCodecDecodeBase16(data string) ([]byte, error) {
|
|
data = strings.TrimSpace(data)
|
|
|
|
if data == "" {
|
|
return nil, fmt.Errorf("empty data")
|
|
}
|
|
|
|
if len(data)%2 != 0 {
|
|
data = "0" + data
|
|
}
|
|
|
|
return hex.DecodeString(data)
|
|
}
|
|
|
|
func obrimCodecDecodeBase32(data string) ([]byte, error) {
|
|
data = strings.TrimSpace(data)
|
|
|
|
if data == "" {
|
|
return nil, fmt.Errorf("empty data")
|
|
}
|
|
|
|
padding := len(data) % 8
|
|
if padding != 0 {
|
|
data += strings.Repeat("=", 8-padding)
|
|
}
|
|
|
|
return base32.StdEncoding.DecodeString(data)
|
|
}
|
|
|
|
func obrimCodecDecodeBase64(data string) ([]byte, error) {
|
|
data = strings.TrimSpace(data)
|
|
|
|
if data == "" {
|
|
return nil, fmt.Errorf("empty data")
|
|
}
|
|
|
|
return base64.StdEncoding.DecodeString(data)
|
|
}
|
|
|
|
func obrimCodecDecodeSqids(data string, config obrimCodecConfig) ([]byte, error) {
|
|
if data == "" {
|
|
return nil, fmt.Errorf("empty data")
|
|
}
|
|
|
|
options := sqids.Options{}
|
|
|
|
if config.charset != "" {
|
|
options.Alphabet = config.charset
|
|
}
|
|
|
|
if config.length > 0 {
|
|
options.MinLength = config.length
|
|
}
|
|
|
|
encoder, err := sqids.New(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
numbers := encoder.Decode(data)
|
|
if len(numbers) != 1 {
|
|
return nil, fmt.Errorf("invalid sqids value")
|
|
}
|
|
|
|
number := new(big.Int).SetUint64(numbers[0])
|
|
return obrimCodecBigIntBytes(number), nil
|
|
}
|
|
|
|
func obrimCodecBigIntBytes(number *big.Int) []byte {
|
|
if number.Sign() == 0 {
|
|
return []byte{0}
|
|
}
|
|
|
|
return number.Bytes()
|
|
}
|
|
|
|
func obrimCodecApplyEncodeTransformations(data []byte, config obrimCodecConfig) ([]byte, string) {
|
|
result := append([]byte(nil), data...)
|
|
|
|
if config.salt != "" {
|
|
result = obrimCodecApplySalt(result, []byte(config.salt))
|
|
}
|
|
|
|
if config.charset != "" && config.format != obrimCodecFormatSqids {
|
|
transformed, err := obrimCodecApplyCharsetEncode(result, config.charset)
|
|
if err != nil {
|
|
return nil, obrimCodecFailureTransformation
|
|
}
|
|
result = transformed
|
|
}
|
|
|
|
return result, ""
|
|
}
|
|
|
|
func obrimCodecApplyDecodeTransformations(data []byte, config obrimCodecConfig) ([]byte, error) {
|
|
result := append([]byte(nil), data...)
|
|
|
|
if config.charset != "" && config.format != obrimCodecFormatSqids {
|
|
transformed, err := obrimCodecApplyCharsetDecode(result, config.charset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = transformed
|
|
}
|
|
|
|
if config.salt != "" {
|
|
result = obrimCodecApplySalt(result, []byte(config.salt))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func obrimCodecApplyCharsetEncode(data []byte, charset string) ([]byte, error) {
|
|
charsetBytes := []byte(charset)
|
|
if len(charsetBytes) < 2 {
|
|
return nil, fmt.Errorf("charset must contain at least two characters")
|
|
}
|
|
|
|
result := make([]byte, len(data))
|
|
|
|
for index, value := range data {
|
|
result[index] = charsetBytes[int(value)%len(charsetBytes)]
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func obrimCodecApplyCharsetDecode(data []byte, charset string) ([]byte, error) {
|
|
charsetBytes := []byte(charset)
|
|
if len(charsetBytes) < 2 {
|
|
return nil, fmt.Errorf("charset must contain at least two characters")
|
|
}
|
|
|
|
reverse := make(map[byte]byte, len(charsetBytes))
|
|
|
|
for index, value := range charsetBytes {
|
|
if _, exists := reverse[value]; exists {
|
|
return nil, fmt.Errorf("charset contains duplicate characters")
|
|
}
|
|
reverse[value] = byte(index)
|
|
}
|
|
|
|
result := make([]byte, len(data))
|
|
|
|
for index, value := range data {
|
|
decoded, exists := reverse[value]
|
|
if !exists {
|
|
return nil, fmt.Errorf("character not present in charset")
|
|
}
|
|
result[index] = decoded
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func obrimCodecApplySalt(data []byte, salt []byte) []byte {
|
|
if len(salt) == 0 {
|
|
return data
|
|
}
|
|
|
|
result := make([]byte, len(data))
|
|
|
|
for index, value := range data {
|
|
result[index] = value ^ salt[index%len(salt)]
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func obrimCodecApplyCase(value string, caseRule string) string {
|
|
switch caseRule {
|
|
case obrimCodecCaseLower:
|
|
return strings.ToLower(value)
|
|
case obrimCodecCaseUpper:
|
|
return strings.ToUpper(value)
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
var _ = bytes.Compare
|
|
var _ = json.Valid
|