From 9cd5f1681e2f7a458a7392af2dc2be10e4d5a79e Mon Sep 17 00:00:00 2001 From: Paul Burns Date: Tue, 18 Aug 2026 20:16:41 -0400 Subject: [PATCH 1/7] Enable henforcer and qualify the imports it requires Adopts the plugin per the org convention, gated at impl (ghc >= 9.6) like beeline, shrubbery and bounded-text. GHC 9.2 cannot run it at all -- it needs base >= 4.17 -- and 9.4 is left off the gate to match the fleet, though henforcer itself supports 9.4. Pinned from Hackage rather than the git commit every other repo uses. The plugin's README still says it is unreleased, but henforcer-1.0.0.1 is published, and its tested-with reaches GHC 9.14.1 -- newer than the commit the fleet pins. This is also the first repo in the org to run henforcer under 9.12, which works. The config sets allowedOpenUnaliasedImports = 1, not 0. henforcer counts the implicit `import Prelude` as an open import, so 0 is unreachable without NoImplicitPrelude -- which is why every repo in the fleet uses 1. That one slot is Prelude, so every explicit import now carries an alias or an import list. The documentation caps are deliberately left out. Network.FTP.Client exports roughly forty-five names, most undocumented, and enforcing that is a documentation project rather than a tooling change. beeline comments the same two rules out. Imports follow the house style of a qualified alias plus an explicit list for operators: attoparsec as AC, Control.Exception as Exception, Data.Bits as Bits, Network.Connection as Connection, Control.Monad.IO.Class as MIO, with (<=<) and (***) imported by name. Prepositive `import qualified` is kept rather than the postpositive form the codex shows, since these modules are Haskell2010 and would otherwise need ImportQualifiedPost. Module headers gain the Copyright field the config requires. The conduit module's header also named Network.FTP.Client, a copy-paste error. --- ftp-client-conduit/ftp-client-conduit.cabal | 4 + ftp-client-conduit/henforcer.toml | 10 + ftp-client-conduit/package.yaml | 5 + .../src/Network/FTP/Client/Conduit.hs | 11 +- ftp-client/ftp-client.cabal | 4 + ftp-client/henforcer.toml | 10 + ftp-client/package.yaml | 5 + ftp-client/src/Network/FTP/Client.hs | 276 +++++++++--------- stack-ghc-9.10.yaml | 6 +- stack-ghc-9.10.yaml.lock | 16 +- stack-ghc-9.12.yaml | 6 +- stack-ghc-9.12.yaml.lock | 16 +- stack-ghc-9.6.yaml | 9 +- stack-ghc-9.6.yaml.lock | 30 +- stack-ghc-9.8.yaml | 6 +- stack-ghc-9.8.yaml.lock | 16 +- 16 files changed, 280 insertions(+), 150 deletions(-) create mode 100644 ftp-client-conduit/henforcer.toml create mode 100644 ftp-client/henforcer.toml diff --git a/ftp-client-conduit/ftp-client-conduit.cabal b/ftp-client-conduit/ftp-client-conduit.cabal index 32e6ba8..5112115 100644 --- a/ftp-client-conduit/ftp-client-conduit.cabal +++ b/ftp-client-conduit/ftp-client-conduit.cabal @@ -63,6 +63,10 @@ library ghc-options: -Wincomplete-export-warnings -Wmissing-poly-kind-signatures -Wterm-variable-capture if impl (ghc >= 9.10) ghc-options: -Wdefaulted-exception-context + if impl (ghc >= 9.6) + ghc-options: -fplugin Henforcer + build-depends: + henforcer test-suite ftp-conduit-test type: exitcode-stdio-1.0 diff --git a/ftp-client-conduit/henforcer.toml b/ftp-client-conduit/henforcer.toml new file mode 100644 index 0000000..f508849 --- /dev/null +++ b/ftp-client-conduit/henforcer.toml @@ -0,0 +1,10 @@ +[forAnyModule] +allowedOpenUnaliasedImports = 1 +moduleHeaderCopyrightMustExistNonEmpty = true +moduleHeaderDescriptionMustExistNonEmpty = false +moduleHeaderLicenseMustExistNonEmpty = true +moduleHeaderMaintainerMustExistNonEmpty = false +[[forPatternModules]] +pattern = "Paths_*" +[forPatternModules.rulesToIgnore] +all = true diff --git a/ftp-client-conduit/package.yaml b/ftp-client-conduit/package.yaml index 0e2ca2a..eb4db75 100644 --- a/ftp-client-conduit/package.yaml +++ b/ftp-client-conduit/package.yaml @@ -82,6 +82,11 @@ library: - condition: impl (ghc >= 9.10) ghc-options: - -Wdefaulted-exception-context + - condition: impl (ghc >= 9.6) + dependencies: + - henforcer + ghc-options: + - -fplugin Henforcer tests: ftp-conduit-test: diff --git a/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs b/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs index 8b9dc17..3968604 100644 --- a/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs +++ b/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs @@ -1,8 +1,9 @@ {-# LANGUAGE ScopedTypeVariables #-} {-| -Module : Network.FTP.Client +Module : Network.FTP.Client.Conduit Description : Transfer files over FTP and FTPS with Conduit +Copyright : Megan Robinson 2018-2019, Flipstone Technology Partners 2024-2026 License : Public Domain Stability : experimental Portability : POSIX @@ -22,7 +23,7 @@ module Network.FTP.Client.Conduit ( import Conduit hiding (MonadResource) import Control.Monad.Trans.Resource (MonadResource) import Data.ByteString.Lazy.Internal (defaultChunkSize) -import System.IO +import qualified System.IO as SIO import Network.FTP.Client ( sendCommandS , FTPCommand(..) @@ -40,7 +41,7 @@ import Network.FTP.Client import qualified Network.FTP.Client as FTP import qualified Data.ByteString as B import Data.ByteString (ByteString) -import Network.Connection +import qualified Network.Connection as Connection import qualified Control.Monad.Catch as M debugging :: Bool @@ -113,7 +114,7 @@ sourceDataCommand ch pa code cmd f = do _ <- sendCommandS ch $ RType code x <- bracketP (createSendDataCommand ch pa cmd) - (liftIO . hClose) + (liftIO . SIO.hClose) (f . sIOHandleImpl) resp <- getResponse ch debugResponse resp @@ -131,7 +132,7 @@ sourceTLSDataCommand ch pa code cmd f = do _ <- sendCommandS ch $ RType code x <- bracketP (createTLSSendDataCommand ch pa cmd) - (liftIO . connectionClose) + (liftIO . Connection.connectionClose) (f . tlsHandleImpl) resp <- getResponse ch debugResponse resp diff --git a/ftp-client/ftp-client.cabal b/ftp-client/ftp-client.cabal index c6993fc..465f926 100644 --- a/ftp-client/ftp-client.cabal +++ b/ftp-client/ftp-client.cabal @@ -64,6 +64,10 @@ library ghc-options: -Wincomplete-export-warnings -Wmissing-poly-kind-signatures -Wterm-variable-capture if impl (ghc >= 9.10) ghc-options: -Wdefaulted-exception-context + if impl (ghc >= 9.6) + ghc-options: -fplugin Henforcer + build-depends: + henforcer test-suite ftp-client-test type: exitcode-stdio-1.0 diff --git a/ftp-client/henforcer.toml b/ftp-client/henforcer.toml new file mode 100644 index 0000000..f508849 --- /dev/null +++ b/ftp-client/henforcer.toml @@ -0,0 +1,10 @@ +[forAnyModule] +allowedOpenUnaliasedImports = 1 +moduleHeaderCopyrightMustExistNonEmpty = true +moduleHeaderDescriptionMustExistNonEmpty = false +moduleHeaderLicenseMustExistNonEmpty = true +moduleHeaderMaintainerMustExistNonEmpty = false +[[forPatternModules]] +pattern = "Paths_*" +[forPatternModules.rulesToIgnore] +all = true diff --git a/ftp-client/package.yaml b/ftp-client/package.yaml index 70400cc..cb802a4 100644 --- a/ftp-client/package.yaml +++ b/ftp-client/package.yaml @@ -83,6 +83,11 @@ library: - condition: impl (ghc >= 9.10) ghc-options: - -Wdefaulted-exception-context + - condition: impl (ghc >= 9.6) + dependencies: + - henforcer + ghc-options: + - -fplugin Henforcer tests: ftp-client-test: diff --git a/ftp-client/src/Network/FTP/Client.hs b/ftp-client/src/Network/FTP/Client.hs index d114fdc..79b6ee8 100644 --- a/ftp-client/src/Network/FTP/Client.hs +++ b/ftp-client/src/Network/FTP/Client.hs @@ -1,6 +1,7 @@ {-| Module : Network.FTP.Client Description : Transfer files over FTP and FTPS +Copyright : Megan Robinson 2018-2019, Flipstone Technology Partners 2024-2026 License : Public Domain Stability : experimental Portability : POSIX @@ -73,29 +74,30 @@ import qualified Data.ByteString.Char8 as C import qualified Data.ByteString as B import Data.ByteString (ByteString) import Data.List (intercalate) -import Data.Attoparsec.ByteString.Char8 +import qualified Data.Attoparsec.ByteString.Char8 as AC import qualified Network.Socket as S import qualified System.IO as SIO -import Control.Exception +import qualified Control.Exception as Exception import Control.Monad.Catch (MonadCatch, MonadMask) import qualified Control.Monad.Catch as M -import Control.Monad -import Control.Monad.IO.Class -import Data.Bits -import Network.Connection +import qualified Control.Monad as Monad +import Control.Monad ((<=<)) +import qualified Control.Monad.IO.Class as MIO +import qualified Data.Bits as Bits +import qualified Network.Connection as Connection import Data.ByteString.Lazy.Internal (defaultChunkSize) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map -import Control.Arrow +import Control.Arrow ((***)) import System.IO.Error (isEOFError) debugging :: Bool debugging = False -debugPrint :: (Show a, MonadIO m) => a -> m () -debugPrint s = when debugging (liftIO $ print s) +debugPrint :: (Show a, MIO.MonadIO m) => a -> m () +debugPrint s = Monad.when debugging (MIO.liftIO $ print s) -debugResponse :: (Show a, MonadIO m) => a -> m () +debugResponse :: (Show a, MIO.MonadIO m) => a -> m () debugResponse s = debugPrint $ "Recieved: " <> show s data Security = Clear | TLS @@ -118,15 +120,15 @@ instance Show FTPMessage where -- | Response from an FTP command. ex "200 Welcome!" data FTPResponse = FTPResponse { - frStatus :: ResponseStatus, -- ^ Interpretation of the first digit of an FTP response code - frCode :: Int, -- ^ The three digit response code + frStatus :: ResponseStatus, -- ^ Interpretation of the first AC.digit of an FTP response code + frCode :: Int, -- ^ The three AC.digit response code frMessage :: FTPMessage -- ^ Text of the response } deriving Eq instance Show FTPResponse where show fr = show (frCode fr) <> " " <> show (frMessage fr) --- | First digit of an FTP response +-- | First AC.digit of an FTP response data ResponseStatus = Wait -- ^ 1 | Success -- ^ 2 @@ -143,7 +145,7 @@ data FTPException | BadProtocolResponseException ByteString deriving (Show) -instance Exception FTPException +instance Exception.Exception FTPException responseStatus :: ByteString -> ResponseStatus responseStatus cbs = @@ -153,7 +155,7 @@ responseStatus cbs = Just ('3', _) -> Continue Just ('4', _) -> FailureRetry Just ('5', _) -> Failure - _ -> throw $ BadProtocolResponseException cbs + _ -> Exception.throw $ BadProtocolResponseException cbs data RTypeCode = TA | TI @@ -257,14 +259,14 @@ getLineRespMaybe h = -- | Get a full response from the server -- Used in 'sendCommand' -getResponse :: MonadIO m => Handle -> m FTPResponse +getResponse :: MIO.MonadIO m => Handle -> m FTPResponse getResponse h = do - line <- liftIO $ getLineResp h + line <- MIO.liftIO $ getLineResp h let (code, rest) = C.splitAt 3 line -- A response must open with a three digit code. Checking that up front keeps -- the 'C.uncons' below and the 'read' further down from being partial. - when (C.length code < 3 || not (C.all isDigit code)) - $ liftIO $ throwIO $ BadProtocolResponseException line + Monad.when (C.length code < 3 || not (C.all AC.isDigit code)) + $ MIO.liftIO $ Exception.throwIO $ BadProtocolResponseException line message <- case C.uncons rest of Just ('-', _) -> MultiLine <$> loopMultiLine h code [line] _ -> return $ SingleLine line @@ -278,18 +280,18 @@ getResponse h = do (read $ C.unpack code) codeDroppedMessage case frStatus response of - FailureRetry -> liftIO $ throwIO $ FailureRetryException response - Failure -> liftIO $ throwIO $ FailureException response + FailureRetry -> MIO.liftIO $ Exception.throwIO $ FailureRetryException response + Failure -> MIO.liftIO $ Exception.throwIO $ FailureException response _ -> return response loopMultiLine - :: MonadIO m + :: MIO.MonadIO m => Handle -> ByteString -> [ByteString] -> m [ByteString] loopMultiLine h code priorLines = do - mNextLine <- liftIO $ getLineRespMaybe h + mNextLine <- MIO.liftIO $ getLineRespMaybe h case mNextLine of -- The server hung up before sending the terminating line. Return what -- was collected rather than looping forever. Note this is end of input, @@ -310,21 +312,21 @@ loopMultiLine h code priorLines = do then return newLines else loopMultiLine h code newLines -ensureSuccess :: MonadIO m => FTPResponse -> m FTPResponse +ensureSuccess :: MIO.MonadIO m => FTPResponse -> m FTPResponse ensureSuccess resp = case frStatus resp of Success -> return resp - _ -> liftIO $ throwIO $ UnsuccessfulException resp + _ -> MIO.liftIO $ Exception.throwIO $ UnsuccessfulException resp -getResponseS :: MonadIO m => Handle -> m FTPResponse +getResponseS :: MIO.MonadIO m => Handle -> m FTPResponse getResponseS = ensureSuccess <=< getResponse -sendCommandLine :: MonadIO m => Handle -> ByteString -> m () -sendCommandLine h = liftIO . send h . (<> "\r\n") +sendCommandLine :: MIO.MonadIO m => Handle -> ByteString -> m () +sendCommandLine h = MIO.liftIO . send h . (<> "\r\n") -- | Send a command to the server and get a response back. -- Some commands use a data 'Handle', and their data is not returned here. -sendCommand :: MonadIO m => Handle -> FTPCommand -> m FTPResponse +sendCommand :: MIO.MonadIO m => Handle -> FTPCommand -> m FTPResponse sendCommand h fc = do let command = serializeCommand fc debugPrint $ "Sending: " <> command @@ -333,42 +335,42 @@ sendCommand h fc = do debugResponse resp return resp -sendCommandS :: MonadIO m => Handle -> FTPCommand -> m FTPResponse +sendCommandS :: MIO.MonadIO m => Handle -> FTPCommand -> m FTPResponse sendCommandS h fc = sendCommand h fc >>= ensureSuccess -- | Equvalent to -- -- > mapM . sendCommand -sendAll :: MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse] +sendAll :: MIO.MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse] sendAll = mapM . sendCommand -- | Equvalent to -- -- > mapM . sendCommandS -sendAllS :: MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse] +sendAllS :: MIO.MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse] sendAllS = mapM . sendCommandS -- Control connection createSocket - :: MonadIO m + :: MIO.MonadIO m => Maybe String -> Int -> S.AddrInfo -> m (S.Socket, S.AddrInfo) createSocket host portNum hints = do - addr <- liftIO $ do + addr <- MIO.liftIO $ do a:_ <- S.getAddrInfo (Just hints) host (Just $ show portNum) return a debugPrint $ "Addr: " <> show addr - sock <- liftIO $ S.socket + sock <- MIO.liftIO $ S.socket (S.addrFamily addr) (S.addrSocketType addr) (S.addrProtocol addr) return (sock, addr) withSocketPassive - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => String -> Int -> (S.Socket -> m a) @@ -379,15 +381,15 @@ withSocketPassive host portNum f = do } M.bracketOnError (createSocket (Just host) portNum hints) - (liftIO . S.close . fst) + (MIO.liftIO . S.close . fst) (\(sock, addr) -> do debugPrint ("Connecting" :: String) - liftIO $ S.connect sock (S.addrAddress addr) + MIO.liftIO $ S.connect sock (S.addrAddress addr) debugPrint ("Connected" :: String) f sock ) -withSocketActive :: (MonadIO m, MonadMask m) => (S.Socket -> m a) -> m a +withSocketActive :: (MIO.MonadIO m, MonadMask m) => (S.Socket -> m a) -> m a withSocketActive f = do let hints = S.defaultHints { S.addrSocketType = S.Stream, @@ -395,18 +397,18 @@ withSocketActive f = do } M.bracketOnError (createSocket Nothing 0 hints) - (liftIO . S.close . fst) + (MIO.liftIO . S.close . fst) (\(sock, addr) -> do debugPrint ("Binding" :: String) - liftIO $ S.bind sock (S.addrAddress addr) - liftIO $ S.listen sock 1 + MIO.liftIO $ S.bind sock (S.addrAddress addr) + MIO.liftIO $ S.listen sock 1 debugPrint ("Listening" :: String) f sock ) -createSIOHandle :: (MonadIO m, MonadMask m) => String -> Int -> m SIO.Handle +createSIOHandle :: (MIO.MonadIO m, MonadMask m) => String -> Int -> m SIO.Handle createSIOHandle host portNum = withSocketPassive host portNum - $ liftIO . flip S.socketToHandle SIO.ReadWriteMode + $ MIO.liftIO . flip S.socketToHandle SIO.ReadWriteMode sIOHandleImpl :: SIO.Handle -> Handle sIOHandleImpl h = Handle @@ -418,14 +420,14 @@ sIOHandleImpl h = Handle } withSIOHandle - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => String -> Int -> (Handle -> m a) -> m a withSIOHandle host portNum f = M.bracket - (liftIO $ createSIOHandle host portNum) - (liftIO . SIO.hClose) + (MIO.liftIO $ createSIOHandle host portNum) + (MIO.liftIO . SIO.hClose) (f . sIOHandleImpl) -- | Takes a host name and port. A handle for interacting with the server @@ -438,7 +440,7 @@ withSIOHandle host portNum f = M.bracket -- print =<< nlst h [] -- @ withFTP - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => String -> Int -> (Handle -> FTPResponse -> m a) @@ -450,7 +452,7 @@ withFTP host portNum f = withSIOHandle host portNum $ \h -> do -- Data connection withDataSocketPasv - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => Handle -> (S.Socket -> m a) -> m a @@ -461,12 +463,12 @@ withDataSocketPasv h f = do withSocketPassive host portNum f withDataSocketActive - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => Handle -> (S.Socket -> m a) -> m a withDataSocketActive h f = withSocketActive $ \socket -> do - (sPort, sHost) <- liftIO $ do + (sPort, sHost) <- MIO.liftIO $ do (S.SockAddrInet p hostAddr) <- S.getSocketName socket return (p,hostAddr) _ <- port h sHost sPort @@ -474,7 +476,7 @@ withDataSocketActive h f = withSocketActive $ \socket -> do -- | Open a socket that can be used for data transfers withDataSocket - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => PortActivity -> Handle -> (S.Socket -> m a) @@ -482,13 +484,13 @@ withDataSocket withDataSocket Active = withDataSocketActive withDataSocket Passive = withDataSocketPasv -acceptData :: MonadIO m => PortActivity -> S.Socket -> m S.Socket +acceptData :: MIO.MonadIO m => PortActivity -> S.Socket -> m S.Socket acceptData Passive = return -acceptData Active = return . fst <=< liftIO . S.accept +acceptData Active = return . fst <=< MIO.liftIO . S.accept -- Response to data commands should be 150 but apparently -- some servers will respond with 200 before 150 so just ignore it -ensureSucessfulData :: MonadIO m => Handle -> FTPResponse -> m () +ensureSucessfulData :: MIO.MonadIO m => Handle -> FTPResponse -> m () ensureSucessfulData h resp = do resp' <- case frStatus resp of Success -> do @@ -496,13 +498,13 @@ ensureSucessfulData h resp = do debugResponse newResp return newResp _ -> return resp - liftIO $ when (frStatus resp' /= Wait) - $ throwIO $ UnsuccessfulException resp + MIO.liftIO $ Monad.when (frStatus resp' /= Wait) + $ Exception.throwIO $ UnsuccessfulException resp -- | Send setup commands to the server and -- create a data 'System.IO.Handle' createSendDataCommand - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => Handle -> PortActivity -> FTPCommand @@ -511,11 +513,11 @@ createSendDataCommand h pa cmd = withDataSocket pa h $ \socket -> do resp <- sendCommand h cmd ensureSucessfulData h resp acceptedSock <- acceptData pa socket - liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode + MIO.liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode -- | Provides a data 'Handle' in a callback for a command withDataCommand - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => Handle -> PortActivity -> RTypeCode @@ -526,28 +528,28 @@ withDataCommand ch pa code cmd f = do _ <- sendCommandS ch $ RType code x <- M.bracket (createSendDataCommand ch pa cmd) - (liftIO . SIO.hClose) + (MIO.liftIO . SIO.hClose) (f . sIOHandleImpl) resp <- getResponse ch debugResponse resp return x -- | Recieve data and interpret it linewise -getAllLineResp :: (MonadIO m, MonadCatch m) => Handle -> m ByteString +getAllLineResp :: (MIO.MonadIO m, MonadCatch m) => Handle -> m ByteString getAllLineResp h = - let collect :: (MonadIO n, MonadCatch n) => [ByteString] -> n ByteString + let collect :: (MIO.MonadIO n, MonadCatch n) => [ByteString] -> n ByteString collect ret = ( do - line <- liftIO $ getLineResp h + line <- MIO.liftIO $ getLineResp h collect (ret <> [line])) `M.catchIOError` (\_ -> return $ C.intercalate "\n" ret) in collect [] -- | Recieve all data and return it as a 'Data.ByteString.ByteString' -recvAll :: (MonadIO m, MonadCatch m) => Handle -> m ByteString +recvAll :: (MIO.MonadIO m, MonadCatch m) => Handle -> m ByteString recvAll h = - let collect :: (MonadIO n, MonadCatch n) => ByteString -> n ByteString + let collect :: (MIO.MonadIO n, MonadCatch n) => ByteString -> n ByteString collect bs = ( do - chunk <- liftIO $ recv h defaultChunkSize + chunk <- MIO.liftIO $ recv h defaultChunkSize if C.null chunk then return bs else collect $ bs <> chunk @@ -556,26 +558,26 @@ recvAll h = -- TLS connection -connectTLS :: MonadIO m => SIO.Handle -> String -> Int -> m Connection +connectTLS :: MIO.MonadIO m => SIO.Handle -> String -> Int -> m Connection.Connection connectTLS h host portNum = do - context <- liftIO initConnectionContext + context <- MIO.liftIO Connection.initConnectionContext let tlsSettings = case def of - simpleSettings@TLSSettingsSimple{} -> - simpleSettings { settingDisableCertificateValidation = True } + simpleSettings@Connection.TLSSettingsSimple{} -> + simpleSettings { Connection.settingDisableCertificateValidation = True } otherSettings -> otherSettings - connectionParams = ConnectionParams - { connectionHostname = host - , connectionPort = toEnum . fromEnum $ portNum - , connectionUseSecure = Just tlsSettings - , connectionUseSocks = Nothing + connectionParams = Connection.ConnectionParams + { Connection.connectionHostname = host + , Connection.connectionPort = toEnum . fromEnum $ portNum + , Connection.connectionUseSecure = Just tlsSettings + , Connection.connectionUseSocks = Nothing } - liftIO $ connectFromHandle context h connectionParams + MIO.liftIO $ Connection.connectFromHandle context h connectionParams createTLSConnection - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => String -> Int - -> m (FTPResponse, Connection) + -> m (FTPResponse, Connection.Connection) createTLSConnection host portNum = do h <- createSIOHandle host portNum let insecureH = sIOHandleImpl h @@ -584,24 +586,24 @@ createTLSConnection host portNum = do conn <- connectTLS h host portNum return (resp, conn) -tlsHandleImpl :: Connection -> Handle +tlsHandleImpl :: Connection.Connection -> Handle tlsHandleImpl c = Handle - { send = connectionPut c - , sendLine = connectionPut c . (<> "\n") - , recv = connectionGet c - , recvLine = connectionGetLine maxBound c + { send = Connection.connectionPut c + , sendLine = Connection.connectionPut c . (<> "\n") + , recv = Connection.connectionGet c + , recvLine = Connection.connectionGetLine maxBound c , security = TLS } withTLSHandle - :: (MonadMask m, MonadIO m) + :: (MonadMask m, MIO.MonadIO m) => String -> Int -> (Handle -> FTPResponse -> m a) -> m a withTLSHandle host portNum f = M.bracket (createTLSConnection host portNum) - (liftIO . connectionClose . snd) + (MIO.liftIO . Connection.connectionClose . snd) (\(resp, conn) -> f (tlsHandleImpl conn) resp) -- | Takes a host name and port. A handle for interacting with the server @@ -614,7 +616,7 @@ withTLSHandle host portNum f = M.bracket -- print =<< nlst h [] -- @ withFTPS - :: (MonadMask m, MonadIO m) + :: (MonadMask m, MIO.MonadIO m) => String -> Int -> (Handle -> FTPResponse -> m a) @@ -626,27 +628,27 @@ withFTPS = withTLSHandle -- | Send setup commands to the server and -- create a data TLS connection createTLSSendDataCommand - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => Handle -> PortActivity -> FTPCommand - -> m Connection + -> m Connection.Connection createTLSSendDataCommand ch pa cmd = do _ <- sendAllS ch [Pbsz 0, Prot P] withDataSocket pa ch $ \socket -> do resp <- sendCommand ch cmd ensureSucessfulData ch resp acceptedSock <- acceptData pa socket - (sPort, sHost) <- liftIO $ do + (sPort, sHost) <- MIO.liftIO $ do (S.SockAddrInet p h) <- S.getSocketName acceptedSock return (p, h) let (h1, h2, h3, h4) = S.hostAddressToTuple sHost hostName = intercalate "." $ show . fromEnum <$> [h1, h2, h3, h4] - h <- liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode - liftIO $ connectTLS h hostName (fromEnum sPort) + h <- MIO.liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode + MIO.liftIO $ connectTLS h hostName (fromEnum sPort) withTLSDataCommand - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => Handle -> PortActivity -> RTypeCode @@ -657,78 +659,78 @@ withTLSDataCommand ch pa code cmd f = do _ <- sendCommandS ch $ RType code x <- M.bracket (createTLSSendDataCommand ch pa cmd) - (liftIO . connectionClose) + (MIO.liftIO . Connection.connectionClose) (f . tlsHandleImpl) resp <- getResponse ch debugPrint $ "Recieved: " <> show resp return x -parseResponse :: MonadIO m => FTPResponse -> Parser a -> m a +parseResponse :: MIO.MonadIO m => FTPResponse -> AC.Parser a -> m a parseResponse resp p = let parsableMessage = case frMessage resp of SingleLine message -> message MultiLine messages -> C.intercalate "\n" messages - in case parseOnly p parsableMessage of + in case AC.parseOnly p parsableMessage of Right x -> return x - Left _ -> liftIO $ throwIO + Left _ -> MIO.liftIO $ Exception.throwIO $ BadProtocolResponseException parsableMessage -ensureCode :: MonadIO m => FTPResponse -> Int -> m () +ensureCode :: MIO.MonadIO m => FTPResponse -> Int -> m () ensureCode resp code = - liftIO $ when (frCode resp /= code) - $ liftIO $ throwIO $ UnsuccessfulException resp + MIO.liftIO $ Monad.when (frCode resp /= code) + $ MIO.liftIO $ Exception.throwIO $ UnsuccessfulException resp -parse227 :: Parser (String, Int) +parse227 :: AC.Parser (String, Int) parse227 = do - _ <- skipWhile (/= '(') *> char '(' - [h1,h2,h3,h4,p1,p2] <- many1 digit `sepBy` char ',' + _ <- AC.skipWhile (/= '(') *> AC.char '(' + [h1,h2,h3,h4,p1,p2] <- AC.many1 AC.digit `AC.sepBy` AC.char ',' let host = intercalate "." [h1,h2,h3,h4] highBits = read p1 lowBits = read p2 - portNum = (highBits `shift` 8) + lowBits + portNum = (highBits `Bits.shift` 8) + lowBits return (host, portNum) -parse257 :: Parser String +parse257 :: AC.Parser String parse257 = do - _ <- char '"' - C.unpack <$> takeTill (== '"') + _ <- AC.char '"' + C.unpack <$> AC.takeTill (== '"') -- Control commands -login :: MonadIO m => Handle -> String -> String -> m FTPResponse +login :: MIO.MonadIO m => Handle -> String -> String -> m FTPResponse login h user pass = do resp <- last <$> sendAll h [User user, Pass pass] ensureSuccess resp -pasv :: MonadIO m => Handle -> m (String, Int) +pasv :: MIO.MonadIO m => Handle -> m (String, Int) pasv h = do resp <- sendCommandS h Pasv ensureCode resp 227 parseResponse resp parse227 -port :: MonadIO m => Handle -> S.HostAddress -> S.PortNumber -> m FTPResponse +port :: MIO.MonadIO m => Handle -> S.HostAddress -> S.PortNumber -> m FTPResponse port h ha pn = sendCommandS h (Port ha pn) -acct :: MonadIO m => Handle -> String -> m FTPResponse +acct :: MIO.MonadIO m => Handle -> String -> m FTPResponse acct h pass = sendCommandS h (Acct pass) -rename :: MonadIO m => Handle -> String -> String -> m FTPResponse +rename :: MIO.MonadIO m => Handle -> String -> String -> m FTPResponse rename h from to = do res <- sendCommand h (Rnfr from) case frStatus res of Continue -> sendCommandS h (Rnto to) _ -> return res -dele :: MonadIO m => Handle -> String -> m FTPResponse +dele :: MIO.MonadIO m => Handle -> String -> m FTPResponse dele h file = sendCommandS h (Dele file) -cwd :: MonadIO m => Handle -> String -> m FTPResponse +cwd :: MIO.MonadIO m => Handle -> String -> m FTPResponse cwd h dir = sendCommandS h $ if dir == ".." then Cdup else Cwd dir -size :: MonadIO m => Handle -> String -> m Int +size :: MIO.MonadIO m => Handle -> String -> m Int size h file = do resp <- sendCommandS h (Size file) ensureCode resp 213 @@ -736,55 +738,55 @@ size h file = do SingleLine message -> read . C.unpack $ message MultiLine _ -> 0 -mkd :: MonadIO m => Handle -> String -> m String +mkd :: MIO.MonadIO m => Handle -> String -> m String mkd h dir = do resp <- sendCommandS h (Mkd dir) ensureCode resp 257 parseResponse resp parse257 -rmd :: MonadIO m => Handle -> String -> m FTPResponse +rmd :: MIO.MonadIO m => Handle -> String -> m FTPResponse rmd h dir = sendCommandS h (Rmd dir) -pwd :: MonadIO m => Handle -> m String +pwd :: MIO.MonadIO m => Handle -> m String pwd h = do resp <- sendCommandS h Pwd ensureCode resp 257 parseResponse resp parse257 -quit :: MonadIO m => Handle -> m FTPResponse +quit :: MIO.MonadIO m => Handle -> m FTPResponse quit h = sendCommandS h Quit -mlst :: (MonadIO m, MonadMask m) => Handle -> String -> m MlsxResponse +mlst :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> m MlsxResponse mlst h path = do resp <- sendCommandS h (Mlst path) case frMessage resp of SingleLine message -> return $ parseMlsxLine message MultiLine messages -> if length messages >= 2 then return $ parseMlsxLine $ messages !! 1 - else liftIO $ throwIO $ BogusResponseFormatException resp + else MIO.liftIO $ Exception.throwIO $ BogusResponseFormatException resp -- TLS commands -pbsz :: MonadIO m => Handle -> Int -> m FTPResponse +pbsz :: MIO.MonadIO m => Handle -> Int -> m FTPResponse pbsz h = sendCommandS h . Pbsz -prot :: MonadIO m => Handle -> ProtType -> m FTPResponse +prot :: MIO.MonadIO m => Handle -> ProtType -> m FTPResponse prot h = sendCommandS h . Prot -ccc :: MonadIO m => Handle -> m FTPResponse +ccc :: MIO.MonadIO m => Handle -> m FTPResponse ccc h = sendCommandS h Ccc -auth :: MonadIO m => Handle -> m FTPResponse +auth :: MIO.MonadIO m => Handle -> m FTPResponse auth h = sendCommandS h Auth -- Data commands -sendType :: MonadIO m => RTypeCode -> ByteString -> Handle -> m () +sendType :: MIO.MonadIO m => RTypeCode -> ByteString -> Handle -> m () sendType TA dat h = mapM_ (sendCommandLine h) $ C.split '\n' dat -sendType TI dat h = liftIO $ send h dat +sendType TI dat h = MIO.liftIO $ send h dat withDataCommandSecurity - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => Handle -> PortActivity -> RTypeCode @@ -796,17 +798,17 @@ withDataCommandSecurity h = Clear -> withDataCommand h TLS -> withTLSDataCommand h -nlst :: (MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString +nlst :: (MIO.MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString nlst h args = withDataCommandSecurity h Passive TA (Nlst args) getAllLineResp -retr :: (MonadIO m, MonadMask m) => Handle -> String -> m ByteString +retr :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> m ByteString retr h path = withDataCommandSecurity h Passive TI (Retr path) recvAll -list :: (MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString +list :: (MIO.MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString list h args = withDataCommandSecurity h Passive TA (List args) getAllLineResp stor - :: (MonadIO m, MonadMask m) + :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> B.ByteString @@ -832,14 +834,14 @@ parseMlsxLine line = facts = Map.fromList $ filter (not . null . fst) - $ join (***) C.unpack <$> bFacts + $ Monad.join (***) C.unpack <$> bFacts in MlsxResponse (C.unpack filename) facts -getMlsxResponse :: (MonadIO m, MonadCatch m) => Handle -> m [MlsxResponse] +getMlsxResponse :: (MIO.MonadIO m, MonadCatch m) => Handle -> m [MlsxResponse] getMlsxResponse h = - let collect :: (MonadIO n, MonadCatch n) => [MlsxResponse] -> n [MlsxResponse] + let collect :: (MIO.MonadIO n, MonadCatch n) => [MlsxResponse] -> n [MlsxResponse] collect ret = ( do - line <- liftIO $ getLineResp h + line <- MIO.liftIO $ getLineResp h collect $ if C.null line then ret @@ -847,5 +849,5 @@ getMlsxResponse h = ) `M.catchIOError` (\_ -> return ret) in collect [] -mlsd :: (MonadIO m, MonadMask m) => Handle -> String -> m [MlsxResponse] +mlsd :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> m [MlsxResponse] mlsd h path = withDataCommandSecurity h Passive TA (Mlsd path) getMlsxResponse diff --git a/stack-ghc-9.10.yaml b/stack-ghc-9.10.yaml index ef17196..cea3040 100644 --- a/stack-ghc-9.10.yaml +++ b/stack-ghc-9.10.yaml @@ -1,4 +1,8 @@ <<: !include stack-base.yaml resolver: lts-24.12 -extra-deps: [] +# henforcer is not in any snapshot. It is only active on GHC >= 9.6, so the +# 9.2 and 9.4 rungs do not carry it. +extra-deps: + - henforcer-1.0.0.1 + - pollock-0.1.0.3 diff --git a/stack-ghc-9.10.yaml.lock b/stack-ghc-9.10.yaml.lock index 6e6c820..c8c5d88 100644 --- a/stack-ghc-9.10.yaml.lock +++ b/stack-ghc-9.10.yaml.lock @@ -3,7 +3,21 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 snapshots: - completed: sha256: 0d0bb681dd5be9b930c8fc070d717aae757b9aed176ae6047d87624b46406816 diff --git a/stack-ghc-9.12.yaml b/stack-ghc-9.12.yaml index ea4b986..3a21c92 100644 --- a/stack-ghc-9.12.yaml +++ b/stack-ghc-9.12.yaml @@ -1,4 +1,8 @@ <<: !include stack-base.yaml resolver: nightly-2026-08-16 -extra-deps: [] +# henforcer is not in any snapshot. It is only active on GHC >= 9.6, so the +# 9.2 and 9.4 rungs do not carry it. +extra-deps: + - henforcer-1.0.0.1 + - pollock-0.1.0.3 diff --git a/stack-ghc-9.12.yaml.lock b/stack-ghc-9.12.yaml.lock index 2f96d08..c796b54 100644 --- a/stack-ghc-9.12.yaml.lock +++ b/stack-ghc-9.12.yaml.lock @@ -3,7 +3,21 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 snapshots: - completed: sha256: 38576406e65852b2665b3006437e74dfdb0a97977409b1c69de0986868f8c5b1 diff --git a/stack-ghc-9.6.yaml b/stack-ghc-9.6.yaml index 28649ec..59bf93a 100644 --- a/stack-ghc-9.6.yaml +++ b/stack-ghc-9.6.yaml @@ -1,4 +1,11 @@ <<: !include stack-base.yaml resolver: lts-22.44 -extra-deps: [] +# henforcer is not in any snapshot. It is only active on GHC >= 9.6, so the +# 9.2 and 9.4 rungs do not carry it. +extra-deps: + - henforcer-1.0.0.1 + - pollock-0.1.0.3 + # lts-22 additionally lacks henforcer's config parser and its dependency + - tomland-1.3.3.3 + - validation-selective-0.2.0.0 diff --git a/stack-ghc-9.6.yaml.lock b/stack-ghc-9.6.yaml.lock index 8d134eb..2553e3d 100644 --- a/stack-ghc-9.6.yaml.lock +++ b/stack-ghc-9.6.yaml.lock @@ -3,7 +3,35 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 +- completed: + hackage: tomland-1.3.3.3@sha256:459e5bf268552e5e582e2d3df895dc41db618bb6306576d536b600cc5729de8f,9404 + pantry-tree: + sha256: 0caf0f2c5f19c16625556b049285a02850912569a4bc26831c04b3a1e388bc67 + size: 6430 + original: + hackage: tomland-1.3.3.3 +- completed: + hackage: validation-selective-0.2.0.0@sha256:a88df9c19a26509fc4b8098b2e110336efda7e4bc0389dd58bcf327e1726005e,3917 + pantry-tree: + sha256: 82a8a83073ac0a360b72c27a8fb6952b4566a20d1ea389b65f738303423177da + size: 697 + original: + hackage: validation-selective-0.2.0.0 snapshots: - completed: sha256: 238fa745b64f91184f9aa518fe04bdde6552533d169b0da5256670df83a0f1a9 diff --git a/stack-ghc-9.8.yaml b/stack-ghc-9.8.yaml index 34d7f18..b2f24d7 100644 --- a/stack-ghc-9.8.yaml +++ b/stack-ghc-9.8.yaml @@ -1,4 +1,8 @@ <<: !include stack-base.yaml resolver: lts-23.28 -extra-deps: [] +# henforcer is not in any snapshot. It is only active on GHC >= 9.6, so the +# 9.2 and 9.4 rungs do not carry it. +extra-deps: + - henforcer-1.0.0.1 + - pollock-0.1.0.3 diff --git a/stack-ghc-9.8.yaml.lock b/stack-ghc-9.8.yaml.lock index a5db08c..6db565e 100644 --- a/stack-ghc-9.8.yaml.lock +++ b/stack-ghc-9.8.yaml.lock @@ -3,7 +3,21 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 snapshots: - completed: sha256: 7e724f347d5969cb5e8dde9f9aae30996e3231c29d1dafd45f21f1700d4c4fcb From 39d829c0b51851c1bf71d5808bb824e09cdd75b2 Mon Sep 17 00:00:00 2001 From: Paul Burns Date: Tue, 18 Aug 2026 20:17:22 -0400 Subject: [PATCH 2/7] Add fourmolu configuration and a formatting check fourmolu.yaml is copied byte for byte from the rest of the org -- all twelve repos that have it are identical, and it matches the config documented in codex programming_style.md. scripts/format-repo follows the git ls-files variant used by eight of the eleven repos, rather than the find-piped-through-grep one in bounded-text and shrubbery, whose pattern is unanchored and unescaped and so matches any path containing "hs". The safe.directory line matters: the container runs as root against a checkout owned by the runner, and git otherwise refuses to read it. The three repos that omit it are exactly the three with no CI job, so it has never been exercised there. The check job needs no stack-root cache because nothing compiles; fourmolu is a plain binary on the image's PATH. This commit only adds the tooling. The repository is not yet formatted, so the new job would fail here -- the reformat is the next commit. --- .github/workflows/main.yaml | 21 +++++++++++++++++++++ fourmolu.yaml | 14 ++++++++++++++ scripts/format-repo | 16 ++++++++++++++++ stack.yaml.lock | 16 +++++++++++++++- 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 fourmolu.yaml create mode 100755 scripts/format-repo diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5806b33..d3b72e8 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -64,6 +64,27 @@ jobs: - name: Build and test run: ./scripts/test --stack-yaml ${{ matrix.stack-yaml }} + formatting-check: + name: Formatting Check + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + + # Nothing is compiled here, so there is no stack-root worth caching. The + # setup is still needed to establish the docker environment the scripts + # run in. + - name: Setup Stack (for docker env) + uses: flipstone/github-actions/setup-dockerized-stack@2c1f2781f8a1b92a0ad82223adf77bbf24b262c2 + with: + stack-root: ./stack-root + + - name: Format and Check for Diff + run: ./scripts/format-repo + cabal-check: name: Cabal Check if: >- diff --git a/fourmolu.yaml b/fourmolu.yaml new file mode 100644 index 0000000..e31b827 --- /dev/null +++ b/fourmolu.yaml @@ -0,0 +1,14 @@ +indentation: 2 +function-arrows: trailing +comma-style: leading # default +import-export-style: leading +indent-wheres: false # default +record-brace-space: true +newlines-between-decls: 1 # default +haddock-style: multi-line # default +haddock-style-module: # default +let-style: newline +in-style: left-align +unicode: never # default +respectful: true # default +single-constraint-parens: never diff --git a/scripts/format-repo b/scripts/format-repo new file mode 100755 index 0000000..cb84d20 --- /dev/null +++ b/scripts/format-repo @@ -0,0 +1,16 @@ +#!/bin/sh + +set -o errexit + +. scripts/lib/run-in-container.sh + +if [ "$CI" ]; then + # The container runs as root against a bind mounted checkout owned by the + # runner, so git refuses to read it without this. + git config --global --add safe.directory "$PWD" + MODE=check +else + MODE=inplace +fi + +git ls-files -z '*.hs' | xargs -0 fourmolu --mode "$MODE" diff --git a/stack.yaml.lock b/stack.yaml.lock index 6e6c820..c8c5d88 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -3,7 +3,21 @@ # For more information, please see the documentation at: # https://docs.haskellstack.org/en/stable/topics/lock_files -packages: [] +packages: +- completed: + hackage: henforcer-1.0.0.1@sha256:69f1c0140122505f3daf2ef2aef4b100ced6579fd741b6f191d3ef4c5e99b8c5,4632 + pantry-tree: + sha256: 9374d76092216baaf986c4538161e5ca4711c944c3915b7f45fbf29ff28ddd98 + size: 3049 + original: + hackage: henforcer-1.0.0.1 +- completed: + hackage: pollock-0.1.0.3@sha256:0c245280f8b7813357c14663b2ab456e3c3998cd6beb2d5cca53741ca9589285,3969 + pantry-tree: + sha256: 88b5bcf2aecd3e3768d5813b80dab7b2d5c19b9a73e6760aec247a6fd19c6ecf + size: 1182 + original: + hackage: pollock-0.1.0.3 snapshots: - completed: sha256: 0d0bb681dd5be9b930c8fc070d717aae757b9aed176ae6047d87624b46406816 From dbe4bbfa76ffcb567ff58a77a639814d567cdc5f Mon Sep 17 00:00:00 2001 From: Paul Burns Date: Tue, 18 Aug 2026 20:17:56 -0400 Subject: [PATCH 3/7] Format the repository with fourmolu Mechanical only, no functional changes. Verify by re-running ./scripts/format-repo rather than by reading the diff. --- example/Setup.hs | 1 + example/app/Main.hs | 14 +- ftp-client-conduit/Setup.hs | 1 + .../src/Network/FTP/Client/Conduit.hs | 331 ++--- ftp-client/Setup.hs | 1 + ftp-client/src/Network/FTP/Client.hs | 1200 +++++++++-------- ftp-client/test/test.hs | 345 +++-- 7 files changed, 1023 insertions(+), 870 deletions(-) diff --git a/example/Setup.hs b/example/Setup.hs index 9a994af..e8ef27d 100644 --- a/example/Setup.hs +++ b/example/Setup.hs @@ -1,2 +1,3 @@ import Distribution.Simple + main = defaultMain diff --git a/example/app/Main.hs b/example/app/Main.hs index ae962e0..24d5570 100644 --- a/example/app/Main.hs +++ b/example/app/Main.hs @@ -1,14 +1,14 @@ module Main (main) where -import Network.FTP.Client -import qualified Network.FTP.Client.Conduit as FC import Conduit import qualified Data.ByteString.Char8 as C +import Network.FTP.Client +import qualified Network.FTP.Client.Conduit as FC main :: IO () main = withFTPS "hostname.com" 21 $ \h _welcome -> do - _ <- login h "username" "password" - runConduitRes - $ FC.mlsd h "." - .| mapC (C.pack . (<> "\n") . show) - .| stdoutC + _ <- login h "username" "password" + runConduitRes $ + FC.mlsd h "." + .| mapC (C.pack . (<> "\n") . show) + .| stdoutC diff --git a/ftp-client-conduit/Setup.hs b/ftp-client-conduit/Setup.hs index 9a994af..e8ef27d 100644 --- a/ftp-client-conduit/Setup.hs +++ b/ftp-client-conduit/Setup.hs @@ -1,2 +1,3 @@ import Distribution.Simple + main = defaultMain diff --git a/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs b/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs index 3968604..9f34c99 100644 --- a/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs +++ b/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs @@ -1,6 +1,6 @@ {-# LANGUAGE ScopedTypeVariables #-} -{-| +{- | Module : Network.FTP.Client.Conduit Description : Transfer files over FTP and FTPS with Conduit Copyright : Megan Robinson 2018-2019, Flipstone Technology Partners 2024-2026 @@ -8,14 +8,14 @@ License : Public Domain Stability : experimental Portability : POSIX -} -module Network.FTP.Client.Conduit ( - -- * Data commands - nlst, - retr, - list, - stor, - mlsd -) where +module Network.FTP.Client.Conduit + ( -- * Data commands + nlst + , retr + , list + , stor + , mlsd + ) where -- MonadResource appears in this module's exported signatures, so it is taken -- from resourcet directly rather than through Conduit's re-export. That keeps @@ -23,188 +23,201 @@ module Network.FTP.Client.Conduit ( import Conduit hiding (MonadResource) import Control.Monad.Trans.Resource (MonadResource) import Data.ByteString.Lazy.Internal (defaultChunkSize) -import qualified System.IO as SIO import Network.FTP.Client - ( sendCommandS - , FTPCommand(..) - , RTypeCode(..) - , createSendDataCommand - , createTLSSendDataCommand - , PortActivity(..) - , getResponse - , sIOHandleImpl - , tlsHandleImpl - , Security(..) - , parseMlsxLine - ) + ( FTPCommand (..) + , PortActivity (..) + , RTypeCode (..) + , Security (..) + , createSendDataCommand + , createTLSSendDataCommand + , getResponse + , parseMlsxLine + , sIOHandleImpl + , sendCommandS + , tlsHandleImpl + ) +import qualified System.IO as SIO -import qualified Network.FTP.Client as FTP -import qualified Data.ByteString as B +import qualified Control.Monad.Catch as M import Data.ByteString (ByteString) +import qualified Data.ByteString as B import qualified Network.Connection as Connection -import qualified Control.Monad.Catch as M +import qualified Network.FTP.Client as FTP debugging :: Bool debugging = False debugPrint :: (Show a, MonadIO m) => a -> m () debugPrint s = - if debugging - then liftIO $ print s - else return () + if debugging + then liftIO $ print s + else return () debugResponse :: (Show a, MonadIO m) => a -> m () debugResponse s = debugPrint $ "Recieved: " <> (show s) -getAllLineRespC - :: forall i m - . MonadIO m - => FTP.Handle - -> ConduitT i ByteString m () +getAllLineRespC :: + forall i m. + MonadIO m => + FTP.Handle -> + ConduitT i ByteString m () getAllLineRespC h = - let loop :: ConduitT i ByteString m () - loop = do - line <- liftIO - $ FTP.getLineResp h `M.catchIOError` const (return "") - if B.null line - then return () - else do - yield line - loop - in loop - -sendAllLineC - :: forall o m - . MonadIO m - => FTP.Handle - -> ConduitT ByteString o m () + let + loop :: ConduitT i ByteString m () + loop = do + line <- + liftIO $ + FTP.getLineResp h `M.catchIOError` const (return "") + if B.null line + then return () + else do + yield line + loop + in + loop + +sendAllLineC :: + forall o m. + MonadIO m => + FTP.Handle -> + ConduitT ByteString o m () sendAllLineC h = - let loop :: ConduitT ByteString o m () - loop = do - mx <- await - case mx of - Nothing -> return () - Just x -> do - liftIO $ FTP.sendLine h x - loop - in loop - -sourceDataCommandSecurity - :: MonadResource m - => FTP.Handle - -> PortActivity - -> RTypeCode - -> FTPCommand - -> (FTP.Handle -> ConduitM i o m r) - -> ConduitM i o m r + let + loop :: ConduitT ByteString o m () + loop = do + mx <- await + case mx of + Nothing -> return () + Just x -> do + liftIO $ FTP.sendLine h x + loop + in + loop + +sourceDataCommandSecurity :: + MonadResource m => + FTP.Handle -> + PortActivity -> + RTypeCode -> + FTPCommand -> + (FTP.Handle -> ConduitM i o m r) -> + ConduitM i o m r sourceDataCommandSecurity h = - case FTP.security h of - Clear -> sourceDataCommand h - TLS -> sourceTLSDataCommand h - -sourceDataCommand - :: MonadResource m - => FTP.Handle - -> PortActivity - -> RTypeCode - -> FTPCommand - -> (FTP.Handle -> ConduitM i o m r) - -> ConduitM i o m r + case FTP.security h of + Clear -> sourceDataCommand h + TLS -> sourceTLSDataCommand h + +sourceDataCommand :: + MonadResource m => + FTP.Handle -> + PortActivity -> + RTypeCode -> + FTPCommand -> + (FTP.Handle -> ConduitM i o m r) -> + ConduitM i o m r sourceDataCommand ch pa code cmd f = do - _ <- sendCommandS ch $ RType code - x <- bracketP - (createSendDataCommand ch pa cmd) - (liftIO . SIO.hClose) - (f . sIOHandleImpl) - resp <- getResponse ch - debugResponse resp - return x - -sourceTLSDataCommand - :: MonadResource m - => FTP.Handle - -> PortActivity - -> RTypeCode - -> FTPCommand - -> (FTP.Handle -> ConduitM i o m r) - -> ConduitM i o m r + _ <- sendCommandS ch $ RType code + x <- + bracketP + (createSendDataCommand ch pa cmd) + (liftIO . SIO.hClose) + (f . sIOHandleImpl) + resp <- getResponse ch + debugResponse resp + return x + +sourceTLSDataCommand :: + MonadResource m => + FTP.Handle -> + PortActivity -> + RTypeCode -> + FTPCommand -> + (FTP.Handle -> ConduitM i o m r) -> + ConduitM i o m r sourceTLSDataCommand ch pa code cmd f = do - _ <- sendCommandS ch $ RType code - x <- bracketP - (createTLSSendDataCommand ch pa cmd) - (liftIO . Connection.connectionClose) - (f . tlsHandleImpl) - resp <- getResponse ch - debugResponse resp - return x - -sourceFTPHandle - :: forall i m - . MonadIO m - => FTP.Handle - -> ConduitT i ByteString m () + _ <- sendCommandS ch $ RType code + x <- + bracketP + (createTLSSendDataCommand ch pa cmd) + (liftIO . Connection.connectionClose) + (f . tlsHandleImpl) + resp <- getResponse ch + debugResponse resp + return x + +sourceFTPHandle :: + forall i m. + MonadIO m => + FTP.Handle -> + ConduitT i ByteString m () sourceFTPHandle h = - let loop :: ConduitT i ByteString m () - loop = do - bs <- liftIO $ FTP.recv h defaultChunkSize - `M.catchIOError` const (return "") - if B.null bs - then return () - else do - yield bs - loop - in loop - -sinkFTPHandle - :: forall o m - . MonadIO m - => FTP.Handle - -> ConduitT ByteString o m () + let + loop :: ConduitT i ByteString m () + loop = do + bs <- + liftIO $ + FTP.recv h defaultChunkSize + `M.catchIOError` const (return "") + if B.null bs + then return () + else do + yield bs + loop + in + loop + +sinkFTPHandle :: + forall o m. + MonadIO m => + FTP.Handle -> + ConduitT ByteString o m () sinkFTPHandle h = - let loop :: ConduitT ByteString o m () - loop = do - mbs <- await - case mbs of - Nothing -> return () - Just bs -> do - liftIO $ FTP.send h bs - loop - in loop - -sendType - :: MonadResource m - => RTypeCode - -> FTP.Handle - -> ConduitT ByteString o m () + let + loop :: ConduitT ByteString o m () + loop = do + mbs <- await + case mbs of + Nothing -> return () + Just bs -> do + liftIO $ FTP.send h bs + loop + in + loop + +sendType :: + MonadResource m => + RTypeCode -> + FTP.Handle -> + ConduitT ByteString o m () sendType TA h = sendAllLineC h sendType TI h = sinkFTPHandle h nlst :: MonadResource m => FTP.Handle -> [String] -> ConduitT i ByteString m () nlst ch args = - sourceDataCommandSecurity ch Passive TA (Nlst args) getAllLineRespC + sourceDataCommandSecurity ch Passive TA (Nlst args) getAllLineRespC retr :: MonadResource m => FTP.Handle -> String -> ConduitT i ByteString m () retr ch path = - sourceDataCommandSecurity ch Passive TI (Retr path) sourceFTPHandle + sourceDataCommandSecurity ch Passive TI (Retr path) sourceFTPHandle list :: MonadResource m => FTP.Handle -> [String] -> ConduitT i ByteString m () list ch args = - sourceDataCommandSecurity ch Passive TA (List args) getAllLineRespC - -stor - :: MonadResource m - => FTP.Handle - -> String - -> RTypeCode - -> ConduitT ByteString o m () + sourceDataCommandSecurity ch Passive TA (List args) getAllLineRespC + +stor :: + MonadResource m => + FTP.Handle -> + String -> + RTypeCode -> + ConduitT ByteString o m () stor ch loc rtype = - sourceDataCommandSecurity ch Passive rtype (Stor loc) $ sendType rtype + sourceDataCommandSecurity ch Passive rtype (Stor loc) $ sendType rtype -mlsd - :: MonadResource m - => FTP.Handle - -> String - -> ConduitT i FTP.MlsxResponse m () +mlsd :: + MonadResource m => + FTP.Handle -> + String -> + ConduitT i FTP.MlsxResponse m () mlsd ch dir = - sourceDataCommandSecurity ch Passive TA (Mlsd dir) getAllLineRespC - .| mapC parseMlsxLine + sourceDataCommandSecurity ch Passive TA (Mlsd dir) getAllLineRespC + .| mapC parseMlsxLine diff --git a/ftp-client/Setup.hs b/ftp-client/Setup.hs index 9a994af..e8ef27d 100644 --- a/ftp-client/Setup.hs +++ b/ftp-client/Setup.hs @@ -1,2 +1,3 @@ import Distribution.Simple + main = defaultMain diff --git a/ftp-client/src/Network/FTP/Client.hs b/ftp-client/src/Network/FTP/Client.hs index 79b6ee8..4c8a2f9 100644 --- a/ftp-client/src/Network/FTP/Client.hs +++ b/ftp-client/src/Network/FTP/Client.hs @@ -1,4 +1,4 @@ -{-| +{- | Module : Network.FTP.Client Description : Transfer files over FTP and FTPS Copyright : Megan Robinson 2018-2019, Flipstone Technology Partners 2024-2026 @@ -6,89 +6,97 @@ License : Public Domain Stability : experimental Portability : POSIX -} -module Network.FTP.Client ( - -- * Main Entrypoints - withFTP, - withFTPS, +module Network.FTP.Client + ( -- * Main Entrypoints + withFTP + , withFTPS + -- * Control Commands - login, - pasv, - rename, - dele, - cwd, - size, - acct, - mkd, - rmd, - pwd, - quit, + , login + , pasv + , rename + , dele + , cwd + , size + , acct + , mkd + , rmd + , pwd + , quit + -- * Data Commands - nlst, - retr, - list, - stor, - mlsd, - mlst, + , nlst + , retr + , list + , stor + , mlsd + , mlst + -- * Types - FTPCommand(..), - FTPResponse(..), - FTPMessage(..), - ResponseStatus(..), - MlsxResponse(..), - RTypeCode(..), - PortActivity(..), - ProtType(..), - Security(..), - Handle(..), + , FTPCommand (..) + , FTPResponse (..) + , FTPMessage (..) + , ResponseStatus (..) + , MlsxResponse (..) + , RTypeCode (..) + , PortActivity (..) + , ProtType (..) + , Security (..) + , Handle (..) + -- * TLS Commands - pbsz, - prot, - ccc, - auth, + , pbsz + , prot + , ccc + , auth + -- * Exceptions - FTPException(..), + , FTPException (..) + -- * System Handle Creation - createSIOHandle, - createTLSConnection, - connectTLS, + , createSIOHandle + , createTLSConnection + , connectTLS + -- * Handle Implementations - sIOHandleImpl, - tlsHandleImpl, + , sIOHandleImpl + , tlsHandleImpl + -- * Lower Level Functions - sendCommand, - sendCommandS, - recvAll, - sendAll, - sendAllS, - getLineResp, - getResponse, - getResponseS, - sendCommandLine, - createSendDataCommand, - createTLSSendDataCommand, - parseMlsxLine -) where + , sendCommand + , sendCommandS + , recvAll + , sendAll + , sendAllS + , getLineResp + , getResponse + , getResponseS + , sendCommandLine + , createSendDataCommand + , createTLSSendDataCommand + , parseMlsxLine + ) where -import Data.Default.Class (def) -import qualified Data.ByteString.Char8 as C -import qualified Data.ByteString as B -import Data.ByteString (ByteString) -import Data.List (intercalate) -import qualified Data.Attoparsec.ByteString.Char8 as AC -import qualified Network.Socket as S -import qualified System.IO as SIO +import Control.Arrow ((***)) import qualified Control.Exception as Exception +import Control.Monad ((<=<)) +import qualified Control.Monad as Monad import Control.Monad.Catch (MonadCatch, MonadMask) import qualified Control.Monad.Catch as M -import qualified Control.Monad as Monad -import Control.Monad ((<=<)) import qualified Control.Monad.IO.Class as MIO +import qualified Data.Attoparsec.ByteString.Char8 as AC import qualified Data.Bits as Bits -import qualified Network.Connection as Connection +import Data.ByteString (ByteString) +import qualified Data.ByteString as B +import qualified Data.ByteString.Char8 as C import Data.ByteString.Lazy.Internal (defaultChunkSize) +import Data.Default.Class (def) +import Data.List (intercalate) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map -import Control.Arrow ((***)) +import qualified Network.Connection as Connection +import qualified Network.Socket as S +import qualified System.IO as SIO import System.IO.Error (isEOFError) debugging :: Bool @@ -104,58 +112,67 @@ data Security = Clear | TLS -- | Can send and recieve a 'Data.ByteString.ByteString'. data Handle = Handle - { send :: ByteString -> IO () - , sendLine :: ByteString -> IO () - , recv :: Int -> IO ByteString - , recvLine :: IO ByteString - , security :: Security - } + { send :: ByteString -> IO () + , sendLine :: ByteString -> IO () + , recv :: Int -> IO ByteString + , recvLine :: IO ByteString + , security :: Security + } data FTPMessage = SingleLine ByteString | MultiLine [ByteString] - deriving Eq + deriving (Eq) instance Show FTPMessage where - show (SingleLine message) = C.unpack message - show (MultiLine messages) = intercalate "\n" $ C.unpack <$> messages + show (SingleLine message) = C.unpack message + show (MultiLine messages) = intercalate "\n" $ C.unpack <$> messages -- | Response from an FTP command. ex "200 Welcome!" -data FTPResponse = FTPResponse { - frStatus :: ResponseStatus, -- ^ Interpretation of the first AC.digit of an FTP response code - frCode :: Int, -- ^ The three AC.digit response code - frMessage :: FTPMessage -- ^ Text of the response -} deriving Eq +data FTPResponse = FTPResponse + { frStatus :: ResponseStatus + -- ^ Interpretation of the first AC.digit of an FTP response code + , frCode :: Int + -- ^ The three AC.digit response code + , frMessage :: FTPMessage + -- ^ Text of the response + } + deriving (Eq) instance Show FTPResponse where - show fr = show (frCode fr) <> " " <> show (frMessage fr) + show fr = show (frCode fr) <> " " <> show (frMessage fr) -- | First AC.digit of an FTP response data ResponseStatus - = Wait -- ^ 1 - | Success -- ^ 2 - | Continue -- ^ 3 - | FailureRetry -- ^ 4 - | Failure -- ^ 5 - deriving (Show, Eq) + = -- | 1 + Wait + | -- | 2 + Success + | -- | 3 + Continue + | -- | 4 + FailureRetry + | -- | 5 + Failure + deriving (Show, Eq) data FTPException - = FailureRetryException FTPResponse - | FailureException FTPResponse - | UnsuccessfulException FTPResponse - | BogusResponseFormatException FTPResponse - | BadProtocolResponseException ByteString - deriving (Show) + = FailureRetryException FTPResponse + | FailureException FTPResponse + | UnsuccessfulException FTPResponse + | BogusResponseFormatException FTPResponse + | BadProtocolResponseException ByteString + deriving (Show) instance Exception.Exception FTPException responseStatus :: ByteString -> ResponseStatus responseStatus cbs = - case C.uncons cbs of - Just ('1', _) -> Wait - Just ('2', _) -> Success - Just ('3', _) -> Continue - Just ('4', _) -> FailureRetry - Just ('5', _) -> Failure - _ -> Exception.throw $ BadProtocolResponseException cbs + case C.uncons cbs of + Just ('1', _) -> Wait + Just ('2', _) -> Success + Just ('3', _) -> Continue + Just ('4', _) -> FailureRetry + Just ('5', _) -> Failure + _ -> Exception.throw $ BadProtocolResponseException cbs data RTypeCode = TA | TI @@ -169,75 +186,77 @@ data ProtType = P | C -- | Commands according to the FTP specification data FTPCommand - = User String - | Pass String - | Acct String - | RType RTypeCode - | Retr String - | Nlst [String] - | Port S.HostAddress S.PortNumber - | Stor String - | List [String] - | Rnfr String - | Rnto String - | Dele String - | Size String - | Mkd String - | Rmd String - | Pbsz Int - | Prot ProtType - | Mlsd String - | Mlst String - | Cwd String - | Cdup - | Ccc - | Auth - | Pwd - | Abor - | Pasv - | Quit + = User String + | Pass String + | Acct String + | RType RTypeCode + | Retr String + | Nlst [String] + | Port S.HostAddress S.PortNumber + | Stor String + | List [String] + | Rnfr String + | Rnto String + | Dele String + | Size String + | Mkd String + | Rmd String + | Pbsz Int + | Prot ProtType + | Mlsd String + | Mlst String + | Cwd String + | Cdup + | Ccc + | Auth + | Pwd + | Abor + | Pasv + | Quit instance Show FTPCommand where - show = serializeCommand + show = serializeCommand formatPort :: S.HostAddress -> S.PortNumber -> String formatPort ha pn = - let (w1, w2, w3, w4) = S.hostAddressToTuple ha - hn = show <$> [w1, w2, w3, w4] - portParts = show <$> [pn `quot` 256, pn `mod` 256] - in intercalate "," (hn <> portParts) + let + (w1, w2, w3, w4) = S.hostAddressToTuple ha + hn = show <$> [w1, w2, w3, w4] + portParts = show <$> [pn `quot` 256, pn `mod` 256] + in + intercalate "," (hn <> portParts) serializeCommand :: FTPCommand -> String -serializeCommand (User user) = "USER " <> user -serializeCommand (Pass pass) = "PASS " <> pass +serializeCommand (User user) = "USER " <> user +serializeCommand (Pass pass) = "PASS " <> pass serializeCommand (Acct account) = "ACCT " <> account -serializeCommand (RType rt) = "TYPE " <> serialzeRTypeCode rt -serializeCommand (Retr file) = "RETR " <> file -serializeCommand (Nlst []) = "NLST" -serializeCommand (Nlst args) = "NLST " <> unwords args +serializeCommand (RType rt) = "TYPE " <> serialzeRTypeCode rt +serializeCommand (Retr file) = "RETR " <> file +serializeCommand (Nlst []) = "NLST" +serializeCommand (Nlst args) = "NLST " <> unwords args serializeCommand (Port ha pn) = "PORT " <> formatPort ha pn -serializeCommand (Stor loc) = "STOR " <> loc -serializeCommand (List []) = "LIST" -serializeCommand (List args) = "LIST " <> unwords args -serializeCommand (Rnfr from) = "RNFR " <> from -serializeCommand (Rnto to) = "RNTO " <> to -serializeCommand (Dele file) = "DELE " <> file -serializeCommand (Size file) = "SIZE " <> file -serializeCommand (Mkd dir) = "MKD " <> dir -serializeCommand (Rmd dir) = "RMD " <> dir -serializeCommand (Pbsz buf) = "PBSZ " <> show buf -serializeCommand (Prot P) = "PROT P" -serializeCommand (Prot C) = "PROT C" -serializeCommand (Mlsd path) = "MLSD " <> path -serializeCommand (Mlst path) = "MLST " <> path -serializeCommand (Cwd dir) = "CWD " <> dir -serializeCommand Cdup = "CDUP" -serializeCommand Ccc = "CCC" -serializeCommand Auth = "AUTH TLS" -serializeCommand Pwd = "PWD" -serializeCommand Abor = "ABOR" -serializeCommand Pasv = "PASV" -serializeCommand Quit = "QUIT" +serializeCommand (Stor loc) = "STOR " <> loc +serializeCommand (List []) = "LIST" +serializeCommand (List args) = "LIST " <> unwords args +serializeCommand (Rnfr from) = "RNFR " <> from +serializeCommand (Rnto to) = "RNTO " <> to +serializeCommand (Dele file) = "DELE " <> file +serializeCommand (Size file) = "SIZE " <> file +serializeCommand (Mkd dir) = "MKD " <> dir +serializeCommand (Rmd dir) = "RMD " <> dir +serializeCommand (Pbsz buf) = "PBSZ " <> show buf +serializeCommand (Prot P) = "PROT P" +serializeCommand (Prot C) = "PROT C" +serializeCommand (Mlsd path) = "MLSD " <> path +serializeCommand (Mlst path) = "MLST " <> path +serializeCommand (Cwd dir) = "CWD " <> dir +serializeCommand Cdup = "CDUP" +serializeCommand Ccc = "CCC" +serializeCommand Auth = "AUTH TLS" +serializeCommand Pwd = "PWD" +serializeCommand Abor = "ABOR" +serializeCommand Pasv = "PASV" +serializeCommand Quit = "QUIT" stripCLRF :: ByteString -> ByteString stripCLRF = C.takeWhile $ (&&) <$> (/= '\r') <*> (/= '\n') @@ -246,77 +265,86 @@ stripCLRF = C.takeWhile $ (&&) <$> (/= '\r') <*> (/= '\n') getLineResp :: Handle -> IO ByteString getLineResp h = stripCLRF <$> recvLine h --- | Get a line from the server, returning 'Nothing' once the stream is --- exhausted. A blank line and end of input are different things: 'recvLine' --- signals end of input by throwing, and an empty 'ByteString' is a legitimate --- line of reply text. +{- | Get a line from the server, returning 'Nothing' once the stream is +exhausted. A blank line and end of input are different things: 'recvLine' +signals end of input by throwing, and an empty 'ByteString' is a legitimate +line of reply text. +-} getLineRespMaybe :: Handle -> IO (Maybe ByteString) getLineRespMaybe h = - (Just <$> getLineResp h) `M.catchIOError` \e -> - if isEOFError e - then return Nothing - else ioError e + (Just <$> getLineResp h) `M.catchIOError` \e -> + if isEOFError e + then return Nothing + else ioError e --- | Get a full response from the server --- Used in 'sendCommand' +{- | Get a full response from the server +Used in 'sendCommand' +-} getResponse :: MIO.MonadIO m => Handle -> m FTPResponse getResponse h = do - line <- MIO.liftIO $ getLineResp h - let (code, rest) = C.splitAt 3 line - -- A response must open with a three digit code. Checking that up front keeps - -- the 'C.uncons' below and the 'read' further down from being partial. - Monad.when (C.length code < 3 || not (C.all AC.isDigit code)) - $ MIO.liftIO $ Exception.throwIO $ BadProtocolResponseException line - message <- case C.uncons rest of - Just ('-', _) -> MultiLine <$> loopMultiLine h code [line] - _ -> return $ SingleLine line - let codeDroppedMessage = case message of - SingleLine singleMessage -> SingleLine $ C.drop 4 singleMessage - MultiLine [] -> MultiLine [] - MultiLine (firstMessage:messages) -> - MultiLine $ C.drop 4 firstMessage : messages - let response = FTPResponse - (responseStatus code) - (read $ C.unpack code) - codeDroppedMessage - case frStatus response of - FailureRetry -> MIO.liftIO $ Exception.throwIO $ FailureRetryException response - Failure -> MIO.liftIO $ Exception.throwIO $ FailureException response - _ -> return response - -loopMultiLine - :: MIO.MonadIO m - => Handle - -> ByteString - -> [ByteString] - -> m [ByteString] + line <- MIO.liftIO $ getLineResp h + let + (code, rest) = C.splitAt 3 line + -- A response must open with a three digit code. Checking that up front keeps + -- the 'C.uncons' below and the 'read' further down from being partial. + Monad.when (C.length code < 3 || not (C.all AC.isDigit code)) $ + MIO.liftIO $ + Exception.throwIO $ + BadProtocolResponseException line + message <- case C.uncons rest of + Just ('-', _) -> MultiLine <$> loopMultiLine h code [line] + _ -> return $ SingleLine line + let + codeDroppedMessage = case message of + SingleLine singleMessage -> SingleLine $ C.drop 4 singleMessage + MultiLine [] -> MultiLine [] + MultiLine (firstMessage : messages) -> + MultiLine $ C.drop 4 firstMessage : messages + let + response = + FTPResponse + (responseStatus code) + (read $ C.unpack code) + codeDroppedMessage + case frStatus response of + FailureRetry -> MIO.liftIO $ Exception.throwIO $ FailureRetryException response + Failure -> MIO.liftIO $ Exception.throwIO $ FailureException response + _ -> return response + +loopMultiLine :: + MIO.MonadIO m => + Handle -> + ByteString -> + [ByteString] -> + m [ByteString] loopMultiLine h code priorLines = do - mNextLine <- MIO.liftIO $ getLineRespMaybe h - case mNextLine of - -- The server hung up before sending the terminating line. Return what - -- was collected rather than looping forever. Note this is end of input, - -- not a blank line: RFC 959 lets the intermediate lines of a multiline - -- reply hold arbitrary text, blank lines included, so a blank line has - -- to be kept and the loop has to continue past it. - Nothing -> return priorLines - Just nextLine -> do - -- RFC 959 (https://datatracker.ietf.org/doc/html/rfc959#page-36) ends a - -- multiline reply with the code followed by a space, and continues it - -- with the code followed by a hyphen. The bare code is accepted too, - -- for servers that omit the trailing space on an empty final line. - let newLines = priorLines <> [C.dropWhile (== ' ') nextLine] - isLastLine = - nextLine == code - || C.isPrefixOf (code <> " ") nextLine - if isLastLine - then return newLines - else loopMultiLine h code newLines + mNextLine <- MIO.liftIO $ getLineRespMaybe h + case mNextLine of + -- The server hung up before sending the terminating line. Return what + -- was collected rather than looping forever. Note this is end of input, + -- not a blank line: RFC 959 lets the intermediate lines of a multiline + -- reply hold arbitrary text, blank lines included, so a blank line has + -- to be kept and the loop has to continue past it. + Nothing -> return priorLines + Just nextLine -> do + -- RFC 959 (https://datatracker.ietf.org/doc/html/rfc959#page-36) ends a + -- multiline reply with the code followed by a space, and continues it + -- with the code followed by a hyphen. The bare code is accepted too, + -- for servers that omit the trailing space on an empty final line. + let + newLines = priorLines <> [C.dropWhile (== ' ') nextLine] + isLastLine = + nextLine == code + || C.isPrefixOf (code <> " ") nextLine + if isLastLine + then return newLines + else loopMultiLine h code newLines ensureSuccess :: MIO.MonadIO m => FTPResponse -> m FTPResponse ensureSuccess resp = - case frStatus resp of - Success -> return resp - _ -> MIO.liftIO $ Exception.throwIO $ UnsuccessfulException resp + case frStatus resp of + Success -> return resp + _ -> MIO.liftIO $ Exception.throwIO $ UnsuccessfulException resp getResponseS :: MIO.MonadIO m => Handle -> m FTPResponse getResponseS = ensureSuccess <=< getResponse @@ -324,94 +352,106 @@ getResponseS = ensureSuccess <=< getResponse sendCommandLine :: MIO.MonadIO m => Handle -> ByteString -> m () sendCommandLine h = MIO.liftIO . send h . (<> "\r\n") --- | Send a command to the server and get a response back. --- Some commands use a data 'Handle', and their data is not returned here. +{- | Send a command to the server and get a response back. +Some commands use a data 'Handle', and their data is not returned here. +-} sendCommand :: MIO.MonadIO m => Handle -> FTPCommand -> m FTPResponse sendCommand h fc = do - let command = serializeCommand fc - debugPrint $ "Sending: " <> command - sendCommandLine h $ C.pack command - resp <- getResponse h - debugResponse resp - return resp + let + command = serializeCommand fc + debugPrint $ "Sending: " <> command + sendCommandLine h $ C.pack command + resp <- getResponse h + debugResponse resp + return resp sendCommandS :: MIO.MonadIO m => Handle -> FTPCommand -> m FTPResponse sendCommandS h fc = sendCommand h fc >>= ensureSuccess --- | Equvalent to --- --- > mapM . sendCommand +{- | Equvalent to + +> mapM . sendCommand +-} sendAll :: MIO.MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse] sendAll = mapM . sendCommand --- | Equvalent to --- --- > mapM . sendCommandS +{- | Equvalent to + +> mapM . sendCommandS +-} sendAllS :: MIO.MonadIO m => Handle -> [FTPCommand] -> m [FTPResponse] sendAllS = mapM . sendCommandS -- Control connection -createSocket - :: MIO.MonadIO m - => Maybe String - -> Int - -> S.AddrInfo - -> m (S.Socket, S.AddrInfo) +createSocket :: + MIO.MonadIO m => + Maybe String -> + Int -> + S.AddrInfo -> + m (S.Socket, S.AddrInfo) createSocket host portNum hints = do - addr <- MIO.liftIO $ do - a:_ <- S.getAddrInfo (Just hints) host (Just $ show portNum) - return a - debugPrint $ "Addr: " <> show addr - sock <- MIO.liftIO $ S.socket + addr <- MIO.liftIO $ do + a : _ <- S.getAddrInfo (Just hints) host (Just $ show portNum) + return a + debugPrint $ "Addr: " <> show addr + sock <- + MIO.liftIO $ + S.socket (S.addrFamily addr) (S.addrSocketType addr) (S.addrProtocol addr) - return (sock, addr) - -withSocketPassive - :: (MIO.MonadIO m, MonadMask m) - => String - -> Int - -> (S.Socket -> m a) - -> m a + return (sock, addr) + +withSocketPassive :: + (MIO.MonadIO m, MonadMask m) => + String -> + Int -> + (S.Socket -> m a) -> + m a withSocketPassive host portNum f = do - let hints = S.defaultHints { - S.addrSocketType = S.Stream - } - M.bracketOnError - (createSocket (Just host) portNum hints) - (MIO.liftIO . S.close . fst) - (\(sock, addr) -> do - debugPrint ("Connecting" :: String) - MIO.liftIO $ S.connect sock (S.addrAddress addr) - debugPrint ("Connected" :: String) - f sock - ) + let + hints = + S.defaultHints + { S.addrSocketType = S.Stream + } + M.bracketOnError + (createSocket (Just host) portNum hints) + (MIO.liftIO . S.close . fst) + ( \(sock, addr) -> do + debugPrint ("Connecting" :: String) + MIO.liftIO $ S.connect sock (S.addrAddress addr) + debugPrint ("Connected" :: String) + f sock + ) withSocketActive :: (MIO.MonadIO m, MonadMask m) => (S.Socket -> m a) -> m a withSocketActive f = do - let hints = S.defaultHints { - S.addrSocketType = S.Stream, - S.addrFlags = [S.AI_PASSIVE] - } - M.bracketOnError - (createSocket Nothing 0 hints) - (MIO.liftIO . S.close . fst) - (\(sock, addr) -> do - debugPrint ("Binding" :: String) - MIO.liftIO $ S.bind sock (S.addrAddress addr) - MIO.liftIO $ S.listen sock 1 - debugPrint ("Listening" :: String) - f sock - ) + let + hints = + S.defaultHints + { S.addrSocketType = S.Stream + , S.addrFlags = [S.AI_PASSIVE] + } + M.bracketOnError + (createSocket Nothing 0 hints) + (MIO.liftIO . S.close . fst) + ( \(sock, addr) -> do + debugPrint ("Binding" :: String) + MIO.liftIO $ S.bind sock (S.addrAddress addr) + MIO.liftIO $ S.listen sock 1 + debugPrint ("Listening" :: String) + f sock + ) createSIOHandle :: (MIO.MonadIO m, MonadMask m) => String -> Int -> m SIO.Handle -createSIOHandle host portNum = withSocketPassive host portNum - $ MIO.liftIO . flip S.socketToHandle SIO.ReadWriteMode +createSIOHandle host portNum = + withSocketPassive host portNum $ + MIO.liftIO . flip S.socketToHandle SIO.ReadWriteMode sIOHandleImpl :: SIO.Handle -> Handle -sIOHandleImpl h = Handle +sIOHandleImpl h = + Handle { send = C.hPut h , sendLine = C.hPutStrLn h , recv = C.hGetSome h @@ -419,69 +459,71 @@ sIOHandleImpl h = Handle , security = Clear } -withSIOHandle - :: (MIO.MonadIO m, MonadMask m) - => String - -> Int - -> (Handle -> m a) - -> m a -withSIOHandle host portNum f = M.bracket +withSIOHandle :: + (MIO.MonadIO m, MonadMask m) => + String -> + Int -> + (Handle -> m a) -> + m a +withSIOHandle host portNum f = + M.bracket (MIO.liftIO $ createSIOHandle host portNum) (MIO.liftIO . SIO.hClose) (f . sIOHandleImpl) --- | Takes a host name and port. A handle for interacting with the server --- will be returned in a callback. --- --- @ --- withFTP "ftp.server.com" 21 $ \h welcome -> do --- print welcome --- login h "username" "password" --- print =<< nlst h [] --- @ -withFTP - :: (MIO.MonadIO m, MonadMask m) - => String - -> Int - -> (Handle -> FTPResponse -> m a) - -> m a +{- | Takes a host name and port. A handle for interacting with the server +will be returned in a callback. + +@ +withFTP "ftp.server.com" 21 $ \h welcome -> do + print welcome + login h "username" "password" + print =<< nlst h [] +@ +-} +withFTP :: + (MIO.MonadIO m, MonadMask m) => + String -> + Int -> + (Handle -> FTPResponse -> m a) -> + m a withFTP host portNum f = withSIOHandle host portNum $ \h -> do - resp <- getResponse h - f h resp + resp <- getResponse h + f h resp -- Data connection -withDataSocketPasv - :: (MIO.MonadIO m, MonadMask m) - => Handle - -> (S.Socket -> m a) - -> m a +withDataSocketPasv :: + (MIO.MonadIO m, MonadMask m) => + Handle -> + (S.Socket -> m a) -> + m a withDataSocketPasv h f = do - (host, portNum) <- pasv h - debugPrint $ "Host: " <> host - debugPrint $ "Port: " <> show portNum - withSocketPassive host portNum f - -withDataSocketActive - :: (MIO.MonadIO m, MonadMask m) - => Handle - -> (S.Socket -> m a) - -> m a + (host, portNum) <- pasv h + debugPrint $ "Host: " <> host + debugPrint $ "Port: " <> show portNum + withSocketPassive host portNum f + +withDataSocketActive :: + (MIO.MonadIO m, MonadMask m) => + Handle -> + (S.Socket -> m a) -> + m a withDataSocketActive h f = withSocketActive $ \socket -> do - (sPort, sHost) <- MIO.liftIO $ do - (S.SockAddrInet p hostAddr) <- S.getSocketName socket - return (p,hostAddr) - _ <- port h sHost sPort - f socket + (sPort, sHost) <- MIO.liftIO $ do + (S.SockAddrInet p hostAddr) <- S.getSocketName socket + return (p, hostAddr) + _ <- port h sHost sPort + f socket -- | Open a socket that can be used for data transfers -withDataSocket - :: (MIO.MonadIO m, MonadMask m) - => PortActivity - -> Handle - -> (S.Socket -> m a) - -> m a -withDataSocket Active = withDataSocketActive +withDataSocket :: + (MIO.MonadIO m, MonadMask m) => + PortActivity -> + Handle -> + (S.Socket -> m a) -> + m a +withDataSocket Active = withDataSocketActive withDataSocket Passive = withDataSocketPasv acceptData :: MIO.MonadIO m => PortActivity -> S.Socket -> m S.Socket @@ -492,102 +534,118 @@ acceptData Active = return . fst <=< MIO.liftIO . S.accept -- some servers will respond with 200 before 150 so just ignore it ensureSucessfulData :: MIO.MonadIO m => Handle -> FTPResponse -> m () ensureSucessfulData h resp = do - resp' <- case frStatus resp of - Success -> do - newResp <- getResponse h - debugResponse newResp - return newResp - _ -> return resp - MIO.liftIO $ Monad.when (frStatus resp' /= Wait) - $ Exception.throwIO $ UnsuccessfulException resp - --- | Send setup commands to the server and --- create a data 'System.IO.Handle' -createSendDataCommand - :: (MIO.MonadIO m, MonadMask m) - => Handle - -> PortActivity - -> FTPCommand - -> m SIO.Handle + resp' <- case frStatus resp of + Success -> do + newResp <- getResponse h + debugResponse newResp + return newResp + _ -> return resp + MIO.liftIO $ + Monad.when (frStatus resp' /= Wait) $ + Exception.throwIO $ + UnsuccessfulException resp + +{- | Send setup commands to the server and +create a data 'System.IO.Handle' +-} +createSendDataCommand :: + (MIO.MonadIO m, MonadMask m) => + Handle -> + PortActivity -> + FTPCommand -> + m SIO.Handle createSendDataCommand h pa cmd = withDataSocket pa h $ \socket -> do - resp <- sendCommand h cmd - ensureSucessfulData h resp - acceptedSock <- acceptData pa socket - MIO.liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode + resp <- sendCommand h cmd + ensureSucessfulData h resp + acceptedSock <- acceptData pa socket + MIO.liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode -- | Provides a data 'Handle' in a callback for a command -withDataCommand - :: (MIO.MonadIO m, MonadMask m) - => Handle - -> PortActivity - -> RTypeCode - -> FTPCommand - -> (Handle -> m a) - -> m a +withDataCommand :: + (MIO.MonadIO m, MonadMask m) => + Handle -> + PortActivity -> + RTypeCode -> + FTPCommand -> + (Handle -> m a) -> + m a withDataCommand ch pa code cmd f = do - _ <- sendCommandS ch $ RType code - x <- M.bracket - (createSendDataCommand ch pa cmd) - (MIO.liftIO . SIO.hClose) - (f . sIOHandleImpl) - resp <- getResponse ch - debugResponse resp - return x + _ <- sendCommandS ch $ RType code + x <- + M.bracket + (createSendDataCommand ch pa cmd) + (MIO.liftIO . SIO.hClose) + (f . sIOHandleImpl) + resp <- getResponse ch + debugResponse resp + return x -- | Recieve data and interpret it linewise getAllLineResp :: (MIO.MonadIO m, MonadCatch m) => Handle -> m ByteString getAllLineResp h = - let collect :: (MIO.MonadIO n, MonadCatch n) => [ByteString] -> n ByteString - collect ret = ( do - line <- MIO.liftIO $ getLineResp h - collect (ret <> [line])) - `M.catchIOError` (\_ -> return $ C.intercalate "\n" ret) - in collect [] + let + collect :: (MIO.MonadIO n, MonadCatch n) => [ByteString] -> n ByteString + collect ret = + ( do + line <- MIO.liftIO $ getLineResp h + collect (ret <> [line]) + ) + `M.catchIOError` (\_ -> return $ C.intercalate "\n" ret) + in + collect [] -- | Recieve all data and return it as a 'Data.ByteString.ByteString' recvAll :: (MIO.MonadIO m, MonadCatch m) => Handle -> m ByteString recvAll h = - let collect :: (MIO.MonadIO n, MonadCatch n) => ByteString -> n ByteString - collect bs = ( do - chunk <- MIO.liftIO $ recv h defaultChunkSize - if C.null chunk - then return bs - else collect $ bs <> chunk - ) `M.catchIOError` (\_ -> return bs) - in collect "" + let + collect :: (MIO.MonadIO n, MonadCatch n) => ByteString -> n ByteString + collect bs = + ( do + chunk <- MIO.liftIO $ recv h defaultChunkSize + if C.null chunk + then return bs + else collect $ bs <> chunk + ) + `M.catchIOError` (\_ -> return bs) + in + collect "" -- TLS connection connectTLS :: MIO.MonadIO m => SIO.Handle -> String -> Int -> m Connection.Connection connectTLS h host portNum = do - context <- MIO.liftIO Connection.initConnectionContext - let tlsSettings = case def of - simpleSettings@Connection.TLSSettingsSimple{} -> - simpleSettings { Connection.settingDisableCertificateValidation = True } - otherSettings -> otherSettings - connectionParams = Connection.ConnectionParams - { Connection.connectionHostname = host - , Connection.connectionPort = toEnum . fromEnum $ portNum - , Connection.connectionUseSecure = Just tlsSettings - , Connection.connectionUseSocks = Nothing - } - MIO.liftIO $ Connection.connectFromHandle context h connectionParams - -createTLSConnection - :: (MIO.MonadIO m, MonadMask m) - => String - -> Int - -> m (FTPResponse, Connection.Connection) + context <- MIO.liftIO Connection.initConnectionContext + let + tlsSettings = case def of + simpleSettings@Connection.TLSSettingsSimple {} -> + simpleSettings {Connection.settingDisableCertificateValidation = True} + otherSettings -> otherSettings + connectionParams = + Connection.ConnectionParams + { Connection.connectionHostname = host + , Connection.connectionPort = toEnum . fromEnum $ portNum + , Connection.connectionUseSecure = Just tlsSettings + , Connection.connectionUseSocks = Nothing + } + MIO.liftIO $ Connection.connectFromHandle context h connectionParams + +createTLSConnection :: + (MIO.MonadIO m, MonadMask m) => + String -> + Int -> + m (FTPResponse, Connection.Connection) createTLSConnection host portNum = do - h <- createSIOHandle host portNum - let insecureH = sIOHandleImpl h - resp <- getResponse insecureH - _ <- sendCommand insecureH Auth - conn <- connectTLS h host portNum - return (resp, conn) + h <- createSIOHandle host portNum + let + insecureH = sIOHandleImpl h + resp <- getResponse insecureH + _ <- sendCommand insecureH Auth + conn <- connectTLS h host portNum + return (resp, conn) tlsHandleImpl :: Connection.Connection -> Handle -tlsHandleImpl c = Handle +tlsHandleImpl c = + Handle { send = Connection.connectionPut c , sendLine = Connection.connectionPut c . (<> "\n") , recv = Connection.connectionGet c @@ -595,118 +653,131 @@ tlsHandleImpl c = Handle , security = TLS } -withTLSHandle - :: (MonadMask m, MIO.MonadIO m) - => String - -> Int - -> (Handle -> FTPResponse -> m a) - -> m a -withTLSHandle host portNum f = M.bracket +withTLSHandle :: + (MonadMask m, MIO.MonadIO m) => + String -> + Int -> + (Handle -> FTPResponse -> m a) -> + m a +withTLSHandle host portNum f = + M.bracket (createTLSConnection host portNum) (MIO.liftIO . Connection.connectionClose . snd) (\(resp, conn) -> f (tlsHandleImpl conn) resp) --- | Takes a host name and port. A handle for interacting with the server --- will be returned in a callback. The commands will be protected with TLS. --- --- @ --- withFTPS "ftps.server.com" 21 $ \h welcome -> do --- print welcome --- login h "username" "password" --- print =<< nlst h [] --- @ -withFTPS - :: (MonadMask m, MIO.MonadIO m) - => String - -> Int - -> (Handle -> FTPResponse -> m a) - -> m a +{- | Takes a host name and port. A handle for interacting with the server +will be returned in a callback. The commands will be protected with TLS. + +@ +withFTPS "ftps.server.com" 21 $ \h welcome -> do + print welcome + login h "username" "password" + print =<< nlst h [] +@ +-} +withFTPS :: + (MonadMask m, MIO.MonadIO m) => + String -> + Int -> + (Handle -> FTPResponse -> m a) -> + m a withFTPS = withTLSHandle -- TLS data connection --- | Send setup commands to the server and --- create a data TLS connection -createTLSSendDataCommand - :: (MIO.MonadIO m, MonadMask m) - => Handle - -> PortActivity - -> FTPCommand - -> m Connection.Connection +{- | Send setup commands to the server and +create a data TLS connection +-} +createTLSSendDataCommand :: + (MIO.MonadIO m, MonadMask m) => + Handle -> + PortActivity -> + FTPCommand -> + m Connection.Connection createTLSSendDataCommand ch pa cmd = do - _ <- sendAllS ch [Pbsz 0, Prot P] - withDataSocket pa ch $ \socket -> do - resp <- sendCommand ch cmd - ensureSucessfulData ch resp - acceptedSock <- acceptData pa socket - (sPort, sHost) <- MIO.liftIO $ do - (S.SockAddrInet p h) <- S.getSocketName acceptedSock - return (p, h) - let (h1, h2, h3, h4) = S.hostAddressToTuple sHost - hostName = intercalate "." $ show . fromEnum <$> [h1, h2, h3, h4] - h <- MIO.liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode - MIO.liftIO $ connectTLS h hostName (fromEnum sPort) - -withTLSDataCommand - :: (MIO.MonadIO m, MonadMask m) - => Handle - -> PortActivity - -> RTypeCode - -> FTPCommand - -> (Handle -> m a) - -> m a + _ <- sendAllS ch [Pbsz 0, Prot P] + withDataSocket pa ch $ \socket -> do + resp <- sendCommand ch cmd + ensureSucessfulData ch resp + acceptedSock <- acceptData pa socket + (sPort, sHost) <- MIO.liftIO $ do + (S.SockAddrInet p h) <- S.getSocketName acceptedSock + return (p, h) + let + (h1, h2, h3, h4) = S.hostAddressToTuple sHost + hostName = intercalate "." $ show . fromEnum <$> [h1, h2, h3, h4] + h <- MIO.liftIO $ S.socketToHandle acceptedSock SIO.ReadWriteMode + MIO.liftIO $ connectTLS h hostName (fromEnum sPort) + +withTLSDataCommand :: + (MIO.MonadIO m, MonadMask m) => + Handle -> + PortActivity -> + RTypeCode -> + FTPCommand -> + (Handle -> m a) -> + m a withTLSDataCommand ch pa code cmd f = do - _ <- sendCommandS ch $ RType code - x <- M.bracket - (createTLSSendDataCommand ch pa cmd) - (MIO.liftIO . Connection.connectionClose) - (f . tlsHandleImpl) - resp <- getResponse ch - debugPrint $ "Recieved: " <> show resp - return x + _ <- sendCommandS ch $ RType code + x <- + M.bracket + (createTLSSendDataCommand ch pa cmd) + (MIO.liftIO . Connection.connectionClose) + (f . tlsHandleImpl) + resp <- getResponse ch + debugPrint $ "Recieved: " <> show resp + return x parseResponse :: MIO.MonadIO m => FTPResponse -> AC.Parser a -> m a parseResponse resp p = - let parsableMessage = case frMessage resp of - SingleLine message -> message - MultiLine messages -> C.intercalate "\n" messages - in case AC.parseOnly p parsableMessage of - Right x -> return x - Left _ -> MIO.liftIO $ Exception.throwIO - $ BadProtocolResponseException parsableMessage + let + parsableMessage = case frMessage resp of + SingleLine message -> message + MultiLine messages -> C.intercalate "\n" messages + in + case AC.parseOnly p parsableMessage of + Right x -> return x + Left _ -> + MIO.liftIO $ + Exception.throwIO $ + BadProtocolResponseException parsableMessage ensureCode :: MIO.MonadIO m => FTPResponse -> Int -> m () ensureCode resp code = - MIO.liftIO $ Monad.when (frCode resp /= code) - $ MIO.liftIO $ Exception.throwIO $ UnsuccessfulException resp + MIO.liftIO $ + Monad.when (frCode resp /= code) $ + MIO.liftIO $ + Exception.throwIO $ + UnsuccessfulException resp parse227 :: AC.Parser (String, Int) parse227 = do - _ <- AC.skipWhile (/= '(') *> AC.char '(' - [h1,h2,h3,h4,p1,p2] <- AC.many1 AC.digit `AC.sepBy` AC.char ',' - let host = intercalate "." [h1,h2,h3,h4] - highBits = read p1 - lowBits = read p2 - portNum = (highBits `Bits.shift` 8) + lowBits - return (host, portNum) + _ <- AC.skipWhile (/= '(') *> AC.char '(' + [h1, h2, h3, h4, p1, p2] <- AC.many1 AC.digit `AC.sepBy` AC.char ',' + let + host = intercalate "." [h1, h2, h3, h4] + highBits = read p1 + lowBits = read p2 + portNum = (highBits `Bits.shift` 8) + lowBits + return (host, portNum) parse257 :: AC.Parser String parse257 = do - _ <- AC.char '"' - C.unpack <$> AC.takeTill (== '"') + _ <- AC.char '"' + C.unpack <$> AC.takeTill (== '"') -- Control commands login :: MIO.MonadIO m => Handle -> String -> String -> m FTPResponse login h user pass = do - resp <- last <$> sendAll h [User user, Pass pass] - ensureSuccess resp + resp <- last <$> sendAll h [User user, Pass pass] + ensureSuccess resp pasv :: MIO.MonadIO m => Handle -> m (String, Int) pasv h = do - resp <- sendCommandS h Pasv - ensureCode resp 227 - parseResponse resp parse227 + resp <- sendCommandS h Pasv + ensureCode resp 227 + parseResponse resp parse227 port :: MIO.MonadIO m => Handle -> S.HostAddress -> S.PortNumber -> m FTPResponse port h ha pn = sendCommandS h (Port ha pn) @@ -716,54 +787,56 @@ acct h pass = sendCommandS h (Acct pass) rename :: MIO.MonadIO m => Handle -> String -> String -> m FTPResponse rename h from to = do - res <- sendCommand h (Rnfr from) - case frStatus res of - Continue -> sendCommandS h (Rnto to) - _ -> return res + res <- sendCommand h (Rnfr from) + case frStatus res of + Continue -> sendCommandS h (Rnto to) + _ -> return res dele :: MIO.MonadIO m => Handle -> String -> m FTPResponse dele h file = sendCommandS h (Dele file) cwd :: MIO.MonadIO m => Handle -> String -> m FTPResponse cwd h dir = - sendCommandS h $ if dir == ".." - then Cdup - else Cwd dir + sendCommandS h $ + if dir == ".." + then Cdup + else Cwd dir size :: MIO.MonadIO m => Handle -> String -> m Int size h file = do - resp <- sendCommandS h (Size file) - ensureCode resp 213 - return $ case frMessage resp of - SingleLine message -> read . C.unpack $ message - MultiLine _ -> 0 + resp <- sendCommandS h (Size file) + ensureCode resp 213 + return $ case frMessage resp of + SingleLine message -> read . C.unpack $ message + MultiLine _ -> 0 mkd :: MIO.MonadIO m => Handle -> String -> m String mkd h dir = do - resp <- sendCommandS h (Mkd dir) - ensureCode resp 257 - parseResponse resp parse257 + resp <- sendCommandS h (Mkd dir) + ensureCode resp 257 + parseResponse resp parse257 rmd :: MIO.MonadIO m => Handle -> String -> m FTPResponse rmd h dir = sendCommandS h (Rmd dir) pwd :: MIO.MonadIO m => Handle -> m String pwd h = do - resp <- sendCommandS h Pwd - ensureCode resp 257 - parseResponse resp parse257 + resp <- sendCommandS h Pwd + ensureCode resp 257 + parseResponse resp parse257 quit :: MIO.MonadIO m => Handle -> m FTPResponse quit h = sendCommandS h Quit mlst :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> m MlsxResponse mlst h path = do - resp <- sendCommandS h (Mlst path) - case frMessage resp of - SingleLine message -> return $ parseMlsxLine message - MultiLine messages -> if length messages >= 2 - then return $ parseMlsxLine $ messages !! 1 - else MIO.liftIO $ Exception.throwIO $ BogusResponseFormatException resp + resp <- sendCommandS h (Mlst path) + case frMessage resp of + SingleLine message -> return $ parseMlsxLine message + MultiLine messages -> + if length messages >= 2 + then return $ parseMlsxLine $ messages !! 1 + else MIO.liftIO $ Exception.throwIO $ BogusResponseFormatException resp -- TLS commands @@ -785,18 +858,18 @@ sendType :: MIO.MonadIO m => RTypeCode -> ByteString -> Handle -> m () sendType TA dat h = mapM_ (sendCommandLine h) $ C.split '\n' dat sendType TI dat h = MIO.liftIO $ send h dat -withDataCommandSecurity - :: (MIO.MonadIO m, MonadMask m) - => Handle - -> PortActivity - -> RTypeCode - -> FTPCommand - -> (Handle -> m a) - -> m a +withDataCommandSecurity :: + (MIO.MonadIO m, MonadMask m) => + Handle -> + PortActivity -> + RTypeCode -> + FTPCommand -> + (Handle -> m a) -> + m a withDataCommandSecurity h = - case security h of - Clear -> withDataCommand h - TLS -> withTLSDataCommand h + case security h of + Clear -> withDataCommand h + TLS -> withTLSDataCommand h nlst :: (MIO.MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString nlst h args = withDataCommandSecurity h Passive TA (Nlst args) getAllLineResp @@ -807,47 +880,56 @@ retr h path = withDataCommandSecurity h Passive TI (Retr path) recvAll list :: (MIO.MonadIO m, MonadMask m) => Handle -> [String] -> m ByteString list h args = withDataCommandSecurity h Passive TA (List args) getAllLineResp -stor - :: (MIO.MonadIO m, MonadMask m) - => Handle - -> String - -> B.ByteString - -> RTypeCode - -> m () +stor :: + (MIO.MonadIO m, MonadMask m) => + Handle -> + String -> + B.ByteString -> + RTypeCode -> + m () stor h loc dat rtype = - withDataCommandSecurity h Passive rtype (Stor loc) $ sendType rtype dat + withDataCommandSecurity h Passive rtype (Stor loc) $ sendType rtype dat -data MlsxResponse = MlsxResponse { - mrFilename :: String, - mrFacts :: Map String String -} deriving (Show) +data MlsxResponse = MlsxResponse + { mrFilename :: String + , mrFacts :: Map String String + } + deriving (Show) splitApart :: Char -> ByteString -> (ByteString, ByteString) splitApart on s = - let (x0, x1) = C.break (== on) s - in (x0, C.drop 1 x1) + let + (x0, x1) = C.break (== on) s + in + (x0, C.drop 1 x1) parseMlsxLine :: ByteString -> MlsxResponse parseMlsxLine line = - let (factLine, filename) = splitApart ' ' line - bFacts = splitApart '=' <$> C.split ';' factLine - facts - = Map.fromList - $ filter (not . null . fst) - $ Monad.join (***) C.unpack <$> bFacts - in MlsxResponse (C.unpack filename) facts + let + (factLine, filename) = splitApart ' ' line + bFacts = splitApart '=' <$> C.split ';' factLine + facts = + Map.fromList $ + filter (not . null . fst) $ + Monad.join (***) C.unpack <$> bFacts + in + MlsxResponse (C.unpack filename) facts getMlsxResponse :: (MIO.MonadIO m, MonadCatch m) => Handle -> m [MlsxResponse] getMlsxResponse h = - let collect :: (MIO.MonadIO n, MonadCatch n) => [MlsxResponse] -> n [MlsxResponse] - collect ret = ( do - line <- MIO.liftIO $ getLineResp h - collect $ - if C.null line - then ret - else parseMlsxLine line : ret - ) `M.catchIOError` (\_ -> return ret) - in collect [] + let + collect :: (MIO.MonadIO n, MonadCatch n) => [MlsxResponse] -> n [MlsxResponse] + collect ret = + ( do + line <- MIO.liftIO $ getLineResp h + collect $ + if C.null line + then ret + else parseMlsxLine line : ret + ) + `M.catchIOError` (\_ -> return ret) + in + collect [] mlsd :: (MIO.MonadIO m, MonadMask m) => Handle -> String -> m [MlsxResponse] mlsd h path = withDataCommandSecurity h Passive TA (Mlsd path) getMlsxResponse diff --git a/ftp-client/test/test.hs b/ftp-client/test/test.hs index eeb72c6..417c8b3 100644 --- a/ftp-client/test/test.hs +++ b/ftp-client/test/test.hs @@ -1,167 +1,222 @@ module Main (main) where +import Control.Concurrent.MVar import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as C -import Test.Hspec import Network.FTP.Client hiding (Success) import qualified Network.FTP.Client as F -import Control.Concurrent.MVar import System.IO.Error (eofErrorType, mkIOError) +import Test.Hspec data TestHandleMVars = TestHandleMVars - { thmSend :: MVar [ByteString] - , _thmSendLine :: MVar [ByteString] - , _thmRecv :: MVar [Int] - } + { thmSend :: MVar [ByteString] + , _thmSendLine :: MVar [ByteString] + , _thmRecv :: MVar [Int] + } data TestHandle = TestHandle TestHandleMVars Handle -testHandle - :: [ByteString] - -> [ByteString] - -> Security - -> IO TestHandle +testHandle :: + [ByteString] -> + [ByteString] -> + Security -> + IO TestHandle testHandle recvResps recvLineResps sec = do - sendMVar <- newMVar [] - sendLineMVar <- newMVar [] - recvMVar <- newMVar [] - recvCount <- newMVar 0 - recvLineCount <- newMVar 0 - let testHandleMVars = TestHandleMVars - sendMVar sendLineMVar recvMVar - handle = Handle - { send = \s -> - modifyMVar_ sendMVar - (\ss -> return $ ss <> [s]) - , sendLine = \s -> - modifyMVar_ sendLineMVar - (\ss -> return $ ss <> [s]) - , recv = \i -> do - modifyMVar_ recvMVar - (\is -> return $ is <> [i]) - nextScripted "recv" recvResps recvCount - , recvLine = nextScripted "recvLine" recvLineResps recvLineCount - , security = sec - } - return $ TestHandle testHandleMVars handle + sendMVar <- newMVar [] + sendLineMVar <- newMVar [] + recvMVar <- newMVar [] + recvCount <- newMVar 0 + recvLineCount <- newMVar 0 + let + testHandleMVars = + TestHandleMVars + sendMVar + sendLineMVar + recvMVar + handle = + Handle + { send = \s -> + modifyMVar_ + sendMVar + (\ss -> return $ ss <> [s]) + , sendLine = \s -> + modifyMVar_ + sendLineMVar + (\ss -> return $ ss <> [s]) + , recv = \i -> do + modifyMVar_ + recvMVar + (\is -> return $ is <> [i]) + nextScripted "recv" recvResps recvCount + , recvLine = nextScripted "recvLine" recvLineResps recvLineCount + , security = sec + } + return $ TestHandle testHandleMVars handle --- | Hand back the next scripted response, or signal end of input the way a --- real handle does once the peer has hung up. +{- | Hand back the next scripted response, or signal end of input the way a +real handle does once the peer has hung up. +-} nextScripted :: String -> [ByteString] -> MVar Int -> IO ByteString nextScripted what scripted countMVar = do - i <- modifyMVar countMVar (\count -> return (count + 1, count)) - case drop i scripted of - (x : _) -> return x - [] -> ioError $ mkIOError eofErrorType what Nothing Nothing + i <- modifyMVar countMVar (\count -> return (count + 1, count)) + case drop i scripted of + (x : _) -> return x + [] -> ioError $ mkIOError eofErrorType what Nothing Nothing main :: IO () main = hspec $ do - describe "Network.FTP.Client.sendCommand" $ do - it "sends USER for User" $ do - let expected = FTPResponse - F.Success 200 - (SingleLine $ C.pack "Ok") - (TestHandle mvars h) <- testHandle [] [C.pack "200 Ok"] Clear - sendCommand h (User "megan") `shouldReturn` expected - takeMVar (thmSend mvars) `shouldReturn` [C.pack "USER megan\r\n"] - it "sends USER for User and receives a multiline response" $ do - let expected = FTPResponse - F.Success 200 - (MultiLine [C.pack "line1", C.pack "line2", C.pack "200 line3"]) - (TestHandle mvars h) <- testHandle [] - [ C.pack "200-line1\r\n" - , C.pack "line2\r\n" - , C.pack "200 line3\r\n" - ] Clear - sendCommand h (User "megan") `shouldReturn` expected - takeMVar (thmSend mvars) `shouldReturn` [C.pack "USER megan\r\n"] - describe "Network.FTP.Client.getResponse" $ do - it "rejects an empty response line" $ do - (TestHandle _ h) <- testHandle [] [C.pack ""] Clear - getResponse h `shouldThrow` isBadProtocolResponse - it "rejects a response line with a non numeric code" $ do - (TestHandle _ h) <- testHandle [] [C.pack "abc def"] Clear - getResponse h `shouldThrow` isBadProtocolResponse - it "rejects a response line with a truncated code" $ do - (TestHandle _ h) <- testHandle [] [C.pack "20 Ok"] Clear - getResponse h `shouldThrow` isBadProtocolResponse - it "accepts a bare code with no message" $ do - let expected = FTPResponse - F.Success 200 - (SingleLine $ C.pack "") - (TestHandle _ h) <- testHandle [] [C.pack "200"] Clear - getResponse h `shouldReturn` expected - it "keeps a blank line inside a multiline response" $ do - -- RFC 959 lets the intermediate lines carry arbitrary text, so a - -- blank line is reply content and must not end the response. Ending - -- early would leave the real terminator unread and every later - -- command would pick up the wrong reply. - let expected = FTPResponse - F.Success 220 - (MultiLine - [ C.pack "First Line" - , C.pack "" - , C.pack "220 Third Line" - ]) - (TestHandle _ h) <- testHandle [] - [ C.pack "220-First Line\r\n" - , C.pack "\r\n" - , C.pack "220 Third Line\r\n" - ] Clear - getResponse h `shouldReturn` expected - it "keeps reading continuation lines that repeat the code" $ do - let expected = FTPResponse - F.Success 220 - (MultiLine - [ C.pack "First Line" - , C.pack "220-Second Line" - , C.pack "220 Third Line" - ]) - (TestHandle _ h) <- testHandle [] - [ C.pack "220-First Line\r\n" - , C.pack "220-Second Line\r\n" - , C.pack "220 Third Line\r\n" - ] Clear - getResponse h `shouldReturn` expected - it "ends a multiline response on a bare code" $ do - let expected = FTPResponse - F.Success 220 - (MultiLine [C.pack "First Line", C.pack "220"]) - (TestHandle _ h) <- testHandle [] - [ C.pack "220-First Line\r\n" - , C.pack "220\r\n" - ] Clear - getResponse h `shouldReturn` expected - it "does not end a multiline response on a different code" $ do - let expected = FTPResponse - F.Success 220 - (MultiLine - [ C.pack "First Line" - , C.pack "331 Not the terminator" - , C.pack "220 Done" - ]) - (TestHandle _ h) <- testHandle [] - [ C.pack "220-First Line\r\n" - , C.pack "331 Not the terminator\r\n" - , C.pack "220 Done\r\n" - ] Clear - getResponse h `shouldReturn` expected - it "stops when the server hangs up during a multiline response" $ do - let expected = FTPResponse - F.Success 220 - (MultiLine [C.pack "First Line"]) - (TestHandle _ h) <- testHandle [] - [ C.pack "220-First Line\r\n" - ] Clear - getResponse h `shouldReturn` expected - describe "Network.FTP.Client.recvAll" $ - it "doesn't hang on empty response" $ do - let expected = C.pack "" - (TestHandle _ h) <- testHandle [C.pack ""] [] Clear - recvAll h `shouldReturn` expected + describe "Network.FTP.Client.sendCommand" $ do + it "sends USER for User" $ do + let + expected = + FTPResponse + F.Success + 200 + (SingleLine $ C.pack "Ok") + (TestHandle mvars h) <- testHandle [] [C.pack "200 Ok"] Clear + sendCommand h (User "megan") `shouldReturn` expected + takeMVar (thmSend mvars) `shouldReturn` [C.pack "USER megan\r\n"] + it "sends USER for User and receives a multiline response" $ do + let + expected = + FTPResponse + F.Success + 200 + (MultiLine [C.pack "line1", C.pack "line2", C.pack "200 line3"]) + (TestHandle mvars h) <- + testHandle + [] + [ C.pack "200-line1\r\n" + , C.pack "line2\r\n" + , C.pack "200 line3\r\n" + ] + Clear + sendCommand h (User "megan") `shouldReturn` expected + takeMVar (thmSend mvars) `shouldReturn` [C.pack "USER megan\r\n"] + describe "Network.FTP.Client.getResponse" $ do + it "rejects an empty response line" $ do + (TestHandle _ h) <- testHandle [] [C.pack ""] Clear + getResponse h `shouldThrow` isBadProtocolResponse + it "rejects a response line with a non numeric code" $ do + (TestHandle _ h) <- testHandle [] [C.pack "abc def"] Clear + getResponse h `shouldThrow` isBadProtocolResponse + it "rejects a response line with a truncated code" $ do + (TestHandle _ h) <- testHandle [] [C.pack "20 Ok"] Clear + getResponse h `shouldThrow` isBadProtocolResponse + it "accepts a bare code with no message" $ do + let + expected = + FTPResponse + F.Success + 200 + (SingleLine $ C.pack "") + (TestHandle _ h) <- testHandle [] [C.pack "200"] Clear + getResponse h `shouldReturn` expected + it "keeps a blank line inside a multiline response" $ do + -- RFC 959 lets the intermediate lines carry arbitrary text, so a + -- blank line is reply content and must not end the response. Ending + -- early would leave the real terminator unread and every later + -- command would pick up the wrong reply. + let + expected = + FTPResponse + F.Success + 220 + ( MultiLine + [ C.pack "First Line" + , C.pack "" + , C.pack "220 Third Line" + ] + ) + (TestHandle _ h) <- + testHandle + [] + [ C.pack "220-First Line\r\n" + , C.pack "\r\n" + , C.pack "220 Third Line\r\n" + ] + Clear + getResponse h `shouldReturn` expected + it "keeps reading continuation lines that repeat the code" $ do + let + expected = + FTPResponse + F.Success + 220 + ( MultiLine + [ C.pack "First Line" + , C.pack "220-Second Line" + , C.pack "220 Third Line" + ] + ) + (TestHandle _ h) <- + testHandle + [] + [ C.pack "220-First Line\r\n" + , C.pack "220-Second Line\r\n" + , C.pack "220 Third Line\r\n" + ] + Clear + getResponse h `shouldReturn` expected + it "ends a multiline response on a bare code" $ do + let + expected = + FTPResponse + F.Success + 220 + (MultiLine [C.pack "First Line", C.pack "220"]) + (TestHandle _ h) <- + testHandle + [] + [ C.pack "220-First Line\r\n" + , C.pack "220\r\n" + ] + Clear + getResponse h `shouldReturn` expected + it "does not end a multiline response on a different code" $ do + let + expected = + FTPResponse + F.Success + 220 + ( MultiLine + [ C.pack "First Line" + , C.pack "331 Not the terminator" + , C.pack "220 Done" + ] + ) + (TestHandle _ h) <- + testHandle + [] + [ C.pack "220-First Line\r\n" + , C.pack "331 Not the terminator\r\n" + , C.pack "220 Done\r\n" + ] + Clear + getResponse h `shouldReturn` expected + it "stops when the server hangs up during a multiline response" $ do + let + expected = + FTPResponse + F.Success + 220 + (MultiLine [C.pack "First Line"]) + (TestHandle _ h) <- + testHandle + [] + [ C.pack "220-First Line\r\n" + ] + Clear + getResponse h `shouldReturn` expected + describe "Network.FTP.Client.recvAll" $ + it "doesn't hang on empty response" $ do + let + expected = C.pack "" + (TestHandle _ h) <- testHandle [C.pack ""] [] Clear + recvAll h `shouldReturn` expected isBadProtocolResponse :: FTPException -> Bool isBadProtocolResponse e = - case e of - BadProtocolResponseException _ -> True - _ -> False + case e of + BadProtocolResponseException _ -> True + _ -> False From 7112eafcc1b20f99beb059eef7cad9bd33e29452 Mon Sep 17 00:00:00 2001 From: Paul Burns Date: Tue, 18 Aug 2026 20:18:40 -0400 Subject: [PATCH 4/7] Ignore the format commit in blame, version 0.5.3.1 and 0.5.0.8 The blame ignore file follows purescript-erumu, which does the same for its purs-tidy sweep. Configure git to use it with git config blame.ignoreRevsFile .git-blame-ignore-revs Both packages get a patch bump: the import qualification and the formatting change no code that a consumer can observe, and henforcer sits behind the manual ci flag, which defaults off. --- .git-blame-ignore-revs | 2 ++ ftp-client-conduit/CHANGELOG.md | 9 +++++++++ ftp-client-conduit/ftp-client-conduit.cabal | 2 +- ftp-client-conduit/package.yaml | 2 +- ftp-client/CHANGELOG.md | 6 ++++++ ftp-client/ftp-client.cabal | 2 +- ftp-client/package.yaml | 2 +- 7 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..d082d55 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Haskell mass formatted with fourmolu +94d65f426b4582efe7d39c74ba7fe9d62ac1147a diff --git a/ftp-client-conduit/CHANGELOG.md b/ftp-client-conduit/CHANGELOG.md index 073de77..2ee7b8e 100644 --- a/ftp-client-conduit/CHANGELOG.md +++ b/ftp-client-conduit/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog for ftp-client-conduit +## 0.5.0.8 + +* Enable the `henforcer` plugin and `fourmolu` under the `ci` flag. Imports are + now qualified per the house style and the source is fourmolu formatted; + neither changes the API. + +* Correct the Haddock module header, which named `Network.FTP.Client` rather + than `Network.FTP.Client.Conduit`. + ## 0.5.0.7 * Add the missing upper bounds on `bytestring`, `conduit` and `exceptions`. diff --git a/ftp-client-conduit/ftp-client-conduit.cabal b/ftp-client-conduit/ftp-client-conduit.cabal index 5112115..cd15f83 100644 --- a/ftp-client-conduit/ftp-client-conduit.cabal +++ b/ftp-client-conduit/ftp-client-conduit.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: ftp-client-conduit -version: 0.5.0.7 +version: 0.5.0.8 synopsis: Transfer file with FTP and FTPS with Conduit description: ftp-client is a library for communicating with an FTP server. It works over both a clear channel or TLS. ftp-client-conduit uses conduit to stream files and data in constant space. category: Web diff --git a/ftp-client-conduit/package.yaml b/ftp-client-conduit/package.yaml index eb4db75..20ecde2 100644 --- a/ftp-client-conduit/package.yaml +++ b/ftp-client-conduit/package.yaml @@ -1,5 +1,5 @@ name: ftp-client-conduit -version: 0.5.0.7 +version: 0.5.0.8 synopsis: Transfer file with FTP and FTPS with Conduit description: ftp-client is a library for communicating with an FTP server. It works over both a clear channel or TLS. ftp-client-conduit uses conduit to stream files and data in constant space. homepage: https://github.com/flipstone/ftp-client diff --git a/ftp-client/CHANGELOG.md b/ftp-client/CHANGELOG.md index 588269c..139d48f 100644 --- a/ftp-client/CHANGELOG.md +++ b/ftp-client/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog for ftp-client +## 0.5.3.1 + +* Enable the `henforcer` plugin and `fourmolu` under the `ci` flag. Imports are + now qualified per the house style and the source is fourmolu formatted; + neither changes the API. + ## 0.5.3.0 * Export `acct`, `pbsz`, `prot`, `ccc` and `auth`. These command wrappers were diff --git a/ftp-client/ftp-client.cabal b/ftp-client/ftp-client.cabal index 465f926..065169b 100644 --- a/ftp-client/ftp-client.cabal +++ b/ftp-client/ftp-client.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: ftp-client -version: 0.5.3.0 +version: 0.5.3.1 synopsis: Transfer files with FTP and FTPS description: ftp-client is a library for communicating with an FTP server. It works over both a clear channel or TLS. category: Web diff --git a/ftp-client/package.yaml b/ftp-client/package.yaml index cb802a4..cf18b69 100644 --- a/ftp-client/package.yaml +++ b/ftp-client/package.yaml @@ -1,5 +1,5 @@ name: ftp-client -version: 0.5.3.0 +version: 0.5.3.1 synopsis: Transfer files with FTP and FTPS description: ftp-client is a library for communicating with an FTP server. It works over both a clear channel or TLS. homepage: https://github.com/flipstone/ftp-client From 61e4897ed0d7f80605a121c944f7b7e5451f215c Mon Sep 17 00:00:00 2001 From: Paul Burns Date: Tue, 18 Aug 2026 20:26:07 -0400 Subject: [PATCH 5/7] Qualify the Conduit import too henforcer exempts any import carrying a hiding clause, so `import Conduit hiding (MonadResource)` was never reported and got left unqualified while everything around it was qualified. That satisfied the plugin but not the convention. Qualifying it removes the workaround rather than adding to it: with Conduit qualified, MonadResource cannot arrive unqualified from it, so the explicit resourcet import is the only source and the hiding clause is unnecessary. MonadIO and liftIO now come from Control.Monad.IO.Class as MIO, matching Network.FTP.Client, instead of riding in on Conduit's re-export. --- .../src/Network/FTP/Client/Conduit.hs | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs b/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs index 9f34c99..0365522 100644 --- a/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs +++ b/ftp-client-conduit/src/Network/FTP/Client/Conduit.hs @@ -17,10 +17,13 @@ module Network.FTP.Client.Conduit , mlsd ) where --- MonadResource appears in this module's exported signatures, so it is taken --- from resourcet directly rather than through Conduit's re-export. That keeps --- the resourcet dependency honest instead of implicit. -import Conduit hiding (MonadResource) +-- MonadResource appears in this module's exported signatures. Conduit +-- re-exports it, so importing it from resourcet by name is what keeps the +-- resourcet dependency genuinely used rather than implicit. + +import Conduit ((.|)) +import qualified Conduit +import qualified Control.Monad.IO.Class as MIO import Control.Monad.Trans.Resource (MonadResource) import Data.ByteString.Lazy.Internal (defaultChunkSize) import Network.FTP.Client @@ -47,49 +50,49 @@ import qualified Network.FTP.Client as FTP debugging :: Bool debugging = False -debugPrint :: (Show a, MonadIO m) => a -> m () +debugPrint :: (Show a, MIO.MonadIO m) => a -> m () debugPrint s = if debugging - then liftIO $ print s + then MIO.liftIO $ print s else return () -debugResponse :: (Show a, MonadIO m) => a -> m () +debugResponse :: (Show a, MIO.MonadIO m) => a -> m () debugResponse s = debugPrint $ "Recieved: " <> (show s) getAllLineRespC :: forall i m. - MonadIO m => + MIO.MonadIO m => FTP.Handle -> - ConduitT i ByteString m () + Conduit.ConduitT i ByteString m () getAllLineRespC h = let - loop :: ConduitT i ByteString m () + loop :: Conduit.ConduitT i ByteString m () loop = do line <- - liftIO $ + MIO.liftIO $ FTP.getLineResp h `M.catchIOError` const (return "") if B.null line then return () else do - yield line + Conduit.yield line loop in loop sendAllLineC :: forall o m. - MonadIO m => + MIO.MonadIO m => FTP.Handle -> - ConduitT ByteString o m () + Conduit.ConduitT ByteString o m () sendAllLineC h = let - loop :: ConduitT ByteString o m () + loop :: Conduit.ConduitT ByteString o m () loop = do - mx <- await + mx <- Conduit.await case mx of Nothing -> return () Just x -> do - liftIO $ FTP.sendLine h x + MIO.liftIO $ FTP.sendLine h x loop in loop @@ -100,8 +103,8 @@ sourceDataCommandSecurity :: PortActivity -> RTypeCode -> FTPCommand -> - (FTP.Handle -> ConduitM i o m r) -> - ConduitM i o m r + (FTP.Handle -> Conduit.ConduitM i o m r) -> + Conduit.ConduitM i o m r sourceDataCommandSecurity h = case FTP.security h of Clear -> sourceDataCommand h @@ -113,14 +116,14 @@ sourceDataCommand :: PortActivity -> RTypeCode -> FTPCommand -> - (FTP.Handle -> ConduitM i o m r) -> - ConduitM i o m r + (FTP.Handle -> Conduit.ConduitM i o m r) -> + Conduit.ConduitM i o m r sourceDataCommand ch pa code cmd f = do _ <- sendCommandS ch $ RType code x <- - bracketP + Conduit.bracketP (createSendDataCommand ch pa cmd) - (liftIO . SIO.hClose) + (MIO.liftIO . SIO.hClose) (f . sIOHandleImpl) resp <- getResponse ch debugResponse resp @@ -132,14 +135,14 @@ sourceTLSDataCommand :: PortActivity -> RTypeCode -> FTPCommand -> - (FTP.Handle -> ConduitM i o m r) -> - ConduitM i o m r + (FTP.Handle -> Conduit.ConduitM i o m r) -> + Conduit.ConduitM i o m r sourceTLSDataCommand ch pa code cmd f = do _ <- sendCommandS ch $ RType code x <- - bracketP + Conduit.bracketP (createTLSSendDataCommand ch pa cmd) - (liftIO . Connection.connectionClose) + (MIO.liftIO . Connection.connectionClose) (f . tlsHandleImpl) resp <- getResponse ch debugResponse resp @@ -147,39 +150,39 @@ sourceTLSDataCommand ch pa code cmd f = do sourceFTPHandle :: forall i m. - MonadIO m => + MIO.MonadIO m => FTP.Handle -> - ConduitT i ByteString m () + Conduit.ConduitT i ByteString m () sourceFTPHandle h = let - loop :: ConduitT i ByteString m () + loop :: Conduit.ConduitT i ByteString m () loop = do bs <- - liftIO $ + MIO.liftIO $ FTP.recv h defaultChunkSize `M.catchIOError` const (return "") if B.null bs then return () else do - yield bs + Conduit.yield bs loop in loop sinkFTPHandle :: forall o m. - MonadIO m => + MIO.MonadIO m => FTP.Handle -> - ConduitT ByteString o m () + Conduit.ConduitT ByteString o m () sinkFTPHandle h = let - loop :: ConduitT ByteString o m () + loop :: Conduit.ConduitT ByteString o m () loop = do - mbs <- await + mbs <- Conduit.await case mbs of Nothing -> return () Just bs -> do - liftIO $ FTP.send h bs + MIO.liftIO $ FTP.send h bs loop in loop @@ -188,19 +191,19 @@ sendType :: MonadResource m => RTypeCode -> FTP.Handle -> - ConduitT ByteString o m () + Conduit.ConduitT ByteString o m () sendType TA h = sendAllLineC h sendType TI h = sinkFTPHandle h -nlst :: MonadResource m => FTP.Handle -> [String] -> ConduitT i ByteString m () +nlst :: MonadResource m => FTP.Handle -> [String] -> Conduit.ConduitT i ByteString m () nlst ch args = sourceDataCommandSecurity ch Passive TA (Nlst args) getAllLineRespC -retr :: MonadResource m => FTP.Handle -> String -> ConduitT i ByteString m () +retr :: MonadResource m => FTP.Handle -> String -> Conduit.ConduitT i ByteString m () retr ch path = sourceDataCommandSecurity ch Passive TI (Retr path) sourceFTPHandle -list :: MonadResource m => FTP.Handle -> [String] -> ConduitT i ByteString m () +list :: MonadResource m => FTP.Handle -> [String] -> Conduit.ConduitT i ByteString m () list ch args = sourceDataCommandSecurity ch Passive TA (List args) getAllLineRespC @@ -209,7 +212,7 @@ stor :: FTP.Handle -> String -> RTypeCode -> - ConduitT ByteString o m () + Conduit.ConduitT ByteString o m () stor ch loc rtype = sourceDataCommandSecurity ch Passive rtype (Stor loc) $ sendType rtype @@ -217,7 +220,7 @@ mlsd :: MonadResource m => FTP.Handle -> String -> - ConduitT i FTP.MlsxResponse m () + Conduit.ConduitT i FTP.MlsxResponse m () mlsd ch dir = sourceDataCommandSecurity ch Passive TA (Mlsd dir) getAllLineRespC - .| mapC parseMlsxLine + .| Conduit.mapC parseMlsxLine From 199f53170a1dab0762fdc9081da6c1013e72dc80 Mon Sep 17 00:00:00 2001 From: Paul Burns Date: Wed, 19 Aug 2026 09:37:27 -0400 Subject: [PATCH 6/7] Point the blame ignore file at the current format commit --- .git-blame-ignore-revs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index d082d55..f8892fe 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,2 @@ # Haskell mass formatted with fourmolu -94d65f426b4582efe7d39c74ba7fe9d62ac1147a +5eaaa756c699eee71b866105cc88dc688f457dad From 73f1af7a40ded06c5c64e2ee61dea02221415451 Mon Sep 17 00:00:00 2001 From: Paul Burns Date: Sat, 22 Aug 2026 17:22:25 -0400 Subject: [PATCH 7/7] Fix up the workflow guard and blame ignore after restacking The formatting-check job was added after the fork-only pull_request guard, so it lacked the condition and would have run twice for every same-repo pull request -- once from push and once from pull_request. The blame ignore file also still named the pre-rebase format commit. That SHA changes every time this branch is restacked, so it is worth re-checking on each rebase until this lands on main. --- .git-blame-ignore-revs | 2 +- .github/workflows/main.yaml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f8892fe..336d822 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,2 @@ # Haskell mass formatted with fourmolu -5eaaa756c699eee71b866105cc88dc688f457dad +dbe4bbfa76ffcb567ff58a77a639814d567cdc5f diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d3b72e8..f84b168 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -66,6 +66,9 @@ jobs: formatting-check: name: Formatting Check + if: >- + github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name != github.repository runs-on: ubuntu-24.04 permissions: contents: read