Sfoglia il codice sorgente

Add SftpException and SftpPathNotFoundException.Path (#1716)

This adds an SftpException which sits between the existing SftpPathNotFoundException/
SftpPermissionDeniedException and SshException, and which contains the response code
from the SSH_FXP_STATUS packet, along with a default message if one was not provided.

SftpPathNotFoundException also gains a Path property which is populated in cases where
it makes sense.
Rob Hague 2 giorni fa
parent
commit
cd9ec8f395

+ 10 - 19
src/Renci.SshNet/Common/NetConfServerException.cs

@@ -1,7 +1,5 @@
-using System;
-#if NETFRAMEWORK
-using System.Runtime.Serialization;
-#endif // NETFRAMEWORK
+#nullable enable
+using System;
 
 namespace Renci.SshNet.Common
 {
@@ -10,7 +8,7 @@ namespace Renci.SshNet.Common
     /// </summary>
 #if NETFRAMEWORK
     [Serializable]
-#endif // NETFRAMEWORK
+#endif
     public class NetConfServerException : SshException
     {
         /// <summary>
@@ -23,8 +21,8 @@ namespace Renci.SshNet.Common
         /// <summary>
         /// Initializes a new instance of the <see cref="NetConfServerException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        public NetConfServerException(string message)
+        /// <inheritdoc cref="Exception(string)" path="/param"/>
+        public NetConfServerException(string? message)
             : base(message)
         {
         }
@@ -32,25 +30,18 @@ namespace Renci.SshNet.Common
         /// <summary>
         /// Initializes a new instance of the <see cref="NetConfServerException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        /// <param name="innerException">The inner exception.</param>
-        public NetConfServerException(string message, Exception innerException)
+        /// <inheritdoc cref="Exception(string, Exception)" path="/param"/>
+        public NetConfServerException(string? message, Exception? innerException)
             : base(message, innerException)
         {
         }
 
 #if NETFRAMEWORK
-        /// <summary>
-        /// Initializes a new instance of the <see cref="NetConfServerException"/> class.
-        /// </summary>
-        /// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
-        /// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
-        /// <exception cref="ArgumentNullException">The <paramref name="info"/> parameter is <see langword="null"/>.</exception>
-        /// <exception cref="SerializationException">The class name is <see langword="null"/> or <see cref="Exception.HResult"/> is zero (0). </exception>
-        protected NetConfServerException(SerializationInfo info, StreamingContext context)
+        /// <inheritdoc/>
+        protected NetConfServerException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
             : base(info, context)
         {
         }
-#endif // NETFRAMEWORK
+#endif
     }
 }

+ 10 - 19
src/Renci.SshNet/Common/ProxyException.cs

@@ -1,7 +1,5 @@
-using System;
-#if NETFRAMEWORK
-using System.Runtime.Serialization;
-#endif // NETFRAMEWORK
+#nullable enable
+using System;
 
 namespace Renci.SshNet.Common
 {
@@ -10,7 +8,7 @@ namespace Renci.SshNet.Common
     /// </summary>
 #if NETFRAMEWORK
     [Serializable]
-#endif // NETFRAMEWORK
+#endif
     public class ProxyException : SshException
     {
         /// <summary>
@@ -23,8 +21,8 @@ namespace Renci.SshNet.Common
         /// <summary>
         /// Initializes a new instance of the <see cref="ProxyException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        public ProxyException(string message)
+        /// <inheritdoc cref="Exception(string)" path="/param"/>
+        public ProxyException(string? message)
             : base(message)
         {
         }
@@ -32,25 +30,18 @@ namespace Renci.SshNet.Common
         /// <summary>
         /// Initializes a new instance of the <see cref="ProxyException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        /// <param name="innerException">The inner exception.</param>
-        public ProxyException(string message, Exception innerException)
+        /// <inheritdoc cref="Exception(string, Exception)" path="/param"/>
+        public ProxyException(string? message, Exception? innerException)
             : base(message, innerException)
         {
         }
 
 #if NETFRAMEWORK
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ProxyException"/> class.
-        /// </summary>
-        /// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
-        /// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
-        /// <exception cref="ArgumentNullException">The <paramref name="info"/> parameter is <see langword="null"/>.</exception>
-        /// <exception cref="SerializationException">The class name is <see langword="null"/> or <see cref="Exception.HResult"/> is zero (0).</exception>
-        protected ProxyException(SerializationInfo info, StreamingContext context)
+        /// <inheritdoc/>
+        protected ProxyException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
             : base(info, context)
         {
         }
-#endif // NETFRAMEWORK
+#endif
     }
 }

+ 11 - 20
src/Renci.SshNet/Common/ScpException.cs

@@ -1,16 +1,14 @@
-using System;
-#if NETFRAMEWORK
-using System.Runtime.Serialization;
-#endif // NETFRAMEWORK
+#nullable enable
+using System;
 
 namespace Renci.SshNet.Common
 {
     /// <summary>
-    /// The exception that is thrown when SCP error occurred.
+    /// The exception that is thrown when an SCP error occurs.
     /// </summary>
 #if NETFRAMEWORK
     [Serializable]
-#endif // NETFRAMEWORK
+#endif
     public class ScpException : SshException
     {
         /// <summary>
@@ -23,8 +21,8 @@ namespace Renci.SshNet.Common
         /// <summary>
         /// Initializes a new instance of the <see cref="ScpException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        public ScpException(string message)
+        /// <inheritdoc cref="Exception(string)" path="/param"/>
+        public ScpException(string? message)
             : base(message)
         {
         }
@@ -32,25 +30,18 @@ namespace Renci.SshNet.Common
         /// <summary>
         /// Initializes a new instance of the <see cref="ScpException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        /// <param name="innerException">The inner exception.</param>
-        public ScpException(string message, Exception innerException)
+        /// <inheritdoc cref="Exception(string, Exception)" path="/param"/>
+        public ScpException(string? message, Exception? innerException)
             : base(message, innerException)
         {
         }
 
 #if NETFRAMEWORK
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ScpException"/> class.
-        /// </summary>
-        /// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
-        /// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
-        /// <exception cref="ArgumentNullException">The <paramref name="info"/> parameter is <see langword="null"/>.</exception>
-        /// <exception cref="SerializationException">The class name is <see langword="null"/> or <see cref="Exception.HResult"/> is zero (0). </exception>
-        protected ScpException(SerializationInfo info, StreamingContext context)
+        /// <inheritdoc/>
+        protected ScpException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
             : base(info, context)
         {
         }
-#endif // NETFRAMEWORK
+#endif
     }
 }

+ 76 - 0
src/Renci.SshNet/Common/SftpException.cs

@@ -0,0 +1,76 @@
+#nullable enable
+using System;
+
+using Renci.SshNet.Sftp;
+
+namespace Renci.SshNet.Common
+{
+    /// <summary>
+    /// The exception that is thrown when an error occurs in the SFTP layer.
+    /// </summary>
+#if NETFRAMEWORK
+    [Serializable]
+#endif
+    public class SftpException : SshException
+    {
+        /// <summary>
+        /// Gets the status code that is associated with this exception.
+        /// </summary>
+        public StatusCode StatusCode { get; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SftpException"/> class.
+        /// </summary>
+        /// <param name="statusCode">The status code that indicates the error that occurred.</param>
+        public SftpException(StatusCode statusCode)
+            : this(statusCode, message: null, innerException: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SftpException"/> class.
+        /// </summary>
+        /// <param name="statusCode">The status code that indicates the error that occurred.</param>
+        /// <param name="message">The error message that explains the reason for the exception.</param>
+        public SftpException(StatusCode statusCode, string? message)
+            : this(statusCode, message, innerException: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SftpException"/> class.
+        /// </summary>
+        /// <param name="statusCode">The status code that indicates the error that occurred.</param>
+        /// <param name="message">The error message that explains the reason for the exception.</param>
+        /// <param name="innerException">The exception that is the cause of the current exception.</param>
+        public SftpException(StatusCode statusCode, string? message, Exception? innerException)
+            : base(string.IsNullOrEmpty(message) ? GetDefaultMessage(statusCode) : message, innerException)
+        {
+            StatusCode = statusCode;
+        }
+
+        private protected static string GetDefaultMessage(StatusCode statusCode)
+        {
+#pragma warning disable IDE0072 // Add missing cases
+            return statusCode switch
+            {
+                StatusCode.Ok => "The operation completed successfully.",
+                StatusCode.NoSuchFile => "A reference was made to a file that does not exist.",
+                StatusCode.PermissionDenied => "The user does not have sufficient permissions to perform the operation.",
+                StatusCode.Failure => "An error occurred, but no specific error code exists to describe the failure.",
+                StatusCode.BadMessage => "A badly formatted packet or SFTP protocol incompatibility was detected.",
+                StatusCode.OperationUnsupported => "An attempt was made to perform an operation which is not supported.",
+                _ => statusCode.ToString()
+            };
+#pragma warning restore IDE0072 // Add missing cases
+        }
+
+#if NETFRAMEWORK
+        /// <inheritdoc/>
+        protected SftpException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
+            : base(info, context)
+        {
+        }
+#endif
+    }
+}

+ 55 - 20
src/Renci.SshNet/Common/SftpPathNotFoundException.cs

@@ -1,7 +1,7 @@
-using System;
-#if NETFRAMEWORK
-using System.Runtime.Serialization;
-#endif // NETFRAMEWORK
+#nullable enable
+using System;
+
+using Renci.SshNet.Sftp;
 
 namespace Renci.SshNet.Common
 {
@@ -10,47 +10,82 @@ namespace Renci.SshNet.Common
     /// </summary>
 #if NETFRAMEWORK
     [Serializable]
-#endif // NETFRAMEWORK
-    public class SftpPathNotFoundException : SshException
+#endif
+    public class SftpPathNotFoundException : SftpException
     {
+        private const StatusCode Code = StatusCode.NoSuchFile;
+
+        /// <summary>
+        /// Gets the path that cannot be found.
+        /// </summary>
+        /// <value>
+        /// The path that cannot be found, or <see langword="null"/> if no path was
+        /// passed to the constructor for this instance.
+        /// </value>
+        public string? Path { get; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="SftpPathNotFoundException"/> class.
         /// </summary>
         public SftpPathNotFoundException()
+            : this(message: null, path: null, innerException: null)
         {
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SftpPathNotFoundException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        public SftpPathNotFoundException(string message)
-            : base(message)
+        /// <inheritdoc cref="Exception(string)" path="/param"/>
+        public SftpPathNotFoundException(string? message)
+            : this(message, path: null, innerException: null)
         {
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SftpPathNotFoundException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        /// <param name="innerException">The inner exception.</param>
-        public SftpPathNotFoundException(string message, Exception innerException)
-            : base(message, innerException)
+        /// <inheritdoc cref="Exception(string)" path="/param"/>
+        public SftpPathNotFoundException(string? message, string? path)
+            : this(message, path, innerException: null)
         {
         }
 
-#if NETFRAMEWORK
         /// <summary>
         /// Initializes a new instance of the <see cref="SftpPathNotFoundException"/> class.
         /// </summary>
-        /// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
-        /// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
-        /// <exception cref="ArgumentNullException">The <paramref name="info"/> parameter is <see langword="null"/>.</exception>
-        /// <exception cref="SerializationException">The class name is <see langword="null"/> or <see cref="Exception.HResult"/> is zero (0). </exception>
-        protected SftpPathNotFoundException(SerializationInfo info, StreamingContext context)
+        /// <inheritdoc cref="Exception(string, Exception)" path="/param"/>
+        public SftpPathNotFoundException(string? message, Exception? innerException)
+            : this(message, path: null, innerException)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SftpPathNotFoundException"/> class.
+        /// </summary>
+        /// <param name="message">The error message that explains the reason for the exception.</param>
+        /// <param name="path">The path that cannot be found.</param>
+        /// <param name="innerException">The exception that is the cause of the current exception.</param>
+        public SftpPathNotFoundException(string? message, string? path, Exception? innerException)
+            : base(Code, string.IsNullOrEmpty(message) ? GetDefaultMessage(path) : message, innerException)
+        {
+            Path = path;
+        }
+
+        private static string GetDefaultMessage(string? path)
+        {
+            var message = GetDefaultMessage(Code);
+
+            return path is not null
+                ? $"{message} Path: '{path}'."
+                : message;
+        }
+
+#if NETFRAMEWORK
+        /// <inheritdoc/>
+        protected SftpPathNotFoundException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
             : base(info, context)
         {
         }
-#endif // NETFRAMEWORK
+#endif
     }
 }

+ 18 - 22
src/Renci.SshNet/Common/SftpPermissionDeniedException.cs

@@ -1,7 +1,7 @@
-using System;
-#if NETFRAMEWORK
-using System.Runtime.Serialization;
-#endif // NETFRAMEWORK
+#nullable enable
+using System;
+
+using Renci.SshNet.Sftp;
 
 namespace Renci.SshNet.Common
 {
@@ -10,47 +10,43 @@ namespace Renci.SshNet.Common
     /// </summary>
 #if NETFRAMEWORK
     [Serializable]
-#endif // NETFRAMEWORK
-    public class SftpPermissionDeniedException : SshException
+#endif
+    public class SftpPermissionDeniedException : SftpException
     {
+        private const StatusCode Code = StatusCode.PermissionDenied;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="SftpPermissionDeniedException"/> class.
         /// </summary>
         public SftpPermissionDeniedException()
+            : base(Code)
         {
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SftpPermissionDeniedException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        public SftpPermissionDeniedException(string message)
-            : base(message)
+        /// <inheritdoc cref="Exception(string)" path="/param"/>
+        public SftpPermissionDeniedException(string? message)
+            : base(Code, message)
         {
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SftpPermissionDeniedException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        /// <param name="innerException">The inner exception.</param>
-        public SftpPermissionDeniedException(string message, Exception innerException)
-            : base(message, innerException)
+        /// <inheritdoc cref="Exception(string, Exception)" path="/param"/>
+        public SftpPermissionDeniedException(string? message, Exception? innerException)
+            : base(Code, message, innerException)
         {
         }
 
 #if NETFRAMEWORK
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SftpPermissionDeniedException"/> class.
-        /// </summary>
-        /// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
-        /// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
-        /// <exception cref="ArgumentNullException">The <paramref name="info"/> parameter is <see langword="null"/>.</exception>
-        /// <exception cref="SerializationException">The class name is <see langword="null"/> or <see cref="Exception.HResult"/> is zero (0). </exception>
-        protected SftpPermissionDeniedException(SerializationInfo info, StreamingContext context)
+        /// <inheritdoc/>
+        protected SftpPermissionDeniedException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
             : base(info, context)
         {
         }
-#endif // NETFRAMEWORK
+#endif
     }
 }

+ 12 - 21
src/Renci.SshNet/Common/SshException.cs

@@ -1,16 +1,14 @@
-using System;
-#if NETFRAMEWORK
-using System.Runtime.Serialization;
-#endif // NETFRAMEWORK
+#nullable enable
+using System;
 
 namespace Renci.SshNet.Common
 {
     /// <summary>
-    /// The exception that is thrown when SSH exception occurs.
+    /// The exception that is thrown when an SSH exception occurs.
     /// </summary>
 #if NETFRAMEWORK
     [Serializable]
-#endif // NETFRAMEWORK
+#endif
     public class SshException : Exception
     {
         /// <summary>
@@ -23,8 +21,8 @@ namespace Renci.SshNet.Common
         /// <summary>
         /// Initializes a new instance of the <see cref="SshException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        public SshException(string message)
+        /// <inheritdoc cref="Exception(string, Exception)" path="/param"/>
+        public SshException(string? message)
             : base(message)
         {
         }
@@ -32,25 +30,18 @@ namespace Renci.SshNet.Common
         /// <summary>
         /// Initializes a new instance of the <see cref="SshException"/> class.
         /// </summary>
-        /// <param name="message">The message.</param>
-        /// <param name="inner">The inner.</param>
-        public SshException(string message, Exception inner)
-            : base(message, inner)
+        /// <inheritdoc cref="Exception(string, Exception)" path="/param"/>
+        public SshException(string? message, Exception? innerException)
+            : base(message, innerException)
         {
         }
 
 #if NETFRAMEWORK
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SshException"/> class.
-        /// </summary>
-        /// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
-        /// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
-        /// <exception cref="ArgumentNullException">The <paramref name="info"/> parameter is <see langword="null"/>.</exception>
-        /// <exception cref="SerializationException">The class name is <see langword="null"/> or <see cref="Exception.HResult"/> is zero (0). </exception>
-        protected SshException(SerializationInfo info, StreamingContext context)
+        /// <inheritdoc/>
+        protected SshException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
             : base(info, context)
         {
         }
-#endif // NETFRAMEWORK
+#endif
     }
 }

+ 6 - 5
src/Renci.SshNet/Sftp/Responses/SftpStatusResponse.cs

@@ -1,4 +1,5 @@
-namespace Renci.SshNet.Sftp.Responses
+#nullable enable
+namespace Renci.SshNet.Sftp.Responses
 {
     internal sealed class SftpStatusResponse : SftpResponse
     {
@@ -12,17 +13,17 @@
         {
         }
 
-        public StatusCodes StatusCode { get; set; }
+        public StatusCode StatusCode { get; set; }
 
-        public string ErrorMessage { get; private set; }
+        public string? ErrorMessage { get; private set; }
 
-        public string Language { get; private set; }
+        public string? Language { get; private set; }
 
         protected override void LoadData()
         {
             base.LoadData();
 
-            StatusCode = (StatusCodes)ReadUInt32();
+            StatusCode = (StatusCode)ReadUInt32();
 
             if (ProtocolVersion < 3)
             {

+ 1 - 1
src/Renci.SshNet/Sftp/SftpFileStream.cs

@@ -282,7 +282,7 @@ namespace Renci.SshNet.Sftp
                     attributes = session.RequestFStat(handle);
                 }
             }
-            catch (SshException ex)
+            catch (SftpException ex)
             {
                 session.SessionLoggerFactory.CreateLogger<SftpFileStream>().LogInformation(
                     ex, "fstat failed after opening {Path}. Will set CanSeek=false.", path);

+ 68 - 59
src/Renci.SshNet/Sftp/SftpSession.cs

@@ -415,7 +415,7 @@ namespace Renci.SshNet.Sftp
         public byte[] RequestOpen(string path, Flags flags, bool nullOnError = false)
         {
             byte[] handle = null;
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -431,7 +431,7 @@ namespace Renci.SshNet.Sftp
                                                   },
                                                   response =>
                                                   {
-                                                      exception = GetSftpException(response);
+                                                      exception = GetSftpException(response, path);
                                                       wait.SetIgnoringObjectDisposed();
                                                   });
 
@@ -473,7 +473,7 @@ namespace Renci.SshNet.Sftp
                                                 _encoding,
                                                 flags,
                                                 response => tcs.TrySetResult(response.Handle),
-                                                response => tcs.TrySetException(GetSftpException(response))));
+                                                response => tcs.TrySetException(GetSftpException(response, path))));
 
             return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
         }
@@ -503,7 +503,7 @@ namespace Renci.SshNet.Sftp
                                               },
                                               response =>
                                               {
-                                                  asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
+                                                  asyncResult.SetAsCompleted(GetSftpException(response, path), completedSynchronously: false);
                                               });
 
             SendRequest(request);
@@ -550,7 +550,7 @@ namespace Renci.SshNet.Sftp
         /// <param name="handle">The handle.</param>
         public void RequestClose(byte[] handle)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -596,7 +596,7 @@ namespace Renci.SshNet.Sftp
                                              handle,
                                              response =>
                                              {
-                                                 if (response.StatusCode == StatusCodes.Ok)
+                                                 if (response.StatusCode == StatusCode.Ok)
                                                  {
                                                      _ = tcs.TrySetResult(true);
                                                  }
@@ -688,7 +688,7 @@ namespace Renci.SshNet.Sftp
                                               },
                                               response =>
                                               {
-                                                  if (response.StatusCode != StatusCodes.Eof)
+                                                  if (response.StatusCode != StatusCode.Eof)
                                                   {
                                                       asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
                                                   }
@@ -746,7 +746,7 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         public byte[] RequestRead(byte[] handle, ulong offset, uint length)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             byte[] data = null;
 
@@ -764,7 +764,7 @@ namespace Renci.SshNet.Sftp
                                                   },
                                                   response =>
                                                   {
-                                                      if (response.StatusCode != StatusCodes.Eof)
+                                                      if (response.StatusCode != StatusCode.Eof)
                                                       {
                                                           exception = GetSftpException(response);
                                                       }
@@ -820,7 +820,7 @@ namespace Renci.SshNet.Sftp
                                             response => tcs.TrySetResult(response.Data),
                                             response =>
                                             {
-                                                if (response.StatusCode == StatusCodes.Eof)
+                                                if (response.StatusCode == StatusCode.Eof)
                                                 {
                                                     _ = tcs.TrySetResult(Array.Empty<byte>());
                                                 }
@@ -853,7 +853,7 @@ namespace Renci.SshNet.Sftp
         {
             Debug.Assert((wait is null) != (writeCompleted is null), "Should have one parameter or the other.");
 
-            SshException exception = null;
+            SftpException exception = null;
 
             var request = new SftpWriteRequest(ProtocolVersion,
                                                NextRequestId,
@@ -918,7 +918,7 @@ namespace Renci.SshNet.Sftp
                                                 length,
                                                 response =>
                                                 {
-                                                    if (response.StatusCode == StatusCodes.Ok)
+                                                    if (response.StatusCode == StatusCode.Ok)
                                                     {
                                                         _ = tcs.TrySetResult(true);
                                                     }
@@ -940,7 +940,7 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         public SftpFileAttributes RequestLStat(string path)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             SftpFileAttributes attributes = null;
             using (var wait = new AutoResetEvent(initialState: false))
@@ -956,7 +956,7 @@ namespace Renci.SshNet.Sftp
                                                    },
                                                    response =>
                                                    {
-                                                       exception = GetSftpException(response);
+                                                       exception = GetSftpException(response, path);
                                                        wait.SetIgnoringObjectDisposed();
                                                    });
 
@@ -996,7 +996,7 @@ namespace Renci.SshNet.Sftp
                                                 path,
                                                 _encoding,
                                                 response => tcs.TrySetResult(response.Attributes),
-                                                response => tcs.TrySetException(GetSftpException(response))));
+                                                response => tcs.TrySetException(GetSftpException(response, path))));
 
             return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
         }
@@ -1024,7 +1024,7 @@ namespace Renci.SshNet.Sftp
                                                },
                                                response =>
                                                {
-                                                   asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
+                                                   asyncResult.SetAsCompleted(GetSftpException(response, path), completedSynchronously: false);
                                                });
             SendRequest(request);
 
