    -- |
    -- Module      :  Distribution.PackageDescription.Check.Paths
    -- Copyright   :  Lennart Kolmodin 2008, Francesco Ariis 2023
    -- License     :  BSD3
    --
    -- Maintainer  :  cabal-devel@haskell.org
    -- Portability :  portable
    --
    -- Functions to check filepaths, directories, globs, etc.
    module Distribution.PackageDescription.Check.Paths
      ( checkGlob
      , checkPath
      , fileExtensionSupportedLanguage
      , isGoodRelativeDirectoryPath
      , isGoodRelativeFilePath
      , isGoodRelativeGlob
      , isInsideDist
      ) where

    import Distribution.Compat.Prelude
    import Prelude ()

    import Distribution.PackageDescription.Check.Common
    import Distribution.PackageDescription.Check.Monad
    import Distribution.Simple.CCompiler
    import Distribution.Simple.Glob
      ( Glob
      , explainGlobSyntaxError
      , isRecursiveInRoot
      , parseFileGlob
      )
    import Distribution.Simple.Utils hiding (findPackageDesc, notice)
    import System.FilePath (splitDirectories, splitPath, takeExtension)

    import qualified System.FilePath.Windows as FilePath.Windows (isValid)

    fileExtensionSupportedLanguage :: FilePath -> Bool
    fileExtensionSupportedLanguage :: FilePath -> Bool
    fileExtensionSupportedLanguage FilePath
    path =
      Bool
    isHaskell Bool -> Bool -> Bool
    || Bool
    isC
      where
        extension :: FilePath
    extension = FilePath -> FilePath
    takeExtension FilePath
    path
        isHaskell :: Bool
    isHaskell = FilePath
    extension FilePath -> [FilePath] -> Bool
    forall a. Eq a => a -> [a] -> Bool
    forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
    `elem` [FilePath
    ".hs", FilePath
    ".lhs"]
        isC :: Bool
    isC = Maybe (CDialect, Bool) -> Bool
    forall a. Maybe a -> Bool
    isJust (FilePath -> Maybe (CDialect, Bool)
    filenameCDialect FilePath
    extension)

    -- Boolean: are absolute paths allowed?
    checkPath
      :: Monad m
      => Bool -- Can be absolute path?
      -> CabalField -- .cabal field that we are checking.
      -> PathKind -- Path type.
      -> FilePath -- Path.
      -> CheckM m ()
    checkPath :: forall (m :: * -> *).
    Monad m =>
    Bool -> FilePath -> PathKind -> FilePath -> CheckM m ()
    checkPath Bool
    isAbs FilePath
    title PathKind
    kind FilePath
    path = do
      Bool -> PackageCheck -> CheckM m ()
    forall (m :: * -> *).
    Monad m =>
    Bool -> PackageCheck -> CheckM m ()
    checkP
        (FilePath -> Bool
    isOutsideTree FilePath
    path)
        (CheckExplanation -> PackageCheck
    PackageBuildWarning (CheckExplanation -> PackageCheck)
    -> CheckExplanation -> PackageCheck
    forall a b. (a -> b) -> a -> b
    $ FilePath -> FilePath -> CheckExplanation
    RelativeOutside FilePath
    title FilePath
    path)
      Bool -> PackageCheck -> CheckM m ()
    forall (m :: * -> *).
    Monad m =>
    Bool -> PackageCheck -> CheckM m ()
    checkP
        (FilePath -> Bool
    isInsideDist FilePath
    path)
        (CheckExplanation -> PackageCheck
    PackageDistInexcusable (CheckExplanation -> PackageCheck)
    -> CheckExplanation -> PackageCheck
    forall a b. (a -> b) -> a -> b
    $ Maybe FilePath -> FilePath -> CheckExplanation
    DistPoint (FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    title) FilePath
    path)
      PathKind -> FilePath -> CheckM m ()
    forall (m :: * -> *).
    Monad m =>
    PathKind -> FilePath -> CheckM m ()
    checkPackageFileNamesWithGlob PathKind
    kind FilePath
    path

      -- Skip if "can be absolute path".
      Bool -> PackageCheck -> CheckM m ()
    forall (m :: * -> *).
    Monad m =>
    Bool -> PackageCheck -> CheckM m ()
    checkP
        (Bool -> Bool
    not Bool
    isAbs Bool -> Bool -> Bool
    && FilePath -> Bool
    isAbsoluteOnAnyPlatform FilePath
    path)
        (CheckExplanation -> PackageCheck
    PackageDistInexcusable (CheckExplanation -> PackageCheck)
    -> CheckExplanation -> PackageCheck
    forall a b. (a -> b) -> a -> b
    $ FilePath -> FilePath -> CheckExplanation
    AbsolutePath FilePath
    title FilePath
    path)
      case FilePath -> PathKind -> Maybe FilePath
    grl FilePath
    path PathKind
    kind of
        Just FilePath
    e ->
          Bool -> PackageCheck -> CheckM m ()
    forall (m :: * -> *).
    Monad m =>
    Bool -> PackageCheck -> CheckM m ()
    checkP
            (Bool -> Bool
    not Bool
    isAbs)
            (CheckExplanation -> PackageCheck
    PackageDistInexcusable (CheckExplanation -> PackageCheck)
    -> CheckExplanation -> PackageCheck
    forall a b. (a -> b) -> a -> b
    $ FilePath -> FilePath -> FilePath -> CheckExplanation
    BadRelativePath FilePath
    title FilePath
    path FilePath
    e)
        Maybe FilePath
    Nothing -> () -> CheckM m ()
    forall a. a -> CheckM m a
    forall (m :: * -> *) a. Monad m => a -> m a
    return ()
      Bool -> FilePath -> CheckM m ()
    forall (m :: * -> *). Monad m => Bool -> FilePath -> CheckM m ()
    checkWindowsPath (PathKind
    kind PathKind -> PathKind -> Bool
    forall a. Eq a => a -> a -> Bool
    == PathKind
    PathKindGlob) FilePath
    path
      where
        isOutsideTree :: FilePath -> Bool
    isOutsideTree FilePath
    wpath = case FilePath -> [FilePath]
    splitDirectories FilePath
    wpath of
          FilePath
    ".." : [FilePath]
    _ -> Bool
    True
          FilePath
    "." : FilePath
    ".." : [FilePath]
    _ -> Bool
    True
          [FilePath]
    _ -> Bool
    False

        -- These are not paths, but globs...
        grl :: FilePath -> PathKind -> Maybe FilePath
    grl FilePath
    wfp PathKind
    PathKindFile = FilePath -> Maybe FilePath
    isGoodRelativeFilePath FilePath
    wfp
        grl FilePath
    wfp PathKind
    PathKindGlob = FilePath -> Maybe FilePath
    isGoodRelativeGlob FilePath
    wfp
        grl FilePath
    wfp PathKind
    PathKindDirectory = FilePath -> Maybe FilePath
    isGoodRelativeDirectoryPath FilePath
    wfp

    -- | Is a 'FilePath' inside `dist`, `dist-newstyle` and friends?
    isInsideDist :: FilePath -> Bool
    isInsideDist :: FilePath -> Bool
    isInsideDist FilePath
    path =
      case (FilePath -> FilePath) -> [FilePath] -> [FilePath]
    forall a b. (a -> b) -> [a] -> [b]
    map FilePath -> FilePath
    lowercase (FilePath -> [FilePath]
    splitDirectories FilePath
    path) of
        FilePath
    "dist" : [FilePath]
    _ -> Bool
    True
        FilePath
    "." : FilePath
    "dist" : [FilePath]
    _ -> Bool
    True
        FilePath
    "dist-newstyle" : [FilePath]
    _ -> Bool
    True
        FilePath
    "." : FilePath
    "dist-newstyle" : [FilePath]
    _ -> Bool
    True
        [FilePath]
    _ -> Bool
    False

    checkPackageFileNamesWithGlob
      :: Monad m
      => PathKind
      -> FilePath -- Filepath or possibly a glob pattern.
      -> CheckM m ()
    checkPackageFileNamesWithGlob :: forall (m :: * -> *).
    Monad m =>
    PathKind -> FilePath -> CheckM m ()
    checkPackageFileNamesWithGlob PathKind
    kind FilePath
    fp = do
      Bool -> FilePath -> CheckM m ()
    forall (m :: * -> *). Monad m => Bool -> FilePath -> CheckM m ()
    checkWindowsPath (PathKind
    kind PathKind -> PathKind -> Bool
    forall a. Eq a => a -> a -> Bool
    == PathKind
    PathKindGlob) FilePath
    fp
      FilePath -> CheckM m ()
    forall (m :: * -> *). Monad m => FilePath -> CheckM m ()
    checkTarPath FilePath
    fp

    checkWindowsPath
      :: Monad m
      => Bool -- Is it a glob pattern?
      -> FilePath -- Path.
      -> CheckM m ()
    checkWindowsPath :: forall (m :: * -> *). Monad m => Bool -> FilePath -> CheckM m ()
    checkWindowsPath Bool
    isGlob FilePath
    path =
      Bool -> PackageCheck -> CheckM m ()
    forall (m :: * -> *).
    Monad m =>
    Bool -> PackageCheck -> CheckM m ()
    checkP
        (Bool -> Bool
    not (Bool -> Bool) -> (FilePath -> Bool) -> FilePath -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . FilePath -> Bool
    FilePath.Windows.isValid (FilePath -> Bool) -> FilePath -> Bool
    forall a b. (a -> b) -> a -> b
    $ Bool -> FilePath -> FilePath
    escape Bool
    isGlob FilePath
    path)
        (CheckExplanation -> PackageCheck
    PackageDistInexcusable (CheckExplanation -> PackageCheck)
    -> CheckExplanation -> PackageCheck
    forall a b. (a -> b) -> a -> b
    $ [FilePath] -> CheckExplanation
    InvalidOnWin [FilePath
    path])
      where
        -- Force a relative name to catch invalid file names like "f:oo" which
        -- otherwise parse as file "oo" in the current directory on the 'f' drive.
        escape :: Bool -> String -> String
        escape :: Bool -> FilePath -> FilePath
    escape Bool
    wisGlob FilePath
    wpath =
          (FilePath
    ".\\" FilePath -> FilePath -> FilePath
    forall a. [a] -> [a] -> [a]
    ++)
          -- Glob paths will be expanded before being dereferenced, so asterisks
          -- shouldn't count against them.
          (FilePath -> FilePath) -> FilePath -> FilePath
    forall a b. (a -> b) -> a -> b
    $
            (Char -> Char) -> FilePath -> FilePath
    forall a b. (a -> b) -> [a] -> [b]
    map (\Char
    c -> if Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '*' Bool -> Bool -> Bool
    && Bool
    wisGlob then Char
    'x' else Char
    c) FilePath
    wpath

    -- | Check a file name is valid for the portable POSIX tar format.
    --
    -- The POSIX tar format has a restriction on the length of file names. It is
    -- unfortunately not a simple restriction like a maximum length. The exact
    -- restriction is that either the whole path be 100 characters or less, or it
    -- be possible to split the path on a directory separator such that the first
    -- part is 155 characters or less and the second part 100 characters or less.
    checkTarPath :: Monad m => FilePath -> CheckM m ()
    checkTarPath :: forall (m :: * -> *). Monad m => FilePath -> CheckM m ()
    checkTarPath FilePath
    path
      | FilePath -> Int
    forall a. [a] -> Int
    forall (t :: * -> *) a. Foldable t => t a -> Int
    length FilePath
    path Int -> Int -> Bool
    forall a. Ord a => a -> a -> Bool
    > Int
    255 = PackageCheck -> CheckM m ()
    forall (m :: * -> *). Monad m => PackageCheck -> CheckM m ()
    tellP PackageCheck
    longPath
      | Bool
    otherwise = case Int -> [FilePath] -> Either PackageCheck [FilePath]
    forall {t :: * -> *} {a}.
    Foldable t =>
    Int -> [t a] -> Either PackageCheck [t a]
    pack Int
    nameMax ([FilePath] -> [FilePath]
    forall a. [a] -> [a]
    reverse (FilePath -> [FilePath]
    splitPath FilePath
    path)) of
          Left PackageCheck
    err -> PackageCheck -> CheckM m ()
    forall (m :: * -> *). Monad m => PackageCheck -> CheckM m ()
    tellP PackageCheck
    err
          Right [] -> () -> CheckM m ()
    forall a. a -> CheckM m a
    forall (m :: * -> *) a. Monad m => a -> m a
    return ()
          Right (FilePath
    h : [FilePath]
    rest) -> case Int -> [FilePath] -> Either PackageCheck [FilePath]
    forall {t :: * -> *} {a}.
    Foldable t =>
    Int -> [t a] -> Either PackageCheck [t a]
    pack Int
    prefixMax [FilePath]
    remainder of
            Left PackageCheck
    err -> PackageCheck -> CheckM m ()
    forall (m :: * -> *). Monad m => PackageCheck -> CheckM m ()
    tellP PackageCheck
    err
            Right [] -> () -> CheckM m ()
    forall a. a -> CheckM m a
    forall (m :: * -> *) a. Monad m => a -> m a
    return ()
            Right (FilePath
    _ : [FilePath]
    _) -> PackageCheck -> CheckM m ()
    forall (m :: * -> *). Monad m => PackageCheck -> CheckM m ()
    tellP PackageCheck
    noSplit
            where
              -- drop the '/' between the name and prefix:
              remainder :: [FilePath]
    remainder = FilePath -> FilePath
    forall a. [a] -> [a]
    safeInit FilePath
    h FilePath -> [FilePath] -> [FilePath]
    forall a. a -> [a] -> [a]
    : [FilePath]
    rest
      where
        nameMax, prefixMax :: Int
        nameMax :: Int
    nameMax = Int
    100
        prefixMax :: Int
    prefixMax = Int
    155

        pack :: Int -> [t a] -> Either PackageCheck [t a]
    pack Int
    _ [] = PackageCheck -> Either PackageCheck [t a]
    forall a b. a -> Either a b
    Left PackageCheck
    emptyName
        pack Int
    maxLen (t a
    c : [t a]
    cs)
          | Int
    n Int -> Int -> Bool
    forall a. Ord a => a -> a -> Bool
    > Int
    maxLen = PackageCheck -> Either PackageCheck [t a]
    forall a b. a -> Either a b
    Left PackageCheck
    longName
          | Bool
    otherwise = [t a] -> Either PackageCheck [t a]
    forall a b. b -> Either a b
    Right (Int -> Int -> [t a] -> [t a]
    forall {t :: * -> *} {a}.
    Foldable t =>
    Int -> Int -> [t a] -> [t a]
    pack' Int
    maxLen Int
    n [t a]
    cs)
          where
            n :: Int
    n = t a -> Int
    forall a. t a -> Int
    forall (t :: * -> *) a. Foldable t => t a -> Int
    length t a
    c

        pack' :: Int -> Int -> [t a] -> [t a]
    pack' Int
    maxLen Int
    n (t a
    c : [t a]
    cs)
          | Int
    n' Int -> Int -> Bool
    forall a. Ord a => a -> a -> Bool
    <= Int
    maxLen = Int -> Int -> [t a] -> [t a]
    pack' Int
    maxLen Int
    n' [t a]
    cs
          where
            n' :: Int
    n' = Int
    n Int -> Int -> Int
    forall a. Num a => a -> a -> a
    + t a -> Int
    forall a. t a -> Int
    forall (t :: * -> *) a. Foldable t => t a -> Int
    length t a
    c
        pack' Int
    _ Int
    _ [t a]
    cs = [t a]
    cs

        longPath :: PackageCheck
    longPath = CheckExplanation -> PackageCheck
    PackageDistInexcusable (FilePath -> CheckExplanation
    FilePathTooLong FilePath
    path)
        longName :: PackageCheck
    longName = CheckExplanation -> PackageCheck
    PackageDistInexcusable (FilePath -> CheckExplanation
    FilePathNameTooLong FilePath
    path)
        noSplit :: PackageCheck
    noSplit = CheckExplanation -> PackageCheck
    PackageDistInexcusable (FilePath -> CheckExplanation
    FilePathSplitTooLong FilePath
    path)
        emptyName :: PackageCheck
    emptyName = CheckExplanation -> PackageCheck
    PackageDistInexcusable CheckExplanation
    FilePathEmpty

    -- `checkGlob` checks glob patterns and returns good ones for further
    -- processing.
    checkGlob
      :: Monad m
      => CabalField -- .cabal field we are checking.
      -> FilePath -- glob filepath pattern
      -> CheckM m (Maybe Glob)
    checkGlob :: forall (m :: * -> *).
    Monad m =>
    FilePath -> FilePath -> CheckM m (Maybe Glob)
    checkGlob FilePath
    title FilePath
    pat = do
      ver <- (CheckCtx m -> CabalSpecVersion) -> CheckM m CabalSpecVersion
    forall (m :: * -> *) a. Monad m => (CheckCtx m -> a) -> CheckM m a
    asksCM CheckCtx m -> CabalSpecVersion
    forall (m :: * -> *). CheckCtx m -> CabalSpecVersion
    ccSpecVersion

      -- Glob sanity check.
      case parseFileGlob ver pat of
        Left GlobSyntaxError
    e -> do
          PackageCheck -> CheckM m ()
    forall (m :: * -> *). Monad m => PackageCheck -> CheckM m ()
    tellP
            ( CheckExplanation -> PackageCheck
    PackageDistInexcusable (CheckExplanation -> PackageCheck)
    -> CheckExplanation -> PackageCheck
    forall a b. (a -> b) -> a -> b
    $
                FilePath -> FilePath -> CheckExplanation
    GlobSyntaxError FilePath
    title (FilePath -> GlobSyntaxError -> FilePath
    explainGlobSyntaxError FilePath
    pat GlobSyntaxError
    e)
            )
          Maybe Glob -> CheckM m (Maybe Glob)
    forall a. a -> CheckM m a
    forall (m :: * -> *) a. Monad m => a -> m a
    return Maybe Glob
    forall a. Maybe a
    Nothing
        Right Glob
    wglob -> do
          -- \* Miscellaneous checks on sane glob.
          -- Checks for recursive glob in root.
          Bool -> PackageCheck -> CheckM m ()
    forall (m :: * -> *).
    Monad m =>
    Bool -> PackageCheck -> CheckM m ()
    checkP
            (Glob -> Bool
    isRecursiveInRoot Glob
    wglob)
            ( CheckExplanation -> PackageCheck
    PackageDistSuspiciousWarn (CheckExplanation -> PackageCheck)
    -> CheckExplanation -> PackageCheck
    forall a b. (a -> b) -> a -> b
    $
                FilePath -> FilePath -> CheckExplanation
    RecursiveGlobInRoot FilePath
    title FilePath
    pat
            )
          Maybe Glob -> CheckM m (Maybe Glob)
    forall a. a -> CheckM m a
    forall (m :: * -> *) a. Monad m => a -> m a
    return (Glob -> Maybe Glob
    forall a. a -> Maybe a
    Just Glob
    wglob)

    -- | Whether a path is a good relative path.  We aren't worried about perfect
    -- cross-platform compatibility here; this function just checks the paths in
    -- the (local) @.cabal@ file, while only Hackage needs the portability.
    --
    -- >>> let test fp = putStrLn $ show (isGoodRelativeDirectoryPath fp) ++ "; " ++ show (isGoodRelativeFilePath fp)
    --
    -- Note that "foo./bar.hs" would be invalid on Windows.
    --
    -- >>> traverse_ test ["foo/bar/quu", "a/b.hs", "foo./bar.hs"]
    -- Nothing; Nothing
    -- Nothing; Nothing
    -- Nothing; Nothing
    --
    -- Trailing slash is not allowed for files, for directories it is ok.
    --
    -- >>> test "foo/"
    -- Nothing; Just "trailing slash"
    --
    -- Leading @./@ is fine, but @.@ and @./@ are not valid files.
    --
    -- >>> traverse_ test [".", "./", "./foo/bar"]
    -- Nothing; Just "trailing dot segment"
    -- Nothing; Just "trailing slash"
    -- Nothing; Nothing
    --
    -- Lastly, not good file nor directory cases:
    --
    -- >>> traverse_ test ["", "/tmp/src", "foo//bar", "foo/.", "foo/./bar", "foo/../bar"]
    -- Just "empty path"; Just "empty path"
    -- Just "posix absolute path"; Just "posix absolute path"
    -- Just "empty path segment"; Just "empty path segment"
    -- Just "trailing same directory segment: ."; Just "trailing same directory segment: ."
    -- Just "same directory segment: ."; Just "same directory segment: ."
    -- Just "parent directory segment: .."; Just "parent directory segment: .."
    --
    -- For the last case, 'isGoodRelativeGlob' doesn't warn:
    --
    -- >>> traverse_ (print . isGoodRelativeGlob) ["foo/../bar"]
    -- Just "parent directory segment: .."
    isGoodRelativeFilePath :: FilePath -> Maybe String
    isGoodRelativeFilePath :: FilePath -> Maybe FilePath
    isGoodRelativeFilePath = FilePath -> Maybe FilePath
    state0
      where
        -- initial state
        state0 :: FilePath -> Maybe FilePath
    state0 [] = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "empty path"
        state0 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state1 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "posix absolute path"
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state5 FilePath
    cs

        -- after initial .
        state1 :: FilePath -> Maybe FilePath
    state1 [] = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "trailing dot segment"
        state1 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state4 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    state2 FilePath
    cs
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state5 FilePath
    cs

        -- after ./ or after / between segments
        state2 :: FilePath -> Maybe FilePath
    state2 [] = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "trailing slash"
        state2 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state3 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "empty path segment"
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state5 FilePath
    cs

        -- after non-first segment's .
        state3 :: FilePath -> Maybe FilePath
    state3 [] = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "trailing same directory segment: ."
        state3 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state4 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "same directory segment: ."
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state5 FilePath
    cs

        -- after ..
        state4 :: FilePath -> Maybe FilePath
    state4 [] = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "trailing parent directory segment: .."
        state4 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state5 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "parent directory segment: .."
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state5 FilePath
    cs

        -- in a segment which is ok.
        state5 :: FilePath -> Maybe FilePath
    state5 [] = Maybe FilePath
    forall a. Maybe a
    Nothing
        state5 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state5 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    state2 FilePath
    cs
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state5 FilePath
    cs

    -- | See 'isGoodRelativeFilePath'.
    --
    -- This is barebones function. We check whether the glob is a valid file
    -- by replacing stars @*@ with @x@ses.
    isGoodRelativeGlob :: FilePath -> Maybe String
    isGoodRelativeGlob :: FilePath -> Maybe FilePath
    isGoodRelativeGlob = FilePath -> Maybe FilePath
    isGoodRelativeFilePath (FilePath -> Maybe FilePath)
    -> (FilePath -> FilePath) -> FilePath -> Maybe FilePath
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Char -> Char) -> FilePath -> FilePath
    forall a b. (a -> b) -> [a] -> [b]
    map Char -> Char
    f
      where
        f :: Char -> Char
    f Char
    '*' = Char
    'x'
        f Char
    c = Char
    c

    -- | See 'isGoodRelativeFilePath'.
    isGoodRelativeDirectoryPath :: FilePath -> Maybe String
    isGoodRelativeDirectoryPath :: FilePath -> Maybe FilePath
    isGoodRelativeDirectoryPath = FilePath -> Maybe FilePath
    state0
      where
        -- initial state
        state0 :: FilePath -> Maybe FilePath
    state0 [] = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "empty path"
        state0 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state5 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "posix absolute path"
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state4 FilePath
    cs

        -- after initial ./ or after / between segments
        state1 :: FilePath -> Maybe FilePath
    state1 [] = Maybe FilePath
    forall a. Maybe a
    Nothing
        state1 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state2 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "empty path segment"
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state4 FilePath
    cs

        -- after non-first setgment's .
        state2 :: FilePath -> Maybe FilePath
    state2 [] = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "trailing same directory segment: ."
        state2 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state3 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "same directory segment: ."
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state4 FilePath
    cs

        -- after ..
        state3 :: FilePath -> Maybe FilePath
    state3 [] = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "trailing parent directory segment: .."
        state3 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state4 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    "parent directory segment: .."
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state4 FilePath
    cs

        -- in a segment which is ok.
        state4 :: FilePath -> Maybe FilePath
    state4 [] = Maybe FilePath
    forall a. Maybe a
    Nothing
        state4 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state4 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    state1 FilePath
    cs
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state4 FilePath
    cs

        -- after initial .
        state5 :: FilePath -> Maybe FilePath
    state5 [] = Maybe FilePath
    forall a. Maybe a
    Nothing -- "."
        state5 (Char
    c : FilePath
    cs)
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '.' = FilePath -> Maybe FilePath
    state3 FilePath
    cs
          | Char
    c Char -> Char -> Bool
    forall a. Eq a => a -> a -> Bool
    == Char
    '/' = FilePath -> Maybe FilePath
    state1 FilePath
    cs
          | Bool
    otherwise = FilePath -> Maybe FilePath
    state4 FilePath
    cs

    -- [Note: Good relative paths]
    --
    -- Using @kleene@ we can define an extended regex:
    --
    -- @
    -- import Algebra.Lattice
    -- import Kleene
    -- import Kleene.ERE (ERE (..), intersections)
    --
    -- data C = CDot | CSlash | CChar
    --   deriving (Eq, Ord, Enum, Bounded, Show)
    --
    -- reservedR :: ERE C
    -- reservedR = notChar CSlash
    --
    -- pathPieceR :: ERE C
    -- pathPieceR = intersections
    --     [ plus reservedR
    --     , ERENot (string [CDot])
    --     , ERENot (string [CDot,CDot])
    --     ]
    --
    -- filePathR :: ERE C
    -- filePathR = optional (string [CDot, CSlash]) <> pathPieceR <> star (char CSlash <> pathPieceR)
    --
    -- dirPathR :: ERE C
    -- dirPathR = (char CDot \/ filePathR) <> optional (char CSlash)
    --
    -- plus :: ERE C -> ERE C
    -- plus r = r <> star r
    --
    -- optional :: ERE C -> ERE C
    -- optional r = mempty \/ r
    -- @
    --
    -- Results in following state machine for @filePathR@
    --
    -- @
    -- 0 -> \x -> if
    --     | x <= CDot           -> 1
    --     | otherwise           -> 5
    -- 1 -> \x -> if
    --     | x <= CDot           -> 4
    --     | x <= CSlash         -> 2
    --     | otherwise           -> 5
    -- 2 -> \x -> if
    --     | x <= CDot           -> 3
    --     | otherwise           -> 5
    -- 3 -> \x -> if
    --     | x <= CDot           -> 4
    --     | otherwise           -> 5
    -- 4 -> \x -> if
    --     | x <= CDot           -> 5
    --     | otherwise           -> 5
    -- 5+ -> \x -> if
    --     | x <= CDot           -> 5
    --     | x <= CSlash         -> 2
    --     | otherwise           -> 5
    -- @
    --
    -- and @dirPathR@:
    --
    -- @
    -- 0 -> \x -> if
    --     | x <= CDot           -> 5
    --     | otherwise           -> 4
    -- 1+ -> \x -> if
    --     | x <= CDot           -> 2
    --     | otherwise           -> 4
    -- 2 -> \x -> if
    --     | x <= CDot           -> 3
    --     | otherwise           -> 4
    -- 3 -> \x -> if
    --     | x <= CDot           -> 4
    --     | otherwise           -> 4
    -- 4+ -> \x -> if
    --     | x <= CDot           -> 4
    --     | x <= CSlash         -> 1
    --     | otherwise           -> 4
    -- 5+ -> \x -> if
    --     | x <= CDot           -> 3
    --     | x <= CSlash         -> 1
    --     | otherwise           -> 4
    -- @