diff --git a/Directory.Build.props b/Directory.Build.props index d05dc2c..576cf45 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ By hook or by crook, perform operations on files and directories. If they are in use by a process, kill the process. - 1.5.1 + 1.6.0 https://github.com/domsleee/forceops https://github.com/domsleee/forceops git diff --git a/ForceOps.Lib/src/DirectoryUtils.cs b/ForceOps.Lib/src/DirectoryUtils.cs index 5b3368e..8eb2db8 100644 --- a/ForceOps.Lib/src/DirectoryUtils.cs +++ b/ForceOps.Lib/src/DirectoryUtils.cs @@ -1,11 +1,28 @@ -namespace ForceOps.Lib; +using System.Text.RegularExpressions; + +namespace ForceOps.Lib; public static class DirectoryUtils { + static readonly Regex ReservedDeviceNamePattern = new( + @"^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(\..+)?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + public static string CombineWithCWDAndGetAbsolutePath(string path) { string currentDirectory = Directory.GetCurrentDirectory(); - return Path.GetFullPath(Path.Combine(currentDirectory, path)); + string combined = Path.Combine(currentDirectory, path); + + // Path.GetFullPath resolves reserved device names (NUL, CON, etc.) to \\.\NUL. + // Resolve the parent directory instead and re-append the filename. + string fileName = Path.GetFileName(combined); + if (IsReservedDeviceName(fileName)) + { + string parentDir = Path.GetFullPath(Path.GetDirectoryName(combined)!); + return Path.Combine(parentDir, fileName); + } + + return Path.GetFullPath(combined); } public static bool IsSymLink(string folder) => IsSymLink(new DirectoryInfo(folder)); @@ -22,4 +39,24 @@ public static void MarkAsNotReadOnly(FileSystemInfo fileSystemInfo) fileSystemInfo.Attributes &= ~FileAttributes.ReadOnly; } } + + public static bool IsReservedDeviceName(string path) + { + var fileName = Path.GetFileName(path); + return ReservedDeviceNamePattern.IsMatch(fileName); + } + + public static bool TryDeleteReservedDeviceNameFile(string absolutePath) + { + if (!IsReservedDeviceName(absolutePath)) + return false; + + var extendedPath = @"\\?\" + absolutePath; + if (File.Exists(extendedPath)) + { + File.Delete(extendedPath); + } + + return true; + } } diff --git a/ForceOps.Lib/src/FileAndDirectoryDeleter.cs b/ForceOps.Lib/src/FileAndDirectoryDeleter.cs index 9015807..97cc802 100644 --- a/ForceOps.Lib/src/FileAndDirectoryDeleter.cs +++ b/ForceOps.Lib/src/FileAndDirectoryDeleter.cs @@ -36,6 +36,10 @@ public void DeleteFileOrDirectory(string fileOrDirectory, bool force) DeleteDirectory(new DirectoryInfo(fileOrDirectory)); return; } + if (TryDeleteReservedDeviceNameFile(fileOrDirectory)) + { + return; + } if (!force) { @@ -54,9 +58,12 @@ internal void DeleteFile(FileInfo file) file.Delete(); break; } - catch when (!file.Exists) { } + catch when (!file.Exists && !IsReservedDeviceName(file.FullName)) { } catch (Exception ex) when (ex is IOException || ex is System.UnauthorizedAccessException) { + if (TryDeleteReservedDeviceNameFile(file.FullName)) + return; + var getProcessesLockingFileFunc = () => { try diff --git a/ForceOps.Test/src/FileAndDirectoryDeleterTest.cs b/ForceOps.Test/src/FileAndDirectoryDeleterTest.cs index 8384764..bae5bf3 100644 --- a/ForceOps.Test/src/FileAndDirectoryDeleterTest.cs +++ b/ForceOps.Test/src/FileAndDirectoryDeleterTest.cs @@ -87,6 +87,34 @@ public void DeletingReadonlyFileOpenByPowershell() Could not delete file .*. Beginning retry 1/10 in 50ms. ForceOps process is not elevated. Found 1 process to try to kill: \[\d+ \- powershell.exe\]", testContext.fakeLoggerFactory.GetAllLogsString()); } + [Fact] + public void DeletingFileWithReservedDeviceName() + { + var nulFilePath = Path.Combine(tempFolderPath, "nul"); + var extendedPath = @"\\?\" + nulFilePath; + + File.Create(extendedPath).Dispose(); + Assert.True(File.Exists(extendedPath)); + + fileAndDirectoryDeleter.DeleteFileOrDirectory(nulFilePath, false); + + Assert.False(File.Exists(extendedPath)); + } + + [Fact] + public void DeletingDirectoryContainingFileWithReservedDeviceName() + { + var nulFilePath = Path.Combine(tempFolderPath, "nul"); + var extendedPath = @"\\?\" + nulFilePath; + + File.Create(extendedPath).Dispose(); + Assert.True(File.Exists(extendedPath)); + + fileAndDirectoryDeleter.DeleteDirectory(new DirectoryInfo(tempFolderPath)); + + Assert.False(Directory.Exists(tempFolderPath)); + } + public ForceOpsMethodsTest() { tempFolderPath = GetTemporaryFileName();