yamlet-1.0.0.0: src/Yamlet/Internal/Input.hs
{-# OPTIONS_HADDOCK not-home #-}
-- | Detection of the encoding of the input.
--
-- This module is intended for internal use only, and may change without warning
-- in subsequent releases.
module Yamlet.Internal.Input
( decodeInput
) where
import Data.Bits
import Data.ByteString qualified as BS
import Data.ByteString.Unsafe qualified as BS
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.Text.Encoding.Error qualified as T
import Data.Text.Unsafe qualified as T
import Yamlet.Error
import Yamlet.Internal.Syntax
import Yamlet.Internal.Utils
-- | Decode the bytes of a stream to text. The encoding is UTF-8, UTF-16 or
-- UTF-32, detected as the YAML specification describes.
decodeInput :: BS.ByteString -> Either Error T.Text
decodeInput bs = case map (BS.indexMaybe bs) [0 .. 3] of
[Just 0, Just 0, Just 0xFE, Just 0xFF] -> utf32 T.decodeUtf32BEWith be32 (BS.drop 4 bs)
[Just 0xFF, Just 0xFE, Just 0, Just 0] -> utf32 T.decodeUtf32LEWith le32 (BS.drop 4 bs)
[Just 0xFE, Just 0xFF, _, _] -> utf16 T.decodeUtf16BEWith be16 (BS.drop 2 bs)
[Just 0xFF, Just 0xFE, _, _] -> utf16 T.decodeUtf16LEWith le16 (BS.drop 2 bs)
[Just 0, Just 0, Just 0, Just _] -> utf32 T.decodeUtf32BEWith be32 bs
[Just x, Just 0, Just 0, Just 0] | x /= 0 -> utf32 T.decodeUtf32LEWith le32 bs
Just 0 : Just _ : _ -> utf16 T.decodeUtf16BEWith be16 bs
Just x : Just 0 : _ | x /= 0 -> utf16 T.decodeUtf16LEWith le16 bs
_ -> utf8
where
utf32
:: (T.OnDecodeError -> BS.ByteString -> T.Text)
-> (BS.ByteString -> Int -> Int)
-> BS.ByteString
-> Either Error T.Text
utf32 decodeWith unit input = checked "invalid UTF-32" decodeWith input (go 0)
where
go :: Int -> Int
go i
| i + 4 > BS.length input = i
| isScalarValue c = go (i + 4)
| otherwise = i
where
c :: Int
c = unit input i
-- Inlining gives a loop for each reading function. Without it, the decode
-- of a large UTF-32 input was slower and allocated more.
{-# INLINE utf32 #-}
utf16
:: (T.OnDecodeError -> BS.ByteString -> T.Text)
-> (BS.ByteString -> Int -> Int)
-> BS.ByteString
-> Either Error T.Text
utf16 decodeWith unit input = checked "invalid UTF-16" decodeWith input (go 0)
where
go :: Int -> Int
go i
| i + 2 > BS.length input = i
| isHighSurrogate u =
if i + 4 <= BS.length input && isLowSurrogate (unit input (i + 2))
then go (i + 4)
else i
| isLowSurrogate u = i
| otherwise = go (i + 2)
where
u :: Int
u = unit input i
-- Inlining gives a loop for each reading function. Without it, the decode
-- of a large UTF-16 input was slower and allocated more.
{-# INLINE utf16 #-}
-- The code unit at the index. The callers make sure that its bytes are in
-- the input.
be16, le16, be32, le32 :: BS.ByteString -> Int -> Int
be16 input i = byte input i `shiftL` 8 .|. byte input (i + 1)
le16 input i = byte input (i + 1) `shiftL` 8 .|. byte input i
be32 input i = be16 input i `shiftL` 16 .|. be16 input (i + 2)
le32 input i = le16 input (i + 2) `shiftL` 16 .|. le16 input i
byte :: BS.ByteString -> Int -> Int
byte input i = fromIntegral (BS.unsafeIndex input i)
-- The text, or an error at the end of the valid prefix of the given length.
checked
:: String
-> (T.OnDecodeError -> BS.ByteString -> T.Text)
-> BS.ByteString
-> Int
-> Either Error T.Text
checked msg decodeWith input valid
| valid == BS.length input = Right $ decodeWith T.lenientDecode input
| otherwise = Left $ errorAt prefix (Offset (T.lengthWord8 prefix)) msg
where
prefix :: T.Text
prefix = decodeWith T.lenientDecode (BS.take valid input)
utf8 :: Either Error T.Text
utf8 = case T.decodeUtf8' bs of
Right t -> Right t
Left _ ->
-- The length of the longest valid prefix.
let valid = fst (T.validateUtf8Chunk bs)
prefix = T.decodeUtf8 (BS.take valid bs)
in Left $ errorAt prefix (Offset valid) "invalid UTF-8"