|
| 1 | +-- | |
| 2 | +-- Module : Streamly.Coreutils.Chmod |
| 3 | +-- Copyright : (c) 2026 Composewell Technologies |
| 4 | +-- License : Apache-2.0 |
| 5 | +-- Maintainer : streamly@composewell.com |
| 6 | +-- Stability : experimental |
| 7 | +-- Portability : GHC |
| 8 | +-- |
| 9 | +-- Change file mode bits. Mirrors GNU @chmod@. Recursive mode not supported |
| 10 | +-- yet. |
| 11 | +-- |
| 12 | +-- == Shell equivalents |
| 13 | +-- |
| 14 | +-- >>> _ = chmod id (ownerRead True . ownerWrite True) -- chmod u=rw FILE |
| 15 | +-- >>> _ = chmod id (ownerRead True . groupRead True . otherRead True) -- chmod a=r FILE |
| 16 | +-- >>> _ = chmod (additive True) (ownerExec True) -- chmod u+x FILE |
| 17 | +-- >>> _ = chmod (additive True) (groupWrite False) -- chmod g-w FILE |
| 18 | +-- >>> _ = chmod (modeFrom file) (groupWrite True) -- chmod --reference=ref FILE |
| 19 | + |
| 20 | +module Streamly.Coreutils.Chmod |
| 21 | + ( -- * Runner |
| 22 | + chmod |
| 23 | + |
| 24 | + -- * Options |
| 25 | + , ChmodOptions |
| 26 | + , additive |
| 27 | + , modeFrom |
| 28 | + |
| 29 | + -- * Mode |
| 30 | + , Mode |
| 31 | + |
| 32 | + -- ** Owner bits |
| 33 | + , ownerRead |
| 34 | + , ownerWrite |
| 35 | + , ownerExec |
| 36 | + |
| 37 | + -- ** Group bits |
| 38 | + , groupRead |
| 39 | + , groupWrite |
| 40 | + , groupExec |
| 41 | + |
| 42 | + -- ** Other bits |
| 43 | + , otherRead |
| 44 | + , otherWrite |
| 45 | + , otherExec |
| 46 | + |
| 47 | + -- ** Special bits |
| 48 | + , setUid |
| 49 | + , setGid |
| 50 | + , sticky |
| 51 | + ) |
| 52 | +where |
| 53 | + |
| 54 | +import Data.Bits (complement, (.&.), (.|.)) |
| 55 | +import Streamly.FileSystem.Path (Path) |
| 56 | +import System.PosixCompat.Files (fileMode, getFileStatus, setFileMode) |
| 57 | +import System.PosixCompat.Types (FileMode) |
| 58 | + |
| 59 | +import qualified Streamly.FileSystem.Path as Path |
| 60 | + |
| 61 | +-- $setup |
| 62 | +-- >>> :set -XQuasiQuotes |
| 63 | +-- >>> import Streamly.Coreutils.Chmod |
| 64 | +-- >>> import Streamly.FileSystem.Path (path) |
| 65 | +-- >>> file = [path|a.txt|] |
| 66 | + |
| 67 | +-- = Design notes |
| 68 | +-- |
| 69 | +-- TODO: add recursive mode. |
| 70 | +-- TODO: add @followSymlinks@ option. |
| 71 | +-- TODO: make windows behavior consistent with filetest. |
| 72 | +-- |
| 73 | +-- Mode builders: |
| 74 | +-- ------------- |
| 75 | +-- |
| 76 | +-- Mode builders can be common to chmod and the file test utility and anything |
| 77 | +-- else in coreutils. We can have a common mode builder (FileMode) module |
| 78 | +-- exposing the mode builders to all consumers. |
| 79 | +-- |
| 80 | +-- The simplest is one function for each bit e.g. "ownerRead True" and compose |
| 81 | +-- them all to create a mode. There can be canned ownerRWX, ownerRX, ownerRW, |
| 82 | +-- ownerWX, that will complete all possible combinations, but does not include |
| 83 | +-- the sticky bit. Another supplementary mechanism could be a quasiquote |
| 84 | +-- [mode|a=rwx|] this can include sticky bit as well without exploding. |
| 85 | +-- |
| 86 | +-- We can also have a quasiquoter to build the chmod options directly e.g. |
| 87 | +-- @chmod [chmodOpt|a=rwx]@. |
| 88 | +-- |
| 89 | +-- Quasiquoter format: |
| 90 | +-- The format of a symbolic mode is [roles][-+=][perms...], where roles is |
| 91 | +-- either zero or more letters from the set "ugoa". perms is either zero or |
| 92 | +-- more letters from the set "rwxXst". Multiple symbolic modes can be given, |
| 93 | +-- separated by commas. |
| 94 | +-- |
| 95 | +-- Examples: |
| 96 | +-- |
| 97 | +-- @ |
| 98 | +-- - |
| 99 | +-- -rwx |
| 100 | +-- g-rx |
| 101 | +-- g-x+r |
| 102 | +-- go-x+rw |
| 103 | +-- go-x+rw,u+r |
| 104 | +-- @ |
| 105 | +-- |
| 106 | + |
| 107 | +------------------------------------------------------------------------------- |
| 108 | +-- Mode |
| 109 | +------------------------------------------------------------------------------- |
| 110 | + |
| 111 | +-- TODO: Should we directly use FileMode here, so that we do not have to export |
| 112 | +-- one more type which might conflict with others.? This is in "base" so should |
| 113 | +-- be fine. |
| 114 | + |
| 115 | +-- | File mode. |
| 116 | +-- |
| 117 | +-- This is an opaque type, construct values only by composing modifier |
| 118 | +-- functions and passing the result to 'chmod'. |
| 119 | +newtype Mode = Mode FileMode |
| 120 | + |
| 121 | +toggle :: FileMode -> Bool -> Mode -> Mode |
| 122 | +toggle bit True (Mode m) = Mode (m .|. bit) |
| 123 | +toggle bit False (Mode m) = Mode (m .&. complement bit) |
| 124 | + |
| 125 | +-- | Toggle the owner read bit (@0o400@). |
| 126 | +-- |
| 127 | +ownerRead :: Bool -> Mode -> Mode |
| 128 | +ownerRead = toggle 0o400 |
| 129 | + |
| 130 | +-- | Toggle the owner write bit (@0o200@). |
| 131 | +-- |
| 132 | +ownerWrite :: Bool -> Mode -> Mode |
| 133 | +ownerWrite = toggle 0o200 |
| 134 | + |
| 135 | +-- | Toggle the owner execute bit (@0o100@). |
| 136 | +-- |
| 137 | +ownerExec :: Bool -> Mode -> Mode |
| 138 | +ownerExec = toggle 0o100 |
| 139 | + |
| 140 | +-- | Toggle the group read bit (@0o040@). |
| 141 | +-- |
| 142 | +groupRead :: Bool -> Mode -> Mode |
| 143 | +groupRead = toggle 0o040 |
| 144 | + |
| 145 | +-- | Toggle the group write bit (@0o020@). |
| 146 | +-- |
| 147 | +groupWrite :: Bool -> Mode -> Mode |
| 148 | +groupWrite = toggle 0o020 |
| 149 | + |
| 150 | +-- | Toggle the group execute bit (@0o010@). |
| 151 | +-- |
| 152 | +groupExec :: Bool -> Mode -> Mode |
| 153 | +groupExec = toggle 0o010 |
| 154 | + |
| 155 | +-- | Toggle the other read bit (@0o004@). |
| 156 | +-- |
| 157 | +otherRead :: Bool -> Mode -> Mode |
| 158 | +otherRead = toggle 0o004 |
| 159 | + |
| 160 | +-- | Toggle the other write bit (@0o002@). |
| 161 | +-- |
| 162 | +otherWrite :: Bool -> Mode -> Mode |
| 163 | +otherWrite = toggle 0o002 |
| 164 | + |
| 165 | +-- | Toggle the other execute bit (@0o001@). |
| 166 | +-- |
| 167 | +otherExec :: Bool -> Mode -> Mode |
| 168 | +otherExec = toggle 0o001 |
| 169 | + |
| 170 | +-- | Toggle the set-user-ID bit (@0o4000@). |
| 171 | +-- |
| 172 | +setUid :: Bool -> Mode -> Mode |
| 173 | +setUid = toggle 0o4000 |
| 174 | + |
| 175 | +-- | Toggle the set-group-ID bit (@0o2000@). |
| 176 | +-- |
| 177 | +setGid :: Bool -> Mode -> Mode |
| 178 | +setGid = toggle 0o2000 |
| 179 | + |
| 180 | +-- | Toggle the sticky bit (@0o1000@). |
| 181 | +-- |
| 182 | +sticky :: Bool -> Mode -> Mode |
| 183 | +sticky = toggle 0o1000 |
| 184 | + |
| 185 | +------------------------------------------------------------------------------- |
| 186 | +-- Options |
| 187 | +------------------------------------------------------------------------------- |
| 188 | + |
| 189 | +data SeedSource = SeedZero | SeedSelf | SeedRef Path |
| 190 | + |
| 191 | +-- | 'chmod' configuration. Build options by composing modifiers with @(.)@ and |
| 192 | +-- pass the composition to 'chmod'; pass @id@ for defaults. |
| 193 | +newtype ChmodOptions = ChmodOptions { chmodSeed :: SeedSource } |
| 194 | + |
| 195 | +defaultOptions :: ChmodOptions |
| 196 | +defaultOptions = ChmodOptions SeedZero |
| 197 | + |
| 198 | +-- | When 'True', mode is added to the existing mode of the file instead of |
| 199 | +-- resetting it. |
| 200 | +-- |
| 201 | +-- Default: 'False'. |
| 202 | +additive :: Bool -> ChmodOptions -> ChmodOptions |
| 203 | +additive True opts = opts { chmodSeed = SeedSelf } |
| 204 | +additive False opts = opts { chmodSeed = SeedZero } |
| 205 | + |
| 206 | +-- NOTE: instead of having modeFrom option modifier, we could use a mode |
| 207 | +-- builder from file e.g. "modeFrom :: Path -> Mode -> IO Mode", but that is |
| 208 | +-- awkward to compose with pure "Mode -> Mode" builders. One way is to use a |
| 209 | +-- combinator like f :: Path -> Mode -> Mode -> IO (Mode -> Mode)". Or lift |
| 210 | +-- pure "Mode -> Mode" to "Mode -> IO Mode" and compose all with kliesli, but |
| 211 | +-- then we will need a chmod (or variant) taking "Mode -> IO Mode" as argument. |
| 212 | +-- It is much simpler to have "modeFrom" as option modifier compared to all |
| 213 | +-- these options. |
| 214 | + |
| 215 | +-- | Use the current mode of the given reference path as the starting mode, the |
| 216 | +-- specified mode is added to the reference mode. |
| 217 | +-- |
| 218 | +-- Default: no reference. |
| 219 | +modeFrom :: Path -> ChmodOptions -> ChmodOptions |
| 220 | +modeFrom ref opts = opts { chmodSeed = SeedRef ref } |
| 221 | + |
| 222 | +------------------------------------------------------------------------------- |
| 223 | +-- Runner |
| 224 | +------------------------------------------------------------------------------- |
| 225 | + |
| 226 | +-- XXX Do not use toString |
| 227 | +modeOf :: Path -> IO FileMode |
| 228 | +modeOf p = fileMode <$> getFileStatus (Path.toString p) |
| 229 | + |
| 230 | +resolveSeed :: SeedSource -> Path -> IO FileMode |
| 231 | +resolveSeed seed target = case seed of |
| 232 | + SeedZero -> pure 0 |
| 233 | + SeedSelf -> modeOf target |
| 234 | + SeedRef ref -> modeOf ref |
| 235 | + |
| 236 | +-- | Change the mode bits of a file. |
| 237 | +-- |
| 238 | +-- The desired mode is built by composing mode setter functions. By default the |
| 239 | +-- mode of the file is set to the supplied mode, the 'additive' modifier can be |
| 240 | +-- used to add to the existing mode. |
| 241 | +-- |
| 242 | +-- Symlinks are followed by default. |
| 243 | +-- |
| 244 | +-- Pass @id@ for default options and the @Mode -> Mode@ composition for the |
| 245 | +-- mode; each modifier documents its own default. |
| 246 | +-- |
| 247 | +-- Note: @chmod id id@ would clear all modes. |
| 248 | +chmod |
| 249 | + :: (ChmodOptions -> ChmodOptions) |
| 250 | + -> (Mode -> Mode) |
| 251 | + -> Path |
| 252 | + -> IO () |
| 253 | +chmod optF modeF path = do |
| 254 | + seed <- resolveSeed (chmodSeed (optF defaultOptions)) path |
| 255 | + let Mode bits = modeF (Mode seed) |
| 256 | + |
| 257 | + -- XXX do not use toString. |
| 258 | + setFileMode (Path.toString path) bits |
0 commit comments