grpc-spec-1.0.0: src/Network/GRPC/Spec/Serialization/Base64.hs
-- | gRPC-style base64-encoding
--
-- The gRPC specification mandates standard Base64-encoding for binary headers
-- <https://datatracker.ietf.org/doc/html/rfc4648#section-4>, /but/ without
-- padding.
module Network.GRPC.Spec.Serialization.Base64 (
encodeBase64
, decodeBase64
) where
import Control.Monad
import Data.ByteString qualified as BS.Strict
import Data.ByteString qualified as Strict (ByteString)
import Data.ByteString.Base64 qualified as BS.Strict.B64
import Data.ByteString.Char8 qualified as BS.Strict.Char8
{-------------------------------------------------------------------------------
Top-level
-------------------------------------------------------------------------------}
encodeBase64 :: Strict.ByteString -> Strict.ByteString
encodeBase64 = removePadding . BS.Strict.B64.encode
decodeBase64 :: Strict.ByteString -> Either String Strict.ByteString
decodeBase64 = BS.Strict.B64.decode <=< addPadding
{-------------------------------------------------------------------------------
Internal: Adding and removing padding
In Base64 encoding, every group of three bytes in the input is represented as
4 bytes in the output. We therefore have three possibilities:
* The input has size @3n@. No padding bytes are added.
* The input has size @3n + 1@. Two padding byte are added.
* The input has size @3n + 2@. One padding byte is added.
Standard base64 encoding (including padding) therefore /always/ has size @4m@.
-------------------------------------------------------------------------------}
removePadding :: Strict.ByteString -> Strict.ByteString
removePadding bs
-- Empty bytestring
--
-- If the bytestring is not null, it must have at least 4 bytes, justifying
-- the calls to @index@ below.
| BS.Strict.null bs
= bs
-- Two padding bytes
| BS.Strict.Char8.index bs (len - 2) == '='
= BS.Strict.take (len - 2) bs
-- One padding byte
| BS.Strict.Char8.index bs (len - 1) == '='
= BS.Strict.take (len - 1) bs
| otherwise
= bs
where
len :: Int
len = BS.Strict.length bs
addPadding :: Strict.ByteString -> Either String Strict.ByteString
addPadding bs
-- Three padding bytes (i.e., invalid strict)
| len `mod` 4 == 1
= Left $ "Invalid length of unpadded base64-encoded string " ++ show len
-- Two padding bytes
| len `mod` 4 == 2
= Right $ bs <> BS.Strict.Char8.pack "=="
-- One padding bytes
| len `mod` 4 == 3
= Right $ bs <> BS.Strict.Char8.pack "="
-- No padding (this includes the empty string)
| otherwise
= Right bs
where
len :: Int
len = BS.Strict.length bs