From 7d77fdea5d471f615e7e4ab7923cce2e87c62d96 Mon Sep 17 00:00:00 2001
From: Saleem Abdulrasool <compnerd@compnerd.org>
Date: Thu, 20 Jul 2023 21:00:21 -0700
Subject: [PATCH] Foundation: make `_moveItem` resilient to multiple volumes

`MoveFileExW` does not permit moving directories across volumes.
Account for this by checking the source and fallback to copy and delete.
---
 Sources/Foundation/FileManager+Win32.swift | 38 +++++++++++++++++++---
 1 file changed, 34 insertions(+), 4 deletions(-)

diff --git a/Sources/Foundation/FileManager+Win32.swift b/Sources/Foundation/FileManager+Win32.swift
index 0b85af1a6d..46ece380b1 100644
--- a/Sources/Foundation/FileManager+Win32.swift
+++ b/Sources/Foundation/FileManager+Win32.swift
@@ -604,14 +604,44 @@ extension FileManager {
         }
 
         try withNTPathRepresentation(of: dstPath) { wszDestination in
-            var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
-            if GetFileAttributesExW(wszDestination, GetFileExInfoStandard, &faAttributes) {
+            var faDestinationnAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
+            if GetFileAttributesExW(wszDestination, GetFileExInfoStandard, &faDestinationnAttributes) {
                 throw CocoaError.error(.fileWriteFileExists, userInfo: [NSFilePathErrorKey:dstPath])
             }
 
             try withNTPathRepresentation(of: srcPath) { wszSource in
-                if !MoveFileExW(wszSource, wszDestination, DWORD(MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH)) {
-                    throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [srcPath, dstPath])
+                var faSourceAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
+                guard GetFileAttributesExW(wszSource, GetFileExInfoStandard, &faSourceAttributes) else {
+                    throw _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [srcPath])
+                }
+
+                // MoveFileExW does not work if the source and destination are
+                // on different volumes and the source is a directory.  In that
+                // case, we need to do a recursive copy & remove.
+                if PathIsSameRootW(wszSource, wszDestination) ||
+                        faSourceAttributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
+                    if !MoveFileExW(wszSource, wszDestination, DWORD(MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH)) {
+                        throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [srcPath, dstPath])
+                    }
+                } else {
+                    try _copyOrLinkDirectoryHelper(atPath: srcPath, toPath: dstPath, variant: "Move") { (src, dst, type) in
+                        do {
+                            switch type {
+                            case .typeRegular:
+                                try _copyRegularFile(atPath: src, toPath: dst, variant: "Move")
+                            case .typeSymbolicLink:
+                                try _copySymlink(atPath: src, toPath: dst, variant: "Move")
+                            default:
+                                break
+                            }
+                        } catch {
+                            if !shouldProceedAfterError(error, movingItemAtPath: src, toPath: dst, isURL: isURL) {
+                                throw error
+                            }
+                        }
+
+                        try _removeItem(atPath: src, isURL: isURL, alreadyConfirmed: true)
+                    }
                 }
             }
         }