tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

UTF8.hs (3245B)


      1 {-# LANGUAGE BangPatterns #-}
      2 -- Stateless bounded Unicode transport. Source owns the <=3-byte carry between
      3 -- calls, text/source policy and sequencing; no shared mutable decoder context.
      4 module IO.UTF8 (encode, decode, scalarLimit) where
      5 
      6 import qualified IO.Resources as R
      7 import qualified Data.ByteString as BS
      8 import qualified Data.Text as Text
      9 import Data.Text.Encoding (encodeUtf8)
     10 import Data.Char (chr, ord)
     11 import Data.Bits ((.&.))
     12 import Data.List (foldl')
     13 
     14 scalarLimit :: Int
     15 scalarLimit = R.chunkLimit `div` 4
     16 
     17 fault op message = R.Problem "invalid-data" op message "none"
     18 
     19 encode :: String -> Either R.Problem BS.ByteString
     20 encode text = encodeUtf8 . Text.pack <$> checked scalarLimit text
     21   where
     22     checked _ [] = Right []
     23     checked 0 _ = Left (R.Problem "invalid-bound" "encode" "Unicode chunk exceeds scalar limit" "none")
     24     checked n (c:rest)
     25       | ord c >= 0xd800 && ord c <= 0xdfff = Left (fault "encode" "surrogate is not a Unicode scalar")
     26       | otherwise = (c:) <$> checked (n-1) rest
     27 
     28 -- Strict UTF-8, including overlong/surrogate/range controls. A nonfinal valid
     29 -- incomplete suffix is copied (not a slice retaining the input buffer). Invalid
     30 -- prefixes fail immediately, even when incomplete. No partial output on error.
     31 decode :: Bool -> BS.ByteString -> Either R.Problem (String, BS.ByteString)
     32 decode final bytes
     33   | BS.length bytes > R.chunkLimit = Left (R.Problem "invalid-bound" "decode" "UTF-8 chunk exceeds byte limit" "none")
     34   | otherwise = walk 0 []
     35   where
     36     end = BS.length bytes
     37     at i = fromIntegral (BS.index bytes i) :: Int
     38     bad offset why = Left (fault "decode" (why ++ " at byte " ++ show offset))
     39     walk !offset chars
     40       | offset == end = Right (reverse chars, BS.empty)
     41       | otherwise =
     42           let lead = at offset
     43               emit code count = let !c = chr code in walk (offset + count) (c:chars)
     44               sequenceOf count low high mask =
     45                 let valid i = let c = at (offset+i)
     46                               in c >= (if i == 1 then low else 0x80)
     47                                  && c <= (if i == 1 then high else 0xbf)
     48                     available = min (count-1) (end-offset-1)
     49                 in if not (all valid [1..available]) then bad offset "invalid UTF-8 sequence"
     50                    else if end-offset < count then
     51                      if final then bad offset "incomplete UTF-8 sequence"
     52                      else Right (reverse chars, BS.copy (BS.drop offset bytes))
     53                    else emit (foldl' (\code i -> code*64 + (at (offset+i) .&. 0x3f))
     54                          (lead .&. mask) [1..count-1]) count
     55           in if lead <= 0x7f then emit lead 1
     56              else if lead >= 0xc2 && lead <= 0xdf then sequenceOf 2 0x80 0xbf 0x1f
     57              else if lead == 0xe0 then sequenceOf 3 0xa0 0xbf 0x0f
     58              else if lead == 0xed then sequenceOf 3 0x80 0x9f 0x0f
     59              else if lead >= 0xe1 && lead <= 0xef then sequenceOf 3 0x80 0xbf 0x0f
     60              else if lead == 0xf0 then sequenceOf 4 0x90 0xbf 0x07
     61              else if lead == 0xf4 then sequenceOf 4 0x80 0x8f 0x07
     62              else if lead >= 0xf1 && lead <= 0xf3 then sequenceOf 4 0x80 0xbf 0x07
     63              else bad offset "invalid UTF-8 leading byte"