Skip to content

Commit c105037

Browse files
authored
Merge pull request #282 from sandydoo/fix-executable-perms
nar: fix executable permissions logic
2 parents 1525aef + b6ab60c commit c105037

File tree

3 files changed

+71
-8
lines changed

3 files changed

+71
-8
lines changed

hnix-store-nar/hnix-store-nar.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ test-suite nar
9696
tasty-discover:tasty-discover
9797
build-depends:
9898
base
99+
, cryptonite
99100
, hnix-store-nar
100101
, base64-bytestring
101102
, cereal

hnix-store-nar/src/System/Nix/Nar/Effects.hs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ module System.Nix.Nar.Effects
44
( NarEffects(..)
55
, narEffectsIO
66
, IsExecutable(..)
7+
, isExecutable
8+
, setExecutable
79
) where
810

911
import Control.Monad.Trans.Control (MonadBaseControl)
@@ -19,10 +21,21 @@ import qualified Data.ByteString
1921
import qualified Data.ByteString.Lazy as Bytes.Lazy
2022
import qualified System.Directory as Directory
2123
import System.Posix.Files ( createSymbolicLink
24+
, fileMode
2225
, fileSize
26+
, FileStatus
2327
, getFileStatus
28+
, getSymbolicLinkStatus
29+
, groupExecuteMode
30+
, intersectFileModes
2431
, isDirectory
32+
, isRegularFile
33+
, nullFileMode
34+
, otherExecuteMode
35+
, ownerExecuteMode
2536
, readSymbolicLink
37+
, setFileMode
38+
, unionFileModes
2639
)
2740
import qualified System.IO as IO
2841
import qualified Control.Exception.Lifted as Exception.Lifted
@@ -59,13 +72,13 @@ narEffectsIO = NarEffects {
5972
narReadFile = liftIO . Bytes.Lazy.readFile
6073
, narWriteFile = \f e c -> liftIO $ do
6174
Bytes.Lazy.writeFile f c
62-
p <- Directory.getPermissions f
63-
Directory.setPermissions f (p { Directory.executable = e == Executable })
75+
Control.Monad.when (e == Executable) $
76+
setExecutable f
6477
, narStreamFile = streamStringOutIO
6578
, narListDir = liftIO . Directory.listDirectory
6679
, narCreateDir = liftIO . Directory.createDirectory
6780
, narCreateLink = \f -> liftIO . createSymbolicLink f
68-
, narIsExec = liftIO . (fmap (bool NonExecutable Executable . Directory.executable)) . Directory.getPermissions
81+
, narIsExec = liftIO . fmap (bool NonExecutable Executable . isExecutable) . getSymbolicLinkStatus
6982
, narIsDir = fmap isDirectory . liftIO . getFileStatus
7083
, narIsSymLink = liftIO . Directory.pathIsSymbolicLink
7184
, narFileSize = fmap (fromIntegral . fileSize) . liftIO . getFileStatus
@@ -102,10 +115,34 @@ streamStringOutIO f executable getChunk =
102115
liftIO $ Data.ByteString.hPut handle c
103116
go handle
104117
updateExecutablePermissions =
105-
Control.Monad.when (executable == Executable) $ do
106-
p <- Directory.getPermissions f
107-
Directory.setPermissions f (p { Directory.executable = True })
118+
Control.Monad.when (executable == Executable) $
119+
setExecutable f
108120
cleanupException (e :: Exception.Lifted.SomeException) = do
109121
liftIO $ Directory.removeFile f
110122
Control.Monad.fail $
111123
"Failed to stream string to " <> f <> ": " <> show e
124+
125+
-- | Check whether the file is executable by the owner.
126+
--
127+
-- Matches the logic used by Nix.
128+
--
129+
-- access() should not be used for this purpose on macOS.
130+
-- It returns false for executables when placed in certain directories.
131+
-- For example, when in an app bundle: App.app/Contents/Resources/en.lproj/myexecutable.strings
132+
isExecutable :: FileStatus -> Bool
133+
isExecutable st =
134+
isRegularFile st
135+
&& fileMode st `intersectFileModes` ownerExecuteMode /= nullFileMode
136+
137+
-- | Set the file to be executable by the owner, group, and others.
138+
--
139+
-- Matches the logic used by Nix.
140+
setExecutable :: FilePath -> IO ()
141+
setExecutable f = do
142+
st <- getSymbolicLinkStatus f
143+
let p =
144+
fileMode st
145+
`unionFileModes` ownerExecuteMode
146+
`unionFileModes` groupExecuteMode
147+
`unionFileModes` otherExecuteMode
148+
setFileMode f p

hnix-store-nar/tests/NarFormat.hs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Control.Applicative (many, optional, (<|>))
77
import qualified Control.Concurrent as Concurrent
88
import Control.Exception (SomeException, try)
99
import Control.Monad (replicateM, void, forM_, when)
10+
import Crypto.Hash (hash, Digest, SHA256)
1011
import Data.Serialize (Serialize(..))
1112
import Data.Serialize (Get, getByteString,
1213
getInt64le,
@@ -35,6 +36,7 @@ import System.Environment (getEnv)
3536
import System.FilePath ((<.>), (</>))
3637
import qualified System.IO as IO
3738
import qualified System.IO.Temp as Temp
39+
import qualified System.Posix.Files as Unix
3840
import qualified System.Posix.Process as Unix
3941
import qualified System.Process as P
4042
import Test.Tasty as T
@@ -142,11 +144,19 @@ unit_nixStoreDirectory = filesystemNixStore "directory" (Nar sampleDirectory)
142144
unit_nixStoreDirectory' :: HU.Assertion
143145
unit_nixStoreDirectory' = filesystemNixStore "directory'" (Nar sampleDirectory')
144146

147+
-- | Test that the executable permissions are handled correctly in app bundles on macOS.
148+
-- In this case, access() returns false for a file under this specific path, even when the executable bit is set.
149+
-- NAR implementations should avoid this syscall on macOS.
150+
test_nixStoreMacOSAppBundle :: TestTree
151+
test_nixStoreMacOSAppBundle = packThenExtract "App.app" $ \ baseDir -> do
152+
let testDir = baseDir </> "App.app" </> "Resources" </> "en.lproj"
153+
Directory.createDirectoryIfMissing True testDir
154+
mkExecutableFile (testDir </> "test.strings")
155+
145156
test_nixStoreBigFile :: TestTree
146157
test_nixStoreBigFile = packThenExtract "bigfile" $ \baseDir -> do
147158
mkBigFile (baseDir </> "bigfile")
148159

149-
150160
test_nixStoreBigDir :: TestTree
151161
test_nixStoreBigDir = packThenExtract "bigdir" $ \baseDir -> do
152162
let testDir = baseDir </> "bigdir"
@@ -350,7 +360,16 @@ packThenExtract testName setup =
350360
IO.withFile hnixNarFile IO.WriteMode $ \h ->
351361
buildNarIO narEffectsIO narFilePath h
352362

353-
-- BSL.writeFile hnixNarFile narBS
363+
-- Compare the hash digests of the two NARs
364+
nixHash :: Digest SHA256 <- hash <$> BS.readFile nixNarFile
365+
hnixHash :: Digest SHA256 <- hash <$> BS.readFile hnixNarFile
366+
step $ unlines
367+
[ "Compare SHA256 digests between NARs:"
368+
, " nix: " <> show nixHash
369+
, " hnix: " <> show hnixHash
370+
]
371+
372+
HU.assertEqual "Hash mismatch between NARs" nixHash hnixHash
354373

355374
step $ "Unpack NAR to " <> outputFile
356375
_narHandle <- IO.withFile nixNarFile IO.ReadMode $ \h ->
@@ -567,6 +586,12 @@ mkBigFile path = do
567586
fsize <- getBigFileSize
568587
BSL.writeFile path (BSL.take fsize $ BSL.cycle "Lorem ipsum")
569588

589+
mkExecutableFile :: FilePath -> IO ()
590+
mkExecutableFile path = do
591+
BSL.writeFile path ""
592+
st <- Unix.getSymbolicLinkStatus path
593+
let p = Unix.fileMode st `Unix.unionFileModes` Unix.ownerExecuteMode
594+
Unix.setFileMode path p
570595

571596
-- | Construct FilePathPart from Text by checking that there
572597
-- are no '/' or '\\NUL' characters

0 commit comments

Comments
 (0)