/* |-------------------------------------------------------------------------- | 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