nova-nix-0.8.0.0: src/Nix/Config.hs
-- | Nix configuration: the @nix.conf@ / @NIX_CONFIG@ settings that decide
-- which binary caches a machine substitutes from and which public keys it
-- trusts.
--
-- == The format
--
-- One @name = value@ assignment per line. A @#@ truncates the rest of the
-- line (a comment). A list value is whitespace separated. These are the
-- rules upstream's @parseConfigFiles@ applies (comment truncation, no line
-- continuation, whitespace tokenizing), matched here.
--
-- == Precedence
--
-- Sources are folded weakest first, so a later source overrides an earlier
-- one. A plain assignment REPLACES the accumulated value; an @extra-@
-- prefixed assignment APPENDS to it. The caller supplies the sources in
-- order (built-in default, then files, then @NIX_CONFIG@, then the command
-- line), so the command line wins, exactly as upstream orders them.
--
-- == Security
--
-- @trusted-public-keys@ decides which signatures a substituted path is
-- accepted under. The precedence is therefore load bearing: a source that
-- REPLACES the trusted set where it should APPEND, or an @extra-@ that is
-- mishandled, silently widens what the machine trusts. The fold here is
-- pure and total so the whole rule set can be tested directly.
--
-- Not yet modelled (tracked separately): @include@ \/ @!include@
-- directives, the @\/etc\/nix@ system file and the @XDG_CONFIG_DIRS@
-- cascade. A line opening with @include@ is refused loudly rather than
-- silently skipped, so a config that depends on one fails visibly.
module Nix.Config
( -- * Resolved settings
NixConfig (..),
defaultNixConfig,
-- * Parsing and folding
ConfigAssignment (..),
parseConfigText,
applyAssignment,
applyConfigText,
resolveConfig,
)
where
import Data.Text (Text)
import qualified Data.Text as T
-- ---------------------------------------------------------------------------
-- Settings
-- ---------------------------------------------------------------------------
-- | The subset of Nix settings that this layer resolves: the binary caches
-- to try and the public keys their signatures are trusted under. Both are
-- ordered lists (upstream's @substituters@ is a @Strings@, and
-- @trusted-public-keys@ is too); order is preserved on write, though it
-- does not affect key acceptance (any trusted key is enough).
data NixConfig = NixConfig
{ ncSubstituters :: ![Text],
ncTrustedPublicKeys :: ![Text]
}
deriving (Eq, Show)
-- | The baseline the fold starts from: no substituters and no trusted
-- keys. This is nova-nix's existing default (nothing is substituted
-- unless configured), a deliberate divergence from upstream's
-- cache.nixos.org default: turning a cache on for every machine is the
-- operator's decision to make in a config file, not a built-in.
defaultNixConfig :: NixConfig
defaultNixConfig = NixConfig {ncSubstituters = [], ncTrustedPublicKeys = []}
-- ---------------------------------------------------------------------------
-- Parsing
-- ---------------------------------------------------------------------------
-- | One parsed @name = value@ assignment, before its name is resolved
-- against the known settings and aliases.
data ConfigAssignment = ConfigAssignment
{ caName :: !Text,
caValue :: !Text
}
deriving (Eq, Show)
-- | Parse config text into ordered assignments. Comments and blank lines
-- drop out; a malformed line (fewer than @name = value@, or a missing
-- @=@) is a loud error rather than a silent skip, matching upstream's
-- @UsageError@ - a typo in a security-relevant file must not pass for an
-- empty setting. An @include@ \/ @!include@ line is refused as not yet
-- supported.
parseConfigText :: Text -> Either Text [ConfigAssignment]
parseConfigText = traverse parseLine . filter (not . isBlank) . map stripComment . T.lines
where
isBlank line = null (T.words line)
stripComment = T.takeWhile (/= '#')
parseLine line = case T.words line of
(directive : _)
| directive == includeDirective || directive == bangIncludeDirective ->
Left ("nix.conf: '" <> directive <> "' is not supported yet")
(name : eq : valueTokens)
| eq == assignEq -> Right (ConfigAssignment name (T.unwords valueTokens))
_ -> Left ("nix.conf: syntax error in line '" <> T.strip line <> "'")
-- ---------------------------------------------------------------------------
-- Applying
-- ---------------------------------------------------------------------------
-- | Apply one assignment to the accumulated config. The name is resolved
-- through the aliases (@binary-caches@ -> @substituters@,
-- @binary-cache-public-keys@ -> @trusted-public-keys@) and the @extra-@
-- prefix (append rather than replace). A name outside the known set is
-- ignored, not an error: upstream warns and continues, and refusing every
-- unknown key would reject a config that also carries settings this layer
-- does not model yet.
applyAssignment :: NixConfig -> ConfigAssignment -> NixConfig
applyAssignment config (ConfigAssignment name value) =
case resolveName name of
Nothing -> config
Just (field, mode) ->
let parsed = T.words value
in setField field (combine mode (getField field config) parsed) config
where
combine ReplaceMode _ new = new
combine AppendMode old new = old ++ new
-- | Which list a setting name targets, once the @extra-@ prefix and the
-- aliases are resolved, and whether it replaces or appends.
resolveName :: Text -> Maybe (ConfigField, ApplyMode)
resolveName name =
case T.stripPrefix extraPrefix name of
Just base -> tag AppendMode (baseField base)
Nothing -> tag ReplaceMode (baseField name)
where
tag mode mField = case mField of
Just field -> Just (field, mode)
Nothing -> Nothing
baseField n
| n == substitutersKey || n == substitutersAlias = Just SubstitutersField
| n == trustedKeysKey || n == trustedKeysAlias = Just TrustedKeysField
| otherwise = Nothing
-- | The two list settings this layer resolves.
data ConfigField = SubstitutersField | TrustedKeysField
deriving (Eq, Show)
-- | Whether an assignment replaces the accumulated value or appends to it.
data ApplyMode = ReplaceMode | AppendMode
deriving (Eq, Show)
getField :: ConfigField -> NixConfig -> [Text]
getField SubstitutersField = ncSubstituters
getField TrustedKeysField = ncTrustedPublicKeys
setField :: ConfigField -> [Text] -> NixConfig -> NixConfig
setField SubstitutersField v config = config {ncSubstituters = v}
setField TrustedKeysField v config = config {ncTrustedPublicKeys = v}
-- | Parse and apply one source's text onto the accumulated config.
applyConfigText :: NixConfig -> Text -> Either Text NixConfig
applyConfigText config text = foldl applyAssignment config <$> parseConfigText text
-- | Fold a list of sources onto the default config, weakest first. The
-- caller orders the list so the command line is last (and so wins); a
-- parse error in any source aborts, since a security-relevant file that
-- does not parse must not be treated as absent.
resolveConfig :: [Text] -> Either Text NixConfig
resolveConfig = foldl step (Right defaultNixConfig)
where
step acc source = acc >>= \config -> applyConfigText config source
-- ---------------------------------------------------------------------------
-- Setting names
-- ---------------------------------------------------------------------------
substitutersKey, substitutersAlias :: Text
substitutersKey = "substituters"
substitutersAlias = "binary-caches"
trustedKeysKey, trustedKeysAlias :: Text
trustedKeysKey = "trusted-public-keys"
trustedKeysAlias = "binary-cache-public-keys"
extraPrefix :: Text
extraPrefix = "extra-"
assignEq :: Text
assignEq = "="
includeDirective, bangIncludeDirective :: Text
includeDirective = "include"
bangIncludeDirective = "!include"