On 2025-02-25 Pali Rohár wrote:
> On Tuesday 25 February 2025 20:11:12 Lasse Collin wrote:
> > While \\?\...\Partition3 without trailing \ isn't a directory, I
> > more and more think that it doesn't matter too much if opendir
> > works on it still. It's so tiny bug that *it alone* isn't worth the
> > the extra code or the runtime overhead of GetFileAttributes. (With
> > stat(), this kind of cheating wouldn't be acceptable.)  
> 
> I'm really not sure. What I think is that readdir() and stat() should
> be in-sync and consistent. And ideally follows the Windows meaning.

I agree that ideally it should be so.

At the same time, it doesn't feel reasonable to add more code and
runtime overhead of GetFileAttributesW solely to reject this kind of
unusual paths. The error handling of FindFirstFileW won't be
meaningfully shorter, and there's a time-of-check to time-of-use race
condition between GetFileAttributesW and FindFirstFileW anyway.

It's not an easy decision, but I think the balance between pros and
cons is such that it's OK to make readdir too permissive with uncommon
paths like these. If you think this isn't OK, I will reconsider again,
but I would like to hear other people's opinions too.

> >   - I have started to think that EIO might be a bit harsh in
> >     _wopendir. I wonder if EINVAL would be better. MS CRTs seem to
> >     use EINVAL as a fallback value (or at least EINVAL is very
> >     common if one tries to do something weird). Another alternative
> >     is defaulting to ENOENT like the old dirent does on
> >     GetFileAttributes failure.
> 
> ENOENT should be returned only in the case when the path really does
> not exist. Not for generic / unknown readdir errors.

I agree now. I changed the unknown errors of GetFullPathNameW and
FindFirstFileW in _wopendir from EIO to EINVAL.

I noticed that ERROR_INVALID_REPARSE_DATA is possible if there is a
symlink that points to nul:

    mklink /d dir-link-to-nul \\?\nul

I guess it's OK to leave that as EINVAL, although now I wonder if it
should actually be ENOENT.

A symlink to \\?\notfound results in ERROR_CANT_RESOLVE_FILENAME and
thus ELOOP. ELOOP is wrong in this case but perhaps it's acceptable. If
it was ENOENT then symlink loops would be ENOENT as well. I'm not sure
which compromise is better. UCRT's _stat uses ENOENT for both
ERROR_CANT_RESOLVE_FILENAME and ERROR_INVALID_REPARSE_DATA.

These aren't common errors so the choice of errno isn't super important
as long as it's not too misleading.

> If readdir returns ENOENT then caller should use mkdir to create the
> non-existent path.

On POSIX, ENOENT occurs if a symlink exists but its target doesn't. So
mkdir can fail even if open, opendir, or stat results in ENOENT.

For \\?\ and \??\ paths, / isn't a directory separator:

   /* Create the search expression:
-   * Add on a slash if the path does not end with one. */
+   * Add on a backslash if the path does not end with a directory separator.
+   * With \\?\ and \??\ paths, '/' isn't a directory separator. */
   if (full_path_len > 0 &&
-      dirp->dd_name[full_path_len - 1] != L'/' &&
+      (dirp->dd_name[full_path_len - 1] != L'/' || !normalize_path) &&
       dirp->dd_name[full_path_len - 1] != L'\\')
     {
       dirp->dd_name[full_path_len++] = L'\\';

I also added a comment why \\?\C: is accepted even though it's not a
directory.

I attached new draft patches. I already put you in Reviewed-by so that
I won't forget it, but obvious I won't proceed with that trailer if you
don't approve. :-)

-- 
Lasse Collin
From 87cee327e99b7993460de6eb07da1f993fbbd0de Mon Sep 17 00:00:00 2001
From: Lasse Collin <lasse.col...@tukaani.org>
Date: Fri, 28 Feb 2025 20:14:36 +0200
Subject: [PATCH 1/2] crt: dirent: Update the disclaimer header and clean up
 white space

Signed-off-by: Lasse Collin <lasse.col...@tukaani.org>
---
 mingw-w64-crt/misc/dirent.c    | 7 +++----
 mingw-w64-headers/crt/dirent.h | 5 ++---
 2 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/mingw-w64-crt/misc/dirent.c b/mingw-w64-crt/misc/dirent.c
index 4897cf492..cf95f9091 100644
--- a/mingw-w64-crt/misc/dirent.c
+++ b/mingw-w64-crt/misc/dirent.c
@@ -1,8 +1,8 @@
 /*
  * dirent.c
  * This file has no copyright assigned and is placed in the Public Domain.
- * This file is part of the mingw-runtime package.
- * No warranty is given; refer to the file DISCLAIMER within the package.
+ * This file is part of the mingw-w64 runtime package.
+ * No warranty is given; refer to the file DISCLAIMER.PD within this package.
  *
  * Derived from DIRLIB.C by Matt J. Weinstein
  * This note appears in the DIRLIB.H
@@ -11,7 +11,7 @@
  * Updated by Jeremy Bettis <jer...@hksys.com>
  * Significantly revised and rewinddir, seekdir and telldir added by Colin
  * Peters <co...@fu.is.saga-u.ac.jp>
- *     
+ *
  */
 
 #ifndef WIN32_LEAN_AND_MEAN
@@ -322,4 +322,3 @@ _tseekdir (_TDIR * dirp, long lPos)
        ;
     }
 }
-
diff --git a/mingw-w64-headers/crt/dirent.h b/mingw-w64-headers/crt/dirent.h
index 2d7a1b73f..421b09278 100644
--- a/mingw-w64-headers/crt/dirent.h
+++ b/mingw-w64-headers/crt/dirent.h
@@ -1,8 +1,8 @@
 /*
  * DIRENT.H (formerly DIRLIB.H)
  * This file has no copyright assigned and is placed in the Public Domain.
- * This file is part of the mingw-runtime package.
- * No warranty is given; refer to the file DISCLAIMER within the package.
+ * This file is part of the mingw-w64 runtime package.
+ * No warranty is given; refer to the file DISCLAIMER.PD within this package.
  *
  */
 
@@ -124,4 +124,3 @@ void __cdecl __MINGW_NOTHROW _wseekdir (_WDIR*, long);
 #endif /* Not RC_INVOKED */
 
 #endif /* Not _DIRENT_H_ */
-
-- 
2.48.1

From ff8e30a5a6d2012c661c159c3dda5a43d58a01e6 Mon Sep 17 00:00:00 2001
From: Lasse Collin <lasse.col...@tukaani.org>
Date: Fri, 28 Feb 2025 20:14:36 +0200
Subject: [PATCH 2/2] crt: dirent: Revise thoroughly (ABI break)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Switch from _findfirst/_wfindfirst to FindFirstFileW:

  - _findfirst doesn't support long UTF-8 filenames that don't fit
    into MAX_PATH bytes. With MSVCRT, it fails with with EINVAL.
    With UCRT, long filenames make the application crash.

  - FindFirstFileA has the same limit of MAX_PATH bytes. It fails
    with ERROR_MORE_DATA on longer filenames.

  - FindFirstFileA and _findfirst (both MSVCRT and UCRT) use best-fit
    mapping. Malicious filenames might result in a directory traversal
    attack.[1] For example, U+2215 becomes ASCII '/' when best-fit
    converted to Windows-1252.

  - FindFirstFileW seems to be faster than UCRT's _findfirst or
    _wfindfirst.

  - FindFirstFileW isn't supported on Windows 95/98/ME, and thus
    the new dirent code doesn't support those Windows versions.

To avoid best-fit mapping, make readdir convert from wide char to
the correct code page. If a lossless conversion isn't possible, the
entry is skipped. When the end of the directory is reached, readdir
fails with errno = EILSEQ if any entries were skipped. The delaying
of EILSEQ allows applications to still see all other filenames in
the directory.

