From a8f93c3c642d601094761793c5260bb56b1e51af Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 8 Jun 2016 10:49:01 -0700 Subject: [PATCH 1/8] Fix the alternate path separator for Linux Because normalization of paths occurs through the location globber and filesystem provider by way of `path.Replace(alternate, default)`, changing the alternate path separator on Linux to be '\' instead of .NET's '/' let's PowerShell be "slash agnostic." --- .../engine/SessionStateStrings.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/SessionStateStrings.cs b/src/System.Management.Automation/engine/SessionStateStrings.cs index 4ea503ed5dc..93e40863c45 100644 --- a/src/System.Management.Automation/engine/SessionStateStrings.cs +++ b/src/System.Management.Automation/engine/SessionStateStrings.cs @@ -26,6 +26,9 @@ internal static class StringLiterals /// /// The default path separator used by the base implementation of the providers. + /// + /// Porting note: IO.Path.DirectorySeparatorChar is correct for all platforms. On Windows, + /// it is '\', and on Linux, it is '/', as expected. /// /// internal static readonly char DefaultPathSeparator = System.IO.Path.DirectorySeparatorChar; @@ -33,9 +36,14 @@ internal static class StringLiterals /// /// The alternate path separator used by the base implementation of the providers. + /// + /// Porting note: we do not use .NET's AlternatePathSeparatorChar here because it correctly + /// states that both the default and alternate are '/' on Linux. However, for PowerShell to + /// be "slash agnostic", we need to use the assumption that a '\' is the alternate path + /// separator on Linux. /// /// - internal static readonly char AlternatePathSeparator = System.IO.Path.AltDirectorySeparatorChar; + internal static readonly char AlternatePathSeparator = Platform.IsWindows ? '/' : '\\'; internal static readonly string AlternatePathSeparatorString = AlternatePathSeparator.ToString(); /// From ee40426fb307d8c78954a4901f4beca851f27c1b Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 8 Jun 2016 10:57:35 -0700 Subject: [PATCH 2/8] Fix path root comparisons for Linux Assuming the path may not be normalized, to make PowerShell slash agnostic in a filesystem whose "drive" is a '/' and a 'C:\', we need to compare to both '/' and '\' for users of PowerShell's alternate path separator on Linux ('\'). --- .../namespaces/FileSystemProvider.cs | 7 ++-- .../namespaces/LocationGlobber.cs | 40 ++++++++++++++----- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index 65541d578a6..bcbfa8cedd3 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -951,7 +951,7 @@ protected override Collection InitializeDefaultDrives() // add the filesystem with the root "/" to the initial drive list, // otherwise path handling will not work correctly because there // is no : available to separate the filesystems from each other - if (Platform.HasSingleRootFilesystem() && root != "/") + if (Platform.HasSingleRootFilesystem() && root != StringLiterals.DefaultPathSeparatorString) continue; // Porting notes: On non-windows platforms .net can report two @@ -4765,12 +4765,13 @@ protected override string GetParentPath (string path, string root) return parentPath; } // GetParentPath + // Note: we don't use IO.Path.IsPathRooted as this deals with "invalid" i.e. unnormalized paths private static bool IsAbsolutePath(string path) { bool result = false; - // this needs to be done differently on single root filesystems - if (Platform.HasSingleRootFilesystem() && path.StartsWith("/")) + // check if we're on a single root filesystem and it's an absolute path + if (LocationGlobber.IsSingleFileSystemAbsolutePath(path)) { return true; } diff --git a/src/System.Management.Automation/namespaces/LocationGlobber.cs b/src/System.Management.Automation/namespaces/LocationGlobber.cs index 4b5441c2925..35801274872 100644 --- a/src/System.Management.Automation/namespaces/LocationGlobber.cs +++ b/src/System.Management.Automation/namespaces/LocationGlobber.cs @@ -1570,6 +1570,30 @@ internal static bool IsProviderQualifiedPath(string path, out string providerId) return result; } // IsProviderQualifiedPath + /// + /// Determines if the given path is absolute while on a single root filesystem. + /// + /// + /// + /// Porting notes: absolute paths on non-Windows filesystems start with a '/' (no "C:" drive + /// prefix, the slash is the prefix). We compare against both '/' and '\' (default and + /// alternate path separator) in order for PowerShell to be slash agnostic. + /// + /// + /// + /// The path used in the determination + /// + /// + /// + /// Returns true if we're on a single root filesystem and the path is absolute. + /// + internal static bool IsSingleFileSystemAbsolutePath(string path) + { + return Platform.HasSingleRootFilesystem() && + (path.StartsWith(StringLiterals.DefaultPathSeparatorString, StringComparison.Ordinal) || + path.StartsWith(StringLiterals.AlternatePathSeparatorString, StringComparison.Ordinal)); + } // IsSingleFileSystemAbsolutePath + /// /// Determines if the given path is relative or absolute /// @@ -1619,11 +1643,8 @@ internal static bool IsAbsolutePath(string path) break; } - // non-windows porting notes: - // this needs to be handled differently on non-windows, paths starting with / are always absolute - // -> still continue with other isAbsolute processing, colons can still happen in drives - // of other providers - if (Platform.HasSingleRootFilesystem() && path.StartsWith("/",StringComparison.Ordinal)) + // check if we're on a single root filesystem and it's an absolute path + if (IsSingleFileSystemAbsolutePath(path)) { result = true; break; @@ -1716,13 +1737,10 @@ internal bool IsAbsolutePath(string path, out string driveName) break; } - // non-windows porting notes: - // this needs to be handled differently on non-windows, paths starting with / are always absolute - // -> still continue with other isAbsolute processing, colons can still happen in drives - // of other providers - if (Platform.HasSingleRootFilesystem() && path.StartsWith("/",StringComparison.Ordinal)) + // check if we're on a single root filesystem and it's an absolute path + if (IsSingleFileSystemAbsolutePath(path)) { - driveName = "/"; + driveName = StringLiterals.DefaultPathSeparatorString; result = true; break; } From 3464f9aee39c2a6fc79ba0f10b638edde735acb1 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 8 Jun 2016 11:01:05 -0700 Subject: [PATCH 3/8] Fix ExpandMshGlobPath for single root filesystems Reverted to original code and fixed correctly. --- .../namespaces/LocationGlobber.cs | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/System.Management.Automation/namespaces/LocationGlobber.cs b/src/System.Management.Automation/namespaces/LocationGlobber.cs index 35801274872..9c52df7eacb 100644 --- a/src/System.Management.Automation/namespaces/LocationGlobber.cs +++ b/src/System.Management.Automation/namespaces/LocationGlobber.cs @@ -3148,49 +3148,42 @@ private Collection ExpandMshGlobPath( } else { - // Platform note: - // this needs to be done differently on non-windows platforms - string unescapedPath = context.SuppressWildcardExpansion ? path : RemoveGlobEscaping(path); - // this is the output of the platform specific code right below - string resolvedPath; + string formatString = "{0}:" + StringLiterals.DefaultPathSeparator + "{1}"; - if (drive.VolumeSeparatedByColon) + // Check to see if its a hidden provider drive. + if (drive.Hidden) { - string formatString = "{0}:" + StringLiterals.DefaultPathSeparator + "{1}"; - - // Check to see if its a hidden provider drive. - if (drive.Hidden) + if (IsProviderDirectPath(unescapedPath)) { - if (IsProviderDirectPath(unescapedPath)) - { - formatString = "{1}"; - } - else - { - formatString = "{0}::{1}"; - } + formatString = "{1}"; } else { - if (path.StartsWith(StringLiterals.DefaultPathSeparator.ToString(), StringComparison.Ordinal)) - { - formatString = "{0}:{1}"; - } + formatString = "{0}::{1}"; + } + } + else + { + if (path.StartsWith(StringLiterals.DefaultPathSeparatorString, StringComparison.Ordinal)) + { + formatString = "{0}:{1}"; } + } + + // Porting note: if the volume is not separated by a colon (non-Windows filesystems), don't add it. + if (!drive.VolumeSeparatedByColon) + { + formatString = "{0}{1}"; + } - resolvedPath = + string resolvedPath = String.Format( System.Globalization.CultureInfo.InvariantCulture, formatString, drive.Name, unescapedPath); - } - else - { - resolvedPath = drive.Name + unescapedPath; - } // Since we didn't do globbing, be sure the path exists if (allowNonexistingPaths || From 3262a4b3ec0f7b32d6b098225197d3ee7a66e337 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 8 Jun 2016 11:05:58 -0700 Subject: [PATCH 4/8] Fix GetDriveQualifiedPath for single root filesystems Reverted to original code and fixed correctly. --- .../namespaces/LocationGlobber.cs | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/System.Management.Automation/namespaces/LocationGlobber.cs b/src/System.Management.Automation/namespaces/LocationGlobber.cs index 9c52df7eacb..5ebbfb52fd8 100644 --- a/src/System.Management.Automation/namespaces/LocationGlobber.cs +++ b/src/System.Management.Automation/namespaces/LocationGlobber.cs @@ -3343,13 +3343,10 @@ internal static string GetDriveQualifiedPath(string path, PSDriveInfo drive) } string result = path; + bool treatAsRelative = true; - // platform notes: - // this needs to be implemented differently depending if the drive uses colon to separate paths if (drive.VolumeSeparatedByColon) { - bool treatAsRelative = true; - // Ensure the drive name is the same as the portion of the path before // :. If not add the drive name and colon as if it was a relative path @@ -3370,29 +3367,37 @@ internal static string GetDriveQualifiedPath(string path, PSDriveInfo drive) } } } - - if (treatAsRelative) + } + else + { + if (IsAbsolutePath(path)) { - string formatString = "{0}:" + StringLiterals.DefaultPathSeparator + "{1}"; + treatAsRelative = false; + } + } - if (path.StartsWith(StringLiterals.DefaultPathSeparator.ToString(), StringComparison.Ordinal)) + if (treatAsRelative) + { + string formatString; + if (drive.VolumeSeparatedByColon) + { + formatString = "{0}:" + StringLiterals.DefaultPathSeparator + "{1}"; + if (path.StartsWith(StringLiterals.DefaultPathSeparatorString, StringComparison.Ordinal)) { formatString = "{0}:{1}"; } - result = - String.Format( - System.Globalization.CultureInfo.InvariantCulture, - formatString, - drive.Name, - path); } - } - else - { - if (path.StartsWith(drive.Name)) - result = path; else - result = drive.Name + path; + { + formatString = "{0}{1}"; + } + + result = + String.Format( + System.Globalization.CultureInfo.InvariantCulture, + formatString, + drive.Name, + path); } tracer.WriteLine("result = {0}", result); From d162437b7b1cf0e12f04e6bef6379fd03ca12da3 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 8 Jun 2016 11:07:07 -0700 Subject: [PATCH 5/8] Remove string literals in registry provider --- src/System.Management.Automation/namespaces/RegistryProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/namespaces/RegistryProvider.cs b/src/System.Management.Automation/namespaces/RegistryProvider.cs index e33492c8f6e..1951f59eead 100644 --- a/src/System.Management.Automation/namespaces/RegistryProvider.cs +++ b/src/System.Management.Automation/namespaces/RegistryProvider.cs @@ -3888,7 +3888,7 @@ private string NormalizePath(string path) if (!String.IsNullOrEmpty(path)) { - result = path.Replace('/', '\\'); + result = path.Replace(StringLiterals.AlternatePathSeparator, StringLiterals.DefaultPathSeparator); // Remove relative path tokens if (HasRelativePathTokens(path)) From b30aabc12b64115a1b96ad64126fc5822b091b00 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 8 Jun 2016 11:20:58 -0700 Subject: [PATCH 6/8] Revert string comparison changes These comparisons did not need to be changed as the input path is not modified. The normalized relate path created from the stack (if this code path is taken) is created with the correct path separators. --- .../namespaces/FileSystemProvider.cs | 19 +++-- .../namespaces/LocationGlobber.cs | 69 +++++++------------ .../namespaces/NavigationProviderBase.cs | 4 +- 3 files changed, 33 insertions(+), 59 deletions(-) diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index bcbfa8cedd3..8f691de069f 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -5240,18 +5240,15 @@ private string NormalizeRelativePathHelper (string path, string basePath) private string RemoveRelativeTokens(string path) { - string sep = System.IO.Path.DirectorySeparatorChar.ToString(); - string altSep = System.IO.Path.AltDirectorySeparatorChar.ToString(); - - string testPath = path.Replace(altSep,sep); + string testPath = path.Replace('/', '\\'); if ( - (testPath.IndexOf(sep, StringComparison.OrdinalIgnoreCase) < 0) || - testPath.StartsWith("." + sep, StringComparison.OrdinalIgnoreCase) || - testPath.StartsWith(".." + sep, StringComparison.OrdinalIgnoreCase) || - testPath.EndsWith(sep + ".", StringComparison.OrdinalIgnoreCase) || - testPath.EndsWith(sep + "..", StringComparison.OrdinalIgnoreCase) || - (testPath.IndexOf(sep + "." + sep, StringComparison.OrdinalIgnoreCase) > 0) || - (testPath.IndexOf(sep + ".." + sep, StringComparison.OrdinalIgnoreCase) > 0)) + (testPath.IndexOf("\\", StringComparison.OrdinalIgnoreCase) < 0) || + testPath.StartsWith(".\\", StringComparison.OrdinalIgnoreCase) || + testPath.StartsWith("..\\", StringComparison.OrdinalIgnoreCase) || + testPath.EndsWith("\\.", StringComparison.OrdinalIgnoreCase) || + testPath.EndsWith("\\..", StringComparison.OrdinalIgnoreCase) || + (testPath.IndexOf("\\.\\", StringComparison.OrdinalIgnoreCase) > 0) || + (testPath.IndexOf("\\..\\", StringComparison.OrdinalIgnoreCase) > 0)) { try { diff --git a/src/System.Management.Automation/namespaces/LocationGlobber.cs b/src/System.Management.Automation/namespaces/LocationGlobber.cs index 5ebbfb52fd8..313e9e0e6c5 100644 --- a/src/System.Management.Automation/namespaces/LocationGlobber.cs +++ b/src/System.Management.Automation/namespaces/LocationGlobber.cs @@ -2284,7 +2284,8 @@ internal string GenerateRelativePath( driveRootRelativeWorkingPath = String.Empty; - // Remove the \ or / from the drive relative path + // Remove the \ or / from the drive relative + // path path = path.Substring(1); @@ -2460,41 +2461,18 @@ internal string GenerateRelativePath( private bool HasRelativePathTokens(string path) { - if (System.IO.Path.DirectorySeparatorChar == '/') - { - string comparePath = path; - - // the next line will only replace something, if the directory separators - // are different on the platform - if (System.IO.Path.DirectorySeparatorChar != System.IO.Path.AltDirectorySeparatorChar) - comparePath = path.Replace(System.IO.Path.AltDirectorySeparatorChar,System.IO.Path.DirectorySeparatorChar); - - return ( - comparePath.Equals(".", StringComparison.OrdinalIgnoreCase) || - comparePath.Equals("..", StringComparison.OrdinalIgnoreCase) || - comparePath.Contains("/./") || - comparePath.Contains("/../") || - comparePath.EndsWith("/..", StringComparison.OrdinalIgnoreCase) || - comparePath.EndsWith("/.", StringComparison.OrdinalIgnoreCase) || - comparePath.StartsWith("../", StringComparison.OrdinalIgnoreCase) || - comparePath.StartsWith("./", StringComparison.OrdinalIgnoreCase) || - comparePath.StartsWith("~", StringComparison.OrdinalIgnoreCase)); - } - else - { - string comparePath = path.Replace('/', '\\'); - - return ( - comparePath.Equals(".", StringComparison.OrdinalIgnoreCase) || - comparePath.Equals("..", StringComparison.OrdinalIgnoreCase) || - comparePath.Contains("\\.\\") || - comparePath.Contains("\\..\\") || - comparePath.EndsWith("\\..", StringComparison.OrdinalIgnoreCase) || - comparePath.EndsWith("\\.", StringComparison.OrdinalIgnoreCase) || - comparePath.StartsWith("..\\", StringComparison.OrdinalIgnoreCase) || - comparePath.StartsWith(".\\", StringComparison.OrdinalIgnoreCase) || - comparePath.StartsWith("~", StringComparison.OrdinalIgnoreCase)); - } + string comparePath = path.Replace('/', '\\'); + + return ( + comparePath.Equals(".", StringComparison.OrdinalIgnoreCase) || + comparePath.Equals("..", StringComparison.OrdinalIgnoreCase) || + comparePath.Contains("\\.\\") || + comparePath.Contains("\\..\\") || + comparePath.EndsWith("\\..", StringComparison.OrdinalIgnoreCase) || + comparePath.EndsWith("\\.", StringComparison.OrdinalIgnoreCase) || + comparePath.StartsWith("..\\", StringComparison.OrdinalIgnoreCase) || + comparePath.StartsWith(".\\", StringComparison.OrdinalIgnoreCase) || + comparePath.StartsWith("~", StringComparison.OrdinalIgnoreCase)); } /// @@ -3179,11 +3157,11 @@ private Collection ExpandMshGlobPath( } string resolvedPath = - String.Format( - System.Globalization.CultureInfo.InvariantCulture, - formatString, - drive.Name, - unescapedPath); + String.Format( + System.Globalization.CultureInfo.InvariantCulture, + formatString, + drive.Name, + unescapedPath); // Since we didn't do globbing, be sure the path exists if (allowNonexistingPaths || @@ -3394,13 +3372,12 @@ internal static string GetDriveQualifiedPath(string path, PSDriveInfo drive) result = String.Format( - System.Globalization.CultureInfo.InvariantCulture, - formatString, - drive.Name, - path); + System.Globalization.CultureInfo.InvariantCulture, + formatString, + drive.Name, + path); } - tracer.WriteLine("result = {0}", result); return result; } // GetDriveQualifiedPath diff --git a/src/System.Management.Automation/namespaces/NavigationProviderBase.cs b/src/System.Management.Automation/namespaces/NavigationProviderBase.cs index b234acd0917..beeb0d80d22 100644 --- a/src/System.Management.Automation/namespaces/NavigationProviderBase.cs +++ b/src/System.Management.Automation/namespaces/NavigationProviderBase.cs @@ -416,8 +416,8 @@ protected string MakePath(string parent, string child, bool childIsLeaf) } else { - // Normalize the path so that only the backslash is used as a separator even if the - // user types a forward slash. + // Normalize the path so that only the default path separator is used as a + // separator even if the user types the alternate slash. parent = parent.Replace(StringLiterals.AlternatePathSeparator, StringLiterals.DefaultPathSeparator); child = child.Replace(StringLiterals.AlternatePathSeparator, StringLiterals.DefaultPathSeparator); From af258919b73a05685e1e2fdf0c8ce7dd0ab754f1 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Fri, 27 May 2016 13:46:24 -0700 Subject: [PATCH 7/8] Add tests for hierarchical paths --- test/powershell/Hierarchical-Path.Tests.ps1 | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/powershell/Hierarchical-Path.Tests.ps1 diff --git a/test/powershell/Hierarchical-Path.Tests.ps1 b/test/powershell/Hierarchical-Path.Tests.ps1 new file mode 100644 index 00000000000..f45c1e8b8de --- /dev/null +++ b/test/powershell/Hierarchical-Path.Tests.ps1 @@ -0,0 +1,46 @@ +Describe "Hierarchical paths" { + BeforeAll { + $data = "Hello World" + Setup -File testFile.txt -Content $data + } + + It "should work with Join-Path " { + $testPath = Join-Path $TestDrive testFile.txt + Get-Content $testPath | Should Be $data + } + + It "should work with platform's slashes" { + $testPath = "$TestDrive$([IO.Path]::DirectorySeparatorChar)testFile.txt" + Get-Content $testPath | Should Be $data + } + + It "should work with forward slashes" { + $testPath = "$TestDrive/testFile.txt" + Get-Content $testPath | Should Be $data + } + + It "should work with backward slashes" { + $testPath = "$TestDrive\testFile.txt" + Get-Content $testPath | Should Be $data + } + + It "should work with backward slashes for each separator" { + $testPath = "$TestDrive\testFile.txt".Replace("/","\") + Get-Content $testPath | should be $data + } + + It "should work with forward slashes for each separator" { + $testPath = "$TestDrive/testFile.txt".Replace("\","/") + Get-Content $testPath | should be $data + } + + It "should work even if there are too many forward slashes" { + $testPath = "$TestDrive//////testFile.txt" + Get-Content $testPath | should be $data + } + + It "should work even if there are too many backward slashes" { + $testPath = "$TestDrive\\\\\\\testFile.txt" + Get-Content $testPath | should be $data + } +} From 0232f160ec89b9ef0fe223878fe933374649ac04 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 15 Jun 2016 12:16:03 -0700 Subject: [PATCH 8/8] Add backward slash in filename as known issue --- docs/KNOWNISSUES.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/KNOWNISSUES.md b/docs/KNOWNISSUES.md index 34f6f312e66..5b6587183cf 100644 --- a/docs/KNOWNISSUES.md +++ b/docs/KNOWNISSUES.md @@ -87,3 +87,38 @@ following cmdlets that exist in the FullCLR version: - Send-MailMessage - Show-Command - Update-List + +## File paths with literal backward slashes + +On some filesystems (Linux, OS X), file paths are allowed to contain literal +backward slashes, '\', as valid filename characters. These slashes, when +escaped, are not directory separators. In Bash, the backward slash is the escape +character, so a `path/with/a\\slash` is two directories, `path` and `with`, and +one file, `a\slash`. In PowerShell, we *will* support this using the normal +backtick escape character, so a `path\with\a``\slash` or a +`path/with/a``\slash`, but this edge case is *currently unsupported*. + +That being said, native commands will work as expected. Thus this is the current +scenario: + +```powershell +PS > Get-Content a`\slash +Get-Content : Cannot find path '/home/andrew/src/PowerShell/a/slash' because it does not exist. +At line:1 char:1 ++ Get-Content a`\slash ++ ~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : ObjectNotFound: (/home/andrew/src/PowerShell/a/slash:String) [Get-Co + ntent], ItemNotFoundException + + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentCommand + +PS > /bin/cat a\slash +hi + +``` + +The PowerShell cmdlet `Get-Content` cannot yet understand the escaped backward +slash, but the path is passed literally to the native command `/bin/cat`. Most +file operations are thus implicitly supported by the native commands. The +notable exception is `cd` since it is not a command, but a shell built-in, +`Set-Location`. So until this issue is resolved, PowerShell cannot change to a +directory whose name contains a literal backward slash.