encoding/base64/base64.go
// 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(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(data []byte, compressLevel int) (string, error) {
var err error
if compressLevel != 0 {
data, err = gziputil.Compress(data, compressLevel)
if err != nil {
return "", err
}
}
return base64.StdEncoding.EncodeToString(data), nil
}
// DecodeGunzip base64 decodes a string with optional
// gzip uncompression.
func DecodeGunzip(encoded string) ([]byte, error) {
encoded = Pad(encoded)
bytes, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return bytes, err
}
bytesUnc, err := gziputil.Uncompress(bytes)
if err != nil {
return bytes, nil
}
return bytesUnc, nil
}
func IsValid(input []byte) bool {
return rxCheckMore.Match(input)
}
func IsValidString(input string) bool {
return rxCheckMore.MatchString(input)
}
func StripPadding(str string) string {
return strings.Replace(str, "=", "", -1)
}
func Pad(encoded string) string {
return encoding.Pad4(encoded, "=")
}
// EncodeGzipJSON encodes a struct that is JSON encoded.
func EncodeGzipJSON(data any, compressLevel int) (string, error) {
bytes, err := json.Marshal(data)
if err != nil {
return "", err
}
return EncodeGzip(bytes, compressLevel)
}
// DecodeGunzipJSON base64 decodes a string with optoinal
// gunzip uncompression and then unmarshals the data to a
// struct.
func DecodeGunzipJSON(encoded string, output any) error {
encoded = strings.TrimSpace(encoded)
if strings.Index(encoded, "{") == 0 || strings.Index(encoded, "[") == 0 {
return json.Unmarshal([]byte(encoded), output)
}
bytes, err := DecodeGunzip(encoded)
if err != nil {
return errorsutil.Wrap(err, "DecodeGunzipJSON.DecodeGunzip")
}
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) {
bytes, err := io.ReadAll(r)
if err != nil {
return bytes, err
}
if IsValid(bytes) {
return Decode(bytes)
}
return bytes, err
}