{- | C-style null-terminated data.

I mix string and bytestring terminology here, due to bad C influences. This
module is specifically interested in bytestrings and their encoding. String/text
encoding is handled in 'Binrep.Type.Text'.
-}

{-# LANGUAGE OverloadedStrings #-} -- for refined errors

module Binrep.Type.NullTerminated where

import Binrep

import FlatParse.Basic qualified as FP

import Rerefined.Predicate.Common
import Rerefined.Refine

import Data.ByteString qualified as B
import Data.Word ( Word8 )

-- | Null-terminated data. Arbitrary length terminated with a null byte.
--   Permits no null bytes inside the data.
data NullTerminate

instance Predicate NullTerminate where
    type PredicateName d NullTerminate = "NullTerminate"

type NullTerminated = Refined NullTerminate

-- | Null-terminated data may not contain any null bytes.
instance Refine NullTerminate B.ByteString where
    -- TODO is there a faster check we can conjure up here...?
    validate :: Proxy# NullTerminate -> ByteString -> Maybe RefineFailure
validate Proxy# NullTerminate
p ByteString
a = Proxy# NullTerminate -> Bool -> Builder -> Maybe RefineFailure
forall {k} (p :: k).
(Predicate p, KnownPredicateName p) =>
Proxy# p -> Bool -> Builder -> Maybe RefineFailure
validateBool Proxy# NullTerminate
p (Bool -> Bool
not ((Word8 -> Bool) -> ByteString -> Bool
B.any (Word8 -> Word8 -> Bool
forall a. Eq a => a -> a -> Bool
== Word8
0x00) ByteString
a)) (Builder -> Maybe RefineFailure) -> Builder -> Maybe RefineFailure
forall a b. (a -> b) -> a -> b
$
        Builder
"null byte not permitted in null-terminated data"

instance BLen a => BLen (NullTerminated a) where
    blen :: NullTerminated a -> Int
blen NullTerminated a
ra = Int
1 Int -> Int -> Int
forall a. Num a => a -> a -> a
+ a -> Int
forall a. BLen a => a -> Int
blen (NullTerminated a -> a
forall {k} (p :: k) a. Refined p a -> a
unrefine NullTerminated a
ra)
    {-# INLINE blen #-}

-- | Serialization of null-terminated data may be defined generally using the
--   data's underlying serializer.
instance Put a => Put (NullTerminated a) where
    {-# INLINE put #-}
    put :: NullTerminated a -> Putter
put NullTerminated a
a = a -> Putter
forall a. Put a => a -> Putter
put (NullTerminated a -> a
forall {k} (p :: k) a. Refined p a -> a
unrefine NullTerminated a
a) Putter -> Putter -> Putter
forall a. Semigroup a => a -> a -> a
<> forall a. Put a => a -> Putter
put @Word8 Word8
0x00

-- | We may parse any null-terminated data using a special flatparse combinator.
--
-- The combinator doesn't permit distinguishing between the two possible
-- failures: either there was no next null, or the inner parser didn't consume
-- up to it.
instance Get a => Get (NullTerminated a) where
    {-# INLINE get #-}
    get :: Getter (NullTerminated a)
get = a -> NullTerminated a
forall {k} a (p :: k). a -> Refined p a
unsafeRefine (a -> NullTerminated a)
-> ParserT PureMode (ParseError Pos Builder) a
-> Getter (NullTerminated a)
forall (f :: Type -> Type) a b. Functor f => (a -> b) -> f a -> f b
<$> ParserT PureMode (ParseError Pos Builder) a
-> [Builder] -> ParserT PureMode (ParseError Pos Builder) a
forall (st :: ZeroBitType) text a.
ParserT st (ParseError Pos text) a
-> [text] -> ParserT st (ParseError Pos text) a
cut1 (ParserT PureMode (ParseError Pos Builder) a
-> ParserT PureMode (ParseError Pos Builder) a
forall (st :: ZeroBitType) e a. ParserT st e a -> ParserT st e a
FP.isolateToNextNull ParserT PureMode (ParseError Pos Builder) a
forall a. Get a => Getter a
get) [Builder]
e
      where e :: [Builder]
e = [ Builder
"while isolating to next null"
                , Builder
"either there was no next null in the input,"
                , Builder
"or the inner parser didn't fully consume its input" ]

{-
I don't know how to do @[a]@. Either I nullterm each element, which is weird
because it's not required in all cases, or I don't, in which case the general
Put doesn't work. Nullterming every element feels weird anyway -- what about
[Word8]?

instance NullCheck a => NullCheck [a] where
    {-# INLINE hasNoNulls #-}
    hasNoNulls = all hasNoNulls
instance NullCheck Word8 where
    {-# INLINE hasNoNulls #-}
    hasNoNulls = \case 0x00 -> False
                       _    -> True
-}