{-# OPTIONS_GHC -fbang-patterns #-} -- | -- Module : Data.ByteString.Seach.BoyerMoore -- Copyright : Daniel Fischer -- Chris Kuklewicz -- License : BSD3 -- Maintainer : Bryan O'Sullivan -- Stability : experimental -- Portability : portable -- -- Fast overlapping Boyer-Moore search of both strict and lazy -- 'S.ByteString' values. -- -- Descriptions of the algorithm can be found at -- -- and -- -- -- Original authors: Daniel Fischer (daniel.is.fischer at web.de) and -- Chris Kuklewicz (haskell at list.mightyreason.com). module Data.ByteString.Search.BoyerMoore ( -- * Overview -- $overview -- ** Parameter and return types -- $types -- ** Lazy ByteStrings -- $lazy -- ** Performance -- $performance -- ** Complexity -- $complexity -- ** Currying -- $currying -- ** Integer overflow -- $overflow -- * Functions matchLL , matchLS , matchSL , matchSS ) where import qualified Data.ByteString as S (ByteString,null,length,concat) import qualified Data.ByteString.Lazy as L (ByteString,toChunks) import qualified Data.ByteString.Base as B (unsafeIndex) import Data.Array.Base (unsafeAt,unsafeRead,unsafeWrite) import Data.Array.ST (newArray,newArray_,runSTUArray) import Data.Array.IArray (array,accumArray) import Data.Array.Unboxed (UArray) import Data.Word (Word8) import Data.Int (Int64) -- $overview -- -- This module exports 4 search functions: 'matchLL', 'matchLS', -- 'matchSL', and 'matchSS'. -- -- If given an empty pattern, a search will always return an empty -- list. -- $types -- -- The first parameter is always the pattern string. The second -- parameter is always the target string to be searched. The returned -- list contains the offsets of all /overlapping/ patterns. -- -- A returned @Int@ or @Int64@ is an index into the target string -- which is aligned to the head of the pattern string. Strict targets -- return @Int@ indices and lazy targets return @Int64@ indices. All -- returned lists are computed and returned in a lazy fashion. -- $lazy -- -- 'matchLL' and 'matchLS' take lazy bytestrings as patterns. For -- performance, if the pattern is not a single strict chunk then all -- the the pattern chunks will copied into a concatenated strict -- bytestring. This limits the patterns to a length of (maxBound :: -- Int). -- -- 'matchLL' and 'matchSL' take lazy bytestrings as targets. -- These are written so that while they work they will not retain a -- reference to all the earlier parts of the the lazy bytestring. -- This means the garbage collector would be able to keep only a small -- amount of the target string and free the rest. -- $currying -- These functions can all be usefully curried. Given only a pattern -- the curried version will compute the supporting lookup tables only -- once, allowing for efficient re-use. Similarly, the curried -- 'matchLL' and 'matchLS' will compute the concatenated pattern only -- once. -- $complexity -- -- Preprocessing the pattern string is O(@patternLength@). The search -- performance is O(@targetLength@\/@patternLength@) in the best case, -- allowing it to go faster than a Knuth-Morris-Pratt algorithm. With -- a non-periodic pattern the worst case uses O(3\*@targetLength@) -- comparisons. The periodic pattern worst case is quadratic -- O(@targetLength@\*@patternLength@) complexity. Improvements -- (e.g. Turbo-Boyer-Moore) to catch and linearize worst case -- performance slow down the loop significantly. -- $performance -- -- Operating on a strict target string is faster than a lazy target -- string. It is unclear why the performance gap is as large as it is -- (patches welcome). To slightly ameliorate this, if the lazy string -- is a single chunk then a copy of the strict algorithm is used. -- $overflow -- -- The current code uses @Int@ to keep track of the locations in the -- target string. If the length of the pattern plus the length of any -- strict chunk of the target string is greater or equal to -- @'maxBound'::Int@ then this will overflow causing an error. We try -- to detect this and call 'error' before a segfault occurs. {-# INLINE matchLL #-} matchLL :: L.ByteString -- ^ lazy pattern -> L.ByteString -- ^ lazy target string -> [Int64] -- ^ offsets of matches matchLL pat = let search = matchSSsd (S.concat (L.toChunks pat)) in search . L.toChunks {-# INLINE matchLS #-} matchLS :: L.ByteString -- ^ lazy pattern -> S.ByteString -- ^ strict target string -> [Int] -- ^ offsets of matches matchLS pat = matchSSd (S.concat (L.toChunks pat)) {-# INLINE matchSL #-} matchSL :: S.ByteString -- ^ strict pattern -> L.ByteString -- ^ lazy target string -> [Int64] -- ^ offsets of matches matchSL pat = let search = matchSSsd pat in search . L.toChunks {-# INLINE matchSS #-} matchSS :: S.ByteString -- ^ strict pattern -> S.ByteString -- ^ strict target string -> [Int] -- ^ offsets of matches matchSS pat = matchSSd pat #ifndef __HADDOCK__ matchSSd :: S.ByteString -> S.ByteString -> [Int] matchSSd pat | S.null pat = const [] | otherwise = let !patLen = S.length pat !patEnd = pred patLen !maxStrLen = maxBound - patLen !occT = occurs pat -- used to compute bad-character shift !suffT = suffShifts pat -- used to compute good-suffix shift !skip = unsafeAt suffT 0 -- used after each matching position is found -- 0 < skip <= patLen {-# INLINE patAt #-} patAt :: Int -> Word8 patAt !i = B.unsafeIndex pat i searcher str | maxStrLen <= S.length str = error "Overflow error in BoyerMoore.matchSSd" | otherwise = let !strLen = S.length str !maxDiff = strLen-patLen {-# INLINE strAt #-} strAt :: Int -> Word8 strAt !i = B.unsafeIndex str i findMatch !diff !patI = case strAt (diff+patI) of c | c==patAt patI -> if patI == 0 then diff : let diff' = diff + skip in if maxDiff < diff' then [] else findMatch diff' patEnd else findMatch diff (pred patI) | otherwise -> let {-# INLINE badShift #-} badShift = patI - unsafeAt occT (fromIntegral c) -- (-patEnd) < badShift <= patLen {-# INLINE goodShift #-} goodShift = unsafeAt suffT patI -- 0 < goodShift <= patLen diff' = diff + max badShift goodShift in if maxDiff < diff' then [] else findMatch diff' patEnd in if maxDiff < 0 then [] else findMatch 0 patEnd in searcher #endif -- release is used to keep the zipper in matchSSs from remembering -- the leading part of the searched string. The deep parameter is the -- number of characters that the past needs to hold. This ensures -- lazy streaming consumption of the searched string. {-# INLINE release #-} release :: Int -> [S.ByteString] -> [S.ByteString] #ifndef __HADDOCK__ release !deep _ | deep <= 0 = [] release !deep (!x:xs) = let !rest = release (deep-S.length x) xs in x : rest release _ [] = error "BoyerMoore 'release' could not find enough past of length deep!" #endif matchSSsd :: S.ByteString -> [S.ByteString] -> [Int64] #ifndef __HADDOCK__ matchSSsd pat | S.null pat = const [] | otherwise = let !patLen = S.length pat !patEnd = pred patLen !occT = occurs pat -- used to compute bad-character shift !suffT = suffShifts pat -- used to compute good-suffix shift !skip = unsafeAt suffT 0 -- used after each matching position is found -- 0 < skip <= patLen {-# INLINE patAt #-} patAt :: Int -> Word8 patAt !i = B.unsafeIndex pat i searcher string = let -- seek is used to position the "zipper" of -- (past,str,future) to the correct S.ByteString to search -- with matcher. This is done by ensuring 0 <= strPos < -- strLen where (strPos == diffPos+patPos). Note that -- future is not a strict parameter. The character being -- compared will then be (strAt strPos) and (patAt -- patPos). Splitting this into specialized versions -- seems like going too, and is only useful if pat is -- close to (or larger than) the chunk size. seek :: Int64 -> [S.ByteString] -> S.ByteString -> [S.ByteString] -> Int -> Int -> [Int64] seek !prior !past !str future !diffPos !patPos | (diffPos+patPos) < 0 = {-# SCC "seek/past" #-} case past of [] -> error "seek back too far!" (h:t) -> let hLen = S.length h in seek (prior - fromIntegral hLen) t h (str:future) (diffPos + hLen) patPos | strLen <= (diffPos+patPos) = {-# SCC "seek/future" #-} case future of [] -> [] (h:t) -> let {-# INLINE prior' #-} prior' = prior + fromIntegral strLen !diffPos' = diffPos - strLen {-# INLINE past' #-} past' = release (-diffPos') (str:past) in if maxStrLen <= S.length h then error "Overflow in BoyerMoore.matchSSsd" else seek prior' past' h t diffPos' patPos | otherwise = {-# SCC "seek/str" #-} -- matcher is the tight loop that walks backwards from the end -- of the pattern checking for matching characters. The upper -- bound of strLen is checked only when strI is shifted -- upwards to strI'. The lower bound must be checked. let matcher !diff !patI = case strAt (diff+patI) of c | c==patAt patI -> if patI == 0 then prior + fromIntegral (diff+patI) : let !diff' = (diff+patI) + skip -- Assert : diff < diff' in if maxDiff < diff' then seek prior past str future diff' patEnd else if diff' < 0 then matcher diff' patEnd else matcherF diff' patEnd else if (diff+patI) == 0 -- diff < 0 means need to check underflow then seek prior past str future diff (pred patI) else matcher diff (pred patI) | otherwise -> let {-# INLINE badShift #-} badShift = patI - unsafeAt occT (fromIntegral c) -- (-patEnd) < badShift <= patLen {-# INLINE goodShift #-} goodShift = unsafeAt suffT patI -- 0 < goodShift <= patLen -- Assert : diff < diff' !diff' = diff + max badShift goodShift in if maxDiff < diff' then seek prior past str future diff' patEnd else if diff' < 0 then matcher diff' patEnd else matcherF diff' patEnd -- mathcherF only needs to check overflow since 0<=diff matcherF !diff !patI = case strAt (diff+patI) of c | c==patAt patI -> if patI == 0 then prior + fromIntegral (diff+patI) : let !diff' = (diff+patI) + skip -- Assert : diff < diff' in if maxDiff < diff' then seek prior past str future diff' patEnd else matcherF diff' patEnd else matcherF diff (pred patI) -- 0 <= diff means no need to check underflow | otherwise -> let {-# INLINE badShift #-} badShift = patI - unsafeAt occT (fromIntegral c) -- (-patEnd) < badShift <= patLen {-# INLINE goodShift #-} goodShift = unsafeAt suffT patI -- 0 < goodShift <= patLen -- Assert : diff < diff' !diff' = diff + max badShift goodShift in if maxDiff < diff' then seek prior past str future diff' patEnd else matcherF diff' patEnd in if diffPos < 0 then matcher diffPos patPos else matcherF diffPos patPos where !strLen = S.length str !maxDiff = strLen - patLen !maxStrLen = pred ((maxBound::Int) - patLen) {-# INLINE strAt #-} strAt :: Int -> Word8 strAt !i = B.unsafeIndex str i in case string of [] -> [] [str] -> -- Steal the quick findMatch from matchSSd for this case: let findMatch !diff !patI = case strAt (diff+patI) of c | c==patAt patI -> if patI == 0 then fromIntegral diff : let diff' = diff + skip in if maxDiff < diff' then [] else findMatch diff' patEnd else findMatch diff (pred patI) | otherwise -> let {-# INLINE badShift #-} badShift = patI - unsafeAt occT (fromIntegral c) -- (-patEnd) < badShift <= patLen {-# INLINE goodShift #-} goodShift = unsafeAt suffT patI -- 0 < goodShift <= patLen diff' = diff + max badShift goodShift in if maxDiff < diff' then [] else findMatch diff' patEnd !strLen = S.length str !maxDiff = strLen - patLen !maxStrLen = ((maxBound::Int) - patLen) {-# INLINE strAt #-} strAt :: Int -> Word8 strAt !i = B.unsafeIndex str i in if maxStrLen <= strLen then error "Overflow in BoyerMoore.matchSSsd" else findMatch 0 patEnd (str:future) -> if ((maxBound::Int) - patLen) <= S.length str then error "Overflow in BoyerMoore.matchSSsd" else seek 0 [] str future 0 patEnd in searcher #endif {- Format of bad character table generated by occurs: Index is good for Word8 / ASCII searching only. The last character (at the last index) in pat is ignored. Excluding that last element, the value is largest index of occurances of that Word8 in the pat. The default value for Word8's not in the pattern is (-1). Range of values: -1 <= value < length of pattern -} {-# INLINE occurs #-} occurs :: S.ByteString -> UArray Word8 Int #ifndef __HADDOCK__ occurs !pat | patEnd < 0 = emptyOccurs | otherwise = runSTUArray (do ar <- newArray (minBound,maxBound) (-1) let loop !i | i == patEnd = return ar | otherwise = do unsafeWrite ar (fromEnum $ pat `B.unsafeIndex` i) i loop (succ i) loop 0) where !patEnd = pred (S.length pat) #endif emptyOccurs :: UArray Word8 Int emptyOccurs = accumArray const (-1) (minBound,maxBound) [] {- Non ST variants of occurs occurs' :: S.ByteString -> UArray Word8 Int occurs' !pat = accumArray (flip const) (-1) (0,255) [ (pat `B.unsafeIndex` i, i) | i <- [0..pred (S.length pat)] ] occurs'' :: S.ByteString -> UArray Word8 Int occurs'' !pat = accumArray (flip const) (-1) (minBound,maxBound) $ zip (init $ S.unpack pat) [0..] -} {- suffLengths uses a ST array to allow for strict querying of previously filled in values durring the fill loops. Format for suffLengths array: Valid index range is the same as for the pat. The value at index k is used when there is a mismatch at index k in pat after checking that all indices j where j > k correctly match. For all indices consider the prefix of pat that ends with the character at that index. Now the value of suffLength is the number of character at the end of this prefix that are identical to the end of pat. By the above definition, the last index has the length of the pattern as its value, since the whole pattern is compared to itself and the overlap is always the whole pattern length. And the maximum value at index k is (k+1). This value itself is a non-negative integer less than the length of pat except for the last index, where the value is the length of pat. For most positions the value will be 0. Aside from the at the last index the value can be non-zero only at indices where the last character of the pat occurs earlier in pat. -} {-# INLINE suffLengths #-} suffLengths :: S.ByteString -> UArray Int Int #ifndef __HADDOCK__ suffLengths !pat | 0==patLen = array (0,-1) [] | otherwise = runSTUArray (do ar <- newArray_ (0,patEnd) unsafeWrite ar patEnd patLen let {-# INLINE matchSuffix #-} matchSuffix !idx !from = do let !d = patEnd - idx helper !i | i < 0 || (pat `B.unsafeIndex` i) /= (pat `B.unsafeIndex` (i+d)) = i | otherwise = helper (pred i) pre' = helper from unsafeWrite ar idx (idx-pre') idxLoop (pred idx) pre' start idxLoop !idx !pre !end | idx < 0 = return ar | pre < idx = do matching <- unsafeRead ar end -- try and reuse old result if pre + matching < idx -- check if old matching length is too long for current idx then do unsafeWrite ar idx matching idxLoop (pred idx) pre (pred end) else matchSuffix idx pre | otherwise = matchSuffix idx idx idxLoop start start start) -- the third argument, the initial value of "end", is never used and does not matter. where !patLen = S.length pat !patEnd = pred patLen !start = pred patEnd #endif {- Format for suffShifts: The valid index range is the same as for pat. The index k is used when there is a mismatch at pat index k and all indices j where j > k have matched. The value is the smallest number of characters one can advance the pattern such that there the shifted pattern agrees at the already checked positions j>k. Thus the value range is : 0 < value <= length of pattern -} {-# INLINE suffShifts #-} suffShifts :: S.ByteString -> UArray Int Int #ifndef __HADDOCK__ suffShifts !pat | patLen == 0 = array (0,-1) [] | otherwise = runSTUArray (do ar <- newArray (0,patEnd) patLen let preShift !idx !j -- idx counts down and j starts at 0 and is non-decreasing | idx < 0 = return () | suff `unsafeAt` idx == idx+1 = do let !shf = patEnd - idx fill_to_shf !i | i==shf = return () | otherwise = do unsafeWrite ar i shf fill_to_shf (succ i) fill_to_shf j preShift (pred idx) shf | otherwise = preShift (pred idx) j sufShift !idx | idx == patEnd = return ar | otherwise = do unsafeWrite ar (patEnd - (suff `unsafeAt` idx)) (patEnd - idx) sufShift (succ idx) preShift start 0 sufShift 0) where !patLen = S.length pat !patEnd = pred patLen !start = pred patEnd !suff = suffLengths pat #endif