628 lines
13 KiB
Go
628 lines
13 KiB
Go
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)
|
|
}
|