@@ -1063,7 +1063,7 @@ namespace Renci.SshNet.Sftp
         /// <inheritdoc/>
         public SftpFileAttributes RequestFStat(byte[] handle)
         {
-            SshException exception = null;
+            SftpException exception = null;
             SftpFileAttributes attributes = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
@@ -1129,7 +1129,7 @@ namespace Renci.SshNet.Sftp
         /// <param name="attributes">The attributes.</param>
         public void RequestSetStat(string path, SftpFileAttributes attributes)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -1140,7 +1140,7 @@ namespace Renci.SshNet.Sftp
                                                      attributes,
                                                      response =>
                                                      {
-                                                         exception = GetSftpException(response);
+                                                         exception = GetSftpException(response, path);
                                                          wait.SetIgnoringObjectDisposed();
                                                      });
 
@@ -1162,7 +1162,7 @@ namespace Renci.SshNet.Sftp
         /// <param name="attributes">The attributes.</param>
         public void RequestFSetStat(byte[] handle, SftpFileAttributes attributes)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -1195,7 +1195,7 @@ namespace Renci.SshNet.Sftp
         /// <returns>File handle.</returns>
         public byte[] RequestOpenDir(string path, bool nullOnError = false)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             byte[] handle = null;
 
@@ -1212,7 +1212,7 @@ namespace Renci.SshNet.Sftp
                                                      },
                                                      response =>
                                                      {
-                                                         exception = GetSftpException(response);
+                                                         exception = GetSftpException(response, path);
                                                          wait.SetIgnoringObjectDisposed();
                                                      });
 
