Skip to content

Commit 0092a29

Browse files
committed
Implement case-insensitive hack
1 parent a63e522 commit 0092a29

File tree

4 files changed

+138
-24
lines changed

4 files changed

+138
-24
lines changed

hnix-store-core/hnix-store-core.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ library
4040
, System.Nix.Internal.Nar.Parser
4141
, System.Nix.Internal.Nar.Streamer
4242
, System.Nix.Internal.Nar.Effects
43+
, System.Nix.Internal.Nar.Options
4344
, System.Nix.Internal.Signature
4445
, System.Nix.Internal.StorePath
4546
, System.Nix.Nar
@@ -55,6 +56,7 @@ library
5556
, base16-bytestring
5657
, base64-bytestring
5758
, bytestring
59+
, case-insensitive
5860
, cereal
5961
, containers
6062
-- Required for cryptonite low-level type convertion
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module System.Nix.Internal.Nar.Options
2+
( NarOptions(..)
3+
, defaultNarOptions
4+
, caseHackSuffix
5+
) where
6+
7+
import qualified System.Info
8+
9+
-- | Options for configuring how NAR files are encoded and decoded.
10+
data NarOptions = NarOptions {
11+
optUseCaseHack :: Bool
12+
-- ^ Whether to enable a case hack to support case-insensitive filesystems.
13+
-- Equivalent to the 'use-case-hack' option in the Nix client.
14+
--
15+
-- The case hack rewrites file names to avoid collisions on case-insensitive file systems, e.g. APFS and HFS+ on macOS.
16+
-- Enabled by default on macOS (Darwin).
17+
}
18+
19+
defaultNarOptions :: NarOptions
20+
defaultNarOptions = NarOptions {
21+
optUseCaseHack =
22+
if System.Info.os == "darwin"
23+
then True
24+
else False
25+
}
26+
27+
caseHackSuffix :: Text
28+
caseHackSuffix = "~nix~case~hack~"

hnix-store-core/src/System/Nix/Internal/Nar/Parser.hs

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
module System.Nix.Internal.Nar.Parser
99
( runParser
10+
, runParserWithOptions
1011
, parseNar
1112
, testParser
1213
, testParser'
@@ -26,6 +27,8 @@ import qualified Control.Monad.State as State
2627
import qualified Control.Monad.Trans as Trans
2728
import qualified Control.Monad.Trans.Control as Base
2829
import qualified Data.ByteString as Bytes
30+
import qualified Data.CaseInsensitive as CI
31+
import qualified Data.HashMap.Strict as HashMap
2932
import qualified Data.List as List
3033
import qualified Data.Map as Map
3134
import qualified Data.Serialize as Serialize
@@ -35,6 +38,7 @@ import System.FilePath as FilePath
3538
import qualified System.IO as IO
3639

3740
import qualified System.Nix.Internal.Nar.Effects as Nar
41+
import qualified System.Nix.Internal.Nar.Options as Nar
3842

3943

4044
-- | NarParser is a monad for parsing a Nar file as a byte stream
@@ -48,19 +52,34 @@ newtype NarParser m a = NarParser
4852
ParserState
4953
(Except.ExceptT
5054
String
51-
(Reader.ReaderT
52-
(Nar.NarEffects m)
53-
m
54-
)
55+
(Reader.ReaderT (ParserEnv m) m)
5556
)
5657
a
5758
}
5859
deriving ( Functor, Applicative, Monad, Fail.MonadFail
5960
, Trans.MonadIO, State.MonadState ParserState
6061
, Except.MonadError String
61-
, Reader.MonadReader (Nar.NarEffects m)
62+
, Reader.MonadReader (ParserEnv m)
6263
)
6364

