8000 Enhance Remove-Item to work with OneDrive (#15571) · PowerShell/PowerShell@45adfee · GitHub
[go: up one dir, main page]

Skip to content 8000

Commit 45adfee

Browse files
authored
Enhance Remove-Item to work with OneDrive (#15571)
1 parent 9204272 commit 45adfee

File tree

6 files changed

+463
-152
lines changed

6 files changed

+463
-152
lines changed

src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2699,7 +2699,7 @@ protected override void ProcessRecord()
26992699
try
27002700
{
27012701
System.IO.DirectoryInfo di = new(providerPath);
2702-
if (di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
2702+
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(di))
27032703
{
27042704
shouldRecurse = false;
27052705
treatAsFile = true;

src/System.Management.Automation/engine/Utils.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1864,7 +1864,7 @@ internal static string GetFormatStyleString(FormatStyle formatStyle)
18641864

18651865
if (ExperimentalFeature.IsEnabled("PSAnsiRendering"))
18661866
{
1867-
PSStyle psstyle = PSStyle.Instance;
1867+
PSStyle psstyle = PSStyle.Instance;
18681868
switch (formatStyle)
18691869
{
18701870
case FormatStyle.Reset:
@@ -2104,6 +2104,14 @@ public static class InternalTestHooks
21042104

21052105
internal static bool ThrowExdevErrorOnMoveDirectory;
21062106

2107+
// To emulate OneDrive behavior we use the hard-coded symlink.
2108+
// If OneDriveTestRecurseOn is false then the symlink works as regular symlink.
2109+
// If OneDriveTestRecurseOn is true then we recurse into the symlink as OneDrive should work.
2110+
// OneDriveTestSymlinkName defines the symlink name used in tests.
2111+
internal static bool OneDriveTestOn;
2112+
internal static bool OneDriveTestRecurseOn;
2113+
internal static string OneDriveTestSymlinkName = "link-Beta";
2114+
21072115
/// <summary>This member is used for internal test purposes.</summary>
21082116
public static void SetTestHook(string property, object value)
21092117
{

src/System.Management.Automation/namespaces/FileSystemProvider.cs

Lines changed: 62 additions & 22 deletions
}
Original file line numberDiff line numberDiff line change
@@ -1891,9 +1891,14 @@ private void Dir(
18911891
}
18921892

18931893
bool hidden = false;
1894+
bool checkReparsePoint = true;
18941895
if (!Force)
18951896
{
18961897
hidden = (recursiveDirectory.Attributes & FileAttributes.Hidden) != 0;
1898+
1899+
// We've already taken the expense of initializing the Attributes property here,
1900+
// so we can use that to avoid needing to call IsReparsePointLikeSymlink() later.
1901+
checkReparsePoint = recursiveDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint);
18971902
}
18981903

18991904
// if "Hidden" is explicitly specified anywhere in the attribute filter, then override
@@ -1907,7 +1912,7 @@ private void Dir(
19071912
// c) it is not a reparse point with a target (not OneDrive or an AppX link).
19081913
if (tracker == null)
19091914
{
1910-
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(recursiveDirectory))
1915+
if (checkReparsePoint && InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(recursiveDirectory))
19111916
{
19121917
continue;
19131918
}
@@ -2062,7 +2067,7 @@ public static string NameString(PSObject instance)
20622067
{
20632068
if (instance?.BaseObject is FileSystemInfo fileInfo)
20642069
{
2065-
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo))
2070+
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(fileInfo))
20662071
{
20672072
return $"{PSStyle.Instance.FileInfo.SymbolicLink}{fileInfo.Name}{PSStyle.Instance.Reset} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}";
20682073
}
@@ -2090,7 +2095,7 @@ public static string NameString(PSObject instance)
20902095
else
20912096
{
20922097
return instance?.BaseObject is FileSystemInfo fileInfo
2093-
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo)
2098+
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(fileInfo)
20942099
? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}"
20952100
: fileInfo.Name
20962101
: string.Empty;
@@ -3131,22 +3136,31 @@ private void RemoveDirectoryInfoItem(DirectoryInfo directory, bool recurse, bool
31313136
continueRemoval = ShouldProcess(directory.FullName, action);
31323137
}
31333138

3134-
if (directory.Attributes.HasFlag(FileAttributes.ReparsePoint))
3139+
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(directory))
31353140
{
3141+
void WriteErrorHelper(Exception exception)
3142+
{
3143+
WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
3144+
}
3145+
31363146
try
31373147
{
3138-
// TODO:
3139-
// Different symlinks seem to vary by behavior.
3140-
// In particular, OneDrive symlinks won't remove without recurse,
3141-
// but the .NET API here does not allow us to distinguish them.
3142-
// We may need to revisit using p/Invokes here to get the right behavior
3143-
directory.Delete();
3148+
if (InternalTestHooks.OneDriveTestOn)
3149+
{
3150+
WriteErrorHelper(new IOException());
3151+
return;
3152+
}
3153+
else
3154+
{
3155+
// Name surrogates should just be detached.
3156+
directory.Delete();
3157+
}
31443158
}
31453159
catch (Exception e)
31463160
{
31473161
string error = StringUtil.Format(FileSystemProviderStrings.CannotRemoveItem, directory.FullName, e.Message);
31483162
var exception = new IOException(error, e);
3149-
WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
3163+
WriteErrorHelper(exception);
31503164
}
31513165

31523166
return;
@@ -8056,8 +8070,7 @@ protected override bool ReleaseHandle()
80568070
private static extern bool FindClose(IntPtr handle);
80578071
}
80588072

8059-
// SetLastError is false as the use of this API doesn't not require GetLastError() to be called
8060-
[DllImport(PinvokeDllNames.FindFirstFileDllName, EntryPoint = "FindFirstFileExW", SetLastError = false, CharSet = CharSet.Unicode)]
8073+
[DllImport(PinvokeDllNames.FindFirstFileDllName, EntryPoint = "FindFirstFileExW", SetLastError = true, CharSet = CharSet.Unicode)]
80618074
private static extern SafeFindHandle FindFirstFileEx(string lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, ref WIN32_FIND_DATA lpFindFileData, FINDEX_SEARCH_OPS fSearchOp, IntPtr lpSearchFilter, int dwAdditionalFlags);
80628075

80638076
internal enum FINDEX_INFO_LEVELS : uint
@@ -8248,28 +8261,55 @@ internal static bool IsReparsePoint(FileSystemInfo fileInfo)
82488261
return fileInfo.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint);
82498262
}
82508263

8251-
internal static bool IsReparsePointWithTarget(FileSystemInfo fileInfo)
8264+
internal static bool IsReparsePointLikeSymlink(FileSystemInfo fileInfo)
82528265
{
8253-
if (!IsReparsePoint(fileInfo))
8266+
#if UNIX
8267+
// Reparse point on Unix is a symlink.
8268+
return IsReparsePoint(fileInfo);
8269+
#else
8270+
if (InternalTestHooks.OneDriveTestOn && fileInfo.Name == InternalTestHooks.OneDriveTestSymlinkName)
82548271
{
8255-
return false;
8272+
return !InternalTestHooks.OneDriveTestRecurseOn;
82568273
}
8257-
#if !UNIX
8258-
// It is a reparse point and we should check some reparse point tags.
8259-
var data = new WIN32_FIND_DATA();
8260-
using (var handle = FindFirstFileEx(fileInfo.FullName, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
8274+
8275+
WIN32_FIND_DATA data = default;
8276+
string fullPath = Path.TrimEndingDirectorySeparator(fileInfo.FullName);
8277+
if (fullPath.Length > MAX_PATH)
8278+
{
8279+
fullPath = PathUtils.EnsureExtendedPrefix(fullPath);
8280+
}
8281+
8282+
using (SafeFindHandle handle = FindFirstFileEx(fullPath, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
82618283
{
8284+
if (handle.IsInvalid)
8285+
{
8286+
// Our handle could be invalidated by something else touching the filesystem,
8287+
// so ensure we deal with that possibility here
8288+
int lastError = Marshal.GetLastWin32Error();
8289+
throw new Win32Exception(lastError);
8290+
}
8291+
8292+
// We already have the file attribute information from our Win32 call,
8293+
// so no need to take the expense of the FileInfo.FileAttributes call
8294+
const int FILE_ATTRIBUTE_REPARSE_POINT = 0x0400;
8295+
if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
8296+
{
8297+
// Not a reparse point.
8298+
return false;
8299+
}
8300+
82628301
// The name surrogate bit 0x20000000 is defined in https://docs.microsoft.com/windows/win32/fileio/reparse-point-tags
82638302
// Name surrogates (0x20000000) are reparse points that point to other named entities local to the filesystem
82648303
// (like symlinks and mount points).
82658304
// In the case of OneDrive, they are not name surrogates and would be safe to recurse into.
8266-
if (!handle.IsInvalid && (data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 != IO_REPARSE_TAG_APPEXECLINK))
8305+
if ((data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 != IO_REPARSE_TAG_APPEXECLINK))
82678306
{
82688307
return false;
82698308
82708309
}
8271-
#endif
8310+
82728311
return true;
8312+
#endif
82738313
}
82748314

82758315
internal static bool WinIsHardLink(FileSystemInfo fileInfo)

src/System.Management.Automation/utils/PathUtils.cs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
using System.Collections.Generic;
55
using System.Globalization;
66
using System.IO;
7+
using System.Management.Automation.Internal;
8+
using System.Runtime.CompilerServices;
79
using System.Text;
810

9-
using System.Management.Automation.Internal;
1011
using Dbg = System.Management.Automation.Diagnostics;
1112

1213
namespace System.Management.Automation
@@ -447,5 +448,110 @@ internal static bool TryDeleteFile(string filepath)
447448

448449
return false;
449450
}
451+
452+
#region Helpers for long paths from .Net Runtime
453+
454+
// Code here is copied from .NET's internal path helper implementation:
455+
// https://github.com/dotnet/runtime/blob/dcce0f56e10f5ac9539354b049341a2d7c0cdebf/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs
456+
// It has been left as a verbatim copy.
457+
458+
internal static string EnsureExtendedPrefix(string path)
459+
{
460+
if (IsPartiallyQualified(path) || IsDevice(path))
461+
return path;
462+
463+
// Given \\server\share in longpath becomes \\?\UNC\server\share
464+
if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase))
465+
return path.Insert(2, UncDevicePrefixToInsert);
466+
467+
return ExtendedDevicePathPrefix + path;
468+
}
469+
470+
private const string ExtendedDevicePathPrefix = @"\\?\";
471+
private const string UncPathPrefix = @"\\";
472+
private const string UncDevicePrefixToInsert = @"?\UNC\";
473+
private const string UncExtendedPathPrefix = @"\\?\UNC\";
474+
private const string DevicePathPrefix = @"\\.\";
475+
476+
// \\?\, \\.\, \??\
477+
private const int DevicePrefixLength = 4;
478+
479+
/// <summary>
480+
/// Returns true if the given character is a valid drive letter
481+
/// </summary>
482+
private static bool IsValidDriveChar(char value)
483+
{
484+
return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'));
485+
}
486+
487+
private static bool IsDevice(string path)
488+
{
489+
return IsExtended(path)
490+
||
491+
(
492+
path.Length >= DevicePrefixLength
493+
&& IsDirectorySeparator(path[0])
494+
&& IsDirectorySeparator(path[1])
495+
&& (path[2] == '.' || path[2] == '?')
496+
&& IsDirectorySeparator(path[3])
497+
);
498+
}
499+
500+
private static bool IsExtended(string path)
501+
{
502+
return path.Length >= DevicePrefixLength
503+
&& path[0] == '\\'
504+
&& (path[1] == '\\' || path[1] == '?')
505+
&& path[2] == '?'
506+
&& path[3] == '\\';
507+
}
508+
509+
/// <summary>
510+
/// Returns true if the path specified is relative to the current drive or working directory.
511+
/// Returns false if the path is fixed to a specific drive or UNC path. This method does no
512+
/// validation of the path (URIs will be returned as relative as a result).
513+
/// </summary>
514+
/// <remarks>
515+
/// Handles paths that use the alternate directory separator. It is a frequent mistake to
516+
/// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
517+
/// "C:a" is drive relative- meaning that it will be resolved against the current directory
518+
/// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
519+
/// will not be used to modify the path).
520+
/// </remarks>
521+
private static bool IsPartiallyQualified(string path)
522+
{
523+
if (path.Length < 2)
524+
{
525+
// It isn't fixed, it must be relative. There is no way to specify a fixed
526+
// path with one character (or less).
527+
return true;
528+
}
529+
530+
if (IsDirectorySeparator(path[0]))
531+
{
532+
// There is no valid way to specify a relative path with two initial slashes or
533+
// \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
534+
return !(path[1] == '?' || IsDirectorySeparator(path[1]));
535+
}
536+
537+
// The only way to specify a fixed path that doesn't begin with two slashes
538+
// is the drive, colon, slash format- i.e. C:\
539+
return !((path.Length >= 3)
540+
&& (path[1] == Path.VolumeSeparatorChar)
541+
&& IsDirectorySeparator(path[2])
542+
// To match old behavior we'll check the drive character for validity as the path is technically
543+
// not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
544+
&& IsValidDriveChar(path[0]));
545+
}
546+
/// <summary>
547+
/// True if the given character is a directory separator.
548+
/// </summary>
549+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
550+
private static bool IsDirectorySeparator(char c)
551+
{
552+
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
553+
}
554+
555+
#endregion
450556
}
451557
}

0 commit comments

Comments
 (0)
0