Add _readdir_8dot3 which tries to use a 8.3 name to avoid EILSEQ.

Delete wdirent.c, and unify DIR and _WDIR. Calls to narrow and wide
functions on the same DIR can now be mixed.

Make dirent functions usable in applications that have been marked
as long path aware in their application manifests (no MAX_PATH limit).

In struct dirent, increase the size of d_name to support long UTF-8
filenames. FindFirstFileW supports filenames up to MAX_PATH (260)
wide chars (UTF-16 code units) including the terminating \0. One
UTF-16 code unit may require up to three bytes in UTF-8, thus
(MAX_PATH - 1) * 3 + 1 = 778 bytes is enough space for any UTF-8
filename including the terminating \0. (Four-byte UTF-8 chars consume
two UTF-16 code units, so they cannot result in a longer string.)
No Windows locale supports a code page with longer encodings,
for example, GB18030 cannot be used as a locale code page.

Both struct dirent and struct _wdirent:

  - Change the type of d_ino from "long" to "unsigned short" because
    ino_t is unsigned short. d_ino is still always 0.

  - Add d_type. Applications that support it can be faster because they
    don't need to stat() each entry to know the type. Together with the
    switch to FindFirstFileW, the time required to scan a directory tree
    may reduce over 80 % (when data from disk is already cached in RAM).

  - Add d_8dot3. d_8dot3 is normally 0 but it is set to 1 by
    _readdir_8dot3 if a 8.3 filename was used in d_name.

  - Set d_reclen to the size of the structure. Previously it was
    always 0.

  - The size reduction of d_ino matches the amount of space needed by
    d_type and d_8dot3, thus the offsets of the old members don't
    change. This might reduce ABI incompatiblity risks in case a pointer
    to struct dirent is passed between components that were built
    against different dirent versions. d_ino has always been 0, so
    if the old struct dirent is interpret by programs built against
    the new code, they will see d_type == DT_UNKNOWN and d_8dot3 == 0.

telldir and seekdir:

  - Make seekdir rewind only when actually needed.

  - Delay the forward seeking until readdir is called. This simplifies
    error handling. Previously seekdir called readdir in a loop.
    If there was an error, it could be detected by checking errno
    after seekdir returned. seekdir is a void function, and portable
    programs won't detect errors from it. When readdir does the forward
    seeking, the seeking errors are reported by readdir.

  - Once readdir returns NULL, make telldir return the position where
    readdir stopped instead of -1. This matches Linux at least in the
    normal end-of-dir situation.

Error handling:

  - Never explictly set errno = 0. The dirent functions are defined
    in POSIX, and no POSIX function shall set errno to zero.[2] To
    distinguish errors from a successful end of the directory, one
    must set errno = 0 before calling readdir().

  - Don't set errno = ENOENT if the directory is completely empty and
    doesn't have even the "." and ".." entries. It may happen when
    reading a root directory of an empty drive.

  - Detect lack of access permissions at opendir (EACCES) instead of
    failing on the first readdir call with EINVAL.

  - Detect dangling directory symlinks at opendir instead of failing
    on the first readdir call. (ENOENT)

  - Diagnose empty pathname argument in opendir with ENOENT as required
    by POSIX (it was ENOTDIR).

Define the DIR structure in dirent.c instead of dirent.h. The header
only provides an opaque typedef.

Don't include <io.h> in dirent.h.

Add API documentation to dirent.h. It mostly describes the details
that are specific to this implementation.

[1] 
https://devco.re/blog/2025/01/09/worstfit-unveiling-hidden-transformers-in-windows-ansi/

[2] "No function in this volume of POSIX.1-2024 shall set errno to zero."
    
https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_03

Reviewed-by: Pali Rohár <pali.ro...@gmail.com>
Signed-off-by: Lasse Collin <lasse.col...@tukaani.org>
---
 mingw-w64-crt/Makefile.am      |   2 +-
 mingw-w64-crt/misc/dirent.c    | 865 ++++++++++++++++++++++++++-------
 mingw-w64-crt/misc/wdirent.c   |   5 -
 mingw-w64-headers/crt/dirent.h | 249 +++++++---
 4 files changed, 866 insertions(+), 255 deletions(-)
 delete mode 100644 mingw-w64-crt/misc/wdirent.c

diff --git a/mingw-w64-crt/Makefile.am b/mingw-w64-crt/Makefile.am
index 423e233ac..479823218 100644
--- a/mingw-w64-crt/Makefile.am
+++ b/mingw-w64-crt/Makefile.am
@@ -986,7 +986,7 @@ src_libmingwex=\
   misc/tsearch.c         misc/twalk.c           \
   misc/wcstof.c \
   misc/wcstold.c \
-  misc/wdirent.c         misc/winbs_uint64.c        misc/winbs_ulong.c      
misc/winbs_ushort.c    \
+  misc/winbs_uint64.c        misc/winbs_ulong.c      misc/winbs_ushort.c    \
   misc/wmemchr.c         misc/wmemcmp.c             misc/wmemcpy.c          
misc/wmemmove.c              misc/wmempcpy.c        \
   misc/wmemset.c         misc/ftw.c                 misc/ftw64.c            
misc/mingw-access.c          \
   \
diff --git a/mingw-w64-crt/misc/dirent.c b/mingw-w64-crt/misc/dirent.c
index cf95f9091..06d28160c 100644
--- a/mingw-w64-crt/misc/dirent.c
+++ b/mingw-w64-crt/misc/dirent.c
@@ -12,6 +12,18 @@
  * Significantly revised and rewinddir, seekdir and telldir added by Colin
  * Peters <co...@fu.is.saga-u.ac.jp>
  *
+ * Revised by Lasse Collin <lasse.col...@tukaani.org> and
+ * Pali Rohár <pali.ro...@gmail.com> in 2025:
+ *   - unified DIR and _WDIR
+ *   - long path aware (no MAX_PATH limit)
+ *   - readdir supports long UTF-8 filenames (up to 778 bytes including \0)
+ *   - readdir avoids best-fit mapping with delayed EILSEQ at the end of the
+ *     dir to prevent directory traversal attacks from malicious filenames
+ *   - _readdir_8dot3 tries to fall back to 8.3 names instead of EILSEQ
+ *   - added d_type to struct dirent and struct _wdirent
+ *   - error handling changes
+ *   - added API docs into dirent.h
+ *   - Windows 95/98/ME are no longer supported
  */
 
 #ifndef WIN32_LEAN_AND_MEAN
@@ -21,199 +33,704 @@
 #include <stdlib.h>
 #include <errno.h>
 #include <string.h>
-#include <io.h>
-#include <direct.h>
+#include <locale.h>
+#include <limits.h>
 #include <dirent.h>
+#include <windows.h>
 
-#include <windows.h> /* for GetFileAttributes */
 
