xref: /freebsd/contrib/pkgconf/libpkgconf/win-dirent.h (revision f9bc005b8ef3b507a5ecbc2d7fe4411ef70eff38)
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