packages feed

htalkat-0.1.1: Petname.hs

-- This file is part of htalkat
-- Copyright (C) 2021 Martin Bays <mbays@sdf.org>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of version 3 of the GNU General Public License as
-- published by the Free Software Foundation, or any later version.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see http://www.gnu.org/licenses/.

{-# LANGUAGE Safe #-}

module Petname where

import           Control.Monad             (msum)
import           Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
import           Data.Char                 (isAlphaNum)
import           Data.List                 (elemIndices)
import           Data.Maybe                (catMaybes)
import           Safe                      (lastMay, readMay)
import           System.Directory          (createDirectoryIfMissing,
                                            listDirectory)
import           System.FileLock           (SharedExclusive (..), withFileLock)
import           System.FilePath           (isValid, takeFileName, (</>))

import           Host
import           Mundanities
import           User

data Petname = Named String | Unnamed Int
    deriving (Eq)

instance Ord Petname where
    compare (Named _) (Unnamed _)    = LT
    compare (Unnamed _) (Named _)    = GT
    compare (Named n) (Named n')
        | c <- compare (length n) (length n')
        , c /= EQ                    = c
    compare (Named n) (Named n')     = compare n n'
    compare (Unnamed n) (Unnamed n') = compare n n'

parsePetname :: String -> Maybe Petname
parsePetname ('+':s) | Just n <- readMay s, n > 0 = Just $ Unnamed n
parsePetname s | isValidPetname s = Just $ Named s
parsePetname _ = Nothing

isValidPetname :: String -> Bool
isValidPetname ('.':_) = False
isValidPetname s       = shellQuotable s && isValid s && s == takeFileName s
    where shellQuotable = notElem '\''

showPetname :: Petname -> String
showPetname (Named s)   = s
showPetname (Unnamed n) = '+':show n

shellQuotePetname :: Petname -> String
shellQuotePetname = shellQuote . showPetname where
    shellQuote s
        | all shellSafe s && not (null s) = s
        | otherwise = '\'' : s <> "'"
    shellSafe c = isAlphaNum c || c `elem` ".,_-+="

namesDir :: FilePath -> FilePath
namesDir = (</> "names")

lookupName :: FilePath -> Petname -> IO (Maybe User)
lookupName ddir name = do
    let ndir = namesDir ddir
        path = ndir </> showPetname name
    createDirectoryIfMissing True ndir
    ignoreIOErrAlt . withFileLock (ndir </> ".lock") Shared $ \_ ->
        parseUser <$> readFile path

resolveTarget :: FilePath -> String -> IO (Maybe User)
resolveTarget ddir target
    | Just user <- parseUser target = pure $ Just user
    | otherwise = runMaybeT $ msum
        [ do
            name <- MaybeT . pure $ parsePetname target
            MaybeT $ lookupName ddir name
        , do
            i <- MaybeT . pure . lastMay $ elemIndices '@' target
            let (n, '@':h) = splitAt i target
            name <- MaybeT . pure $ parsePetname n
            host <- MaybeT . pure $ parseHost h
            MaybeT $ ((\u -> u { userHost = Just host }) <$>) <$>
                lookupName ddir name
        ]

writeName :: FilePath -> User -> Petname -> IO ()
writeName ddir user name = do
    let ndir = namesDir ddir
        path = ndir </> showPetname name
    createDirectoryIfMissing True ndir
    withFileLock (ndir </> ".lock") Exclusive $ \_ ->
        writeFile path $ showUser user

loadNames :: FilePath -> IO [Petname]
loadNames ddir = catMaybes . (parsePetname <$>) <$>
    listDirectory (namesDir ddir)