-#include <tchar.h>
-#define SUFFIX _T("*")
-#define        SLASH   _T("\\")
+/* Maximum number of wide characters allowed in a pathname including
+ * the terminating \0. This is the limit for paths that begin with \\?\
+ * and for all paths in long path aware applications.
+ * GetFullPathNameW which rejects input strings that exceed this size. */
+#define DIRENT_WPATH_MAX 32767
+
+
+struct __dirent_DIR
+{
+       /*
+        * Status of search:
+        *   0 = rewinddir or seekdir has been called, and prepare_next_entry
+        *       needs to call FindFirstFileW.
+        *   1 = The entry from FindFirstFileW is in dd_wfd. This is the
+        *       status after opendir or _wopendir if the directory contains
+        *       at least one entry.
+        *  >1 = FindNextFileW has been called (dd_stat - 1) times.
+        *  -1 = The end of the directory was reached or an error occurred.
+        */
+       long                    dd_stat;
+
+       /*
+        * Position in the directory as seen by the application.
+        * This is 0 after opendir, _wopendir, and rewinddir.
+        * readdir (via prepare_next_entry) increments this.
+        * seekdir sets this to an application-provided non-negative value.
+        */
+       long                    dd_pos;
+
+       /*
+        * prepare_next_entry returns this when the end of the directory
+        * is reached. This is -1 when there are no delayed errors. If
+        * a character set conversion problem occurs, this is set to EILSEQ.
+        */
+       int                     dd_endval;
+
+       /* Handle from FindFirstFileW */
+       HANDLE                  dd_handle;
+
+       /* Result from FindFirstFileW or FindNextFileW */
+       WIN32_FIND_DATAW        dd_wfd;
+
+       /* dirent struct to return from readdir (NOTE: this makes this thread
+        * safe as long as only one thread uses a particular DIR struct at
+        * a time) */
+       union
+       {
+               struct dirent           a;
+               struct _wdirent         w;
+       } dd_entry;
+
+       /* given path for dir with search pattern (struct is extended) */
+       wchar_t                 dd_name[1];
+};
 
 
 /*
- * opendir
+ * Get the code page used for filenames.
  *
- * Returns a pointer to a DIR structure appropriately filled in to begin
- * searching a directory.
+ * If locale is UTF-8, the UCRT functions (fopen, _open, ...) use UTF-8
+ * filenames even if CP_ACP isn't UTF-8 or if SetFileApisToOEM has been
+ * called. That is, it's possible that UCRT functions and Win32 API ANSI
+ * functions use a different encoding for filenames. This can only
+ * happen with UTF-8 locales.
+ *
+ * If UTF-8 is set via an application manifest, then CP_ACP and CP_OEMCP
+ * are UTF-8, and both UCRT and Win32 API always use UTF-8 for filenames.
+ * This is true even if one calls setlocale to set a non-UTF-8 locale or
+ * if setlocale isn't called at all.
+ *
+ * MSVCRT doesn't support UTF-8 locales, and this cannot return CP_UTF8.
  */
-_TDIR *
-_topendir (const _TCHAR *szPath)
+static unsigned int
+get_code_page (void)
 {
-  _TDIR *nd;
-  unsigned int rc;
-  _TCHAR szFullPath[MAX_PATH];
+  return (___lc_codepage_func () == CP_UTF8)
+        ? CP_UTF8
+        : AreFileApisANSI () ? CP_ACP : CP_OEMCP;
+}
 
-  errno = 0;
 
-  if (!szPath)
+DIR *
+_wopendir (const wchar_t *path)
+{
+  BOOL normalize_path;
+  DWORD full_path_alloc_size;
+  DWORD full_path_len;
+  int err;
+  DIR *dirp;
+
+  if (!path)
     {
       errno = EFAULT;
-      return (_TDIR *) 0;
+      return NULL;
     }
 
-  if (szPath[0] == _T('\0'))
+  /* POSIX specifies that an empty string results in ENOENT. */
+  if (path[0] == L'\0')
     {
-      errno = ENOTDIR;
-      return (_TDIR *) 0;
+      errno = ENOENT;
+      return NULL;
     }
 
-  /* Attempt to determine if the given path really is a directory. */
-  rc = GetFileAttributes (szPath);
-  if (rc == INVALID_FILE_ATTRIBUTES)
+  /*
+   * We need an absolute pathname so that things keep working even if
+   * the current directory is changed.
+   *
+   * If the pathname begins with "\\?\" or "\??\" it is used as is.
+   * Otherwise GetFullPathNameW is used, including variations with
+   * forward slashes like "//?/" or "/\?\".
+   */
+  if (path[0] == '\\' &&
+      (path[1] == '\\' || path[1] == '?') &&
+      path[2] == '?' &&
+      path[3] == '\\')
     {
-      /* call GetLastError for more error info */
-      errno = ENOENT;
-      return (_TDIR *) 0;
+      normalize_path = FALSE;
+      full_path_alloc_size = wcslen (path) + 1;
+      if (full_path_alloc_size > DIRENT_WPATH_MAX)
+       {
+         errno = ENAMETOOLONG;
+         return NULL;
+       }
     }
-  if (!(rc & FILE_ATTRIBUTE_DIRECTORY))
+  else
     {
-      /* Error, entry exists but not a directory. */
-      errno = ENOTDIR;
-      return (_TDIR *) 0;
+      /* When buffer is too small or not provided, the return value of
+       * GetFullPathNameW includes the space needed for the terminating \0. */
+      normalize_path = TRUE;
+      full_path_alloc_size = GetFullPathNameW (path, 0, NULL, NULL);
+      if (full_path_alloc_size == 0)
+       {
+         switch (GetLastError ())
+           {
+             case ERROR_CALL_NOT_IMPLEMENTED:
+               /* Windows 95/98/ME is not supported by this dirent code. */
+               errno = ENOSYS;
+               return NULL;
+
+             case ERROR_FILENAME_EXCED_RANGE:
+               /* See the comment of DIRENT_WPATH_MAX in this file. */
+               errno = ENAMETOOLONG;
+               return NULL;
+
+             default:
+               /* Unknown error. GetFullPathNameW accepts various invalid
+                * inputs so this error should be uncommon. */
+               errno = EINVAL;
+               return NULL;
+           }
+       }
     }
 
-  /* Make an absolute pathname.  */
-  _tfullpath (szFullPath, szPath, MAX_PATH);
+  /* Allocate enough space to store DIR structure, the full path, and
+   * the two characters "\*" that will be appended. full_path_alloc_size
+   * already includes the space needed for the terminating \0. */
+  dirp = malloc (sizeof (DIR) + (full_path_alloc_size + 2) * sizeof (wchar_t));
+  if (!dirp)
+    {
+      errno = ENOMEM;
+      return NULL;
+    }
 
-  /* Allocate enough space to store DIR structure and the complete
-   * directory path given. */
-  nd = (_TDIR *) malloc (sizeof (_TDIR) + (_tcslen (szFullPath)
-                                          + _tcslen (SLASH)
-                                          + _tcslen (SUFFIX) + 1)
-                                         * sizeof (_TCHAR));
+  if (normalize_path)
+    {
+      /* On success, the return value from GetFullPathNameW does not include
+       * the terminating \0. */
+      full_path_len = GetFullPathNameW (path, full_path_alloc_size,
+                                       dirp->dd_name, NULL);
+      if (full_path_len >= full_path_alloc_size)
+       {
+         free (dirp);
+         errno = EINVAL;
+         return NULL;
+       }
+    }
+  else
+    {
+      /* Copy the \\?\ or \??\ pathname as is. */
+      memcpy (dirp->dd_name, path, full_path_alloc_size * sizeof (wchar_t));
+      full_path_len = full_path_alloc_size - 1;
+    }
 
-  if (!nd)
+  /* Create the search expression:
+   * Add on a backslash if the path does not end with a directory separator.
+   * With \\?\ and \??\ paths, '/' isn't a directory separator. */
+  if (full_path_len > 0 &&
+      (dirp->dd_name[full_path_len - 1] != L'/' || !normalize_path) &&
+      dirp->dd_name[full_path_len - 1] != L'\\')
     {
-      /* Error, out of memory. */
-      errno = ENOMEM;
-      return (_TDIR *) 0;
+      dirp->dd_name[full_path_len++] = L'\\';
     }
 
-  /* Create the search expression. */
-  _tcscpy (nd->dd_name, szFullPath);
+  /* Add on the search pattern. */
+  dirp->dd_name[full_path_len++] = L'*';
+  dirp->dd_name[full_path_len] = L'\0';
+
+  /* dd_name has now been initialized. dd_entry is initialized in every
+   * readdir call so it's not initialized here.
+   *
+   * Initialize the status to indicate that FindFirstFileW has been called
+   * to read the first entry into dd_wfd (it will be done a few lines later).
+   *
+   * Initialize the readdir/telldir position to be at the beginning of
+   * the directory (0).
+   *
+   * There is no delayed error to be returned at the end of the directory. */
+  dirp->dd_stat = 1;
+  dirp->dd_pos = 0;
+  dirp->dd_endval = -1;
+
+  /* Initialize dd_handle and dd_wfd. The FindFirstFileW call cannot be
+   * delayed until readdir because that would result in worse error detection.
+   * Specifically, it's not enough to call GetFileAttributesW here because it
+   * cannot detect EACCES or dangling directory symlinks.
+   *
+   * NOTE: \\?\C:\ is a directory but \\?\C: isn't. We incorrectly accept the
+   * latter as a directory still because appending \* produces \\?\C:\*.
+   * GetFileAttributesW would detect paths that aren't directories when the
+   * trailing \ is missing, but the overhead of GetFileAttributesW isn't worth
+   * it if the only benefit is being pedantic about this kind of paths. */
+  err = 0;
+  dirp->dd_handle = FindFirstFileW (dirp->dd_name, &dirp->dd_wfd);
+  if (dirp->dd_handle == INVALID_HANDLE_VALUE)
+    {
+      switch (GetLastError ())
+       {
+         case ERROR_FILE_NOT_FOUND:
+           /* It's a completely empty directory, for example, the root
+            * directory of an empty drive. This isn't an error.
+            * Mark that there are no more filenames to be read. */
+           dirp->dd_stat = -1;
+           break;
+
+         case ERROR_PATH_NOT_FOUND:
+         case ERROR_INVALID_NAME:
+         case ERROR_BAD_PATHNAME:
+         case ERROR_BAD_NETPATH:
+         case ERROR_BAD_NET_NAME:
+         case ERROR_CANT_ACCESS_FILE:
+           /* In addition to the obvious reason, ERROR_PATH_NOT_FOUND
+            * may occur also if the search pattern is too long:
+            * 32767 wide chars including the \0 for a long path aware app,
+            * or 260 (MAX_PATH) including the \0 if the app isn't
+            * long path aware.
+            *
+            * ERROR_INVALID_NAME occurs with C:\:\* and such invalid inputs.
+            * GetFullPathNameW does accept invalid paths so this error is
+            * indeed possible.
+            *
+            * ERROR_BAD_PATHNAME occurs if \\* is passed to FindFirstFileW.
+            *
+            * ERROR_BAD_NETPATH occurs if the server name cannot be resolved
+            * or if the server doesn't support file sharing.
+            *
+            * ERROR_BAD_NET_NAME occurs if the server can be contacted but
+            * the share doesn't exist.
+            *
+            * ERROR_CANT_ACCESS_FILE occurs with directories that have
+            * an unhandled reparse point tag. Treat them the same way as
+            * directory symlinks and junctions whose targets don't exist. */
+           err = ENOENT;
+           break;
+
+         case ERROR_DIRECTORY:
+         case ERROR_INVALID_FUNCTION:
+         case ERROR_NOT_FOUND:
+           /* Detecting non-directories only works with the last pathname
+            * component. For example, if there is a file foo, passing
+            * foo\* to FindFirstFileW will fail with ERROR_DIRECTORY.
+            * foo\bar\* should be ENOTDIR too but it becomes ENOENT
+            * because FindFirstFileW fails with ERROR_PATH_NOT_FOUND.
+            *
+            * ERROR_INVALID_FUNCTION happens at least with the device
+            * namespace. _wopendir(L"nul") makes GetFullPathNameW produce
+            * \\.\nul, and then FindFirstFileW is called with \\.\nul\*
+            * which results in ERROR_INVALID_FUNCTION.
+            *
+            * _wopendir(L"con") behaves like nul except that \\.\con\*
+            * results in ERROR_NOT_FOUND */
+           err = ENOTDIR;
+           break;
+
+         case ERROR_CANT_RESOLVE_FILENAME:
+           /* A symlink loop produces this error. However, it also occurs
+            * with directory symlinks that have an unusual non-existing
+            * target like \\?\notfound. */
+           err = ELOOP;
+           break;
+
+         case ERROR_ACCESS_DENIED:
+           err = EACCES;
+           break;
+
+         case ERROR_FILENAME_EXCED_RANGE:
+           /* Unsure if this error code can happen from FindFirstFileW
+            * but handle it just in case to avoid the default case. */
+           err = ENAMETOOLONG;
+           break;
+
+         case ERROR_NOT_ENOUGH_MEMORY:
+           err = ENOMEM;
+           break;
+
+         /*
+          * NOTE: ERROR_MORE_DATA is possible with FindFirstFileA if
+          * it encounters a filename that exceeds MAX_PATH bytes.
+          * That error shouldn't be possible with FindFirstFileW.
+          */
+
+         case ERROR_CALL_NOT_IMPLEMENTED:
+           /* We might get here if GetFullPathNameW wasn't called. */
+           err = ENOSYS;
+           break;
+
+         default:
+           /* Unknown error. */
+           err = EINVAL;
+           break;
+       }
+    }
 
-  /* Add on a slash if the path does not end with one. */
-  if (nd->dd_name[0] != _T('\0') &&
-      nd->dd_name[_tcslen (nd->dd_name) - 1] != _T('/') &&
-      nd->dd_name[_tcslen (nd->dd_name) - 1] != _T('\\'))
+  if (err != 0)
     {
-      _tcscat (nd->dd_name, SLASH);
+      free (dirp);
+      errno = err;
+      return NULL;
     }
 
-  /* Add on the search pattern */
-  _tcscat (nd->dd_name, SUFFIX);
+  return dirp;
+}
 