65+
66+
data ParserEnv m = ParserEnv
67+
{ envNarEffects :: Nar.NarEffects m
68+
, envNarOptions :: Nar.NarOptions
69+
}
70+
71+
72+
getNarEffects :: Monad m => NarParser m (Nar.NarEffects m)
73+
getNarEffects = fmap envNarEffects ask
74+
75+
76+
getNarEffect :: Monad m => (Nar.NarEffects m -> a) -> NarParser m a
77+
getNarEffect eff = fmap eff getNarEffects
78+
79+
80+
getNarOptions :: Monad m => NarParser m Nar.NarOptions
81+
getNarOptions = fmap envNarOptions ask
82+
6483
-- | Run a @NarParser@ over a byte stream
6584
-- This is suitable for testing the top-level NAR parser, or any of the
6685
-- smaller utilities parsers, if you have bytes appropriate for them
@@ -77,9 +96,26 @@ runParser
7796
-> FilePath
7897
-- ^ The root file system object to be created by the NAR
7998
-> m (Either String a)
80-
runParser effs (NarParser action) h target = do
99+
runParser effs parser h target = do
100+
runParserWithOptions Nar.defaultNarOptions effs parser h target
101+
102+
runParserWithOptions
103+
:: forall m a
104+
. (IO.MonadIO m, Base.MonadBaseControl IO m)
105+
=> Nar.NarOptions
106+
-> Nar.NarEffects m
107+
-- ^ Provide the effects set, usually @narEffectsIO@
108+
-> NarParser m a
109+
-- ^ A parser to run, such as @parseNar@
110+
-> IO.Handle
111+
-- ^ A handle the stream containg the NAR. It should already be
112+
-- open and in @ReadMode@
113+
-> FilePath
114+
-- ^ The root file system object to be created by the NAR
115+
-> m (Either String a)
116+
runParserWithOptions opts effs (NarParser action) h target = do
81117
unpackResult <-
82-
runReaderT (runExceptT $ State.evalStateT action state0) effs
118+
runReaderT (runExceptT $ State.evalStateT action state0) (ParserEnv effs opts)
83119
`Exception.Lifted.catch` exceptionHandler
84120
when (isLeft unpackResult) cleanup
85121
pure unpackResult
@@ -92,6 +128,7 @@ runParser effs (NarParser action) h target = do
92128
, handle = h
93129
, directoryStack = [target]
94130
, links = []
131+
, filePaths = HashMap.empty
95132
}
96133

97134
exceptionHandler :: Exception.Lifted.SomeException -> m (Either String a)
@@ -126,6 +163,9 @@ data ParserState = ParserState
126163
, links :: [LinkInfo]
127164
-- ^ Unlike with files and directories, we collect symlinks
128165
-- from the NAR on
166+
, filePaths :: HashMap.HashMap (CI.CI FilePath) Int
167+
-- ^ A map of case-insensitive files paths to the number of collisions encountered.
168+
-- See @Nar.NarOptions.optUseCaseHack@ for details.
129169
}
130170

131171

@@ -230,11 +270,11 @@ parseFile = do
230270
pure $ Just chunk
231271

232272
target <- currentFile
233-
streamFile <- asks Nar.narStreamFile
273+
streamFile <- getNarEffect Nar.narStreamFile
234274
lift (streamFile target getChunk)
235275

236276
when (s == "executable") $ do
237-
effs :: Nar.NarEffects m <- ask
277+
effs :: Nar.NarEffects m <- getNarEffects
238278
lift $ do
239279
p <- Nar.narGetPerms effs target
240280
Nar.narSetPerms effs target (p { Directory.executable = True })
@@ -246,34 +286,50 @@ parseFile = do
246286
-- handles for target files longer than needed
247287
parseDirectory :: (IO.MonadIO m, Fail.MonadFail m) => NarParser m ()
248288
parseDirectory = do
249-
createDirectory <- asks Nar.narCreateDir
289+
createDirectory <- getNarEffect Nar.narCreateDir
250290
target <- currentFile
251291
lift $ createDirectory target
252-
parseEntryOrFinish
292+
parseEntryOrFinish target
253293

254294
where
255295

256-
parseEntryOrFinish :: (IO.MonadIO m, Fail.MonadFail m) => NarParser m ()
257-
parseEntryOrFinish =
296+
parseEntryOrFinish :: (IO.MonadIO m, Fail.MonadFail m) => FilePath -> NarParser m ()
297+
parseEntryOrFinish path =
258298
-- If we reach a ")", we finished the directory's entries, and we have
259299
-- to put ")" back into the stream, because the outer call to @parens@
260300
-- expects to consume it.
261301
-- Otherwise, parse an entry as a fresh file system object
262302
matchStr
263303
[ ( ")" , pushStr ")" )
264-
, ("entry", parseEntry )
304+
, ("entry", parseEntry path )
265305
]
266306

267-
parseEntry :: (IO.MonadIO m, Fail.MonadFail m) => NarParser m ()
268-
parseEntry = do
307+
parseEntry :: (IO.MonadIO m, Fail.MonadFail m) => FilePath -> NarParser m ()
308+
parseEntry path = do
309+
opts <- getNarOptions
269310
parens $ do
270311
expectStr "name"
271-
fName <- parseStr
312+
fName <-
313+
if Nar.optUseCaseHack opts then
314+
addCaseHack path =<< parseStr
315+
else
316+
parseStr
272317
pushFileName (toString fName)
273318
expectStr "node"
274319
parens parseFSO
275320
popFileName
276-
parseEntryOrFinish
321+
parseEntryOrFinish path
322+
323+
addCaseHack :: (IO.MonadIO m, Fail.MonadFail m) => FilePath -> Text -> NarParser m Text
324+
addCaseHack path fName = do
325+
let key = path </> Text.unpack fName
326+
recordFilePath key
327+
conflictCount <- getFilePathConflictCount key
328+
pure $
329+
if conflictCount > 0 then
330+
fName <> Nar.caseHackSuffix <> show conflictCount
331+
else
332+
fName
277333

