Skip to content

Commit eb0bafc

Browse files
committed
Add a routine for path normalization
1 parent 2761290 commit eb0bafc

File tree

5 files changed

+154
-1
lines changed

5 files changed

+154
-1
lines changed

core/src/Streamly/Internal/FileSystem/Path/Common.hs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ module Streamly.Internal.FileSystem.Path.Common
2424
, toString
2525
, toChars
2626

27+
-- * Conversion
28+
, normalize
29+
2730
-- * Operations
2831
, primarySeparator
2932
, isSeparator
@@ -45,6 +48,7 @@ where
4548

4649
import Control.Monad.Catch (MonadThrow(..))
4750
import Data.Char (ord, isAlpha)
51+
import Data.Function ((&))
4852
import Data.Functor.Identity (Identity(..))
4953
#ifdef DEBUG
5054
import Data.Maybe (fromJust)
@@ -54,7 +58,7 @@ import GHC.Base (unsafeChr)
5458
import Language.Haskell.TH (Q, Exp)
5559
import Language.Haskell.TH.Quote (QuasiQuoter (..))
5660
import Streamly.Internal.Data.Array (Array(..))
57-
import Streamly.Internal.Data.MutByteArray (Unbox)
61+
import Streamly.Internal.Data.MutByteArray (Unbox(..))
5862
import Streamly.Internal.Data.Path (PathException(..))
5963
import Streamly.Internal.Data.Stream (Stream)
6064
import System.IO.Unsafe (unsafePerformIO)
@@ -367,3 +371,52 @@ append :: (Unbox a, Integral a) =>
367371
OS -> (Array a -> String) -> Array a -> Array a -> Array a
368372
append os toStr a b =
369373
withAppendCheck os toStr b (doAppend os a b)
374+
375+
{-# INLINE normalize #-}
376+
normalize :: forall a. (Unbox a, Integral a) => OS -> Array a -> Array a
377+
normalize os arr =
378+
if arrElemLen == 1
379+
then arr
380+
else Array.unsafeFreeze $ unsafePerformIO $ do
381+
let workSliceMut = Array.unsafeThaw workSlice
382+
workSliceStream = MutArray.read workSliceMut
383+
(mid :: MutArray.MutArray a) <-
384+
Stream.indexOnSuffix (== sepElem) workSliceStream
385+
& Stream.filter (not . shouldFilterOut)
386+
& fmap (\(i, len) -> getSliceWithSepSuffix i len workSliceMut)
387+
& Stream.fold (Fold.foldlM' MutArray.unsafeSplice initBufferM)
388+
if startsWithDotSlash && MutArray.length mid == 0
389+
then MutArray.fromListN 2 [fstElem, sndElem]
390+
else pure mid
391+
392+
where
393+
394+
sepElem = fromIntegral (ord (primarySeparator os))
395+
dotElem = fromIntegral (ord '.')
396+
arrElemLen = Array.length arr
397+
398+
fstElem = Array.getIndexUnsafe 0 arr
399+
sndElem = Array.getIndexUnsafe 1 arr
400+
401+
startsWithSep = fstElem == sepElem
402+
startsWithDotSlash = fstElem == dotElem && sndElem == sepElem
403+
404+
workSlice
405+
| startsWithSep = Array.getSliceUnsafe 1 (arrElemLen - 1) arr
406+
| startsWithDotSlash = Array.getSliceUnsafe 2 (arrElemLen - 2) arr
407+
| otherwise = arr
408+
workSliceElemLen = Array.length workSlice
409+
410+
shouldFilterOut (off, len) =
411+
len == 0 ||
412+
(len == 1 && Array.getIndexUnsafe off workSlice == dotElem)
413+
414+
getSliceWithSepSuffix i len
415+
| i + len == workSliceElemLen = MutArray.unsafeGetSlice i len
416+
getSliceWithSepSuffix i len = MutArray.unsafeGetSlice i (len + 1)
417+
418+
initBufferM = do
419+
(newArr :: MutArray.MutArray a) <- MutArray.emptyOf arrElemLen
420+
if startsWithSep
421+
then MutArray.unsafeSnoc newArr fstElem
422+
else pure newArr

core/src/Streamly/Internal/FileSystem/PosixPath.hs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module Streamly.Internal.FileSystem.OS_PATH
4141
-- * Conversions
4242
, IsPath (..)
4343
, adapt
44+
, normalize
4445

4546
-- * Construction
4647
, fromChunk
@@ -360,3 +361,49 @@ append (OS_PATH a) (OS_PATH b) =
360361
OS_PATH
361362
$ Common.append
362363
Common.OS_NAME (Common.toString Unicode.UNICODE_DECODER) a b
364+
365+
-- | Normalize the path.
366+
--
367+
-- The behaviour is similar to FilePath.normalise.
368+
--
369+
-- >>> Path.toString $ Path.normalize $ [path|/file/\test////|]
370+
-- "/file/\\test/"
371+
--
372+
-- >>> Path.toString $ Path.normalize $ [path|/file/./test|]
373+
-- "/file/test"
374+
--
375+
-- >>> Path.toString $ Path.normalize $ [path|/test/file/../bob/fred/|]
376+
-- "/test/file/../bob/fred/"
377+
--
378+
-- >>> Path.toString $ Path.normalize $ [path|../bob/fred/|]
379+
-- "../bob/fred/"
380+
--
381+
-- >>> Path.toString $ Path.normalize $ [path|/a/../c|]
382+
-- "/a/../c"
383+
--
384+
-- >>> Path.toString $ Path.normalize $ [path|./bob/fred/|]
385+
-- "bob/fred/"
386+
--
387+
-- >>> Path.toString $ Path.normalize $ [path|.|]
388+
-- "."
389+
--
390+
-- >>> Path.toString $ Path.normalize $ [path|./|]
391+
-- "./"
392+
--
393+
-- >>> Path.toString $ Path.normalize $ [path|./.|]
394+
-- "./"
395+
--
396+
-- >>> Path.toString $ Path.normalize $ [path|/./|]
397+
-- "/"
398+
--
399+
-- >>> Path.toString $ Path.normalize $ [path|/|]
400+
-- "/"
401+
--
402+
-- >>> Path.toString $ Path.normalize $ [path|bob/fred/.|]
403+
-- "bob/fred/"
404+
--
405+
-- >>> Path.toString $ Path.normalize $ [path|//home|]
406+
-- "/home"
407+
--
408+
normalize :: OS_PATH -> OS_PATH
409+
normalize (OS_PATH a) = OS_PATH $ Common.normalize Common.OS_NAME a

streamly.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ extra-source-files:
127127
test/Streamly/Test/FileSystem/Event/Windows.hs
128128
test/Streamly/Test/FileSystem/Event/Linux.hs
129129
test/Streamly/Test/FileSystem/Handle.hs
130+
test/Streamly/Test/FileSystem/Path.hs
130131
test/Streamly/Test/Network/Socket.hs
131132
test/Streamly/Test/Network/Inet/TCP.hs
132133
test/Streamly/Test/Prelude.hs
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-- |
2+
-- Module : Streamly.Test.FileSystem.Path
3+
-- Copyright : (c) 2021 Composewell Technologies
4+
-- License : BSD-3-Clause
5+
-- Maintainer : [email protected]
6+
-- Stability : experimental
7+
-- Portability : GHC
8+
--
9+
10+
module Streamly.Test.FileSystem.Path (main) where
11+
12+
import qualified System.FilePath as FilePath
13+
import qualified Streamly.Internal.FileSystem.Path as Path
14+
15+
import Test.Hspec as H
16+
17+
moduleName :: String
18+
moduleName = "FileSystem.Path"
19+
20+
testNormalize :: String -> Spec
21+
testNormalize inp =
22+
it ("normalize: " ++ show inp) $ do
23+
p <- Path.fromString inp
24+
let expected = FilePath.normalise inp
25+
got = Path.toString (Path.normalize p)
26+
got `shouldBe` expected
27+
28+
main :: IO ()
29+
main =
30+
hspec $
31+
H.parallel $
32+
describe moduleName $ do
33+
describe "normalize" $ do
34+
testNormalize "/file/\\test////"
35+
testNormalize "/file/./test"
36+
testNormalize "/test/file/../bob/fred/"
37+
testNormalize "../bob/fred/"
38+
testNormalize "/a/../c"
39+
testNormalize "./bob/fred/"
40+
testNormalize "."
41+
testNormalize "./"
42+
testNormalize "./."
43+
testNormalize "/./"
44+
testNormalize "/"
45+
testNormalize "bob/fred/."
46+
testNormalize "//home"

test/streamly-tests.cabal

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,12 @@ test-suite FileSystem.Handle
445445
if flag(use-streamly-core)
446446
buildable: False
447447

448+
test-suite FileSystem.Path
449+
import: test-options
450+
type: exitcode-stdio-1.0
451+
main-is: Streamly/Test/FileSystem/Path.hs
452+
ghc-options: -main-is Streamly.Test.FileSystem.Path.main
453+
448454
test-suite Network.Inet.TCP
449455
import: lib-options
450456
type: exitcode-stdio-1.0

0 commit comments

Comments
 (0)