grokify/mogo

View on GitHub
encoding/base64/base64.go

Summary

Maintainability
A
0 mins
Test Coverage
// base64 supports Base64 encoding and decoding.
package base64

import (
    "encoding/base64"
    "encoding/json"
    "io"
    "regexp"
    "strings"

    "github.com/grokify/mogo/compress/gziputil"
    "github.com/grokify/mogo/encoding"
    "github.com/grokify/mogo/errors/errorsutil"
)

// Decode decodes a byte array to provide an interface like `base64/DecodeString`.
func Decode(enc []byte) ([]byte, error) {
    dst := make([]byte, base64.StdEncoding.DecodedLen(len(string(enc))))
    if n, err := base64.StdEncoding.Decode(dst, []byte(string(enc))); err != nil {
        return []byte{}, err
    } else {
        return dst[:n], nil
    }
}

/*
func DecodeOld(input []byte) ([]byte, error) {
    var output []byte
    n, err := base64.StdEncoding.Decode(output, input)
    return output[:n], err
}
*/

const (
    // RxCheckMore is from https://stackoverflow.com/a/8571649/1908967
    RxCheckMore      = `^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`
    RxCheckSimple    = `^[0-9A-Za-z/\+]*=*$`
    RxCheckNoPadding = `^[0-9A-Za-z/\+]*$`
)

var (
    rxCheckMore      = regexp.MustCompile(RxCheckMore)
    rxCheck          = regexp.MustCompile(RxCheckSimple)
    rxCheckNoPadding = regexp.MustCompile(RxCheckNoPadding)
)

// Encode with optional gzip compression. 0 = no compression.
// 9 = best compression.
func EncodeGzip(src []byte, compressLevel int) (string, error) {
    var err error
    if compressLevel != 0 {
        src, err = gziputil.CompressBytes(src, compressLevel)
        if err != nil {
            return "", err
        }
    }
    return base64.StdEncoding.EncodeToString(src), nil
}

// DecodeGunzip base64 decodes a string with optional gzip uncompression.
func DecodeGunzip(enc string) ([]byte, error) {
    enc = Pad(enc)
    if bytes, err := base64.StdEncoding.DecodeString(enc); err != nil {
        return bytes, err
    } else if bytesUnc, err := gziputil.UncompressBytes(bytes); err != nil {
        return bytes, nil
    } else {
        return bytesUnc, nil
    }
}

func IsValid(enc []byte) bool {
    return rxCheckMore.Match(enc)
}

func IsValidString(enc string) bool {
    return rxCheckMore.MatchString(enc)
}

func StripPadding(enc string) string {
    return strings.Replace(enc, "=", "", -1)
}

func Pad(enc string) string {
    return encoding.Pad4(enc, "=")
}

// EncodeGzipJSON encodes a struct that is JSON encoded.
func EncodeGzipJSON(data any, compressLevel int) (string, error) {
    if bytes, err := json.Marshal(data); err != nil {
        return "", err
    } else {
        return EncodeGzip(bytes, compressLevel)
    }
}

// DecodeGunzipJSON base64 decodes a string with optoinal
// gunzip uncompression and then unmarshals the data to a
// struct.
func DecodeGunzipJSON(enc string, output any) error {
    enc = strings.TrimSpace(enc)
    if strings.Index(enc, "{") == 0 || strings.Index(enc, "[") == 0 {
        return json.Unmarshal([]byte(enc), output)
    } else if bytes, err := DecodeGunzip(enc); err != nil {
        return errorsutil.Wrap(err, "DecodeGunzipJSON.DecodeGunzip")
    } else {
        return json.Unmarshal(bytes, output)
    }
}

// ReadAll provides an interface like `io.ReadAll`
// with optional base64 decoding. It is useful for
// decoding `*http.Response.Body`.
func ReadAll(r io.Reader) ([]byte, error) {
    if bytes, err := io.ReadAll(r); err != nil {
        return bytes, err
    } else if IsValid(bytes) {
        return Decode(bytes)
    } else {
        return bytes, err
    }
}