packages feed

willow-0.1.0.0: src/Web/Willow/Common/Encoding/SingleByte.hs

{-# LANGUAGE Trustworthy #-}

{-|
Description:    All encoders and decoders for one-byte-per-'Char' encodings.

Copyright:      (c) 2020 Sam May
License:        MPL-2.0
Maintainer:     ag.eitilt@gmail.com

Stability:      experimental
Portability:    portable
-}
module Web.Willow.Common.Encoding.SingleByte
    ( decoder
    , encoder
    , encodings
    ) where


import qualified Control.Applicative as A
import qualified Control.Monad as N

import qualified Data.ByteString.Short as BS.SH
import qualified Data.Char as C
import qualified Data.HashMap.Lazy as M.L
import qualified Data.HashMap.Strict as M.S
import qualified Data.Maybe as Y
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as V.M
import qualified Data.Word as W

import qualified System.IO.Unsafe as IO.Unsafe

import Data.Vector ( (!?) )

import Web.Willow.Common.Encoding.Common
import Web.Willow.Common.Parser


-- | __Encoding:__
--      @[single-byte decoder]
--      (https://encoding.spec.whatwg.org/#single-byte-decoder)@
-- 
-- Decodes a 'Char' from a binary stream encoded with a given
-- byte-per-character encoding, or returns 'Left' if the stream starts with a
-- byte not used by that encoding.
-- 
-- Fails if the 'Encoding' is handled by a different algorithm.
decoder :: Encoding -> TextBuilder
decoder enc = do
    index <- maybe A.empty return $ M.L.lookup enc decodeIndices
    byte <- next
    if isAsciiByte byte
        then emit [byte] . toEnum $ fromIntegral byte
        else maybe (decoderFailure [byte]) (emit [byte]) . N.join $
            index !? fromIntegral (byte - 0x80)

-- | __Encoding:__
--      @[single-byte encoder]
--      (https://encoding.spec.whatwg.org/#single-byte-encoder)@
-- 
-- Encode the first 'Char' in a string according to a given byte-per-character
-- encoding scheme, or return that same character if that scheme doesn't define
-- a binary representation for it.
-- 
-- Fails if the 'Encoding' is handled by a different algorithm.
encoder :: Encoding -> BinaryBuilder
encoder enc = do
    index <- maybe A.empty return $ M.L.lookup enc encodeIndices
    char <- next
    Y.fromMaybe (encoderFailure char) $ if C.isAscii char
        then Just $ fromAscii char
        else return . pure . BS.SH.pack . (: []) . (+ 0x80) <$> M.S.lookup char index


-- | __Encoding:__
--      @[Legacy single-byte encodings]
--      (https://encoding.spec.whatwg.org/#legacy-single-byte-encodings)@
--      table column 1
-- 
-- All byte-per-character encodings handled by 'decoder' and 'encoder'; those
-- parsers will fail if passed any 'Encoding' not in this list.
encodings :: [Encoding]
encodings = map fst indexNames


-- | The lookup tables generated for reading byte-per-character encodings.  The
-- 'V.Vector's themselves should hopefully not actually be evaluated (and
-- therefore read from disc) until they're needed.
decodeIndices :: M.L.HashMap Encoding (V.Vector (Maybe Char))
decodeIndices = M.L.fromList $ map (fmap readDecodeIndex) indexNames

-- | Generate the in-memory representation of high bytes in byte-per-character
-- encodings.
-- 
-- As all the single-byte 'Encoding's used by the HTML standard pack the
-- limited space tightly, and because that space is indeed limited, using a
-- 'V.Vector' instead of a 'M.S.HashMap' is able to provide great random lookup
-- and space efficiency.
-- 
-- This uses 'IO.Unsafe.unsafePerformIO' internally, as the index files
-- shouldn't change during runtime, and the vector mutability is
-- tightly-scoped.  This should therefore be a safe use of it.
readDecodeIndex :: String -> V.Vector (Maybe Char)
readDecodeIndex name = IO.Unsafe.unsafePerformIO $ do
    vector <- V.M.replicate 0x80 Nothing
    mapM_ (setDecodeIndex vector) $ loadIndex name
    V.freeze vector
{-# NOINLINE readDecodeIndex #-}

-- | Modify the data storage used for a particular 'Encoding' based on a single
-- pre-processed line in its index file.
setDecodeIndex :: V.M.IOVector (Maybe Char) -> (Word, Char) -> IO ()
setDecodeIndex vector (index, char) = V.M.write vector (fromIntegral index) (Just char)


-- | The lookup tables generated for writing in byte-per-character encodings.
-- The value 'M.S.HashMap's themselves should hopefully not actually be
-- evaluated (and therefore read from disc) until they're needed.
encodeIndices :: M.L.HashMap Encoding (M.S.HashMap Char W.Word8)
encodeIndices = M.L.fromList $ map (fmap readEncodeIndex) indexNames

-- | Generate the in-memory lookup of high bytes in byte-per-character
-- encodings.
-- 
-- The limited size and resulting high percentage of characters used per-file
-- mean that a strict 'M.S.HashMap' should take less memory than a lazy one,
-- with its thunks.
readEncodeIndex :: String -> M.S.HashMap Char W.Word8
readEncodeIndex name = M.S.fromList . map pack $ loadIndex name
  where pack (index, char) = (char, fromIntegral index)


-- | __Encoding:__
--      @[Legacy single-byte encodings]
--      (https://encoding.spec.whatwg.org/#legacy-single-byte-encodings)@
--      table columns 1-2
-- 
-- The mapping between byte-per-character encodings and their lookup index
-- file name (without extension or path).  If one isn't listed here, 'decoder'
-- and 'encoder' will fail if given that 'Encoding'.
indexNames :: [(Encoding, String)]
indexNames =
    [ (Ibm866, "ibm866")
    , (Iso8859_2, "iso-8859-2")
    , (Iso8859_3, "iso-8859-3")
    , (Iso8859_4, "iso-8859-4")
    , (Iso8859_5, "iso-8859-5")
    , (Iso8859_6, "iso-8859-6")
    , (Iso8859_7, "iso-8859-7")
    , (Iso8859_8, "iso-8859-8")
    , (Iso8859_8i, "iso-8859-8")
    , (Iso8859_10, "iso-8859-10")
    , (Iso8859_13, "iso-8859-13")
    , (Iso8859_14, "iso-8859-14")
    , (Iso8859_15, "iso-8859-15")
    , (Iso8859_16, "iso-8859-16")
    , (Koi8R, "koi8-r")
    , (Koi8U, "koi8-u")
    , (Macintosh, "macintosh")
    , (MacintoshCyrillic, "x-mac-cyrillic")
    , (Windows874, "windows-874")
    , (Windows1250, "windows-1250")
    , (Windows1251, "windows-1251")
    , (Windows1252, "windows-1252")
    , (Windows1253, "windows-1253")
    , (Windows1254, "windows-1254")
    , (Windows1255, "windows-1255")
    , (Windows1256, "windows-1256")
    , (Windows1257, "windows-1257")
    , (Windows1258, "windows-1258")
    ]