packages feed

zxcvbn-hs-0.2.0.0: tools/Zxcvbn/Freq.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}

{-|

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 Zxcvbn.Freq
  ( run
  ) where

--------------------------------------------------------------------------------
-- Imports:
import Control.Monad (forM_, foldM)
import Data.Char (isSpace)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Read as Text
import Pipes (Pipe, (>->), runEffect, each)
import qualified Pipes.Prelude as Pipes
import qualified Pipes.Prelude.Text as PT
import Pipes.Safe (runSafeT)
import System.FilePath (takeFileName, dropExtension)

--------------------------------------------------------------------------------
import Zxcvbn.Encode (header, encode)
import Zxcvbn.Global (Global(..))

--------------------------------------------------------------------------------
-- | A list that we'll read from a file.
data List = List
  { path  :: FilePath
  , name  :: Text
  , limit :: Maybe Int
  } deriving (Show, Eq, Ord)

--------------------------------------------------------------------------------
-- | Words read from all frequency lists.
data Entry = Entry
  { list :: List  -- ^ The list this entry was read from.
  , rank :: Int   -- ^ The rank this word has in the given list
  } deriving (Show, Eq, Ord)

--------------------------------------------------------------------------------
-- | The temporary map used to hold all frequency lists.
type Freqs = HashMap Text Entry

--------------------------------------------------------------------------------
-- | Insert an entry into the map.  If the entry is already in the
-- table keep the one with the lowest rank.
insert :: (Text, Entry) -> Freqs -> Freqs
insert (word, ent) = HashMap.insertWith go word ent
  where
    go :: Entry -> Entry -> Entry
    go new old = if rank new < rank old then new else old

--------------------------------------------------------------------------------
-- | Process the lines from the given list, adding entries to 'Freqs'.
processList :: List -> Freqs -> IO Freqs
processList lst tbl = runSafeT $ runEffect $
    Pipes.fold (\b e -> insert (entry e) b) tbl id
      (Pipes.zip (each [1..])
        (PT.readFileLn (path lst) >->
         Pipes.map word           >->
         restrict) >-> Pipes.filter (not . shouldThrowAway))

  where
    -- Create an entry with the given rank and line of text.
    entry :: (Int, Text) -> (Text, Entry)
    entry (n, t) = (t, Entry lst n)

    -- Extract the first word from a line of text.
    word :: Text -> Text
    word = Text.takeWhile (not . isSpace)

    shouldThrowAway :: (Int, Text) -> Bool
    shouldThrowAway (n, t) = n >= 10 ^ Text.length t

    -- Only process @limit@ lines of text.
    restrict :: (Monad m) => Pipe a a m ()
    restrict =
      case limit lst of
        Just n  -> Pipes.take n
        Nothing -> Pipes.drop 0

--------------------------------------------------------------------------------
-- | Alias for a map whose keys are list names and values are a map of
-- words and ranks.
type Lists = HashMap Text (HashMap Text Int)

--------------------------------------------------------------------------------
-- | Given a @Freqs@ table, extract all the component lists.
extractLists :: Freqs -> Lists
extractLists = HashMap.foldrWithKey go HashMap.empty
  where
    go :: Text -> Entry -> Lists -> Lists
    go word Entry{..} = ins (name list) word rank

    ins :: Text -> Text -> Int -> Lists -> Lists
    ins lst word rnk = HashMap.insertWith HashMap.union lst (HashMap.singleton word rnk)

--------------------------------------------------------------------------------
-- | Turn a file name with an optional line number limit into a 'List'.
mkList :: String -> List
mkList s =
  case Text.split (== ':') (Text.pack s) of
    [x,y] -> List (Text.unpack x) (name' x) (limit' y)
    _     -> List s (name' $ Text.pack s) Nothing

  where
    name' :: Text -> Text
    name' = Text.pack . dropExtension . takeFileName . Text.unpack

    limit' :: Text -> Maybe Int
    limit' t = case Text.decimal t of
      Left _       -> Nothing
      Right (n, _) -> Just n

--------------------------------------------------------------------------------
-- | Let's go!
run :: Global -> IO ()
run Global{..} = do
  putStr (header $ fromMaybe "Text.Password.Strength.Generated.Frequency" mname)
  putStr "import Text.Password.Strength.Internal.Adjacency\n\n"

  ls <- extractLists <$> foldM (flip processList) HashMap.empty (map mkList files)

  forM_ (HashMap.toList ls) $ \(lst, tbl) ->
    putStrLn (encode (Text.unpack lst) "HashMap Text Int" tbl)