278334

279335

@@ -373,7 +429,7 @@ parens act = do
373429
-- (Targets must be created before the links that target them)
374430
createLinks :: IO.MonadIO m => NarParser m ()
375431
createLinks = do
376-
createLink <- asks Nar.narCreateLink
432+
createLink <- getNarEffect Nar.narCreateLink
377433
allLinks <- State.gets links
378434
sortedLinks <- IO.liftIO $ sortLinksIO allLinks
379435
forM_ sortedLinks $ \li -> do
@@ -473,6 +529,16 @@ pushLink :: Monad m => LinkInfo -> NarParser m ()
473529
pushLink linkInfo = State.modify (\s -> s { links = linkInfo : links s })
474530

475531

532+
-- | Add a file path to the collection of encountered file paths
533+
recordFilePath :: Monad m => FilePath -> NarParser m ()
534+
recordFilePath fPath =
535+
State.modify (\s -> s { filePaths = HashMap.insertWith (\_ v -> v + 1) (CI.mk fPath) 0 (filePaths s) })
536+
537+
getFilePathConflictCount :: Monad m => FilePath -> NarParser m Int
538+
getFilePathConflictCount fPath = do
539+
fileMap <- State.gets filePaths
540+
pure $ HashMap.findWithDefault 0 (CI.mk fPath) fileMap
541+
476542
------------------------------------------------------------------------------
477543
-- * Utilities
478544

hnix-store-core/src/System/Nix/Internal/Nar/Streamer.hs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module System.Nix.Internal.Nar.Streamer
77
, dumpString
88
, dumpPath
99
, streamNarIO
10+
, streamNarIOWithOptions
1011
, IsExecutable(..)
1112
)
1213
where
@@ -15,12 +16,13 @@ import qualified Control.Monad.IO.Class as IO
1516
import qualified Data.ByteString as Bytes
1617
import qualified Data.ByteString.Lazy as Bytes.Lazy
1718
import qualified Data.Serialize as Serial
18-
import qualified Data.Text as T (pack)
19+
import qualified Data.Text as T (pack, breakOn)
1920
import qualified Data.Text.Encoding as TE (encodeUtf8)
2021
import qualified System.Directory as Directory
2122
import System.FilePath ((</>))
2223

2324
import qualified System.Nix.Internal.Nar.Effects as Nar
25+
import qualified System.Nix.Internal.Nar.Options as Nar
2426

2527

2628
-- | NarSource
@@ -56,7 +58,11 @@ dumpPath = streamNarIO Nar.narEffectsIO
5658
-- function from any streaming library, and repeatedly calls
5759
-- it while traversing the filesystem object to Nar encode
5860
streamNarIO :: forall m . IO.MonadIO m => Nar.NarEffects IO -> FilePath -> NarSource m
59-
streamNarIO effs basePath yield = do
61+
streamNarIO effs basePath yield =
62+
streamNarIOWithOptions Nar.defaultNarOptions effs basePath yield
63+
64+
streamNarIOWithOptions :: forall m . IO.MonadIO m => Nar.NarOptions -> Nar.NarEffects IO -> FilePath -> NarSource m
65+
streamNarIOWithOptions opts effs basePath yield = do
6066
yield $ str "nix-archive-1"
6167
parens $ go basePath
6268
where
@@ -76,7 +82,12 @@ streamNarIO effs basePath yield = do
7682
yield $ str "entry"
7783
parens $ do
7884
let fullName = path </> f
79-
yield $ strs ["name", filePathToBS f, "node"]
85+
let serializedPath =
86+
if Nar.optUseCaseHack opts then
87+
filePathToBSWithCaseHack f
88+
else
89+
filePathToBS f
90+
yield $ strs ["name", serializedPath, "node"]
8091
parens $ go fullName
8192
else do
8293
isExec <- IO.liftIO $ isExecutable effs path
@@ -87,8 +98,6 @@ streamNarIO effs basePath yield = do
8798
yield $ int fSize
8899
yieldFile path fSize
89100

90-
filePathToBS = TE.encodeUtf8 . T.pack
91-
92101
parens act = do
93102
yield $ str "("
94103
r <- act
@@ -130,3 +139,12 @@ padBS strSize bs = bs <> Bytes.replicate (padLen strSize) 0
130139

131140
strs :: [ByteString] -> ByteString
132141
strs xs = Bytes.concat $ str <$> xs
142+
143+
filePathToBS :: FilePath -> ByteString
144+
filePathToBS = TE.encodeUtf8 . T.pack
145+
146+
filePathToBSWithCaseHack :: FilePath -> ByteString
147+
filePathToBSWithCaseHack = TE.encodeUtf8 . undoCaseHack . T.pack
148+
149+
undoCaseHack :: Text -> Text
150+
undoCaseHack = fst . T.breakOn Nar.caseHackSuffix

0 commit comments

Comments
 (0)