zxcvbn-hs-0.2.0.0: tools/Zxcvbn/Adjacency.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-|
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.Adjacency
( run
) where
--------------------------------------------------------------------------------
-- Library Imports:
import Control.Lens
import Control.Lens.TH (makePrisms)
import Control.Monad (forM_)
import Control.Monad.State.Strict
import Data.Char (isSpace)
import Data.Map (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes, fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import System.FilePath (takeFileName, dropExtension)
import qualified Data.Set as Set
import System.IO
--------------------------------------------------------------------------------
-- Project Imports:
import qualified Text.Password.Strength.Internal as Internal
import Zxcvbn.Encode (header, encode)
import Zxcvbn.Global (Global(..))
import Text.Password.Strength.Internal
( Direction(..)
, Move(..)
, Adjacency(..)
, AdjacencyTable(..)
, Pattern
)
--------------------------------------------------------------------------------
type Layer = Int
type Row = Int
type Column = Int
--------------------------------------------------------------------------------
data Entry
= A -- ^ Alignment space.
| K Char -- ^ Character
deriving (Show, Eq)
makePrisms ''Entry
--------------------------------------------------------------------------------
data Keyboard = Keyboard
{ kbName :: String
, kbLayers :: Int
, kbTable :: Map (Layer, Row, Column) Entry
} deriving (Show)
--------------------------------------------------------------------------------
-- | Translate a line of text into a keyboard row.
parseRow :: Text -> State (Layer, Row) [((Layer, Row, Column), Entry)]
parseRow t | t == "\f" = nextLayer >> pure []
| otherwise = do
(layer, row) <- get <* nextRow
pure $ zipWith (\c n -> ((layer, row, n), classify c))
(Text.unpack t) [1..]
where
nextLayer :: State (Layer, Row) ()
nextLayer = do
_1 += 1
_2 .= 1
nextRow :: State (Layer, Row) ()
nextRow =
_2 += 1
classify :: Char -> Entry
classify c | isSpace c = A
| otherwise = K c
--------------------------------------------------------------------------------
-- | Parse a keyboard definition file.
keyboard :: FilePath -> Text -> Keyboard
keyboard file t =
let (rows, (layers, _)) = runState (mapM parseRow $ Text.lines t) (1,1)
in Keyboard { kbName = dropExtension (takeFileName file)
, kbLayers = layers
, kbTable = Map.fromList (join rows)
}
--------------------------------------------------------------------------------
keyboard' :: FilePath -> IO Keyboard
keyboard' f = keyboard f <$> Text.readFile f
--------------------------------------------------------------------------------
-- | Move along the keyboard in the given direction.
move :: Direction -> (Row, Column) -> (Row, Column)
move d (x, y) =
case d of
N -> (x-1, y )
NE -> (x-1, y+1)
E -> (x, y+1)
SE -> (x+1, y+1)
S -> (x+1, y )
SW -> (x+1, y-1)
W -> (x, y-1)
NW -> (x-1, y-1)
--------------------------------------------------------------------------------
filterKey :: Keyboard -> Layer -> Direction -> (Row, Column) -> Maybe Char
filterKey k l d (x, y) =
try =<< Map.lookup (l, x, y) (kbTable k)
where
try :: Entry -> Maybe Char
try (K c) = Just c
try A = case d of
N -> Nothing
NE -> retry E
E -> retry E
SE -> retry E
S -> Nothing
SW -> retry W
W -> retry W
NW -> retry W
retry :: Direction -> Maybe Char
retry d' = filterKey k l d' (move d (x, y))
--------------------------------------------------------------------------------
-- | Calculate all the possible neighbors of a key on a single layer.
-- Does not include the key itself.
neighbors :: (Row, Column) -> [(Direction, (Row, Column))]
neighbors x = map (\d -> (d, move d x)) [minBound .. maxBound]
--------------------------------------------------------------------------------
-- | Generate an adjacency association for the given key.
adjacency :: Keyboard -> (Layer, Row, Column) -> [((Char, Char), Adjacency)]
adjacency k (l, x, y) = fromMaybe [] $ do
charA <- self l
pure $ flip concatMap [1 .. kbLayers k] $ \layer ->
flip map (onLayer layer) $ \(m, charB) ->
((charA, charB), Adjacency m (toLayer l) (toLayer layer))
where
-- | Find the current character on the given layer.
self :: Layer -> Maybe Char
self layer = Map.lookup (layer, x, y) (kbTable k) >>= preview _K
-- | All adjacent characters on the given layer.
onLayer :: Layer -> [(Move, Char)]
onLayer layer = catMaybes $ ((Stay,) <$> self layer) :
map (\(d, rc) -> (Move d,) <$> filterKey k layer d rc)
(neighbors (x, y))
toLayer :: Layer -> Internal.Layer
toLayer 1 = Internal.Primary
toLayer _ = Internal.Secondary
--------------------------------------------------------------------------------
-- | Convert a keyboard to an adjacency table.
adjTable :: Keyboard -> AdjacencyTable
adjTable k = AdjacencyTable chars avns table
where
-- The adjacency table.
table :: Map Pattern Adjacency
table = Map.foldrWithKey append Map.empty (kbTable k)
-- Add entries to the table.
append :: (Layer, Row, Column) -> Entry -> Map Pattern Adjacency -> Map Pattern Adjacency
append key _ m = foldr (uncurry Map.insert) m (adjacency k key)
-- Total number of keys on the keyboard.
chars :: Int
chars = Set.size $ Set.fromList $ map (^. _1._1) $ Map.assocs table
-- Average number of neighbors. In the paper, keys can only be
-- neighbors if they are on the same layer and not the same key.
-- Also, I truncate the average so we can continue to use integer
-- arithmetic everywhere.
avns :: Int
avns =
let ns = filter (\a -> _firstLayer a == _secondLayer a) (Map.elems table)
in (length ns - chars) `div` chars
--------------------------------------------------------------------------------
run :: Global -> IO ()
run Global{..} = do
putStr (header $ fromMaybe "Text.Password.Strength.Generated.Adjacency" mname)
putStr "import Text.Password.Strength.Types\n\n"
forM_ files $ \file -> do
k@Keyboard{..} <- keyboard' file
let table = adjTable k
when debug $ do
hPutStrLn stderr ("==> " <> file)
hPutStrLn stderr (" Total keys: " <> show (_totalChars table))
hPutStrLn stderr ("Average Neighbors: " <> show (_averageNeighbors table))
forM_ (Map.assocs (_patterns table)) $ \(key, val) ->
hPutStrLn stderr (show key <> "\t" <> show val)
putStrLn (encode kbName "AdjacencyTable" table)