mangrove-0.1.0.0: src/Web/Mangrove/Parse/Encoding/Preprocess.hs
{-# LANGUAGE CPP #-}
{-|
Description: Clean a 'Char' stream by normalizing newlines and warning about control characters.
Copyright: (c) 2020 Sam May
License: MPL-2.0
Maintainer: ag.eitilt@gmail.com
Stability: experimental
Portability: portable
To simplify the tokenization parsers, the many representations of line breaks
are unified into a single, Unix-style @\\n@. While we're iterating over the
input, and before some of the special characters are replaced, it's also a good
time to trigger the warnings for unexpected characters
('ControlCharacterInInputStream', 'SurrogateInInputStream', and
'NoncharacterInInputStream').
-}
module Web.Mangrove.Parse.Encoding.Preprocess
( preprocess
, preprocessStep
-- * Initialization
, Encoding ( .. )
, DecoderState
, initialDecoderState
) where
import qualified Data.Bifunctor as F.B
import qualified Data.ByteString as BS
import qualified Data.ByteString.Short as BS.SH
import qualified Data.Tuple.HT as U.HT
#if MIN_VERSION_base(4,11,0)
#else
import Data.Semigroup ( (<>) )
#endif
import Web.Mangrove.Parse.Common.Error
import Web.Willow.Common.Encoding
import Web.Willow.Common.Encoding.Character
import Web.Willow.Common.Parser.Util
-- | __Encoding:__
-- @[preprocessing the input stream]
-- (https://encoding.spec.whatwg.org/#preprocessing-the-input-stream)@
--
-- Given a character encoding scheme, transform a dependant 'BS.ByteString'
-- into portable 'Char's. If any byte sequences are meaningless or illegal,
-- they are replaced with the Unicode replacement character @\\xFFFD@. All
-- newlines are normallized to a single @\\n@ 'Char', and Unicode control
-- characters, surrogate characters, and non-characters are marked with the
-- proper errors.
--
-- See 'preprocessStep' to operate over only a minimal section.
preprocess :: DecoderState -> BS.ByteString -> ([([ParseError], Char)], DecoderState)
preprocess state = F.B.first (normalize . map charError . concatMap flatten) . decode state
where normalize ((err1, '\r'):(err2, '\n'):cs) = (err1 <> err2, '\n') : normalize cs
normalize ((err, '\r'):cs) = (err, '\n') : normalize cs
normalize (c:cs) = c : normalize cs
normalize [] = []
-- | __Encoding:__
-- @[preprocessing the input stream]
-- (https://encoding.spec.whatwg.org/#preprocessing-the-input-stream)@
--
-- Read the smallest number of bytes from the head of the 'BS.ByteString'
-- which would leave the decoder in a re-enterable state. Any byte
-- sequences which are meaningless or illegal are replaced with the Unicode
-- replacement character @\\xFFFD@. All newlines are normallized to a single
-- @\\n@ 'Char', and Unicode control characters, surrogate characters, and
-- non-characters are marked with the proper errors.
--
-- See 'preprocess' to operate over the entire string at once.
preprocessStep
:: DecoderState
-> BS.ByteString
-> ([([ParseError], Char)], DecoderState, BS.ByteString)
preprocessStep state stream = normalize' . flatten' $ decodeStep state stream
where flatten' = U.HT.mapFst3 (maybe [] $ map charError . flatten)
normalize' c'@([], _, _) = c'
normalize' ([(errs, '\r')], state', stream') = case flatten' $ decodeStep state' stream' of
((errs', '\n'):cs, state'', stream'') ->
normalize' ((errs ++ errs', '\n') : cs, state'', stream'')
_ -> ([(errs, '\n')], state', stream')
normalize' ((errs, '\r'):(errs', '\n'):cs, state', stream') =
U.HT.mapFst3 ((errs ++ errs', '\n') :) $ normalize' (cs, state', stream')
normalize' ((errs, '\r'):cs, state', stream') =
U.HT.mapFst3 ((errs, '\n') :) $ normalize' (cs, state', stream')
normalize' (c:cs, state', stream') =
U.HT.mapFst3 (c :) $ normalize' (cs, state', stream')
-- | Add a 'ControlCharacterInInputStream', 'SurrogateInInputStream', or
-- 'NoncharacterInInputStream' error to the relevant characters.
charError :: ([ParseError], Char) -> ([ParseError], Char)
charError c'@(_, c)
| range '\xD800' '\xDFFF' c = addErr SurrogateInInputStream
| range '\xFDD0' '\xFDEF' c = addErr NoncharacterInInputStream
| noncharacter = addErr NoncharacterInInputStream
| range '\SOH' '\US' c && notElem c "\t\n\f\r" = addErr ControlCharacterInInputStream
| range '\DEL' '\x9F' c = addErr ControlCharacterInInputStream
| otherwise = c'
where noncharacter = case mod (fromEnum c) 0x10000 of
0xFFFE -> True
0xFFFF -> True
_ -> False
-- If this function could ever be applied twice, any errors would be
-- duplicated. As it's not exported from the module, that shouldn't be
-- an issue.
addErr err = F.B.first (err :) c'
-- | Rewrap the split type containing a list into a list at the top level. Any
-- 'Left' errors are replaced by @[('InvalidByteSequence' /bytes/, '\\xFFFD')]@.
flatten :: Either BS.SH.ShortByteString String -> [([ParseError], Char)]
flatten = either (\err -> [([InvalidByteSequence err], replacementChar)]) (map $ \c -> ([], c))