-  /* Initialize handle to -1 so that a premature closedir doesn't try
-   * to call _findclose on it. */
-  nd->dd_handle = -1;
 
-  /* Initialize the status. */
-  nd->dd_stat = 0;
+DIR *
+opendir (const char *path)
+{
+  unsigned int cp = get_code_page ();
+  wchar_t *wpath;
+  int wpath_size;
+  DIR *dirp;
 
-  /* Initialize the dirent structure. ino and reclen are invalid under
-   * Win32, and name simply points at the appropriate part of the
-   * findfirst_t structure. */
-  nd->dd_dir.d_ino = 0;
-  nd->dd_dir.d_reclen = 0;
-  nd->dd_dir.d_namlen = 0;
-  memset (nd->dd_dir.d_name, 0, 260 * sizeof(nd->dd_dir.d_name[0])  
/*FILENAME_MAX*/);
+  if (!path)
+    {
+      errno = EFAULT;
+      return NULL;
+    }
 
-  return nd;
+  /* Convert path to wide char. Use MB_ERR_INVALID_CHARS because UCRT's
+   * fopen, _open, _mkdir, ... reject invalid multibyte strings too.
+   * Compare to Win32 API's FindFirstFileA which apparently converts
+   * the argument without this flag and then accesses the resulting file
+   * or directory. */
+  wpath_size = MultiByteToWideChar (cp, MB_ERR_INVALID_CHARS,
+                                   path, -1, NULL, 0);
+  if (wpath_size <= 0)
+    {
+      errno = EILSEQ;
+      return NULL;
+    }
+
+  /* Don't try to allocate memory if the pathname is so long that Windows
+   * would reject it anyway. */
+  if (wpath_size > DIRENT_WPATH_MAX)
+    {
+      errno = ENAMETOOLONG;
+      return NULL;
+    }
+
+  wpath = malloc ((size_t) wpath_size * sizeof (wchar_t));
+  if (!wpath)
+    {
+      errno = ENOMEM;
+      return NULL;
+    }
+
+  if (MultiByteToWideChar (cp, MB_ERR_INVALID_CHARS,
+                          path, -1, wpath, wpath_size) != wpath_size)
+    {
+      free (wpath);
+      errno = EILSEQ;
+      return NULL;
+    }
+
+  /* Open the directory using the wide char path. */
+  dirp = _wopendir (wpath);
+
+  /* wpath isn't needed anymore but we need to remember the errno
+   * in case _wopendir failed. */
+  {
+    int saved_errno = errno;
+    free (wpath);
+    errno = saved_errno;
+  }
+
+  return dirp;
 }
 
 
