cabal icon indicating copy to clipboard operation
cabal copied to clipboard

Cabal writes local tilde (`~`) directory

Open tbidne opened this issue 6 months ago • 3 comments

Describe the bug

Under some circumstances, cabal will write a local directory with the name ./~ when ~ is not expanded. This is clearly related to https://github.com/haskell/cabal/issues/6776, however I am opening a new issue as I think this is more dangerous than mere inconvenience.

To Reproduce

In contrast to the description of that issue, its reproducer:

$ cabal v2-install cabal-install --installdir=~/.cabal/bin

does not error out for me (I guess it used to?). It creates a local directory ./~:

$ tree -a './~'
./~
└── .cabal
    └── bin
        └── cabal -> ...

Expected behavior

IMO this is dangerous because the user is now a mere typo away from accidentally deleting their home directory (this just happened to me; I was only saved by a write-protected file).

While I am sure that @phadej is correct that actual expansion requires a systematic approach (--store-dir is also affected), at least throwing an error when ~ is detected on a case-by-case basis would be safer than the status quo.

System information

  • NixOS
  • Cabal: 3.14 and master.
  • GHC: 9.10.2

Additional context

Incidentally, you won't see this problem with git status in this repository because of *~ in .gitignore. Hence if you try to reproduce this in your local cabal copy, be very careful cleaning up e.g. run ls './~' first then replace ls.

Also related: https://github.com/haskell/cabal/issues/7812

tbidne avatar Jun 19 '25 01:06 tbidne

FWIW, the = is optional, so you can do

cabal v2-install cabal-install --installdir ~/.cabal/bin

phadej avatar Jun 22 '25 14:06 phadej

Thanks, that is good advice. Thinking about this a bit more, this isn't really cabal's fault, since shell expansion is the shell's responsibility. That said, this is an easy mistake to make, and it leaves users in a bad spot.

It is not hard to:

  • Replace "tilde prefixes" (~/..., ~\..., ~) with the home directory (solving the linked issue).
  • Throw an exception for all other ~ (this issue).

However, I am not sure if there is a good way to do this besides a systematic approach. Probably the "right" thing to do would be to parse whatever CLI/config paths String -> SomePath and use that everywhere aka solve https://github.com/haskell/cabal/issues/6667. Taking this to that issue...

tbidne avatar Jun 24 '25 23:06 tbidne

It'd be easy to agree on a patch throwing an error on any ~ without trying to do anything smart™, if someone were to submit a patch like that.

ulysses4ever avatar Jun 25 '25 01:06 ulysses4ever

Do you have any suggestions on where this logic would go? I'm trying to figure out how raw string args end up as path arguments, and the most plausible place I've seen so far is this:

makeSymbolicPath :: FilePath -> SymbolicPath from to
makeSymbolicPath fp = SymbolicPath fp

This may work if erroring out here is acceptable, but it's slightly unsatisfactory as there is no path from this to resolving ~ as the home directory (the original issue), since full resolution requires IO. The hackage-security has a nice way of doing this that defers IO:

fromFilePath :: FilePath -> FsPath
fromFilePath fp
    | FP.Native.isAbsolute fp = FsPath (mkPathNative fp  :: Path Absolute)
    | Just fp' <- atHome fp   = FsPath (mkPathNative fp' :: Path HomeDir)
    | otherwise               = FsPath (mkPathNative fp  :: Path Relative)
  where
    -- TODO: I don't know if there a standard way that Windows users refer to
    -- their home directory. For now, we'll only interpret '~'. Everybody else
    -- can specify an absolute path if this doesn't work.
    atHome :: FilePath -> Maybe FilePath
    atHome "~" = Just ""
    atHome ('~':sep:fp') | FP.Native.isPathSeparator sep = Just fp'
    atHome _otherwise = Nothing

The same cannot be easily done for makeSymbolicPath since makeSymbolicPath universally quantifies the from, whereas we need existentials.

tbidne avatar Jul 07 '25 01:07 tbidne

That's the wrong place; it just records what kind of path it is. You want what converts SymbolicPath back to an actual path. I suggest looking over #9718.

geekosaur avatar Jul 07 '25 01:07 geekosaur

Searching the Distribution.Utils.Path module for -> FilePath yields the following functions:

getSymbolicPath :: SymbolicPathX allowAbsolute from to -> FilePath

interpretSymbolicPath :: Maybe (SymbolicPath CWD (Dir from)) -> SymbolicPathX allowAbsolute from to -> FilePath

interpretSymbolicPathCWD :: SymbolicPathX allowAbsolute from to -> FilePath

getAbsolutePath :: AbsolutePath to -> FilePath

interpretSymbolicPathAbsolute :: AbsolutePath (Dir Pkg) -> SymbolicPathX allowAbsolute Pkg to -> FilePath

From the docs, it appears that interpretSymbolicPath and interpretSymbolicPathCWD are the primary deserializers. Are you suggesting doing the handling in these functions i.e. either erroring or expanding (the latter requires IO or passing the home dir as a param)?

tbidne avatar Jul 07 '25 03:07 tbidne

Those and the first one you found are the only places that make any sense to do it. In either case you need to either put them in IO or pass the home directory.

geekosaur avatar Jul 07 '25 03:07 geekosaur