nova-nix-0.8.0.0: src/Nix/Store/Path.hs
-- | Nix store path types and computation.
--
-- == The Nix store model
--
-- The Nix store is a flat directory of immutable, content-addressed
-- packages. Every entry looks like:
--
-- @\/nix\/store\/\<hash\>-\<name\>@
--
-- On Windows, this becomes:
--
-- @C:\\nix\\store\\\<hash\>-\<name\>@
--
-- The store is IMMUTABLE. Once a path is registered, it never changes.
-- This is enforced by making the store directory read-only after builds.
-- This immutability is what enables:
--
-- * Atomic upgrades (install new version, switch symlink, done)
-- * Rollbacks (old version still in store, just switch symlink back)
-- * Concurrent installs (no file conflicts - different hashes = different dirs)
-- * Garbage collection (delete unreferenced paths, everything else stays)
-- * Binary substitution (if hash matches, the build output is identical)
--
-- == References
--
-- A store path can REFERENCE other store paths. For example, a compiled
-- binary references its shared libraries, its interpreter, etc. Nix
-- scans the output for store path strings to discover these references
-- automatically. The reference graph is what the garbage collector
-- follows - anything reachable from a GC root is kept.
module Nix.Store.Path
( -- * Store directory
StoreDir (..),
defaultStoreDir,
defaultStoreDirText,
platformStoreDir,
platformStoreDirText,
windowsStoreDir,
-- * Store paths
StorePath (spHash, spName),
StoreWriteMode (..),
storePathToFilePath,
storePathToText,
isCanonicalStoreText,
storeTextToFilePath,
parseStorePath,
parseStorePathBaseName,
-- * Name validation
StorePathNameError (..),
StorePathNameReason (..),
checkStorePathName,
validStorePathName,
storePathNameErrorText,
storePathNameReasonText,
-- * Constants
storePathHashLen,
)
where
import Data.Char (isAsciiLower, isAsciiUpper, isDigit)
import Data.Text (Text)
import qualified Data.Text as T
import Nix.Store.Path.Internal (StorePath (..))
import System.FilePath ((</>))
import qualified System.Info
-- | The base directory of the Nix store.
newtype StoreDir = StoreDir {unStoreDir :: FilePath}
deriving (Eq, Show)
-- | Default store directory on Unix: @\/nix\/store@.
defaultStoreDir :: StoreDir
defaultStoreDir = StoreDir "/nix/store"
-- | Default store directory as 'Text', for use in the evaluator.
defaultStoreDirText :: Text
defaultStoreDirText = T.pack (unStoreDir defaultStoreDir)
-- | Platform-appropriate store directory.
-- Returns @C:\\nix\\store@ on Windows, @\/nix\/store@ on Unix.
-- Use this for filesystem operations and operator-facing output.
-- Use 'defaultStoreDir' for identity - hashing, ATerm text, and every
-- eval-visible store-path string (including @builtins.storeDir@).
platformStoreDir :: StoreDir
platformStoreDir = case System.Info.os of
"mingw32" -> windowsStoreDir
_ -> defaultStoreDir
-- | Platform-appropriate store directory as 'Text'.
-- Returns @C:\\nix\\store@ on Windows, @\/nix\/store@ on Unix.
platformStoreDirText :: Text
platformStoreDirText = T.pack (unStoreDir platformStoreDir)
-- | Default store directory on Windows: @C:\\nix\\store@.
windowsStoreDir :: StoreDir
windowsStoreDir = StoreDir "C:\\nix\\store"
-- | Convert a 'StorePath' to a full filesystem path under a 'StoreDir'.
storePathToFilePath :: StoreDir -> StorePath -> FilePath
storePathToFilePath (StoreDir dir) sp =
dir </> T.unpack (spHash sp <> "-" <> spName sp)
-- | Convert a 'StorePath' to canonical 'Text' with forward slashes.
-- Unlike 'storePathToFilePath', this always uses @\/@ as the separator,
-- making it safe for ATerm serialization and cross-platform round-trips.
storePathToText :: StoreDir -> StorePath -> Text
storePathToText (StoreDir dir) sp =
T.pack dir <> "/" <> spHash sp <> "-" <> spName sp
-- | Whether path text lies under the canonical store dir: exactly
-- @\/nix\/store@, or @\/nix\/store@ followed by a separator of either
-- spelling. This is the spelling writers put into eval-visible values.
isCanonicalStoreText :: Text -> Bool
isCanonicalStoreText txt = case T.stripPrefix defaultStoreDirText txt of
Just rest -> case T.uncons rest of
Nothing -> True
Just (c, _) -> c == '/' || c == '\\'
Nothing -> False
-- | Resolve path-value text to the filesystem location it names: text
-- under the canonical store dir resolves into the given store dir, and
-- every other path is taken as written. Writers emit the canonical
-- spelling into eval values, so every reader that performs IO on a path
-- value must resolve through this - on Windows the rooted @\/nix@ prefix
-- would otherwise resolve against the working drive.
--
-- The store dir is a parameter rather than 'platformStoreDir' because a
-- caller may have been given one: resolving reads against the platform
-- default while writes went elsewhere is how @--store@ came to write to
-- one directory and read from another.
storeTextToFilePath :: StoreDir -> Text -> FilePath
storeTextToFilePath storeDir txt
| isCanonicalStoreText txt =
unStoreDir storeDir <> T.unpack (T.drop (T.length defaultStoreDirText) txt)
| otherwise = T.unpack txt
-- | Length of the Nix base-32 hash component in store paths (32 chars).
storePathHashLen :: Int
storePathHashLen = 32
-- | Parse a full store path string like @\/nix\/store\/abc...-name@ into
-- a 'StorePath'. Returns 'Nothing' if the path doesn't match the
-- expected format: store dir prefix + separator + 32-char hash + dash + name.
-- Accepts both @\/@ and @\\@ as the separator after the store dir,
-- so paths round-trip correctly regardless of which OS serialized them.
parseStorePath :: StoreDir -> Text -> Maybe StorePath
parseStorePath (StoreDir dir) path =
let dirText = T.pack dir
tryWithSep sep = T.stripPrefix (dirText <> sep) path >>= parseStorePathBaseName
in case tryWithSep "/" of
Just sp -> Just sp
Nothing -> tryWithSep "\\"
-- | Parse a store path basename like @abc...-name@ - the form narinfo
-- @References@ and @Deriver@ fields carry on the wire - into a
-- 'StorePath': 32 nix-base32 hash chars, dash, valid non-empty name.
--
-- Both components are charset-checked, as upstream does at its parse
-- boundary: parsed text can come from a cache, and an unchecked
-- component (separators, dots, drive colons in the hash or name slot)
-- would later become a filesystem path via 'storePathToFilePath' that
-- escapes the store root.
parseStorePathBaseName :: Text -> Maybe StorePath
parseStorePathBaseName basename
| T.length basename < storePathHashLen + 2 = Nothing
| otherwise =
let hashPart = T.take storePathHashLen basename
afterHash = T.drop storePathHashLen basename
in case T.uncons afterHash of
Just ('-', name)
| T.all isNixBase32Char hashPart && validStorePathName name ->
Just (StorePath hashPart name)
_ -> Nothing
-- | The nix-base32 alphabet: @0-9a-z@ without @e o u t@ (chosen upstream
-- to avoid accidental words). Hash components may contain nothing else.
isNixBase32Char :: Char -> Bool
isNixBase32Char c =
isDigit c
|| (isAsciiLower c && c /= 'e' && c /= 'o' && c /= 'u' && c /= 't')
-- | A rejected store-path name: the name itself plus the first rule it
-- broke, so every boundary reports the same diagnosis.
data StorePathNameError = StorePathNameError
{ -- | The rejected name.
spneName :: !Text,
-- | The rule it broke.
spneReason :: !StorePathNameReason
}
deriving (Eq, Show)
-- | Which path-construction scheme named an eval-time store write:
-- upstream's text-path scheme (@builtins.toFile@), a recursive
-- fixed-output path (source copies, @builtins.path@), or a flat
-- fixed-output path (@builtins.fetchurl@). Materialization re-derives
-- the path from on-disk content under the same scheme before
-- registering, so a tree that does not reproduce its path is refused
-- instead of registered valid under a hash its bytes do not have.
data StoreWriteMode = WriteText | WriteRecursive | WriteFlat
deriving (Eq, Show)
-- | The store-path name rules, one constructor per rule.
data StorePathNameReason
= -- | The name is empty.
NameEmpty
| -- | The name exceeds 'maxStorePathNameLen'; carries the actual length.
NameTooLong !Int
| -- | The name contains a character outside @[A-Za-z0-9+._?=-]@.
NameIllegalChar !Char
| -- | The first dash-separated component is the carried @.@ or @..@,
-- which would name a dot segment on disk.
NameDotSegment !Text
deriving (Eq, Show)
-- | Upstream's store path name rules (its checkName boundary):
-- 1-211 characters from @[A-Za-z0-9+._?=-]@, and the first dash-separated
-- component may not be @.@ or @..@. One rule rejects the traversal names
-- and their @.-@ / @..-@ prefixed forms alike, while other dot-leading
-- names (@.config-1.0@) stay valid.
--
-- Enforced at BOTH boundaries: parse ('parseStorePathBaseName') and
-- construction (the @makeStorePath@ family in "Nix.Hash"), so an unclean
-- name cannot become a 'StorePath' from either side - in particular, no
-- write sink can be handed a path that resolves outside the store root.
checkStorePathName :: Text -> Either StorePathNameError ()
checkStorePathName name
| T.null name = broke NameEmpty
| T.length name > maxStorePathNameLen = broke (NameTooLong (T.length name))
| firstDashComponent == "." || firstDashComponent == ".." = broke (NameDotSegment firstDashComponent)
| Just c <- T.find (not . isStorePathNameChar) name = broke (NameIllegalChar c)
| otherwise = Right ()
where
broke = Left . StorePathNameError name
firstDashComponent = T.takeWhile (/= '-') name
isStorePathNameChar c =
isAsciiLower c
|| isAsciiUpper c
|| isDigit c
|| c == '+'
|| c == '.'
|| c == '_'
|| c == '?'
|| c == '='
|| c == '-'
-- | Boolean form of 'checkStorePathName', for the parse boundary.
validStorePathName :: Text -> Bool
validStorePathName = either (const False) (const True) . checkStorePathName
-- | Render a rejection as @invalid store path name '<name>': <rule>@.
storePathNameErrorText :: StorePathNameError -> Text
storePathNameErrorText (StorePathNameError name reason) =
"invalid store path name '" <> name <> "': " <> storePathNameReasonText reason
-- | Render just the broken rule, for callers that frame the name
-- themselves (e.g. @invalid derivation output name@).
storePathNameReasonText :: StorePathNameReason -> Text
storePathNameReasonText reason = case reason of
NameEmpty -> "the name is empty"
NameTooLong len ->
"the name is "
<> T.pack (show len)
<> " characters, above the "
<> T.pack (show maxStorePathNameLen)
<> " maximum"
NameIllegalChar c -> "contains the illegal character " <> T.pack (show c)
NameDotSegment seg -> "the first dash-separated component may not be '" <> seg <> "'"
-- | Upstream's maximum store path name length.
maxStorePathNameLen :: Int
maxStorePathNameLen = 211