@@ -1252,7 +1252,7 @@ namespace Renci.SshNet.Sftp
                                                path,
                                                _encoding,
                                                response => tcs.TrySetResult(response.Handle),
-                                               response => tcs.TrySetException(GetSftpException(response))));
+                                               response => tcs.TrySetException(GetSftpException(response, path))));
 
             return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
         }
@@ -1267,7 +1267,7 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         public KeyValuePair<string, SftpFileAttributes>[] RequestReadDir(byte[] handle)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             KeyValuePair<string, SftpFileAttributes>[] result = null;
 
@@ -1283,7 +1283,7 @@ namespace Renci.SshNet.Sftp
                                                      },
                                                      response =>
                                                      {
-                                                         if (response.StatusCode != StatusCodes.Eof)
+                                                         if (response.StatusCode != StatusCode.Eof)
                                                          {
                                                              exception = GetSftpException(response);
                                                          }
@@ -1330,7 +1330,7 @@ namespace Renci.SshNet.Sftp
                                                response => tcs.TrySetResult(response.Files),
                                                response =>
                                                {
-                                                   if (response.StatusCode == StatusCodes.Eof)
+                                                   if (response.StatusCode == StatusCode.Eof)
                                                    {
                                                        _ = tcs.TrySetResult(null);
                                                    }
@@ -1349,7 +1349,7 @@ namespace Renci.SshNet.Sftp
         /// <param name="path">The path.</param>
         public void RequestRemove(string path)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -1359,7 +1359,7 @@ namespace Renci.SshNet.Sftp
                                                     _encoding,
                                                     response =>
                                                     {
-                                                        exception = GetSftpException(response);
+                                                        exception = GetSftpException(response, path);
                                                         wait.SetIgnoringObjectDisposed();
                                                     });
 
@@ -1397,13 +1397,13 @@ namespace Renci.SshNet.Sftp
                                                 _encoding,
                                                 response =>
                                                 {
-                                                    if (response.StatusCode == StatusCodes.Ok)
+                                                    if (response.StatusCode == StatusCode.Ok)
                                                     {
                                                         _ = tcs.TrySetResult(true);
                                                     }
                                                     else
                                                     {
-                                                        _ = tcs.TrySetException(GetSftpException(response));
+                                                        _ = tcs.TrySetException(GetSftpException(response, path));
                                                     }
                                                 }));
 
@@ -1416,7 +1416,7 @@ namespace Renci.SshNet.Sftp
         /// <param name="path">The path.</param>
         public void RequestMkDir(string path)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -1462,7 +1462,7 @@ namespace Renci.SshNet.Sftp
                                              _encoding,
                                              response =>
                                                  {
-                                                     if (response.StatusCode == StatusCodes.Ok)
+                                                     if (response.StatusCode == StatusCode.Ok)
                                                      {
                                                          _ = tcs.TrySetResult(true);
                                                      }
@@ -1481,7 +1481,7 @@ namespace Renci.SshNet.Sftp
         /// <param name="path">The path.</param>
         public void RequestRmDir(string path)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -1491,7 +1491,7 @@ namespace Renci.SshNet.Sftp
                                                    _encoding,
                                                    response =>
                                                    {
-                                                       exception = GetSftpException(response);
+                                                       exception = GetSftpException(response, path);
                                                        wait.SetIgnoringObjectDisposed();
                                                    });
 
@@ -1522,7 +1522,7 @@ namespace Renci.SshNet.Sftp
                                              _encoding,
                                              response =>
                                                  {
-                                                     var exception = GetSftpException(response);
+                                                     var exception = GetSftpException(response, path);
                                                      if (exception is not null)
                                                      {
                                                          _ = tcs.TrySetException(exception);
@@ -1546,7 +1546,7 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         internal KeyValuePair<string, SftpFileAttributes>[] RequestRealPath(string path, bool nullOnError = false)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             KeyValuePair<string, SftpFileAttributes>[] result = null;
 
@@ -1563,7 +1563,7 @@ namespace Renci.SshNet.Sftp
                                                       },
                                                       response =>
                                                       {
-                                                          exception = GetSftpException(response);
+                                                          exception = GetSftpException(response, path);
                                                           wait.SetIgnoringObjectDisposed();
                                                       });
 
@@ -1602,7 +1602,7 @@ namespace Renci.SshNet.Sftp
                                                     }
                                                     else
                                                     {
-                                                        _ = tcs.TrySetException(GetSftpException(response));
+                                                        _ = tcs.TrySetException(GetSftpException(response, path));
                                                     }
                                                 }));
 
@@ -1627,7 +1627,7 @@ namespace Renci.SshNet.Sftp
                                                   path,
                                                   _encoding,
                                                   response => asyncResult.SetAsCompleted(response.Files[0].Key, completedSynchronously: false),
-                                                  response => asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false));
+                                                  response => asyncResult.SetAsCompleted(GetSftpException(response, path), completedSynchronously: false));
             SendRequest(request);
 
             return asyncResult;
@@ -1672,7 +1672,7 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         public SftpFileAttributes RequestStat(string path, bool nullOnError = false)
         {
-            SshException exception = null;
+            SftpException exception = null;
 
             SftpFileAttributes attributes = null;
 
@@ -1689,7 +1689,7 @@ namespace Renci.SshNet.Sftp
                                                   },
                                                   response =>
                                                   {
-                                                      exception = GetSftpException(response);
+                                                      exception = GetSftpException(response, path);
                                                       wait.SetIgnoringObjectDisposed();
                                                   });
 
@@ -1724,7 +1724,7 @@ namespace Renci.SshNet.Sftp
                                               path,
                                               _encoding,
                                               response => asyncResult.SetAsCompleted(response.Attributes, completedSynchronously: false),
-                                              response => asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false));
+                                              response => asyncResult.SetAsCompleted(GetSftpException(response, path), completedSynchronously: false));
             SendRequest(request);
 
             return asyncResult;
@@ -1771,7 +1771,7 @@ namespace Renci.SshNet.Sftp
                 throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_RENAME operation is not supported in {0} version that server operates in.", ProtocolVersion));
             }
 
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -1822,7 +1822,7 @@ namespace Renci.SshNet.Sftp
                                                 _encoding,
                                                 response =>
                                                 {
-                                                    if (response.StatusCode == StatusCodes.Ok)
+                                                    if (response.StatusCode == StatusCode.Ok)
                                                     {
                                                         _ = tcs.TrySetResult(true);
                                                     }
@@ -1851,7 +1851,7 @@ namespace Renci.SshNet.Sftp
                 throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_READLINK operation is not supported in {0} version that server operates in.", ProtocolVersion));
             }
 
-            SshException exception = null;
+            SftpException exception = null;
 
             KeyValuePair<string, SftpFileAttributes>[] result = null;
 
@@ -1868,7 +1868,7 @@ namespace Renci.SshNet.Sftp
                                                       },
                                                       response =>
                                                       {
-                                                          exception = GetSftpException(response);
+                                                          exception = GetSftpException(response, path);
                                                           wait.SetIgnoringObjectDisposed();
                                                       });
 
@@ -1897,7 +1897,7 @@ namespace Renci.SshNet.Sftp
                 throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_SYMLINK operation is not supported in {0} version that server operates in.", ProtocolVersion));
             }
 
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -1935,7 +1935,7 @@ namespace Renci.SshNet.Sftp
                 throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
             }
 
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -1981,7 +1981,7 @@ namespace Renci.SshNet.Sftp
                 throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
             }
 
-            SshException exception = null;
+            SftpException exception = null;
 
             SftpFileSystemInformation information = null;
 
@@ -1998,7 +1998,7 @@ namespace Renci.SshNet.Sftp
                                                  },
                                                  response =>
                                                  {
-                                                     exception = GetSftpException(response);
+                                                     exception = GetSftpException(response, path);
                                                      wait.SetIgnoringObjectDisposed();
                                                  });
 
@@ -2049,7 +2049,7 @@ namespace Renci.SshNet.Sftp
                                             path,
                                             _encoding,
                                             response => tcs.TrySetResult(response.GetReply<StatVfsReplyInfo>().Information),
