summaryrefslogtreecommitdiffstats
path: root/src/Codec/MIME/Base64.hs
blob: 4372a7f643a7a73df6693d15d01e832752845dca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
--------------------------------------------------------------------
-- |
-- Module    : Codec.MIME.Base64
-- Copyright : (c) 2006-2009, Galois, Inc.
-- License   : BSD3
--
-- Maintainer: Sigbjorn Finne <sigbjorn.finne@gmail.com>
-- Stability : provisional
-- Portability: portable
--
--
-- Base64 decoding and encoding routines, multiple entry
-- points for either depending on use and level of control
-- wanted over the encoded output (and its input form on the
-- decoding side.)
--
--------------------------------------------------------------------
module Codec.MIME.Base64
        ( encodeRaw         -- :: Bool -> String -> [Word8]
        , encodeRawString   -- :: Bool -> String -> String
        , encodeRawPrim     -- :: Bool -> Char -> Char -> [Word8] -> String

        , formatOutput      -- :: Int    -> Maybe String -> String -> String

        , decode            -- :: String -> [Word8]
        , decodeToString    -- :: String -> String
        , decodePrim        -- :: Char -> Char -> String -> [Word8]
        ) where

import Data.Bits
import Data.Char
import Data.Word
import Data.Maybe

encodeRawString :: Bool -> String -> String
encodeRawString trail xs = encodeRaw trail (map (fromIntegral.ord) xs)

-- | @formatOutput n mbLT str@ formats @str@, splitting it
-- into lines of length @n@. The optional value lets you control what
-- line terminator sequence to use; the default is CRLF (as per MIME.)
formatOutput :: Int -> Maybe String -> String -> String
formatOutput n mbTerm str
 | n <= 0    = error ("Codec.MIME.Base64.formatOutput: negative line length " ++ show n)
 | otherwise = chop n str
   where
     crlf :: String
     crlf = fromMaybe "\r\n" mbTerm

     chop _ "" = ""
     chop i xs =
       case splitAt i xs of
         (as,"") -> as
         (as,bs) -> as ++ crlf ++ chop i bs

encodeRaw :: Bool -> [Word8] -> String
encodeRaw trail bs = encodeRawPrim trail '+' '/' bs

-- | @encodeRawPrim@ lets you control what non-alphanum characters to use
-- (The base64url variation uses @*@ and @-@, for instance.)
-- No support for mapping these to multiple characters in the output though.
encodeRawPrim :: Bool -> Char -> Char -> [Word8] -> String
encodeRawPrim trail ch62 ch63 ls = encoder ls
 where
  trailer xs ys
   | not trail = xs
   | otherwise = xs ++ ys
  f = fromB64 ch62 ch63
  encoder []    = []
  encoder [x]   = trailer (take 2 (encode3 f x 0 0 "")) "=="
  encoder [x,y] = trailer (take 3 (encode3 f x y 0 "")) "="
  encoder (x:y:z:ws) = encode3 f x y z (encoder ws)

encode3 :: (Word8 -> Char) -> Word8 -> Word8 -> Word8 -> String -> String
encode3 f a b c rs =
     f (low6 (w24 `shiftR` 18)) :
     f (low6 (w24 `shiftR` 12)) :
     f (low6 (w24 `shiftR` 6))  :
     f (low6 w24) : rs
   where
    w24 :: Word32
    w24 = (fromIntegral a `shiftL` 16) +
          (fromIntegral b `shiftL` 8)  +
           fromIntegral c

decodeToString :: String -> String
decodeToString str = map (chr.fromIntegral) $ decode str

decode :: String -> [Word8]
decode str = decodePrim '+' '/' str

decodePrim :: Char -> Char -> String -> [Word8]
decodePrim ch62 ch63 str =  decoder $ takeUntilEnd str
 where
  takeUntilEnd "" = []
  takeUntilEnd ('=':_) = []
  takeUntilEnd (x:xs) =
    case toB64 ch62 ch63 x of
      Nothing -> takeUntilEnd xs
      Just b  -> b : takeUntilEnd xs

decoder :: [Word8] -> [Word8]
decoder [] = []
decoder [x] = take 1 (decode4 x 0 0 0 [])
decoder [x,y] = take 1 (decode4 x y 0 0 []) -- upper 4 bits of second val are known to be 0.
decoder [x,y,z] = take 2 (decode4 x y z 0 [])
decoder (x:y:z:w:xs) = decode4 x y z w (decoder xs)

decode4 :: Word8 -> Word8 -> Word8 -> Word8 -> [Word8] -> [Word8]
decode4 a b c d rs =
  (lowByte (w24 `shiftR` 16)) :
  (lowByte (w24 `shiftR` 8))  :
  (lowByte w24) : rs
 where
  w24 :: Word32
  w24 =
   (fromIntegral a) `shiftL` 18 .|.
   (fromIntegral b) `shiftL` 12 .|.
   (fromIntegral c) `shiftL` 6  .|.
   (fromIntegral d)

toB64 :: Char -> Char -> Char -> Maybe Word8
toB64 a b ch
  | ch >= 'A' && ch <= 'Z' = Just (fromIntegral (ord ch - ord 'A'))
  | ch >= 'a' && ch <= 'z' = Just (26 + fromIntegral (ord ch - ord 'a'))
  | ch >= '0' && ch <= '9' = Just (52 + fromIntegral (ord ch - ord '0'))
  | ch == a = Just 62
  | ch == b = Just 63
  | otherwise = Nothing

fromB64 :: Char -> Char -> Word8 -> Char
fromB64 ch62 ch63 x
  | x < 26    = chr (ord 'A' + xi)
  | x < 52    = chr (ord 'a' + (xi-26))
  | x < 62    = chr (ord '0' + (xi-52))
  | x == 62   = ch62
  | x == 63   = ch63
  | otherwise = error ("fromB64: index out of range " ++ show x)
 where
  xi :: Int
  xi = fromIntegral x

low6 :: Word32 -> Word8
low6 x = fromIntegral (x .&. 0x3f)

lowByte :: Word32 -> Word8
lowByte x = (fromIntegral x) .&. 0xff