diff --git a/src/BizHawk.Common/Extensions/PathExtensions.cs b/src/BizHawk.Common/Extensions/PathExtensions.cs index fe9dac7bec..c013fdc592 100644 --- a/src/BizHawk.Common/Extensions/PathExtensions.cs +++ b/src/BizHawk.Common/Extensions/PathExtensions.cs @@ -4,7 +4,7 @@ using BizHawk.Common.StringExtensions; namespace BizHawk.Common.PathExtensions { - public static partial class PathExtensions + public static class PathExtensions { /// iff indicates a child of , with being returned if either path is /// algorithm for Windows taken from https://stackoverflow.com/a/7710620/7467292 @@ -48,7 +48,19 @@ namespace BizHawk.Common.PathExtensions /// 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 } /// iff absolute (OS-dependent) diff --git a/src/BizHawk.Common/Extensions/Pillaged/PathInternal.Unix.cs b/src/BizHawk.Common/Extensions/Pillaged/PathInternal.Unix.cs deleted file mode 100644 index 6dfeb0a299..0000000000 --- a/src/BizHawk.Common/Extensions/Pillaged/PathInternal.Unix.cs +++ /dev/null @@ -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 -{ - /// Contains internal path helpers that are shared between many projects. - internal static partial class PathInternal - { - public static bool IsPathFullyQualified(string path) - { - return !PathInternal.IsPartiallyQualified(path); - } - - internal static int GetRootLength(ReadOnlySpan path) - { - return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0; - } - - internal static bool EndsInDirectorySeparator(ReadOnlySpan path) - => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); - - internal static ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan path) => - EndsInDirectorySeparator(path) && !IsRoot(path) ? - path.Slice(0, path.Length - 1) : - path; - - internal static bool IsRoot(ReadOnlySpan 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])); - } - } - } -} diff --git a/src/BizHawk.Common/Extensions/Pillaged/PathInternal.Windows.cs b/src/BizHawk.Common/Extensions/Pillaged/PathInternal.Windows.cs deleted file mode 100644 index c05b908ae6..0000000000 --- a/src/BizHawk.Common/Extensions/Pillaged/PathInternal.Windows.cs +++ /dev/null @@ -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 -{ - /// Contains internal path helpers that are shared between many projects. - 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; - - /// - /// Returns true if the given character is a valid drive letter - /// - 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 == '.'; - } - - /// - /// 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) - /// - [return: NotNullIfNotNull("path")] - internal static string? EnsureExtendedPrefixIfNeeded(string? path) - { - if (path != null && (path.Length >= MaxShortPath || EndsWithPeriodOrSpace(path))) - { - return EnsureExtendedPrefix(path); - } - else - { - return path; - } - } - - /// - /// Adds the extended path prefix (\\?\) if not relative or already a device path. - /// - 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; - } - - /// - /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") - /// - 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]) - ); - } - - /// - /// 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. - /// - 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] == '\\'; - } - - } -}