1 /* 2 * win-dirent.h - minimal POSIX dirent shim for Windows (MSVC / MinGW-w64) 3 * 4 * Implements only opendir(), readdir(), closedir(): the subset used by 5 * pkgconf and its test suite. 6 * 7 * Deliberately not included under Cygwin, which provides its own dirent.h. 8 * 9 * SPDX-License-Identifier: pkgconf 10 * 11 * Copyright (c) 2026 Elizabeth Ashford. All rights reserved. 12 * 13 * Permission to use, copy, modify, and/or distribute this software for any 14 * purpose with or without fee is hereby granted, provided that the above 15 * copyright notice and this permission notice appear in all copies. 16 * 17 * This software is provided 'as is' and without any warranty, express or 18 * implied. In no event shall the authors be liable for any damages arising 19 * from the use of this software. 20 */ 21 22 #ifndef PKGCONF_WIN_DIRENT_H 23 #define PKGCONF_WIN_DIRENT_H 24 25 #if defined(_WIN32) && !defined(__CYGWIN__) 26 27 #ifndef WIN32_LEAN_AND_MEAN 28 # define WIN32_LEAN_AND_MEAN 29 #endif 30 #include <windows.h> 31 #include <errno.h> 32 #include <stdlib.h> 33 #include <string.h> 34 #include <sys/types.h> 35 36 #ifndef PATH_MAX 37 # define PATH_MAX MAX_PATH 38 #endif 39 #ifndef NAME_MAX 40 # define NAME_MAX MAX_PATH 41 #endif 42 43 // d_type constants: only the values pkgconf actually tests against 44 #define DT_UNKNOWN 0 45 #define DT_REG 8 // S_IFREG >> 12 46 #define DT_DIR 4 // S_IFDIR >> 12 47 48 #ifdef __cplusplus 49 extern "C" { 50 #endif 51 52 /* ============ */ 53 /* Public types */ 54 /* ============ */ 55 56 struct dirent 57 { 58 // Standard fields pkgconf actually use 59 char d_name[PATH_MAX + 1]; 60 size_t d_namlen; 61 int d_type; 62 63 unsigned short d_reclen; 64 }; 65 66 typedef struct pkgconf_DIR 67 { 68 WIN32_FIND_DATAW data; 69 HANDLE handle; 70 struct dirent ent; 71 int cached; // non-zero when data holds an unread entry 72 } DIR; 73 74 /* ================ */ 75 /* Internal helpers */ 76 /* ================ */ 77 78 /* 79 * Convert a Win32 file-attribute word to a DT_* constant. 80 * Devices can't be distinguished from regular files on win32, so we map everything that isn't a 81 * directory to DT_REG. 82 */ 83 static inline int 84 pkgconf__attr_to_dtype(DWORD attr) 85 { 86 if (attr & FILE_ATTRIBUTE_DIRECTORY) 87 return DT_DIR; 88 return DT_REG; 89 } 90 91 /* 92 * Populate ent from the current data field. 93 * Returns 0 on success, -1 if the filename could not be converted to UTF-8. 94 */ 95 static inline int 96 pkgconf__fill_dirent(DIR *dirp) 97 { 98 int n = WideCharToMultiByte( 99 CP_UTF8, 0, 100 dirp->data.cFileName, -1, 101 dirp->ent.d_name, PATH_MAX + 1, 102 NULL, NULL); 103 104 if (n <= 0) 105 { 106 /* 107 * Conversion failed. Rather than returning a broken entry, signal the error so the caller can 108 * skip or abort. 109 */ 110 errno = EILSEQ; 111 return -1; 112 } 113 114 dirp->ent.d_namlen = (size_t)(n - 1); 115 dirp->ent.d_type = pkgconf__attr_to_dtype(dirp->data.dwFileAttributes); 116 dirp->ent.d_reclen = sizeof(struct dirent); 117 return 0; 118 } 119 120 /* ========= */ 121 /* API shims */ 122 /* ========= */ 123 124 static inline DIR * 125 opendir(const char *path) 126 { 127 DIR *dirp; 128 wchar_t wpath[PATH_MAX + 3]; // +3: possible '\', '*', NUL 129 wchar_t *p; 130 int wlen; 131 132 if (path == NULL || path[0] == '\0') 133 { 134 errno = ENOENT; 135 return NULL; 136 } 137 138 // Convert caller-supplied UTF-8 path to wide string 139 wlen = MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, PATH_MAX + 1); 140 if (wlen <= 0) 141 { 142 errno = ENOENT; 143 return NULL; 144 } 145 146 // Append \* search glob, handling paths that already end in a separator 147 p = wpath + wlen - 1; 148 if (*p != L'\\' && *p != L'/' && *p != L':') 149 *p++ = L'\\'; 150 *p++ = L'*'; 151 *p = L'\0'; 152 153 dirp = (DIR *)malloc(sizeof(DIR)); 154 if (dirp == NULL) 155 { 156 errno = ENOMEM; 157 return NULL; 158 } 159 160 dirp->handle = FindFirstFileExW( 161 wpath, 162 FindExInfoBasic, // skip short (8.3) names, faster 163 &dirp->data, 164 FindExSearchNameMatch, 165 NULL, 166 FIND_FIRST_EX_LARGE_FETCH); 167 168 if (dirp->handle == INVALID_HANDLE_VALUE) 169 { 170 DWORD err = GetLastError(); 171 free(dirp); 172 switch (err) 173 { 174 case ERROR_ACCESS_DENIED: 175 errno = EACCES; 176 break; 177 case ERROR_DIRECTORY: 178 errno = ENOTDIR; 179 break; 180 default: 181 errno = ENOENT; 182 break; 183 } 184 return NULL; 185 } 186 187 dirp->cached = 1; // first entry already sitting in data 188 return dirp; 189 } 190 191 static inline struct dirent * 192 readdir(DIR *dirp) 193 { 194 if (dirp == NULL) 195 { 196 errno = EBADF; 197 return NULL; 198 } 199 200 if (dirp->cached) 201 { 202 // Consume the entry that opendir() or the previous FindNextFileW already placed in data 203 dirp->cached = 0; 204 } 205 else 206 { 207 // Advance to the next entry 208 if (dirp->handle == INVALID_HANDLE_VALUE) 209 return NULL; 210 211 if (!FindNextFileW(dirp->handle, &dirp->data)) 212 { 213 // End of directory or hard error, stop 214 FindClose(dirp->handle); 215 dirp->handle = INVALID_HANDLE_VALUE; 216 return NULL; 217 } 218 } 219 220 if (pkgconf__fill_dirent(dirp) != 0) 221 return NULL; 222 223 return &dirp->ent; 224 } 225 226 static inline int 227 closedir(DIR *dirp) 228 { 229 if (dirp == NULL) 230 { 231 errno = EBADF; 232 return -1; 233 } 234 235 if (dirp->handle != INVALID_HANDLE_VALUE) 236 FindClose(dirp->handle); 237 238 free(dirp); 239 return 0; 240 } 241 242 #ifdef __cplusplus 243 } 244 #endif 245 246 #endif // defined(_WIN32) && !defined(__CYGWIN__) 247 #endif // PKGCONF_WIN_DIRENT_H 248