-/*
- * readdir
- *
- * Return a pointer to a dirent structure filled with the information on the
- * next entry in the directory.
- */
-struct _tdirent *
-_treaddir (_TDIR * dirp)
+/* Convert attributes from WIN32_FIND_DATAW to d_type. */
+static unsigned char
+get_d_type (DWORD attrs, DWORD reparse_tag)
 {
-  errno = 0;
+  /* Check for a reparse point before checking if it is a directory.
+   * A reparse point can also have the directory attribute. This
+   * includes "directory symlinks" too. */
+  if (attrs & FILE_ATTRIBUTE_REPARSE_POINT)
+    {
+      switch (reparse_tag)
+       {
+         case IO_REPARSE_TAG_SYMLINK:
+           return DT_LNK;
+
+         case IO_REPARSE_TAG_AF_UNIX:
+           return DT_SOCK;
+
+         default:
+           return DT_UNKNOWN;
+       }
+    }
 
-  /* Check for valid DIR struct. */
+  return (attrs & FILE_ATTRIBUTE_DIRECTORY) ? DT_DIR : DT_REG;
+}
+
+
+/* Prepare dirp->dd_wfd and the common members of dirp->dd_entry. Return 0 on
+ * success, -1 on end of directory, and >0 (an errno value) on error. */
+static int
+prepare_next_entry (DIR *dirp)
+{
   if (!dirp)
     {
-      errno = EFAULT;
-      return (struct _tdirent *) 0;
+      return EFAULT;
     }
 
   if (dirp->dd_stat < 0)
     {
-      /* We have already returned all files in the directory
-       * (or the structure has an invalid dd_stat). */
-      return (struct _tdirent *) 0;
+      /* We have already successfully returned all files in the directory or
+       * an error has occurred. If there was an error, it has alredy been
+       * returned. Don't return it again. This ensures that an application
+       * cannot end up in an infite loop in case it tries to continue
+       * reading after an error. */
+      return -1;
     }
-  else if (dirp->dd_stat == 0)
+
+  /* After rewinddir or seekdir, we might need to restart from the beginning
+   * of the directory. */
+  if (dirp->dd_stat == 0)
     {
-      /* We haven't started the search yet. */
-      /* Start the search */
-      dirp->dd_handle = _tfindfirst (dirp->dd_name, &(dirp->dd_dta));
+      if (dirp->dd_handle != INVALID_HANDLE_VALUE)
+       {
+         (void) FindClose (dirp->dd_handle);
+       }
 
-      if (dirp->dd_handle == -1)
+      dirp->dd_handle = FindFirstFileW (dirp->dd_name, &dirp->dd_wfd);
+      if (dirp->dd_handle == INVALID_HANDLE_VALUE)
        {
-         /* Whoops! Seems there are no files in that
-          * directory. */
+         /* It's an empty directory or an error occurred. Make telldir
+          * return the position (0) where the end of directory or the
+          * error occurred. Update dd_stat to indicate the end of the
+          * directory or an error status. */
+         dirp->dd_pos = 0;
          dirp->dd_stat = -1;
+
+         switch (GetLastError ())
+           {
+             case ERROR_FILE_NOT_FOUND:
+               /* It's a completely empty directory, for example, the root
+                * directory of an empty drive. Return dd_endval because
+                * seekdir doesn't reset it to -1. */
+               return dirp->dd_endval;
+
+             case ERROR_NOT_ENOUGH_MEMORY:
+               return ENOMEM;
+
+             default:
+               /* On POSIX systems, rewinddir or seekdir shouldn't fail
+                * since they can keep the directory open. On Windows, we
+                * have to restart the directory reading, and if the
+                * directory has been renamed, removed, or permissions
+                * changed, this can fail in various ways. Treat all these
+                * errors as an invalid position of the directory stream. */
+               return ENOENT;
+           }
        }
-      else
+
+      /* The first entry has been read into dd_wfd. */
+      dirp->dd_stat = 1;
+    }
+
+  if (dirp->dd_pos == LONG_MAX)
+    {
+      /* Too many entries. Treat it as invalid directory stream position.
+       * It's highly unlikely that there are this many files in a directory,
+       * so adding code to handle larger directories seems pointless. */
+      dirp->dd_stat = -1;
+      return ENOENT;
+    }
+
+  /* Increment the target position. */
+  dirp->dd_pos++;
+
+  /* Read the next entry into dd_wfd if required. Right after _wopendir we
+   * already have the first entry in dd_wfd and the loop doesn't run at all
+   * (dd_stat == 1 && dd_pos == 1). If we are seeking forward due to a seekdir
+   * call, we might read multiple entries to reach the target position. */
+  while (dirp->dd_stat < dirp->dd_pos)
+    {
+      if (FindNextFileW (dirp->dd_handle, &dirp->dd_wfd) == 0)
        {
-         dirp->dd_stat = 1;
+         /* The end of the directory was reached or an error occurred.
+          * Make telldir return this position. Then update dd_stat to
+          * indicate the end of the directory or an error status. */
+         dirp->dd_pos = dirp->dd_stat;
+         dirp->dd_stat = -1;
+
+         switch (GetLastError ())
+           {
+             case ERROR_NO_MORE_FILES:
+               return dirp->dd_endval;
+
+             case ERROR_NOT_ENOUGH_MEMORY:
+               return ENOMEM;
+
+             /* NOTE: ERROR_MORE_DATA is possible with FindNextFileA if
+              * it encounters a filename that exceeds MAX_PATH bytes.
+              * That error shouldn't be possible with FindNextFileW. */
+
+             default:
+               /* Unknown error. Use EIO instead of EINVAL because the
+                * pathname was valid when FindFirstFileW was called. */
+               return EIO;
+           }
        }
+
+      /* Because dd_stat < dd_pos at the beginning of the loop,
+       * this cannot overflow. The loop runs more than once only
+       * when seekdir has been used. */
+      dirp->dd_stat++;
     }
-  else
+
+  /* These members are shared between the narrow and wide structs. */
+  dirp->dd_entry.w.d_ino = 0;
+  dirp->dd_entry.w.d_type = get_d_type (dirp->dd_wfd.dwFileAttributes,
+                                       dirp->dd_wfd.dwReserved0);
+  dirp->dd_entry.w.d_8dot3 = 0;
+
+  return 0;
+}
+
+
+struct _wdirent *
+_wreaddir (DIR *dirp)
+{
+  /* errno must be preserved when the end of the directory is
+   * successfully reached and we return NULL. */
+  int saved_errno = errno;
+
+  int err = prepare_next_entry (dirp);
+  if (err == 0)
+    {
+      /* Determine d_namlen and copy the filename from dd_wfd. */
+      size_t namlen = wcslen (dirp->dd_wfd.cFileName);
+      dirp->dd_entry.w.d_namlen = (unsigned short) namlen;
+      memcpy (dirp->dd_entry.w.d_name, dirp->dd_wfd.cFileName,
+             (namlen + 1) * sizeof (wchar_t));
+
+      /* d_reclen has a fixed value. */
+      dirp->dd_entry.w.d_reclen = sizeof (dirp->dd_entry.w);
+
+      /* It shouldn't be necessary to preserve errno when we return non-NULL.
+       * Do it anyway. */
+      errno = saved_errno;
+      return &dirp->dd_entry.w;
+    }
+
+  /* End of directory (-1) or an error (>0). */
+  errno = (err > 0) ? err : saved_errno;
+  return NULL;
+}
+
+
+static struct dirent *
+readdir_impl (DIR *dirp, BOOL fallback8dot3)
+{
+  int saved_errno = errno;
+
+  /*
+   * We want to detect lossy conversion in WideCharToMultiByte. The required
+   * arguments depend on the code page:
+   *
+   *   - CP_UTF8 requires WC_ERR_INVALID_CHARS, and the last argument must be
+   *     NULL. If the filename contains unpaired surrogates (invalid UTF-16),
+   *     the return value will be 0. WC_ERR_INVALID_CHARS only works on
+   *     Windows Vista and later, but UTF-8 locales are only supported with
+   *     new enough UCRT.
+   *
+   *   - CP_ACP and CP_OEMCP support WC_NO_BEST_FIT_CHARS even when those
+   *     code pages are set to UTF-8. Lossy conversion is detected via the
+   *     last argument. This works on Windows 2000 and later. On Windows 10,
+   *     this may work with CP_UTF8 too, but it's undocumented.
+   *
+   * d_name is big enough that conversion cannot run out of buffer space
+   * with double-byte character sets or UTF-8.
+   */
+  unsigned int cp = get_code_page ();
+  DWORD flags = (cp == CP_UTF8) ? WC_ERR_INVALID_CHARS : WC_NO_BEST_FIT_CHARS;
+  BOOL was_lossy = FALSE;
+  BOOL *was_lossy_ptr = (cp == CP_UTF8) ? NULL : &was_lossy;
+
+  /* More than one entry may be read if there are filenames that cannot
+   * be represented in the code page specified in cp. */
+  int err;
+  while ((err = prepare_next_entry (dirp)) == 0)
     {
-      /* Get the next search entry. */
-      if (_tfindnext (dirp->dd_handle, &(dirp->dd_dta)))
+      /* Try to convert the wide char filename to multibyte. */
+      int conv_result = WideCharToMultiByte (
+         cp, flags,
+         dirp->dd_wfd.cFileName, -1,
+         dirp->dd_entry.a.d_name, sizeof (dirp->dd_entry.a.d_name),
+         NULL, was_lossy_ptr);
+
+      if (conv_result == 0 && GetLastError () == ERROR_INVALID_FLAGS)
        {
-         /* We are off the end or otherwise error.
-            _findnext sets errno to ENOENT if no more file
-            Undo this. */
-         DWORD winerr = GetLastError ();
-         if (winerr == ERROR_NO_MORE_FILES)
-           errno = 0;
-         _findclose (dirp->dd_handle);
-         dirp->dd_handle = -1;
-         dirp->dd_stat = -1;
+         /* Be compatible with WinXP (UTF-8 locale with app-local deployment
+          * of new enough UCRT) and NT4. In these cases, lossy conversion of
+          * some characters are allowed. */
+         flags = 0;
+         conv_result = WideCharToMultiByte (
+             cp, flags,
+             dirp->dd_wfd.cFileName, -1,
+             dirp->dd_entry.a.d_name, sizeof (dirp->dd_entry.a.d_name),
+             NULL, was_lossy_ptr);
        }
-      else
+
+      /* Check for <= 1 instead of <= 0 to ensure that the filename
+       * isn't an empty string. */
+      if (conv_result <= 1 || was_lossy)
        {
-         /* Update the status to indicate the correct
-          * number. */
-         dirp->dd_stat++;
+         /* If fallback to a 8.3 name wasn't requested or a 8.3 name doesn't
+          * exist, this filename has to be skipped. */
+         if (!fallback8dot3 || dirp->dd_wfd.cAlternateFileName[0] == '\0')
+           {
+             dirp->dd_endval = EILSEQ;
+             continue;
+           }
+
+         /* Try to use the 8.3 name. */
+         conv_result = WideCharToMultiByte (
+             cp, flags,
+             dirp->dd_wfd.cAlternateFileName, -1,
+             dirp->dd_entry.a.d_name, sizeof (dirp->dd_entry.a.d_name),
+             NULL, was_lossy_ptr);
+         if (conv_result <= 1 || was_lossy)
+           {
+             dirp->dd_endval = EILSEQ;
+             continue;
+           }
+
+         /* Mark that d_name contains a 8.3 name. */
+         dirp->dd_entry.a.d_8dot3 = 1;
        }
-    }
 
-  if (dirp->dd_stat > 0)
-    {
-      /* Successfully got an entry. Everything about the file is
-       * already appropriately filled in except the length of the
-       * file name. */
-      dirp->dd_dir.d_namlen = _tcslen (dirp->dd_dta.name);
-      _tcscpy (dirp->dd_dir.d_name, dirp->dd_dta.name);
-      return &dirp->dd_dir;
+      dirp->dd_entry.a.d_namlen = (unsigned short) (conv_result - 1);
+      dirp->dd_entry.a.d_reclen = sizeof (dirp->dd_entry.a);
+
+      errno = saved_errno;
+      return &dirp->dd_entry.a;
     }
 
-  return (struct _tdirent *) 0;
+  errno = (err > 0) ? err : saved_errno;
+  return NULL;
 }
 
 
