Skip to content
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
35 changes: 35 additions & 0 deletions docs/KNOWNISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
10 changes: 9 additions & 1 deletion src/System.Management.Automation/engine/SessionStateStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,24 @@ internal static class StringLiterals

/// <summary>
/// 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.
/// </summary>
///
internal static readonly char DefaultPathSeparator = System.IO.Path.DirectorySeparatorChar;
internal static readonly string DefaultPathSeparatorString = DefaultPathSeparator.ToString();

/// <summary>
/// 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.
/// </summary>
///
internal static readonly char AlternatePathSeparator = System.IO.Path.AltDirectorySeparatorChar;
internal static readonly char AlternatePathSeparator = Platform.IsWindows ? '/' : '\\';
internal static readonly string AlternatePathSeparatorString = AlternatePathSeparator.ToString();

/// <summary>
Expand Down
26 changes: 12 additions & 14 deletions src/System.Management.Automation/namespaces/FileSystemProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ protected override Collection<PSDriveInfo> 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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -5239,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
{
Expand Down
189 changes: 91 additions & 98 deletions src/System.Management.Automation/namespaces/LocationGlobber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,30 @@ internal static bool IsProviderQualifiedPath(string path, out string providerId)
return result;
} // IsProviderQualifiedPath

/// <summary>
/// Determines if the given path is absolute while on a single root filesystem.
/// </summary>
///
/// <remarks>
/// 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.
/// </remarks>
///
/// <param name="path">
/// The path used in the determination
/// </param>
///
/// <returns>
/// Returns true if we're on a single root filesystem and the path is absolute.
/// </returns>
internal static bool IsSingleFileSystemAbsolutePath(string path)
{
return Platform.HasSingleRootFilesystem() &&
(path.StartsWith(StringLiterals.DefaultPathSeparatorString, StringComparison.Ordinal) ||
path.StartsWith(StringLiterals.AlternatePathSeparatorString, StringComparison.Ordinal));
} // IsSingleFileSystemAbsolutePath

/// <summary>
/// Determines if the given path is relative or absolute
/// </summary>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -2266,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);

Expand Down Expand Up @@ -2442,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));
}

/// <summary>
Expand Down Expand Up @@ -3130,50 +3126,43 @@ private Collection<string> 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}";
}

resolvedPath =
String.Format(
System.Globalization.CultureInfo.InvariantCulture,
formatString,
drive.Name,
unescapedPath);
}
else
{
resolvedPath = drive.Name + unescapedPath;
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}";
}

string resolvedPath =
String.Format(
System.Globalization.CultureInfo.InvariantCulture,
formatString,
drive.Name,
unescapedPath);

// Since we didn't do globbing, be sure the path exists
if (allowNonexistingPaths ||
provider.ItemExists(GetProviderPath(resolvedPath, context), context))
Expand Down Expand Up @@ -3332,13 +3321,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

Expand All @@ -3359,32 +3345,39 @@ 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);
return result;
} // GetDriveQualifiedPath

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading