Skip to content

Commit

Permalink
Correct and expand errno mapping to Python exeptions (#1871)
Browse files Browse the repository at this point in the history
* Winsock error mappings

* Correct and expand errno mapping to Python exeptions

* Unwrap FACILITY_WIN32 HRESULT errors
  • Loading branch information
BCSharp authored Jan 14, 2025
1 parent 1cd8e71 commit 0b605ec
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 61 deletions.
4 changes: 4 additions & 0 deletions Src/IronPython.Modules/nt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2326,6 +2326,10 @@ private static string GetWin32ErrorMessage(int errorCode) {

[SupportedOSPlatform("windows")]
internal static Exception GetWin32Error(int winerror, string? filename = null, string? filename2 = null) {
// Unwrap FACILITY_WIN32 HRESULT errors
if ((winerror & 0xFFFF0000) == 0x80070000) {
winerror &= 0x0000FFFF;
}
var msg = GetWin32ErrorMessage(winerror);
return PythonOps.OSError(0, msg, filename, winerror, filename2);
}
Expand Down
12 changes: 7 additions & 5 deletions Src/IronPython/Modules/_fileio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
using IronPython.Runtime.Exceptions;
using IronPython.Runtime.Operations;
using IronPython.Runtime.Types;
using PythonErrno = IronPython.Runtime.Exceptions.PythonExceptions._OSError.Errno;

using Microsoft.Scripting;
using Microsoft.Scripting.Runtime;
using Mono.Unix.Native;

#nullable enable

Expand Down Expand Up @@ -179,7 +181,7 @@ public FileIO(CodeContext/*!*/ context, [NotNone] string name, [NotNone] string
// In such case:
// _streams = new(new UnixStream(fd, ownsHandle: true))
// _context.FileManager.Add(fd, _streams);
throw PythonOps.OSError(PythonFileManager.EBADF, "Bad file descriptor");
throw PythonOps.OSError(PythonErrno.EBADF, "Bad file descriptor");
}
} else {
throw PythonOps.TypeError("expected integer from opener");
Expand Down Expand Up @@ -450,7 +452,7 @@ public override BigInteger seek(CodeContext/*!*/ context, BigInteger offset, [Op

var origin = (SeekOrigin)GetInt(whence);
if (origin < SeekOrigin.Begin || origin > SeekOrigin.End)
throw PythonOps.OSError(PythonFileManager.EINVAL, "Invalid argument");
throw PythonOps.OSError(PythonErrno.EINVAL, "Invalid argument");

long ofs = checked((long)offset);

Expand Down Expand Up @@ -584,13 +586,13 @@ private static void AddFilename(CodeContext context, string name, Exception ioe)


private static Stream OpenFile(CodeContext/*!*/ context, PlatformAdaptationLayer pal, string name, FileMode fileMode, FileAccess fileAccess, FileShare fileShare) {
if (string.IsNullOrWhiteSpace(name)) throw PythonOps.OSError(PythonFileManager.ENOENT, "No such file or directory", filename: name);
if (string.IsNullOrWhiteSpace(name)) throw PythonOps.OSError(PythonErrno.ENOENT, "No such file or directory", filename: name);
try {
return pal.OpenFileStream(name, fileMode, fileAccess, fileShare, 1); // Use a 1 byte buffer size to disable buffering (if the FileStream implementation supports it).
} catch (UnauthorizedAccessException) {
throw PythonOps.OSError(PythonFileManager.EACCES, "Permission denied", name);
throw PythonOps.OSError(PythonErrno.EACCES, "Permission denied", name);
} catch (FileNotFoundException) {
throw PythonOps.OSError(PythonFileManager.ENOENT, "No such file or directory", name);
throw PythonOps.OSError(PythonErrno.ENOENT, "No such file or directory", name);
} catch (IOException e) {
AddFilename(context, name, e);
throw;
Expand Down
134 changes: 107 additions & 27 deletions Src/IronPython/Runtime/Exceptions/PythonExceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using IronPython.Runtime.Operations;
using IronPython.Runtime.Types;


#if !FEATURE_REMOTING
using MarshalByRefObject = System.Object;
#endif
Expand All @@ -36,7 +37,7 @@ namespace IronPython.Runtime.Exceptions {
/// Because the oddity of the built-in exception types all sharing the same physical layout
/// (see also PythonExceptions.BaseException) some classes are defined as classes w/ their
/// proper name and some classes are defined as PythonType fields. When a class is defined
/// for convenience their's also an _TypeName version which is the PythonType.
/// for convenience there's also an _TypeName version which is the PythonType.
/// </summary>
public static partial class PythonExceptions {
private static readonly object _pythonExceptionKey = typeof(BaseException);
Expand Down Expand Up @@ -124,9 +125,9 @@ public partial class _OSError {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
if (args.Length >= 4 && args[3] is int winerror) {
errno = WinErrorToErrno(winerror);
}
}
}
cls = ErrnoToPythonType(ErrnoToErrorEnum(errno));
cls = ErrorEnumToPythonType(ErrnoToErrorEnum(errno));
}
return Activator.CreateInstance(cls.UnderlyingSystemType, cls);
}
Expand Down Expand Up @@ -179,6 +180,13 @@ public override void __init__(params object[] args) {
base.__init__(args);
}

// This enum is used solely for the purpose of mapping errno values to Python exception types.
// The values are based on errno codes but do not exactly match them;
// they are selected such that it is possible to algorithmically map them from true platform-dependent errno values.
// The subset of codes is chosen that is sufficient for mapping all relevant Python exceptions.
// Because it is an enum, it can be used in switch statements and expressions, simplifying the code
// over using actual errno values (which are not always compile-time constants) while keeping it readable.
// In a way it is subset-equivalent to Mono.Unix.Native.Errno, but it is not dependent on Mono.Posix assembly.
private enum Error {
UNSPECIFIED = -1,
EPERM = 1,
Expand Down Expand Up @@ -211,15 +219,17 @@ private enum Error {
WSAECONNREFUSED = 10061,
}

// Not all input errno values are mapped to existing constants of Error.
// This is suffcient since all values that are not listed as Error constants are mapped to OSError.
private static Error ErrnoToErrorEnum(int errno) {
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
if (errno == 11) return Error.UNSPECIFIED; // EAGAIN on Linux/Windows but EDEADLK on OSX, which is not being remapped
if (errno >= 35) errno += 10000; // add WSABASEERR to map to Windows error range
if (errno >= 35) errno += WSABASEERR; // add WSABASEERR to map to Windows error range
}
return (Error)errno;
}

private static PythonType ErrnoToPythonType(Error errno) {
private static PythonType ErrorEnumToPythonType(Error errno) {
var res = errno switch {
Error.EPERM => PermissionError,
Error.ENOENT => FileNotFoundError,
Expand Down Expand Up @@ -263,6 +273,42 @@ private static PythonType ErrnoToPythonType(Error errno) {
return res ?? OSError;
}

/// <summary>
/// Provides a subset of platform-independent errno codes to be used in this assembly.
/// </summary>
/// <remarks>
/// Values of the Errno codes defined here are identical with values defined in PythonErrno in IronPython.Modules.dll.
/// </remarks>
internal static class Errno {

#region Generated Common Errno Codes

// *** BEGIN GENERATED CODE ***
// generated by function: generate_common_errno_codes from: generate_os_codes.py

internal const int ENOENT = 2;
internal const int E2BIG = 7;
internal const int ENOEXEC = 8;
internal const int EBADF = 9;
internal const int ECHILD = 10;
internal static int EAGAIN => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 11 : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 35 : 11;
internal const int ENOMEM = 12;
internal const int EACCES = 13;
internal const int EEXIST = 17;
internal const int EXDEV = 18;
internal const int ENOTDIR = 20;
internal const int EMFILE = 24;
internal const int ENOSPC = 28;
internal const int EPIPE = 32;
internal static int ENOTEMPTY => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 41 : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 66 : 39;
internal static int EILSEQ => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 42 : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 92 : 84;
internal const int EINVAL = 22;

// *** END GENERATED CODE ***

#endregion
}

/*
* errors were generated using this script run against CPython:
f = open(r'C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\WinError.h', 'r')
Expand Down Expand Up @@ -373,6 +419,7 @@ private static PythonType ErrnoToPythonType(Error errno) {
internal const int ERROR_NESTING_NOT_ALLOWED = 215;
internal const int ERROR_NO_DATA = 232;
internal const int ERROR_DIRECTORY = 267;
internal const int ERROR_NO_UNICODE_TRANSLATION = 1113;
internal const int ERROR_NOT_ENOUGH_QUOTA = 1816;

// These map to POSIX errno 22 and are added by hand as needed.
Expand All @@ -381,14 +428,25 @@ private static PythonType ErrnoToPythonType(Error errno) {
internal const int ERROR_FILE_INVALID = 1006;
internal const int ERROR_MAPPED_ALIGNMENT = 1132;

// Some Winsock error codes are errno values.
internal const int WSABASEERR = 10000;
internal const int WSAEINTR = WSABASEERR + 4;
internal const int WSAEBADF = WSABASEERR + 9;
internal const int WSAEACCES = WSABASEERR + 13;
internal const int WSAEFAULT = WSABASEERR + 14;
internal const int WSAEINVAL = WSABASEERR + 22;
internal const int WSAEMFILE = WSABASEERR + 24;

// See also errmap.h in CPython
internal static int WinErrorToErrno(int winerror) {
// Unwrap FACILITY_WIN32 HRESULT errors
if ((winerror & 0xFFFF0000) == 0x80070000) {
winerror &= 0x0000FFFF;
}

int errno = winerror;
if (winerror < 10000) {
if (winerror < WSABASEERR) {
switch (winerror) {
case ERROR_BROKEN_PIPE:
case ERROR_NO_DATA:
errno = 32;
break;
case ERROR_FILE_NOT_FOUND:
case ERROR_PATH_NOT_FOUND:
case ERROR_INVALID_DRIVE:
Expand All @@ -397,10 +455,10 @@ internal static int WinErrorToErrno(int winerror) {
case ERROR_BAD_NET_NAME:
case ERROR_BAD_PATHNAME:
case ERROR_FILENAME_EXCED_RANGE:
errno = 2;
errno = Errno.ENOENT;
break;
case ERROR_BAD_ENVIRONMENT:
errno = 7;
errno = Errno.E2BIG;
break;
case ERROR_BAD_FORMAT:
case ERROR_INVALID_STARTING_CODESEG:
Expand All @@ -418,27 +476,27 @@ internal static int WinErrorToErrno(int winerror) {
case ERROR_RING2SEG_MUST_BE_MOVABLE:
case ERROR_RELOC_CHAIN_XEEDS_SEGLIM:
case ERROR_INFLOOP_IN_RELOC_CHAIN:
errno = 8;
errno = Errno.ENOEXEC;
break;
case ERROR_INVALID_HANDLE:
case ERROR_INVALID_TARGET_HANDLE:
case ERROR_DIRECT_ACCESS_HANDLE:
errno = 9;
errno = Errno.EBADF;
break;
case ERROR_WAIT_NO_CHILDREN:
case ERROR_CHILD_NOT_COMPLETE:
errno = 10;
errno = Errno.ECHILD;
break;
case ERROR_NO_PROC_SLOTS:
case ERROR_MAX_THRDS_REACHED:
case ERROR_NESTING_NOT_ALLOWED:
errno = 11;
errno = Errno.EAGAIN;
break;
case ERROR_ARENA_TRASHED:
case ERROR_NOT_ENOUGH_MEMORY:
case ERROR_INVALID_BLOCK:
case ERROR_NOT_ENOUGH_QUOTA:
errno = 12;
errno = Errno.ENOMEM;
break;
case ERROR_ACCESS_DENIED:
case ERROR_CURRENT_DIRECTORY:
Expand Down Expand Up @@ -466,29 +524,51 @@ internal static int WinErrorToErrno(int winerror) {
case ERROR_SEEK_ON_DEVICE:
case ERROR_NOT_LOCKED:
case ERROR_LOCK_FAILED:
errno = 13;
case 35: // undefined
errno = Errno.EACCES;
break;
case ERROR_FILE_EXISTS:
case ERROR_ALREADY_EXISTS:
errno = 17;
errno = Errno.EEXIST;
break;
case ERROR_NOT_SAME_DEVICE:
errno = 18;
errno = Errno.EXDEV;
break;
case ERROR_DIRECTORY:
errno = 20;
break;
case ERROR_DIR_NOT_EMPTY:
errno = 41;
errno = Errno.ENOTDIR;
break;
case ERROR_TOO_MANY_OPEN_FILES:
errno = 24;
errno = Errno.EMFILE;
break;
case ERROR_DISK_FULL:
errno = 28;
errno = Errno.ENOSPC;
break;
case ERROR_BROKEN_PIPE:
case ERROR_NO_DATA:
errno = Errno.EPIPE;
break;
case ERROR_DIR_NOT_EMPTY: // ENOTEMPTY
errno = Errno.ENOTEMPTY;
break;
case ERROR_NO_UNICODE_TRANSLATION: // EILSEQ
errno = Errno.EILSEQ;
break;
default:
errno = Errno.EINVAL;
break;
}
} else if (winerror < 12000) { // Winsock error codes are 10000-11999
switch (winerror) {
case WSAEINTR:
case WSAEBADF:
case WSAEACCES:
case WSAEFAULT:
case WSAEINVAL:
case WSAEMFILE:
errno = winerror - WSABASEERR;
break;
default:
errno = 22;
errno = winerror;
break;
}
}
Expand Down
11 changes: 6 additions & 5 deletions Src/IronPython/Runtime/PosixFileStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#endif

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
Expand All @@ -15,8 +16,8 @@
using Mono.Unix.Native;

using IronPython.Runtime.Operations;
using System.Diagnostics;
using IronPython.Runtime.Exceptions;
using PythonErrno = IronPython.Runtime.Exceptions.PythonExceptions._OSError.Errno;

#nullable enable

Expand All @@ -39,7 +40,7 @@ public PosixFileStream(int fileDescriptor) {
throw new PlatformNotSupportedException("This stream only works on POSIX systems");

if (fileDescriptor < 0)
throw PythonOps.OSError(PythonFileManager.EBADF, "Bad file descriptor");
throw PythonOps.OSError(PythonErrno.EBADF, "Bad file descriptor");

_fd = fileDescriptor;

Expand Down Expand Up @@ -97,7 +98,7 @@ public override long Seek(long offset, SeekOrigin origin) {
SeekOrigin.Begin => SeekFlags.SEEK_SET,
SeekOrigin.Current => SeekFlags.SEEK_CUR,
SeekOrigin.End => SeekFlags.SEEK_END,
_ => throw PythonOps.OSError(PythonFileManager.EINVAL, "Invalid argument")
_ => throw PythonOps.OSError(PythonErrno.EINVAL, "Invalid argument")
};

long result = Syscall.lseek(_fd, offset, whence);
Expand Down Expand Up @@ -126,7 +127,7 @@ public override void SetLength(long value) {
public int Read(Span<byte> buffer) {
ThrowIfDisposed();
if (!CanRead)
throw PythonOps.OSError(PythonFileManager.EBADF, "Bad file descriptor");
throw PythonOps.OSError(PythonErrno.EBADF, "Bad file descriptor");


if (buffer.Length == 0)
Expand Down Expand Up @@ -169,7 +170,7 @@ public override int ReadByte() {
public void Write(ReadOnlySpan<byte> buffer) {
ThrowIfDisposed();
if (!CanWrite)
throw PythonOps.OSError(PythonFileManager.EBADF, "Bad file descriptor");
throw PythonOps.OSError(PythonErrno.EBADF, "Bad file descriptor");

if (buffer.Length == 0)
return;
Expand Down
Loading

0 comments on commit 0b605ec

Please sign in to comment.