duplicati/Duplicati/Library/Utility/ExceptionExtensions.cs
Kenneth Skovhede af82ed3a49 Detect EPERM on mac/Linux
This PR updates the check for permission denied to include EPERM and EACCES on macOS/Linux.
2025-06-12 13:36:53 +02:00

288 lines
No EOL
10 KiB
C#

// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Library.Common.IO;
namespace Duplicati.Library.Utility;
/// <summary>
/// Extension methods for exceptions
/// </summary>
public static class ExceptionExtensions
{
/// <summary>
/// Flattens an exception and its inner exceptions
/// </summary>
/// <param name="ex">The exception to flatten</param>
/// <returns>An enumerable of exceptions</returns>
public static IEnumerable<Exception> FlattenException(Exception? ex)
{
if (ex == null)
yield break;
yield return ex;
if (ex is AggregateException aex)
foreach (var iex in aex.Flatten().InnerExceptions)
foreach (var iex2 in FlattenException(iex))
yield return iex2;
foreach (var iex in FlattenException(ex.InnerException))
yield return iex;
}
/// <summary>
/// Checks if an exception is a stop, cancel or timeout exception
/// </summary>
/// <param name="ex">The operation to check</param>
/// <returns><c>true</c> if the exception is a stop or cancel exception, <c>false</c> otherwise</returns>
public static bool IsAbortOrCancelException(this Exception ex)
=> ex is OperationCanceledException || ex is ThreadAbortException || ex is TaskCanceledException || ex is TimeoutException;
/// <summary>
/// Checks if an exception is a stop exception
/// </summary>
/// <param name="ex">The operation to check</param>
/// <returns><c>true</c> if the exception is a stop exception, <c>false</c> otherwise</returns>
public static bool IsAbortException(this Exception ex)
=> ex is OperationCanceledException || ex is ThreadAbortException;
/// <summary>
/// List of Windows error codes that indicate a permission denied error
/// </summary>
private static readonly int[] WindowsPermissionDeniedCodes = {
5, // ERROR_ACCESS_DENIED
19, // ERROR_WRITE_PROTECT
65, // ERROR_NETWORK_ACCESS_DENIED
82, // ERROR_CANNOT_MAKE
1314, // ERROR_PRIVILEGE_NOT_HELD
};
/// <summary>
/// List of Windows error codes that indicate a file lock error
/// </summary>
private static readonly int[] WindowsFileLockCodes = {
32, // ERROR_SHARING_VIOLATION
33 // ERROR_LOCK_VIOLATION
};
/// <summary>
/// List of Windows error codes that indicate a path not found error
/// </summary>
private static readonly int[] WindowsPathNotFoundCodes = {
2, // ERROR_FILE_NOT_FOUND
3, // ERROR_PATH_NOT_FOUND
21, // ERROR_NOT_READY (e.g., missing drive)
267 // ERROR_DIRECTORY
};
/// <summary>
/// List of Windows error codes that indicate a path too long error
/// </summary>
private static readonly int[] WindowsPathTooLongCodes = {
206, // ERROR_FILENAME_EXCED_RANGE
3 // ERROR_PATH_NOT_FOUND (used when long path causes resolution failure)
};
/// <summary>
/// List of Posix error codes that indicate a permission denied error
/// </summary>
private static readonly int[] PosixPermissionErrnos = {
1, // EPERM
13, // EACCES
30, // EROFS - Read-only file system
};
/// <summary>
/// List of Posix error codes that indicate a file lock error
/// </summary>
private static readonly int[] PosixFileLockErrnos = {
// Linux file lock errors are not always distinguishable
11 // EAGAIN (on NFS), or temporary unavailable (used for lock contention in some cases)
};
/// <summary>
/// List of Posix error codes that indicate a path not found error
/// </summary>
private static readonly int[] PosixPathNotFoundErrnos = {
2, // ENOENT - No such file or directory
20 // ENOTDIR - A path component is not a directory
};
/// <summary>
/// List of Posix error codes that indicate a path too long error
/// </summary>
private static readonly int[] PosixPathTooLongErrnos = {
36, // ENAMETOOLONG - File name too long
63 // ENAMETOOLONG (on some macOS versions)
};
/// <summary>
/// Checks if an exception is a permission denied error
/// </summary>
/// <param name="ex">The operation to check</param>
/// <returns><c>true</c> if the exception is a permission denied error, <c>false</c> otherwise</returns>
public static bool IsPermissionDeniedException(this Exception? ex)
{
if (ex is UnauthorizedAccessException)
return true;
if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
return WindowsPermissionDeniedCodes.Contains(win32Ex.NativeErrorCode);
if (ex is IOException ioEx)
{
if (ioEx.HResult == unchecked((int)0x80070005)) // E_ACCESSDENIED
return true;
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
if (ioEx.InnerException is Win32Exception innerWin32 &&
PosixPermissionErrnos.Contains(innerWin32.NativeErrorCode))
return true;
var errno = ioEx.HResult & 0xFFFF;
return PosixPermissionErrnos.Contains(errno);
}
}
if (ex is PosixFile.PosixException posixEx)
{
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
return posixEx.Errno == Mono.Unix.Native.Errno.EPERM || posixEx.Errno == Mono.Unix.Native.Errno.EACCES;
}
}
return false;
}
/// <summary>
/// Checks if an exception is a file lock error
/// </summary>
/// <param name="ex">The operation to check</param>
/// <returns><c>true</c> if the exception is a file lock error, <c>false</c> otherwise</returns>
public static bool IsFileLockedException(this Exception? ex)
{
if (ex is IOException ioEx)
{
if (OperatingSystem.IsWindows())
{
// Check inner Win32Exception
if (ioEx.InnerException is Win32Exception innerWin32 &&
WindowsFileLockCodes.Contains(innerWin32.NativeErrorCode))
return true;
// Fallback to HRESULT
var winCode = ioEx.HResult & 0xFFFF;
return WindowsFileLockCodes.Contains(winCode);
}
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
if (ioEx.InnerException is Win32Exception innerWin32 &&
PosixFileLockErrnos.Contains(innerWin32.NativeErrorCode))
return true;
var errno = ioEx.HResult & 0xFFFF;
return PosixFileLockErrnos.Contains(errno);
}
}
return false;
}
/// <summary>
/// Checks if an exception is a path not found error
/// </summary>
/// <param name="ex">The operation to check</param>
/// <returns><c>true</c> if the exception is a path not found error, <c>false</c> otherwise</returns>
public static bool IsPathNotFoundException(this Exception? ex)
{
if (ex is FileNotFoundException || ex is DirectoryNotFoundException)
return true;
if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
return WindowsPathNotFoundCodes.Contains(win32Ex.NativeErrorCode);
if (ex is IOException ioEx)
{
var code = ioEx.HResult & 0xFFFF;
if (OperatingSystem.IsWindows())
return WindowsPathNotFoundCodes.Contains(code);
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
if (ioEx.InnerException is Win32Exception innerWin32 &&
PosixPathNotFoundErrnos.Contains(innerWin32.NativeErrorCode))
return true;
return PosixPathNotFoundErrnos.Contains(code);
}
}
return false;
}
/// <summary>
/// Checks if an exception is a path too long error
/// </summary>
/// <param name="ex">The operation to check</param>
/// <returns><c>true</c> if the exception is a path too long error, <c>false</c> otherwise</returns>
public static bool IsPathTooLongException(this Exception? ex)
{
if (ex is PathTooLongException)
return true;
if (ex is Win32Exception win32Ex && OperatingSystem.IsWindows())
return WindowsPathTooLongCodes.Contains(win32Ex.NativeErrorCode);
if (ex is IOException ioEx)
{
var code = ioEx.HResult & 0xFFFF;
if (OperatingSystem.IsWindows())
return WindowsPathTooLongCodes.Contains(code);
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
if (ioEx.InnerException is Win32Exception innerWin32 &&
PosixPathTooLongErrnos.Contains(innerWin32.NativeErrorCode))
return true;
return PosixPathTooLongErrnos.Contains(code);
}
}
return false;
}
}