@@ -16,20 +16,28 @@ import System.Nix.Hash
1616 ( HashAlgorithm (Truncated , SHA256 )
1717 , Digest
1818 , encodeBase32
19+ , decodeBase32
1920 , SomeNamedDigest
2021 )
21- import Text.Regex.Base.RegexLike ( makeRegex , matchTest )
22- import Text.Regex.TDFA.Text ( Regex )
22+ import System.Nix.Internal.Base32 ( digits32 )
23+
2324import Data.Text (Text )
2425import Data.Text.Encoding (encodeUtf8 )
26+ import qualified Data.Text as T
2527import GHC.TypeLits (Symbol , KnownSymbol , symbolVal )
2628import Data.ByteString (ByteString )
2729import qualified Data.ByteString as BS
2830import qualified Data.ByteString.Char8 as BC
31+ import qualified Data.Char
2932import Data.Hashable (Hashable (.. ))
3033import Data.HashSet (HashSet )
3134import Data.Proxy (Proxy (.. ))
3235
36+ import Data.Attoparsec.Text.Lazy (Parser , (<?>) )
37+
38+ import qualified Data.Attoparsec.Text.Lazy
39+ import qualified System.FilePath
40+
3341-- | A path in a Nix store.
3442--
3543-- From the Nix thesis: A store path is the full path of a store
@@ -39,20 +47,25 @@ import Data.Proxy (Proxy(..))
3947--
4048-- See the 'StoreDir' haddocks for details on why we represent this at
4149-- the type level.
42- data StorePath ( storeDir :: StoreDir ) = StorePath
50+ data StorePath = StorePath
4351 { -- | The 160-bit hash digest reflecting the "address" of the name.
4452 -- Currently, this is a truncated SHA256 hash.
4553 storePathHash :: ! (Digest StorePathHashAlgo )
4654 , -- | The (typically human readable) name of the path. For packages
4755 -- this is typically the package name and version (e.g.
4856 -- hello-1.2.3).
4957 storePathName :: ! StorePathName
58+ , -- | Root of the store
59+ storePathRoot :: ! FilePath
5060 } deriving (Eq , Ord )
5161
52- instance Hashable ( StorePath storeDir ) where
62+ instance Hashable StorePath where
5363 hashWithSalt s (StorePath {.. }) =
5464 s `hashWithSalt` storePathHash `hashWithSalt` storePathName
5565
66+ instance Show StorePath where
67+ show p = BC. unpack $ storePathToRawFilePath p
68+
5669-- | The name portion of a Nix path.
5770--
5871-- 'unStorePathName' must only contain a-zA-Z0-9+._?=-, can't start
@@ -67,7 +80,7 @@ newtype StorePathName = StorePathName
6780type StorePathHashAlgo = 'Truncated 20 'SHA256
6881
6982-- | A set of 'StorePath's.
70- type StorePathSet storeDir = HashSet ( StorePath storeDir )
83+ type StorePathSet = HashSet StorePath
7184
7285-- | An address for a content-addressable store path, i.e. one whose
7386-- store path hash is purely a function of its contents (as opposed to
@@ -99,103 +112,116 @@ data NarHashMode
99112 -- file if so desired.
100113 Recursive
101114
102- -- | A type-level representation of the root directory of a Nix store.
103- --
104- -- The extra complexity of type indices requires justification.
105- -- Fundamentally, this boils down to the fact that there is little
106- -- meaningful sense in which 'StorePath's rooted at different
107- -- directories are of the same type, i.e. there are few if any
108- -- non-trivial non-contrived functions or data types that could
109- -- equally well accept 'StorePath's from different stores. In current
110- -- practice, any real application dealing with Nix stores (including,
111- -- in particular, the Nix expression language) only operates over one
112- -- store root and only cares about 'StorePath's belonging to that
113- -- root. One could imagine a use case that cares about multiple store
114- -- roots at once (e.g. the normal \/nix\/store along with some private
115- -- store at \/root\/nix\/store to contain secrets), but in that case
116- -- distinguishing 'StorePath's that belong to one store or the other
117- -- is even /more/ critical: Most operations will only be correct over
118- -- one of the stores or another, and it would be an error to mix and
119- -- match (e.g. a 'StorePath' in one store could not legitimately refer
120- -- to one in another).
121- --
122- -- As of @5886bc5996537fbf00d1fcfbb29595b8ccc9743e@, the C++ Nix
123- -- codebase contains 30 separate places where we assert that a given
124- -- store dir is, in fact, in the store we care about; those run-time
125- -- assertions could be completely removed if we had stronger types
126- -- there. Moreover, there are dozens of other cases where input coming
127- -- from the user, from serializations, etc. is parsed and then
128- -- required to be in the appropriate store; this case is the
129- -- equivalent of an existentially quantified version of 'StorePath'
130- -- and, notably, requiring at runtime that the index matches the
131- -- ambient store directory we're working in. In every case where a
132- -- path is treated as a store path, there is exactly one legitimate
133- -- candidate for the store directory it belongs to.
134- --
135- -- It may be instructive to consider the example of "chroot stores".
136- -- Since Nix 2.0, it has been possible to have a store actually live
137- -- at one directory (say, $HOME\/nix\/store) with a different
138- -- effective store directory (say, \/nix\/store). Nix can build into
139- -- a chroot store by running the builds in a mount namespace where the
140- -- store is at the effective store directory, can download from a
141- -- binary cache containing paths for the effective store directory,
142- -- and can run programs in the store that expect to be living at the
143- -- effective store directory (via nix run). When viewed as store paths
144- -- (rather than random files in the filesystem), paths in a chroot
145- -- store have nothing in common with paths in a non-chroot store that
146- -- lives in the same directory, and a lot in common with paths in a
147- -- non-chroot store that lives in the effective store directory of the
148- -- store in question. Store paths in stores with the same effective
149- -- store directory share the same hashing scheme, can be copied
150- -- between each other, etc. Store paths in stores with different
151- -- effective store directories have no relationship to each other that
152- -- they don't have to arbitrary other files.
153- type StoreDir = Symbol
154-
155- -- | Smart constructor for 'StorePathName' that ensures the underlying
156- -- content invariant is met.
157- makeStorePathName :: Text -> Maybe StorePathName
158- makeStorePathName n = case matchTest storePathNameRegex n of
159- True -> Just $ StorePathName n
160- False -> Nothing
161-
162- -- | Regular expression to match valid store path names.
163- storePathNameRegex :: Regex
164- storePathNameRegex = makeRegex r
165- where
166- r :: String
167- r = " [a-zA-Z0-9\\ +\\ -\\ _\\ ?\\ =][a-zA-Z0-9\\ +\\ -\\ .\\ _\\ ?\\ =]*"
115+ makeStorePathName :: Text -> Either String StorePathName
116+ makeStorePathName n = case validStorePathName n of
117+ True -> Right $ StorePathName n
118+ False -> Left $ reasonInvalid n
119+
120+ reasonInvalid :: Text -> String
121+ reasonInvalid n | n == " " = " Empty name"
122+ reasonInvalid n | (T. length n > 211 ) = " Path too long"
123+ reasonInvalid n | (T. head n == ' .' ) = " Leading dot"
124+ reasonInvalid n | otherwise = " Invalid character"
125+
126+ validStorePathName :: Text -> Bool
127+ validStorePathName " " = False
128+ validStorePathName n = (T. length n <= 211 )
129+ && T. head n /= ' .'
130+ && T. all validStorePathNameChar n
131+
132+ validStorePathNameChar :: Char -> Bool
133+ validStorePathNameChar c = any ($ c) $
134+ [ Data.Char. isAsciiLower -- 'a'..'z'
135+ , Data.Char. isAsciiUpper -- 'A'..'Z'
136+ , Data.Char. isDigit
137+ ] ++
138+ map (==) " +-._?="
168139
169140-- | Copied from @RawFilePath@ in the @unix@ package, duplicated here
170141-- to avoid the dependency.
171142type RawFilePath = ByteString
172143
173144-- | Render a 'StorePath' as a 'RawFilePath'.
174145storePathToRawFilePath
175- :: forall storeDir . (KnownStoreDir storeDir )
176- => StorePath storeDir
146+ :: StorePath
177147 -> RawFilePath
178- storePathToRawFilePath ( StorePath {.. }) = BS. concat
148+ storePathToRawFilePath StorePath {.. } = BS. concat
179149 [ root
180150 , " /"
181151 , hashPart
182152 , " -"
183153 , name
184154 ]
185155 where
186- root = storeDirVal @ storeDir
156+ root = BC. pack storePathRoot
187157 hashPart = encodeUtf8 $ encodeBase32 storePathHash
188158 name = encodeUtf8 $ unStorePathName storePathName
189159
190- -- | Get a value-level representation of a 'KnownStoreDir'
191- storeDirVal :: forall storeDir . (KnownStoreDir storeDir )
192- => ByteString
193- storeDirVal = BC. pack $ symbolVal @ storeDir Proxy
160+ -- | Render a 'StorePath' as a 'FilePath'.
161+ storePathToFilePath
162+ :: StorePath
163+ -> FilePath
164+ storePathToFilePath = BC. unpack . storePathToRawFilePath
194165
195- -- | A 'StoreDir' whose value is known at compile time.
196- --
197- -- A valid instance of 'KnownStoreDir' should represent a valid path,
198- -- i.e. all "characters" fit into bytes (as determined by the logic of
199- -- 'BC.pack') and there are no 0 "characters". Currently this is not
200- -- enforced, but it should be.
201- type KnownStoreDir = KnownSymbol
166+ -- | Render a 'StorePath' as a 'Text'.
167+ storePathToText
168+ :: StorePath
169+ -> Text
170+ storePathToText = T. pack . BC. unpack . storePathToRawFilePath
171+
172+ -- | Build `narinfo` suffix from `StorePath` which
173+ -- can be used to query binary caches.
174+ storePathToNarInfo
175+ :: StorePath
176+ -> BC. ByteString
177+ storePathToNarInfo StorePath {.. } = BS. concat
178+ [ encodeUtf8 $ encodeBase32 storePathHash
179+ , " .narinfo"
180+ ]
181+
182+ -- | Parse `StorePath` from `BC.ByteString`, checking
183+ -- that store directory matches `expectedRoot`.
184+ parsePath
185+ :: FilePath
186+ -> BC. ByteString
187+ -> Either String StorePath
188+ parsePath expectedRoot x =
189+ let
190+ (rootDir, fname) = System.FilePath. splitFileName . BC. unpack $ x
191+ (digestPart, namePart) = T. breakOn " -" $ T. pack fname
192+ digest = decodeBase32 digestPart
193+ name = makeStorePathName . T. drop 1 $ namePart
194+ -- rootDir' = dropTrailingPathSeparator rootDir
195+ -- cannot use ^^ as it drops multiple slashes /a/b/// -> /a/b
196+ rootDir' = init rootDir
197+ storeDir = if expectedRoot == rootDir'
198+ then Right rootDir'
199+ else Left $ unwords $ [ " Root store dir mismatch, expected" , expectedRoot, " got" , rootDir']
200+ in
201+ StorePath <$> digest <*> name <*> storeDir
202+
203+ pathParser :: FilePath -> Parser StorePath
204+ pathParser expectedRoot = do
205+ Data.Attoparsec.Text.Lazy. string (T. pack expectedRoot)
206+ <?> " Store root mismatch" -- e.g. /nix/store
207+
208+ Data.Attoparsec.Text.Lazy. char ' /'
209+ <?> " Expecting path separator"
210+
211+ digest <- decodeBase32
212+ <$> Data.Attoparsec.Text.Lazy. takeWhile1 (\ c -> c `elem` digits32)
213+ <?> " Invalid Base32 part"
214+
215+ Data.Attoparsec.Text.Lazy. char ' -'
216+ <?> " Expecting dash (path name separator)"
217+
218+ c0 <- Data.Attoparsec.Text.Lazy. satisfy (\ c -> c /= ' .' && validStorePathNameChar c)
219+ <?> " Leading path name character is a dot or invalid character"
220+
221+ rest <- Data.Attoparsec.Text.Lazy. takeWhile validStorePathNameChar
222+ <?> " Path name contains invalid character"
223+
224+ let name = makeStorePathName $ T. cons c0 rest
225+
226+ either fail return
227+ $ StorePath <$> digest <*> name <*> pure expectedRoot
0 commit comments