diff --git a/hie.yaml b/hie.yaml index a6993d8..c32f52b 100644 --- a/hie.yaml +++ b/hie.yaml @@ -6,6 +6,8 @@ cradle: cabal: - path: "./src" component: "lib:streamly-coreutils" + - path: "./test" + component: "test:coreutils-test" dependencies: - streamly-coreutils.cabal - hie.yaml diff --git a/src/Streamly/Coreutils/Chmod.hs b/src/Streamly/Coreutils/Chmod.hs new file mode 100644 index 0000000..4c82557 --- /dev/null +++ b/src/Streamly/Coreutils/Chmod.hs @@ -0,0 +1,75 @@ +{-# LANGUAGE QuasiQuotes #-} +-- | +-- Module : Streamly.Coreutils.Chmod +-- Copyright : (c) 2022 Composewell Technologies +-- License : BSD-3-Clause +-- Maintainer : streamly@composewell.com +-- Stability : experimental +-- Portability : GHC +-- +-- change file mode bits. + +module Streamly.Coreutils.Chmod + ( chmod + , perm + ) +where + +import Data.Bits ((.|.), Bits ((.&.), complement)) +import Streamly.Coreutils.StringQ +import qualified System.Posix as Posix + +modifyBit :: Bool -> Posix.FileMode -> Posix.FileMode -> Posix.FileMode +modifyBit False b m = m .&. complement b +modifyBit True b m = m .|. b + +chmodWith :: UserType -> Permissions -> FilePath -> IO () +chmodWith utype (Permissions r w e) path = do + case utype of + Owner -> setOwnerPermissions + Group -> setGroupPermissions + Others -> setOthersPermissions + All -> setAllPermissions + + where + + setOwnerPermissions = do + stat <- Posix.getFileStatus path + Posix.setFileMode + path + ( modifyBit e Posix.ownerExecuteMode + . modifyBit w Posix.ownerWriteMode + . modifyBit r Posix.ownerReadMode + . Posix.fileMode $ stat + ) + + setGroupPermissions = do + stat <- Posix.getFileStatus path + Posix.setFileMode + path + ( modifyBit e Posix.groupExecuteMode + . modifyBit w Posix.groupWriteMode + . modifyBit r Posix.groupReadMode + . Posix.fileMode $ stat + ) + + setOthersPermissions = do + stat <- Posix.getFileStatus path + Posix.setFileMode + path + ( modifyBit e Posix.otherExecuteMode + . modifyBit w Posix.otherWriteMode + . modifyBit r Posix.otherReadMode + . Posix.fileMode $ stat + ) + + setAllPermissions = do + setOwnerPermissions + setGroupPermissions + setOthersPermissions + +-- | Supports only override permissions bits +-- >> chmod [perm|a=rwx|] "a.txt" +-- +chmod :: UserTypePerm -> FilePath -> IO () +chmod pat = chmodWith (utype pat) (permssions pat) diff --git a/src/Streamly/Coreutils/StringQ.hs b/src/Streamly/Coreutils/StringQ.hs new file mode 100644 index 0000000..e1b0ace --- /dev/null +++ b/src/Streamly/Coreutils/StringQ.hs @@ -0,0 +1,133 @@ +{-# LANGUAGE TemplateHaskell #-} +-- | +-- Module : Streamly.Coreutils.StringQ +-- Copyright : (c) 2022 Composewell Technologies +-- License : BSD-3-Clause +-- Maintainer : streamly@composewell.com +-- Stability : experimental +-- Portability : GHC +-- +-- change file mode bits. + +module Streamly.Coreutils.StringQ + ( + perm + , UserType(..) + , Permissions(..) + , UserTypePerm(..) + ) +where + +import Control.Applicative (Alternative(..)) +import Control.Monad.Catch (MonadCatch) +import Control.Monad.IO.Class (liftIO, MonadIO) +import Data.Char (chr) +import Data.Data (Data) +import Data.Default.Class (Default(..)) +import Language.Haskell.TH (Exp, Q, Pat) +import Language.Haskell.TH.Quote (QuasiQuoter(..), dataToExpQ, dataToPatQ) +import Streamly.Internal.Data.Parser (Parser) + +import qualified Streamly.Internal.Data.Fold as Fold +import qualified Streamly.Internal.Data.Parser as Parser +import qualified Streamly.Internal.Data.Stream.IsStream as Stream +import qualified Streamly.Internal.Unicode.Char.Parser as Parser + +strParser :: MonadCatch m => Parser m Char String +strParser = + let ut = Parser.char 'u' + <|> Parser.char 'g' + <|> Parser.char 'o' + <|> Parser.char 'a' + op = Parser.char '=' -- supports only override permissions bits + p1 = Parser.char (chr 0) + <|> Parser.char 'r' + <|> Parser.char 'w' + <|> Parser.char 'x' + r = ut *> op + r1 = ut *> op *> p1 + r2 = ut *> op *> p1 *> p1 + r3 = ut *> op *> p1 *> p1 *> p1 + s = r <|> r1 <|> r2 <|> r3 + in Parser.some s Fold.toList + +expandVars :: String -> IO () +expandVars ln = + case Stream.parse strParser (Stream.fromList ln) of + Left _ -> fail "Parsing of perm quoted string failed." + Right _ -> return () + +data Permissions = Permissions + { readable :: Bool + , writable :: Bool + , executable :: Bool + } deriving (Eq, Ord, Read, Show, Data) + +data UserType = + Owner + | Group + | Others + | All + deriving (Eq, Ord, Read, Show, Data) + +data UserTypePerm = + UserTypePerm + { utype :: UserType + , permssions :: Permissions + } deriving (Eq, Ord, Read, Show, Data) + +instance Default Permissions where + def = Permissions + { readable = False + , writable = False + , executable = False + } + +parseExpr :: MonadIO m => String -> m UserTypePerm +parseExpr s = do + liftIO $ expandVars s + let ut = head s + bits = tail $ tail s + return $ + case ut of + 'u' -> UserTypePerm Owner $ setPermission bits + 'g' -> UserTypePerm Group $ setPermission bits + 'o' -> UserTypePerm Others $ setPermission bits + 'a' -> UserTypePerm All $ setPermission bits + _ -> error "Invalid permissions" + + where + + setPermission bits = + case bits of + "rwx" -> Permissions True True True + "rw" -> Permissions True True False + "r" -> Permissions True False False + "w" -> Permissions False True False + "x" -> Permissions False False True + "rx" -> Permissions True False True + "wx" -> Permissions False True True + _ -> def + +quoteExprExp :: String -> Q Exp +quoteExprExp s = do + expr <- parseExpr s + dataToExpQ (const Nothing) expr + +quoteExprPat :: String -> Q Pat +quoteExprPat s = do + expr <- parseExpr s + dataToPatQ (const Nothing) expr + +perm :: QuasiQuoter +perm = + QuasiQuoter + { quoteExp = quoteExprExp + , quotePat = quoteExprPat + , quoteType = notSupported + , quoteDec = notSupported + } + + where + + notSupported = error "perm: Not supported." diff --git a/streamly-coreutils.cabal b/streamly-coreutils.cabal index 2df6343..77b28e7 100644 --- a/streamly-coreutils.cabal +++ b/streamly-coreutils.cabal @@ -105,10 +105,15 @@ library , unix >= 2.7.0 && < 2.8 , directory >= 1.2.2 && < 1.4 , filepath >= 1.4 && < 1.5 + , data-default-class >= 0.1 && < 0.2 + , template-haskell >= 2.10.0 && < 2.19.0 + hs-source-dirs: src exposed-modules: Streamly.Coreutils + , Streamly.Coreutils.Chmod , Streamly.Coreutils.Common + , Streamly.Coreutils.StringQ , Streamly.Coreutils.Cp , Streamly.Coreutils.Directory , Streamly.Coreutils.Dirname @@ -127,6 +132,7 @@ library , Streamly.Coreutils.Which , Streamly.Coreutils.Ln + default-language: Haskell2010 ------------------------------------------------------------------------------- @@ -144,6 +150,7 @@ benchmark coreutils-bench , base >= 4.8 && < 5 , gauge >= 0.2.4 && < 0.3 , random >= 1.0.0 && < 2 + default-language: Haskell2010 ------------------------------------------------------------------------------- @@ -154,9 +161,13 @@ test-suite coreutils-test import: compile-options type: exitcode-stdio-1.0 main-is: Main.hs + other-modules: Common hs-source-dirs: test build-depends: streamly-coreutils , streamly , base >= 4.8 && < 5 + , filepath >= 1.4.2 && < 1.4.3 + , directory >= 1.3.6 && < 1.3.7 + , temporary >= 1.3 && < 1.4 default-language: Haskell2010 diff --git a/test/Common.hs b/test/Common.hs new file mode 100644 index 0000000..13dc8a8 --- /dev/null +++ b/test/Common.hs @@ -0,0 +1,35 @@ +module Common + ( createParent + , createDirWithParent + , createDir + , createFileWithParent + , createFile + ) +where + +import Control.Monad (unless) +import System.Directory (createDirectory, createDirectoryIfMissing) +import System.FilePath ((), takeDirectory) +import System.IO ( IOMode (WriteMode), openFile, hClose) + +createParent :: FilePath -> FilePath -> IO () +createParent file parent = do + createDirectoryIfMissing True (parent takeDirectory file) + +createDirWithParent :: FilePath -> FilePath -> IO () +createDirWithParent dir parent = + unless (null dir) $ createDirectoryIfMissing True (parent dir) + +createDir :: FilePath -> FilePath -> IO () +createDir dir parent = + unless (null dir) $ createDirectory (parent dir) + +createFileWithParent :: FilePath -> FilePath -> IO () +createFileWithParent file parent = do + unless (null file) $ + createDirectoryIfMissing True (parent takeDirectory file) + openFile (parent file) WriteMode >>= hClose + +createFile :: FilePath -> FilePath -> IO () +createFile file parent = + openFile (parent file) WriteMode >>= hClose diff --git a/test/Main.hs b/test/Main.hs index 3aec86e..f78210a 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE QuasiQuotes #-} module Main (main) where @@ -5,10 +7,18 @@ where import qualified Streamly.Prelude as S import qualified Streamly.Internal.Data.Fold as FL +import Streamly.Coreutils.Common (Switch(..)) import Streamly.Coreutils.Uniq +import Streamly.Coreutils.Rm +import Streamly.Coreutils.Chmod +import System.FilePath (()) +import System.IO.Temp (withSystemTempDirectory) +import Control.Exception (try, SomeException) import Control.Monad.IO.Class (MonadIO) import Streamly.Prelude (IsStream) +import Common +import System.Exit (exitFailure) opt :: UniqOptions opt = defaultUniqOptions {skipFields = 1, skipChar = 1} @@ -49,8 +59,151 @@ gen c n = S.unfoldr step (0, True) -- * File parent dirs not having permissions -- * File owned by someone else +rmDir :: FilePath +rmDir = "rmDir" + +processResult :: Either SomeException s -> IO String +processResult res = return $ + case res of + Left _ -> "Failed" + Right _ -> "Passed" + +testRmDefault :: IO String +testRmDefault = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + file = "file.txt" + path = dir file + createFileWithParent file dir + try (rm id path) >>= processResult + +testRmDefaultFail :: IO String +testRmDefaultFail = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + file = "fileRO.txt" + path = dir file + createFileWithParent file dir + chmod [perm|u=r|] path + try (rm id path) >>= processResult + +testRmNonExist :: IO String +testRmNonExist = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + fileNE = "fileNE.txt" + pathNE = dir fileNE + try (rm id pathNE) >>= processResult + +-- make path read-only +-- chmod [perm|u=r|] "path" + +testRmROFile :: IO String +testRmROFile = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + file = "fileRO.txt" + path = dir file + createFileWithParent file dir + chmod [perm|u=r|] path + try (rm id path) >>= processResult + +testRmForceFile :: IO String +testRmForceFile = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + file = "fileRO.txt" + path = dir file + createFileWithParent file dir + chmod [perm|u=r|] path + try (rm (force Force) path) >>= processResult + +testRmForceFailRO :: IO String +testRmForceFailRO = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + file = "fileRW.txt" + path = dir file + createFileWithParent file dir + chmod [perm|u=r|] dir + try (rm (force Force) path) >>= processResult + +testRmForceFailNP :: IO String +testRmForceFailNP = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + file = "fileRW.txt" + path = dir file + createFileWithParent file dir + chmod [perm|a=|] dir + try (rm (force Force) path) >>= processResult + +testRmNuke :: IO String +testRmNuke = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + file = "fileRW.txt" + createFileWithParent file dir + chmod [perm|u=r|] dir + try (rm (force Nuke . recursive On) dir) >>= processResult + +testRmNukeNoPerm :: IO String +testRmNukeNoPerm = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + file = "fileRW.txt" + createFileWithParent file dir + chmod [perm|a=|] dir + try (rm (force Nuke . recursive On) dir) >>= processResult + +testRmNukeRecOff :: IO String +testRmNukeRecOff = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" + file = "fileRW.txt" + createFileWithParent file dir + chmod [perm|a=|] dir + try (rm (force Nuke . recursive Off) dir) >>= processResult + +testRmRecursive ::(Rm -> Rm) -> IO String +testRmRecursive f = + withSystemTempDirectory rmDir $ \fp -> do + let dir = fp "testDir" "testDir" + file = "fileRW.txt" + createFileWithParent file dir + try (rm f dir) >>= processResult + +testRmRecursiveOn :: IO String +testRmRecursiveOn = testRmRecursive (recursive On) + +testRmRecursiveOff :: IO String +testRmRecursiveOff = testRmRecursive (recursive Off) + +describe :: String -> String -> IO String -> IO () +describe tc expec m = do + res <- m + if res == expec + then print (tc ++ "->" ++ "PASS") + else print (tc ++ "->" ++ "FAILED") >> exitFailure + +testRm :: IO () +testRm = do + describe "default" "Passed" testRmDefault + describe "defaultFail" "Failed" testRmDefaultFail + describe "nonExistant" "Failed" testRmNonExist + describe "readOnly" "Failed" testRmROFile + describe "forcePass" "Passed" testRmForceFile + describe "forceFail ReadOnly" "Failed" testRmForceFailRO + describe "forceFail None Permission" "Failed" testRmForceFailNP + describe "recursiveOn" "Passed" testRmRecursiveOn + describe "recursiveOff" "Failed" testRmRecursiveOff + describe "nuke" "Passed" testRmNuke + describe "nuke None Permission" "Passed" testRmNukeNoPerm + describe "nuke Recursive Off" "Failed" testRmNukeRecOff + main :: IO () main = do + testRm let comp = compareUsingOptions opt S.drain $ S.mapM print $ splitOnNewLine $ S.take 100 $ gen 'a' 6 S.drain $