@@ -285,8 +285,8 @@ public static bool access(CodeContext context, object? path, int mode, [ParamDic
285285#if FEATURE_FILESYSTEM
286286
287287 public static void chdir ( [ NotNone ] string path ) {
288- if ( String . IsNullOrEmpty ( path ) ) {
289- throw PythonOps . OSError ( PythonExceptions . _OSError . ERROR_INVALID_NAME , "Path cannot be an empty string" , path , PythonExceptions . _OSError . ERROR_INVALID_NAME ) ;
288+ if ( string . IsNullOrEmpty ( path ) ) {
289+ throw GetOsOrWinError ( PythonErrno . ENOENT , PythonExceptions . _OSError . ERROR_INVALID_NAME , path ) ;
290290 }
291291
292292 try {
@@ -553,7 +553,7 @@ public static PythonList listdir(CodeContext/*!*/ context, string? path = null)
553553 }
554554
555555 if ( path == string . Empty ) {
556- throw PythonOps . OSError ( PythonExceptions . _OSError . ERROR_PATH_NOT_FOUND , "The system cannot find the path specified" , path , PythonExceptions . _OSError . ERROR_PATH_NOT_FOUND ) ;
556+ throw GetOsOrWinError ( PythonErrno . ENOENT , PythonExceptions . _OSError . ERROR_PATH_NOT_FOUND , path ) ;
557557 }
558558
559559#if ! NETFRAMEWORK
@@ -628,7 +628,7 @@ internal DirEntry(CodeContext context, FileSystemInfo info, bool asBytes) {
628628
629629 [ LightThrowing ]
630630 public object ? inode ( ) {
631- var obj = stat ( follow_symlinks : false ) ;
631+ var obj = PythonNT . stat ( info . FullName , new Dictionary < string , object > ( ) ) ;
632632 if ( obj is stat_result res ) return res . st_ino ;
633633 return obj ;
634634 }
@@ -640,9 +640,15 @@ internal DirEntry(CodeContext context, FileSystemInfo info, bool asBytes) {
640640 public bool is_symlink ( ) => info . Attributes . HasFlag ( FileAttributes . ReparsePoint ) ? throw new NotImplementedException ( ) : false ;
641641
642642 [ LightThrowing ]
643- public object ? stat ( bool follow_symlinks = true ) => PythonNT . stat ( info . FullName , new Dictionary < string , object > ( ) ) ;
643+ public object ? stat ( bool follow_symlinks = true ) {
644+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
645+ return statWindowsImpl ( info ) ;
646+ return PythonNT . stat ( info . FullName , new Dictionary < string , object > ( ) ) ;
647+ }
644648
645649 public string __repr__ ( CodeContext context ) => $ "<DirEntry { PythonOps . Repr ( context , name ) } >";
650+
651+ public object __fspath__ ( CodeContext context ) => path ;
646652 }
647653
648654 [ PythonType , PythonHidden ]
@@ -675,6 +681,8 @@ internal ScandirIterator(CodeContext context, IEnumerable<FileSystemInfo> list,
675681
676682 [ PythonHidden ]
677683 public void Reset ( ) => enumerator . Reset ( ) ;
684+
685+ public void close ( ) => Dispose ( ) ;
678686 }
679687
680688 public static ScandirIterator scandir ( CodeContext context , string ? path = null )
@@ -689,7 +697,7 @@ private static IEnumerable<FileSystemInfo> ScandirHelper(CodeContext context, st
689697 }
690698
691699 if ( path == string . Empty ) {
692- throw PythonOps . OSError ( PythonExceptions . _OSError . ERROR_PATH_NOT_FOUND , "The system cannot find the path specified" , path , PythonExceptions . _OSError . ERROR_PATH_NOT_FOUND ) ;
700+ throw GetOsOrWinError ( PythonErrno . ENOENT , PythonExceptions . _OSError . ERROR_PATH_NOT_FOUND , path ) ;
693701 }
694702
695703#if ! NETFRAMEWORK
@@ -1396,8 +1404,8 @@ public PythonTuple __reduce__() {
13961404 }
13971405 }
13981406
1399- private static bool HasExecutableExtension ( string path ) {
1400- string extension = Path . GetExtension ( path ) . ToLower ( CultureInfo . InvariantCulture ) ;
1407+ private static bool HasExecutableExtension ( string extension ) {
1408+ extension = extension . ToLower ( CultureInfo . InvariantCulture ) ;
14011409 return ( extension == ".exe" || extension == ".dll" || extension == ".com" || extension == ".bat" ) ;
14021410 }
14031411
@@ -1456,50 +1464,22 @@ public static object stat([NotNone] string path, [ParamDictionary] IDictionary<s
14561464 VerifyPath ( path , functionName : nameof ( stat ) , argName : nameof ( path ) ) ;
14571465
14581466 if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ) {
1467+ if ( IsNulFile ( path ) ) {
1468+ return new stat_result ( 0x2000 ) ;
1469+ }
1470+
14591471 try {
14601472 FileInfo fi = new FileInfo ( path ) ;
1461- int mode = 0 ;
1462- long size ;
1463-
1464- if ( IsNulFile ( path ) ) {
1465- return new stat_result ( 0x2000 ) ;
1466- } else if ( Directory . Exists ( path ) ) {
1467- size = 0 ;
1468- mode = 0x4000 | S_IEXEC ;
1469- } else if ( File . Exists ( path ) ) {
1470- size = fi . Length ;
1471- mode = 0x8000 ;
1472- if ( HasExecutableExtension ( path ) ) {
1473- mode |= S_IEXEC ;
1474- }
1475- } else {
1476- return LightExceptions . Throw ( PythonOps . OSError ( 0 , "file does not exist" , path , PythonExceptions . _OSError . ERROR_PATH_NOT_FOUND ) ) ;
1477- }
1478-
1479- mode |= S_IREAD ;
1480- if ( ( fi . Attributes & FileAttributes . ReadOnly ) == 0 ) {
1481- mode |= S_IWRITE ;
1473+ if ( fi . Exists ) {
1474+ return statWindowsImpl ( fi ) ;
14821475 }
1483-
1484- const long epochDifferenceLong = 62135596800 * TimeSpan . TicksPerSecond ;
1485-
1486- // 1 tick = 100 nanoseconds
1487- long st_atime_ns = ( fi . LastAccessTime . ToUniversalTime ( ) . Ticks - epochDifferenceLong ) * 100 ;
1488- long st_mtime_ns = ( fi . LastWriteTime . ToUniversalTime ( ) . Ticks - epochDifferenceLong ) * 100 ;
1489- long st_ctime_ns = ( fi . CreationTime . ToUniversalTime ( ) . Ticks - epochDifferenceLong ) * 100 ;
1490-
1491- ulong fileIdx = 0 ;
1492- var handle = CreateFile ( path , FILE_READ_ATTRIBUTES , 0 , IntPtr . Zero , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT , IntPtr . Zero ) ;
1493- if ( ! handle . IsInvalid ) {
1494- if ( GetFileInformationByHandle ( handle , out BY_HANDLE_FILE_INFORMATION fileInfo ) ) {
1495- fileIdx = ( ( ( ulong ) fileInfo . FileIndexHigh ) << 32 ) + fileInfo . FileIndexLow ;
1496- }
1497- handle . Close ( ) ;
1476+ DirectoryInfo di = new DirectoryInfo ( path ) ;
1477+ if ( di . Exists ) {
1478+ return statWindowsImpl ( di ) ;
14981479 }
1499-
1500- return new stat_result ( mode , fileIdx , size , st_atime_ns , st_mtime_ns , st_ctime_ns ) ;
1480+ return LightExceptions . Throw ( GetOsOrWinError ( PythonErrno . ENOENT , PythonExceptions . _OSError . ERROR_FILE_NOT_FOUND , path ) ) ;
15011481 } catch ( ArgumentException ) {
1502- return LightExceptions . Throw ( PythonOps . OSError ( 0 , "The path is invalid" , path , PythonExceptions . _OSError . ERROR_INVALID_NAME ) ) ;
1482+ return LightExceptions . Throw ( GetOsOrWinError ( PythonErrno . ENOENT , PythonExceptions . _OSError . ERROR_INVALID_NAME , path ) ) ;
15031483 } catch ( Exception e ) {
15041484 return LightExceptions . Throw ( ToPythonException ( e , path ) ) ;
15051485 }
@@ -1510,6 +1490,46 @@ public static object stat([NotNone] string path, [ParamDictionary] IDictionary<s
15101490 }
15111491 }
15121492
1493+ [ SupportedOSPlatform ( "windows" ) ]
1494+ private static stat_result statWindowsImpl ( FileSystemInfo fsi ) {
1495+ int mode = 0 ;
1496+ long size ;
1497+ if ( fsi is FileInfo fi ) {
1498+ size = fi . Length ;
1499+ mode = 0x8000 ;
1500+ if ( HasExecutableExtension ( fi . Extension ) ) {
1501+ mode |= S_IEXEC ;
1502+ }
1503+ } else {
1504+ Debug . Assert ( fsi is DirectoryInfo ) ;
1505+ size = 0 ;
1506+ mode = 0x4000 | S_IEXEC ;
1507+ }
1508+
1509+ mode |= S_IREAD ;
1510+ if ( ( fsi . Attributes & FileAttributes . ReadOnly ) == 0 ) {
1511+ mode |= S_IWRITE ;
1512+ }
1513+
1514+ const long epochDifferenceLong = 62135596800 * TimeSpan . TicksPerSecond ;
1515+
1516+ // 1 tick = 100 nanoseconds
1517+ long st_atime_ns = ( fsi . LastAccessTime . ToUniversalTime ( ) . Ticks - epochDifferenceLong ) * 100 ;
1518+ long st_mtime_ns = ( fsi . LastWriteTime . ToUniversalTime ( ) . Ticks - epochDifferenceLong ) * 100 ;
1519+ long st_ctime_ns = ( fsi . CreationTime . ToUniversalTime ( ) . Ticks - epochDifferenceLong ) * 100 ;
1520+
1521+ ulong fileIdx = 0 ;
1522+ var handle = CreateFile ( fsi . FullName , FILE_READ_ATTRIBUTES , 0 , IntPtr . Zero , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT , IntPtr . Zero ) ;
1523+ if ( ! handle . IsInvalid ) {
1524+ if ( GetFileInformationByHandle ( handle , out BY_HANDLE_FILE_INFORMATION fileInfo ) ) {
1525+ fileIdx = ( ( ( ulong ) fileInfo . FileIndexHigh ) << 32 ) + fileInfo . FileIndexLow ;
1526+ }
1527+ handle . Close ( ) ;
1528+ }
1529+
1530+ return new stat_result ( mode , fileIdx , size , st_atime_ns , st_mtime_ns , st_ctime_ns ) ;
1531+ }
1532+
15131533 [ LightThrowing , Documentation ( "" ) ]
15141534 public static object stat ( CodeContext context , [ NotNone ] Bytes path , [ ParamDictionary ] IDictionary < string , object > dict )
15151535 => stat ( path . ToFsString ( context ) , dict ) ;
@@ -1663,7 +1683,20 @@ public static void remove([NotNone] string path, [ParamDictionary] IDictionary<s
16631683 throw PythonOps . TypeError ( "'{0}' is an invalid keyword argument for this function" , key ) ;
16641684 }
16651685 }
1666- UnlinkWorker ( path ) ;
1686+
1687+ if ( path . IndexOfAny ( Path . GetInvalidPathChars ( ) ) != - 1 || Path . GetFileName ( path ) . IndexOfAny ( Path . GetInvalidFileNameChars ( ) ) != - 1 ) {
1688+ throw GetOsOrWinError ( PythonErrno . ENOENT , PythonExceptions . _OSError . ERROR_INVALID_NAME , path ) ;
1689+ }
1690+
1691+ bool existing = File . Exists ( path ) ; // will return false also on access denied
1692+ try {
1693+ File . Delete ( path ) ; // will throw an exception on access denied, no exception on file not existing
1694+ } catch ( Exception e ) {
1695+ throw ToPythonException ( e , path ) ;
1696+ }
1697+ if ( ! existing ) { // file was not existing in the first place
1698+ throw GetOsOrWinError ( PythonErrno . ENOENT , PythonExceptions . _OSError . ERROR_FILE_NOT_FOUND , path ) ;
1699+ }
16671700 }
16681701
16691702 [ Documentation ( "" ) ]
@@ -1685,24 +1718,6 @@ public static void unlink(CodeContext context, [NotNone] Bytes path, [ParamDicti
16851718 [ Documentation ( "" ) ]
16861719 public static void unlink ( CodeContext context , object ? path , [ ParamDictionary ] IDictionary < string , object > kwargs )
16871720 => unlink ( ConvertToFsString ( context , path , nameof ( path ) ) , kwargs ) ;
1688-
1689- private static void UnlinkWorker ( string path ) {
1690- if ( path == null ) {
1691- throw new ArgumentNullException ( nameof ( path ) ) ;
1692- } else if ( path . IndexOfAny ( Path . GetInvalidPathChars ( ) ) != - 1 || Path . GetFileName ( path ) . IndexOfAny ( Path . GetInvalidFileNameChars ( ) ) != - 1 ) {
1693- throw PythonOps . OSError ( PythonExceptions . _OSError . ERROR_INVALID_NAME , "The filename, directory name, or volume label syntax is incorrect" , path , PythonExceptions . _OSError . ERROR_INVALID_NAME ) ;
1694- }
1695-
1696- bool existing = File . Exists ( path ) ; // will return false also on access denied
1697- try {
1698- File . Delete ( path ) ; // will throw an exception on access denied, no exception on file not existing
1699- } catch ( Exception e ) {
1700- throw ToPythonException ( e , path ) ;
1701- }
1702- if ( ! existing ) { // file was not existing in the first place
1703- throw PythonOps . OSError ( PythonExceptions . _OSError . ERROR_FILE_NOT_FOUND , "The system cannot find the file specified" , path , PythonExceptions . _OSError . ERROR_FILE_NOT_FOUND ) ;
1704- }
1705- }
17061721#endif
17071722
17081723#if FEATURE_PROCESS
@@ -2127,15 +2142,13 @@ private static Exception ToPythonException(Exception e, string? filename = null)
21272142 message = e . Message ;
21282143 isWindowsError = true ;
21292144 } else if ( e is UnauthorizedAccessException unauth ) {
2130- return RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ?
2131- GetWin32Error ( PythonExceptions . _OSError . ERROR_ACCESS_DENIED , filename ) :
2132- GetOsError ( PythonErrno . EACCES , filename ) ;
2145+ return GetOsOrWinError ( PythonErrno . EACCES , PythonExceptions . _OSError . ERROR_ACCESS_DENIED , filename ) ;
21332146 } else {
21342147 var ioe = e as IOException ;
21352148 Exception ? pe = IOExceptionToPythonException ( ioe , error , filename ) ;
21362149 if ( pe != null ) return pe ;
21372150
2138- errorCode = System . Runtime . InteropServices . Marshal . GetHRForException ( e ) ;
2151+ errorCode = Marshal . GetHRForException ( e ) ;
21392152
21402153 if ( ( errorCode & ~ 0xfff ) == ( unchecked ( ( int ) 0x80070000 ) ) ) {
21412154 // Win32 HR, translate HR to Python error code if possible, otherwise
@@ -2310,19 +2323,21 @@ private static bool TryGetShellCommand(string command, [NotNullWhen(true)] out s
23102323
23112324#endif
23122325
2313- private static Exception DirectoryExistsError ( string ? filename ) {
2326+ private static Exception DirectoryExistsError ( string ? filename )
2327+ => GetOsOrWinError ( PythonErrno . EEXIST , PythonExceptions . _OSError . ERROR_ALREADY_EXISTS , filename ) ;
2328+
2329+ internal static Exception GetOsError ( int errno , string ? filename = null , string ? filename2 = null )
2330+ => PythonOps . OSError ( errno , strerror ( errno ) , filename , null , filename2 ) ;
2331+
2332+ internal static Exception GetOsOrWinError ( int errno , int winerror , string ? filename = null , string ? filename2 = null ) {
23142333#if FEATURE_NATIVE
23152334 if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ) {
2316- return GetWin32Error ( PythonExceptions . _OSError . ERROR_ALREADY_EXISTS , filename ) ;
2335+ return GetWin32Error ( winerror , filename , filename2 ) ;
23172336 }
23182337#endif
2319- return GetOsError ( PythonErrno . EEXIST , filename ) ;
2338+ return PythonOps . OSError ( errno , strerror ( errno ) , filename , null , filename2 ) ;
23202339 }
23212340
2322-
2323- internal static Exception GetOsError ( int errno , string ? filename = null , string ? filename2 = null )
2324- => PythonOps . OSError ( errno , strerror ( errno ) , filename , null , filename2 ) ;
2325-
23262341#if FEATURE_NATIVE || FEATURE_CTYPES
23272342
23282343 [ SupportedOSPlatform ( "windows" ) ]
0 commit comments