packages feed

hnix-store-core-0.9.0.0: src/System/Nix/ContentAddress.hs

{-# LANGUAGE OverloadedStrings #-}

module System.Nix.ContentAddress (
    ContentAddress (..)
  , methodToText
  , textToMethod
  , ContentAddressMethod (..)
  , methodAlgoBuilder
  , methodAlgoParser
  , contentAddressBuilder
  , contentAddressParser
  , buildContentAddress
  , parseContentAddress
  ) where

import Control.Applicative
import Control.DeepSeq (NFData)
import Crypto.Hash (Digest)
import Data.Attoparsec.Text (Parser)
import Data.Attoparsec.Text qualified
import Data.Dependent.Sum (DSum)
import Data.Some (Some(..))
import Data.Text (Text)
import Data.Text qualified
import Data.Text.Lazy qualified
import Data.Text.Lazy.Builder (Builder)
import Data.Text.Lazy.Builder qualified
import GHC.Generics (Generic)

import System.Nix.Hash (HashAlgo)
import System.Nix.Hash qualified

data ContentAddressMethod
  = ContentAddressMethod_Flat
  | ContentAddressMethod_NixArchive
  | ContentAddressMethod_Text
  -- ^ The path is a plain file added via makeTextPath or
  -- addTextToStore. It is addressed according to a sha256sum of the
  -- file contents.
  deriving (Eq, Generic, Ord, Show)

methodToText :: ContentAddressMethod -> Text
methodToText = \case
  ContentAddressMethod_Flat -> "flat"
  ContentAddressMethod_NixArchive -> "nar"
  ContentAddressMethod_Text -> "text"

textToMethod :: Text -> Either String ContentAddressMethod
textToMethod = \case
  "flat" -> Right ContentAddressMethod_Flat
  "nar" -> Right ContentAddressMethod_NixArchive
  "text" -> Right ContentAddressMethod_Text
  name -> Left $ "Unknown store object content-addressing method " <> Data.Text.unpack name

instance NFData ContentAddressMethod

-- | 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 ContentAddress = ContentAddress
  ContentAddressMethod
  (DSum HashAlgo Digest)
  deriving (Eq, Generic, Ord, Show)

-- | Marshall `ContentAddressableAddress` to `Text`
-- in form suitable for remote protocol usage.
buildContentAddress :: ContentAddress -> Text
buildContentAddress =
  Data.Text.Lazy.toStrict
  . Data.Text.Lazy.Builder.toLazyText
  . contentAddressBuilder

-- | Render the method prefix used in content address strings.
--
-- @"text:"@, @"fixed:"@ (flat), @"fixed:r:"@ (nar)
methodPrefix :: ContentAddressMethod -> Builder
methodPrefix = \case
  ContentAddressMethod_Text -> "text:"
  ContentAddressMethod_Flat -> "fixed:"
  ContentAddressMethod_NixArchive -> "fixed:r:"

-- | Render method and hash algorithm, e.g. @"fixed:r:sha256"@.
--
-- Used in the daemon wire protocol for @AddToStore@ (>= 1.25).
methodAlgoBuilder :: ContentAddressMethod -> Some HashAlgo -> Builder
methodAlgoBuilder method (Some hashAlgo) =
  methodPrefix method <> Data.Text.Lazy.Builder.fromText (System.Nix.Hash.algoToText hashAlgo)

-- | Parse method and hash algorithm from wire format.
methodAlgoParser :: Parser (ContentAddressMethod, Some HashAlgo)
methodAlgoParser = do
  method <- parseContentAddressMethod
  _ <- ":"
  hashAlgoText <- "sha256" <|> "sha512" <|> "sha1" <|> "md5"
  case System.Nix.Hash.textToAlgo hashAlgoText of
    Left e -> fail e
    Right algo -> pure (method, algo)

contentAddressBuilder :: ContentAddress -> Builder
contentAddressBuilder (ContentAddress method digest) =
  methodPrefix method <> System.Nix.Hash.algoDigestBuilder digest

-- | Parse `ContentAddressableAddress` from `ByteString`
parseContentAddress
  :: Text -> Either String ContentAddress
parseContentAddress =
  Data.Attoparsec.Text.parseOnly contentAddressParser

-- | Parser for content addressable field
contentAddressParser :: Parser ContentAddress
contentAddressParser = do
  method <- parseContentAddressMethod
  _ <- ":"
  digest <- parseTypedDigest
  case digest of
    Left e -> fail e
    Right x -> return $ ContentAddress method x

parseContentAddressMethod :: Parser ContentAddressMethod
parseContentAddressMethod =
      (ContentAddressMethod_Text <$ "text")
  <|> (ContentAddressMethod_NixArchive <$ "fixed:r")
  <|> (ContentAddressMethod_Flat <$ "fixed")

parseTypedDigest :: Parser (Either String (DSum HashAlgo Digest))
parseTypedDigest = System.Nix.Hash.mkNamedDigest <$> parseHashType <*> parseHash
  where
    parseHashType :: Parser Text
    parseHashType =
      ("sha256" <|> "sha512" <|> "sha1" <|> "md5") <* (":" <|> "-")

    parseHash :: Parser Text
    parseHash = Data.Attoparsec.Text.takeWhile1 (/= ':')