packages feed

octane-0.5.2: library/Octane/Type/Primitive/PCString.hs

{-# LANGUAGE DeriveGeneric #-}

module Octane.Type.Primitive.PCString (PCString(..)) where

import qualified Control.DeepSeq as DeepSeq
import qualified Control.Newtype as Newtype
import qualified Data.Aeson as Aeson
import qualified Data.Binary as Binary
import qualified Data.Binary.Get as Binary
import qualified Data.Binary.Put as Binary
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Char as Char
import Data.Function ((&))
import qualified Data.String as String
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Encoding
import qualified GHC.Generics as Generics
import qualified Octane.Type.Primitive.Word32LE as Word32LE

-- | A length-prefixed null-terminated string.
newtype PCString =
    PCString Text.Text
    deriving (Eq,Generics.Generic,Ord,Show)

instance Binary.Binary PCString where
    get = do
        (Word32LE.Word32LE rawSize) <- Binary.get
        -- In some tiny percentage of replays, this nonsensical string size
        -- shows up. As far as I can tell the next 3 bytes are always null. And
        -- the actual string is "None", which is 5 bytes including the null
        -- terminator.
        --
        -- These annoying replays come from around 2015-10-25 to 2015-11-01.
        size <- if rawSize == 0x05000000
            then do
                bytes <- Binary.getByteString 3
                if BS.all (== 0) bytes
                    then return 5
                    else error
                        ( "read special size "
                        ++ show rawSize
                        ++ " but next 3 bytes were "
                        ++ show bytes
                        ++ " instead of all null"
                        )
            else return rawSize
        string <-
            if size == 0
                then fail ("invalid PCString size " ++ show size)
                else if size < 0
                         then do
                             let actualSize = 2 * negate size
                             bytes <-
                                 Binary.getByteString (fromIntegral actualSize)
                             bytes & Encoding.decodeUtf16LE & return
                         else do
                             bytes <- Binary.getByteString (fromIntegral size)
                             bytes & Encoding.decodeLatin1 & return
        string & Text.dropEnd 1 & Newtype.pack & return
    put string = do
        let cString = string & Newtype.unpack & flip Text.snoc '\NUL'
        let size = cString & Text.length & fromIntegral
        if Text.all Char.isLatin1 cString
            then do
                size & Word32LE.Word32LE & Binary.put
                cString & encodeLatin1 & Binary.putByteString
            else do
                size & negate & Word32LE.Word32LE & Binary.put
                cString & Encoding.encodeUtf16LE & Binary.putByteString

instance String.IsString PCString where
    fromString string = string & Text.pack & Newtype.pack

instance Newtype.Newtype PCString

instance DeepSeq.NFData PCString

instance Aeson.ToJSON PCString

encodeLatin1 :: Text.Text -> BS.ByteString
encodeLatin1 text = text & Text.unpack & BS8.pack