{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE RankNTypes #-}

-- |
-- Module      : Clod.Types
-- Description : Core types for the Clod application
-- Copyright   : (c) Fuzz Leonard, 2025
-- License     : MIT
-- Maintainer  : ink@fuzz.ink
-- Stability   : experimental
--
-- This module defines the core types used throughout the Clod application.
-- Clod is a utility for preparing and uploading files to Claude AI's Project Knowledge
-- feature. It tracks file changes, respects .gitignore and .clodignore patterns, and
-- optimizes filenames for Claude's UI.
--
-- The primary types include:
--
-- * 'ClodConfig' - Configuration for file processing and staging
-- * 'ClodM' - A monad for handling errors during file operations
-- * 'ClodError' - Various error types that can occur during operation
-- * 'FileResult' - Result of processing a file (success or skipped)

module Clod.Types
  ( -- * Core Types
    ClodConfig(..)
  , FileResult(..)
  , ClodError(..)
  , DatabaseErrorType(..)
  , ClodM
  , FileEntry(..)
  , ClodDatabase(..)
  , SerializableClodDatabase(..)
  , toSerializable
  , fromSerializable
  
    -- * Validation types and functions
  , Validated(..)
  , validatedToEither
  , eitherToValidated

    -- * Type conversions and runners
  , runClodM
  , throwError
  , catchError
  , liftIO
  , ask
  , asks
  , local
  , runReaderT
  , runExceptT
  
    -- * Newtypes for type safety
  , IgnorePattern(..)
  , OptimizedName(..)
  , OriginalPath(..)
  , Checksum(..)
  
    -- * Capability types
  , FileReadCap(..)
  , FileWriteCap(..)
  , fileReadCap
  , fileWriteCap
  
    -- * Path validation
  , isPathAllowed
    
    -- * Lens operators and accessors
  , (^.), (.~), (%~), (&)
    
    -- * Field lenses for ClodConfig
  , projectPath
  , stagingDir
  , configDir
  , databaseFile
  , timestamp
  , currentStaging
  , previousStaging
  , testMode
  , verbose
  , flushMode
  , lastMode
  , ignorePatterns
    
    -- * Field lenses for ClodDatabase
  , dbFiles
  , dbChecksums
  , dbLastStagingDir
  , dbLastRunTime
    
    -- * Field lenses for FileEntry
  , entryPath
  , entryChecksum
  , entryLastModified
  , entryOptimizedName
    
    -- * Field lenses for capability types
  , allowedReadDirs
  , allowedWriteDirs
  ) where

import Control.Monad.Except (ExceptT, runExceptT, throwError, catchError)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (ReaderT, ask, asks, local, runReaderT)
import Control.Lens (Lens', lens, (^.), (.~), (%~), (&), makePrisms)
import Data.String (IsString(..))
import GHC.Generics (Generic)
import Data.List (isPrefixOf)
import System.Directory (canonicalizePath)
import Data.Time.Clock (UTCTime)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Dhall (FromDhall, ToDhall)
import Data.Aeson (FromJSON, ToJSON)

-- | Newtype for ignore patterns to prevent mixing with other string types
newtype IgnorePattern = IgnorePattern { IgnorePattern -> FilePath
unIgnorePattern :: String }
  deriving (Int -> IgnorePattern -> ShowS
[IgnorePattern] -> ShowS
IgnorePattern -> FilePath
(Int -> IgnorePattern -> ShowS)
-> (IgnorePattern -> FilePath)
-> ([IgnorePattern] -> ShowS)
-> Show IgnorePattern
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> IgnorePattern -> ShowS
showsPrec :: Int -> IgnorePattern -> ShowS
$cshow :: IgnorePattern -> FilePath
show :: IgnorePattern -> FilePath
$cshowList :: [IgnorePattern] -> ShowS
showList :: [IgnorePattern] -> ShowS
Show, IgnorePattern -> IgnorePattern -> Bool
(IgnorePattern -> IgnorePattern -> Bool)
-> (IgnorePattern -> IgnorePattern -> Bool) -> Eq IgnorePattern
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: IgnorePattern -> IgnorePattern -> Bool
== :: IgnorePattern -> IgnorePattern -> Bool
$c/= :: IgnorePattern -> IgnorePattern -> Bool
/= :: IgnorePattern -> IgnorePattern -> Bool
Eq, Eq IgnorePattern
Eq IgnorePattern =>
(IgnorePattern -> IgnorePattern -> Ordering)
-> (IgnorePattern -> IgnorePattern -> Bool)
-> (IgnorePattern -> IgnorePattern -> Bool)
-> (IgnorePattern -> IgnorePattern -> Bool)
-> (IgnorePattern -> IgnorePattern -> Bool)
-> (IgnorePattern -> IgnorePattern -> IgnorePattern)
-> (IgnorePattern -> IgnorePattern -> IgnorePattern)
-> Ord IgnorePattern
IgnorePattern -> IgnorePattern -> Bool
IgnorePattern -> IgnorePattern -> Ordering
IgnorePattern -> IgnorePattern -> IgnorePattern
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: IgnorePattern -> IgnorePattern -> Ordering
compare :: IgnorePattern -> IgnorePattern -> Ordering
$c< :: IgnorePattern -> IgnorePattern -> Bool
< :: IgnorePattern -> IgnorePattern -> Bool
$c<= :: IgnorePattern -> IgnorePattern -> Bool
<= :: IgnorePattern -> IgnorePattern -> Bool
$c> :: IgnorePattern -> IgnorePattern -> Bool
> :: IgnorePattern -> IgnorePattern -> Bool
$c>= :: IgnorePattern -> IgnorePattern -> Bool
>= :: IgnorePattern -> IgnorePattern -> Bool
$cmax :: IgnorePattern -> IgnorePattern -> IgnorePattern
max :: IgnorePattern -> IgnorePattern -> IgnorePattern
$cmin :: IgnorePattern -> IgnorePattern -> IgnorePattern
min :: IgnorePattern -> IgnorePattern -> IgnorePattern
Ord) via String
  deriving (FilePath -> IgnorePattern
(FilePath -> IgnorePattern) -> IsString IgnorePattern
forall a. (FilePath -> a) -> IsString a
$cfromString :: FilePath -> IgnorePattern
fromString :: FilePath -> IgnorePattern
IsString, NonEmpty IgnorePattern -> IgnorePattern
IgnorePattern -> IgnorePattern -> IgnorePattern
(IgnorePattern -> IgnorePattern -> IgnorePattern)
-> (NonEmpty IgnorePattern -> IgnorePattern)
-> (forall b. Integral b => b -> IgnorePattern -> IgnorePattern)
-> Semigroup IgnorePattern
forall b. Integral b => b -> IgnorePattern -> IgnorePattern
forall a.
(a -> a -> a)
-> (NonEmpty a -> a)
-> (forall b. Integral b => b -> a -> a)
-> Semigroup a
$c<> :: IgnorePattern -> IgnorePattern -> IgnorePattern
<> :: IgnorePattern -> IgnorePattern -> IgnorePattern
$csconcat :: NonEmpty IgnorePattern -> IgnorePattern
sconcat :: NonEmpty IgnorePattern -> IgnorePattern
$cstimes :: forall b. Integral b => b -> IgnorePattern -> IgnorePattern
stimes :: forall b. Integral b => b -> IgnorePattern -> IgnorePattern
Semigroup, Semigroup IgnorePattern
IgnorePattern
Semigroup IgnorePattern =>
IgnorePattern
-> (IgnorePattern -> IgnorePattern -> IgnorePattern)
-> ([IgnorePattern] -> IgnorePattern)
-> Monoid IgnorePattern
[IgnorePattern] -> IgnorePattern
IgnorePattern -> IgnorePattern -> IgnorePattern
forall a.
Semigroup a =>
a -> (a -> a -> a) -> ([a] -> a) -> Monoid a
$cmempty :: IgnorePattern
mempty :: IgnorePattern
$cmappend :: IgnorePattern -> IgnorePattern -> IgnorePattern
mappend :: IgnorePattern -> IgnorePattern -> IgnorePattern
$cmconcat :: [IgnorePattern] -> IgnorePattern
mconcat :: [IgnorePattern] -> IgnorePattern
Monoid) via String

-- | Newtype for optimized filename used in Claude's UI
newtype OptimizedName = OptimizedName { OptimizedName -> FilePath
unOptimizedName :: String }
  deriving (Int -> OptimizedName -> ShowS
[OptimizedName] -> ShowS
OptimizedName -> FilePath
(Int -> OptimizedName -> ShowS)
-> (OptimizedName -> FilePath)
-> ([OptimizedName] -> ShowS)
-> Show OptimizedName
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> OptimizedName -> ShowS
showsPrec :: Int -> OptimizedName -> ShowS
$cshow :: OptimizedName -> FilePath
show :: OptimizedName -> FilePath
$cshowList :: [OptimizedName] -> ShowS
showList :: [OptimizedName] -> ShowS
Show, OptimizedName -> OptimizedName -> Bool
(OptimizedName -> OptimizedName -> Bool)
-> (OptimizedName -> OptimizedName -> Bool) -> Eq OptimizedName
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: OptimizedName -> OptimizedName -> Bool
== :: OptimizedName -> OptimizedName -> Bool
$c/= :: OptimizedName -> OptimizedName -> Bool
/= :: OptimizedName -> OptimizedName -> Bool
Eq, Eq OptimizedName
Eq OptimizedName =>
(OptimizedName -> OptimizedName -> Ordering)
-> (OptimizedName -> OptimizedName -> Bool)
-> (OptimizedName -> OptimizedName -> Bool)
-> (OptimizedName -> OptimizedName -> Bool)
-> (OptimizedName -> OptimizedName -> Bool)
-> (OptimizedName -> OptimizedName -> OptimizedName)
-> (OptimizedName -> OptimizedName -> OptimizedName)
-> Ord OptimizedName
OptimizedName -> OptimizedName -> Bool
OptimizedName -> OptimizedName -> Ordering
OptimizedName -> OptimizedName -> OptimizedName
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: OptimizedName -> OptimizedName -> Ordering
compare :: OptimizedName -> OptimizedName -> Ordering
$c< :: OptimizedName -> OptimizedName -> Bool
< :: OptimizedName -> OptimizedName -> Bool
$c<= :: OptimizedName -> OptimizedName -> Bool
<= :: OptimizedName -> OptimizedName -> Bool
$c> :: OptimizedName -> OptimizedName -> Bool
> :: OptimizedName -> OptimizedName -> Bool
$c>= :: OptimizedName -> OptimizedName -> Bool
>= :: OptimizedName -> OptimizedName -> Bool
$cmax :: OptimizedName -> OptimizedName -> OptimizedName
max :: OptimizedName -> OptimizedName -> OptimizedName
$cmin :: OptimizedName -> OptimizedName -> OptimizedName
min :: OptimizedName -> OptimizedName -> OptimizedName
Ord) via String
  deriving (FilePath -> OptimizedName
(FilePath -> OptimizedName) -> IsString OptimizedName
forall a. (FilePath -> a) -> IsString a
$cfromString :: FilePath -> OptimizedName
fromString :: FilePath -> OptimizedName
IsString, NonEmpty OptimizedName -> OptimizedName
OptimizedName -> OptimizedName -> OptimizedName
(OptimizedName -> OptimizedName -> OptimizedName)
-> (NonEmpty OptimizedName -> OptimizedName)
-> (forall b. Integral b => b -> OptimizedName -> OptimizedName)
-> Semigroup OptimizedName
forall b. Integral b => b -> OptimizedName -> OptimizedName
forall a.
(a -> a -> a)
-> (NonEmpty a -> a)
-> (forall b. Integral b => b -> a -> a)
-> Semigroup a
$c<> :: OptimizedName -> OptimizedName -> OptimizedName
<> :: OptimizedName -> OptimizedName -> OptimizedName
$csconcat :: NonEmpty OptimizedName -> OptimizedName
sconcat :: NonEmpty OptimizedName -> OptimizedName
$cstimes :: forall b. Integral b => b -> OptimizedName -> OptimizedName
stimes :: forall b. Integral b => b -> OptimizedName -> OptimizedName
Semigroup, Semigroup OptimizedName
OptimizedName
Semigroup OptimizedName =>
OptimizedName
-> (OptimizedName -> OptimizedName -> OptimizedName)
-> ([OptimizedName] -> OptimizedName)
-> Monoid OptimizedName
[OptimizedName] -> OptimizedName
OptimizedName -> OptimizedName -> OptimizedName
forall a.
Semigroup a =>
a -> (a -> a -> a) -> ([a] -> a) -> Monoid a
$cmempty :: OptimizedName
mempty :: OptimizedName
$cmappend :: OptimizedName -> OptimizedName -> OptimizedName
mappend :: OptimizedName -> OptimizedName -> OptimizedName
$cmconcat :: [OptimizedName] -> OptimizedName
mconcat :: [OptimizedName] -> OptimizedName
Monoid) via String
  deriving ((forall x. OptimizedName -> Rep OptimizedName x)
-> (forall x. Rep OptimizedName x -> OptimizedName)
-> Generic OptimizedName
forall x. Rep OptimizedName x -> OptimizedName
forall x. OptimizedName -> Rep OptimizedName x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. OptimizedName -> Rep OptimizedName x
from :: forall x. OptimizedName -> Rep OptimizedName x
$cto :: forall x. Rep OptimizedName x -> OptimizedName
to :: forall x. Rep OptimizedName x -> OptimizedName
Generic)

instance FromDhall OptimizedName
instance ToDhall OptimizedName
instance FromJSON OptimizedName
instance ToJSON OptimizedName

-- | Newtype for original filepath in the repository
newtype OriginalPath = OriginalPath { OriginalPath -> FilePath
unOriginalPath :: String }
  deriving (Int -> OriginalPath -> ShowS
[OriginalPath] -> ShowS
OriginalPath -> FilePath
(Int -> OriginalPath -> ShowS)
-> (OriginalPath -> FilePath)
-> ([OriginalPath] -> ShowS)
-> Show OriginalPath
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> OriginalPath -> ShowS
showsPrec :: Int -> OriginalPath -> ShowS
$cshow :: OriginalPath -> FilePath
show :: OriginalPath -> FilePath
$cshowList :: [OriginalPath] -> ShowS
showList :: [OriginalPath] -> ShowS
Show, OriginalPath -> OriginalPath -> Bool
(OriginalPath -> OriginalPath -> Bool)
-> (OriginalPath -> OriginalPath -> Bool) -> Eq OriginalPath
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: OriginalPath -> OriginalPath -> Bool
== :: OriginalPath -> OriginalPath -> Bool
$c/= :: OriginalPath -> OriginalPath -> Bool
/= :: OriginalPath -> OriginalPath -> Bool
Eq, Eq OriginalPath
Eq OriginalPath =>
(OriginalPath -> OriginalPath -> Ordering)
-> (OriginalPath -> OriginalPath -> Bool)
-> (OriginalPath -> OriginalPath -> Bool)
-> (OriginalPath -> OriginalPath -> Bool)
-> (OriginalPath -> OriginalPath -> Bool)
-> (OriginalPath -> OriginalPath -> OriginalPath)
-> (OriginalPath -> OriginalPath -> OriginalPath)
-> Ord OriginalPath
OriginalPath -> OriginalPath -> Bool
OriginalPath -> OriginalPath -> Ordering
OriginalPath -> OriginalPath -> OriginalPath
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: OriginalPath -> OriginalPath -> Ordering
compare :: OriginalPath -> OriginalPath -> Ordering
$c< :: OriginalPath -> OriginalPath -> Bool
< :: OriginalPath -> OriginalPath -> Bool
$c<= :: OriginalPath -> OriginalPath -> Bool
<= :: OriginalPath -> OriginalPath -> Bool
$c> :: OriginalPath -> OriginalPath -> Bool
> :: OriginalPath -> OriginalPath -> Bool
$c>= :: OriginalPath -> OriginalPath -> Bool
>= :: OriginalPath -> OriginalPath -> Bool
$cmax :: OriginalPath -> OriginalPath -> OriginalPath
max :: OriginalPath -> OriginalPath -> OriginalPath
$cmin :: OriginalPath -> OriginalPath -> OriginalPath
min :: OriginalPath -> OriginalPath -> OriginalPath
Ord) via String
  deriving (FilePath -> OriginalPath
(FilePath -> OriginalPath) -> IsString OriginalPath
forall a. (FilePath -> a) -> IsString a
$cfromString :: FilePath -> OriginalPath
fromString :: FilePath -> OriginalPath
IsString, NonEmpty OriginalPath -> OriginalPath
OriginalPath -> OriginalPath -> OriginalPath
(OriginalPath -> OriginalPath -> OriginalPath)
-> (NonEmpty OriginalPath -> OriginalPath)
-> (forall b. Integral b => b -> OriginalPath -> OriginalPath)
-> Semigroup OriginalPath
forall b. Integral b => b -> OriginalPath -> OriginalPath
forall a.
(a -> a -> a)
-> (NonEmpty a -> a)
-> (forall b. Integral b => b -> a -> a)
-> Semigroup a
$c<> :: OriginalPath -> OriginalPath -> OriginalPath
<> :: OriginalPath -> OriginalPath -> OriginalPath
$csconcat :: NonEmpty OriginalPath -> OriginalPath
sconcat :: NonEmpty OriginalPath -> OriginalPath
$cstimes :: forall b. Integral b => b -> OriginalPath -> OriginalPath
stimes :: forall b. Integral b => b -> OriginalPath -> OriginalPath
Semigroup, Semigroup OriginalPath
OriginalPath
Semigroup OriginalPath =>
OriginalPath
-> (OriginalPath -> OriginalPath -> OriginalPath)
-> ([OriginalPath] -> OriginalPath)
-> Monoid OriginalPath
[OriginalPath] -> OriginalPath
OriginalPath -> OriginalPath -> OriginalPath
forall a.
Semigroup a =>
a -> (a -> a -> a) -> ([a] -> a) -> Monoid a
$cmempty :: OriginalPath
mempty :: OriginalPath
$cmappend :: OriginalPath -> OriginalPath -> OriginalPath
mappend :: OriginalPath -> OriginalPath -> OriginalPath
$cmconcat :: [OriginalPath] -> OriginalPath
mconcat :: [OriginalPath] -> OriginalPath
Monoid) via String
  deriving ((forall x. OriginalPath -> Rep OriginalPath x)
-> (forall x. Rep OriginalPath x -> OriginalPath)
-> Generic OriginalPath
forall x. Rep OriginalPath x -> OriginalPath
forall x. OriginalPath -> Rep OriginalPath x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. OriginalPath -> Rep OriginalPath x
from :: forall x. OriginalPath -> Rep OriginalPath x
$cto :: forall x. Rep OriginalPath x -> OriginalPath
to :: forall x. Rep OriginalPath x -> OriginalPath
Generic)

instance FromDhall OriginalPath
instance ToDhall OriginalPath
instance FromJSON OriginalPath
instance ToJSON OriginalPath

-- | Configuration for the clod program
data ClodConfig = ClodConfig
  { ClodConfig -> FilePath
_projectPath    :: !FilePath      -- ^ Root path of the project
  , ClodConfig -> FilePath
_stagingDir     :: !FilePath      -- ^ Directory where files will be staged for Claude
  , ClodConfig -> FilePath
_configDir      :: !FilePath      -- ^ Directory for configuration files
  , ClodConfig -> FilePath
_databaseFile   :: !FilePath      -- ^ Path to the checksums database file
  , ClodConfig -> FilePath
_timestamp      :: !String        -- ^ Timestamp for the current run
  , ClodConfig -> FilePath
_currentStaging :: !FilePath      -- ^ Path to the current staging directory
  , ClodConfig -> Maybe FilePath
_previousStaging :: !(Maybe FilePath) -- ^ Path to the previous staging directory, if any
  , ClodConfig -> Bool
_testMode       :: !Bool          -- ^ Whether we're running in test mode
  , ClodConfig -> Bool
_verbose        :: !Bool          -- ^ Whether to print verbose output
  , ClodConfig -> Bool
_flushMode      :: !Bool          -- ^ Whether to flush stale entries from the database
  , ClodConfig -> Bool
_lastMode       :: !Bool          -- ^ Whether to use the previous staging directory
  , ClodConfig -> [IgnorePattern]
_ignorePatterns :: ![IgnorePattern] -- ^ Patterns from .gitignore and .clodignore
  } deriving stock (Int -> ClodConfig -> ShowS
[ClodConfig] -> ShowS
ClodConfig -> FilePath
(Int -> ClodConfig -> ShowS)
-> (ClodConfig -> FilePath)
-> ([ClodConfig] -> ShowS)
-> Show ClodConfig
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ClodConfig -> ShowS
showsPrec :: Int -> ClodConfig -> ShowS
$cshow :: ClodConfig -> FilePath
show :: ClodConfig -> FilePath
$cshowList :: [ClodConfig] -> ShowS
showList :: [ClodConfig] -> ShowS
Show, ClodConfig -> ClodConfig -> Bool
(ClodConfig -> ClodConfig -> Bool)
-> (ClodConfig -> ClodConfig -> Bool) -> Eq ClodConfig
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ClodConfig -> ClodConfig -> Bool
== :: ClodConfig -> ClodConfig -> Bool
$c/= :: ClodConfig -> ClodConfig -> Bool
/= :: ClodConfig -> ClodConfig -> Bool
Eq, (forall x. ClodConfig -> Rep ClodConfig x)
-> (forall x. Rep ClodConfig x -> ClodConfig) -> Generic ClodConfig
forall x. Rep ClodConfig x -> ClodConfig
forall x. ClodConfig -> Rep ClodConfig x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. ClodConfig -> Rep ClodConfig x
from :: forall x. ClodConfig -> Rep ClodConfig x
$cto :: forall x. Rep ClodConfig x -> ClodConfig
to :: forall x. Rep ClodConfig x -> ClodConfig
Generic)

-- | Lens for projectPath field
projectPath :: Lens' ClodConfig FilePath
projectPath :: Lens' ClodConfig FilePath
projectPath = (ClodConfig -> FilePath)
-> (ClodConfig -> FilePath -> ClodConfig)
-> Lens' ClodConfig FilePath
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> FilePath
_projectPath (\ClodConfig
c FilePath
v -> ClodConfig
c { _projectPath = v })

-- | Lens for stagingDir field
stagingDir :: Lens' ClodConfig FilePath
stagingDir :: Lens' ClodConfig FilePath
stagingDir = (ClodConfig -> FilePath)
-> (ClodConfig -> FilePath -> ClodConfig)
-> Lens' ClodConfig FilePath
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> FilePath
_stagingDir (\ClodConfig
c FilePath
v -> ClodConfig
c { _stagingDir = v })

-- | Lens for configDir field
configDir :: Lens' ClodConfig FilePath
configDir :: Lens' ClodConfig FilePath
configDir = (ClodConfig -> FilePath)
-> (ClodConfig -> FilePath -> ClodConfig)
-> Lens' ClodConfig FilePath
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> FilePath
_configDir (\ClodConfig
c FilePath
v -> ClodConfig
c { _configDir = v })

-- | Lens for databaseFile field
databaseFile :: Lens' ClodConfig FilePath
databaseFile :: Lens' ClodConfig FilePath
databaseFile = (ClodConfig -> FilePath)
-> (ClodConfig -> FilePath -> ClodConfig)
-> Lens' ClodConfig FilePath
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> FilePath
_databaseFile (\ClodConfig
c FilePath
v -> ClodConfig
c { _databaseFile = v })

-- | Lens for timestamp field
timestamp :: Lens' ClodConfig String
timestamp :: Lens' ClodConfig FilePath
timestamp = (ClodConfig -> FilePath)
-> (ClodConfig -> FilePath -> ClodConfig)
-> Lens' ClodConfig FilePath
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> FilePath
_timestamp (\ClodConfig
c FilePath
v -> ClodConfig
c { _timestamp = v })

-- | Lens for currentStaging field
currentStaging :: Lens' ClodConfig FilePath
currentStaging :: Lens' ClodConfig FilePath
currentStaging = (ClodConfig -> FilePath)
-> (ClodConfig -> FilePath -> ClodConfig)
-> Lens' ClodConfig FilePath
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> FilePath
_currentStaging (\ClodConfig
c FilePath
v -> ClodConfig
c { _currentStaging = v })

-- | Lens for previousStaging field
previousStaging :: Lens' ClodConfig (Maybe FilePath)
previousStaging :: Lens' ClodConfig (Maybe FilePath)
previousStaging = (ClodConfig -> Maybe FilePath)
-> (ClodConfig -> Maybe FilePath -> ClodConfig)
-> Lens' ClodConfig (Maybe FilePath)
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> Maybe FilePath
_previousStaging (\ClodConfig
c Maybe FilePath
v -> ClodConfig
c { _previousStaging = v })

-- | Lens for testMode field
testMode :: Lens' ClodConfig Bool
testMode :: Lens' ClodConfig Bool
testMode = (ClodConfig -> Bool)
-> (ClodConfig -> Bool -> ClodConfig) -> Lens' ClodConfig Bool
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> Bool
_testMode (\ClodConfig
c Bool
v -> ClodConfig
c { _testMode = v })

-- | Lens for verbose field
verbose :: Lens' ClodConfig Bool
verbose :: Lens' ClodConfig Bool
verbose = (ClodConfig -> Bool)
-> (ClodConfig -> Bool -> ClodConfig) -> Lens' ClodConfig Bool
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> Bool
_verbose (\ClodConfig
c Bool
v -> ClodConfig
c { _verbose = v })

-- | Lens for flushMode field
flushMode :: Lens' ClodConfig Bool
flushMode :: Lens' ClodConfig Bool
flushMode = (ClodConfig -> Bool)
-> (ClodConfig -> Bool -> ClodConfig) -> Lens' ClodConfig Bool
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> Bool
_flushMode (\ClodConfig
c Bool
v -> ClodConfig
c { _flushMode = v })

-- | Lens for lastMode field
lastMode :: Lens' ClodConfig Bool
lastMode :: Lens' ClodConfig Bool
lastMode = (ClodConfig -> Bool)
-> (ClodConfig -> Bool -> ClodConfig) -> Lens' ClodConfig Bool
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> Bool
_lastMode (\ClodConfig
c Bool
v -> ClodConfig
c { _lastMode = v })

-- | Lens for ignorePatterns field
ignorePatterns :: Lens' ClodConfig [IgnorePattern]
ignorePatterns :: Lens' ClodConfig [IgnorePattern]
ignorePatterns = (ClodConfig -> [IgnorePattern])
-> (ClodConfig -> [IgnorePattern] -> ClodConfig)
-> Lens' ClodConfig [IgnorePattern]
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodConfig -> [IgnorePattern]
_ignorePatterns (\ClodConfig
c [IgnorePattern]
v -> ClodConfig
c { _ignorePatterns = v })

-- | Result of processing a file
-- 
-- * 'Success' indicates the file was successfully processed and included
-- * 'Skipped' indicates the file was skipped with a reason (matched ignore pattern, binary file, etc.)
data FileResult 
  = Success              -- ^ File was successfully processed
  | Skipped !String      -- ^ File was skipped with the given reason
  deriving stock (Int -> FileResult -> ShowS
[FileResult] -> ShowS
FileResult -> FilePath
(Int -> FileResult -> ShowS)
-> (FileResult -> FilePath)
-> ([FileResult] -> ShowS)
-> Show FileResult
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> FileResult -> ShowS
showsPrec :: Int -> FileResult -> ShowS
$cshow :: FileResult -> FilePath
show :: FileResult -> FilePath
$cshowList :: [FileResult] -> ShowS
showList :: [FileResult] -> ShowS
Show, FileResult -> FileResult -> Bool
(FileResult -> FileResult -> Bool)
-> (FileResult -> FileResult -> Bool) -> Eq FileResult
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: FileResult -> FileResult -> Bool
== :: FileResult -> FileResult -> Bool
$c/= :: FileResult -> FileResult -> Bool
/= :: FileResult -> FileResult -> Bool
Eq, (forall x. FileResult -> Rep FileResult x)
-> (forall x. Rep FileResult x -> FileResult) -> Generic FileResult
forall x. Rep FileResult x -> FileResult
forall x. FileResult -> Rep FileResult x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. FileResult -> Rep FileResult x
from :: forall x. FileResult -> Rep FileResult x
$cto :: forall x. Rep FileResult x -> FileResult
to :: forall x. Rep FileResult x -> FileResult
Generic)

-- | Database error types for more specific error reporting
data DatabaseErrorType 
  = DBFileNotFound      -- ^ Database file could not be found
  | DBCorrupted String  -- ^ Database file is corrupted with details
  | DBVersionMismatch   -- ^ Database version is incompatible
  | DBOtherError String -- ^ Other database error with description
  deriving stock (Int -> DatabaseErrorType -> ShowS
[DatabaseErrorType] -> ShowS
DatabaseErrorType -> FilePath
(Int -> DatabaseErrorType -> ShowS)
-> (DatabaseErrorType -> FilePath)
-> ([DatabaseErrorType] -> ShowS)
-> Show DatabaseErrorType
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> DatabaseErrorType -> ShowS
showsPrec :: Int -> DatabaseErrorType -> ShowS
$cshow :: DatabaseErrorType -> FilePath
show :: DatabaseErrorType -> FilePath
$cshowList :: [DatabaseErrorType] -> ShowS
showList :: [DatabaseErrorType] -> ShowS
Show, DatabaseErrorType -> DatabaseErrorType -> Bool
(DatabaseErrorType -> DatabaseErrorType -> Bool)
-> (DatabaseErrorType -> DatabaseErrorType -> Bool)
-> Eq DatabaseErrorType
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: DatabaseErrorType -> DatabaseErrorType -> Bool
== :: DatabaseErrorType -> DatabaseErrorType -> Bool
$c/= :: DatabaseErrorType -> DatabaseErrorType -> Bool
/= :: DatabaseErrorType -> DatabaseErrorType -> Bool
Eq, (forall x. DatabaseErrorType -> Rep DatabaseErrorType x)
-> (forall x. Rep DatabaseErrorType x -> DatabaseErrorType)
-> Generic DatabaseErrorType
forall x. Rep DatabaseErrorType x -> DatabaseErrorType
forall x. DatabaseErrorType -> Rep DatabaseErrorType x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. DatabaseErrorType -> Rep DatabaseErrorType x
from :: forall x. DatabaseErrorType -> Rep DatabaseErrorType x
$cto :: forall x. Rep DatabaseErrorType x -> DatabaseErrorType
to :: forall x. Rep DatabaseErrorType x -> DatabaseErrorType
Generic)

-- | Errors that can occur during Clod operation
--
-- These represent the different categories of errors that can occur during
-- file processing, allowing for specific error handling for each case.
data ClodError 
  = FileSystemError !FilePath !IOError        -- ^ Error related to filesystem operations
  | ConfigError !String                       -- ^ Error related to configuration (e.g., invalid settings)
  | PatternError !String                      -- ^ Error related to pattern matching (e.g., invalid pattern)
  | CapabilityError !FilePath !String         -- ^ Error related to capability validation with the path
  | DatabaseError !FilePath !DatabaseErrorType -- ^ Error related to checksums database with file and type
  | ChecksumError !FilePath !String           -- ^ Error related to checksum calculation with the file
  deriving stock (Int -> ClodError -> ShowS
[ClodError] -> ShowS
ClodError -> FilePath
(Int -> ClodError -> ShowS)
-> (ClodError -> FilePath)
-> ([ClodError] -> ShowS)
-> Show ClodError
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ClodError -> ShowS
showsPrec :: Int -> ClodError -> ShowS
$cshow :: ClodError -> FilePath
show :: ClodError -> FilePath
$cshowList :: [ClodError] -> ShowS
showList :: [ClodError] -> ShowS
Show, ClodError -> ClodError -> Bool
(ClodError -> ClodError -> Bool)
-> (ClodError -> ClodError -> Bool) -> Eq ClodError
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ClodError -> ClodError -> Bool
== :: ClodError -> ClodError -> Bool
$c/= :: ClodError -> ClodError -> Bool
/= :: ClodError -> ClodError -> Bool
Eq, (forall x. ClodError -> Rep ClodError x)
-> (forall x. Rep ClodError x -> ClodError) -> Generic ClodError
forall x. Rep ClodError x -> ClodError
forall x. ClodError -> Rep ClodError x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. ClodError -> Rep ClodError x
from :: forall x. ClodError -> Rep ClodError x
$cto :: forall x. Rep ClodError x -> ClodError
to :: forall x. Rep ClodError x -> ClodError
Generic)

-- Generate prisms for ClodError
makePrisms ''ClodError

-- | Validation type for collecting multiple errors
-- This allows us to accumulate errors instead of stopping at the first one
data Validated a = Invalid [ClodError] | Valid a
  deriving stock ((forall a b. (a -> b) -> Validated a -> Validated b)
-> (forall a b. a -> Validated b -> Validated a)
-> Functor Validated
forall a b. a -> Validated b -> Validated a
forall a b. (a -> b) -> Validated a -> Validated b
forall (f :: * -> *).
(forall a b. (a -> b) -> f a -> f b)
-> (forall a b. a -> f b -> f a) -> Functor f
$cfmap :: forall a b. (a -> b) -> Validated a -> Validated b
fmap :: forall a b. (a -> b) -> Validated a -> Validated b
$c<$ :: forall a b. a -> Validated b -> Validated a
<$ :: forall a b. a -> Validated b -> Validated a
Functor, Int -> Validated a -> ShowS
[Validated a] -> ShowS
Validated a -> FilePath
(Int -> Validated a -> ShowS)
-> (Validated a -> FilePath)
-> ([Validated a] -> ShowS)
-> Show (Validated a)
forall a. Show a => Int -> Validated a -> ShowS
forall a. Show a => [Validated a] -> ShowS
forall a. Show a => Validated a -> FilePath
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: forall a. Show a => Int -> Validated a -> ShowS
showsPrec :: Int -> Validated a -> ShowS
$cshow :: forall a. Show a => Validated a -> FilePath
show :: Validated a -> FilePath
$cshowList :: forall a. Show a => [Validated a] -> ShowS
showList :: [Validated a] -> ShowS
Show, Validated a -> Validated a -> Bool
(Validated a -> Validated a -> Bool)
-> (Validated a -> Validated a -> Bool) -> Eq (Validated a)
forall a. Eq a => Validated a -> Validated a -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: forall a. Eq a => Validated a -> Validated a -> Bool
== :: Validated a -> Validated a -> Bool
$c/= :: forall a. Eq a => Validated a -> Validated a -> Bool
/= :: Validated a -> Validated a -> Bool
Eq)

instance Applicative Validated where
  pure :: forall a. a -> Validated a
pure = a -> Validated a
forall a. a -> Validated a
Valid
  Invalid [ClodError]
errs1 <*> :: forall a b. Validated (a -> b) -> Validated a -> Validated b
<*> Invalid [ClodError]
errs2 = [ClodError] -> Validated b
forall a. [ClodError] -> Validated a
Invalid ([ClodError]
errs1 [ClodError] -> [ClodError] -> [ClodError]
forall a. [a] -> [a] -> [a]
++ [ClodError]
errs2)
  Invalid [ClodError]
errs <*> Validated a
_ = [ClodError] -> Validated b
forall a. [ClodError] -> Validated a
Invalid [ClodError]
errs
  Validated (a -> b)
_ <*> Invalid [ClodError]
errs = [ClodError] -> Validated b
forall a. [ClodError] -> Validated a
Invalid [ClodError]
errs
  Valid a -> b
f <*> Valid a
a = b -> Validated b
forall a. a -> Validated a
Valid (a -> b
f a
a)

-- | Convert from Validated to Either for compatibility
validatedToEither :: Validated a -> Either ClodError a
validatedToEither :: forall a. Validated a -> Either ClodError a
validatedToEither (Valid a
a) = a -> Either ClodError a
forall a b. b -> Either a b
Right a
a
validatedToEither (Invalid []) = ClodError -> Either ClodError a
forall a b. a -> Either a b
Left (FilePath -> ClodError
ConfigError FilePath
"Unknown error")
validatedToEither (Invalid (ClodError
e:[ClodError]
_)) = ClodError -> Either ClodError a
forall a b. a -> Either a b
Left ClodError
e  -- Return first error for simplicity

-- | Convert from Either to Validated for integration
eitherToValidated :: Either ClodError a -> Validated a
eitherToValidated :: forall a. Either ClodError a -> Validated a
eitherToValidated (Right a
a) = a -> Validated a
forall a. a -> Validated a
Valid a
a
eitherToValidated (Left ClodError
e) = [ClodError] -> Validated a
forall a. [ClodError] -> Validated a
Invalid [ClodError
e]

-- | Newtype for file checksums to prevent mixing with other string types
newtype Checksum = Checksum { Checksum -> FilePath
unChecksum :: String }
  deriving (Int -> Checksum -> ShowS
[Checksum] -> ShowS
Checksum -> FilePath
(Int -> Checksum -> ShowS)
-> (Checksum -> FilePath) -> ([Checksum] -> ShowS) -> Show Checksum
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Checksum -> ShowS
showsPrec :: Int -> Checksum -> ShowS
$cshow :: Checksum -> FilePath
show :: Checksum -> FilePath
$cshowList :: [Checksum] -> ShowS
showList :: [Checksum] -> ShowS
Show, Checksum -> Checksum -> Bool
(Checksum -> Checksum -> Bool)
-> (Checksum -> Checksum -> Bool) -> Eq Checksum
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Checksum -> Checksum -> Bool
== :: Checksum -> Checksum -> Bool
$c/= :: Checksum -> Checksum -> Bool
/= :: Checksum -> Checksum -> Bool
Eq, Eq Checksum
Eq Checksum =>
(Checksum -> Checksum -> Ordering)
-> (Checksum -> Checksum -> Bool)
-> (Checksum -> Checksum -> Bool)
-> (Checksum -> Checksum -> Bool)
-> (Checksum -> Checksum -> Bool)
-> (Checksum -> Checksum -> Checksum)
-> (Checksum -> Checksum -> Checksum)
-> Ord Checksum
Checksum -> Checksum -> Bool
Checksum -> Checksum -> Ordering
Checksum -> Checksum -> Checksum
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: Checksum -> Checksum -> Ordering
compare :: Checksum -> Checksum -> Ordering
$c< :: Checksum -> Checksum -> Bool
< :: Checksum -> Checksum -> Bool
$c<= :: Checksum -> Checksum -> Bool
<= :: Checksum -> Checksum -> Bool
$c> :: Checksum -> Checksum -> Bool
> :: Checksum -> Checksum -> Bool
$c>= :: Checksum -> Checksum -> Bool
>= :: Checksum -> Checksum -> Bool
$cmax :: Checksum -> Checksum -> Checksum
max :: Checksum -> Checksum -> Checksum
$cmin :: Checksum -> Checksum -> Checksum
min :: Checksum -> Checksum -> Checksum
Ord) via String
  deriving (FilePath -> Checksum
(FilePath -> Checksum) -> IsString Checksum
forall a. (FilePath -> a) -> IsString a
$cfromString :: FilePath -> Checksum
fromString :: FilePath -> Checksum
IsString, NonEmpty Checksum -> Checksum
Checksum -> Checksum -> Checksum
(Checksum -> Checksum -> Checksum)
-> (NonEmpty Checksum -> Checksum)
-> (forall b. Integral b => b -> Checksum -> Checksum)
-> Semigroup Checksum
forall b. Integral b => b -> Checksum -> Checksum
forall a.
(a -> a -> a)
-> (NonEmpty a -> a)
-> (forall b. Integral b => b -> a -> a)
-> Semigroup a
$c<> :: Checksum -> Checksum -> Checksum
<> :: Checksum -> Checksum -> Checksum
$csconcat :: NonEmpty Checksum -> Checksum
sconcat :: NonEmpty Checksum -> Checksum
$cstimes :: forall b. Integral b => b -> Checksum -> Checksum
stimes :: forall b. Integral b => b -> Checksum -> Checksum
Semigroup, Semigroup Checksum
Checksum
Semigroup Checksum =>
Checksum
-> (Checksum -> Checksum -> Checksum)
-> ([Checksum] -> Checksum)
-> Monoid Checksum
[Checksum] -> Checksum
Checksum -> Checksum -> Checksum
forall a.
Semigroup a =>
a -> (a -> a -> a) -> ([a] -> a) -> Monoid a
$cmempty :: Checksum
mempty :: Checksum
$cmappend :: Checksum -> Checksum -> Checksum
mappend :: Checksum -> Checksum -> Checksum
$cmconcat :: [Checksum] -> Checksum
mconcat :: [Checksum] -> Checksum
Monoid) via String
  deriving ((forall x. Checksum -> Rep Checksum x)
-> (forall x. Rep Checksum x -> Checksum) -> Generic Checksum
forall x. Rep Checksum x -> Checksum
forall x. Checksum -> Rep Checksum x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. Checksum -> Rep Checksum x
from :: forall x. Checksum -> Rep Checksum x
$cto :: forall x. Rep Checksum x -> Checksum
to :: forall x. Rep Checksum x -> Checksum
Generic)

instance FromDhall Checksum
instance ToDhall Checksum
instance FromJSON Checksum
instance ToJSON Checksum

-- | File entry in the checksum database
data FileEntry = FileEntry
  { FileEntry -> FilePath
_entryPath         :: !FilePath       -- ^ Original path
  , FileEntry -> Checksum
_entryChecksum     :: !Checksum       -- ^ File content checksum
  , FileEntry -> UTCTime
_entryLastModified :: !UTCTime        -- ^ Last modified time
  , FileEntry -> OptimizedName
_entryOptimizedName :: !OptimizedName -- ^ Name in staging directory
  } deriving stock (Int -> FileEntry -> ShowS
[FileEntry] -> ShowS
FileEntry -> FilePath
(Int -> FileEntry -> ShowS)
-> (FileEntry -> FilePath)
-> ([FileEntry] -> ShowS)
-> Show FileEntry
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> FileEntry -> ShowS
showsPrec :: Int -> FileEntry -> ShowS
$cshow :: FileEntry -> FilePath
show :: FileEntry -> FilePath
$cshowList :: [FileEntry] -> ShowS
showList :: [FileEntry] -> ShowS
Show, FileEntry -> FileEntry -> Bool
(FileEntry -> FileEntry -> Bool)
-> (FileEntry -> FileEntry -> Bool) -> Eq FileEntry
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: FileEntry -> FileEntry -> Bool
== :: FileEntry -> FileEntry -> Bool
$c/= :: FileEntry -> FileEntry -> Bool
/= :: FileEntry -> FileEntry -> Bool
Eq, (forall x. FileEntry -> Rep FileEntry x)
-> (forall x. Rep FileEntry x -> FileEntry) -> Generic FileEntry
forall x. Rep FileEntry x -> FileEntry
forall x. FileEntry -> Rep FileEntry x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. FileEntry -> Rep FileEntry x
from :: forall x. FileEntry -> Rep FileEntry x
$cto :: forall x. Rep FileEntry x -> FileEntry
to :: forall x. Rep FileEntry x -> FileEntry
Generic)
    deriving anyclass (InputNormalizer -> Decoder FileEntry
(InputNormalizer -> Decoder FileEntry) -> FromDhall FileEntry
forall a. (InputNormalizer -> Decoder a) -> FromDhall a
$cautoWith :: InputNormalizer -> Decoder FileEntry
autoWith :: InputNormalizer -> Decoder FileEntry
FromDhall, InputNormalizer -> Encoder FileEntry
(InputNormalizer -> Encoder FileEntry) -> ToDhall FileEntry
forall a. (InputNormalizer -> Encoder a) -> ToDhall a
$cinjectWith :: InputNormalizer -> Encoder FileEntry
injectWith :: InputNormalizer -> Encoder FileEntry
ToDhall, Maybe FileEntry
Value -> Parser [FileEntry]
Value -> Parser FileEntry
(Value -> Parser FileEntry)
-> (Value -> Parser [FileEntry])
-> Maybe FileEntry
-> FromJSON FileEntry
forall a.
(Value -> Parser a)
-> (Value -> Parser [a]) -> Maybe a -> FromJSON a
$cparseJSON :: Value -> Parser FileEntry
parseJSON :: Value -> Parser FileEntry
$cparseJSONList :: Value -> Parser [FileEntry]
parseJSONList :: Value -> Parser [FileEntry]
$comittedField :: Maybe FileEntry
omittedField :: Maybe FileEntry
FromJSON, [FileEntry] -> Value
[FileEntry] -> Encoding
FileEntry -> Bool
FileEntry -> Value
FileEntry -> Encoding
(FileEntry -> Value)
-> (FileEntry -> Encoding)
-> ([FileEntry] -> Value)
-> ([FileEntry] -> Encoding)
-> (FileEntry -> Bool)
-> ToJSON FileEntry
forall a.
(a -> Value)
-> (a -> Encoding)
-> ([a] -> Value)
-> ([a] -> Encoding)
-> (a -> Bool)
-> ToJSON a
$ctoJSON :: FileEntry -> Value
toJSON :: FileEntry -> Value
$ctoEncoding :: FileEntry -> Encoding
toEncoding :: FileEntry -> Encoding
$ctoJSONList :: [FileEntry] -> Value
toJSONList :: [FileEntry] -> Value
$ctoEncodingList :: [FileEntry] -> Encoding
toEncodingList :: [FileEntry] -> Encoding
$comitField :: FileEntry -> Bool
omitField :: FileEntry -> Bool
ToJSON)

-- | Lens for entryPath field
entryPath :: Lens' FileEntry FilePath
entryPath :: Lens' FileEntry FilePath
entryPath = (FileEntry -> FilePath)
-> (FileEntry -> FilePath -> FileEntry) -> Lens' FileEntry FilePath
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens FileEntry -> FilePath
_entryPath (\FileEntry
e FilePath
v -> FileEntry
e { _entryPath = v })

-- | Lens for entryChecksum field
entryChecksum :: Lens' FileEntry Checksum
entryChecksum :: Lens' FileEntry Checksum
entryChecksum = (FileEntry -> Checksum)
-> (FileEntry -> Checksum -> FileEntry) -> Lens' FileEntry Checksum
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens FileEntry -> Checksum
_entryChecksum (\FileEntry
e Checksum
v -> FileEntry
e { _entryChecksum = v })

-- | Lens for entryLastModified field
entryLastModified :: Lens' FileEntry UTCTime
entryLastModified :: Lens' FileEntry UTCTime
entryLastModified = (FileEntry -> UTCTime)
-> (FileEntry -> UTCTime -> FileEntry) -> Lens' FileEntry UTCTime
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens FileEntry -> UTCTime
_entryLastModified (\FileEntry
e UTCTime
v -> FileEntry
e { _entryLastModified = v })

-- | Lens for entryOptimizedName field
entryOptimizedName :: Lens' FileEntry OptimizedName
entryOptimizedName :: Lens' FileEntry OptimizedName
entryOptimizedName = (FileEntry -> OptimizedName)
-> (FileEntry -> OptimizedName -> FileEntry)
-> Lens' FileEntry OptimizedName
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens FileEntry -> OptimizedName
_entryOptimizedName (\FileEntry
e OptimizedName
v -> FileEntry
e { _entryOptimizedName = v }) 

-- | Main database structure
data ClodDatabase = ClodDatabase
  { ClodDatabase -> Map FilePath FileEntry
_dbFiles          :: !(Map FilePath FileEntry)  -- ^ All tracked files by path
  , ClodDatabase -> Map FilePath FilePath
_dbChecksums      :: !(Map String FilePath)     -- ^ Mapping from checksum to path (for rename detection)
  , ClodDatabase -> Maybe FilePath
_dbLastStagingDir :: !(Maybe FilePath)          -- ^ Previous staging directory
  , ClodDatabase -> UTCTime
_dbLastRunTime    :: !UTCTime                  -- ^ Time of last run
  } deriving stock (Int -> ClodDatabase -> ShowS
[ClodDatabase] -> ShowS
ClodDatabase -> FilePath
(Int -> ClodDatabase -> ShowS)
-> (ClodDatabase -> FilePath)
-> ([ClodDatabase] -> ShowS)
-> Show ClodDatabase
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ClodDatabase -> ShowS
showsPrec :: Int -> ClodDatabase -> ShowS
$cshow :: ClodDatabase -> FilePath
show :: ClodDatabase -> FilePath
$cshowList :: [ClodDatabase] -> ShowS
showList :: [ClodDatabase] -> ShowS
Show, ClodDatabase -> ClodDatabase -> Bool
(ClodDatabase -> ClodDatabase -> Bool)
-> (ClodDatabase -> ClodDatabase -> Bool) -> Eq ClodDatabase
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ClodDatabase -> ClodDatabase -> Bool
== :: ClodDatabase -> ClodDatabase -> Bool
$c/= :: ClodDatabase -> ClodDatabase -> Bool
/= :: ClodDatabase -> ClodDatabase -> Bool
Eq, (forall x. ClodDatabase -> Rep ClodDatabase x)
-> (forall x. Rep ClodDatabase x -> ClodDatabase)
-> Generic ClodDatabase
forall x. Rep ClodDatabase x -> ClodDatabase
forall x. ClodDatabase -> Rep ClodDatabase x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. ClodDatabase -> Rep ClodDatabase x
from :: forall x. ClodDatabase -> Rep ClodDatabase x
$cto :: forall x. Rep ClodDatabase x -> ClodDatabase
to :: forall x. Rep ClodDatabase x -> ClodDatabase
Generic)

-- | Lens for dbFiles field
dbFiles :: Lens' ClodDatabase (Map FilePath FileEntry)
dbFiles :: Lens' ClodDatabase (Map FilePath FileEntry)
dbFiles = (ClodDatabase -> Map FilePath FileEntry)
-> (ClodDatabase -> Map FilePath FileEntry -> ClodDatabase)
-> Lens' ClodDatabase (Map FilePath FileEntry)
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodDatabase -> Map FilePath FileEntry
_dbFiles (\ClodDatabase
d Map FilePath FileEntry
v -> ClodDatabase
d { _dbFiles = v })

-- | Lens for dbChecksums field
dbChecksums :: Lens' ClodDatabase (Map String FilePath)
dbChecksums :: Lens' ClodDatabase (Map FilePath FilePath)
dbChecksums = (ClodDatabase -> Map FilePath FilePath)
-> (ClodDatabase -> Map FilePath FilePath -> ClodDatabase)
-> Lens' ClodDatabase (Map FilePath FilePath)
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodDatabase -> Map FilePath FilePath
_dbChecksums (\ClodDatabase
d Map FilePath FilePath
v -> ClodDatabase
d { _dbChecksums = v })

-- | Lens for dbLastStagingDir field
dbLastStagingDir :: Lens' ClodDatabase (Maybe FilePath)
dbLastStagingDir :: Lens' ClodDatabase (Maybe FilePath)
dbLastStagingDir = (ClodDatabase -> Maybe FilePath)
-> (ClodDatabase -> Maybe FilePath -> ClodDatabase)
-> Lens' ClodDatabase (Maybe FilePath)
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodDatabase -> Maybe FilePath
_dbLastStagingDir (\ClodDatabase
d Maybe FilePath
v -> ClodDatabase
d { _dbLastStagingDir = v })

-- | Lens for dbLastRunTime field
dbLastRunTime :: Lens' ClodDatabase UTCTime
dbLastRunTime :: Lens' ClodDatabase UTCTime
dbLastRunTime = (ClodDatabase -> UTCTime)
-> (ClodDatabase -> UTCTime -> ClodDatabase)
-> Lens' ClodDatabase UTCTime
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens ClodDatabase -> UTCTime
_dbLastRunTime (\ClodDatabase
d UTCTime
v -> ClodDatabase
d { _dbLastRunTime = v })

-- | Serialization-friendly version of ClodDatabase
data SerializableClodDatabase = SerializableClodDatabase
  { SerializableClodDatabase -> [(FilePath, FileEntry)]
serializedFiles          :: ![(FilePath, FileEntry)]  -- ^ All tracked files as pairs
  , SerializableClodDatabase -> [(FilePath, FilePath)]
serializedChecksums      :: ![(String, FilePath)]     -- ^ Checksums as pairs
  , SerializableClodDatabase -> Maybe FilePath
serializedLastStagingDir :: !(Maybe FilePath)          -- ^ Previous staging directory
  , SerializableClodDatabase -> UTCTime
serializedLastRunTime    :: !UTCTime                  -- ^ Time of last run
  } deriving stock (Int -> SerializableClodDatabase -> ShowS
[SerializableClodDatabase] -> ShowS
SerializableClodDatabase -> FilePath
(Int -> SerializableClodDatabase -> ShowS)
-> (SerializableClodDatabase -> FilePath)
-> ([SerializableClodDatabase] -> ShowS)
-> Show SerializableClodDatabase
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> SerializableClodDatabase -> ShowS
showsPrec :: Int -> SerializableClodDatabase -> ShowS
$cshow :: SerializableClodDatabase -> FilePath
show :: SerializableClodDatabase -> FilePath
$cshowList :: [SerializableClodDatabase] -> ShowS
showList :: [SerializableClodDatabase] -> ShowS
Show, SerializableClodDatabase -> SerializableClodDatabase -> Bool
(SerializableClodDatabase -> SerializableClodDatabase -> Bool)
-> (SerializableClodDatabase -> SerializableClodDatabase -> Bool)
-> Eq SerializableClodDatabase
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: SerializableClodDatabase -> SerializableClodDatabase -> Bool
== :: SerializableClodDatabase -> SerializableClodDatabase -> Bool
$c/= :: SerializableClodDatabase -> SerializableClodDatabase -> Bool
/= :: SerializableClodDatabase -> SerializableClodDatabase -> Bool
Eq, (forall x.
 SerializableClodDatabase -> Rep SerializableClodDatabase x)
-> (forall x.
    Rep SerializableClodDatabase x -> SerializableClodDatabase)
-> Generic SerializableClodDatabase
forall x.
Rep SerializableClodDatabase x -> SerializableClodDatabase
forall x.
SerializableClodDatabase -> Rep SerializableClodDatabase x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x.
SerializableClodDatabase -> Rep SerializableClodDatabase x
from :: forall x.
SerializableClodDatabase -> Rep SerializableClodDatabase x
$cto :: forall x.
Rep SerializableClodDatabase x -> SerializableClodDatabase
to :: forall x.
Rep SerializableClodDatabase x -> SerializableClodDatabase
Generic)
    deriving anyclass (InputNormalizer -> Decoder SerializableClodDatabase
(InputNormalizer -> Decoder SerializableClodDatabase)
-> FromDhall SerializableClodDatabase
forall a. (InputNormalizer -> Decoder a) -> FromDhall a
$cautoWith :: InputNormalizer -> Decoder SerializableClodDatabase
autoWith :: InputNormalizer -> Decoder SerializableClodDatabase
FromDhall, InputNormalizer -> Encoder SerializableClodDatabase
(InputNormalizer -> Encoder SerializableClodDatabase)
-> ToDhall SerializableClodDatabase
forall a. (InputNormalizer -> Encoder a) -> ToDhall a
$cinjectWith :: InputNormalizer -> Encoder SerializableClodDatabase
injectWith :: InputNormalizer -> Encoder SerializableClodDatabase
ToDhall, Maybe SerializableClodDatabase
Value -> Parser [SerializableClodDatabase]
Value -> Parser SerializableClodDatabase
(Value -> Parser SerializableClodDatabase)
-> (Value -> Parser [SerializableClodDatabase])
-> Maybe SerializableClodDatabase
-> FromJSON SerializableClodDatabase
forall a.
(Value -> Parser a)
-> (Value -> Parser [a]) -> Maybe a -> FromJSON a
$cparseJSON :: Value -> Parser SerializableClodDatabase
parseJSON :: Value -> Parser SerializableClodDatabase
$cparseJSONList :: Value -> Parser [SerializableClodDatabase]
parseJSONList :: Value -> Parser [SerializableClodDatabase]
$comittedField :: Maybe SerializableClodDatabase
omittedField :: Maybe SerializableClodDatabase
FromJSON, [SerializableClodDatabase] -> Value
[SerializableClodDatabase] -> Encoding
SerializableClodDatabase -> Bool
SerializableClodDatabase -> Value
SerializableClodDatabase -> Encoding
(SerializableClodDatabase -> Value)
-> (SerializableClodDatabase -> Encoding)
-> ([SerializableClodDatabase] -> Value)
-> ([SerializableClodDatabase] -> Encoding)
-> (SerializableClodDatabase -> Bool)
-> ToJSON SerializableClodDatabase
forall a.
(a -> Value)
-> (a -> Encoding)
-> ([a] -> Value)
-> ([a] -> Encoding)
-> (a -> Bool)
-> ToJSON a
$ctoJSON :: SerializableClodDatabase -> Value
toJSON :: SerializableClodDatabase -> Value
$ctoEncoding :: SerializableClodDatabase -> Encoding
toEncoding :: SerializableClodDatabase -> Encoding
$ctoJSONList :: [SerializableClodDatabase] -> Value
toJSONList :: [SerializableClodDatabase] -> Value
$ctoEncodingList :: [SerializableClodDatabase] -> Encoding
toEncodingList :: [SerializableClodDatabase] -> Encoding
$comitField :: SerializableClodDatabase -> Bool
omitField :: SerializableClodDatabase -> Bool
ToJSON)

-- | Convert to serializable form
toSerializable :: ClodDatabase -> SerializableClodDatabase
toSerializable :: ClodDatabase -> SerializableClodDatabase
toSerializable ClodDatabase
db = SerializableClodDatabase
  { serializedFiles :: [(FilePath, FileEntry)]
serializedFiles = Map FilePath FileEntry -> [(FilePath, FileEntry)]
forall k a. Map k a -> [(k, a)]
Map.toList (ClodDatabase
db ClodDatabase
-> Getting
     (Map FilePath FileEntry) ClodDatabase (Map FilePath FileEntry)
-> Map FilePath FileEntry
forall s a. s -> Getting a s a -> a
^. Getting
  (Map FilePath FileEntry) ClodDatabase (Map FilePath FileEntry)
Lens' ClodDatabase (Map FilePath FileEntry)
dbFiles)
  , serializedChecksums :: [(FilePath, FilePath)]
serializedChecksums = Map FilePath FilePath -> [(FilePath, FilePath)]
forall k a. Map k a -> [(k, a)]
Map.toList (ClodDatabase
db ClodDatabase
-> Getting
     (Map FilePath FilePath) ClodDatabase (Map FilePath FilePath)
-> Map FilePath FilePath
forall s a. s -> Getting a s a -> a
^. Getting
  (Map FilePath FilePath) ClodDatabase (Map FilePath FilePath)
Lens' ClodDatabase (Map FilePath FilePath)
dbChecksums)
  , serializedLastStagingDir :: Maybe FilePath
serializedLastStagingDir = ClodDatabase
db ClodDatabase
-> Getting (Maybe FilePath) ClodDatabase (Maybe FilePath)
-> Maybe FilePath
forall s a. s -> Getting a s a -> a
^. Getting (Maybe FilePath) ClodDatabase (Maybe FilePath)
Lens' ClodDatabase (Maybe FilePath)
dbLastStagingDir
  , serializedLastRunTime :: UTCTime
serializedLastRunTime = ClodDatabase
db ClodDatabase -> Getting UTCTime ClodDatabase UTCTime -> UTCTime
forall s a. s -> Getting a s a -> a
^. Getting UTCTime ClodDatabase UTCTime
Lens' ClodDatabase UTCTime
dbLastRunTime
  }

-- | Convert from serializable form
fromSerializable :: SerializableClodDatabase -> ClodDatabase
fromSerializable :: SerializableClodDatabase -> ClodDatabase
fromSerializable SerializableClodDatabase
sdb = ClodDatabase
  { _dbFiles :: Map FilePath FileEntry
_dbFiles = [(FilePath, FileEntry)] -> Map FilePath FileEntry
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList (SerializableClodDatabase -> [(FilePath, FileEntry)]
serializedFiles SerializableClodDatabase
sdb)
  , _dbChecksums :: Map FilePath FilePath
_dbChecksums = [(FilePath, FilePath)] -> Map FilePath FilePath
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList (SerializableClodDatabase -> [(FilePath, FilePath)]
serializedChecksums SerializableClodDatabase
sdb)
  , _dbLastStagingDir :: Maybe FilePath
_dbLastStagingDir = SerializableClodDatabase -> Maybe FilePath
serializedLastStagingDir SerializableClodDatabase
sdb
  , _dbLastRunTime :: UTCTime
_dbLastRunTime = SerializableClodDatabase -> UTCTime
serializedLastRunTime SerializableClodDatabase
sdb
  }

-- | The Clod monad
--
-- This monad stack combines:
--
-- * Reader for dependency injection of ClodConfig
-- * Error handling with ExceptT for 'ClodError'
-- * IO for filesystem, git, and other side effects
--
-- This replaces the previous effects-based approach with a simpler,
-- more traditional monad stack.
type ClodM a = ReaderT ClodConfig (ExceptT ClodError IO) a

-- | Run a ClodM computation, returning either an error or a result
runClodM :: ClodConfig -> ClodM a -> IO (Either ClodError a)
runClodM :: forall a. ClodConfig -> ClodM a -> IO (Either ClodError a)
runClodM ClodConfig
config ClodM a
action = ExceptT ClodError IO a -> IO (Either ClodError a)
forall e (m :: * -> *) a. ExceptT e m a -> m (Either e a)
runExceptT (ClodM a -> ClodConfig -> ExceptT ClodError IO a
forall r (m :: * -> *) a. ReaderT r m a -> r -> m a
runReaderT ClodM a
action ClodConfig
config)

-- | Capability for reading files within certain directories
data FileReadCap = FileReadCap 
  { FileReadCap -> [FilePath]
_allowedReadDirs :: [FilePath] -- ^ Directories where reading is permitted
  } deriving (Int -> FileReadCap -> ShowS
[FileReadCap] -> ShowS
FileReadCap -> FilePath
(Int -> FileReadCap -> ShowS)
-> (FileReadCap -> FilePath)
-> ([FileReadCap] -> ShowS)
-> Show FileReadCap
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> FileReadCap -> ShowS
showsPrec :: Int -> FileReadCap -> ShowS
$cshow :: FileReadCap -> FilePath
show :: FileReadCap -> FilePath
$cshowList :: [FileReadCap] -> ShowS
showList :: [FileReadCap] -> ShowS
Show, FileReadCap -> FileReadCap -> Bool
(FileReadCap -> FileReadCap -> Bool)
-> (FileReadCap -> FileReadCap -> Bool) -> Eq FileReadCap
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: FileReadCap -> FileReadCap -> Bool
== :: FileReadCap -> FileReadCap -> Bool
$c/= :: FileReadCap -> FileReadCap -> Bool
/= :: FileReadCap -> FileReadCap -> Bool
Eq)

-- | Lens for allowedReadDirs field
allowedReadDirs :: Lens' FileReadCap [FilePath]
allowedReadDirs :: Lens' FileReadCap [FilePath]
allowedReadDirs = (FileReadCap -> [FilePath])
-> (FileReadCap -> [FilePath] -> FileReadCap)
-> Lens' FileReadCap [FilePath]
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens FileReadCap -> [FilePath]
_allowedReadDirs (\FileReadCap
c [FilePath]
v -> FileReadCap
c { _allowedReadDirs = v })

-- | Capability for writing files within certain directories
data FileWriteCap = FileWriteCap 
  { FileWriteCap -> [FilePath]
_allowedWriteDirs :: [FilePath] -- ^ Directories where writing is permitted
  } deriving (Int -> FileWriteCap -> ShowS
[FileWriteCap] -> ShowS
FileWriteCap -> FilePath
(Int -> FileWriteCap -> ShowS)
-> (FileWriteCap -> FilePath)
-> ([FileWriteCap] -> ShowS)
-> Show FileWriteCap
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> FileWriteCap -> ShowS
showsPrec :: Int -> FileWriteCap -> ShowS
$cshow :: FileWriteCap -> FilePath
show :: FileWriteCap -> FilePath
$cshowList :: [FileWriteCap] -> ShowS
showList :: [FileWriteCap] -> ShowS
Show, FileWriteCap -> FileWriteCap -> Bool
(FileWriteCap -> FileWriteCap -> Bool)
-> (FileWriteCap -> FileWriteCap -> Bool) -> Eq FileWriteCap
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: FileWriteCap -> FileWriteCap -> Bool
== :: FileWriteCap -> FileWriteCap -> Bool
$c/= :: FileWriteCap -> FileWriteCap -> Bool
/= :: FileWriteCap -> FileWriteCap -> Bool
Eq)

-- | Lens for allowedWriteDirs field
allowedWriteDirs :: Lens' FileWriteCap [FilePath]
allowedWriteDirs :: Lens' FileWriteCap [FilePath]
allowedWriteDirs = (FileWriteCap -> [FilePath])
-> (FileWriteCap -> [FilePath] -> FileWriteCap)
-> Lens' FileWriteCap [FilePath]
forall s a b t. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens FileWriteCap -> [FilePath]
_allowedWriteDirs (\FileWriteCap
c [FilePath]
v -> FileWriteCap
c { _allowedWriteDirs = v })

-- | Create a file read capability for specified directories
fileReadCap :: [FilePath] -> FileReadCap
fileReadCap :: [FilePath] -> FileReadCap
fileReadCap [FilePath]
dirs = FileReadCap { _allowedReadDirs :: [FilePath]
_allowedReadDirs = [FilePath]
dirs }

-- | Create a file write capability for specified directories
fileWriteCap :: [FilePath] -> FileWriteCap
fileWriteCap :: [FilePath] -> FileWriteCap
fileWriteCap [FilePath]
dirs = FileWriteCap { _allowedWriteDirs :: [FilePath]
_allowedWriteDirs = [FilePath]
dirs }

-- | Check if a path is within allowed directories
-- This improved version handles path traversal attacks by comparing canonical paths
isPathAllowed :: [FilePath] -> FilePath -> IO Bool
isPathAllowed :: [FilePath] -> FilePath -> IO Bool
isPathAllowed [FilePath]
allowedDirs FilePath
path = do
  -- Get canonical paths to resolve any `.`, `..`, or symlinks
  canonicalPath <- FilePath -> IO FilePath
canonicalizePath FilePath
path
  -- Check if the canonical path is within any of the allowed directories
  checks <- mapM (\FilePath
dir -> do
                   canonicalDir <- FilePath -> IO FilePath
canonicalizePath FilePath
dir
                   -- A path is allowed if:
                   -- 1. It equals an allowed directory exactly, or
                   -- 2. It's a proper subdirectory (dir is a prefix and has a path separator)
                   let isAllowed = FilePath
canonicalDir FilePath -> FilePath -> Bool
forall a. Eq a => a -> a -> Bool
== FilePath
canonicalPath Bool -> Bool -> Bool
|| 
                                  (FilePath
canonicalDir FilePath -> FilePath -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isPrefixOf` FilePath
canonicalPath Bool -> Bool -> Bool
&& 
                                   FilePath -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length FilePath
canonicalPath Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> FilePath -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length FilePath
canonicalDir Bool -> Bool -> Bool
&&
                                   Char -> Bool
isPathSeparator (FilePath
canonicalPath FilePath -> Int -> Char
forall a. HasCallStack => [a] -> Int -> a
!! FilePath -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length FilePath
canonicalDir))
                   return isAllowed) allowedDirs
  -- Return result
  return (or checks)
  where
    isPathSeparator :: Char -> Bool
isPathSeparator Char
c = Char
c Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'/' Bool -> Bool -> Bool
|| Char
c Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'\\'