willow-0.1.0.0: src/Web/Willow/Common/Encoding/Utf8.hs
{-|
Description: Character translation functions to and from the UTF-8 encoding scheme.
Copyright: (c) 2020 Sam May
License: MPL-2.0
Maintainer: ag.eitilt@gmail.com
Stability: experimental
Portability: portable
-}
module Web.Willow.Common.Encoding.Utf8
( decoder
, encoder
, byteOrderMark
) where
import qualified Control.Applicative as A
import qualified Data.Bifunctor as F.B
import qualified Data.Bits as B
import qualified Data.ByteString as BS
import qualified Data.ByteString.Short as BS.SH
import qualified Data.Char as C
import qualified Data.Word as W
import Data.Functor ( ($>) )
import Data.Bits ( (.&.), (.|.) )
import Web.Willow.Common.Encoding.Common
import Web.Willow.Common.Parser
-- | The binary encoding of a byte-order mark character in UTF-8.
--
-- Fails if the stream does not start with the exact sequence @[0xEF, 0xBB,
-- 0xBF]@. If successful, always returns 'Utf8'.
byteOrderMark
:: (A.Alternative gather, Monad gather)
=> ParserT BS.ByteString gather Encoding
byteOrderMark = chunk (BS.pack [0xEF, 0xBB, 0xBF]) $> Utf8
-- | __Encoding:__
-- @[UTF-8 decoder]
-- (https://encoding.spec.whatwg.org/#utf-8-decoder)@
--
-- Decodes a 'Char' from a binary stream encoded with the 'Utf8' encoding
-- scheme, or returns 'Left' if the stream starts with an invalid byte
-- sequence.
decoder :: TextBuilder
decoder = do
byte <- next
c <- decoder' $ fromIntegral byte
case c of
Left err -> decoderFailure . (byte :) $ BS.SH.unpack err
Right code -> emit [byte] . toEnum $ fromIntegral code
where decoder' b
| b <= 0x7F = return $ Right b
| b <= 0xC1 = return $ Left BS.SH.empty
| b <= 0xDF = packError . decodeChar 1 defL defH $ b .&. 0x1F
| b == 0xE0 = packError . decodeChar 2 0xA0 defH $ b .&. 0xF
| b == 0xED = packError . decodeChar 2 defL 0x9F $ b .&. 0xF
| b <= 0xEF = packError . decodeChar 2 defL defH $ b .&. 0xF
| b == 0xF0 = packError . decodeChar 3 0x90 defH $ b .&. 0x7
| b <= 0xF3 = packError . decodeChar 3 defL defH $ b .&. 0x7
| b == 0xF4 = packError . decodeChar 3 defL 0x8F $ b .&. 0x7
| otherwise = return $ Left BS.SH.empty
defL = 0x80
defH = 0xBF
packError = fmap $ F.B.first BS.SH.pack
-- | __Encoding:__
-- @[UTF-8 decoder]
-- (https://encoding.spec.whatwg.org/#utf-8-decoder)@
-- steps 4-11
--
-- Decodes the remainder of a 'Utf8' encoded 'Char', given its remaining length,
-- the allowed range of continuation bytes, and the so-far-parsed portion
-- (right-justified; i.e. when called for the second byte of a two-byte
-- character, the first byte would be trimmed with @'.&.' 0x1F@ /not/
-- 'B.shiftR').
decodeChar
:: Word
-- ^ The remaining length of the binary sequence.
-> W.Word8
-- ^ The lowest allowed value for this byte.
-> W.Word8
-- ^ The highest allowed value for this byte.
-> Word
-- ^ The accumulated code point bits so far.
-> Decoder (Either [W.Word8] Word)
decodeChar 0 _ _ code = return $ Right code
decodeChar len low high code = do
byte <- next
if range low high byte
then F.B.first (byte :) <$>
decodeChar (len - 1) 0x80 0xBF (B.shiftL code 6 .|. (fromIntegral byte .&. 0x3F))
else push byte $> Left []
-- | __Encoding:__
-- @[UTF-8 encoder]
-- (https://encoding.spec.whatwg.org/#utf-8-encoder)@
--
-- Encode the first 'Char' in a string according to the 'Utf8' encoding scheme,
-- or return that same character if that scheme doesn't define a binary
-- representation for it.
encoder :: BinaryBuilder
encoder = do
char <- next
(len, offset) <- countOffset char
let first = fromIntegral $ B.shiftR (fromEnum char) (6 * len) + offset
return . pure . BS.SH.pack $ first : trail (fromEnum char) len
where countOffset char
| C.isAscii char = pure (0, 0)
| char <= '\x07FF' = pure (1, 0xC0)
| char <= '\xFFFF' = pure (2, 0xE0)
| char <= '\x10FFFF' = pure (3, 0xF0)
-- 'Char' can't even be initialized this big, but include to make
-- the pattern match complete.
| otherwise = A.empty
trail _ 0 = []
trail code count = fromIntegral (0x80 .|. (B.shiftR code (6 * (count - 1)) .&. 0x3F)) :
trail code (count - 1)