packages feed

elocrypt-2.0.1: src/cli/Main.hs

module Main where

import Control.Monad
import Data.Char
import Data.List
import Data.Maybe (fromMaybe)
import System.Console.GetOpt
import System.Environment
import System.Exit
import System.IO
import System.Random

import Data.Elocrypt

version :: String
termLen :: Int
termHeight :: Int

version = "elocrypt 2.0.1"
termLen = 80
termHeight = 10

data Options = Options {
      optCapitals  :: Bool,       -- Include capital letters?
      optLength    :: Int,        -- Size of the password(s)
      optMaxLength :: Int,
      optNumber    :: Maybe Int,  -- Number of passwords to generate
      optPassType  :: PassType,   -- Generate passwords or passphrases
      optHelp      :: Bool,
      optVersion   :: Bool
  } deriving (Show)

data PassType
  = Phrase
  | Word
  deriving (Eq, Show)

defaultOptions :: Options
defaultOptions = Options
  { optCapitals = False
  , optLength = 8
  , optMaxLength = 10
  , optNumber = Nothing
  , optPassType = Word
  , optHelp = False
  , optVersion = False
  }

options :: [OptDescr (Options -> Options)]
options =
  [ Option
    ['c']
    ["capitals"]
    (NoArg (\o -> o { optCapitals = True }))
    "Include at least one capital letter"
  , Option
    ['n']
    ["number"]
    (ReqArg (\n o -> o { optNumber = Just (read n) }) "NUMBER")
    "The number of passwords to generate"
  , Option
    ['p']
    ["passphrase"]
    (NoArg (\o -> o { optPassType = Phrase }))
    "Generate passphrases instead of passwords"
  , Option ['h'] ["help"] (NoArg (\o -> o { optHelp = True })) "Show this help"
  , Option
    ['v']
    ["version"]
    (NoArg (\o -> o { optVersion = True }))
    "Show version and exit"
  ]

main :: IO ()
main = do
  args <- getArgs
  opts <- elocryptOpts args
  gen <- getStdGen

  when (optHelp opts) $ do
    hPutStrLn stderr usage
    exitSuccess

  when (optVersion opts) $ do
    hPutStrLn stderr version
    exitSuccess

  when (optLength opts == 0) exitSuccess    -- Nothing to do

  putStrLn (generate opts gen)

elocryptOpts :: [String] -> IO Options
elocryptOpts args = do
  (opts, nonopts) <- elocryptOpts' args

  return $ case nonopts of
    (o : os : _) -> opts { optLength = read o, optMaxLength = read os }
    (o : _) -> opts { optLength = read o }
    [] -> opts

elocryptOpts' :: [String] -> IO (Options, [String])
elocryptOpts' args = case getOpt Permute options args of
  (opts, nonopts, []) -> do
    let opts' = foldl (flip id) defaultOptions opts
    return (opts', nonopts)

  (_, _, errs) -> do
    hPutStrLn stderr (concat errs)
    hPutStrLn stderr usage
    exitFailure

generate :: RandomGen g => Options -> g -> String
generate opts@Options { optPassType = Word } = passwords opts
generate opts@Options { optPassType = Phrase } = passphrases opts

passwords :: RandomGen g => Options -> g -> String
passwords opts@Options { optLength = len, optNumber = n } gen =
  format "  " . groupWith splitAt' width "  " $ ps
  where
    ps = newPasswords len num (getGenOptions opts) gen
    cols = columns len
    num = fromMaybe (nWords cols) n
    width = max termLen (len + 2)

passphrases :: RandomGen g => Options -> g -> String
passphrases opts@Options { optCapitals = caps, optLength = minLen, optMaxLength = maxLen, optNumber = n } gen
  = format " " . take lines' . groupWith splitAt' width " " $ passphrase
  where
    passphrase = newPassphrase words' minLen maxLen (getGenOptions opts) gen
    words' = columns minLen * lines'
    width = max termLen (maxLen + 1)
    lines' = fromMaybe termHeight n

getGenOptions :: Options -> GenOptions
getGenOptions Options { optCapitals = caps } =
  genOptions { genCapitals = caps }

usage :: String
usage = usageInfo (intercalate "\n" headerLines) options
  where
    headerLines =
      [ "Usage: elocrypt [option...] length"
      , "       elocrypt -p [option...] min-length max-length"
      ]

-- Utilities

-- Calculate the number of passwords to print per line
columns :: Int -> Int
columns len
  | len <= termLen - 2 = termLen `div` (len + 2)
  | otherwise = 1

-- Format a 2D list of Strings,
--  1 list per line
format :: String -> [[String]] -> String
format sep = intercalate "\n" . map (intercalate sep)

-- Calculate the number of words to print
nWords :: Int -> Int
nWords cols = termHeight * cols

-- Group a 2D array with a function by total length
groupWith
  :: (Int -> [a] -> [[a]] -> ([[a]], [[a]])) -> Int -> [a] -> [[a]] -> [[[a]]]
groupWith _ _ _ [] = []
groupWith f i sep ls
  | null groups = [[head ls]]
  | otherwise = groups
  where groups = groupWith' f i sep ls

groupWith'
  :: (Int -> [a] -> [[a]] -> ([[a]], [[a]])) -> Int -> [a] -> [[a]] -> [[[a]]]
groupWith' _ _ _ [] = []
groupWith' f i sep ls = g : groupWith f i sep ls' where (g, ls') = f i sep ls

-- Split a 2D array by the total length
splitAt' :: Int -> [a] -> [[a]] -> ([[a]], [[a]])
splitAt' 0 _ ls = ([], ls)
splitAt' _ _ [] = ([], [])
splitAt' n sep (l : ls)
  | n >= length l + sl = (l : xs, xs')
  | otherwise = ([], l : ls)
  where
    (xs, xs') = splitAt' (n - length l - sl) sep ls
    sl = length sep