packages feed

nova-cache-0.4.0.0: src/NovaCache/StorePath.hs

-- | Nix store path parsing, rendering, and validation.
--
-- A store path has the form @\/nix\/store\/\<hash\>-\<name\>@ where the hash
-- is a 32-character nix-base32 string and the name contains only characters
-- from a restricted set: alphanumeric plus @-._+?=@.
module NovaCache.StorePath
  ( StoreDir (..),
    StorePath (..),
    StorePathHash (..),
    StorePathName (..),
    parseStorePath,
    renderStorePath,
    storePathHashString,
    storePathBaseName,
    defaultStoreDir,
  )
where

import Data.Char (isAlphaNum)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import NovaCache.Base32 (isBase32Char)

-- ---------------------------------------------------------------------------
-- Types
-- ---------------------------------------------------------------------------

-- | Root directory of the Nix store.
newtype StoreDir = StoreDir FilePath
  deriving (Eq, Ord, Show)

-- | A parsed store path: hash prefix and validated name.
data StorePath = StorePath
  { spHash :: !StorePathHash,
    spName :: !StorePathName
  }
  deriving (Eq, Ord, Show)

-- | The 32-character nix-base32 hash prefix of a store path.
newtype StorePathHash = StorePathHash Text
  deriving (Eq, Ord, Show)

-- | The validated name component of a store path.
newtype StorePathName = StorePathName Text
  deriving (Eq, Ord, Show)

-- ---------------------------------------------------------------------------
-- Constants
-- ---------------------------------------------------------------------------

-- | The standard Nix store directory.
defaultStoreDir :: StoreDir
defaultStoreDir = StoreDir defaultStoreDirPath

-- | Default store directory path.
defaultStoreDirPath :: FilePath
defaultStoreDirPath = "/nix/store"

-- | Length of the nix-base32 hash in a store path.
storePathHashLen :: Int
storePathHashLen = 32

-- | Minimum basename length: hash + separator.
minBaseNameLen :: Int
minBaseNameLen = storePathHashLen + 1

-- | The separator between hash and name in a store path.
hashNameSeparator :: Char
hashNameSeparator = '-'

-- ---------------------------------------------------------------------------
-- Validation
-- ---------------------------------------------------------------------------

-- | Characters allowed in the name component of a store path.
--
-- Alphanumeric plus @-._+?=@, matching the Nix specification.
validNameChar :: Char -> Bool
validNameChar c =
  isAlphaNum c
    || c == '-'
    || c == '_'
    || c == '.'
    || c == '+'
    || c == '?'
    || c == '='

-- ---------------------------------------------------------------------------
-- Parsing
-- ---------------------------------------------------------------------------

-- | Parse a store path from a full path or bare basename.
--
-- Accepts @\/nix\/store\/\<hash\>-\<name\>@ or just @\<hash\>-\<name\>@.
parseStorePath :: StoreDir -> Text -> Either String StorePath
parseStorePath (StoreDir dir) txt =
  parseBaseName (stripDirPrefix dir txt)

-- | Strip the store directory prefix if present.
stripDirPrefix :: FilePath -> Text -> Text
stripDirPrefix dir txt =
  fromMaybe txt (T.stripPrefix (T.pack dir <> "/") txt)

-- | Parse a @\<hash\>-\<name\>@ basename into a 'StorePath'.
parseBaseName :: Text -> Either String StorePath
parseBaseName basename
  | T.length basename < minBaseNameLen =
      Left ("store path too short: " ++ T.unpack basename)
  | T.index basename storePathHashLen /= hashNameSeparator =
      Left ("expected '-' after hash in store path: " ++ T.unpack basename)
  | not (T.all isBase32Char hashPart) =
      Left ("invalid nix-base32 hash in store path: " ++ T.unpack hashPart)
  | T.null name =
      Left ("empty name in store path: " ++ T.unpack basename)
  | not (T.all validNameChar name) =
      Left ("invalid characters in store path name: " ++ T.unpack name)
  | otherwise =
      Right StorePath {spHash = StorePathHash hashPart, spName = StorePathName name}
  where
    hashPart = T.take storePathHashLen basename
    name = T.drop minBaseNameLen basename

-- ---------------------------------------------------------------------------
-- Rendering
-- ---------------------------------------------------------------------------

-- | Render a store path as a full path under the given store directory.
renderStorePath :: StoreDir -> StorePath -> Text
renderStorePath (StoreDir dir) sp =
  T.pack dir <> "/" <> storePathBaseName sp

-- | Extract the 32-character nix-base32 hash string.
storePathHashString :: StorePath -> Text
storePathHashString (StorePath (StorePathHash h) _) = h

-- | Render the @\<hash\>-\<name\>@ basename.
storePathBaseName :: StorePath -> Text
storePathBaseName (StorePath (StorePathHash h) (StorePathName n)) =
  h <> T.singleton hashNameSeparator <> n