-/*
- * closedir
- *
- * Frees up resources allocated by opendir.
- */
-int
-_tclosedir (_TDIR * dirp)
+struct dirent *
+readdir (DIR *dirp)
 {
-  int rc;
+  return readdir_impl (dirp, FALSE);
+}
 
-  errno = 0;
-  rc = 0;
+
+struct dirent *
+_readdir_8dot3 (DIR *dirp)
+{
+  return readdir_impl (dirp, TRUE);
+}
+
+
+int
+closedir (DIR *dirp)
+{
+  int rc = 0;
 
   if (!dirp)
     {
@@ -221,104 +738,92 @@ _tclosedir (_TDIR * dirp)
       return -1;
     }
 
-  if (dirp->dd_handle != -1)
+  if (dirp->dd_handle != INVALID_HANDLE_VALUE)
     {
-      rc = _findclose (dirp->dd_handle);
+      rc = !FindClose (dirp->dd_handle);
     }
 
   /* Delete the dir structure. */
   free (dirp);
 
-  return rc;
+  if (rc)
+    {
+      errno = EBADF;
+      return -1;
+    }
+
+  return 0;
 }
 
-/*
- * rewinddir
- *
- * Return to the beginning of the directory "stream". We simply call findclose
- * and then reset things like an opendir.
- */
+
 void
-_trewinddir (_TDIR * dirp)
+rewinddir (DIR *dirp)
 {
-  errno = 0;
-
   if (!dirp)
     {
       errno = EFAULT;
       return;
     }
 
-  if (dirp->dd_handle != -1)
-    {
-      _findclose (dirp->dd_handle);
-    }
-
-  dirp->dd_handle = -1;
+  /* prepare_next_entry will call FindFirstFileW. */
   dirp->dd_stat = 0;
+  dirp->dd_pos = 0;
+
+  /* Clear a possible delayed EILSEQ. */
+  dirp->dd_endval = -1;
 }
 
-/*
- * telldir
- *
- * Returns the "position" in the "directory stream" which can be used with
- * seekdir to go back to an old entry. We simply return the value in stat.
- */
+
 long
-_ttelldir (_TDIR * dirp)
+telldir (DIR *dirp)
 {
-  errno = 0;
-
   if (!dirp)
     {
       errno = EFAULT;
       return -1;
     }
-  return dirp->dd_stat;
+
+  /* Return dd_pos even if dd_stat indicates end of directory or error.
+   * This way the end of directory behavior matches Linux where telldir keeps
+   * returning the same value as it did right after reading the last entry. */
+  return dirp->dd_pos;
 }
 
-/*
- * seekdir
- *
- * Seek to an entry previously returned by telldir. We rewind the directory
- * and call readdir repeatedly until either dd_stat is the position number
- * or -1 (off the end). This is not perfect, in that the directory may
- * have changed while we weren't looking. But that is probably the case with
- * any such system.
- */
+
 void
