Simplify PathExtensions.IsAbsolute
I've somewhat extensively tested this and this should be good. I've gotten to the point where even `Path.IsPathFullyQualified` returns wrong results (for "\?test"), so I'm not gonna look further.
This commit is contained in:
parent
fa9c581d19
commit
d9069ea2cc
|
@ -4,7 +4,7 @@ using BizHawk.Common.StringExtensions;
|
|||
|
||||
namespace BizHawk.Common.PathExtensions
|
||||
{
|
||||
public static partial class PathExtensions
|
||||
public static class PathExtensions
|
||||
{
|
||||
/// <returns><see langword="true"/> iff <paramref name="childPath"/> indicates a child of <paramref name="parentPath"/>, with <see langword="false"/> being returned if either path is <see langword="null"/></returns>
|
||||
/// <remarks>algorithm for Windows taken from https://stackoverflow.com/a/7710620/7467292</remarks>
|
||||
|
@ -48,7 +48,19 @@ namespace BizHawk.Common.PathExtensions
|
|||
/// <seealso cref="IsRelative"/>
|
||||
public static bool IsAbsolute(this string path)
|
||||
{
|
||||
return PathInternal.IsPathFullyQualified(path);
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
|
||||
return Path.IsPathFullyQualified(path);
|
||||
#else
|
||||
if (OSTailoredCode.IsUnixHost)
|
||||
{
|
||||
return path.StartsWith(Path.DirectorySeparatorChar);
|
||||
}
|
||||
else
|
||||
{
|
||||
var root = Path.GetPathRoot(path);
|
||||
return root.StartsWithOrdinal(@"\\") || root.EndsWith('\\') && root is not @"\";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <returns><see langword="false"/> iff absolute (OS-dependent)</returns>
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#pragma warning disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace BizHawk.Common.PathExtensions
|
||||
{
|
||||
/// <summary>Contains internal path helpers that are shared between many projects.</summary>
|
||||
internal static partial class PathInternal
|
||||
{
|
||||
public static bool IsPathFullyQualified(string path)
|
||||
{
|
||||
return !PathInternal.IsPartiallyQualified(path);
|
||||
}
|
||||
|
||||
internal static int GetRootLength(ReadOnlySpan<char> path)
|
||||
{
|
||||
return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0;
|
||||
}
|
||||
|
||||
internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path)
|
||||
=> path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);
|
||||
|
||||
internal static ReadOnlySpan<char> TrimEndingDirectorySeparator(ReadOnlySpan<char> path) =>
|
||||
EndsInDirectorySeparator(path) && !IsRoot(path) ?
|
||||
path.Slice(0, path.Length - 1) :
|
||||
path;
|
||||
|
||||
internal static bool IsRoot(ReadOnlySpan<char> path)
|
||||
=> path.Length == GetRootLength(path);
|
||||
|
||||
internal static bool IsDirectorySeparator(char c)
|
||||
{
|
||||
// The alternate directory separator char is the same as the directory separator,
|
||||
// so we only need to check one.
|
||||
if (OSTailoredCode.IsUnixHost)
|
||||
{
|
||||
Debug.Assert(Path.DirectorySeparatorChar == Path.AltDirectorySeparatorChar);
|
||||
return c == Path.DirectorySeparatorChar;
|
||||
}
|
||||
else
|
||||
{
|
||||
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsPartiallyQualified(string path)
|
||||
{
|
||||
if (OSTailoredCode.IsUnixHost)
|
||||
{
|
||||
// This is much simpler than Windows where paths can be rooted, but not fully qualified (such as Drive Relative)
|
||||
// As long as the path is rooted in Unix it doesn't use the current directory and therefore is fully qualified.
|
||||
return string.IsNullOrEmpty(path) || path[0] != Path.DirectorySeparatorChar;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (path.Length < 2)
|
||||
{
|
||||
// It isn't fixed, it must be relative. There is no way to specify a fixed
|
||||
// path with one character (or less).
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsDirectorySeparator(path[0]))
|
||||
{
|
||||
// There is no valid way to specify a relative path with two initial slashes or
|
||||
// \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
|
||||
return !(path[1] == '?' || IsDirectorySeparator(path[1]));
|
||||
}
|
||||
|
||||
// The only way to specify a fixed path that doesn't begin with two slashes
|
||||
// is the drive, colon, slash format- i.e. C:\
|
||||
return !((path.Length >= 3)
|
||||
&& (path[1] == Path.VolumeSeparatorChar)
|
||||
&& IsDirectorySeparator(path[2])
|
||||
// To match old behavior we'll check the drive character for validity as the path is technically
|
||||
// not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
|
||||
&& IsValidDriveChar(path[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#pragma warning disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace BizHawk.Common.PathExtensions
|
||||
{
|
||||
/// <summary>Contains internal path helpers that are shared between many projects.</summary>
|
||||
internal static partial class PathInternal
|
||||
{
|
||||
// All paths in Win32 ultimately end up becoming a path to a File object in the Windows object manager. Passed in paths get mapped through
|
||||
// DosDevice symbolic links in the object tree to actual File objects under \Devices. To illustrate, this is what happens with a typical
|
||||
// path "Foo" passed as a filename to any Win32 API:
|
||||
//
|
||||
// 1. "Foo" is recognized as a relative path and is appended to the current directory (say, "C:\" in our example)
|
||||
// 2. "C:\Foo" is prepended with the DosDevice namespace "\??\"
|
||||
// 3. CreateFile tries to create an object handle to the requested file "\??\C:\Foo"
|
||||
// 4. The Object Manager recognizes the DosDevices prefix and looks
|
||||
// a. First in the current session DosDevices ("\Sessions\1\DosDevices\" for example, mapped network drives go here)
|
||||
// b. If not found in the session, it looks in the Global DosDevices ("\GLOBAL??\")
|
||||
// 5. "C:" is found in DosDevices (in our case "\GLOBAL??\C:", which is a symbolic link to "\Device\HarddiskVolume6")
|
||||
// 6. The full path is now "\Device\HarddiskVolume6\Foo", "\Device\HarddiskVolume6" is a File object and parsing is handed off
|
||||
// to the registered parsing method for Files
|
||||
// 7. The registered open method for File objects is invoked to create the file handle which is then returned
|
||||
//
|
||||
// There are multiple ways to directly specify a DosDevices path. The final format of "\??\" is one way. It can also be specified
|
||||
// as "\\.\" (the most commonly documented way) and "\\?\". If the question mark syntax is used the path will skip normalization
|
||||
// (essentially GetFullPathName()) and path length checks.
|
||||
|
||||
// Windows Kernel-Mode Object Manager
|
||||
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff565763.aspx
|
||||
// https://channel9.msdn.com/Shows/Going+Deep/Windows-NT-Object-Manager
|
||||
//
|
||||
// Introduction to MS-DOS Device Names
|
||||
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff548088.aspx
|
||||
//
|
||||
// Local and Global MS-DOS Device Names
|
||||
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff554302.aspx
|
||||
|
||||
internal const string ExtendedDevicePathPrefix = @"\\?\";
|
||||
internal const string UncPathPrefix = @"\\";
|
||||
internal const string UncDevicePrefixToInsert = @"?\UNC\";
|
||||
internal const string UncExtendedPathPrefix = @"\\?\UNC\";
|
||||
internal const string DevicePathPrefix = @"\\.\";
|
||||
|
||||
internal const int MaxShortPath = 260;
|
||||
|
||||
// \\?\, \\.\, \??\
|
||||
internal const int DevicePrefixLength = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given character is a valid drive letter
|
||||
/// </summary>
|
||||
internal static bool IsValidDriveChar(char value)
|
||||
{
|
||||
return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'));
|
||||
}
|
||||
|
||||
private static bool EndsWithPeriodOrSpace(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return false;
|
||||
|
||||
char c = path[path.Length - 1];
|
||||
return c == ' ' || c == '.';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the extended path prefix (\\?\) if not already a device path, IF the path is not relative,
|
||||
/// AND the path is more than 259 characters. (> MAX_PATH + null). This will also insert the extended
|
||||
/// prefix if the path ends with a period or a space. Trailing periods and spaces are normally eaten
|
||||
/// away from paths during normalization, but if we see such a path at this point it should be
|
||||
/// normalized and has retained the final characters. (Typically from one of the *Info classes)
|
||||
/// </summary>
|
||||
[return: NotNullIfNotNull("path")]
|
||||
internal static string? EnsureExtendedPrefixIfNeeded(string? path)
|
||||
{
|
||||
if (path != null && (path.Length >= MaxShortPath || EndsWithPeriodOrSpace(path)))
|
||||
{
|
||||
return EnsureExtendedPrefix(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the extended path prefix (\\?\) if not relative or already a device path.
|
||||
/// </summary>
|
||||
internal static string EnsureExtendedPrefix(string path)
|
||||
{
|
||||
// Putting the extended prefix on the path changes the processing of the path. It won't get normalized, which
|
||||
// means adding to relative paths will prevent them from getting the appropriate current directory inserted.
|
||||
|
||||
// If it already has some variant of a device path (\??\, \\?\, \\.\, //./, etc.) we don't need to change it
|
||||
// as it is either correct or we will be changing the behavior. When/if Windows supports long paths implicitly
|
||||
// in the future we wouldn't want normalization to come back and break existing code.
|
||||
|
||||
// In any case, all internal usages should be hitting normalize path (Path.GetFullPath) before they hit this
|
||||
// shimming method. (Or making a change that doesn't impact normalization, such as adding a filename to a
|
||||
// normalized base path.)
|
||||
if (IsPartiallyQualified(path) || IsDevice(path))
|
||||
return path;
|
||||
|
||||
// Given \\server\share in longpath becomes \\?\UNC\server\share
|
||||
if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
return path.Insert(2, UncDevicePrefixToInsert);
|
||||
|
||||
return ExtendedDevicePathPrefix + path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\")
|
||||
/// </summary>
|
||||
internal static bool IsDevice(string path)
|
||||
{
|
||||
// If the path begins with any two separators is will be recognized and normalized and prepped with
|
||||
// "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not.
|
||||
return IsExtended(path)
|
||||
||
|
||||
(
|
||||
path.Length >= DevicePrefixLength
|
||||
&& IsDirectorySeparator(path[0])
|
||||
&& IsDirectorySeparator(path[1])
|
||||
&& (path[2] == '.' || path[2] == '?')
|
||||
&& IsDirectorySeparator(path[3])
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the
|
||||
/// path matches exactly (cannot use alternate directory separators) Windows will skip normalization
|
||||
/// and path length checks.
|
||||
/// </summary>
|
||||
internal static bool IsExtended(string path)
|
||||
{
|
||||
// While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
|
||||
// Skipping of normalization will *only* occur if back slashes ('\') are used.
|
||||
return path.Length >= DevicePrefixLength
|
||||
&& path[0] == '\\'
|
||||
&& (path[1] == '\\' || path[1] == '?')
|
||||
&& path[2] == '?'
|
||||
&& path[3] == '\\';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue