{-| Copyright: This file is part of the package zxcvbn-hs. It is subject to the license terms in the LICENSE file found in the top-level directory of this distribution and at: https://code.devalot.com/sthenauth/zxcvbn-hs No part of this package, including this file, may be copied, modified, propagated, or distributed except according to the terms contained in the LICENSE file. License: MIT -} module Text.Password.Strength.Internal.Repeat ( -- * Repeating Token Matches RepeatMap, Repeat, mkRepeatMap, repeatMatch ) where -------------------------------------------------------------------------------- -- Library Imports: import Control.Arrow ((&&&)) import Control.Lens ((^.), _1) import Data.Function (on) import Data.List (sortBy, subsequences, maximumBy) import Data.Map (Map) import qualified Data.Map as Map import Data.Text (Text) import qualified Data.Text as Text -------------------------------------------------------------------------------- -- Project Imports: import Text.Password.Strength.Internal.Token -------------------------------------------------------------------------------- -- | Internal mapping of repeating tokens. newtype RepeatMap = RepeatMap { getMap :: Map Text [Token] } -------------------------------------------------------------------------------- -- | Type alias for a count of repeating tokens. type Repeat = Int -------------------------------------------------------------------------------- -- | Generate a repeat map from an existing token map. mkRepeatMap :: Map Token a -> RepeatMap mkRepeatMap = RepeatMap . Map.foldrWithKey f Map.empty where f t _ = Map.insertWith (<>) (t ^. tokenChars) [t] -------------------------------------------------------------------------------- -- | Test to see if the given token is repeated. -- -- If a repeat is found, the number of occurrences is returned along -- with the full token representing the repeating sequence. -- -- In other words, if the token passed in is "word" and in the map we -- find that the original password contains "wordword", we return 2 to -- indicate 2 repeats and the token that represents the sequence -- "wordword". repeatMatch :: RepeatMap -> Token -> Maybe (Repeat, Token) repeatMatch m t = Map.lookup (t ^. tokenChars) (getMap m) >>= ordered >>= longestSequence >>= mkToken where ordered :: [Token] -> Maybe [Token] ordered [] = Nothing ordered [_] = Nothing -- Must have at least two elements to repeat. ordered xs = Just $ sortBy (compare `on` (^. startIndex)) xs longestSequence :: [Token] -> Maybe (Repeat, [Token]) longestSequence ts = let f = filter (\(n,_) -> n >= 2) . map (length &&& id) . filter (all isSequence . lineUp) . subsequences in case f ts of [] -> Nothing xs -> Just $ maximumBy (compare `on` (^. _1)) xs mkToken :: (Repeat, [Token]) -> Maybe (Repeat, Token) mkToken (_, []) = Nothing mkToken (n, ts) = Just $ let s = head ts ^. startIndex e = last ts ^. endIndex c = Text.replicate n (t ^. tokenChars) l = Text.replicate n (t ^. tokenLower) in (n, Token c l s e) lineUp :: [Token] -> [(Token, Token)] lineUp xs = zip xs (drop 1 xs) isSequence :: (Token, Token) -> Bool isSequence (x, y) = (y ^. startIndex) - (x ^. endIndex) == 1