/
Interpreter.hs
382 lines (344 loc) · 13 KB
/
Interpreter.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}
-- | A bunch of 'Layout' description interpreters
module System.Directory.Layout.Interpreter
( pretty
, examples
, fromErrors
, fit
, FitError(..)
, FitContentsError(..)
, make
, remake
, MakeError(..)
#ifdef TEST
, (\/)(..)
#endif
) where
import Control.Applicative
import Control.Exception (Exception(..), SomeException(..), throwIO, try)
import Control.Monad
import Control.Monad.Free
import Data.Bifoldable (Bifoldable(..))
import Data.Bifunctor (Bifunctor(..))
import Data.Bitraversable (Bitraversable(..))
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Lazy as ByteStringLazy
import Data.Data (Data, Typeable)
import Data.Foldable (Foldable, sequenceA_, for_, toList)
import Data.Functor.Compose (Compose(..))
import Data.List.NonEmpty (NonEmpty(..))
import Data.Maybe (fromMaybe)
import Data.Semigroup (Semigroup(..))
import Data.Text (Text)
import qualified Data.Text.IO as Text
import Data.Traversable (Traversable)
import Data.Typeable (cast)
import GHC.Generics (Generic)
import Numeric (showOct)
import System.Directory (createDirectory, createDirectoryIfMissing, removeDirectoryRecursive)
import System.FilePath (combine)
import System.IO.Error (IOErrorType, ioeGetErrorType, ioeGetFileName, ioeGetLocation)
import qualified System.Posix as Posix
import Test.Hspec (Spec, context, it)
import Text.Printf (printf)
import System.Directory.Layout.Internal
-- | Pretty print the directory layout
pretty :: Layout a -> String
pretty = unlines . iter go . unL . fmap (const []) where
go f@(F _ _ _ other) = prettyF f : other
go f@(SL _ _ _ _ other) = prettyF f : other
go f@(D _ is _ other) = prettyF f : map indent is ++ other
go E = []
indent :: String -> String
indent s = "┆ " ++ s
prettyF :: F a -> String
prettyF (F name cs _ _) = printf "‘%s’, %s" name (prettyC cs)
prettyF (SL name s _ _ _) = printf "‘%s’, a link to ‘%s’" name s
prettyF (D name _ _ _) = '/' : name
prettyF E = ""
prettyC :: Maybe Contents -> String
prettyC (Just (Binary _)) = "raw bytes"
prettyC (Just (Text _)) = "text"
prettyC (Just (CopyOf p)) = printf "(copy of ‘%s’)" p
prettyC Nothing = "anything"
-- | Interpret the directory layout as a 'Spec'
examples :: FilePath -> Layout a -> Spec
examples p = go p . unL where
go root (Free f@(F _ _ _ m)) = do
examplesF root f
go root m
go root (Free f@(SL _ _ _ _ m)) = do
examplesF root f
go root m
go root (Free f@(D (combine root -> fullpath) is _ m)) = do
examplesF root f
context (printf "directory ‘%s’" fullpath) (go fullpath is)
go root m
go _ (Free E) = return ()
go _ (Pure _) = return ()
examplesF :: FilePath -> F a -> Spec
examplesF root = go where
go f@(F name cs _ _) = it (printf "has a %s file ‘%s’" (examplesC cs) name) (fitIO root f)
go f@(SL name s _ _ _) = it (printf "has a symlink ‘%s’ pointing to ‘%s’" name s) (fitIO root f)
go f@(D (combine root -> fullpath) _ _ _) = it (printf "has a subdirectory ‘%s’" fullpath) (fitIO root f)
go E = return ()
examplesC :: Maybe Contents -> String
examplesC (Just (Binary _)) = "binary"
examplesC (Just (Text _)) = "plain text"
examplesC (Just (CopyOf p)) = printf "(copy of ‘%s’)" p
examplesC Nothing = "regular"
validate
:: Exception e
=> (forall a. FilePath -> F a -> IO ()) -> FilePath -> Layout b -> IO (NonEmpty e \/ ())
validate g p = getCompose . go p . unL where
go root (Free f@(F _ _ _ m)) =
sequenceA_ [Compose (validateF root f), go root m]
go root (Free f@(SL _ _ _ _ m)) =
sequenceA_ [Compose (validateF root f), go root m]
go root (Free f@(D (combine root -> fullpath) is _ m)) =
sequenceA_ [Compose (validateF root f), go fullpath is, go root m]
go _ (Free E) = pure ()
go _ (Pure _) = pure ()
validateF root = validateIO . g root
validateIO :: Exception e => IO a -> IO (NonEmpty e \/ a)
validateIO io = first pure . fromEither <$> try io
-- | Check the real directory layout fits the description
fit :: FilePath -> Layout a -> IO (Either (NonEmpty FitError) ())
fit p = fmap toEither . validate fitIO p
fitIO :: FilePath -> F a -> IO ()
fitIO root = go where
go (F (combine root -> fullpath) cs a _) = do
for_ cs $ \cs' -> case cs' of
Binary bs -> do
real <- ByteString.readFile fullpath
when (real /= bs) $
throwIO (FitBadFileContents fullpath (FitBadBinary bs real))
Text t -> do
real <- Text.readFile fullpath
when (real /= t) $
throwIO (FitBadFileContents fullpath (FitBadText t real))
CopyOf f -> do
origin <- ByteStringLazy.readFile f
copy <- ByteStringLazy.readFile fullpath
when (origin /= copy) $
throwIO (FitBadFileContents fullpath (FitBadCopyOf f))
fitIOAux a fullpath
go (SL (combine root -> fullpath) s e a _) = do
path <- Posix.readSymbolicLink fullpath
when (path /= s) $
throwIO (FitBadLinkSource fullpath s path)
when e $
() <$ Posix.getFileStatus fullpath
fitIOAux a fullpath
go (D (combine root -> fullpath) _ a _) = () <$ do
fitIOAux a fullpath
go E = return ()
fitIOAux :: Aux -> FilePath -> IO ()
fitIOAux (Aux muid mgid mperm) path = do
status <- Posix.getSymbolicLinkStatus path
for_ muid $ \case
UserID i ->
unless (Posix.fileOwner status == i) $
throwIO (FitBadOwnerUser path (UserID i) (UserID (Posix.fileOwner status)))
Username name -> do
i <- getUserID name
n <- getUsername (Posix.fileOwner status)
unless (Posix.fileOwner status == i) $
throwIO (FitBadOwnerUser path (Username name) (Username n))
for_ mgid $ \case
GroupID i ->
unless (Posix.fileGroup status == i) $
throwIO (FitBadOwnerGroup path (GroupID i) (GroupID (Posix.fileGroup status)))
Groupname name -> do
i <- getGroupID name
n <- getGroupname (Posix.fileGroup status)
unless (Posix.fileGroup status == i) $
throwIO (FitBadOwnerGroup path (Groupname name) (Groupname n))
for_ mperm $ \perm ->
unless (Posix.fileMode status == perm) $
throwIO (FitBadFileMode path perm (Posix.fileMode status))
-- | Errors encountered while running 'fit'
data FitError =
FitBadFileContents FilePath FitContentsError
| FitBadLinkSource FilePath String {- expected -} String {- actual -}
| FitBadOwnerUser FilePath User {- expected -} User {- actual -}
| FitBadOwnerGroup FilePath Group {- expected -} Group {- actual -}
| FitBadFileMode FilePath Posix.FileMode {- expected -} Posix.FileMode {- actual -}
| FitIOException FilePath IOErrorType
deriving (Eq, Typeable, Generic)
-- | Expected/actual file contents mismatch
data FitContentsError =
FitBadBinary ByteString ByteString
| FitBadText Text Text
| FitBadCopyOf FilePath
deriving (Eq, Typeable, Generic)
instance Show FitError where
show (FitBadFileContents path mismatch) = unlines $
printf "Bad contents at ‘%s’" path : showCE mismatch
where
showCE :: FitContentsError -> [String]
showCE (FitBadBinary expected actual) =
[ "expected:"
, printf " %s" (show (ByteString.unpack expected))
, "actual:"
, printf " %s" (show (ByteString.unpack actual))
]
showCE (FitBadText expected actual) =
[ "expected:"
, printf " %s" (show expected)
, "actual:"
, printf " %s" (show actual)
]
showCE (FitBadCopyOf f) =
[ "expected:"
, printf " a copy of ‘%s’" f
, "actual:"
, " something else"
]
show (FitBadLinkSource path expected actual) = unlines $
[ printf "Bad symlink source at ‘%s’" path
, "expected:"
, printf " ‘%s’" expected
, "actual:"
, printf " ‘%s’" actual
]
show (FitBadOwnerUser path expected actual) = unlines $
[ printf "Bad owner user id at ‘%s’" path
, "expected:"
, printf " %s" (show expected)
, "actual:"
, printf " %s" (show actual)
]
show (FitBadOwnerGroup path expected actual) = unlines $
[ printf "Bad owner group id at ‘%s’" path
, "expected:"
, printf " %s" (show expected)
, "actual:"
, printf " %s" (show actual)
]
show (FitBadFileMode path expected actual) = unlines $
[ printf "Bad file permissions id at ‘%s’" path
, "expected:"
, printf " %s" (showOct expected "")
, "actual:"
, printf " %s" (showOct actual "")
]
show (FitIOException eloc etype) =
printf "Generic IO exception of type ‘%s’ happened at ‘%s’\n" (show etype) eloc
instance Exception FitError where
toException = SomeException
fromException e'@(SomeException e)
| Just ioe <- fromException e' =
Just (FitIOException (fromMaybe (ioeGetLocation ioe) (ioeGetFileName ioe)) (ioeGetErrorType ioe))
| otherwise = cast e
-- | Make the real directory layout from the description
make :: FilePath -> Layout a -> IO (Either (NonEmpty MakeError) ())
make p = fmap toEither . validate makeIO p
-- | Make the real directory layout from the description removing any previous contents
remake :: FilePath -> Layout a -> IO (Either (NonEmpty MakeError) ())
remake p l = fmap toEither . getCompose $
Compose (validateIO (removeDirectoryRecursive p *> createDirectory p)) *>
Compose (fmap fromEither (make p l))
makeIO :: FilePath -> F a -> IO ()
makeIO root = go where
go (F (combine root -> fullpath) cs a _) = do
case cs of
Just (Binary bs) -> ByteString.writeFile fullpath bs
Just (Text t) -> Text.writeFile fullpath t
Just (CopyOf p) -> ByteStringLazy.readFile p >>= ByteStringLazy.writeFile fullpath
Nothing -> ByteString.writeFile fullpath (ByteString.pack [])
makeIOAux a fullpath
go (SL (combine root -> fullpath) s _ a _) = do
Posix.createSymbolicLink s fullpath
makeIOAux a fullpath
go (D (combine root -> fullpath) _ a _) = do
createDirectoryIfMissing False fullpath
makeIOAux a fullpath
go E = return ()
makeIOAux :: Aux -> FilePath -> IO ()
makeIOAux (Aux muid mgid mperm) path = do
for_ muid $ \case
UserID i ->
Posix.setSymbolicLinkOwnerAndGroup path i (-1)
Username name -> do
i <- getUserID name
Posix.setSymbolicLinkOwnerAndGroup path i (-1)
for_ mgid $ \case
GroupID i ->
Posix.setSymbolicLinkOwnerAndGroup path (-1) i
Groupname name -> do
i <- getGroupID name
Posix.setSymbolicLinkOwnerAndGroup path (-1) i
for_ mperm $
Posix.setFileMode path
-- | Errors encountered while running 'make'
data MakeError =
MakeIOException FilePath IOErrorType
deriving (Show, Eq, Typeable, Generic)
instance Exception MakeError where
toException = SomeException
fromException e'@(SomeException e)
| Just ioe <- fromException e' =
Just (MakeIOException (fromMaybe (ioeGetLocation ioe) (ioeGetFileName ioe)) (ioeGetErrorType ioe))
| otherwise = cast e
getUserID :: String -> IO Posix.UserID
getUserID = fmap Posix.userID . Posix.getUserEntryForName
getUsername :: Posix.UserID -> IO String
getUsername = fmap Posix.userName . Posix.getUserEntryForID
getGroupID :: String -> IO Posix.GroupID
getGroupID = fmap Posix.groupID . Posix.getGroupEntryForName
getGroupname :: Posix.GroupID -> IO String
getGroupname = fmap Posix.groupName . Posix.getGroupEntryForID
-- | This type is isomorphic to 'Either' but its 'Applicative' instance accumulates errors
data e \/ a = Error e | Result a
deriving (Show, Eq, Ord, Functor, Foldable, Traversable, Typeable, Data, Generic)
instance Bifunctor (\/) where
bimap f _ (Error a) = Error (f a)
bimap _ g (Result a) = Result (g a)
instance Bifoldable (\/) where
bifoldMap f _ (Error a) = f a
bifoldMap _ g (Result a) = g a
instance Bitraversable (\/) where
bitraverse f _ (Error a) = Error <$> f a
bitraverse _ g (Result a) = Result <$> g a
instance Semigroup e => Applicative ((\/) e) where
pure = Result
Error f <*> Error x = Error (f <> x)
Error f <*> _ = Error f
_ <*> Error x = Error x
Result f <*> Result x = Result (f x)
fromEither :: Either e a -> e \/ a
fromEither = either Error Result
toEither :: e \/ a -> Either e a
toEither = validation Left Right
validation :: (e -> t) -> (a -> t) -> e \/ a -> t
validation f _ (Error e) = f e
validation _ g (Result a) = g a
-- | Construct 'Validation' value from the list of errors
--
-- >>> fromErrors []
-- Right ()
--
-- >>> fromErrors Nothing
-- Right ()
--
-- >>> fromErrors "hello"
-- Left ('h' :| "ello")
--
-- >>> fromErrors (Just "hello")
-- Left ("hello" :| [])
fromErrors :: Foldable t => t e -> Either (NonEmpty e) ()
fromErrors = go . toList
where
go [] = Right ()
go (x : xs) = Left (x :| xs)