passman-core-0.1.0.0: src/Passman/Core/Entry.hs
-- Copyright (C) 2017 Matthew Harm Bekkema
--
-- This file is part of passman-core
--
-- passman-core is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- passman-core is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
-----------------------------------------------------------------------------
-- |
-- Module : Passman.Core.PassList
-- Copyright : Matthew Harm Bekkema 2017
-- License : GPL-3
-- Maintainer : mbekkema97@gmail.com
-----------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}
module Passman.Core.Entry
( -- * Data structures
Entry (..)
-- * Parsing
, load
, textToEntry
, csvToEntry
-- * Saving
, save
, append
, entryToText
, entryToCsv
) where
import Control.Monad.Trans.Resource (MonadResource, MonadThrow,
throwM)
import Control.Monad (mfilter)
import Data.Conduit (ConduitM, (.|))
import qualified Data.Conduit.List as C
import Data.Conduit.Binary (sourceFile, sinkFile, sinkIOHandle)
import Data.Conduit.Text (decodeUtf8, encodeUtf8)
import Data.CSV.Conduit (intoCSV, fromCSV, defCSVSettings)
import Numeric.Natural (Natural)
import Text.Read (readMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import System.IO (IOMode (AppendMode), openBinaryFile)
import Passman.Core.Mode (Mode, readMode, defaultMode, characterCode)
import Passman.Core.Info (Info (Info), fromInfo)
-- | Represents an entry in the password list.
--
-- Password lists are intended to be saved to file in UTF8 CSV format. All
-- fields except for `info` are optional and can be left blank for a default
-- value.
--
-- This example password list file:
--
-- >google.com
-- >projecteuler.net,32
-- >wiki.haskell.org,,nl
--
-- Would be parsed as:
--
-- >[ Entry {info = "google.com", maxLength = 0, mode = Mode (ModeN :| [ModeC,ModeL])}
-- >, Entry {info = "projecteuler.net", maxLength = 32, mode = Mode (ModeN :| [ModeC,ModeL])}
-- >, Entry {info = "wiki.haskell.org", maxLength = 0, mode = Mode (ModeN :| [ModeL])}
-- >]
data Entry = Entry
{ -- | The info string to generate the password for.
info :: Info
-- | Maximum length of the generated password or 0 if no maximum length.
, maxLength :: Natural
-- | The mode to use when generating the password.
, mode :: Mode
}
deriving (Show, Eq)
-- | Load `Entry`s from the specified file.
load :: MonadResource m => FilePath -> ConduitM i Entry m ()
load f = sourceFile f .| decodeUtf8 .| textToEntry
-- | Convert `Text` into `Entry`s.
textToEntry :: MonadThrow m => ConduitM Text Entry m ()
textToEntry = intoCSV defCSVSettings .| C.mapM csvToEntry
-- | Attempt to convert CSV fields into an `Entry`.
csvToEntry :: MonadThrow m => [Text] -> m Entry
csvToEntry [] = failM "no info column"
csvToEntry [a] = pure $ Entry (Info a) 0 defaultMode
csvToEntry [a,b] = flip (Entry (Info a)) defaultMode <$> parseLength b
csvToEntry [a,b,c] = Entry (Info a) <$> parseLength b <*> parseMode c
csvToEntry _ = failM "too many columns"
-- | Save `Entry`s to the specified file.
save :: MonadResource m => FilePath -> ConduitM Entry o m ()
save f = entryToText .| encodeUtf8 .| sinkFile f
-- | Append `Entry`s to the specified file.
append :: MonadResource m => FilePath -> ConduitM Entry o m ()
append fp = entryToText .| encodeUtf8 .| sinkIOHandle
(openBinaryFile fp AppendMode)
-- | Convert an `Entry` into `Text` for saving to file.
entryToText :: Monad m => ConduitM Entry Text m ()
entryToText = C.map entryToCsv .| fromCSV defCSVSettings
-- | Convert an `Entry` into CSV fields.
entryToCsv :: Entry -> [Text]
entryToCsv (Entry i l m) =
[ fromInfo i
, if l == 0 then T.empty else T.pack $ show l
, T.pack $ characterCode m
]
parseLength :: MonadThrow m => Text -> m Natural
parseLength = parseOrError
(parseWithOrEmpty (mfilter (>0) . readMaybe . T.unpack) 0)
"invalid length"
parseMode :: MonadThrow m => Text -> m Mode
parseMode = parseOrError (parseWithOrEmpty (readMode . T.unpack) defaultMode)
"invalid mode"
parseWithOrEmpty :: (Text -> Maybe a) -> a -> Text -> Maybe a
parseWithOrEmpty p d x = if T.null x' then Just d else p x'
where
x' = T.strip x
parseOrError :: MonadThrow m => (a -> Maybe b) -> String -> a -> m b
parseOrError f err = maybe (failM err) pure . f
failM :: MonadThrow m => String -> m a
failM = throwM . userError