Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix W.splitFileName "/\\?/a:" #220

Merged
merged 5 commits into from
Jan 26, 2024
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
4 changes: 3 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ jobs:
set -eux
cabal update
cabal build --enable-tests --enable-benchmarks
cabal test
cabal test --test-show-details=direct filepath-tests
cabal test --test-show-details=direct --test-options='--quickcheck-tests 50_000' filepath-equivalent-tests
cabal test --test-show-details=direct abstract-filepath
cabal bench
cabal haddock
cabal check
Expand Down
49 changes: 39 additions & 10 deletions System/FilePath/Internal.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE MultiWayIf #-}

-- This template expects CPP definitions for:
-- MODULE_NAME = Posix | Windows
Expand Down Expand Up @@ -602,6 +603,7 @@ isDrive x = not (null x) && null (dropDrive x)
-- > Posix: splitFileName "/" == ("/","")
-- > Windows: splitFileName "c:" == ("c:","")
-- > Windows: splitFileName "\\\\?\\A:\\fred" == ("\\\\?\\A:\\","fred")
-- > Windows: splitFileName "\\\\?\\A:" == ("\\\\?\\A:","")
splitFileName :: FILEPATH -> (STRING, STRING)
splitFileName x = if null path
then (dotSlash, file)
Expand Down Expand Up @@ -644,20 +646,46 @@ splitFileName_ fp
-- or UNC location "\\?\UNC\foo", where path separator is a part of the drive name.
-- We can test this by trying dropDrive and falling back to splitDrive.
| isWindows
, Just (s1, _s2, bs') <- uncons2 dirSlash
, isPathSeparator s1
-- If bs' is empty, then s2 as the last character of dirSlash must be a path separator,
-- so we are in the middle of shared drive.
-- Otherwise, since s1 is a path separator, we might be in the middle of UNC path.
, null bs' || maybe False isIncompleteUNC (readDriveUNC dirSlash)
= (fp, mempty)
= case uncons2 dirSlash of
Just (s1, s2, bs')
| isPathSeparator s1
-- If bs' is empty, then s2 as the last character of dirSlash must be a path separator,
-- so we are in the middle of shared drive.
-- Otherwise, since s1 is a path separator, we might be in the middle of UNC path.
, null bs' || maybe False isIncompleteUNC (readDriveUNC dirSlash)
-> (fp, mempty)
-- This handles inputs like "//?/A:" and "//?/A:foo"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trivia: //?/A:foo is actually invalid in some sense... it's hard to interpret it. UNC paths are absolute, but A:foo on its own is a relative directory (relative to current working dir on drive A). The windows API probably won't care, but no such file can ever exist, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but filepath-1.4.2.2 does not mind to process //?/A:foo even if it's complete nonsense.

| isPathSeparator s1
, isPathSeparator s2
, Just (s3, s4, bs'') <- uncons2 bs'
, s3 == _question
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is in fact a bug (although the old filepath behaves the same way):

ghci> System.FilePath.Windows.splitFileName "\\\\.\\a:foo"
("\\\\.\\","a:foo")

There are three UNC namespaces:

  • file namespace: //?/
  • device namespace: //./
  • NT namespace /??/

I'm still unsure whether fixing those bugs will cause more good than harm, but those are bugs.

, isPathSeparator s4
, null bs''
, Just (drive, rest) <- readDriveLetter file
-> (dirSlash <> drive, rest)
_ -> (dirSlash, file)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line is redundant? I'tll fall through to | otherwise, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it does not fall through, the line is required.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I misread the code.

| otherwise
= (dirSlash, file)
= (dirSlash, file)
where
(dirSlash, file) = breakEnd isPathSeparator fp

-- an adjustant variant of 'dropTrailingPathSeparator' that normalises trailing path separators
-- on windows
dropTrailingPathSeparator' x =
if hasTrailingPathSeparator x
then let x' = dropWhileEnd isPathSeparator x
in if | null x' -> singleton (last x)
| isDrive x -> addTrailingPathSeparator x'
| otherwise -> x'
else x

-- an "incomplete" UNC is one without a path (but potentially a drive)
isIncompleteUNC (pref, suff) = null suff && not (hasPenultimateColon pref)
hasPenultimateColon = maybe False (maybe False ((== _colon) . snd) . unsnoc . fst) . unsnoc

-- e.g. @//?/a:/@ or @//?/a://@, but not @//?/a:@
hasPenultimateColon pref
| hasTrailingPathSeparator pref
= maybe False (maybe False ((== _colon) . snd) . unsnoc . fst) . unsnoc . dropTrailingPathSeparator' $ pref
| otherwise = False

-- | Set the filename.
--
Expand All @@ -671,6 +699,7 @@ replaceFileName x y = a </> y where (a,_) = splitFileName_ x
--
-- > dropFileName "/directory/file.ext" == "/directory/"
-- > dropFileName x == fst (splitFileName x)
-- > isPrefixOf (takeDrive x) (dropFileName x)
dropFileName :: FILEPATH -> FILEPATH
dropFileName = fst . splitFileName

Expand Down
4 changes: 4 additions & 0 deletions filepath.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,12 @@ test-suite filepath-equivalent-tests
, base
, bytestring >=0.11.3.0
, filepath
, generic-random
, generic-deriving
, os-string >=2.0.1
, QuickCheck >=2.7 && <2.15
, tasty
, tasty-quickcheck

test-suite abstract-filepath
default-language: Haskell2010
Expand Down
Loading
Loading