-_tseekdir (_TDIR * dirp, long lPos)
+seekdir (DIR *dirp, long pos)
 {
-  errno = 0;
-
   if (!dirp)
     {
       errno = EFAULT;
       return;
     }
 
-  if (lPos < -1)
+  if (pos < 0)
     {
       /* Seeking to an invalid position. */
       errno = EINVAL;
       return;
     }
-  else if (lPos == -1)
+
+  /* If dd_stat indicates an error or the end of the directory, or if dd_stat
+   * is past the requested position by more than one entry, prepare_next_entry
+   * needs to reopen the dir and skip entries to reach the requested position.
+   *
+   * If pos is exactly one entry past the current position in dd_stat,
+   * there is no need to reopen the directory because prepare_next_entry
+   * can reuse the most recent entry (or use the first entry after reopening
+   * the directory if pos == 0 and an earlier rewinddir or seekdir already
+   * set dd_stat = 0).
+   *
+   * NOTE: pos can be LONG_MAX. Avoid an integer overflow. */
+  if (dirp->dd_stat < 0 || dirp->dd_stat - 1 > pos)
     {
-      /* Seek past end. */
-      if (dirp->dd_handle != -1)
-       {
-         _findclose (dirp->dd_handle);
-       }
-      dirp->dd_handle = -1;
-      dirp->dd_stat = -1;
+      /* prepare_next_entry will call FindFirstFileW. */
+      dirp->dd_stat = 0;
     }
-  else
-    {
-      /* Rewind and read forward to the appropriate index. */
-      _trewinddir (dirp);
 
-      while ((dirp->dd_stat < lPos) && _treaddir (dirp))
-       ;
-    }
+  /* If the next call is telldir, it will return the new position as required
+   * by POSIX. Any forward seeking is done when readdir is called. */
+  dirp->dd_pos = pos;
 }
diff --git a/mingw-w64-crt/misc/wdirent.c b/mingw-w64-crt/misc/wdirent.c
deleted file mode 100644
index 6dcf42bd5..000000000
--- a/mingw-w64-crt/misc/wdirent.c
+++ /dev/null
@@ -1,5 +0,0 @@
-#define _UNICODE 1
-#define UNICODE 1
-
-#include <wchar.h>
-#include "dirent.c"
diff --git a/mingw-w64-headers/crt/dirent.h b/mingw-w64-headers/crt/dirent.h
index 421b09278..876e224b1 100644
--- a/mingw-w64-headers/crt/dirent.h
+++ b/mingw-w64-headers/crt/dirent.h
@@ -4,6 +4,7 @@
  * This file is part of the mingw-w64 runtime package.
  * No warranty is given; refer to the file DISCLAIMER.PD within this package.
  *
+ * The dirent implementation was revised in mingw-w64 13.0.0.
  */
 
 #ifndef _DIRENT_H_
@@ -12,8 +13,6 @@
 /* All the headers include this file. */
 #include <crtdefs.h>
 
-#include <io.h>
-
 #ifndef RC_INVOKED
 
 #pragma pack(push,_CRT_PACKING)
