// Adapted from https://github.com/dotnet/runtime/blob/v5.0.12/src/libraries/Common/src/System/IO/PathInternal.Windows.cs // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // The MIT License (MIT) // // Copyright(c).NET Foundation and Contributors // // All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Runtime.CompilerServices; using System.IO; namespace Duplicati.Library.Common.IO { /// Contains internal path helpers that are shared between many projects. internal static class PathInternalWindows { // 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')); } /// /// Returns true if the path specified is relative to the current drive or working directory. /// Returns false if the path is fixed to a specific drive or UNC path. This method does no /// validation of the path (URIs will be returned as relative as a result). /// /// /// Handles paths that use the alternate directory separator. It is a frequent mistake to /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case. /// "C:a" is drive relative- meaning that it will be resolved against the current directory /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory /// will not be used to modify the path). /// internal static bool IsPartiallyQualified(string path) { 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])); } /// /// True if the given character is a directory separator. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsDirectorySeparator(char c) { return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; } } }