hq-json-0.1.1: src/HQ/JSON/Decoder/String.hs
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoImplicitPrelude #-}
-- | String parsing for the streaming JSON decoder, including escapes,
-- unicode escapes and surrogate pairs.
module HQ.JSON.Decoder.String where
import Data.Bits (Bits (..), shiftL)
import Data.Text qualified as T
import HQ.JSON.Decoder.Core
import HQ.JSON.Decoder.Error (DecodeError (..))
import HQ.JSON.Event (JSONEvent (..))
import Relude hiding (Compose, id, many, some, state)
startString ::
StringTarget -> Text -> DecoderState -> Either DecodeError DecoderResult
startString target input = consumeString target input emptyStringBuffer
consumeString ::
StringTarget ->
Text ->
StringBuffer ->
DecoderState ->
Either DecodeError DecoderResult
consumeString target input buffer decoder = case T.uncons rest of
Nothing ->
let bufferedTarget = BufferedStringTarget target newBuffer
parserString = DecoderPhaseString (InString bufferedTarget)
decoder' = decoder {decoderInput = mempty, decoderPhase = parserString}
in Right $ NeedInput decoder'
Just (c, rest')
| c == '"' -> case buffer of
-- Single escape-free fragment (the common case for keys and
-- short values): reference the input slice directly instead
-- of copying it through 'StringBuffer' and 'T.concat'.
StringBuffer [] -> finishStringText target chunk rest' decoder
_ -> finishString target newBuffer rest' decoder
| c == '\\' -> consumeStringEscape target rest' newBuffer decoder
| otherwise -> Left (UnexpectedChar c)
where
(chunk, rest) = T.span isStringChar input
newBuffer = appendStringBuffer chunk buffer
consumeStringEscape ::
StringTarget ->
Text ->
StringBuffer ->
DecoderState ->
Either DecodeError DecoderResult
consumeStringEscape target input buffer decoder = case T.uncons input of
Nothing ->
let parserString =
DecoderPhaseString
$ AfterEscape
$ BufferedStringTarget target buffer
in Right
$ NeedInput decoder {decoderInput = mempty, decoderPhase = parserString}
Just (c, rest)
| c == 'u' -> consumeUnicode target rest buffer decoder
| isSimpleEscape c ->
let buf = appendCharStringBuffer (decodeSimpleEscape c) buffer
in consumeString target rest buf decoder
| otherwise -> Left (InvalidEscape c)
consumeUnicode ::
StringTarget ->
Text ->
StringBuffer ->
DecoderState ->
Either DecodeError DecoderResult
consumeUnicode target input buffer = consumeUnicode' target input buffer 0 0
consumeUnicode' ::
StringTarget ->
Text ->
StringBuffer ->
Int ->
Int ->
DecoderState ->
Either DecodeError DecoderResult
consumeUnicode' target input buffer value digits decoder
| newDigits >= 4 = finishUnicode target rest buffer newValue decoder
| T.null rest =
let unicode = Unicode newValue newDigits
bufferedString = BufferedStringTarget target buffer
parserString = DecoderPhaseString $ InUnicodeEscape bufferedString unicode
newDecoder = decoder {decoderInput = "", decoderPhase = parserString}
in Right $ NeedInput newDecoder
| otherwise = Left InvalidUnicodeEscape
where
-- Take at most the digits still needed to complete the escape: a
-- further hex digit belongs to the text following the escape.
(newValue, newDigits, rest) = accumulateHex value digits input
finishUnicode ::
StringTarget ->
Text ->
StringBuffer ->
Int ->
DecoderState ->
Either DecodeError DecoderResult
finishUnicode target input buffer value decoder
| isHighSurrogate value =
let bufferedStringT = BufferedStringTarget target buffer
parserString = DecoderPhaseString $ AfterHighSurrogate bufferedStringT value
newDecoder = decoder {decoderInput = input, decoderPhase = parserString}
in Right $ NeedInput newDecoder
| isLowSurrogate value = Left InvalidSurrogatePair
| otherwise =
consumeString target input (appendCharStringBuffer (chr value) buffer) decoder
stepString :: DecoderState -> StringState -> Either DecodeError DecoderResult
stepString decoder state = case state of
InString (BufferedStringTarget target buffer) ->
consumeString target (decoderInput decoder) buffer decoder
AfterEscape (BufferedStringTarget target buffer) ->
consumeStringEscape target (decoderInput decoder) buffer decoder
InUnicodeEscape (BufferedStringTarget target buffer) (Unicode value digits) ->
consumeUnicode' target (decoderInput decoder) buffer value digits decoder
AfterHighSurrogate (BufferedStringTarget target buffer) high ->
consumeLowSurrogate target (decoderInput decoder) buffer high decoder
AfterLowBackslash (BufferedStringTarget target buffer) high ->
consumeLowBackslash target (decoderInput decoder) buffer high decoder
InLowSurrogateEscape (BufferedStringTarget target buffer) high (Unicode value digits) ->
consumeLowSurrogateDigits target (decoderInput decoder) buffer high value digits decoder
consumeLowSurrogate ::
StringTarget ->
Text ->
StringBuffer ->
Int ->
DecoderState ->
Either DecodeError DecoderResult
consumeLowSurrogate target input buffer high decoder = case T.uncons input of
Nothing -> Right $ NeedInput decoder {decoderInput = mempty}
Just ('\\', rest) -> case T.uncons rest of
Nothing ->
let parserString = DecoderPhaseString $ AfterLowBackslash (BufferedStringTarget target buffer) high
in Right $ NeedInput decoder {decoderInput = mempty, decoderPhase = parserString}
Just ('u', rest') ->
consumeLowSurrogateDigits target rest' buffer high 0 0 decoder
_ -> Left InvalidSurrogatePair
_ -> Left InvalidSurrogatePair
-- | Resume after a chunk split between the low escape's backslash and
-- @u@: only @u@ may follow, continuing into the low hex digits.
consumeLowBackslash ::
StringTarget ->
Text ->
StringBuffer ->
Int ->
DecoderState ->
Either DecodeError DecoderResult
consumeLowBackslash target input buffer high decoder = case T.uncons input of
Nothing -> Right $ NeedInput decoder {decoderInput = mempty}
Just ('u', rest) ->
consumeLowSurrogateDigits target rest buffer high 0 0 decoder
_ -> Left InvalidSurrogatePair
consumeLowSurrogateDigits ::
StringTarget ->
Text ->
StringBuffer ->
Int ->
Int ->
Int ->
DecoderState ->
Either DecodeError DecoderResult
consumeLowSurrogateDigits target input buffer high value digits decoder
| digits == 4 =
if isLowSurrogate value
then
let ch = chr (combineSurrogates high value)
newBuffer = appendCharStringBuffer ch buffer
in consumeString target input newBuffer decoder
else Left InvalidSurrogatePair
| otherwise =
let (newValue, newDigits, rest) = accumulateHex value digits input
in if newDigits == 4
then
if isLowSurrogate newValue
then
let ch = chr (combineSurrogates high newValue)
newBuffer = appendCharStringBuffer ch buffer
in consumeString target rest newBuffer decoder
else Left InvalidSurrogatePair
else
if T.null rest
then
Right
$ NeedInput
decoder
{ decoderInput = mempty,
decoderPhase =
DecoderPhaseString
$ InLowSurrogateEscape
(BufferedStringTarget target buffer)
high
(Unicode newValue newDigits)
}
else
Left InvalidUnicodeEscape
-- | Combine a surrogate pair into its codepoint.
{-# INLINE combineSurrogates #-}
combineSurrogates :: Int -> Int -> Int
combineSurrogates high low =
0x10000 + ((high - 0xD800) `shiftL` 10) + (low - 0xDC00)
finishString ::
StringTarget ->
StringBuffer ->
Text ->
DecoderState ->
Either DecodeError DecoderResult
finishString target = finishStringText target . finishStringBuffer
-- | Finish a string from its materialized text. 'finishString' funnels
-- here after concatenating multi-fragment buffers, while the
-- single-fragment fast path in 'consumeString' calls it directly with
-- the input slice.
finishStringText ::
StringTarget ->
Text ->
Text ->
DecoderState ->
Either DecodeError DecoderResult
finishStringText target text remaining decoder = case target of
StringValue -> emitEvent (JSONString text) remaining decoder
StringKey -> do
let key = JSONObjectKey text
let newDecoder =
decoder
{ decoderInput = remaining,
decoderPhase = DecoderPhaseObjectColon
}
Right $ Emit key newDecoder