-                                            response => tcs.TrySetException(GetSftpException(response))));
+                                            response => tcs.TrySetException(GetSftpException(response, path))));
 
             return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
         }
@@ -2070,7 +2070,7 @@ namespace Renci.SshNet.Sftp
                 throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
             }
 
-            SshException exception = null;
+            SftpException exception = null;
 
             SftpFileSystemInformation information = null;
 
@@ -2120,7 +2120,7 @@ namespace Renci.SshNet.Sftp
                 throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
             }
 
-            SshException exception = null;
+            SftpException exception = null;
 
             using (var wait = new AutoResetEvent(initialState: false))
             {
@@ -2209,19 +2209,28 @@ namespace Renci.SshNet.Sftp
             return Math.Min(bufferSize, maximumPacketSize) - lengthOfNonDataProtocolFields;
         }
 
-        internal static SshException GetSftpException(SftpStatusResponse response)
+        internal static SftpException GetSftpException(SftpStatusResponse response, string path = null)
         {
 #pragma warning disable IDE0010 // Add missing cases
             switch (response.StatusCode)
             {
-                case StatusCodes.Ok:
+                case StatusCode.Ok:
                     return null;
-                case StatusCodes.PermissionDenied:
+                case StatusCode.PermissionDenied:
                     return new SftpPermissionDeniedException(response.ErrorMessage);
-                case StatusCodes.NoSuchFile:
-                    return new SftpPathNotFoundException(response.ErrorMessage);
+                case StatusCode.NoSuchFile:
+
+                    var message = response.ErrorMessage;
+
+                    if (!string.IsNullOrEmpty(message) && path is not null)
+                    {
+                        message = $"{message}{(message[^1] == '.' ? " " : ". ")}Path: '{path}'.";
+                    }
+
+                    return new SftpPathNotFoundException(message, path);
+
                 default:
-                    return new SshException(response.ErrorMessage);
+                    return new SftpException(response.StatusCode, response.ErrorMessage);
             }
 #pragma warning restore IDE0010 // Add missing cases
         }

+ 86 - 0
src/Renci.SshNet/Sftp/StatusCode.cs

@@ -0,0 +1,86 @@
+namespace Renci.SshNet.Sftp
+{
+    /// <summary>
+    /// Specifies status codes returned by the server in response to SFTP requests.
+    /// </summary>
+    public enum StatusCode
+    {
+        /// <summary>
+        /// SSH_FX_OK.
+        /// </summary>
+        /// <remarks>
+        /// The operation completed successfully.
+        /// </remarks>
+        Ok = 0,
+
+        /// <summary>
+        /// SSH_FX_EOF.
+        /// </summary>
+        /// <remarks>
+        /// An attempt was made to read past the end of the file,
+        /// or no more directory entries were available.
+        /// </remarks>
+        Eof = 1,
+
+        /// <summary>
+        /// SSH_FX_NO_SUCH_FILE.
+        /// </summary>
+        /// <remarks>
+        /// A reference was made to a file that does not exist.
+        /// </remarks>
+        NoSuchFile = 2,
+
+        /// <summary>
+        /// SSH_FX_PERMISSION_DENIED.
+        /// </summary>
+        /// <remarks>
+        /// The user does not have sufficient permissions to perform the operation.
+        /// </remarks>
+        PermissionDenied = 3,
+
+        /// <summary>
+        /// SSH_FX_FAILURE.
+        /// </summary>
+        /// <remarks>
+        /// An error occurred, but no specific error code exists to describe
+        /// the failure.
+        /// </remarks>
+        Failure = 4,
+
+        /// <summary>
+        /// SSH_FX_BAD_MESSAGE.
+        /// </summary>
+        /// <remarks>
+        /// A badly formatted packet or SFTP protocol incompatibility was detected.
+        /// </remarks>
+        BadMessage = 5,
+
+        /// <summary>
+        /// SSH_FX_NO_CONNECTION.
+        /// </summary>
+        /// <remarks>
+        /// A pseudo-error which indicates that the client has no
+        /// connection to the server (it can only be generated locally
+        /// by the client, and MUST NOT be returned by servers).
+        /// </remarks>
+        NoConnection = 6,
+
+        /// <summary>
+        /// SSH_FX_CONNECTION_LOST.
+        /// </summary>
+        /// <remarks>
+        /// A pseudo-error which indicates that the connection to the
+        /// server has been lost (it can only be generated locally by
+        /// the client, and MUST NOT be returned by servers).
+        /// </remarks>
+        ConnectionLost = 7,
+
+        /// <summary>
+        /// SSH_FX_OP_UNSUPPORTED.
+        /// </summary>
+        /// <remarks>
+        /// The operation could not be completed because the server did not support it.
+        /// </remarks>
+        OperationUnsupported = 8,
+    }
+}

+ 0 - 165
src/Renci.SshNet/Sftp/StatusCodes.cs

