Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 9 additions & 19 deletions src/Nix/Eval.hs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ import Nix.Eval.CBytecode (cbcArg1, cbcArg2, cbcArg3, cbcCountedPayload, cbcData
import Nix.Eval.CEnv (cenvPushWith)
import Nix.Eval.CList (CList (..), clistGet)
import Nix.Eval.CThunk (CThunkPtr)
import Nix.Eval.CanonPath (canonBaseName, canonDirName, canonPath)
import Nix.Eval.CanonPath (canonBaseName, canonDirName, canonPathValue)
import Nix.Eval.Compile (BcAttrKey (..), BcBinding (..), compileExpr, decodeBcBindings, decodeBcCaptureInfo, decodeBcFormals, reassembleDouble, reassembleInt64)
import Nix.Eval.Context (extractAllOutputRefs, extractInputDrvs, extractInputSrcs, plainContext)
import Nix.Eval.Operator (checkedAdd, checkedMul, checkedSub, evalBinary, evalUnary, nixCompare, nixEqual)
Expand Down Expand Up @@ -360,7 +360,7 @@ evalAddWithCoercion left right = case (left, right) of
-- Path + path: text concatenation, canonicalized - the joined spelling
-- (dot segments, doubled separators) never survives into the value, as
-- upstream (CanonPath on the concatenated text).
(VPath a, VPath b) -> pure (VPath (canonPath (a <> b)))
(VPath a, VPath b) -> pure (VPath (canonPathValue (a <> b)))
-- Path + coercible: the result stays a path, the right side coerces
-- WITHOUT a store copy, and a right side carrying string context is an
-- error, as upstream (a store-path reference cannot survive inside a
Expand All @@ -371,7 +371,7 @@ evalAddWithCoercion left right = case (left, right) of
if rightCtx == emptyContext
then do
appended <- decodedText "path concatenation" rightStr
pure (VPath (canonPath (a <> appended)))
pure (VPath (canonPathValue (a <> appended)))
else throwEvalError "cannot append a string with context (a store-path reference) to a path"
-- Strings: direct concat.
(VStr {}, VStr {}) -> evalBinary force OpAdd left right
Expand Down Expand Up @@ -3215,7 +3215,7 @@ builtinToPath (VStr rawBytes _) = do
case T.uncons s of
Nothing -> throwEvalError "builtins.toPath: empty path"
-- Canonicalized like every other path production site, as upstream.
Just ('/', _) -> pure (VPath (canonPath s))
Just ('/', _) -> pure (VPath (canonPathValue s))
Just _ -> throwEvalError ("builtins.toPath: path must be absolute, got " <> s)
builtinToPath other =
throwEvalError ("builtins.toPath: expected a string or path, got " <> typeName other)
Expand Down Expand Up @@ -3309,14 +3309,14 @@ findFirst [] name =
findFirst ((prefix, path) : rest) name
| prefix == name || (not (T.null prefix) && (prefix <> "/") `T.isPrefixOf` name) =
let suffix = if prefix == name then "" else T.drop (T.length prefix + 1) name
candidate = canonPath (if T.null suffix then path else path <> "/" <> suffix)
candidate = canonPathValue (if T.null suffix then path else path <> "/" <> suffix)
in do
exists <- doesPathExist candidate
if exists
then pure (VPath candidate)
else findFirst rest name
| T.null prefix =
let candidate = canonPath (path <> "/" <> name)
let candidate = canonPathValue (path <> "/" <> name)
in do
exists <- doesPathExist candidate
if exists
Expand Down Expand Up @@ -3494,7 +3494,7 @@ builtinFetchGit (VStr rawUrl _) = do
(code, _, errOut) <-
runProcess "git" (gitTransportConfig ++ ["clone", "--depth", "1", "--", allowedUrl, cloneDir]) ""
case code of
0 -> pure (VPath cloneDir)
0 -> pure (VPath (canonPathValue cloneDir))
_ -> do
removeScratchDir cloneDir
throwEvalError ("builtins.fetchGit: git clone failed: " <> errOut)
Expand Down Expand Up @@ -4849,7 +4849,7 @@ builtinPath (VAttrs attrs) = do
pinText <- decodedText "builtins.path" pin
Just <$> decodeSha256Pin "builtins.path" pinText
other -> throwEvalError ("builtins.path: 'sha256' must be a string, got " <> typeName other)
let name = fromMaybe (extractBaseName pathStr) nameOverride
let name = fromMaybe (canonBaseName pathStr) nameOverride
pinSubject = "builtins.path: " <> pathStr
storePathText <- case attrSetLookup "filter" attrs of
Nothing -> copyPathToStore pathStr name (fmap (pinSubject,) expectedDigest)
Expand Down Expand Up @@ -4919,16 +4919,6 @@ filteredSourceNar filterFn rootPath = do
VBool b -> pure b
other -> throwEvalError ("builtins.path: the filter function must return a Boolean, got " <> typeName other)

-- | Extract the last path component from a path string.
extractBaseName :: Text -> Text
extractBaseName path =
let stripped = T.dropWhileEnd (\c -> c == '/' || c == '\\') path
in case T.breakOnEnd "/" stripped of
("", _) -> case T.breakOnEnd "\\" stripped of
("", _) -> stripped
(_, name) -> name
(_, name) -> name

-- ---------------------------------------------------------------------------
-- Builtin implementations - filterSource
-- ---------------------------------------------------------------------------
Expand All @@ -4948,7 +4938,7 @@ builtinFilterSource _ other =
filterSourceInto :: (MonadEval m) => NixValue -> Text -> m NixValue
filterSourceInto filterFn path = do
narBytes <- filteredSourceNar filterFn path
storePathText <- addSourceNar (extractBaseName path) narBytes
storePathText <- addSourceNar (canonBaseName path) narBytes
pure (sourceResultString storePathText)

-- ---------------------------------------------------------------------------
Expand Down
23 changes: 23 additions & 0 deletions src/Nix/Eval/CanonPath.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
-- and a fully-collapsed relative path is @.@.
module Nix.Eval.CanonPath
( canonPath,
canonPathValue,
canonBaseName,
canonDirName,
)
Expand All @@ -31,6 +32,28 @@ import Data.Text (Text)
import qualified Data.Text as T
import System.FilePath (isPathSeparator, pathSeparator)

-- | The producer gate for path VALUES: a path value's text is its
-- absolute path spelled with forward slashes; on Windows a drive
-- designator precedes the root. Platform spelling exists only at the
-- filesystem boundary. (The same split git's object model makes: tree
-- identity is slash-canonical, the working-tree boundary converts.)
--
-- Folding is by 'isPathSeparator', so it is platform-correct with no
-- conditional: on POSIX a backslash is an ordinary file-name character
-- and passes through untouched - upstream's semantics - while on
-- Windows it is a separator and folds to @/@. Every site that
-- produces a 'VPath' from platform-tainted text (literal resolution,
-- base-dir joins, fetcher scratch dirs, search-path entries) goes
-- through this gate; store-path text is canonical by construction.
canonPathValue :: Text -> Text
canonPathValue t = canonPath (if T.any needsFold t then T.map foldSeparator t else t)
where
-- Copy only when a non-'/' separator is present: on POSIX
-- 'isPathSeparator' is '/' alone, so this is never, and the
-- common already-canonical path shares its text on Windows too.
needsFold c = isPathSeparator c && c /= '/'
foldSeparator c = if needsFold c then '/' else c

-- | Canonicalize a path's text form. See the module comment for the
-- algorithm and the separator-preservation guarantee.
canonPath :: Text -> Text
Expand Down
13 changes: 6 additions & 7 deletions src/Nix/Eval/IO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import Nix.Derivation (fromATerm)
import Nix.Eval (eval)
import Nix.Eval.CList (CList (..))
import Nix.Eval.CThunk (CThunkPtr, cthunkGetAttrs, cthunkGetBcIdx, cthunkGetBool, cthunkGetCtxStr, cthunkGetFloat, cthunkGetInt, cthunkGetLambda, cthunkGetList, cthunkGetPath, cthunkGetStr, cthunkMarkBlackhole, cthunkMarkPending, cthunkPayload, cthunkSetComputed, cthunkSetComputedAttrs, cthunkSetComputedBool, cthunkSetComputedCtxStr, cthunkSetComputedFloat, cthunkSetComputedInt, cthunkSetComputedLambda, cthunkSetComputedList, cthunkSetComputedNull, cthunkSetComputedPath, cthunkSetComputedStr, cthunkState, cthunkValueTag)
import Nix.Eval.CanonPath (canonBaseName, canonPath)
import Nix.Eval.CanonPath (canonBaseName, canonPath, canonPathValue)
import Nix.Eval.Symbol (Symbol (..), symbolBytes, symbolIntern, symbolInternBytes, symbolText)
import Nix.Eval.Types (AttrSet (..), Env (..), MonadEval (..), NixValue (..), Thunk (..), attrSetSize, emptyContext, marshalLambda, marshalStringContext, storePathOrThrow, unmarshalLambdaValue, unmarshalStringContext, pattern ValueAttrs, pattern ValueBool, pattern ValueCtxStr, pattern ValueFloat, pattern ValueInt, pattern ValueLambda, pattern ValueList, pattern ValueNull, pattern ValuePath, pattern ValueStr)
import Nix.Expr.Types (AttrKey (..), Binding (..), Expr (..), Formal (..), Formals (..), NixAtom (..), StringPart (..))
Expand Down Expand Up @@ -425,18 +425,17 @@ instance MonadEval EvalIO where
baseDir <- EvalIO (asks esBaseDir)
-- ~/x resolves against the home directory (upstream lexes HPATH and
-- expands it at eval); everything else relative joins the base dir.
-- Both end in lexical canonicalization: no dot segment or repeated
-- separator survives into the path value. Native separators are
-- PRESERVED (CanonPath's style rule), so a native base dir yields a
-- native-spelled value - path-value consumers split separator-aware
-- ('canonBaseName' \/ 'canonDirName'), never on '/' alone.
-- Both end at the producer gate ('canonPathValue'): the value is
-- absolute, lexically canonical, and slash-spelled regardless of
-- the base dir's native spelling - platform separators exist only
-- at the filesystem boundary.
expanded <- case T.stripPrefix "~/" path of
Just below -> do
home <- wrapIO Dir.getHomeDirectory
pure (home </> T.unpack below)
Nothing -> pure (T.unpack path)
let absolute = if isRelative expanded then baseDir </> expanded else expanded
pure (canonPath (T.pack absolute))
pure (canonPathValue (T.pack absolute))

forceThunk evalFn (Thunk ptr) = do
-- Force protocol: PENDING to BLACKHOLE to COMPUTED with memoization.
Expand Down
4 changes: 2 additions & 2 deletions src/Nix/Eval/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ import Nix.Eval.CEnv (NnEnv, cenvAllocSlots, cenvAllocWithScopes, cenvEmpty, cen
import Nix.Eval.CLambda (clambdaAllowExtra, clambdaBody, clambdaEntryDefault, clambdaEntryHasDefault, clambdaEntryName, clambdaEnv, clambdaFormalCount, clambdaFormalsType, clambdaNameSym, clambdaNew, clambdaSetEntry)
import Nix.Eval.CList (CList (..), clistFromThunks, clistLen, clistThunks, emptyCList)
import Nix.Eval.CThunk (CThunkPtr, cthunkGetAttrs, cthunkGetBcIdx, cthunkGetBool, cthunkGetCtxStr, cthunkGetFloat, cthunkGetInt, cthunkGetList, cthunkGetPath, cthunkGetStr, cthunkNewBc, cthunkNewComputed, cthunkNewComputedAttrs, cthunkNewComputedBool, cthunkNewComputedCtxStr, cthunkNewComputedFloat, cthunkNewComputedInt, cthunkNewComputedLambda, cthunkNewComputedList, cthunkNewComputedNull, cthunkNewComputedPath, cthunkNewComputedStr, cthunkPayload, cthunkState, cthunkValueTag)
import Nix.Eval.CanonPath (canonPath)
import Nix.Eval.CanonPath (canonPathValue)
import Nix.Eval.Compile (compileExpr, compileFormalsToEval)
import Nix.Eval.EvalFormals (EvalFormal (..), EvalFormals (..))
import Nix.Eval.Symbol (Symbol (..), symbolBytes, symbolIntern, symbolInternBytes, symbolText)
Expand Down Expand Up @@ -1318,7 +1318,7 @@ instance MonadEval PureEval where
-- Pure eval cannot read files: a path coerces to itself (no store copy);
-- the real copy-to-store happens only under 'EvalIO'.
storeSourcePath = pure
resolvePathLiteral = pure . canonPath
resolvePathLiteral = pure . canonPathValue
forceThunk evalFn (Thunk ptr) =
-- Read the C thunk via unsafePerformIO - safe because reads are
-- idempotent and PureEval never writes back (no memoization).
Expand Down
13 changes: 13 additions & 0 deletions src/Nix/Store/DB.hs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ registerPaths db regs = withTransaction (sdbConn db) $ do
-- | Insert (or refresh) a single ValidPaths row.
insertPathRow :: StoreDB -> PathRegistration -> IO ()
insertPathRow db reg = do
-- DB rows key store paths in PLATFORM spelling (storePathToFilePath):
-- the database is host-local state describing this host's store tree,
-- every writer and reader in this module uses the same spelling, and
-- the store dir itself is host configuration. Identity artifacts
-- (drv ATerm, narinfo, eval-visible store-path strings) spell
-- canonically; the DB is deliberately not one of them.
let pathText = T.pack (storePathToFilePath (sdbDir db) (prPath reg))
execute
(sdbConn db)
Expand Down Expand Up @@ -255,6 +261,13 @@ isValidPath db sp = do

-- | Query the references of a registered store path.
-- Returns the full path strings of referenced store paths.
--
-- Reads return the stored text without re-parsing: every row was
-- written from a validated 'StorePath' inside this module's
-- transactions, so this is trust-on-read of host-local state the
-- module itself wrote (the delete path documents the same stance for
-- its raw-basename key). Callers that need a 'StorePath' back parse
-- at their own boundary.
queryReferences :: StoreDB -> StorePath -> IO [Text]
queryReferences db sp = do
let pathText = T.pack (storePathToFilePath (sdbDir db) sp)
Expand Down
24 changes: 21 additions & 3 deletions test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import Nix.Eval.Arena (arenaDestroy, arenaInit)
import Nix.Eval.CAttrSet (cattrsetFreeze, cattrsetInsert, cattrsetKeys, cattrsetLookup, cattrsetNew, cattrsetSize, cattrsetUnion)
import Nix.Eval.CBytecode (binaryAdd, captureSlots, captureWithScopes, cbcArg1, cbcArg2, cbcArg3, cbcData, cbcFlags, cbcOpCount, cbcOpcode, cbcShortArg, formalName, formalNamedSet, formalSet, strpartInterp, strpartLit, unaryNegate, pattern OpApp, pattern OpAssert, pattern OpAttrs, pattern OpBinary, pattern OpHasAttr, pattern OpIf, pattern OpIndStr, pattern OpLambda, pattern OpLet, pattern OpList, pattern OpLitBool, pattern OpLitFloat, pattern OpLitInt, pattern OpLitNull, pattern OpLitPath, pattern OpLitUri, pattern OpResolvedVar, pattern OpSelect, pattern OpStr, pattern OpUnary, pattern OpVar, pattern OpWith, pattern OpWithVar)
import Nix.Eval.CThunk (CThunkPtr, cthunkCount, cthunkGet, cthunkGetBcIdx, cthunkMarkBlackhole, cthunkNewBc, cthunkNewComputed, cthunkPayload, cthunkSetComputed, cthunkState)
import Nix.Eval.CanonPath (canonPath)
import Nix.Eval.CanonPath (canonPath, canonPathValue)
import Nix.Eval.Compile (compileExpr)
import qualified Nix.Eval.Context as Context
import Nix.Eval.IO (EvalState (..), newEvalState, runEvalIO)
Expand Down Expand Up @@ -1501,6 +1501,24 @@ testBatch1 = do
| v == mkStr "regression-name.nix" -> Pass
| otherwise -> Fail ("wrong basename: " <> T.pack (show v))
Left err -> Fail ("eval failed: " <> T.pack (show err)),
-- The path-value spec: absolute, lexically canonical, and
-- slash-spelled regardless of the base dir's native spelling.
runTestM "path values are slash-canonical from a native base" $ do
cwd <- Dir.getCurrentDirectory
result <- evalNixIO cwd "toString ./spec-name.nix"
let expected = mkStr (T.replace "\\" "/" (T.pack cwd) <> "/spec-name.nix")
pure $ case result of
Right v
| v == expected -> Pass
| otherwise -> Fail ("wrong spelling: " <> T.pack (show v))
Left err -> Fail ("eval failed: " <> T.pack (show err)),
runTest "canonPathValue folds platform separators only" $
let folded = canonPathValue "C:\\a\\.\\b"
expectedByPlatform =
if SI.os == "mingw32"
then "C:/a/b" -- '\\' is a separator here and folds
else "C:\\a\\.\\b" -- '\\' is a file-name character, preserved
in assertEqual "platform fold" expectedByPlatform folded,
runTestM "dirOf on a native-based path value" $ do
cwd <- Dir.getCurrentDirectory
dirResult <- evalNixIO cwd "builtins.dirOf ./sub/regression-name.nix"
Expand Down Expand Up @@ -2349,7 +2367,7 @@ testBatchCIO = do
<> nixQuotedPath nixpkgsDir
<> "; } ] \"nixpkgs\""
)
(VPath (T.pack nixpkgsDir)),
(VPath (canonPathValue (T.pack nixpkgsDir))),
runTestIOFail
"findFile no match"
testDir
Expand Down Expand Up @@ -7298,7 +7316,7 @@ testClassIFollowupsIO = do
runTestM "tilde path literal expands against the home directory" $ do
home <- Dir.getHomeDirectory
result <- evalNixIO testDir "builtins.toString ~/nova-tilde-probe"
let expected = canonPath (T.pack (home </> "nova-tilde-probe"))
let expected = canonPathValue (T.pack (home </> "nova-tilde-probe"))
pure $ assertRight "tilde" result $ \val ->
assertEqual "tilde-expanded" (mkStr expected) val,
-- a search-path match is returned CANONICALIZED
Expand Down
Loading