packages feed

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

{-# LANGUAGE Trustworthy #-}

{-|
Description:    Character translation functions to and from the gb18030 and GBK encoding schemes.

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

Stability:      experimental
Portability:    portable
-}
module Web.Willow.Common.Encoding.GB
    ( decoder
    , encoderGb18030
    , encoderGbk
    ) where


import qualified Control.Applicative as A

import qualified Data.ByteString as BS
import qualified Data.ByteString.Short as BS.SH
import qualified Data.Char as C
import qualified Data.Maybe as Y
import qualified Data.Tuple as U
import qualified Data.Vector as V
import qualified Data.Word as W

import Control.Applicative ( (<|>) )

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


-- | __Encoding:__
--      @[gp18030 decoder]
--      (https://encoding.spec.whatwg.org/#gb18030-decoder)@
-- 
-- Decodes a 'Char' from a binary stream encoded with either the 'Gb18030' or
-- the 'Gbk' encoding schemes, or returns 'Left' if the stream starts with an
-- invalid byte sequence.
decoder :: TextBuilder
decoder = next >>= switch
    [ If isAsciiByte toUnicode1
    , If_ (== 0x80) $ emit [0x80] '\x20AC'
    , If (<= 0xFE) $ \first -> decoderFourBytes first <|> decoderTwoBytes first <|> do
        bs <- lookAhead $ nextChunk 3
        if BS.length bs < 3
            then A.empty
            else decoderFailure1 first
    , Else decoderFailure1
    ]

-- | __Encoding:__
--      @[gp18030 decoder]
--      (https://encoding.spec.whatwg.org/#gb18030-decoder)@
--      step 5
-- 
-- Parse a two-byte sequence given the offset of the second byte.
decoderTwoBytes :: W.Word8 -> TextBuilder
decoderTwoBytes first = next >>= switch
    [ If (range 0x40 0x7E) $ decoderTwoBytes' 0x40
    , If (range 0x80 0xFE) $ decoderTwoBytes' 0x41
    , If (== 0xFF) $ decoderFailure2 first
    , Else $ \b -> push b *> decoderFailure1 first
    ]
  where decoderTwoBytes' offset second = maybe (recovery second) (emit [first, second]) .
            decodeIndex $ (fromIntegral first - 0x81) * 190 + fromIntegral (second - offset)
        -- The index is a complete mapping, so this recovery will never be used.
        recovery second
            | isAsciiByte second = push second *> decoderFailure1 first
            | otherwise = decoderFailure [first, second]

-- | __Encoding:__
--      @[gp18030 decoder]
--      (https://encoding.spec.whatwg.org/#gb18030-decoder)@
--      steps 2-4
-- 
-- Parse a four-byte-long encoded sequence.  This is so much easier with a
-- stateful, byte-at-a-time parser as used in the spec rather than a pure,
-- character-at-a-time one.
-- 
-- The joint constraints which make this hard to code are:
-- 
-- * Fail if the second byte is outside the allowed range in order to continue
--   to the other choices.
-- * Return 'Left' without consuming input if all of the four are.
-- * Fail again (this time consuming input) if the end of the stream is reached
--   before the fourth byte.
-- * Succeed consuming input if everything's good.
-- 
-- This handles all of them.
-- 
-- As above, fails without consuming input if the byte at the head of the
-- stream is outside the allowed bounds.  Returns 'Left' consuming input if
-- there are less than three bytes left in the stream.
decoderFourBytes :: W.Word8 -> TextBuilder
decoderFourBytes first = do
    second <- lookAhead next >>= satisfying (range 0x30 0x39)
    trail <- A.optional $ do
        _ <- next
        third <- next >>= satisfying (range 0x81 0xFE)
        fourth <- next >>= satisfying (range 0x30 0x39)
        return (third, fourth)
    decoderFourBytes' second trail
  where decoderFourBytes' second (Just (third, fourth)) = maybe
            (decoderFailure [first, second, third, fourth])
            (emit [first, second, third, fourth]) .
            decodeRange $
                (fromIntegral first - 0x81) * 12600
                + (fromIntegral second - 0x30) * 1260
                + (fromIntegral third - 0x81) * 10
                + fromIntegral fourth - 0x30
        decoderFourBytes' _ Nothing = decoderFailure1 first


-- | __Encoding:__
--      @[gb18030 encoder]
--      (https://encoding.spec.whatwg.org/#gb18030-encoder)@
--      without @is GBK@
-- 
-- Encode the first 'Char' in a string according to the 'Gb18030' encoding
-- scheme, or return that same character if that scheme doesn't define a binary
-- representation for it.
-- 
-- Does not round-trip on @\\xE5E5@.
encoderGb18030 :: BinaryBuilder
encoderGb18030 = encoder False

-- | __Encoding:__
--      @[gb18030 encoder]
--      (https://encoding.spec.whatwg.org/#gb18030-encoder)@
--      with @is GBK@
-- 
-- Encode the first 'Char' in a string according to the 'Gbk' encoding scheme,
-- or return that same character if that scheme doesn't define a binary
-- representation for it.
-- 
-- Does not round-trip on @\\xE5E5@.
encoderGbk :: BinaryBuilder
encoderGbk = encoder True

-- | __Encoding:__
--      @[gb18030 encoder]
--      (https://encoding.spec.whatwg.org/#gb18030-encoder)@
-- 
-- Encode the first 'Char' in a string according to either the 'Gbk' ('True')
-- or 'Gb18030' ('False') encoding scheme, or return that same character if
-- that scheme doesn't define a binary representation for it.
-- 
-- Does not round-trip on @\\xE5E5@.
encoder :: Bool -> BinaryBuilder
encoder isGbk = next >>= switch
    [ If C.isAscii fromAscii
    , If (== '\xE5E5') encoderFailure
    , If_ (\c -> c == '\x20AC' && isGbk) $ return (pure $ BS.SH.pack [0x80])
    , If (Y.isJust . encodeIndex) $ \c ->
        let (lead', trail) = divMod (Y.fromJust $ encodeIndex c) 190
            lead = fromIntegral lead' + 0x81
        in  return . pure . BS.SH.pack $ if trail < 0x3F
                then [lead, fromIntegral trail + 0x40]
                else [lead, fromIntegral trail + 0x41]
    , If (const isGbk) encoderFailure
    , Else $ \c ->
        let (b1, p1) = divMod (encodeRange c) 12600
            (b2, p2) = divMod p1 1260
            (b3, b4) = divMod p2 10
        in  return . pure $ BS.SH.pack
                [ fromIntegral b1 + 0x81
                , fromIntegral b2 + 0x30
                , fromIntegral b3 + 0x81
                , fromIntegral b4 + 0x30
                ]
    ]


-- | Look for a character in the 'Gb18030' encoding at the given index.
decodeIndex :: Word -> Maybe Char
decodeIndex index = lookupMemoizedIndex decodeIndexM (Just encodeIndexM) index readDecodeIndex

-- | Memoization table to save lookup time in the over-large 'Gb18030' index.
decodeIndexM :: DecoderMemoTable
decodeIndexM = newMemoizationTable
{-# NOINLINE decodeIndexM #-}

-- | Read the character at a given offset from the 'Gb18030' index.  Note that
-- this is a heavy function, and should be cached whenever possible.
readDecodeIndex :: Word -> Maybe Char
readDecodeIndex index = search index $ loadIndex "gb18030"


-- | Look for the index of a given character in the 'Gb18030' encoding.
encodeIndex :: Char -> Maybe Word
encodeIndex char = lookupMemoizedIndex encodeIndexM (Just decodeIndexM) char readEncodeIndex

-- | Memoization table to save lookup time in the over-large 'Gb18030' index.
encodeIndexM :: EncoderMemoTable
encodeIndexM = newMemoizationTable
{-# NOINLINE encodeIndexM #-}

-- | Find the offset of a given character in the 'Gb18030' index.  Note that
-- this is a heavy function, and should be cached whenever possible.
readEncodeIndex :: Char -> Maybe Word
readEncodeIndex char = i1 <|> lookup char is2
  where (is1, is2) = splitAt 6176 . map U.swap $ loadIndex "gb18030"
        i1 = if char >= '\x4E02' && char <= '\xE525'
            then search char is1
            else Nothing


-- | __Encoding:__
--      @[index gb18030 ranges code point]
--      (https://encoding.spec.whatwg.org/#index-gb18030-ranges-code-point)@
-- 
-- Retrieve a character reference from the compressed list of four-byte
-- sequences, or 'Nothing' if the character is not included in 'Gb18030'.
-- 
-- Note that this only applies to characters which are part of a four-byte,
-- sequential range.  Offsets to sections with no relation to the Unicode order
-- will not return the proper 'Char'.
decodeRange :: Word -> Maybe Char
decodeRange index
    | index > 39419 && index < 189000 = Nothing
    | index > 1237575 = Nothing
    | index == 7457 = Just '\xE7C7'
    | otherwise = Just . toEnum . fromIntegral $ co + index - po
  where (po, co) = findRange $ \(i, _) -> i <= index

-- | __Encoding:__
--      @[index gb18030 ranges pointer]
--      (https://encoding.spec.whatwg.org/#index-gb18030-ranges-pointer)@
-- 
-- Retrieve an index offset from the compressed list of characters encoded in
-- four-byte sequences.  Returns the numeric value of the input unchanged if it
-- is an ASCII character.
-- 
-- Note that this only applies to characters which are part of a four-byte,
-- sequential range.  Characters in sections with no relation to the Unicode
-- order will not return any meaningful offset.
encodeRange :: Char -> Word
encodeRange '\xE7C7' = 7457
encodeRange char = po + fromIntegral (fromEnum char) - co
  where (po, co) = findRange $ \(_, c) -> c <= char

-- | Look up the offset-character pair at the start of the sequential range
-- indicated by the predicate; normally, you would test if the value you
-- already have is greater than or equal to the one passed to the argument
-- function.
-- 
-- Note that there is no length information about the ranges included, so even
-- if the predicate applies it may not be any help in finding the true
-- 'Gb18030' 'Char' or byte sequence.
-- 
-- Returns @(0, 0)@ for any sequence over ASCII, which is actually correct if
-- not really applicable to the four-byte sequences.
findRange :: ((Word, Char) -> Bool) -> (Word, Word)
findRange f = fmap (fromIntegral . fromEnum) . Y.fromMaybe (0, '\NUL') $ V.find f gbRanges

-- | Unlike the unordered indices, the range table is more than small enough to
-- keep resident in memory.  While it does not provide good keys for random
-- access, it is intended to be iterated over---which still perfectly fits the
-- data model of a 'V.Vector'.
-- 
-- Note that, as the values in the index indicate the lower bounds of each
-- range, the memory storage is reversed (highest offsets first) to allow
-- testing for "greatest less-than-or-equal-to" without needing lookahead or
-- backtracking.
gbRanges :: V.Vector (Word, Char)
gbRanges = V.fromList . reverse $ loadIndex "gb18030-ranges"