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"