{-# Language OverloadedStrings #-} module Client.Mask ( Mask , matchMask , buildMask ) where import Irc.UserInfo import Irc.Identifier import Data.Text (Text) import qualified Data.Text as Text import Text.Regex.TDFA import Text.Regex.TDFA.String (compile) import Data.List newtype Mask = Mask Regex -- | Compile a list of masks down to a single, reuseable 'Mask' value -- suitable for being used with 'matchMask'. -- -- Masks can match zero-to-many arbitrary characters with @*@. -- -- Masks can match one arbitrary character with @?@. -- -- Literal @*@ @?@ and @|@ can be matched with a preceding @\@. -- -- Missing host or username components of a mask will automatically -- be treated as wildcards. buildMask :: [Identifier] {- ^ masks -} -> Mask buildMask patterns = case componentsToMask (map (translate . parseMaskComponents . idTextNorm) patterns) of Left e -> error e Right m -> m -- | Determine if a given 'Mask' matches a given 'UserInfo' matchMask :: Mask -> UserInfo -> Bool matchMask (Mask re) userInfo = matchTest re (Text.unpack (normalized (renderUserInfo userInfo))) normalized :: Text -> Text normalized = idTextNorm . mkId -- | Parse a mask into the nick, user, and hostname components -- while replacing omitted components with @"*"@. parseMaskComponents :: Text -> String parseMaskComponents str = Text.unpack nick ++ "!" ++ user ++ "@" ++ host where (nickuser,rawhost) = Text.break (=='@') str (nick ,rawuser) = Text.break (=='!') nickuser user = defaultWild rawuser host = defaultWild rawhost defaultWild x = case Text.uncons x of Nothing -> "*" Just (_, y) -> Text.unpack y componentsToMask :: [String] -> Either String Mask componentsToMask xs = Mask <$> compile defaultCompOpt { multiline = False } defaultExecOpt { captureGroups = False } ("^(" ++ intercalate "|" xs ++ ")$") -- | Translate from the languge of masks to the language of -- regular expressions. -- -- Masks support the @*@ (many) and @?@ (one) wildcards. Wildcards -- and @\@ can be escaped by preceding them with a @\@. All other -- uses of @\@ are treated as matching the literal backslash. translate :: String -> String translate [] = [] translate ('\\' : '*' : xs) = '\\' : '*' : translate xs translate ('\\' : '?' : xs) = '\\' : '?' : translate xs translate ('\\' : '\\' : xs) = '\\' : '\\' : translate xs translate ('*' : xs) = '.' : '*' : translate xs translate ('?' : xs) = '.' : '?' : translate xs translate (x : xs) | isMetaChar x = '\\' : x : translate xs | otherwise = x : translate xs -- | returns True iff the charactr is a regular expression meta character: -- @^$\\.|*?+()[]{}@ isMetaChar :: Char -> Bool isMetaChar c = case c of '^' -> True '\\' -> True '.' -> True '|' -> True '*' -> True '?' -> True '+' -> True '(' -> True ')' -> True '[' -> True ']' -> True '{' -> True '}' -> True '$' -> True _ -> False