Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 111 additions & 45 deletions src/core/IronPython.Modules/nt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1533,9 +1533,14 @@ public PythonTuple __reduce__() {
}
}

private static readonly string[] ExecutableExtensions = [".com", ".exe", ".bat", ".cmd"]; // default PATHEXT order

[SupportedOSPlatform("windows")]
private static bool HasExecutableExtension(string extension) {
extension = extension.ToLower(CultureInfo.InvariantCulture);
return (extension == ".exe" || extension == ".dll" || extension == ".com" || extension == ".bat");
foreach (string e in ExecutableExtensions) {
if (extension.Equals(e, StringComparison.OrdinalIgnoreCase)) return true;
}
return false;
}

private const int OPEN_EXISTING = 3;
Expand All @@ -1554,6 +1559,7 @@ private static extern SafeFileHandle CreateFile(
int dwFlagsAndAttributes,
IntPtr hTemplateFile);

private const int FILE_TYPE_DISK = 0x0001;
private const int FILE_TYPE_CHAR = 0x0002;
private const int FILE_TYPE_PIPE = 0x0003;

Expand All @@ -1577,6 +1583,30 @@ private struct BY_HANDLE_FILE_INFORMATION {
public uint FileIndexLow;
}

private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);

[DllImport("kernel32.dll", EntryPoint = "FindFirstFileW", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
private static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);

[DllImport("kernel32.dll")]
private static extern bool FindClose(IntPtr hFindFile);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct WIN32_FIND_DATA {
public uint dwFileAttributes;
public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint dwReserved0;
public uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}

[Documentation("stat(path, *, dir_fd=None, follow_symlinks=True) -> stat_result\n\n" +
"Gathers statistics about the specified file or directory")]
[LightThrowing]
Expand Down Expand Up @@ -1610,33 +1640,7 @@ private static object statImpl(string path, bool follow_symlinks) {
VerifyPath(path, functionName: nameof(stat), argName: nameof(path));

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
try {
FileInfo fi = new FileInfo(path);
if (fi.Exists) {
// FileInfo trims trailing separators, so maybe it doesn't exist after all...
if (path.EndsWith(Path.DirectorySeparatorChar) || path.EndsWith(Path.AltDirectorySeparatorChar)) {
return LightExceptions.Throw(GetOsOrWinError(PythonErrno.ENOTDIR, PythonExceptions._OSError.ERROR_DIRECTORY, path));
}
return statWindowsImpl(fi);
}
DirectoryInfo di = new DirectoryInfo(path);
if (di.Exists) {
return statWindowsImpl(di);
}
using var handle = CreateFile(path, FILE_READ_ATTRIBUTES, 0, IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
if (handle.IsInvalid) {
return LightExceptions.Throw(GetWin32Error(Marshal.GetLastWin32Error(), path));
}
switch (GetFileType(handle)) {
case FILE_TYPE_CHAR: return new stat_result(0x2000);
case FILE_TYPE_PIPE: return new stat_result(0x1000);
}
return LightExceptions.Throw(GetOsOrWinError(PythonErrno.ENOENT, PythonExceptions._OSError.ERROR_PATH_NOT_FOUND, path));
} catch (ArgumentException) {
return LightExceptions.Throw(GetOsOrWinError(PythonErrno.ENOENT, PythonExceptions._OSError.ERROR_INVALID_NAME, path));
} catch (Exception e) {
return LightExceptions.Throw(ToPythonException(e, path));
}
return statWindows(path);
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
if (!follow_symlinks) return lstatUnix(path);
return statUnix(path);
Expand All @@ -1645,27 +1649,64 @@ private static object statImpl(string path, bool follow_symlinks) {
}
}

/// <summary>
/// Gets all the stat information from a single handle (like CPython's win32_xstat_slow_impl).
/// The information is about the path itself, reparse points are not followed (TODO).
/// </summary>
[SupportedOSPlatform("windows")]
private static stat_result statWindowsImpl(FileSystemInfo fsi) {
int mode = 0;
long size;
if (fsi is FileInfo fi) {
size = fi.Length;
mode = 0x8000;
if (HasExecutableExtension(fi.Extension)) {
mode |= S_IEXEC;
private static object statWindows(string path) {
using 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);
if (handle.IsInvalid) {
int error = Marshal.GetLastWin32Error();
if (error == PythonExceptions._OSError.ERROR_ACCESS_DENIED || error == PythonExceptions._OSError.ERROR_SHARING_VIOLATION) {
// the file exists but can't be opened, try reading the parent directory
IntPtr findHandle = FindFirstFile(path, out WIN32_FIND_DATA findData);
if (findHandle != INVALID_HANDLE_VALUE) {
FindClose(findHandle);
return MakeStatResult(findData.dwFileAttributes, findData.nFileSizeHigh, findData.nFileSizeLow, findData.ftLastAccessTime, findData.ftLastWriteTime, findData.ftCreationTime, fileIdx: 0);
}
}
} else {
Debug.Assert(fsi is DirectoryInfo);
size = 0;
mode = 0x4000 | S_IEXEC;
return LightExceptions.Throw(GetWin32Error(error, path));
}

mode |= S_IREAD;
if ((fsi.Attributes & FileAttributes.ReadOnly) == 0) {
mode |= S_IWRITE;
switch (GetFileType(handle)) {
case FILE_TYPE_DISK:
break;
case FILE_TYPE_CHAR:
return new stat_result(0x2000);
case FILE_TYPE_PIPE:
return new stat_result(0x1000);
default:
return LightExceptions.Throw(GetOsOrWinError(PythonErrno.ENOENT, PythonExceptions._OSError.ERROR_PATH_NOT_FOUND, path));
}

if (!GetFileInformationByHandle(handle, out BY_HANDLE_FILE_INFORMATION info)) {
return LightExceptions.Throw(GetWin32Error(Marshal.GetLastWin32Error(), path));
}

return MakeStatResult(info.FileAttributes, info.FileSizeHigh, info.FileSizeLow, info.LastAccessTime, info.LastWriteTime, info.CreationTime, ((ulong)info.FileIndexHigh << 32) + info.FileIndexLow);

stat_result MakeStatResult(uint attributes, uint sizeHigh, uint sizeLow, System.Runtime.InteropServices.ComTypes.FILETIME atime, System.Runtime.InteropServices.ComTypes.FILETIME mtime, System.Runtime.InteropServices.ComTypes.FILETIME ctime, ulong fileIdx) {
bool isDirectory = ((FileAttributes)attributes & FileAttributes.Directory) != 0;
return MakeWindowsStatResult(
(FileAttributes)attributes,
isDirectory ? null : Path.GetExtension(path),
isDirectory ? 0 : ((long)sizeHigh << 32) | sizeLow,
ToUnixTimeNs(atime),
ToUnixTimeNs(mtime),
ToUnixTimeNs(ctime),
fileIdx);
}

static long ToUnixTimeNs(System.Runtime.InteropServices.ComTypes.FILETIME ft) {
// FILETIME counts 100 nanosecond intervals since 1601-01-01 UTC
const long epochDifference = 116444736000000000;
return ((((long)ft.dwHighDateTime) << 32 | (uint)ft.dwLowDateTime) - epochDifference) * 100;
}
}

[SupportedOSPlatform("windows")]
private static stat_result statWindowsImpl(FileSystemInfo fsi) {
const long epochDifferenceLong = 62135596800 * TimeSpan.TicksPerSecond;

// 1 tick = 100 nanoseconds
Expand All @@ -1679,6 +1720,31 @@ private static stat_result statWindowsImpl(FileSystemInfo fsi) {
fileIdx = (((ulong)fileInfo.FileIndexHigh) << 32) + fileInfo.FileIndexLow;
}

if (fsi is FileInfo fi) {
return MakeWindowsStatResult(fsi.Attributes, fi.Extension, fi.Length, st_atime_ns, st_mtime_ns, st_ctime_ns, fileIdx);
}
Debug.Assert(fsi is DirectoryInfo);
return MakeWindowsStatResult(fsi.Attributes, null, 0, st_atime_ns, st_mtime_ns, st_ctime_ns, fileIdx);
}

/// <param name="extension">The file extension, or null for a directory.</param>
[SupportedOSPlatform("windows")]
private static stat_result MakeWindowsStatResult(FileAttributes attributes, string? extension, long size, long st_atime_ns, long st_mtime_ns, long st_ctime_ns, ulong fileIdx) {
int mode;
if (extension is null) {
mode = 0x4000 | S_IEXEC;
} else {
mode = 0x8000;
if (HasExecutableExtension(extension)) {
mode |= S_IEXEC;
}
}

mode |= S_IREAD;
if ((attributes & FileAttributes.ReadOnly) == 0) {
mode |= S_IWRITE;
}

return new stat_result(mode, fileIdx, size, st_atime_ns, st_mtime_ns, st_ctime_ns);
}

Expand Down Expand Up @@ -2454,7 +2520,7 @@ private static bool TryGetExecutableCommand(string command, out string baseComma

// TODO: need revisit
string sysdir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.System);
foreach (string suffix in new string[] { string.Empty, ".com", ".exe", "cmd", ".bat" }) {
foreach (string suffix in (string[])[string.Empty, ..ExecutableExtensions]) {
fullpath = Path.Combine(sysdir, baseCommand + suffix);
if (File.Exists(fullpath)) {
baseCommand = fullpath;
Expand Down
Loading