Skip to content

Commit 0b605ec

Browse files
authored
Correct and expand errno mapping to Python exeptions (#1871)
* Winsock error mappings * Correct and expand errno mapping to Python exeptions * Unwrap FACILITY_WIN32 HRESULT errors
1 parent 1cd8e71 commit 0b605ec

File tree

6 files changed

+129
-61
lines changed

6 files changed

+129
-61
lines changed

Src/IronPython.Modules/nt.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2326,6 +2326,10 @@ private static string GetWin32ErrorMessage(int errorCode) {
23262326

23272327
[SupportedOSPlatform("windows")]
23282328
internal static Exception GetWin32Error(int winerror, string? filename = null, string? filename2 = null) {
2329+
// Unwrap FACILITY_WIN32 HRESULT errors
2330+
if ((winerror & 0xFFFF0000) == 0x80070000) {
2331+
winerror &= 0x0000FFFF;
2332+
}
23292333
var msg = GetWin32ErrorMessage(winerror);
23302334
return PythonOps.OSError(0, msg, filename, winerror, filename2);
23312335
}

Src/IronPython/Modules/_fileio.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
using IronPython.Runtime.Exceptions;
1616
using IronPython.Runtime.Operations;
1717
using IronPython.Runtime.Types;
18+
using PythonErrno = IronPython.Runtime.Exceptions.PythonExceptions._OSError.Errno;
1819

1920
using Microsoft.Scripting;
2021
using Microsoft.Scripting.Runtime;
22+
using Mono.Unix.Native;
2123

2224
#nullable enable
2325

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

451453
var origin = (SeekOrigin)GetInt(whence);
452454
if (origin < SeekOrigin.Begin || origin > SeekOrigin.End)
453-
throw PythonOps.OSError(PythonFileManager.EINVAL, "Invalid argument");
455+
throw PythonOps.OSError(PythonErrno.EINVAL, "Invalid argument");
454456

455457
long ofs = checked((long)offset);
456458

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

585587

586588
private static Stream OpenFile(CodeContext/*!*/ context, PlatformAdaptationLayer pal, string name, FileMode fileMode, FileAccess fileAccess, FileShare fileShare) {
587-
if (string.IsNullOrWhiteSpace(name)) throw PythonOps.OSError(PythonFileManager.ENOENT, "No such file or directory", filename: name);
589+
if (string.IsNullOrWhiteSpace(name)) throw PythonOps.OSError(PythonErrno.ENOENT, "No such file or directory", filename: name);
588590
try {
589591
return pal.OpenFileStream(name, fileMode, fileAccess, fileShare, 1); // Use a 1 byte buffer size to disable buffering (if the FileStream implementation supports it).
590592
} catch (UnauthorizedAccessException) {
591-
throw PythonOps.OSError(PythonFileManager.EACCES, "Permission denied", name);
593+
throw PythonOps.OSError(PythonErrno.EACCES, "Permission denied", name);
592594
} catch (FileNotFoundException) {
593-
throw PythonOps.OSError(PythonFileManager.ENOENT, "No such file or directory", name);
595+
throw PythonOps.OSError(PythonErrno.ENOENT, "No such file or directory", name);
594596
} catch (IOException e) {
595597
AddFilename(context, name, e);
596598
throw;

Src/IronPython/Runtime/Exceptions/PythonExceptions.cs

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using IronPython.Runtime.Operations;
2121
using IronPython.Runtime.Types;
2222

23+
2324
#if !FEATURE_REMOTING
2425
using MarshalByRefObject = System.Object;
2526
#endif
@@ -36,7 +37,7 @@ namespace IronPython.Runtime.Exceptions {
3637
/// Because the oddity of the built-in exception types all sharing the same physical layout
3738
/// (see also PythonExceptions.BaseException) some classes are defined as classes w/ their
3839
/// proper name and some classes are defined as PythonType fields. When a class is defined
39-
/// for convenience their's also an _TypeName version which is the PythonType.
40+
/// for convenience there's also an _TypeName version which is the PythonType.
4041
/// </summary>
4142
public static partial class PythonExceptions {
4243
private static readonly object _pythonExceptionKey = typeof(BaseException);
@@ -124,9 +125,9 @@ public partial class _OSError {
124125
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
125126
if (args.Length >= 4 && args[3] is int winerror) {
126127
errno = WinErrorToErrno(winerror);
127-
}
128+
}
128129
}
129-
cls = ErrnoToPythonType(ErrnoToErrorEnum(errno));
130+
cls = ErrorEnumToPythonType(ErrnoToErrorEnum(errno));
130131
}
131132
return Activator.CreateInstance(cls.UnderlyingSystemType, cls);
132133
}
@@ -179,6 +180,13 @@ public override void __init__(params object[] args) {
179180
base.__init__(args);
180181
}
181182

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

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

222-
private static PythonType ErrnoToPythonType(Error errno) {
232+
private static PythonType ErrorEnumToPythonType(Error errno) {
223233
var res = errno switch {
224234
Error.EPERM => PermissionError,
225235
Error.ENOENT => FileNotFoundError,
@@ -263,6 +273,42 @@ private static PythonType ErrnoToPythonType(Error errno) {
263273
return res ?? OSError;
264274
}
265275

276+
/// <summary>
277+
/// Provides a subset of platform-independent errno codes to be used in this assembly.
278+
/// </summary>
279+
/// <remarks>
280+
/// Values of the Errno codes defined here are identical with values defined in PythonErrno in IronPython.Modules.dll.
281+
/// </remarks>
282+
internal static class Errno {
283+
284+
#region Generated Common Errno Codes
285+
286+
// *** BEGIN GENERATED CODE ***
287+
// generated by function: generate_common_errno_codes from: generate_os_codes.py
288+
289+
internal const int ENOENT = 2;
290+
internal const int E2BIG = 7;
291+
internal const int ENOEXEC = 8;
292+
internal const int EBADF = 9;
293+
internal const int ECHILD = 10;
294+
internal static int EAGAIN => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 11 : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 35 : 11;
295+
internal const int ENOMEM = 12;
296+
internal const int EACCES = 13;
297+
internal const int EEXIST = 17;
298+
internal const int EXDEV = 18;
299+
internal const int ENOTDIR = 20;
300+
internal const int EMFILE = 24;
301+
internal const int ENOSPC = 28;
302+
internal const int EPIPE = 32;
303+
internal static int ENOTEMPTY => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 41 : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 66 : 39;
304+
internal static int EILSEQ => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 42 : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 92 : 84;
305+
internal const int EINVAL = 22;
306+
307+
// *** END GENERATED CODE ***
308+
309+
#endregion
310+
}
311+
266312
/*
267313
* errors were generated using this script run against CPython:
268314
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) {
373419
internal const int ERROR_NESTING_NOT_ALLOWED = 215;
374420
internal const int ERROR_NO_DATA = 232;
375421
internal const int ERROR_DIRECTORY = 267;
422+
internal const int ERROR_NO_UNICODE_TRANSLATION = 1113;
376423
internal const int ERROR_NOT_ENOUGH_QUOTA = 1816;
377424

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

431+
// Some Winsock error codes are errno values.
432+
internal const int WSABASEERR = 10000;
433+
internal const int WSAEINTR = WSABASEERR + 4;
434+
internal const int WSAEBADF = WSABASEERR + 9;
435+
internal const int WSAEACCES = WSABASEERR + 13;
436+
internal const int WSAEFAULT = WSABASEERR + 14;
437+
internal const int WSAEINVAL = WSABASEERR + 22;
438+
internal const int WSAEMFILE = WSABASEERR + 24;
439+
440+
// See also errmap.h in CPython
384441
internal static int WinErrorToErrno(int winerror) {
442+
// Unwrap FACILITY_WIN32 HRESULT errors
443+
if ((winerror & 0xFFFF0000) == 0x80070000) {
444+
winerror &= 0x0000FFFF;
445+
}
446+
385447
int errno = winerror;
386-
if (winerror < 10000) {
448+
if (winerror < WSABASEERR) {
387449
switch (winerror) {
388-
case ERROR_BROKEN_PIPE:
389-
case ERROR_NO_DATA:
390-
errno = 32;
391-
break;
392450
case ERROR_FILE_NOT_FOUND:
393451
case ERROR_PATH_NOT_FOUND:
394452
case ERROR_INVALID_DRIVE:
@@ -397,10 +455,10 @@ internal static int WinErrorToErrno(int winerror) {
397455
case ERROR_BAD_NET_NAME:
398456
case ERROR_BAD_PATHNAME:
399457
case ERROR_FILENAME_EXCED_RANGE:
400-
errno = 2;
458+
errno = Errno.ENOENT;
401459
break;
402460
case ERROR_BAD_ENVIRONMENT:
403-
errno = 7;
461+
errno = Errno.E2BIG;
404462
break;
405463
case ERROR_BAD_FORMAT:
406464
case ERROR_INVALID_STARTING_CODESEG:
@@ -418,27 +476,27 @@ internal static int WinErrorToErrno(int winerror) {
418476
case ERROR_RING2SEG_MUST_BE_MOVABLE:
419477
case ERROR_RELOC_CHAIN_XEEDS_SEGLIM:
420478
case ERROR_INFLOOP_IN_RELOC_CHAIN:
421-
errno = 8;
479+
errno = Errno.ENOEXEC;
422480
break;
423481
case ERROR_INVALID_HANDLE:
424482
case ERROR_INVALID_TARGET_HANDLE:
425483
case ERROR_DIRECT_ACCESS_HANDLE:
426-
errno = 9;
484+
errno = Errno.EBADF;
427485
break;
428486
case ERROR_WAIT_NO_CHILDREN:
429487
case ERROR_CHILD_NOT_COMPLETE:
430-
errno = 10;
488+
errno = Errno.ECHILD;
431489
break;
432490
case ERROR_NO_PROC_SLOTS:
433491
case ERROR_MAX_THRDS_REACHED:
434492
case ERROR_NESTING_NOT_ALLOWED:
435-
errno = 11;
493+
errno = Errno.EAGAIN;
436494
break;
437495
case ERROR_ARENA_TRASHED:
438496
case ERROR_NOT_ENOUGH_MEMORY:
439497
case ERROR_INVALID_BLOCK:
440498
case ERROR_NOT_ENOUGH_QUOTA:
441-
errno = 12;
499+
errno = Errno.ENOMEM;
442500
break;
443501
case ERROR_ACCESS_DENIED:
444502
case ERROR_CURRENT_DIRECTORY:
@@ -466,29 +524,51 @@ internal static int WinErrorToErrno(int winerror) {
466524
case ERROR_SEEK_ON_DEVICE:
467525
case ERROR_NOT_LOCKED:
468526
case ERROR_LOCK_FAILED:
469-
errno = 13;
527+
case 35: // undefined
528+
errno = Errno.EACCES;
470529
break;
471530
case ERROR_FILE_EXISTS:
472531
case ERROR_ALREADY_EXISTS:
473-
errno = 17;
532+
errno = Errno.EEXIST;
474533
break;
475534
case ERROR_NOT_SAME_DEVICE:
476-
errno = 18;
535+
errno = Errno.EXDEV;
477536
break;
478537
case ERROR_DIRECTORY:
479-
errno = 20;
480-
break;
481-
case ERROR_DIR_NOT_EMPTY:
482-
errno = 41;
538+
errno = Errno.ENOTDIR;
483539
break;
484540
case ERROR_TOO_MANY_OPEN_FILES:
485-
errno = 24;
541+
errno = Errno.EMFILE;
486542
break;
487543
case ERROR_DISK_FULL:
488-
errno = 28;
544+
errno = Errno.ENOSPC;
545+
break;
546+
case ERROR_BROKEN_PIPE:
547+
case ERROR_NO_DATA:
548+
errno = Errno.EPIPE;
549+
break;
550+
case ERROR_DIR_NOT_EMPTY: // ENOTEMPTY
551+
errno = Errno.ENOTEMPTY;
552+
break;
553+
case ERROR_NO_UNICODE_TRANSLATION: // EILSEQ
554+
errno = Errno.EILSEQ;
555+
break;
556+
default:
557+
errno = Errno.EINVAL;
558+
break;
559+
}
560+
} else if (winerror < 12000) { // Winsock error codes are 10000-11999
561+
switch (winerror) {
562+
case WSAEINTR:
563+
case WSAEBADF:
564+
case WSAEACCES:
565+
case WSAEFAULT:
566+
case WSAEINVAL:
567+
case WSAEMFILE:
568+
errno = winerror - WSABASEERR;
489569
break;
490570
default:
491-
errno = 22;
571+
errno = winerror;
492572
break;
493573
}
494574
}

Src/IronPython/Runtime/PosixFileStream.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#endif
88

99
using System;
10+
using System.Diagnostics;
1011
using System.IO;
1112
using System.Runtime.InteropServices;
1213
using System.Runtime.Versioning;
@@ -15,8 +16,8 @@
1516
using Mono.Unix.Native;
1617

1718
using IronPython.Runtime.Operations;
18-
using System.Diagnostics;
1919
using IronPython.Runtime.Exceptions;
20+
using PythonErrno = IronPython.Runtime.Exceptions.PythonExceptions._OSError.Errno;
2021

2122
#nullable enable
2223

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

4142
if (fileDescriptor < 0)
42-
throw PythonOps.OSError(PythonFileManager.EBADF, "Bad file descriptor");
43+
throw PythonOps.OSError(PythonErrno.EBADF, "Bad file descriptor");
4344

4445
_fd = fileDescriptor;
4546

@@ -97,7 +98,7 @@ public override long Seek(long offset, SeekOrigin origin) {
9798
SeekOrigin.Begin => SeekFlags.SEEK_SET,
9899
SeekOrigin.Current => SeekFlags.SEEK_CUR,
99100
SeekOrigin.End => SeekFlags.SEEK_END,
100-
_ => throw PythonOps.OSError(PythonFileManager.EINVAL, "Invalid argument")
101+
_ => throw PythonOps.OSError(PythonErrno.EINVAL, "Invalid argument")
101102
};
102103

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

131132

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

174175
if (buffer.Length == 0)
175176
return;

0 commit comments

Comments
 (0)