@@ -22,97 +21,209 @@
 extern "C" {
 #endif
 
+
+/*
+ * In addition to the d_ino and d_name members required by POSIX,
+ * also d_type, d_8dot3, d_reclen, and d_namlen are present in
+ * struct dirent and struct _wdirent.
+ */
+#define _DIRENT_HAVE_D_TYPE    1
+#define _DIRENT_HAVE_D_8DOT3   1
+#define _DIRENT_HAVE_D_RECLEN  1
+#define _DIRENT_HAVE_D_NAMLEN  1
+
+/*
+ * Values for d_type:
+ *   DT_DIR      Not a reparse point and has FILE_ATTRIBUTE_DIRECTORY
+ *   DT_REG      Not a reparse point and doesn't have FILE_ATTRIBUTE_DIRECTORY
+ *   DT_LNK      A reparse point with the tag IO_REPARSE_TAG_SYMLINK
+ *   DT_SOCK     A reparse point with the tag IO_REPARSE_TAG_AF_UNIX
+ *   DT_UNKNOWN  All other reparse points
+ *   DT_FIFO     Not used (cannot appear in a directory listing)
+ *   DT_CHR      Not used (cannot appear in a directory listing)
+ *
+ * The constants are the same as on many POSIX systems and match the
+ * pattern DT_xxx == (S_IFxxx >> 12). However, as of Febrary 2025:
+ *   - S_IFLNK and S_IFSOCK don't exist in mingw-w64.
+ *   - S_IFBLK is a mingw-w64 extension with an unusual value 0x3000 instead
+ *     of the normal 0x6000, thus DT_BLK was omitted for now. Like DT_FIFO
+ *     and DT_CHR, DT_BLK wouldn't appear in a directory listing.
+ */
+#define DT_UNKNOWN     0
+#define DT_FIFO                1
+#define DT_CHR         2
+#define DT_DIR         4
+#define DT_REG         8
+#define DT_LNK         10
+#define DT_SOCK                12
+
 struct dirent
 {
-       long            d_ino;          /* Always zero. */
-       unsigned short  d_reclen;       /* Always zero. */
+       unsigned short  d_ino;          /* Always zero. */
+       unsigned char   d_type;         /* File type if known. */
+       unsigned char   d_8dot3;        /* 0 or 1, see _readdir_8dot3. */
+       unsigned short  d_reclen;       /* Size of this struct. */
        unsigned short  d_namlen;       /* Length of name in d_name. */
-       char            d_name[260]; /* [FILENAME_MAX] */ /* File name. */
+       char            d_name[778]; /* [(MAX_PATH-1)*3+1] */ /* File name. */
 };
 
+typedef struct __dirent_DIR DIR;
+
 /*
- * This is an internal data structure. Good programmers will not use it
- * except as an argument to one of the functions below.
- * dd_stat field is now int (was short in older versions).
+ * opendir opens a directory stream corresponding to the specified directory.
+ * A pointer to a DIR is returned on success. On error, NULL is returned
+ * and errno is set:
+ *     EFAULT    The argument is NULL.
+ *     ENOMEM    Memory allocation failed.
+ *     EILSEQ    The pathname isn't a valid multibyte string.
+ *     ENAMETOOLONG
+ *               The pathname is too long. In many cases, too long
+ *               pathnames result in ENOENT instead of ENAMETOOLONG.
+ *     ENOENT    The pathname doesn't exist or it is an empty string.
+ *     ENOTDIR   The last element in the pathname is not a directory.
+ *     ELOOP     Too many levels of symbolic links or other error that
+ *               Windows reports as ERROR_CANT_RESOLVE_FILENAME.
+ *     EACCES    Access denied.
+ *     EINVAL    Unknown error. For example, the pathname might refer to
+ *               an unformatted drive or there was an I/O error.
+ *     ENOSYS    This dirent implementation doesn't work on Windows 95/98/ME.
  */
-typedef struct
-{
-       /* disk transfer area for this dir */
-       struct _finddata_t      dd_dta;
-
-       /* dirent struct to return from dir (NOTE: this makes this thread
-        * safe as long as only one thread uses a particular DIR struct at
-        * a time) */
-       struct dirent           dd_dir;
-
-       /* _findnext handle */
-       intptr_t                dd_handle;
+DIR* __cdecl __MINGW_NOTHROW opendir (const char*);
 
-       /*
-        * Status of search:
-        *   0 = not started yet (next entry to read is first entry)
-        *  -1 = off the end
-        *   positive = 0 based index of next entry
-        */
-       int                     dd_stat;
+/*
+ * readdir reads the next filename entry from the specified directory stream.
+ * On success, a pointer to struct dirent is returned. If there are no more
+ * entries, NULL is returned and the value of errno is not modified. If an
+ * error occurs, errno is set and NULL is returned. To distinguish the end
+ * of a directory from errors, the caller must set errno = 0 before calling
+ * readdir.
+ *
+ * If a filename is found that cannot be represented as a multibyte string,
+ * it is skipped. Once the end of the directory is successfully reached,
+ * readdir (or _readdir_8dot3 or _wreaddir) will fail with errno = EILSEQ.
+ * Delaying the error ensures that an application will see all other
+ * filenames in the directory still.
+ *
+ * Possible errors:
+ *     EFAULT    The argument is NULL.
+ *     ENOMEM    Memory allocation failed.
+ *     EILSEQ    At the end of the directory: at least one filename was
+ *               skipped because it cannot be correctly converted to
+ *               a multibyte string.
+ *     ENOENT    The directory stream position is invalid:
+ *                 - Previous call was rewinddir or seekdir, and now
+ *                   readdir couldn't reopen the directory.
+ *                 - 2,147,483,647 (LONG_MAX) entries have been read.
+ *                   Reading more would overflow the internal counters.
+ *     EIO       Unknown error, possibly an I/O error.
+ *
+ * If _DIRENT_USE_READDIR_8DOT3 is defined before <dirent.h> is included,
+ * then readdir is an alias for _readdir_8dot3. See below how it differs.
+ */
+#ifndef _DIRENT_USE_READDIR_8DOT3
+struct dirent* __cdecl __MINGW_NOTHROW readdir (DIR*);
+#else
+struct dirent* __cdecl __MINGW_NOTHROW readdir (DIR*) 
__MINGW_ASM_CALL(_readdir_8dot3);
+#endif
 
-       /* given path for dir with search pattern (struct is extended) */
-       char                    dd_name[1];
-} DIR;
+/*
+ * _readdir_8dot3 is like readdir except that if character set conversion
+ * fails, this function tries to fall back to short 8.3 filename if such
+ * a name exists. This way an application might be able to access files
+ * whose long name cannot be encoded in a multibyte string. A filename
+ * may still be skipped like in readdir if a short name isn't available.
+ *
+ * If a 8.3 name is returned, the d_8dot3 member in struct dirent is set
+ * to 1. Otherwise d_8dot3 is set to 0. readdir and _wreaddir never use
+ * 8.3 names, thus they always set d_8dot3 to 0.
+ */
+struct dirent* __cdecl __MINGW_NOTHROW _readdir_8dot3 (DIR*);
 
-DIR* __cdecl __MINGW_NOTHROW opendir (const char*);
-struct dirent* __cdecl __MINGW_NOTHROW readdir (DIR*);
+/*
+ * closedir closes the specified directory stream. On success, 0 is returned.
+ * On error, -1 is returned and errno is set:
+ *     EFAULT    The argument is NULL.
+ *     EBADF     Error closing the underlying HANDLE with FindClose. The
+ *               memory allocated for the DIR was freed still.
+ */
 int __cdecl __MINGW_NOTHROW closedir (DIR*);
+
+/*
+ * rewinddir resets the position to the beginning of the directory, and
+ * clears a possible delayed EILSEQ set by an earlier call to readdir
+ * or _readdir_8dot3.
+ *
+ * After rewinding, the next call to readdir, _readdir_8dot3, or _wreaddir
+ * will reopen the directory. It can fail, for example, if the directory
+ * has been renamed, removed, or access permissions have changed.
+ */
 void __cdecl __MINGW_NOTHROW rewinddir (DIR*);
+
+/*
+ * telldir returns the current position on success. On error, -1 is returned
+ * and errno is set. There is only one possible error:
+ *     EFAULT    The argument is NULL.
+ */
 long __cdecl __MINGW_NOTHROW telldir (DIR*);
+
+/*
+ * seekdir sets the position for the next readdir, _readdir_8dot3, or
+ * _wreaddir call. A possible delayed EILSEQ set by an earlier call to
+ * readdir or _readdir_8dot3 is preserved (this differs from rewinddir).
+ *
+ * If seeking backwards, the next call to readdir, _readdir_8dot3, or
+ * _wreaddir might need to reopen the directory. It can fail, for example,
+ * if the directory has been renamed, removed, or access permissions
+ * have changed. If reopening is successful, readdir will try to reach
+ * the requested position. This produces expected results only if the
+ * file system always returns the entries in the same order.
+ */
 void __cdecl __MINGW_NOTHROW seekdir (DIR*, long);
 
 
-/* wide char versions */
+/*
+ * Wide char versions
+ *
+ * One can mix calls to narrow and wide functions on the same DIR.
+ */
 
 struct _wdirent
 {
-       long            d_ino;          /* Always zero. */
-       unsigned short  d_reclen;       /* Always zero. */
+       unsigned short  d_ino;          /* Always zero. */
+       unsigned char   d_type;         /* File type if known. */
+       unsigned char   d_8dot3;        /* Always zero. */
+       unsigned short  d_reclen;       /* Size of this struct. */
        unsigned short  d_namlen;       /* Length of name in d_name. */
-       wchar_t         d_name[260]; /* [FILENAME_MAX] */ /* File name. */
+       wchar_t         d_name[260]; /* [MAX_PATH] */ /* File name. */
 };
 
 /*
- * This is an internal data structure. Good programmers will not use it
- * except as an argument to one of the functions below.
+ * _wopendir takes a wide character directory name. See opendir.
+ *
+ * _wopendir cannot fail with EILSEQ.
  */
-typedef struct
-{
-       /* disk transfer area for this dir */
-       struct _wfinddata_t     dd_dta;
-
-       /* dirent struct to return from dir (NOTE: this makes this thread
-        * safe as long as only one thread uses a particular DIR struct at
-        * a time) */
-       struct _wdirent         dd_dir;
-
-       /* _findnext handle */
-       intptr_t                dd_handle;
-
-       /*
-        * Status of search:
-        *   0 = not started yet (next entry to read is first entry)
-        *  -1 = off the end
-        *   positive = 0 based index of next entry
-        */
-       int                     dd_stat;
-
-       /* given path for dir with search pattern (struct is extended) */
-       wchar_t                 dd_name[1];
-} _WDIR;
-
-_WDIR* __cdecl __MINGW_NOTHROW _wopendir (const wchar_t*);
-struct _wdirent* __cdecl __MINGW_NOTHROW _wreaddir (_WDIR*);
-int __cdecl __MINGW_NOTHROW _wclosedir (_WDIR*);
-void __cdecl __MINGW_NOTHROW _wrewinddir (_WDIR*);
-long __cdecl __MINGW_NOTHROW _wtelldir (_WDIR*);
-void __cdecl __MINGW_NOTHROW _wseekdir (_WDIR*, long);
+DIR* __cdecl __MINGW_NOTHROW _wopendir (const wchar_t*);
+
+/*
+ * _wreaddir returns a struct with wide char filename. See readdir.
+ *
+ * Character set conversion problems cannot occur in _wreaddir.
+ * However, if readdir or _readdir_8dot3 have been used on the same DIR,
+ * then a delayed error from those calls may be indicated at the end of
+ * the directory. (If filenames were already skipped by readdir or
+ * _readdir_8dot3, they aren't returned by a later _wreaddir call.)
+ */
+struct _wdirent* __cdecl __MINGW_NOTHROW _wreaddir (DIR*);
+
+/*
+ * Compatibility aliases for source code that has been written against
+ * the old API:
+ */
+typedef DIR _WDIR;
+int __cdecl __MINGW_NOTHROW _wclosedir (DIR*) __MINGW_ASM_CALL(closedir);
+void __cdecl __MINGW_NOTHROW _wrewinddir (DIR*) __MINGW_ASM_CALL(rewinddir);
+long __cdecl __MINGW_NOTHROW _wtelldir (DIR*) __MINGW_ASM_CALL(telldir);
+void __cdecl __MINGW_NOTHROW _wseekdir (DIR*, long) __MINGW_ASM_CALL(seekdir);
 
 
 #ifdef __cplusplus
-- 
2.48.1

_______________________________________________
Mingw-w64-public mailing list
Mingw-w64-public@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/mingw-w64-public

Reply via email to