@@ -1,165 +0,0 @@
-namespace Renci.SshNet.Sftp
-{
-    internal enum StatusCodes : uint
-    {
-        /// <summary>
-        /// SSH_FX_OK.
-        /// </summary>
-        Ok = 0,
-
-        /// <summary>
-        /// SSH_FX_EOF.
-        /// </summary>
-        Eof = 1,
-
-        /// <summary>
-        /// SSH_FX_NO_SUCH_FILE.
-        /// </summary>
-        NoSuchFile = 2,
-
-        /// <summary>
-        /// SSH_FX_PERMISSION_DENIED.
-        /// </summary>
-        PermissionDenied = 3,
-
-        /// <summary>
-        /// SSH_FX_FAILURE.
-        /// </summary>
-        Failure = 4,
-
-        /// <summary>
-        /// SSH_FX_BAD_MESSAGE.
-        /// </summary>
-        BadMessage = 5,
-
-        /// <summary>
-        /// SSH_FX_NO_CONNECTION.
-        /// </summary>
-        NoConnection = 6,
-
-        /// <summary>
-        /// SSH_FX_CONNECTION_LOST.
-        /// </summary>
-        ConnectionLost = 7,
-
-        /// <summary>
-        /// SSH_FX_OP_UNSUPPORTED.
-        /// </summary>
-        OperationUnsupported = 8,
-
-        /// <summary>
-        /// SSH_FX_INVALID_HANDLE.
-        /// </summary>
-        InvalidHandle = 9,
-
-        /// <summary>
-        /// SSH_FX_NO_SUCH_PATH.
-        /// </summary>
-        NoSuchPath = 10,
-
-        /// <summary>
-        /// SSH_FX_FILE_ALREADY_EXISTS.
-        /// </summary>
-        FileAlreadyExists = 11,
-
-        /// <summary>
-        /// SSH_FX_WRITE_PROTECT.
-        /// </summary>
-        WriteProtect = 12,
-
-        /// <summary>
-        /// SSH_FX_NO_MEDIA.
-        /// </summary>
-        NoMedia = 13,
-
-        /// <summary>
-        /// SSH_FX_NO_SPACE_ON_FILESYSTEM.
-        /// </summary>
-        NoSpaceOnFilesystem = 14,
-
-        /// <summary>
-        /// SSH_FX_QUOTA_EXCEEDED.
-        /// </summary>
-        QuotaExceeded = 15,
-
-        /// <summary>
-        /// SSH_FX_UNKNOWN_PRINCIPAL.
-        /// </summary>
-        UnknownPrincipal = 16,
-
-        /// <summary>
-        /// SSH_FX_LOCK_CONFLICT.
-        /// </summary>
-        LockConflict = 17,
-
-        /// <summary>
-        /// SSH_FX_DIR_NOT_EMPTY.
-        /// </summary>
-        DirNotEmpty = 18,
-
-        /// <summary>
-        /// SSH_FX_NOT_A_DIRECTORY.
-        /// </summary>
-        NotDirectory = 19,
-
-        /// <summary>
-        /// SSH_FX_INVALID_FILENAME.
-        /// </summary>
-        InvalidFilename = 20,
-
-        /// <summary>
-        /// SSH_FX_LINK_LOOP.
-        /// </summary>
-        LinkLoop = 21,
-
-        /// <summary>
-        /// SSH_FX_CANNOT_DELETE.
-        /// </summary>
-        CannotDelete = 22,
-
-        /// <summary>
-        /// SSH_FX_INVALID_PARAMETER.
-        /// </summary>
-        InvalidParameter = 23,
-
-        /// <summary>
-        /// SSH_FX_FILE_IS_A_DIRECTORY.
-        /// </summary>
-        FileIsADirectory = 24,
-
-        /// <summary>
-        /// SSH_FX_BYTE_RANGE_LOCK_CONFLICT.
-        /// </summary>
-        ByteRangeLockConflict = 25,
-
-        /// <summary>
-        /// SSH_FX_BYTE_RANGE_LOCK_REFUSED.
-        /// </summary>
-        ByteRangeLockRefused = 26,
-
-        /// <summary>
-        /// SSH_FX_DELETE_PENDING.
-        /// </summary>
-        DeletePending = 27,
-
-        /// <summary>
-        /// SSH_FX_FILE_CORRUPT.
-        /// </summary>
-        FileCorrupt = 28,
-
-        /// <summary>
-        /// SSH_FX_OWNER_INVALID.
-        /// </summary>
-        OwnerInvalid = 29,
-
-        /// <summary>
-        /// SSH_FX_GROUP_INVALID.
-        /// </summary>
-        GroupInvalid = 30,
-
-        /// <summary>
-        /// SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK.
-        /// </summary>
-        NoMatchingByteRangeLock = 31
-    }
-}

+ 1 - 1
src/Renci.SshNet/SftpClient.cs

@@ -2476,7 +2476,7 @@ namespace Renci.SshNet
                             return;
                         }
 
-                        Debug.Assert(s.StatusCode == StatusCodes.Ok);
+                        Debug.Assert(s.StatusCode == StatusCode.Ok);
 
                         asyncResult?.Update(writtenBytes);
 

+ 3 - 1
test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.CreateDirectory.cs

@@ -1,5 +1,6 @@
 
 using Renci.SshNet.Common;
+using Renci.SshNet.Sftp;
 
 namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
 {
@@ -56,7 +57,8 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
 
                 sftp.CreateDirectory("test");
 
-                Assert.ThrowsExactly<SshException>(() => sftp.CreateDirectory("test"));
+                var ex = Assert.ThrowsExactly<SftpException>(() => sftp.CreateDirectory("test"));
+                Assert.AreEqual(StatusCode.Failure, ex.StatusCode);
             }
         }
     }

+ 52 - 36
test/Renci.SshNet.IntegrationTests/SftpTests.cs

@@ -266,7 +266,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -393,7 +394,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -513,7 +515,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -637,7 +640,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -759,7 +763,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
 
                 finally
@@ -877,7 +882,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -999,7 +1005,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -1125,7 +1132,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -1269,7 +1277,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -1361,7 +1370,8 @@ namespace Renci.SshNet.IntegrationTests
                     catch (SftpPathNotFoundException ex)
                     {
                         Assert.IsNull(ex.InnerException);
-                        Assert.AreEqual("No such file", ex.Message);
+                        Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                        Assert.AreEqual(remoteFile, ex.Path);
 
                         // ensure file was not created by us
                         Assert.IsFalse(client.Exists(remoteFile));
@@ -1399,7 +1409,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
 
                     // ensure file was not created by us
                     Assert.IsFalse(client.Exists(remoteFile));
@@ -1482,7 +1493,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
 
                     // ensure file was not created by us
                     Assert.IsFalse(client.Exists(remoteFile));
@@ -1567,7 +1579,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
 
                     // ensure file was not created by us
                     Assert.IsFalse(client.Exists(remoteFile));
@@ -1649,7 +1662,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
 
                     // ensure file was not created by us
                     Assert.IsFalse(client.Exists(remoteFile));
@@ -1733,7 +1747,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
 
                     // ensure file was not created by us
                     Assert.IsFalse(client.Exists(remoteFile));
@@ -1830,7 +1845,8 @@ namespace Renci.SshNet.IntegrationTests
                     });
 
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
 
                     // ensure file was not created by us
                     Assert.IsFalse(client.Exists(remoteFile));
