|
| 1 | +{-| |
| 2 | +
|
| 3 | +Helpers for the /security-advisories/ file system. |
| 4 | +
|
| 5 | +Top-level functions that take a @FilePath@ expect the path to the |
| 6 | +top-level directory of the /security-advisories/ repository (i.e. |
| 7 | +it must have the @advisories/@ subdirectory). |
| 8 | +
|
| 9 | +-} |
| 10 | +module Security.Advisories.Filesystem |
| 11 | + ( |
| 12 | + dirNameAdvisories |
| 13 | + , dirNameReserved |
| 14 | + , isSecurityAdvisoriesRepo |
| 15 | + , getReservedIds |
| 16 | + , getAdvisoryIds |
| 17 | + , getAllocatedIds |
| 18 | + , greatestId |
| 19 | + , getGreatestId |
| 20 | + , forReserved |
| 21 | + , forAdvisory |
| 22 | + ) where |
| 23 | + |
| 24 | +import Control.Applicative (liftA2) |
| 25 | +import Data.Foldable (fold) |
| 26 | +import Data.Semigroup (Max(Max, getMax)) |
| 27 | +import Data.Traversable (for) |
| 28 | + |
| 29 | +import Control.Monad.IO.Class (MonadIO) |
| 30 | +import Control.Monad.Writer.Strict (execWriterT, tell) |
| 31 | +import System.FilePath ((</>), takeBaseName) |
| 32 | +import System.Directory (doesDirectoryExist) |
| 33 | +import System.Directory.PathWalk |
| 34 | + |
| 35 | +import Security.Advisories.HsecId (HsecId, parseHsecId, placeholder) |
| 36 | + |
| 37 | + |
| 38 | +dirNameAdvisories :: FilePath |
| 39 | +dirNameAdvisories = "advisories" |
| 40 | + |
| 41 | +dirNameReserved :: FilePath |
| 42 | +dirNameReserved = "reserved" |
| 43 | + |
| 44 | +-- | Check whether the directory appears to be the root of a |
| 45 | +-- /security-advisories/ filesystem. Only checks that the |
| 46 | +-- @advisories@ subdirectory exists. |
| 47 | +-- |
| 48 | +isSecurityAdvisoriesRepo :: FilePath -> IO Bool |
| 49 | +isSecurityAdvisoriesRepo path = |
| 50 | + doesDirectoryExist (path </> dirNameAdvisories) |
| 51 | + |
| 52 | + |
| 53 | +-- | Get a list of reserved HSEC IDs. The order is unspecified. |
| 54 | +-- |
| 55 | +getReservedIds :: FilePath -> IO [HsecId] |
| 56 | +getReservedIds root = |
| 57 | + forReserved root (\_ hsid -> pure [hsid]) |
| 58 | + |
| 59 | +-- | Get a list of used IDs (does not include reserved IDs) |
| 60 | +-- There may be duplicates and the order is unspecified. |
| 61 | +-- |
| 62 | +getAdvisoryIds :: FilePath -> IO [HsecId] |
| 63 | +getAdvisoryIds root = |
| 64 | + forAdvisory root (\_ hsid -> pure [hsid]) |
| 65 | + |
| 66 | +-- | Get all allocated IDs, including reserved IDs. |
| 67 | +-- There may be duplicates and the order is unspecified. |
| 68 | +-- |
| 69 | +getAllocatedIds :: FilePath -> IO [HsecId] |
| 70 | +getAllocatedIds root = |
| 71 | + liftA2 (<>) |
| 72 | + (getAdvisoryIds root) |
| 73 | + (getReservedIds root) |
| 74 | + |
| 75 | +-- | Return the greatest ID in a collection of IDs. If the |
| 76 | +-- collection is empty, return the 'placeholder'. |
| 77 | +-- |
| 78 | +greatestId :: (Foldable t) => t HsecId -> HsecId |
| 79 | +greatestId = getMax . foldr ((<>) . Max) (Max placeholder) |
| 80 | + |
| 81 | +-- | Return the greatest ID in the database, including reserved IDs. |
| 82 | +-- If there are IDs in the database, returns the 'placeholder'. |
| 83 | +-- |
| 84 | +getGreatestId :: FilePath -> IO HsecId |
| 85 | +getGreatestId = fmap greatestId . getAllocatedIds |
| 86 | + |
| 87 | + |
| 88 | +-- | Invoke a callback for each HSEC ID in the reserved |
| 89 | +-- directory. The results are combined monoidally. |
| 90 | +-- |
| 91 | +forReserved |
| 92 | + :: (MonadIO m, Monoid r) |
| 93 | + => FilePath -> (FilePath -> HsecId -> m r) -> m r |
| 94 | +forReserved root = |
| 95 | + _forFiles (root </> dirNameAdvisories </> dirNameReserved) |
| 96 | + |
| 97 | +-- | Invoke a callback for each HSEC ID under each of the advisory |
| 98 | +-- subdirectories, excluding the @reserved@ directory. The results |
| 99 | +-- are combined monoidally. |
| 100 | +-- |
| 101 | +-- The same ID could appear multiple times. In particular, the callback |
| 102 | +-- is invoked for symbolic links as well as regular files. |
| 103 | +-- |
| 104 | +forAdvisory |
| 105 | + :: (MonadIO m, Monoid r) |
| 106 | + => FilePath -> (FilePath -> HsecId -> m r) -> m r |
| 107 | +forAdvisory root go = do |
| 108 | + let dir = root </> dirNameAdvisories |
| 109 | + subdirs <- filter (/= dirNameReserved) <$> _getSubdirs dir |
| 110 | + fmap fold $ for subdirs $ \subdir -> _forFiles (dir </> subdir) go |
| 111 | + |
| 112 | +-- | Get names (not paths) of subdirectories of the given directory |
| 113 | +-- (one level). There's no monoidal, interruptible variant of |
| 114 | +-- @pathWalk@ so we use @WriterT@ to smuggle the result out. |
| 115 | +-- |
| 116 | +_getSubdirs :: (MonadIO m) => FilePath -> m [FilePath] |
| 117 | +_getSubdirs root = |
| 118 | + execWriterT $ |
| 119 | + pathWalkInterruptible root $ \_ subdirs _ -> do |
| 120 | + tell subdirs |
| 121 | + pure Stop |
| 122 | + |
| 123 | +_forFiles |
| 124 | + :: (MonadIO m, Monoid r) |
| 125 | + => FilePath -- ^ (sub)directory name |
| 126 | + -> (FilePath -> HsecId -> m r) |
| 127 | + -> m r |
| 128 | +_forFiles root go = |
| 129 | + pathWalkAccumulate root $ \_ _ files -> |
| 130 | + fmap fold $ for files $ \file -> |
| 131 | + case parseHsecId (takeBaseName file) of |
| 132 | + Nothing -> pure mempty |
| 133 | + Just hsid -> go (root </> file) hsid |
0 commit comments