packages feed

bytepatch-0.4.1: src/StreamPatch/Patch/Compare.hs

{- TODO
  * Via -> Strategy (? I do like the brevity of Via)
  * Rename Compare (but not sure what to...)
-}

{-# LANGUAGE AllowAmbiguousTypes, UndecidableInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

module StreamPatch.Patch.Compare where

import GHC.Generics ( Generic )
import Data.Data ( Typeable, Data )

import Numeric.Natural
import Data.ByteString qualified as BS
import Data.Text qualified as Text
import Data.Text ( Text )

import Data.Void
import Control.Monad ( void )

import GHC.Exts ( Proxy#, proxy# )
import GHC.TypeLits ( KnownSymbol, symbolVal' )

import Binrep.Extra.HexByteString

import Data.Aeson qualified as Aeson
import Data.Aeson ( ToJSON(..), FromJSON(..) )

import Text.Megaparsec
import Text.Megaparsec.Char qualified as MC
import Text.Megaparsec.Char.Lexer qualified as MCL

import BLAKE3 qualified as B3
import Data.ByteArray.Sized qualified as BA
import Data.ByteString qualified as B
import Data.Word ( Word8 )

import Data.Singletons.TH
-- required for deriving instances (seems like bug)
import Prelude.Singletons hiding ( AbsSym0, Compare )
import Data.Singletons.Base.TH ( FromString, sFromString )

$(singletons [d|
    -- | What sort of equality check to do.
    data EqualityCheck
      = Exact -- ^ "Exact equality" is defined as whatever the 'Eq' class does. (lol)
      | PrefixOf
        deriving stock (Show, Eq)
    |])
deriving stock instance Generic  EqualityCheck
deriving stock instance Typeable EqualityCheck
deriving stock instance Data     EqualityCheck

$(singletons [d|
    data HashFunc
      = B3
      | SHA256
      | MD5
        deriving stock (Show, Eq)
    |])
deriving stock instance Generic  HashFunc
deriving stock instance Typeable HashFunc
deriving stock instance Data     HashFunc

$(singletons [d|
    -- | How should we compare two values?
    data Via
      -- | Are they equal in some way?
      = ViaEq EqualityCheck

      -- | Do they have the same size?
      | ViaSize

      -- | Do they have the same digest under the given hash function?
      | ViaDigest HashFunc
        deriving stock (Show, Eq)
    |])
deriving stock instance Generic  Via
deriving stock instance Typeable Via
deriving stock instance Data     Via

data Meta (v :: Via) a = Meta
  { mCompare :: Maybe (CompareRep v a)
  } deriving stock (Generic)

deriving stock instance Eq   (CompareRep v a) => Eq   (Meta v a)
deriving stock instance Show (CompareRep v a) => Show (Meta v a)

deriving anyclass instance ToJSON   (CompareRep v a) => ToJSON   (Meta v a)
deriving anyclass instance FromJSON (CompareRep v a) => FromJSON (Meta v a)

instance SingI v => Functor     (Meta v) where
    fmap f (Meta c) = case sing @v of
      SViaEq     _ -> Meta $ fmap f c
      SViaSize     -> Meta c
      SViaDigest _ -> Meta c

instance SingI v => Foldable    (Meta v) where
    foldMap f (Meta c) = case sing @v of
      SViaEq     _ -> foldMap f c
      SViaSize     -> mempty
      SViaDigest _ -> mempty

instance SingI v => Traversable (Meta v) where
    traverse f (Meta c) = case sing @v of
      SViaEq     _ -> Meta <$> traverse f c
      SViaSize     -> pure $ Meta c
      SViaDigest _ -> pure $ Meta c

type family CompareRep (v :: Via) a where
    CompareRep ('ViaEq _)     a = a
    CompareRep 'ViaSize       _ = Natural
    CompareRep ('ViaDigest h) _ = Digest h B.ByteString

-- | The resulting digest from hashing some data using the given hash function.
--
-- TODO
-- As of 2022, most good cryptographic hash functions produce digest sizes
-- between 256-512 bits. That's 32-64 bytes. So I want to use a ShortByteString,
-- but the BLAKE3 library uses the memory library, which I can't figure out. I
-- bet it'd be more efficient. So, I'm polymorphising in preparation.
newtype Digest (h :: HashFunc) a = Digest { getDigest :: a }
    deriving stock (Generic, Eq, Show)

type Digest' h = Digest h B.ByteString

type family HashFuncLabel (h :: HashFunc) where
    HashFuncLabel 'B3     = "b3"
    HashFuncLabel 'SHA256 = "sha256"
    HashFuncLabel 'MD5    = "md5"

hashFuncLabel :: forall h l. (l ~ HashFuncLabel h, KnownSymbol l) => Text
hashFuncLabel = Text.pack (symbolVal' (proxy# :: Proxy# l))

-- | Add a @digest:@ prefix to better separate from regular text.
instance (l ~ HashFuncLabel h, KnownSymbol l) => ToJSON   (Digest h B.ByteString) where
    toJSON = Aeson.String . Text.append "digest:" . prettyDigest B.unpack

instance (l ~ HashFuncLabel h, KnownSymbol l) => FromJSON (Digest h B.ByteString) where
    parseJSON = Aeson.withText "hex hash digest" $ \t ->
        case parseMaybe @Void parseDigest t of
          Nothing   -> fail "failed to parse hex hash digest"
          Just hash -> pure hash

-- | Pretty print a hash like @hashfunc:123abc@.
prettyDigest
    :: forall h a l. (l ~ HashFuncLabel h, KnownSymbol l)
    => (a -> [Word8]) -> Digest h a -> Text
prettyDigest unpack (Digest d) =
       hashFuncLabel @h
    <> Text.singleton ':'
    <> prettyHexByteStringCompact unpack d

-- Bad naming, these maybe aren't symbols/lexemes in the expected sense
-- (horizontal spacing is optional).
parseDigest
    :: forall h l e s m
    .  (l ~ HashFuncLabel h, KnownSymbol l, MonadParsec e s m, Token s ~ Char, Tokens s ~ Text)
    => m (Digest h B.ByteString)
parseDigest = do
    symbol "digest"
    symbol ":"
    symbol $ hashFuncLabel @h
    symbol ":"
    Digest <$> parseHexByteString B.pack
  where symbol = void . MCL.lexeme MC.hspace . chunk

class Compare (v :: Via) a where
    compare'     :: CompareRep v a -> CompareRep v a -> Maybe String
    toCompareRep :: a -> CompareRep v a

compareTo :: forall v a. Compare v a => CompareRep v a -> a -> Maybe String
compareTo cmp = compare' @v @a cmp . toCompareRep @v @a

-- Free instance: Compare for exact equality via 'Eq'.
-- TODO show is bandaid. no need to handle it here.
instance (Eq a, Show a) => Compare ('ViaEq 'Exact) a where
    toCompareRep = id
    compare' c1 c2 | c1 == c2  = Nothing
                   | otherwise = Just $ "values not equal: "<>show c1<>" /= "<>show c2

instance Compare ('ViaDigest 'B3) BS.ByteString where
    toCompareRep = Digest . hashB3
    compare' c1 c2 | c1 == c2  = Nothing
                   | otherwise = Just "digests not equal"

-- TODO I need to define the compare class better lol, I'm confused which is
-- real and which is test.
instance Compare ('ViaEq 'PrefixOf) BS.ByteString where
    toCompareRep = id
    compare' c1 c2 | c2 `B.isPrefixOf` c1 = Nothing
                   | otherwise = Just $ "prefix compare check fail: "<>show c1<>" vs. "<>show c2

-- I unpack to '[Word8]' then repack to 'ByteString' because the memory library
-- is very keen on complicated unsafe IO. cheers no thanks
hashB3 :: BS.ByteString -> BS.ByteString
hashB3 bs = BA.unSizedByteArray $ B3.hash @B3.DEFAULT_DIGEST_LEN Nothing [bs]

class SwapCompare a (vFrom :: Via) (vTo :: Via) where
    swapCompare :: CompareRep vFrom a -> Either String (CompareRep vTo a)

instance SwapCompare a v v where
    swapCompare = Right

instance SwapCompare BS.ByteString ('ViaEq 'Exact) ('ViaDigest 'B3) where
    swapCompare = Right . Digest . hashB3