obrimbasecli/essential/visible/service/helper/codec/codec.go

687 lines
15 KiB
Go

/*
|--------------------------------------------------------------------------
| Metadata
|--------------------------------------------------------------------------
|
| Name:
| - Codec
|
| Purpose:
| - Provide reusable encoding and decoding capabilities for framework
| and software services.
|
| Guideline:
| - Use ObrimCodec() as the public entry point for all codec operations.
| - Always provide a valid operation type and configuration map.
| - Use supported formats only: base8, base10, base16, base32,
| base64, and sqids.
| - Validate all inputs before processing encoding or decoding requests.
| - Use charset and salt values consistently between encode and decode
| operations.
| - Use case configuration for non-sqids formats.
| - Use length configuration for sqids format.
| - Treat empty sqids inputs and outputs as operation failures.
| - Consume responses through the standardized status, code, and
| payload structure.
|
| Example:
| - ObrimCodec("encode", config)
| - ObrimCodec("decode", config)
| - ObrimCodec("encode", map[string]any{
| "format": "base64",
| "data": "example",
| "charset": "",
| "salt": "",
| "case": "lower",
| })
| - ObrimCodec("decode", map[string]any{
| "format": "sqids",
| "data": "abc123",
| "charset": "abcdefghijklmnopqrstuvwxyz",
| "salt": "",
| "length": 8,
| })
|
|--------------------------------------------------------------------------
*/
/*
|--------------------------------------------------------------------------
| Credit
|--------------------------------------------------------------------------
|
| Contributor:
| - Rajon Ahmed
| - Scionite
|
|--------------------------------------------------------------------------
*/
package codec
import (
"encoding/base32"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"math/big"
"strconv"
"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"
ObrimCodecCodeSuccessEncode = "SUCCESS_ENCODE"
ObrimCodecCodeSuccessDecode = "SUCCESS_DECODE"
ObrimCodecCodeFailedValidation = "FAILED_VALIDATION"
ObrimCodecCodeFailedUnsupportedType = "FAILED_UNSUPPORTED_TYPE"
ObrimCodecCodeFailedUnsupportedFormat = "FAILED_UNSUPPORTED_FORMAT"
ObrimCodecCodeFailedMissingConfiguration = "FAILED_MISSING_CONFIGURATION"
ObrimCodecCodeFailedInvalidConfiguration = "FAILED_INVALID_CONFIGURATION"
ObrimCodecCodeFailedOperation = "FAILED_OPERATION"
)
// ObrimCodecResponse defines the standardized utility response.
type ObrimCodecResponse struct {
Status bool `json:"status"`
Code string `json:"code"`
Payload map[string]any `json:"payload"`
}
// obrimCodecConfiguration stores validated codec configuration.
type obrimCodecConfiguration struct {
Format string
Data any
Charset string
Salt string
Case string
Length int
}
// ObrimCodec validates configuration, dispatches codec operations, and returns structured results.
func ObrimCodec(codecType string, config map[string]any) map[string]any {
validatedConfig, code := obrimCodecValidation(codecType, config)
if code != "" {
return obrimCodecBuildResponse(false, code, nil)
}
value, err := obrimCodecDispatch(codecType, validatedConfig)
if err != nil {
return obrimCodecBuildResponse(
false,
ObrimCodecCodeFailedOperation,
nil,
)
}
var successCode string
switch codecType {
case obrimCodecTypeEncode:
successCode = ObrimCodecCodeSuccessEncode
case obrimCodecTypeDecode:
successCode = ObrimCodecCodeSuccessDecode
}
return obrimCodecBuildResponse(
true,
successCode,
map[string]any{
"operation": codecType,
"format": validatedConfig.Format,
"value": value,
},
)
}
// obrimCodecValidation validates required parameters, supported operations, formats, and parameter types.
func obrimCodecValidation(
codecType string,
config map[string]any,
) (*obrimCodecConfiguration, string) {
if config == nil {
return nil, ObrimCodecCodeFailedMissingConfiguration
}
switch codecType {
case obrimCodecTypeEncode:
case obrimCodecTypeDecode:
default:
return nil, ObrimCodecCodeFailedUnsupportedType
}
formatValue, exists := config["format"]
if !exists {
return nil, ObrimCodecCodeFailedMissingConfiguration
}
format, ok := formatValue.(string)
if !ok || strings.TrimSpace(format) == "" {
return nil, ObrimCodecCodeFailedInvalidConfiguration
}
switch format {
case obrimCodecFormatBase8:
case obrimCodecFormatBase10:
case obrimCodecFormatBase16:
case obrimCodecFormatBase32:
case obrimCodecFormatBase64:
case obrimCodecFormatSqids:
default:
return nil, ObrimCodecCodeFailedUnsupportedFormat
}
dataValue, exists := config["data"]
if !exists {
return nil, ObrimCodecCodeFailedMissingConfiguration
}
switch dataValue.(type) {
case string:
case []byte:
default:
return nil, ObrimCodecCodeFailedInvalidConfiguration
}
charsetValue, exists := config["charset"]
if !exists {
return nil, ObrimCodecCodeFailedMissingConfiguration
}
charset, ok := charsetValue.(string)
if !ok {
return nil, ObrimCodecCodeFailedInvalidConfiguration
}
saltValue, exists := config["salt"]
if !exists {
return nil, ObrimCodecCodeFailedMissingConfiguration
}
salt, ok := saltValue.(string)
if !ok {
return nil, ObrimCodecCodeFailedInvalidConfiguration
}
var validatedConfig = &obrimCodecConfiguration{
Format: format,
Data: dataValue,
Charset: charset,
Salt: salt,
}
if format == obrimCodecFormatSqids {
lengthValue, exists := config["length"]
if !exists {
return nil, ObrimCodecCodeFailedMissingConfiguration
}
switch value := lengthValue.(type) {
case int:
validatedConfig.Length = value
case int8:
validatedConfig.Length = int(value)
case int16:
validatedConfig.Length = int(value)
case int32:
validatedConfig.Length = int(value)
case int64:
validatedConfig.Length = int(value)
case float64:
validatedConfig.Length = int(value)
default:
return nil, ObrimCodecCodeFailedInvalidConfiguration
}
if validatedConfig.Length < 0 {
return nil, ObrimCodecCodeFailedInvalidConfiguration
}
} else {
caseValue, exists := config["case"]
if !exists {
return nil, ObrimCodecCodeFailedMissingConfiguration
}
caseRule, ok := caseValue.(string)
if !ok {
return nil, ObrimCodecCodeFailedInvalidConfiguration
}
switch caseRule {
case obrimCodecCaseLower:
case obrimCodecCaseUpper:
default:
return nil, ObrimCodecCodeFailedInvalidConfiguration
}
validatedConfig.Case = caseRule
}
return validatedConfig, ""
}
// obrimCodecDispatch routes validated requests to the appropriate codec implementation.
func obrimCodecDispatch(
codecType string,
config *obrimCodecConfiguration,
) (string, error) {
switch codecType {
case obrimCodecTypeEncode:
switch config.Format {
case obrimCodecFormatBase8:
return obrimCodecEncodeBase8(config)
case obrimCodecFormatBase10:
return obrimCodecEncodeBase10(config)
case obrimCodecFormatBase16:
return obrimCodecEncodeBase16(config)
case obrimCodecFormatBase32:
return obrimCodecEncodeBase32(config)
case obrimCodecFormatBase64:
return obrimCodecEncodeBase64(config)
case obrimCodecFormatSqids:
return obrimCodecEncodeSqids(config)
}
case obrimCodecTypeDecode:
switch config.Format {
case obrimCodecFormatBase8:
return obrimCodecDecodeBase8(config)
case obrimCodecFormatBase10:
return obrimCodecDecodeBase10(config)
case obrimCodecFormatBase16:
return obrimCodecDecodeBase16(config)
case obrimCodecFormatBase32:
return obrimCodecDecodeBase32(config)
case obrimCodecFormatBase64:
return obrimCodecDecodeBase64(config)
case obrimCodecFormatSqids:
return obrimCodecDecodeSqids(config)
}
}
return "", obrimCodecError("unsupported dispatch")
}
// obrimCodecBuildResponse builds a standard response.
func obrimCodecBuildResponse(
status bool,
code string,
payload map[string]any,
) map[string]any {
return map[string]any{
"status": status,
"code": code,
"payload": payload,
}
}
// obrimCodecNormalizeInput converts supported input types into string form.
func obrimCodecNormalizeInput(data any) string {
switch value := data.(type) {
case string:
return value
case []byte:
return string(value)
default:
return ""
}
}
// obrimCodecApplyTransform applies charset and salt transformations.
func obrimCodecApplyTransform(
value string,
charset string,
salt string,
) string {
return charset + salt + value
}
// obrimCodecReverseTransform reverses charset and salt transformations.
func obrimCodecReverseTransform(
value string,
charset string,
salt string,
) string {
var prefix = charset + salt
return strings.TrimPrefix(value, prefix)
}
// obrimCodecApplyCase applies output casing.
func obrimCodecApplyCase(
value string,
caseRule string,
) string {
switch caseRule {
case obrimCodecCaseUpper:
return strings.ToUpper(value)
default:
return strings.ToLower(value)
}
}
// obrimCodecEncodeBase8 encodes data using Base8 encoding.
func obrimCodecEncodeBase8(
config *obrimCodecConfiguration,
) (string, error) {
var input = obrimCodecApplyTransform(
obrimCodecNormalizeInput(config.Data),
config.Charset,
config.Salt,
)
var builder strings.Builder
for _, value := range []byte(input) {
builder.WriteString(fmt.Sprintf("%03o", value))
}
return obrimCodecApplyCase(
builder.String(),
config.Case,
), nil
}
// obrimCodecDecodeBase8 decodes Base8 encoded data.
func obrimCodecDecodeBase8(
config *obrimCodecConfiguration,
) (string, error) {
var input = strings.ToLower(
obrimCodecNormalizeInput(config.Data),
)
if len(input)%3 != 0 {
return "", obrimCodecError("invalid base8 data")
}
var output []byte
for index := 0; index < len(input); index += 3 {
value, err := strconv.ParseUint(
input[index:index+3],
8,
8,
)
if err != nil {
return "", err
}
output = append(output, byte(value))
}
return obrimCodecReverseTransform(
string(output),
config.Charset,
config.Salt,
), nil
}
// obrimCodecEncodeBase10 encodes data using Base10 encoding.
func obrimCodecEncodeBase10(
config *obrimCodecConfiguration,
) (string, error) {
var input = obrimCodecApplyTransform(
obrimCodecNormalizeInput(config.Data),
config.Charset,
config.Salt,
)
var value = new(big.Int).SetBytes([]byte(input))
return obrimCodecApplyCase(
value.Text(10),
config.Case,
), nil
}
// obrimCodecDecodeBase10 decodes Base10 encoded data.
func obrimCodecDecodeBase10(
config *obrimCodecConfiguration,
) (string, error) {
var value = new(big.Int)
if _, ok := value.SetString(
obrimCodecNormalizeInput(config.Data),
10,
); !ok {
return "", obrimCodecError("invalid base10 data")
}
return obrimCodecReverseTransform(
string(value.Bytes()),
config.Charset,
config.Salt,
), nil
}
// obrimCodecEncodeBase16 encodes data using Base16 encoding.
func obrimCodecEncodeBase16(
config *obrimCodecConfiguration,
) (string, error) {
var input = obrimCodecApplyTransform(
obrimCodecNormalizeInput(config.Data),
config.Charset,
config.Salt,
)
var output = hex.EncodeToString([]byte(input))
return obrimCodecApplyCase(
output,
config.Case,
), nil
}
// obrimCodecDecodeBase16 decodes Base16 encoded data.
func obrimCodecDecodeBase16(
config *obrimCodecConfiguration,
) (string, error) {
var output, err = hex.DecodeString(
obrimCodecNormalizeInput(config.Data),
)
if err != nil {
return "", err
}
return obrimCodecReverseTransform(
string(output),
config.Charset,
config.Salt,
), nil
}
// obrimCodecEncodeBase32 encodes data using Base32 encoding.
func obrimCodecEncodeBase32(
config *obrimCodecConfiguration,
) (string, error) {
var input = obrimCodecApplyTransform(
obrimCodecNormalizeInput(config.Data),
config.Charset,
config.Salt,
)
var output = base32.StdEncoding.EncodeToString(
[]byte(input),
)
return obrimCodecApplyCase(
output,
config.Case,
), nil
}
// obrimCodecDecodeBase32 decodes Base32 encoded data.
func obrimCodecDecodeBase32(
config *obrimCodecConfiguration,
) (string, error) {
var output, err = base32.StdEncoding.DecodeString(
strings.ToUpper(
obrimCodecNormalizeInput(config.Data),
),
)
if err != nil {
return "", err
}
return obrimCodecReverseTransform(
string(output),
config.Charset,
config.Salt,
), nil
}
// obrimCodecEncodeBase64 encodes data using Base64 encoding.
func obrimCodecEncodeBase64(
config *obrimCodecConfiguration,
) (string, error) {
var input = obrimCodecApplyTransform(
obrimCodecNormalizeInput(config.Data),
config.Charset,
config.Salt,
)
var output = base64.StdEncoding.EncodeToString(
[]byte(input),
)
return obrimCodecApplyCase(
output,
config.Case,
), nil
}
// obrimCodecDecodeBase64 decodes Base64 encoded data.
func obrimCodecDecodeBase64(
config *obrimCodecConfiguration,
) (string, error) {
var output, err = base64.StdEncoding.DecodeString(
obrimCodecNormalizeInput(config.Data),
)
if err != nil {
return "", err
}
return obrimCodecReverseTransform(
string(output),
config.Charset,
config.Salt,
), nil
}
// obrimCodecEncodeSqids encodes data using Sqids encoding with minimum length enforcement.
func obrimCodecEncodeSqids(
config *obrimCodecConfiguration,
) (string, error) {
var input = obrimCodecApplyTransform(
obrimCodecNormalizeInput(config.Data),
config.Charset,
config.Salt,
)
var numbers = make([]uint64, 0, len(input))
for _, value := range []byte(input) {
numbers = append(numbers, uint64(value))
}
if len(numbers) == 0 {
return "", obrimCodecError("empty sqids input")
}
var sqidsInstance, err = sqids.New(
sqids.Options{
Alphabet: config.Charset,
MinLength: uint8(config.Length),
},
)
if err != nil {
return "", err
}
return sqidsInstance.Encode(numbers)
}
// obrimCodecDecodeSqids decodes Sqids encoded data using deterministic length validation.
func obrimCodecDecodeSqids(
config *obrimCodecConfiguration,
) (string, error) {
var sqidsInstance, err = sqids.New(
sqids.Options{
Alphabet: config.Charset,
MinLength: uint8(config.Length),
},
)
if err != nil {
return "", err
}
var numbers = sqidsInstance.Decode(
obrimCodecNormalizeInput(config.Data),
)
if len(numbers) == 0 {
return "", obrimCodecError(
"invalid or empty sqids data",
)
}
var output = make([]byte, 0, len(numbers))
for _, value := range numbers {
if value > 255 {
return "", obrimCodecError(
"decoded sqids value exceeds byte range",
)
}
output = append(output, byte(value))
}
return obrimCodecReverseTransform(
string(output),
config.Charset,
config.Salt,
), nil
}
// obrimCodecError formats internal errors.
func obrimCodecError(message string) error {
return errors.New(message)
}