packages feed

hsjuiz-0.2.6: src/Juiz/Internal/Base64.hs

-- | base64 (RFC 4648 §4、パディングあり)。データ型の bytes を辞書で表すのに使う (docs/spec/datatype.md §4)
module Juiz.Internal.Base64
  ( encodeBase64
  , decodeBase64
  ) where

import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BC
import Data.Char (chr, ord)
import Data.Word (Word8)

alphabet :: BS.ByteString
alphabet = BC.pack "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

encodeBase64 :: BS.ByteString -> String
encodeBase64 bs = go (BS.unpack bs)
  where
    sym :: Int -> Char
    sym i = chr (fromIntegral (BS.index alphabet i))
    go (a : b : c : rest) =
      let n = w a `shiftL` 16 .|. w b `shiftL` 8 .|. w c
      in map (\s -> sym ((n `shiftR` s) .&. 63)) [18, 12, 6, 0] ++ go rest
    go [a, b] =
      let n = w a `shiftL` 16 .|. w b `shiftL` 8
      in map (\s -> sym ((n `shiftR` s) .&. 63)) [18, 12, 6] ++ "="
    go [a] =
      let n = w a `shiftL` 16
      in map (\s -> sym ((n `shiftR` s) .&. 63)) [18, 12] ++ "=="
    go [] = []
    w :: Word8 -> Int
    w = fromIntegral

-- | パディングまで正しい base64 だけを受け付ける。誤りなら Nothing
decodeBase64 :: String -> Maybe BS.ByteString
decodeBase64 s
  | length s `mod` 4 /= 0 = Nothing
  | otherwise = BS.pack . concat <$> mapM quad (chunks s)
  where
    chunks [] = []
    chunks xs = let (a, b) = splitAt 4 xs in a : chunks b
    value c
      | c >= 'A' && c <= 'Z' = Just (ord c - ord 'A')
      | c >= 'a' && c <= 'z' = Just (ord c - ord 'a' + 26)
      | c >= '0' && c <= '9' = Just (ord c - ord '0' + 52)
      | c == '+' = Just 62
      | c == '/' = Just 63
      | otherwise = Nothing
    -- 使わない下位のビットが 0 でなければ誤り (同じバイト列の表し方を 1 つにする)
    bytes :: Int -> [Int] -> Maybe [Word8]
    bytes count vs
      | n .&. ((1 `shiftL` (8 * (3 - count))) - 1) /= 0 = Nothing
      | otherwise = Just [fromIntegral ((n `shiftR` (16 - 8 * i)) .&. 255) | i <- [0 .. count - 1]]
      where
        n = combine vs
    quad [a, b, '=', '='] = mapM value [a, b] >>= bytes 1
    quad [a, b, c, '='] = mapM value [a, b, c] >>= bytes 2
    quad q = mapM value q >>= bytes 3
    combine vs = foldl (\acc v -> acc `shiftL` 6 .|. v) 0 vs `shiftL` (6 * (4 - length vs))