packages feed

nova-nix-0.9.0.0: src/Nix/Environment.hs

{-# LANGUAGE CPP #-}

-- | The process environment, read the way a Nix string holds it: as bytes.
--
-- Upstream reads a variable with @getenv@ and keeps the bytes it returns
-- (@getEnv@ in src\/libutil\/environment-variables.cc at Nix 2.24.9), so
-- a value reaches @builtins.getEnv@ and @builtins.nixPath@ unchanged
-- whatever its encoding.  On POSIX this module makes the same call.
-- 'System.Environment.lookupEnv' does not: it decodes the value with the
-- file-system encoding, which carries an undecodable byte as a lone
-- surrogate that 'Data.Text.pack' then replaces with U+FFFD, and it
-- encodes the name with the foreign encoding, which drops a character
-- the locale cannot spell instead of failing.
--
-- A Windows environment is UTF-16.  A value becomes the UTF-8 bytes a
-- Nix string carries, and a name is read as UTF-8.  An unpaired
-- surrogate has no UTF-8 form, so a value holding one is refused rather
-- than approximated, the rule the NAR serialiser applies to such a file
-- name.
module Nix.Environment
  ( EnvLookup (..),
    lookupEnvBytes,
  )
where

import Data.ByteString (ByteString)
#if defined(mingw32_HOST_OS)
import Data.Char (GeneralCategory (Surrogate), generalCategory)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import System.Environment (lookupEnv)
#else
import qualified System.Posix.Env.ByteString as PosixEnv
#endif

-- | What the environment holds under one name.
data EnvLookup
  = -- | No variable has the name.
    EnvUnset
  | -- | The value, as the bytes a Nix string carries.
    EnvValue !ByteString
  | -- | A Windows value holding an unpaired UTF-16 surrogate, which has no
    -- UTF-8 form.  Never the answer on POSIX, where a value is bytes.
    EnvUnpairedSurrogate
  deriving (Eq, Show)

-- | Look up a variable by the bytes of its name.  Nothing is caught: the
-- read cannot fail other than by finding nothing, since @getenv@ answers
-- NULL for a missing name and base's 'System.Environment.lookupEnv'
-- answers 'Nothing' whenever @GetEnvironmentVariableW@ finds no value.
lookupEnvBytes :: ByteString -> IO EnvLookup
#if defined(mingw32_HOST_OS)
lookupEnvBytes name = case TE.decodeUtf8' name of
  -- A Windows name is UTF-16, so bytes with no UTF-8 reading name no
  -- variable.
  Left _ -> pure EnvUnset
  Right decoded -> maybe EnvUnset utf8Value <$> lookupEnv (T.unpack decoded)
  where
    -- base decodes a surrogate pair into one character and keeps an
    -- unpaired surrogate as its own code point, which 'T.pack' would
    -- replace with U+FFFD.
    utf8Value value
      | any ((== Surrogate) . generalCategory) value = EnvUnpairedSurrogate
      | otherwise = EnvValue (TE.encodeUtf8 (T.pack value))
#else
lookupEnvBytes name = maybe EnvUnset EnvValue <$> PosixEnv.getEnv name
#endif