packages feed

ktx-codec-0.0.2.0: src/Codec/Ktx/KeyValue.hs

module Codec.Ktx.KeyValue
  ( KeyValueData

    -- * Writing
  , Value(..)
  , text
  , bytes
  , number

    -- * Reading
  , FromValue(..)
  , textual

    -- * Binary operations
  , getDataLe
  , getData

  , putDataLe
  , putData
  ) where

import Data.Binary.Get (Get, getWord32le, getByteString, isolate, skip)
import Data.Binary.Put (Put, putByteString, putWord32le)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Foldable (for_)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as Text (pack)
import Data.Text.Encoding qualified as Text
import Data.Word (Word32)
import GHC.Generics (Generic)

type KeyValueData = Map Text Value

{- | A wrapper for raw data.

Use "FromValue"/"ToValue" to process.
-}
newtype Value = Value ByteString
  deriving (Eq, Show, Generic)

class FromValue a where
  fromValue :: Value -> Maybe a

instance FromValue Text where
  fromValue (Value bs)
    | BS.null bs =
        Nothing
    | BS.last bs == 0x00 =
        either (const Nothing) Just $
          Text.decodeUtf8' $ BS.init bs
    | otherwise =
        Nothing

instance FromValue ByteString where
  fromValue (Value bs) = Just bs

text :: Text -> Value
text t = Value $ BS.snoc (Text.encodeUtf8 t) 0x00

bytes :: ByteString -> Value
bytes = Value

number :: (Num a, Show a) => a -> Value
number = text . Text.pack . show

-- | Extract all valid (null-terminated utf8) values.
textual :: KeyValueData -> Map Text Text
textual = Map.mapMaybe fromValue

{-# INLINE getDataLe #-}
getDataLe :: Int -> Get KeyValueData
getDataLe = getData getWord32le

getData :: Get Word32 -> Int -> Get KeyValueData
getData getSize bytesOfKeyValueData =
  isolate bytesOfKeyValueData $
    go bytesOfKeyValueData []
  where
    go remains acc
      | remains == 0 =
          pure $ Map.fromList acc

      | remains < 0 =
          fail "Attempted to read beyond bytesOfKeyValueData"

      | otherwise = do
          keyAndValueByteSize <- fmap fromIntegral getSize
          let paddingSize = 3 - ((keyAndValueByteSize + 3) `rem` 4)

          keyAndValue <- getByteString keyAndValueByteSize
          skip paddingSize

          {- XXX: Spec says:
              Any byte value is allowed.
              It is encouraged that the value be a NUL terminated UTF-8 string but this is not required.
              If the Value data is a string of bytes then the NUL termination
              should be included in the keyAndValueByteSize byte count
              (but programs that read KTX files must not rely on this).
          -}
          let
            (keyBS, valueBS) = BS.span (/= 0x00) keyAndValue
            key = Text.decodeUtf8 keyBS
            value = Value $ BS.drop 1 valueBS

          go
            (remains - keyAndValueByteSize - 4 - paddingSize)
            ((key, value) : acc)

{-# INLINE putDataLe #-}
putDataLe :: KeyValueData -> Put
putDataLe = putData putWord32le

putData :: (Word32 -> Put) -> KeyValueData -> Put
putData putSize kvs =
  for_ (Map.toList kvs) \(key, Value value) -> do
    let
      keyAndValue = mconcat [Text.encodeUtf8 key, BS.singleton 0x00, value]
      keyAndValueByteSize = BS.length keyAndValue
      paddingSize = 3 - ((keyAndValueByteSize + 3) `rem` 4)

    putSize (fromIntegral keyAndValueByteSize)
    putByteString keyAndValue
    putByteString $ BS.replicate paddingSize 0