// 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; /// /// Extension methods for exceptions /// public static class ExceptionExtensions { /// /// Flattens an exception and its inner exceptions /// /// The exception to flatten /// An enumerable of exceptions public static IEnumerable 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; } /// /// Checks if an exception is a stop, cancel or timeout exception /// /// The operation to check /// true if the exception is a stop or cancel exception, false otherwise public static bool IsAbortOrCancelException(this Exception ex) => ex is OperationCanceledException || ex is ThreadAbortException || ex is TaskCanceledException || ex is TimeoutException; /// /// Checks if an exception is a stop exception /// /// The operation to check /// true if the exception is a stop exception, false otherwise public static bool IsAbortException(this Exception ex) => ex is OperationCanceledException || ex is ThreadAbortException; /// /// List of Windows error codes that indicate a permission denied error /// 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 }; /// /// List of Windows error codes that indicate a file lock error /// private static readonly int[] WindowsFileLockCodes = { 32, // ERROR_SHARING_VIOLATION 33 // ERROR_LOCK_VIOLATION }; /// /// List of Windows error codes that indicate a path not found error /// 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 }; /// /// List of Windows error codes that indicate a path too long error /// private static readonly int[] WindowsPathTooLongCodes = { 206, // ERROR_FILENAME_EXCED_RANGE 3 // ERROR_PATH_NOT_FOUND (used when long path causes resolution failure) }; /// /// List of Posix error codes that indicate a permission denied error /// private static readonly int[] PosixPermissionErrnos = { 1, // EPERM 13, // EACCES 30, // EROFS - Read-only file system }; /// /// List of Posix error codes that indicate a file lock error /// 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) }; /// /// List of Posix error codes that indicate a path not found error /// private static readonly int[] PosixPathNotFoundErrnos = { 2, // ENOENT - No such file or directory 20 // ENOTDIR - A path component is not a directory }; /// /// List of Posix error codes that indicate a path too long error /// private static readonly int[] PosixPathTooLongErrnos = { 36, // ENAMETOOLONG - File name too long 63 // ENAMETOOLONG (on some macOS versions) }; /// /// Checks if an exception is a permission denied error /// /// The operation to check /// true if the exception is a permission denied error, false otherwise 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; } /// /// Checks if an exception is a file lock error /// /// The operation to check /// true if the exception is a file lock error, false otherwise 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; } /// /// Checks if an exception is a path not found error /// /// The operation to check /// true if the exception is a path not found error, false otherwise 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; } /// /// Checks if an exception is a path too long error /// /// The operation to check /// true if the exception is a path too long error, false otherwise 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; } }