@@ -1913,7 +1929,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -2036,7 +2053,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -2164,7 +2182,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -2290,7 +2309,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -2416,7 +2436,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -2543,7 +2564,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -2670,7 +2692,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                    Assert.AreEqual(remoteFile, ex.Path);
                 }
                 finally
                 {
@@ -2799,7 +2822,8 @@ namespace Renci.SshNet.IntegrationTests
                         catch (SftpPathNotFoundException ex)
                         {
                             Assert.IsNull(ex.InnerException);
-                            Assert.AreEqual("No such file", ex.Message);
+                            Assert.AreEqual($"No such file. Path: '{remoteFile}'.", ex.Message);
+                            Assert.AreEqual(remoteFile, ex.Path);
 
                             // ensure file was not created by us
                             Assert.IsFalse(client.Exists(remoteFile));
@@ -2844,7 +2868,8 @@ namespace Renci.SshNet.IntegrationTests
                 catch (SftpPathNotFoundException ex)
                 {
                     Assert.IsNull(ex.InnerException);
-                    Assert.AreEqual("No such file", ex.Message);
+                    Assert.AreEqual($"No such file. Path: '{remoteDirectory}'.", ex.Message);
+                    Assert.AreEqual(remoteDirectory, ex.Path);
 
                     // ensure directory was not created by us
                     Assert.IsFalse(client.Exists(remoteDirectory));
@@ -3098,17 +3123,8 @@ namespace Renci.SshNet.IntegrationTests
 
                     var asyncResult = client.BeginUploadFile(uploadMemoryStream, remoteFile, false, null, null);
 
-                    try
-                    {
-                        client.EndUploadFile(asyncResult);
-                        Assert.Fail();
-                    }
-                    catch (SshException ex)
-                    {
-                        Assert.AreEqual(typeof(SshException), ex.GetType());
-                        Assert.IsNull(ex.InnerException);
-                        Assert.AreEqual("Failure", ex.Message);
-                    }
+                    var ex = Assert.Throws<SftpException>(() => client.EndUploadFile(asyncResult));
+                    Assert.AreEqual(StatusCode.Failure, ex.StatusCode);
                 }
                 finally
                 {

+ 66 - 0
test/Renci.SshNet.Tests/Classes/Common/SftpExceptionTest.cs

@@ -0,0 +1,66 @@
+using System;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Renci.SshNet.Common;
+using Renci.SshNet.Sftp;
+
+namespace Renci.SshNet.Tests.Classes.Common
+{
+    [TestClass]
+    public class SftpExceptionTest
+    {
+        [TestMethod]
+        public void StatusCodes()
+        {
+            Assert.AreEqual(StatusCode.BadMessage, new SftpException(StatusCode.BadMessage).StatusCode);
+            Assert.AreEqual(StatusCode.OperationUnsupported, new SftpException(StatusCode.OperationUnsupported, null).StatusCode);
+            Assert.AreEqual(StatusCode.Failure, new SftpException(StatusCode.Failure, null, null).StatusCode);
+
+            Assert.AreEqual(StatusCode.PermissionDenied, new SftpPermissionDeniedException().StatusCode);
+            Assert.AreEqual(StatusCode.PermissionDenied, new SftpPermissionDeniedException(null).StatusCode);
+            Assert.AreEqual(StatusCode.PermissionDenied, new SftpPermissionDeniedException(null, null).StatusCode);
+
+            Assert.AreEqual(StatusCode.NoSuchFile, new SftpPathNotFoundException().StatusCode);
+            Assert.AreEqual(StatusCode.NoSuchFile, new SftpPathNotFoundException(null).StatusCode);
+            Assert.AreEqual(StatusCode.NoSuchFile, new SftpPathNotFoundException(null, path: null).StatusCode);
+            Assert.AreEqual(StatusCode.NoSuchFile, new SftpPathNotFoundException(null, innerException: null).StatusCode);
+            Assert.AreEqual(StatusCode.NoSuchFile, new SftpPathNotFoundException(null, null, null).StatusCode);
+        }
+
+        [TestMethod]
+        public void Message()
+        {
+            Assert.IsFalse(string.IsNullOrWhiteSpace(new SftpException(StatusCode.Failure).Message));
+            Assert.IsFalse(string.IsNullOrWhiteSpace(new SftpException(StatusCode.Failure, "").Message));
+            Assert.AreEqual("Custom message", new SftpException(StatusCode.Failure, "Custom message").Message);
+
+            Assert.IsFalse(string.IsNullOrWhiteSpace(new SftpPermissionDeniedException().Message));
+            Assert.IsFalse(string.IsNullOrWhiteSpace(new SftpPermissionDeniedException("").Message));
+            Assert.IsFalse(string.IsNullOrWhiteSpace(new SftpPermissionDeniedException("", null).Message));
+            Assert.AreEqual("Custom message1", new SftpPermissionDeniedException("Custom message1").Message);
+            Assert.AreEqual("Custom message2", new SftpPermissionDeniedException("Custom message2", null).Message);
+
+            Assert.IsFalse(string.IsNullOrWhiteSpace(new SftpPathNotFoundException().Message));
+            Assert.IsFalse(string.IsNullOrWhiteSpace(new SftpPathNotFoundException("").Message));
+            Assert.IsFalse(string.IsNullOrWhiteSpace(new SftpPathNotFoundException("", path: null).Message));
+            Assert.AreEqual("Custom message1", new SftpPathNotFoundException("Custom message1").Message);
+            Assert.AreEqual("Custom message2", new SftpPathNotFoundException("Custom message2", path: null).Message);
+            Assert.AreEqual("Custom message2", new SftpPathNotFoundException("Custom message2", "path1").Message);
+            Assert.AreEqual("Custom message3", new SftpPathNotFoundException("Custom message3", innerException: null).Message);
+            Assert.AreEqual("Custom message4", new SftpPathNotFoundException("Custom message4", null, null).Message);
+        }
+
+        [TestMethod]
+        public void PathNotFoundException_Path()
+        {
+            Assert.IsNull(new SftpPathNotFoundException().Path);
+            Assert.IsNull(new SftpPathNotFoundException("message").Path);
+            Assert.AreEqual("path1", new SftpPathNotFoundException("message", "path1").Path);
+            Assert.AreEqual("path2", new SftpPathNotFoundException(null, "path2", null).Path);
+
+            Assert.Contains("Path: 'path3'.", new SftpPathNotFoundException(message: null, path: "path3").Message, StringComparison.Ordinal);
+            Assert.Contains("Path: 'path4'.", new SftpPathNotFoundException(message: "", path: "path4").Message, StringComparison.Ordinal);
+        }
+    }
+}

+ 1 - 1
test/Renci.SshNet.Tests/Classes/SftpClientTest.UploadFile.cs

@@ -145,7 +145,7 @@ namespace Renci.SshNet.Tests.Classes
                     else if (dataMsg.Data[sizeof(uint)] == (byte)SftpMessageTypes.Write)
                     {
                         // Fail the 5th write request
-                        var statusCode = ++_numWriteRequests == 5 ? StatusCodes.PermissionDenied : StatusCodes.Ok;
+                        var statusCode = ++_numWriteRequests == 5 ? StatusCode.PermissionDenied : StatusCode.Ok;
                         var responseId = ++_numRequests;
 
                         // Dispatch the responses on a different thread to simulate reality.

+ 1 - 1
version.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
-  "version": "2025.0.1-prerelease.{height}",
+  "version": "2025.1.0-prerelease.{height}",
   "assemblyVersion": {
     "precision": "revision"
   },