packages feed

nova-nix-0.6.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 (..),
    storePathToFilePath,
    storePathToText,
    parseStorePath,

    -- * Constants
    storePathHashLen,
  )
where

import Data.Text (Text)
import qualified Data.Text as T
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 user-facing output.
-- Use 'defaultStoreDir' only for Nix-internal canonical paths (ATerm hashing).
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.
-- Used for user-facing values like @builtins.storeDir@.
platformStoreDirText :: Text
platformStoreDirText = T.pack (unStoreDir platformStoreDir)

-- | Default store directory on Windows: @C:\\nix\\store@.
windowsStoreDir :: StoreDir
windowsStoreDir = StoreDir "C:\\nix\\store"

-- | A parsed store path: the hash and name components.
data StorePath = StorePath
  { -- | The 32-character Nix base-32 hash.
    spHash :: !Text,
    -- | The human-readable name (e.g. @hello-2.12.1@).
    spName :: !Text
  }
  deriving (Eq, Ord, Show)

-- | 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

-- | 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 >>= parseRest
   in case tryWithSep "/" of
        Just sp -> Just sp
        Nothing -> tryWithSep "\\"
  where
    parseRest rest
      | T.length rest < storePathHashLen + 2 = Nothing
      | otherwise =
          let hashPart = T.take storePathHashLen rest
              afterHash = T.drop storePathHashLen rest
           in case T.uncons afterHash of
                Just ('-', name)
                  | not (T.null name) -> Just (StorePath hashPart name)
                _ -> Nothing