packages feed

cmph-0.0.1: Data/CMPH.hs

{-# LANGUAGE EmptyDataDecls           #-}
{-# LANGUAGE FlexibleContexts         #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE OverloadedStrings        #-}


module Data.CMPH ( CMPH, hash, buildHash, size ) where
import           Control.Exception      (bracket)
import           Control.Monad          (guard, liftM)
import           Data.Array
import           Data.Array.IO          (IOArray, newArray, newArray_,
                                         writeArray)
import           Data.Array.Storable    (StorableArray, withStorableArray)
import           Data.Array.Unsafe      (unsafeFreeze)
import qualified Data.ByteString        as BS
import qualified Data.ByteString.Char8  as S
import qualified Data.ByteString.Unsafe as Unsafe
import           Data.Monoid
import qualified Data.Set               as Set
import           Data.Word
import           Foreign                (Ptr)
import           Foreign.C.String       (CString)
import           Foreign.C.Types
import           Foreign.Marshal.Array  ()
import           GHC.Arr                (unsafeAt)
import           Prelude                hiding (lookup)
import           System.IO.Unsafe       (unsafePerformIO)

data ForeignHash

foreign import ccall unsafe "stub.h cmph_search" c_cmph_search
  :: Ptr ForeignHash -> CString -> CUInt -> IO CULong
foreign import ccall unsafe "stub.h build_hash"  c_build_hash
  :: Ptr CString -> CUInt -> IO (Ptr ForeignHash)
foreign import ccall unsafe "stub.h free_ptrs"   c_free_ptrs
  :: CUInt -> Ptr CString -> IO ()
foreign import ccall unsafe "string.h strdup" c_strdup
  :: CString -> IO CString

data CMPH = CMPH { rawHash :: Ptr ForeignHash
                 , size    :: Word32
                 }

-- we pull a few dumb tricks to get around some partiality in libcmph.
--
--   no empty strings allowed
--     so prepend 'a' to all strings
--   can't hash an empty list
--     so add an element to empty lists.
--   no repeats
--     strip them out before passing to the c lib
--   can't have embedded nulls in the inserted strings
--     return Nothing. Too hard.

buildHash :: [BS.ByteString] -> IO (Maybe CMPH)
buildHash input'
  | all noNull input' = do
      let input = uniqued ("":"b":input')
      let len = fromIntegral $ length input
      raw <-
        bracket (buildCPtrs (map ("a"<>) input))
                (freeCPtrs len)
                (\a -> withStorableArray a (`c_build_hash` (CUInt len)))
      return $ Just $ CMPH raw len
  | otherwise = return Nothing

uniqued :: Ord a => [a] -> [a]
uniqued = Set.toList . Set.fromList

noNull :: BS.ByteString -> Bool
noNull = (==Nothing) . S.find (=='\NUL')

hash :: CMPH-> BS.ByteString -> IO Word64
hash cmph bs = do
  (CULong w) <- S.useAsCStringLen ("a"<>bs) $ \(cstr,len) -> c_cmph_search (rawHash cmph) cstr (CUInt $ fromIntegral len)
  return w

freeCPtrs :: Word32
          -> StorableArray Int CString
          -> IO ()
freeCPtrs len arr = withStorableArray arr $ c_free_ptrs (CUInt len)

buildCPtrs bs = do
  buffer <- newArray_ (0,length bs-1)
  (`mapM_` (zip [0..] bs)) $ \(i,bs) ->
    S.useAsCStringLen bs $ \(cstr,len) -> do
      writeArray buffer i =<< c_strdup cstr
  return buffer