EtaMOO-0.1.0.0: src/MOO/Builtins/Match.hsc
{-# LANGUAGE ForeignFunctionInterface, EmptyDataDecls #-}
-- | Regular expression matching via PCRE through the FFI
module MOO.Builtins.Match (
Regexp
, MatchResult(..)
-- ** Compiling
, newRegexp
-- ** Matching
, match
, rmatch
) where
import Foreign hiding (unsafePerformIO)
import Foreign.C
import Control.Monad
import Control.Exception
import Control.Concurrent.MVar
import Data.Text (Text)
import Data.Text.Encoding
import Data.ByteString (ByteString, useAsCString, useAsCStringLen)
import Data.IORef
import System.IO.Unsafe (unsafePerformIO)
import qualified Data.Text as T
import qualified Data.ByteString as BS
{-# ANN module ("HLint: ignore Avoid lambda" :: String) #-}
#include <pcre.h>
data PCRE
data PCREExtra
data PCRECalloutBlock
data CharacterTables
type Callout = Ptr PCRECalloutBlock -> IO CInt
foreign import ccall unsafe "static pcre.h"
pcre_compile :: CString -> CInt -> Ptr CString -> Ptr CInt ->
Ptr CharacterTables -> IO (Ptr PCRE)
foreign import ccall unsafe "static pcre.h"
pcre_study :: Ptr PCRE -> CInt -> Ptr CString -> IO (Ptr PCREExtra)
foreign import ccall unsafe "static pcre.h &"
pcre_free_study :: FunPtr (Ptr PCREExtra -> IO ())
foreign import ccall safe "static pcre.h"
pcre_exec :: Ptr PCRE -> Ptr PCREExtra -> CString -> CInt -> CInt ->
CInt -> Ptr CInt -> CInt -> IO CInt
foreign import ccall unsafe "static pcre.h &"
pcre_free :: Ptr (FunPtr (Ptr a -> IO ()))
foreign import ccall "static pcre.h &"
pcre_callout :: Ptr (FunPtr Callout)
foreign import ccall "wrapper"
mkCallout :: Callout -> IO (FunPtr Callout)
data Regexp = Regexp {
pattern :: Text
, caseMatters :: Bool
, code :: ForeignPtr PCRE
, extra :: ForeignPtr PCREExtra
}
deriving Show
instance Eq Regexp where
Regexp { pattern = p1, caseMatters = cm1 } ==
Regexp { pattern = p2, caseMatters = cm2 } =
cm1 == cm2 && p1 == p2
data RewriteState = StateBase
| StateEsc
| StateCsetInit
| StateCsetInit2
| StateCset
{-# ANN translate ("HLint: ignore Use list literal" :: String) #-}
-- | Translate MOO regular expression syntax into PCRE syntax.
--
-- Aside from changing % to \ and sundry tweaks we also address an
-- incompatibility between MOO %b, %B, %w, %W and PCRE \b, \B, \w, \W --
-- namely, the inclusion of _ in \w and its absence in %w.
translate :: Text -> Text
translate = T.pack . concat . rewrite . T.unpack
where
-- wrap entire expression so we can add a callout at the end
rewrite s = "(?:" : rewrite' StateBase s
rewrite' StateBase ('%':cs) = rewrite' StateEsc cs
rewrite' StateBase ('[':cs) = "[" : rewrite' StateCsetInit cs
rewrite' StateBase ( c :cs)
| c `elem` "\\|(){" = "\\" : [c] : rewrite' StateBase cs
| otherwise = [c] : rewrite' StateBase cs
rewrite' StateCsetInit ('^':cs) = "^" : rewrite' StateCsetInit2 cs
rewrite' StateCsetInit ( c :cs)
| c `elem` "\\[" = "\\" : [c] : rewrite' StateCset cs
| otherwise = [c] : rewrite' StateCset cs
rewrite' StateCsetInit2 (c:cs)
| c `elem` "\\[" = "\\" : [c] : rewrite' StateCset cs
| otherwise = [c] : rewrite' StateCset cs
rewrite' StateCset (']':cs) = "]" : rewrite' StateBase cs
rewrite' StateCset ( c :cs)
| c `elem` "\\[" = "\\" : [c] : rewrite' StateCset cs
| otherwise = [c] : rewrite' StateCset cs
-- insert a null-op (comment) to prevent special sequences
rewrite' StateEsc ('(':cs) = "((?#)" : rewrite' StateBase cs
rewrite' StateEsc ('b':cs) = alt wordBegin wordEnd
: rewrite' StateBase cs
rewrite' StateEsc ('B':cs) = alt (lookba word word)
(lookba nonword nonword)
: rewrite' StateBase cs
rewrite' StateEsc ('<':cs) = wordBegin : rewrite' StateBase cs
rewrite' StateEsc ('>':cs) = wordEnd : rewrite' StateBase cs
rewrite' StateEsc ('w':cs) = word : rewrite' StateBase cs
rewrite' StateEsc ('W':cs) = nonword : rewrite' StateBase cs
rewrite' StateEsc ( c :cs)
| c `elem` ['1'..'9'] = "\\" : [c] : "(?#)"
: rewrite' StateBase cs
| c `elem` "\\^$.[?*+{" = "\\" : [c] : rewrite' StateBase cs
| otherwise = [c] : rewrite' StateBase cs
-- add callout at end of pattern for rmatch
rewrite' state [] = ")(?C)" : rewriteFinal state
-- don't let a trailing % get away without a syntax error
rewriteFinal StateEsc = "\\" : []
rewriteFinal _ = []
word = "[^\\W_]"
nonword = "[\\W_]"
alt a b = "(?:" ++ a ++ "|" ++ b ++ ")"
lbehind p = "(?<=" ++ p ++ ")"
lahead p = "(?=" ++ p ++ ")"
lookba b a = lbehind b ++ lahead a
wordBegin = alt "^" (lbehind nonword) ++ lahead word
wordEnd = lbehind word ++ alt "$" (lahead nonword)
-- | @newRegexp@ /regexp/ /case-matters/ compiles a regular expression pattern
-- /regexp/ into a 'Regexp' value, or returns an error description if the
-- pattern is malformed. The returned 'CInt' is a byte offset into an
-- internally translated pattern, and thus is probably not very useful.
newRegexp :: Text -> Bool -> IO (Either (String, CInt) Regexp)
newRegexp regexp caseMatters =
useAsCString (encodeUtf8 $ translate regexp) $ \pattern ->
alloca $ \errorPtr ->
alloca $ \errorOffsetPtr -> do
code <- pcre_compile pattern options errorPtr errorOffsetPtr nullPtr
if code == nullPtr
then do error <- peek errorPtr >>= peekCString
errorOffset <- peek errorOffsetPtr
return $ Left (patchError error, errorOffset)
else do extraFP <- mkExtra code
setExtraFlags extraFP
codeFP <- peek pcre_free >>= flip newForeignPtr code
return $ Right Regexp { pattern = regexp
, caseMatters = caseMatters
, code = codeFP
, extra = extraFP
}
where
mkExtra code = alloca $ \errorPtr -> do
extra <- pcre_study code 0 errorPtr
if extra == nullPtr
then do extraFP <- mallocForeignPtrBytes #{const sizeof(pcre_extra)}
withForeignPtr extraFP $ \extra ->
#{poke pcre_extra, flags} extra (0 :: CULong)
return extraFP
else newForeignPtr pcre_free_study extra
setExtraFlags extraFP = withForeignPtr extraFP $ \extra -> do
#{poke pcre_extra, match_limit} extra matchLimit
#{poke pcre_extra, match_limit_recursion} extra matchLimitRecursion
flags <- #{peek pcre_extra, flags} extra
#{poke pcre_extra, flags} extra $ flags .|. (0 :: CULong)
.|. #{const PCRE_EXTRA_MATCH_LIMIT}
.|. #{const PCRE_EXTRA_MATCH_LIMIT_RECURSION}
matchLimit = 100000 :: CULong
matchLimitRecursion = 5000 :: CULong
patchError = concatMap patch
where patch '\\' = "%"
patch '(' = "%("
patch ')' = "%)"
patch c = [c]
options = #{const PCRE_UTF8 | PCRE_NO_UTF8_CHECK}
-- allow PCRE to optimize .* at beginning of pattern by implicit anchor
.|. #{const PCRE_DOTALL}
.|. if caseMatters then 0 else #{const PCRE_CASELESS}
maxCaptures = 10
ovecLen = maxCaptures * 3
data MatchResult = MatchFailed
| MatchAborted
| MatchSucceeded [(Int, Int)]
deriving Show
-- We need a lock to protect pcre_callout which is shared by all threads
matchLock :: MVar ()
matchLock = unsafePerformIO $ newMVar ()
{-# NOINLINE matchLock #-}
match :: Regexp -> Text -> IO MatchResult
match Regexp { code = codeFP, extra = extraFP } text =
bracket (takeMVar matchLock) (putMVar matchLock) $ \_ ->
withForeignPtr codeFP $ \code ->
withForeignPtr extraFP $ \extra ->
useAsCStringLen string $ \(cstring, len) ->
allocaArray ovecLen $ \ovec -> do
flags <- #{peek pcre_extra, flags} extra
#{poke pcre_extra, flags} extra $ flags .&. complement (0 :: CULong)
.&. complement #{const PCRE_EXTRA_CALLOUT_DATA}
poke pcre_callout nullFunPtr
rc <- pcre_exec code extra cstring (fromIntegral len) 0 options
ovec (fromIntegral ovecLen)
if rc < 0
then case rc of
#{const PCRE_ERROR_NOMATCH} -> return MatchFailed
_ -> return MatchAborted
else mkMatchResult rc ovec subject
where string = encodeUtf8 text
subject = (string, T.length text)
options = #{const PCRE_NO_UTF8_CHECK}
rmatch :: Regexp -> Text -> IO MatchResult
rmatch Regexp {code = codeFP, extra = extraFP } text =
bracket (takeMVar matchLock) (putMVar matchLock) $ \_ ->
withForeignPtr codeFP $ \code ->
withForeignPtr extraFP $ \extra ->
useAsCStringLen string $ \(cstring, len) ->
allocaArray ovecLen $ \ovec ->
allocaArray ovecLen $ \rOvec -> do
rdRef <- newIORef RmatchData { rmatchResult = 0, rmatchOvec = rOvec }
bracket (newStablePtr rdRef) freeStablePtr $ \sp -> do
#{poke pcre_extra, callout_data} extra sp
flags <- #{peek pcre_extra, flags} extra
#{poke pcre_extra, flags} extra $ flags .|. (0 :: CULong)
.|. #{const PCRE_EXTRA_CALLOUT_DATA}
bracket (mkCallout rmatchCallout) freeHaskellFunPtr $ \callout -> do
poke pcre_callout callout
rc <- pcre_exec code extra cstring (fromIntegral len) 0 options
ovec (fromIntegral ovecLen)
if rc < 0
then case rc of
#{const PCRE_ERROR_NOMATCH} -> do
rd <- readIORef rdRef
if valid rd
then mkMatchResult (rmatchResult rd) (rmatchOvec rd) subject
else return MatchFailed
_ -> return MatchAborted
else mkMatchResult rc ovec subject
where string = encodeUtf8 text
subject = (string, T.length text)
options = #{const PCRE_NO_UTF8_CHECK}
mkMatchResult :: CInt -> Ptr CInt -> (ByteString, Int) -> IO MatchResult
mkMatchResult rc ovec (subject, subjectCharLen) =
(MatchSucceeded . pairs . map (rebase . fromIntegral)) `liftM`
peekArray (n * 2) ovec
where rc' = fromIntegral rc
n = if rc' == 0 || rc' > maxCaptures then maxCaptures else rc'
pairs (s:e:rs) = (s, e) : pairs rs
pairs [] = []
-- translate UTF-8 byte offset to character offset
rebase 0 = 0
rebase i = subjectCharLen - T.length (decodeUtf8 $ BS.drop i subject)
data RmatchData = RmatchData {
rmatchResult :: CInt
, rmatchOvec :: Ptr CInt
}
valid :: RmatchData -> Bool
valid RmatchData { rmatchResult = rc } = rc /= 0
rmatchCallout :: Callout
rmatchCallout block = do
rdRef <- deRefStablePtr =<< #{peek pcre_callout_block, callout_data} block
rd <- readIORef rdRef
currentPos <- #{peek pcre_callout_block, current_position} block
startMatch <- #{peek pcre_callout_block, start_match} block
let ovec = rmatchOvec rd
ovec0 <- peekElemOff ovec 0
ovec1 <- peekElemOff ovec 1
when (not (valid rd) || startMatch > ovec0 ||
(startMatch == ovec0 && currentPos > ovec1)) $ do
-- make a copy of the offsets vector so the last such vector found can
-- be returned as the rightmost match
pokeElemOff ovec 0 startMatch
pokeElemOff ovec 1 currentPos
offsetVector <- #{peek pcre_callout_block, offset_vector} block
captureTop <- #{peek pcre_callout_block, capture_top} block
copyArray (ovec `advancePtr` 2)
(offsetVector `advancePtr` 2)
(sizeOf ovec0 * 2 * (fromIntegral captureTop - 1))
writeIORef rdRef rd { rmatchResult = captureTop }
return 1 -- cause match failure at current point, but continue trying