diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# FilePath [![Hackage version](https://img.shields.io/hackage/v/filepath.svg?style=flat)](https://hackage.haskell.org/package/filepath) [![Build Status](https://img.shields.io/travis/haskell/filepath.svg?style=flat)](https://travis-ci.org/haskell/filepath)
+# FilePath [![Hackage version](https://img.shields.io/hackage/v/filepath.svg?label=Hackage)](https://hackage.haskell.org/package/filepath) [![Linux Build Status](https://img.shields.io/travis/haskell/filepath.svg?label=Linux%20build)](https://travis-ci.org/haskell/filepath) [![Windows Build Status](https://img.shields.io/appveyor/ci/ndmitchell/filepath.svg?label=Windows%20build)](https://ci.appveyor.com/project/ndmitchell/filepath)
 
 The `filepath` package provides functionality for manipulating `FilePath` values, and is shipped with both [GHC](https://www.haskell.org/ghc/) and the [Haskell Platform](https://www.haskell.org/platform/). It provides three modules:
 
@@ -7,3 +7,13 @@
 * [`System.FilePath`](http://hackage.haskell.org/package/filepath/docs/System-FilePath.html) is an alias for the module appropriate to your platform.
 
 All three modules provide the same API, and the same documentation (calling out differences in the different variants).
+
+### Should `FilePath` be an abstract data type?
+
+The answer for this library is "no". While an abstract `FilePath` has some advantages (mostly type safety), it also has some disadvantages:
+
+* In Haskell the definition is `type FilePath = String`, and all file-orientated functions operate on this type alias, e.g. `readFile`/`writeFile`. Any abstract type would require wrappers for these functions or lots of casts between `String` and the abstraction.
+* It is not immediately obvious what a `FilePath` is, and what is just a pure `String`. For example, `/path/file.ext` is a `FilePath`. Is `/`? `/path`? `path`? `file.ext`? `.ext`? `file`?
+* Often it is useful to represent invalid files, e.g. `/foo/*.txt` probably isn't an actual file, but a glob pattern. Other programs use `foo//bar` for globs, which is definitely not a file, but might want to be stored as a `FilePath`.
+* Some programs use syntactic non-semantic details of the `FilePath` to change their behaviour. For example, `foo`, `foo/` and `foo/.` are all similar, and refer to the same location on disk, but may behave differently when passed to command-line tools.
+* A useful step to introducing an abstract `FilePath` is to reduce the amount of manipulating `FilePath` values like lists. This library hopes to help in that effort.
diff --git a/System/FilePath/Internal.hs b/System/FilePath/Internal.hs
--- a/System/FilePath/Internal.hs
+++ b/System/FilePath/Internal.hs
@@ -19,7 +19,7 @@
 -- A library for 'FilePath' manipulations, using MODULE_NAME style paths on
 -- all platforms. Importing "System.FilePath" is usually better.
 --
--- Given the eample 'FilePath': @\/directory\/file.ext@
+-- Given the example 'FilePath': @\/directory\/file.ext@
 --
 -- We can use the following functions to extract pieces.
 --
@@ -75,7 +75,8 @@
     -- * Extension functions
     splitExtension,
     takeExtension, replaceExtension, (-<.>), dropExtension, addExtension, hasExtension, (<.>),
-    splitExtensions, dropExtensions, takeExtensions,
+    splitExtensions, dropExtensions, takeExtensions, replaceExtensions,
+    stripExtension,
 
     -- * Filename\/directory functions
     splitFileName,
@@ -104,6 +105,7 @@
 
 import Data.Char(toLower, toUpper, isAsciiLower, isAsciiUpper)
 import Data.Maybe(isJust)
+import Data.List(stripPrefix)
 
 import System.Environment(getEnv)
 
@@ -188,6 +190,8 @@
 -- Path methods (environment $PATH)
 
 -- | Take a string, split it on the 'searchPathSeparator' character.
+--   Blank items are ignored on Windows, and converted to @.@ on Posix.
+--   On Windows path elements are stripped of quotes.
 --
 --   Follows the recommendations in
 --   <http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html>
@@ -254,7 +258,7 @@
 (-<.>) :: FilePath -> String -> FilePath
 (-<.>) = replaceExtension
 
--- | Set the extension of a file, overwriting one if already present, equivalent to '<.>'.
+-- | Set the extension of a file, overwriting one if already present, equivalent to '-<.>'.
 --
 -- > replaceExtension "/directory/path.txt" "ext" == "/directory/path.ext"
 -- > replaceExtension "/directory/path.txt" ".ext" == "/directory/path.ext"
@@ -308,6 +312,28 @@
 hasExtension = any isExtSeparator . takeFileName
 
 
+-- | Drop the given extension from a FilePath, and the @\".\"@ preceding it.
+--   Returns 'Nothing' if the FilePath does not have the given extension, or
+--   'Just' and the part before the extension if it does.
+--
+--   This function can be more predictable than 'dropExtensions', especially if the filename
+--   might itself contain @.@ characters.
+--
+-- > stripExtension "hs.o" "foo.x.hs.o" == Just "foo.x"
+-- > stripExtension "hi.o" "foo.x.hs.o" == Nothing
+-- > dropExtension x == fromJust (stripExtension (takeExtension x) x)
+-- > dropExtensions x == fromJust (stripExtension (takeExtensions x) x)
+-- > stripExtension ".c.d" "a.b.c.d"  == Just "a.b"
+-- > stripExtension ".c.d" "a.b..c.d" == Just "a.b."
+-- > stripExtension "baz"  "foo.bar"  == Nothing
+-- > stripExtension "bar"  "foobar"   == Nothing
+-- > stripExtension ""     x          == Just x
+stripExtension :: String -> FilePath -> Maybe FilePath
+stripExtension []        path = Just path
+stripExtension ext@(x:_) path = stripSuffix dotExt path
+    where dotExt = if isExtSeparator x then ext else '.':ext
+
+
 -- | Split on all extensions.
 --
 -- > splitExtensions "/directory/path.ext" == ("/directory/path",".ext")
@@ -338,7 +364,18 @@
 takeExtensions = snd . splitExtensions
 
 
+-- | Replace all extensions of a file with a new extension. Note
+--   that 'replaceExtension' and 'addExtension' both work for adding
+--   multiple extensions, so only required when you need to drop
+--   all extensions first.
+--
+-- > replaceExtensions "file.fred.bob" "txt" == "file.txt"
+-- > replaceExtensions "file.fred.bob" "tar.gz" == "file.tar.gz"
+replaceExtensions :: FilePath -> String -> FilePath
+replaceExtensions x y = dropExtensions x <.> y
 
+
+
 ---------------------------------------------------------------------
 -- Drive methods
 
@@ -459,7 +496,7 @@
 ---------------------------------------------------------------------
 -- Operations on a filepath, as a list of directories
 
--- | Split a filename into directory and file. 'combine' is the inverse.
+-- | Split a filename into directory and file. '</>' is the inverse.
 --   The first component will often end with a trailing slash.
 --
 -- > splitFileName "/directory/file.ext" == ("/directory/","file.ext")
@@ -601,39 +638,7 @@
 replaceDirectory x dir = combineAlways dir (takeFileName x)
 
 
--- | Combine two paths, if the second path starts with a path separator or a
--- drive letter, then it returns the second.
---
--- > Valid x => combine (takeDirectory x) (takeFileName x) `equalFilePath` x
---
--- Combined:
--- > Posix:   combine "/" "test" == "/test"
--- > Posix:   combine "home" "bob" == "home/bob"
--- > Posix:   combine "x:" "foo" == "x:/foo"
--- > Windows: combine "C:\\foo" "bar" == "C:\\foo\\bar"
--- > Windows: combine "home" "bob" == "home\\bob"
---
--- Not combined:
--- > Posix:   combine "home" "/bob" == "/bob"
--- > Windows: combine "home" "C:\\bob" == "C:\\bob"
---
--- Not combined (tricky):
--- On Windows, if a filepath starts with a single slash, it is relative to the
--- root of the current drive. In [1], this is (confusingly) referred to as an
--- absolute path.
--- The current behavior of @combine@ is to never combine these forms.
---
--- > Windows: combine "home" "/bob" == "/bob"
--- > Windows: combine "home" "\\bob" == "\\bob"
--- > Windows: combine "C:\\home" "\\bob" == "\\bob"
---
--- On Windows, from [1]: "If a file name begins with only a disk designator
--- but not the backslash after the colon, it is interpreted as a relative path
--- to the current directory on the drive with the specified letter."
--- The current behavior of @combine@ is to never combine these forms.
---
--- > Windows: combine "D:\\foo" "C:bar" == "C:bar"
--- > Windows: combine "C:\\foo" "C:bar" == "C:bar"
+-- | An alias for '</>'.
 combine :: FilePath -> FilePath -> FilePath
 combine a b | hasLeadingPathSeparator b || hasDrive b = b
             | otherwise = combineAlways a b
@@ -648,10 +653,47 @@
                       _ -> a ++ [pathSeparator] ++ b
 
 
--- | Join two values with a path separator. For examples and caveats see the equivalent function 'combine'.
+-- | Combine two paths with a path separator.
+--   If the second path starts with a path separator or a drive letter, then it returns the second.
+--   The intention is that @readFile (dir '</>' file)@ will access the same file as
+--   @setCurrentDirectory dir; readFile file@.
 --
 -- > Posix:   "/directory" </> "file.ext" == "/directory/file.ext"
 -- > Windows: "/directory" </> "file.ext" == "/directory\\file.ext"
+-- >          "directory" </> "/file.ext" == "/file.ext"
+-- > Valid x => (takeDirectory x </> takeFileName x) `equalFilePath` x
+--
+--   Combined:
+--
+-- > Posix:   "/" </> "test" == "/test"
+-- > Posix:   "home" </> "bob" == "home/bob"
+-- > Posix:   "x:" </> "foo" == "x:/foo"
+-- > Windows: "C:\\foo" </> "bar" == "C:\\foo\\bar"
+-- > Windows: "home" </> "bob" == "home\\bob"
+--
+--   Not combined:
+--
+-- > Posix:   "home" </> "/bob" == "/bob"
+-- > Windows: "home" </> "C:\\bob" == "C:\\bob"
+--
+--   Not combined (tricky):
+--
+--   On Windows, if a filepath starts with a single slash, it is relative to the
+--   root of the current drive. In [1], this is (confusingly) referred to as an
+--   absolute path.
+--   The current behavior of '</>' is to never combine these forms.
+--
+-- > Windows: "home" </> "/bob" == "/bob"
+-- > Windows: "home" </> "\\bob" == "\\bob"
+-- > Windows: "C:\\home" </> "\\bob" == "\\bob"
+--
+--   On Windows, from [1]: "If a file name begins with only a disk designator
+--   but not the backslash after the colon, it is interpreted as a relative path
+--   to the current directory on the drive with the specified letter."
+--   The current behavior of '</>' is to never combine these forms.
+--
+-- > Windows: "D:\\foo" </> "C:bar" == "C:bar"
+-- > Windows: "C:\\foo" </> "C:bar" == "C:bar"
 (</>) :: FilePath -> FilePath -> FilePath
 (</>) = combine
 
@@ -727,7 +769,10 @@
             | otherwise = dropTrailingPathSeparator $ normalise x
 
 
--- | Contract a filename, based on a relative path.
+-- | Contract a filename, based on a relative path. Note that the resulting path
+--   will never introduce @..@ paths, as the presence of symlinks means @..\/b@
+--   may not reach @a\/b@ if it starts from @a\/c@. For a worked example see
+--   <http://neilmitchell.blogspot.co.uk/2015/10/filepaths-are-subtle-symlinks-are-hard.html this blog post>.
 --
 --   The corresponding @makeAbsolute@ function can be found in
 --   @System.Directory@.
@@ -834,15 +879,22 @@
         repSlash x = if isPathSeparator x then pathSeparator else x
 
 -- Information for validity functions on Windows. See [1].
-badCharacters :: [Char]
-badCharacters = ":*?><|\""
+isBadCharacter :: Char -> Bool
+isBadCharacter x = x >= '\0' && x <= '\31' || x `elem` ":*?><|\""
+
 badElements :: [FilePath]
-badElements = ["CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "CLOCK$"]
+badElements =
+    ["CON","PRN","AUX","NUL","CLOCK$"
+    ,"COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9"
+    ,"LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9"]
 
 
--- | Is a FilePath valid, i.e. could you create a file like it?
+-- | Is a FilePath valid, i.e. could you create a file like it? This function checks for invalid names,
+--   and invalid characters, but does not check if length limits are exceeded, as these are typically
+--   filesystem dependent.
 --
 -- >          isValid "" == False
+-- >          isValid "\0" == False
 -- > Posix:   isValid "/random_ path:*" == True
 -- > Posix:   isValid x == not (null x)
 -- > Windows: isValid "c:\\test" == True
@@ -854,17 +906,21 @@
 -- > Windows: isValid "\\\\" == False
 -- > Windows: isValid "\\\\\\foo" == False
 -- > Windows: isValid "\\\\?\\D:file" == False
+-- > Windows: isValid "foo\tbar" == False
+-- > Windows: isValid "nul .txt" == False
+-- > Windows: isValid " nul.txt" == True
 isValid :: FilePath -> Bool
 isValid "" = False
+isValid x | '\0' `elem` x = False
 isValid _ | isPosix = True
 isValid path =
-        not (any (`elem` badCharacters) x2) &&
+        not (any isBadCharacter x2) &&
         not (any f $ splitDirectories x2) &&
         not (isJust (readDriveShare x1) && all isPathSeparator x1) &&
         not (isJust (readDriveUNC x1) && not (hasTrailingPathSeparator x1))
     where
         (x1,x2) = splitDrive path
-        f x = map toUpper (dropExtensions x) `elem` badElements
+        f x = map toUpper (dropWhileEnd (== ' ') $ dropExtensions x) `elem` badElements
 
 
 -- | Take a FilePath and make it valid; does not change already valid FilePaths.
@@ -872,6 +928,7 @@
 -- > isValid (makeValid x)
 -- > isValid x ==> makeValid x == x
 -- > makeValid "" == "_"
+-- > makeValid "file\0name" == "file_name"
 -- > Windows: makeValid "c:\\already\\/valid" == "c:\\already\\/valid"
 -- > Windows: makeValid "c:\\test:of_test" == "c:\\test_of_test"
 -- > Windows: makeValid "test*" == "test_"
@@ -881,10 +938,11 @@
 -- > Windows: makeValid "c:\\nul\\file" == "c:\\nul_\\file"
 -- > Windows: makeValid "\\\\\\foo" == "\\\\drive"
 -- > Windows: makeValid "\\\\?\\D:file" == "\\\\?\\D:\\file"
+-- > Windows: makeValid "nul .txt" == "nul _.txt"
 makeValid :: FilePath -> FilePath
 makeValid "" = "_"
 makeValid path
-        | isPosix = path
+        | isPosix = map (\x -> if x == '\0' then '_' else x) path
         | isJust (readDriveShare drv) && all isPathSeparator drv = take 2 drv ++ "drive"
         | isJust (readDriveUNC drv) && not (hasTrailingPathSeparator drv) =
             makeValid (drv ++ [pathSeparator] ++ pth)
@@ -893,13 +951,12 @@
         (drv,pth) = splitDrive path
 
         validChars = map f
-        f x | x `elem` badCharacters = '_'
-            | otherwise = x
+        f x = if isBadCharacter x then '_' else x
 
         validElements x = joinPath $ map g $ splitPath x
         g x = h a ++ b
             where (a,b) = break isPathSeparator x
-        h x = if map toUpper a `elem` badElements then a ++ "_" <.> b else x
+        h x = if map toUpper (dropWhileEnd (== ' ') a) `elem` badElements then a ++ "_" <.> b else x
             where (a,b) = splitExtensions x
 
 
@@ -963,3 +1020,9 @@
 -- breakEnd (< 2) [1,2,3,4,1,2,3,4] == ([1,2,3,4,1],[2,3,4])
 breakEnd :: (a -> Bool) -> [a] -> ([a], [a])
 breakEnd p = spanEnd (not . p)
+
+-- | The stripSuffix function drops the given suffix from a list. It returns
+-- Nothing if the list did not end with the suffix given, or Just the list
+-- before the suffix, if it does.
+stripSuffix :: Eq a => [a] -> [a] -> Maybe [a]
+stripSuffix xs ys = fmap reverse $ stripPrefix (reverse xs) (reverse ys)
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,18 @@
 
 _Note: below all `FilePath` values are unquoted, so `\\` really means two backslashes._
 
+## 1.4.1.0  *Dec 2015*
+
+ * Bundled with GHC 8.0.1
+
+ * Add `replaceExtensions` and `stripExtension` functions.
+
+ * Make `isValid` detect more invalid Windows paths, e.g. `nul .txt` and `foo\nbar`.
+
+ * Improve the documentation.
+
+ * Bug fix: `isValid "\0"` now returns `False`, instead of `True`
+
 ## 1.4.0.0  *Mar 2015*
 
   * Bundled with GHC 7.10.1
diff --git a/filepath.cabal b/filepath.cabal
--- a/filepath.cabal
+++ b/filepath.cabal
@@ -1,5 +1,5 @@
 name:           filepath
-version:        1.4.0.0
+version:        1.4.1.0
 -- NOTE: Don't forget to update ./changelog.md
 license:        BSD3
 license-file:   LICENSE
@@ -26,13 +26,19 @@
 
 extra-source-files:
     System/FilePath/Internal.hs
+extra-doc-files:
     README.md
     changelog.md
 
+source-repository head
+    type:     git
+    location: https://github.com/haskell/filepath.git
+
 library
     default-language: Haskell98
     other-extensions:
         CPP
+        PatternGuards
     if impl(GHC >= 7.2)
         other-extensions: Safe
 
@@ -42,7 +48,7 @@
         System.FilePath.Windows
 
     build-depends:
-        base >= 4 && < 4.9
+        base >= 4 && < 4.10
 
     ghc-options: -Wall
 
@@ -58,8 +64,4 @@
     build-depends:
         filepath,
         base,
-        QuickCheck >= 2.7 && < 2.8
-
-source-repository head
-    type:     git
-    location: https://github.com/haskell/filepath.git
+        QuickCheck >= 2.7 && < 2.9
diff --git a/tests/TestGen.hs b/tests/TestGen.hs
--- a/tests/TestGen.hs
+++ b/tests/TestGen.hs
@@ -105,6 +105,24 @@
     ,("W.hasExtension \"/directory/path\" == False", test $ W.hasExtension "/directory/path" == False)
     ,("null (P.takeExtension x) == not (P.hasExtension x)", test $ \(QFilePath x) -> null (P.takeExtension x) == not (P.hasExtension x))
     ,("null (W.takeExtension x) == not (W.hasExtension x)", test $ \(QFilePath x) -> null (W.takeExtension x) == not (W.hasExtension x))
+    ,("P.stripExtension \"hs.o\" \"foo.x.hs.o\" == Just \"foo.x\"", test $ P.stripExtension "hs.o" "foo.x.hs.o" == Just "foo.x")
+    ,("W.stripExtension \"hs.o\" \"foo.x.hs.o\" == Just \"foo.x\"", test $ W.stripExtension "hs.o" "foo.x.hs.o" == Just "foo.x")
+    ,("P.stripExtension \"hi.o\" \"foo.x.hs.o\" == Nothing", test $ P.stripExtension "hi.o" "foo.x.hs.o" == Nothing)
+    ,("W.stripExtension \"hi.o\" \"foo.x.hs.o\" == Nothing", test $ W.stripExtension "hi.o" "foo.x.hs.o" == Nothing)
+    ,("P.dropExtension x == fromJust (P.stripExtension (P.takeExtension x) x)", test $ \(QFilePath x) -> P.dropExtension x == fromJust (P.stripExtension (P.takeExtension x) x))
+    ,("W.dropExtension x == fromJust (W.stripExtension (W.takeExtension x) x)", test $ \(QFilePath x) -> W.dropExtension x == fromJust (W.stripExtension (W.takeExtension x) x))
+    ,("P.dropExtensions x == fromJust (P.stripExtension (P.takeExtensions x) x)", test $ \(QFilePath x) -> P.dropExtensions x == fromJust (P.stripExtension (P.takeExtensions x) x))
+    ,("W.dropExtensions x == fromJust (W.stripExtension (W.takeExtensions x) x)", test $ \(QFilePath x) -> W.dropExtensions x == fromJust (W.stripExtension (W.takeExtensions x) x))
+    ,("P.stripExtension \".c.d\" \"a.b.c.d\" == Just \"a.b\"", test $ P.stripExtension ".c.d" "a.b.c.d" == Just "a.b")
+    ,("W.stripExtension \".c.d\" \"a.b.c.d\" == Just \"a.b\"", test $ W.stripExtension ".c.d" "a.b.c.d" == Just "a.b")
+    ,("P.stripExtension \".c.d\" \"a.b..c.d\" == Just \"a.b.\"", test $ P.stripExtension ".c.d" "a.b..c.d" == Just "a.b.")
+    ,("W.stripExtension \".c.d\" \"a.b..c.d\" == Just \"a.b.\"", test $ W.stripExtension ".c.d" "a.b..c.d" == Just "a.b.")
+    ,("P.stripExtension \"baz\" \"foo.bar\" == Nothing", test $ P.stripExtension "baz" "foo.bar" == Nothing)
+    ,("W.stripExtension \"baz\" \"foo.bar\" == Nothing", test $ W.stripExtension "baz" "foo.bar" == Nothing)
+    ,("P.stripExtension \"bar\" \"foobar\" == Nothing", test $ P.stripExtension "bar" "foobar" == Nothing)
+    ,("W.stripExtension \"bar\" \"foobar\" == Nothing", test $ W.stripExtension "bar" "foobar" == Nothing)
+    ,("P.stripExtension \"\" x == Just x", test $ \(QFilePath x) -> P.stripExtension "" x == Just x)
+    ,("W.stripExtension \"\" x == Just x", test $ \(QFilePath x) -> W.stripExtension "" x == Just x)
     ,("P.splitExtensions \"/directory/path.ext\" == (\"/directory/path\", \".ext\")", test $ P.splitExtensions "/directory/path.ext" == ("/directory/path", ".ext"))
     ,("W.splitExtensions \"/directory/path.ext\" == (\"/directory/path\", \".ext\")", test $ W.splitExtensions "/directory/path.ext" == ("/directory/path", ".ext"))
     ,("P.splitExtensions \"file.tar.gz\" == (\"file\", \".tar.gz\")", test $ P.splitExtensions "file.tar.gz" == ("file", ".tar.gz"))
@@ -127,6 +145,10 @@
     ,("W.takeExtensions \"/directory/path.ext\" == \".ext\"", test $ W.takeExtensions "/directory/path.ext" == ".ext")
     ,("P.takeExtensions \"file.tar.gz\" == \".tar.gz\"", test $ P.takeExtensions "file.tar.gz" == ".tar.gz")
     ,("W.takeExtensions \"file.tar.gz\" == \".tar.gz\"", test $ W.takeExtensions "file.tar.gz" == ".tar.gz")
+    ,("P.replaceExtensions \"file.fred.bob\" \"txt\" == \"file.txt\"", test $ P.replaceExtensions "file.fred.bob" "txt" == "file.txt")
+    ,("W.replaceExtensions \"file.fred.bob\" \"txt\" == \"file.txt\"", test $ W.replaceExtensions "file.fred.bob" "txt" == "file.txt")
+    ,("P.replaceExtensions \"file.fred.bob\" \"tar.gz\" == \"file.tar.gz\"", test $ P.replaceExtensions "file.fred.bob" "tar.gz" == "file.tar.gz")
+    ,("W.replaceExtensions \"file.fred.bob\" \"tar.gz\" == \"file.tar.gz\"", test $ W.replaceExtensions "file.fred.bob" "tar.gz" == "file.tar.gz")
     ,("uncurry (++) (P.splitDrive x) == x", test $ \(QFilePath x) -> uncurry (++) (P.splitDrive x) == x)
     ,("uncurry (++) (W.splitDrive x) == x", test $ \(QFilePath x) -> uncurry (++) (W.splitDrive x) == x)
     ,("W.splitDrive \"file\" == (\"\", \"file\")", test $ W.splitDrive "file" == ("", "file"))
@@ -265,22 +287,24 @@
     ,("W.replaceDirectory \"root/file.ext\" \"/directory/\" == \"/directory/file.ext\"", test $ W.replaceDirectory "root/file.ext" "/directory/" == "/directory/file.ext")
     ,("P.replaceDirectory x (P.takeDirectory x) `P.equalFilePath` x", test $ \(QFilePathValidP x) -> P.replaceDirectory x (P.takeDirectory x) `P.equalFilePath` x)
     ,("W.replaceDirectory x (W.takeDirectory x) `W.equalFilePath` x", test $ \(QFilePathValidW x) -> W.replaceDirectory x (W.takeDirectory x) `W.equalFilePath` x)
-    ,("P.combine (P.takeDirectory x) (P.takeFileName x) `P.equalFilePath` x", test $ \(QFilePathValidP x) -> P.combine (P.takeDirectory x) (P.takeFileName x) `P.equalFilePath` x)
-    ,("W.combine (W.takeDirectory x) (W.takeFileName x) `W.equalFilePath` x", test $ \(QFilePathValidW x) -> W.combine (W.takeDirectory x) (W.takeFileName x) `W.equalFilePath` x)
-    ,("P.combine \"/\" \"test\" == \"/test\"", test $ P.combine "/" "test" == "/test")
-    ,("P.combine \"home\" \"bob\" == \"home/bob\"", test $ P.combine "home" "bob" == "home/bob")
-    ,("P.combine \"x:\" \"foo\" == \"x:/foo\"", test $ P.combine "x:" "foo" == "x:/foo")
-    ,("W.combine \"C:\\\\foo\" \"bar\" == \"C:\\\\foo\\\\bar\"", test $ W.combine "C:\\foo" "bar" == "C:\\foo\\bar")
-    ,("W.combine \"home\" \"bob\" == \"home\\\\bob\"", test $ W.combine "home" "bob" == "home\\bob")
-    ,("P.combine \"home\" \"/bob\" == \"/bob\"", test $ P.combine "home" "/bob" == "/bob")
-    ,("W.combine \"home\" \"C:\\\\bob\" == \"C:\\\\bob\"", test $ W.combine "home" "C:\\bob" == "C:\\bob")
-    ,("W.combine \"home\" \"/bob\" == \"/bob\"", test $ W.combine "home" "/bob" == "/bob")
-    ,("W.combine \"home\" \"\\\\bob\" == \"\\\\bob\"", test $ W.combine "home" "\\bob" == "\\bob")
-    ,("W.combine \"C:\\\\home\" \"\\\\bob\" == \"\\\\bob\"", test $ W.combine "C:\\home" "\\bob" == "\\bob")
-    ,("W.combine \"D:\\\\foo\" \"C:bar\" == \"C:bar\"", test $ W.combine "D:\\foo" "C:bar" == "C:bar")
-    ,("W.combine \"C:\\\\foo\" \"C:bar\" == \"C:bar\"", test $ W.combine "C:\\foo" "C:bar" == "C:bar")
     ,("\"/directory\" P.</> \"file.ext\" == \"/directory/file.ext\"", test $ "/directory" P.</> "file.ext" == "/directory/file.ext")
     ,("\"/directory\" W.</> \"file.ext\" == \"/directory\\\\file.ext\"", test $ "/directory" W.</> "file.ext" == "/directory\\file.ext")
+    ,("\"directory\" P.</> \"/file.ext\" == \"/file.ext\"", test $ "directory" P.</> "/file.ext" == "/file.ext")
+    ,("\"directory\" W.</> \"/file.ext\" == \"/file.ext\"", test $ "directory" W.</> "/file.ext" == "/file.ext")
+    ,("(P.takeDirectory x P.</> P.takeFileName x) `P.equalFilePath` x", test $ \(QFilePathValidP x) -> (P.takeDirectory x P.</> P.takeFileName x) `P.equalFilePath` x)
+    ,("(W.takeDirectory x W.</> W.takeFileName x) `W.equalFilePath` x", test $ \(QFilePathValidW x) -> (W.takeDirectory x W.</> W.takeFileName x) `W.equalFilePath` x)
+    ,("\"/\" P.</> \"test\" == \"/test\"", test $ "/" P.</> "test" == "/test")
+    ,("\"home\" P.</> \"bob\" == \"home/bob\"", test $ "home" P.</> "bob" == "home/bob")
+    ,("\"x:\" P.</> \"foo\" == \"x:/foo\"", test $ "x:" P.</> "foo" == "x:/foo")
+    ,("\"C:\\\\foo\" W.</> \"bar\" == \"C:\\\\foo\\\\bar\"", test $ "C:\\foo" W.</> "bar" == "C:\\foo\\bar")
+    ,("\"home\" W.</> \"bob\" == \"home\\\\bob\"", test $ "home" W.</> "bob" == "home\\bob")
+    ,("\"home\" P.</> \"/bob\" == \"/bob\"", test $ "home" P.</> "/bob" == "/bob")
+    ,("\"home\" W.</> \"C:\\\\bob\" == \"C:\\\\bob\"", test $ "home" W.</> "C:\\bob" == "C:\\bob")
+    ,("\"home\" W.</> \"/bob\" == \"/bob\"", test $ "home" W.</> "/bob" == "/bob")
+    ,("\"home\" W.</> \"\\\\bob\" == \"\\\\bob\"", test $ "home" W.</> "\\bob" == "\\bob")
+    ,("\"C:\\\\home\" W.</> \"\\\\bob\" == \"\\\\bob\"", test $ "C:\\home" W.</> "\\bob" == "\\bob")
+    ,("\"D:\\\\foo\" W.</> \"C:bar\" == \"C:bar\"", test $ "D:\\foo" W.</> "C:bar" == "C:bar")
+    ,("\"C:\\\\foo\" W.</> \"C:bar\" == \"C:bar\"", test $ "C:\\foo" W.</> "C:bar" == "C:bar")
     ,("P.splitPath \"/directory/file.ext\" == [\"/\", \"directory/\", \"file.ext\"]", test $ P.splitPath "/directory/file.ext" == ["/", "directory/", "file.ext"])
     ,("W.splitPath \"/directory/file.ext\" == [\"/\", \"directory/\", \"file.ext\"]", test $ W.splitPath "/directory/file.ext" == ["/", "directory/", "file.ext"])
     ,("concat (P.splitPath x) == x", test $ \(QFilePath x) -> concat (P.splitPath x) == x)
@@ -369,6 +393,8 @@
     ,("P.normalise \"//home\" == \"/home\"", test $ P.normalise "//home" == "/home")
     ,("P.isValid \"\" == False", test $ P.isValid "" == False)
     ,("W.isValid \"\" == False", test $ W.isValid "" == False)
+    ,("P.isValid \"\\0\" == False", test $ P.isValid "\0" == False)
+    ,("W.isValid \"\\0\" == False", test $ W.isValid "\0" == False)
     ,("P.isValid \"/random_ path:*\" == True", test $ P.isValid "/random_ path:*" == True)
     ,("P.isValid x == not (null x)", test $ \(QFilePath x) -> P.isValid x == not (null x))
     ,("W.isValid \"c:\\\\test\" == True", test $ W.isValid "c:\\test" == True)
@@ -380,12 +406,17 @@
     ,("W.isValid \"\\\\\\\\\" == False", test $ W.isValid "\\\\" == False)
     ,("W.isValid \"\\\\\\\\\\\\foo\" == False", test $ W.isValid "\\\\\\foo" == False)
     ,("W.isValid \"\\\\\\\\?\\\\D:file\" == False", test $ W.isValid "\\\\?\\D:file" == False)
+    ,("W.isValid \"foo\\tbar\" == False", test $ W.isValid "foo\tbar" == False)
+    ,("W.isValid \"nul .txt\" == False", test $ W.isValid "nul .txt" == False)
+    ,("W.isValid \" nul.txt\" == True", test $ W.isValid " nul.txt" == True)
     ,("P.isValid (P.makeValid x)", test $ \(QFilePath x) -> P.isValid (P.makeValid x))
     ,("W.isValid (W.makeValid x)", test $ \(QFilePath x) -> W.isValid (W.makeValid x))
     ,("P.isValid x ==> P.makeValid x == x", test $ \(QFilePath x) -> P.isValid x ==> P.makeValid x == x)
     ,("W.isValid x ==> W.makeValid x == x", test $ \(QFilePath x) -> W.isValid x ==> W.makeValid x == x)
     ,("P.makeValid \"\" == \"_\"", test $ P.makeValid "" == "_")
     ,("W.makeValid \"\" == \"_\"", test $ W.makeValid "" == "_")
+    ,("P.makeValid \"file\\0name\" == \"file_name\"", test $ P.makeValid "file\0name" == "file_name")
+    ,("W.makeValid \"file\\0name\" == \"file_name\"", test $ W.makeValid "file\0name" == "file_name")
     ,("W.makeValid \"c:\\\\already\\\\/valid\" == \"c:\\\\already\\\\/valid\"", test $ W.makeValid "c:\\already\\/valid" == "c:\\already\\/valid")
     ,("W.makeValid \"c:\\\\test:of_test\" == \"c:\\\\test_of_test\"", test $ W.makeValid "c:\\test:of_test" == "c:\\test_of_test")
     ,("W.makeValid \"test*\" == \"test_\"", test $ W.makeValid "test*" == "test_")
@@ -395,6 +426,7 @@
     ,("W.makeValid \"c:\\\\nul\\\\file\" == \"c:\\\\nul_\\\\file\"", test $ W.makeValid "c:\\nul\\file" == "c:\\nul_\\file")
     ,("W.makeValid \"\\\\\\\\\\\\foo\" == \"\\\\\\\\drive\"", test $ W.makeValid "\\\\\\foo" == "\\\\drive")
     ,("W.makeValid \"\\\\\\\\?\\\\D:file\" == \"\\\\\\\\?\\\\D:\\\\file\"", test $ W.makeValid "\\\\?\\D:file" == "\\\\?\\D:\\file")
+    ,("W.makeValid \"nul .txt\" == \"nul _.txt\"", test $ W.makeValid "nul .txt" == "nul _.txt")
     ,("W.isRelative \"path\\\\test\" == True", test $ W.isRelative "path\\test" == True)
     ,("W.isRelative \"c:\\\\test\" == False", test $ W.isRelative "c:\\test" == False)
     ,("W.isRelative \"c:test\" == True", test $ W.isRelative "c:test" == True)
diff --git a/tests/TestUtil.hs b/tests/TestUtil.hs
--- a/tests/TestUtil.hs
+++ b/tests/TestUtil.hs
@@ -3,11 +3,13 @@
     (==>), QFilePath(..), QFilePathValidW(..), QFilePathValidP(..),
     test, Test,
     module Test.QuickCheck,
-    module Data.List
+    module Data.List,
+    module Data.Maybe
     ) where
 
 import Test.QuickCheck hiding ((==>))
 import Data.List
+import Data.Maybe
 import Control.Monad
 import qualified System.FilePath.Windows as W
 import qualified System.FilePath.Posix as P
