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:
Morilli 2024-09-05 06:46:23 +02:00
parent fa9c581d19
commit d9069ea2cc
3 changed files with 14 additions and 239 deletions

View File

@ -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>

View File

@ -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]));
}
}
}
}

View File

@ -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] == '\\';
}
}
}