diff --git a/Src/IronPython.Modules/nt.cs b/Src/IronPython.Modules/nt.cs index b77b65a51..9746769d9 100644 --- a/Src/IronPython.Modules/nt.cs +++ b/Src/IronPython.Modules/nt.cs @@ -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); } diff --git a/Src/IronPython/Modules/_fileio.cs b/Src/IronPython/Modules/_fileio.cs index 61dbcc600..203bcf2b9 100644 --- a/Src/IronPython/Modules/_fileio.cs +++ b/Src/IronPython/Modules/_fileio.cs @@ -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 @@ -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"); @@ -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); @@ -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; diff --git a/Src/IronPython/Runtime/Exceptions/PythonExceptions.cs b/Src/IronPython/Runtime/Exceptions/PythonExceptions.cs index 9c48ded0b..566653c29 100644 --- a/Src/IronPython/Runtime/Exceptions/PythonExceptions.cs +++ b/Src/IronPython/Runtime/Exceptions/PythonExceptions.cs @@ -20,6 +20,7 @@ using IronPython.Runtime.Operations; using IronPython.Runtime.Types; + #if !FEATURE_REMOTING using MarshalByRefObject = System.Object; #endif @@ -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. /// public static partial class PythonExceptions { private static readonly object _pythonExceptionKey = typeof(BaseException); @@ -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); } @@ -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, @@ -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, @@ -263,6 +273,42 @@ private static PythonType ErrnoToPythonType(Error errno) { return res ?? OSError; } + /// + /// Provides a subset of platform-independent errno codes to be used in this assembly. + /// + /// + /// Values of the Errno codes defined here are identical with values defined in PythonErrno in IronPython.Modules.dll. + /// + 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') @@ -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. @@ -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: @@ -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: @@ -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: @@ -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; } } diff --git a/Src/IronPython/Runtime/PosixFileStream.cs b/Src/IronPython/Runtime/PosixFileStream.cs index 36a380b0a..d4b9e8a8a 100644 --- a/Src/IronPython/Runtime/PosixFileStream.cs +++ b/Src/IronPython/Runtime/PosixFileStream.cs @@ -7,6 +7,7 @@ #endif using System; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -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 @@ -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; @@ -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); @@ -126,7 +127,7 @@ public override void SetLength(long value) { public int Read(Span buffer) { ThrowIfDisposed(); if (!CanRead) - throw PythonOps.OSError(PythonFileManager.EBADF, "Bad file descriptor"); + throw PythonOps.OSError(PythonErrno.EBADF, "Bad file descriptor"); if (buffer.Length == 0) @@ -169,7 +170,7 @@ public override int ReadByte() { public void Write(ReadOnlySpan 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; diff --git a/Src/IronPython/Runtime/PythonFileManager.cs b/Src/IronPython/Runtime/PythonFileManager.cs index f1efa378f..20cdfb36c 100644 --- a/Src/IronPython/Runtime/PythonFileManager.cs +++ b/Src/IronPython/Runtime/PythonFileManager.cs @@ -16,9 +16,9 @@ using Microsoft.Scripting.Runtime; using Microsoft.Scripting.Utils; -using Mono.Unix; using IronPython.Runtime.Operations; +using PythonErrno = IronPython.Runtime.Exceptions.PythonExceptions._OSError.Errno; namespace IronPython.Runtime { @@ -214,25 +214,6 @@ internal class PythonFileManager { /// public const int LIMIT_OFILE = 0x100000; // hard limit on Linux - - // Values of the Errno codes below have to be identical with values defined in PythonErrorNumber - - #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 EBADF = 9; - internal const int EACCES = 13; - internal const int EINVAL = 22; - internal const int EMFILE = 24; - - // *** END GENERATED CODE *** - - #endregion - - private readonly object _synchObject = new(); private readonly Dictionary _table = new(); private const int _offset = 3; // number of lowest keys that are not automatically allocated @@ -247,7 +228,7 @@ public int Add(int id, StreamBox streams) { ContractUtils.Requires(id >= 0, nameof(id)); lock (_synchObject) { if (_table.ContainsKey(id)) { - throw PythonOps.OSError(EBADF, "Bad file descriptor", id.ToString()); + throw PythonOps.OSError(PythonErrno.EBADF, "Bad file descriptor", id.ToString()); } streams.Id = id; _table.Add(id, streams); @@ -265,7 +246,7 @@ public int Add(StreamBox streams) { while (_table.ContainsKey(_current)) { _current++; if (_current >= LIMIT_OFILE) - throw PythonOps.OSError(EMFILE, "Too many open files"); + throw PythonOps.OSError(PythonErrno.EMFILE, "Too many open files"); } streams.Id = _current; _table.Add(_current, streams); @@ -302,7 +283,7 @@ public StreamBox GetStreams(int id) { if (TryGetStreams(id, out StreamBox? streams)) { return streams; } - throw PythonOps.OSError(EBADF, "Bad file descriptor"); + throw PythonOps.OSError(PythonErrno.EBADF, "Bad file descriptor"); } public int GetOrAssignId(StreamBox streams) { diff --git a/Src/Scripts/generate_os_codes.py b/Src/Scripts/generate_os_codes.py index 57719d5ee..b69069634 100644 --- a/Src/Scripts/generate_os_codes.py +++ b/Src/Scripts/generate_os_codes.py @@ -102,7 +102,7 @@ def darwin_code_expr(codes, fmt): def linux_code_expr(codes, fmt): return fmt(codes[linux_idx]) -common_errno_codes = ['ENOENT', 'EBADF', 'EACCES', 'EINVAL', 'EMFILE'] +common_errno_codes = ['ENOENT', 'E2BIG', 'ENOEXEC', 'EBADF', 'ECHILD', 'EAGAIN', 'ENOMEM', 'EACCES', 'EEXIST', 'EXDEV', 'ENOTDIR', 'EMFILE', 'ENOSPC', 'EPIPE', 'ENOTEMPTY', 'EILSEQ', 'EINVAL'] def generate_common_errno_codes(cw): for name in common_errno_codes: