hnix-store-core-0.4.2.0: src/System/Nix/Internal/StorePath.hs
{-|
Description : Representation of Nix store paths.
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE TypeInType #-} -- Needed for GHC 8.4.4 for some reason
module System.Nix.Internal.StorePath where
import System.Nix.Hash ( HashAlgorithm
( Truncated
, SHA256
)
, Digest
, BaseEncoding(..)
, encodeInBase
, decodeBase
, SomeNamedDigest
)
import qualified System.Nix.Internal.Base32 as Nix.Base32
( digits32 )
import Data.ByteString ( ByteString )
import qualified Data.ByteString.Char8 as Bytes.Char8
import qualified Data.Char as Char
import Data.Text ( Text )
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
( encodeUtf8 )
import Data.Attoparsec.Text.Lazy ( Parser
, (<?>)
)
import qualified Data.Attoparsec.Text.Lazy as Parser.Text.Lazy
import qualified System.FilePath as FilePath
import Data.Hashable ( Hashable(..) )
import Data.HashSet ( HashSet )
-- | A path in a Nix store.
--
-- From the Nix thesis: A store path is the full path of a store
-- object. It has the following anatomy: storeDir/hashPart-name.
--
-- @storeDir@: The root of the Nix store (e.g. \/nix\/store).
--
-- See the 'StoreDir' haddocks for details on why we represent this at
-- the type level.
data StorePath = StorePath
{ -- | The 160-bit hash digest reflecting the "address" of the name.
-- Currently, this is a truncated SHA256 hash.
storePathHash :: !(Digest StorePathHashAlgo)
, -- | The (typically human readable) name of the path. For packages
-- this is typically the package name and version (e.g.
-- hello-1.2.3).
storePathName :: !StorePathName
, -- | Root of the store
storePathRoot :: !FilePath
}
deriving (Eq, Ord)
instance Hashable StorePath where
hashWithSalt s StorePath{..} =
s `hashWithSalt` storePathHash `hashWithSalt` storePathName
instance Show StorePath where
show p = Bytes.Char8.unpack $ storePathToRawFilePath p
-- | The name portion of a Nix path.
--
-- 'unStorePathName' must only contain a-zA-Z0-9+._?=-, can't start
-- with a -, and must have at least one character (i.e. it must match
-- 'storePathNameRegex').
newtype StorePathName = StorePathName
{ -- | Extract the contents of the name.
unStorePathName :: Text
} deriving (Eq, Hashable, Ord)
-- | The hash algorithm used for store path hashes.
type StorePathHashAlgo = 'Truncated 20 'SHA256
-- | A set of 'StorePath's.
type StorePathSet = HashSet StorePath
-- | An address for a content-addressable store path, i.e. one whose
-- store path hash is purely a function of its contents (as opposed to
-- paths that are derivation outputs, whose hashes are a function of
-- the contents of the derivation file instead).
--
-- For backwards-compatibility reasons, the same information is
-- encodable in multiple ways, depending on the method used to add the
-- path to the store. These unfortunately result in separate store
-- paths.
data ContentAddressableAddress
= -- | The path is a plain file added via makeTextPath or
-- addTextToStore. It is addressed according to a sha256sum of the
-- file contents.
Text !(Digest 'SHA256)
| -- | The path was added to the store via makeFixedOutputPath or
-- addToStore. It is addressed according to some hash algorithm
-- applied to the nar serialization via some 'NarHashMode'.
Fixed !NarHashMode !SomeNamedDigest
-- | Schemes for hashing a Nix archive.
--
-- For backwards-compatibility reasons, there are two different modes
-- here, even though 'Recursive' should be able to cover both.
data NarHashMode
= -- | Require the nar to represent a non-executable regular file.
RegularFile
| -- | Hash an arbitrary nar, including a non-executable regular
-- file if so desired.
Recursive
makeStorePathName :: Text -> Either String StorePathName
makeStorePathName n =
if validStorePathName n
then Right $ StorePathName n
else Left $ reasonInvalid n
reasonInvalid :: Text -> String
reasonInvalid n
| n == "" = "Empty name"
| Text.length n > 211 = "Path too long"
| Text.head n == '.' = "Leading dot"
| otherwise = "Invalid character"
validStorePathName :: Text -> Bool
validStorePathName n =
n /= ""
&& Text.length n <= 211
&& Text.head n /= '.'
&& Text.all validStorePathNameChar n
validStorePathNameChar :: Char -> Bool
validStorePathNameChar c =
any ($ c)
[ Char.isAsciiLower -- 'a'..'z', isAscii..er probably faster then putting it out
, Char.isAsciiUpper -- 'A'..'Z'
, Char.isDigit
, (`elem` ("+-._?=" :: String))
]
-- | Copied from @RawFilePath@ in the @unix@ package, duplicated here
-- to avoid the dependency.
type RawFilePath = ByteString
-- | Render a 'StorePath' as a 'RawFilePath'.
storePathToRawFilePath :: StorePath -> RawFilePath
storePathToRawFilePath StorePath{..} =
root <> "/" <> hashPart <> "-" <> name
where
root = Bytes.Char8.pack storePathRoot
hashPart = Text.encodeUtf8 $ encodeInBase Base32 storePathHash
name = Text.encodeUtf8 $ unStorePathName storePathName
-- | Render a 'StorePath' as a 'FilePath'.
storePathToFilePath :: StorePath -> FilePath
storePathToFilePath = Bytes.Char8.unpack . storePathToRawFilePath
-- | Render a 'StorePath' as a 'Text'.
storePathToText :: StorePath -> Text
storePathToText = Text.pack . Bytes.Char8.unpack . storePathToRawFilePath
-- | Build `narinfo` suffix from `StorePath` which
-- can be used to query binary caches.
storePathToNarInfo :: StorePath -> Bytes.Char8.ByteString
storePathToNarInfo StorePath{..} =
Text.encodeUtf8 $ encodeInBase Base32 storePathHash <> ".narinfo"
-- | Parse `StorePath` from `Bytes.Char8.ByteString`, checking
-- that store directory matches `expectedRoot`.
parsePath :: FilePath -> Bytes.Char8.ByteString -> Either String StorePath
parsePath expectedRoot x =
let
(rootDir, fname) = FilePath.splitFileName . Bytes.Char8.unpack $ x
(digestPart, namePart) = Text.breakOn "-" $ Text.pack fname
digest = decodeBase Base32 digestPart
name = makeStorePathName . Text.drop 1 $ namePart
--rootDir' = dropTrailingPathSeparator rootDir
-- cannot use ^^ as it drops multiple slashes /a/b/// -> /a/b
rootDir' = init rootDir
storeDir =
if expectedRoot == rootDir'
then Right rootDir'
else Left $ "Root store dir mismatch, expected" <> expectedRoot <> "got" <> rootDir'
in
StorePath <$> digest <*> name <*> storeDir
pathParser :: FilePath -> Parser StorePath
pathParser expectedRoot = do
_ <-
Parser.Text.Lazy.string (Text.pack expectedRoot)
<?> "Store root mismatch" -- e.g. /nix/store
_ <- Parser.Text.Lazy.char '/'
<?> "Expecting path separator"
digest <-
decodeBase Base32
<$> Parser.Text.Lazy.takeWhile1 (`elem` Nix.Base32.digits32)
<?> "Invalid Base32 part"
_ <- Parser.Text.Lazy.char '-' <?> "Expecting dash (path name separator)"
c0 <-
Parser.Text.Lazy.satisfy (\c -> c /= '.' && validStorePathNameChar c)
<?> "Leading path name character is a dot or invalid character"
rest <-
Parser.Text.Lazy.takeWhile validStorePathNameChar
<?> "Path name contains invalid character"
let name = makeStorePathName $ Text.cons c0 rest
either
fail
pure
(StorePath <$> digest <*> name <*> pure expectedRoot)