]>
Commit | Line | Data |
---|---|---|
22a2d3d5 UG |
1 | /* |
2 | * By default, use a read/write loop to copy files on POSIX systems. | |
3 | * On Linux, use sendfile by default as it's slightly faster. On | |
4 | * macOS, we avoid fcopyfile by default because it's slightly slower. | |
5 | */ | |
6 | #undef USE_FCOPYFILE | |
7 | #define USE_SENDFILE 1 | |
8 | ||
2e6f06a8 VM |
9 | #ifdef _WIN32 |
10 | ||
c25aa7cd PP |
11 | #ifdef CLAR_WIN32_LONGPATHS |
12 | # define CLAR_MAX_PATH 4096 | |
13 | #else | |
14 | # define CLAR_MAX_PATH MAX_PATH | |
15 | #endif | |
16 | ||
2e6f06a8 VM |
17 | #define RM_RETRY_COUNT 5 |
18 | #define RM_RETRY_DELAY 10 | |
19 | ||
20 | #ifdef __MINGW32__ | |
21 | ||
22 | /* These security-enhanced functions are not available | |
23 | * in MinGW, so just use the vanilla ones */ | |
24 | #define wcscpy_s(a, b, c) wcscpy((a), (c)) | |
25 | #define wcscat_s(a, b, c) wcscat((a), (c)) | |
26 | ||
27 | #endif /* __MINGW32__ */ | |
28 | ||
7be88b4c | 29 | static int |
2e6f06a8 VM |
30 | fs__dotordotdot(WCHAR *_tocheck) |
31 | { | |
32 | return _tocheck[0] == '.' && | |
33 | (_tocheck[1] == '\0' || | |
34 | (_tocheck[1] == '.' && _tocheck[2] == '\0')); | |
35 | } | |
36 | ||
37 | static int | |
38 | fs_rmdir_rmdir(WCHAR *_wpath) | |
39 | { | |
40 | unsigned retries = 1; | |
41 | ||
42 | while (!RemoveDirectoryW(_wpath)) { | |
43 | /* Only retry when we have retries remaining, and the | |
44 | * error was ERROR_DIR_NOT_EMPTY. */ | |
45 | if (retries++ > RM_RETRY_COUNT || | |
46 | ERROR_DIR_NOT_EMPTY != GetLastError()) | |
47 | return -1; | |
48 | ||
49 | /* Give whatever has a handle to a child item some time | |
50 | * to release it before trying again */ | |
51 | Sleep(RM_RETRY_DELAY * retries * retries); | |
52 | } | |
53 | ||
54 | return 0; | |
55 | } | |
56 | ||
c25aa7cd PP |
57 | static void translate_path(WCHAR *path, size_t path_size) |
58 | { | |
59 | size_t path_len, i; | |
60 | ||
61 | if (wcsncmp(path, L"\\\\?\\", 4) == 0) | |
62 | return; | |
63 | ||
64 | path_len = wcslen(path); | |
65 | cl_assert(path_size > path_len + 4); | |
66 | ||
67 | for (i = path_len; i > 0; i--) { | |
68 | WCHAR c = path[i - 1]; | |
69 | ||
70 | if (c == L'/') | |
71 | path[i + 3] = L'\\'; | |
72 | else | |
73 | path[i + 3] = path[i - 1]; | |
74 | } | |
75 | ||
76 | path[0] = L'\\'; | |
77 | path[1] = L'\\'; | |
78 | path[2] = L'?'; | |
79 | path[3] = L'\\'; | |
80 | path[path_len + 4] = L'\0'; | |
81 | } | |
82 | ||
2e6f06a8 VM |
83 | static void |
84 | fs_rmdir_helper(WCHAR *_wsource) | |
85 | { | |
c25aa7cd | 86 | WCHAR buffer[CLAR_MAX_PATH]; |
2e6f06a8 VM |
87 | HANDLE find_handle; |
88 | WIN32_FIND_DATAW find_data; | |
f3738eba | 89 | size_t buffer_prefix_len; |
2e6f06a8 VM |
90 | |
91 | /* Set up the buffer and capture the length */ | |
c25aa7cd PP |
92 | wcscpy_s(buffer, CLAR_MAX_PATH, _wsource); |
93 | translate_path(buffer, CLAR_MAX_PATH); | |
94 | wcscat_s(buffer, CLAR_MAX_PATH, L"\\"); | |
2e6f06a8 VM |
95 | buffer_prefix_len = wcslen(buffer); |
96 | ||
97 | /* FindFirstFile needs a wildcard to match multiple items */ | |
c25aa7cd | 98 | wcscat_s(buffer, CLAR_MAX_PATH, L"*"); |
2e6f06a8 VM |
99 | find_handle = FindFirstFileW(buffer, &find_data); |
100 | cl_assert(INVALID_HANDLE_VALUE != find_handle); | |
101 | ||
102 | do { | |
103 | /* FindFirstFile/FindNextFile gives back . and .. | |
104 | * entries at the beginning */ | |
105 | if (fs__dotordotdot(find_data.cFileName)) | |
106 | continue; | |
107 | ||
c25aa7cd | 108 | wcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName); |
2e6f06a8 VM |
109 | |
110 | if (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes) | |
111 | fs_rmdir_helper(buffer); | |
112 | else { | |
113 | /* If set, the +R bit must be cleared before deleting */ | |
114 | if (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes) | |
115 | cl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY)); | |
116 | ||
117 | cl_assert(DeleteFileW(buffer)); | |
118 | } | |
119 | } | |
120 | while (FindNextFileW(find_handle, &find_data)); | |
121 | ||
122 | /* Ensure that we successfully completed the enumeration */ | |
123 | cl_assert(ERROR_NO_MORE_FILES == GetLastError()); | |
124 | ||
125 | /* Close the find handle */ | |
126 | FindClose(find_handle); | |
127 | ||
128 | /* Now that the directory is empty, remove it */ | |
129 | cl_assert(0 == fs_rmdir_rmdir(_wsource)); | |
130 | } | |
131 | ||
132 | static int | |
133 | fs_rm_wait(WCHAR *_wpath) | |
134 | { | |
135 | unsigned retries = 1; | |
136 | DWORD last_error; | |
137 | ||
138 | do { | |
139 | if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath)) | |
140 | last_error = GetLastError(); | |
141 | else | |
142 | last_error = ERROR_SUCCESS; | |
143 | ||
144 | /* Is the item gone? */ | |
145 | if (ERROR_FILE_NOT_FOUND == last_error || | |
146 | ERROR_PATH_NOT_FOUND == last_error) | |
147 | return 0; | |
148 | ||
149 | Sleep(RM_RETRY_DELAY * retries * retries); | |
150 | } | |
151 | while (retries++ <= RM_RETRY_COUNT); | |
152 | ||
153 | return -1; | |
154 | } | |
155 | ||
156 | static void | |
157 | fs_rm(const char *_source) | |
158 | { | |
c25aa7cd | 159 | WCHAR wsource[CLAR_MAX_PATH]; |
2e6f06a8 VM |
160 | DWORD attrs; |
161 | ||
162 | /* The input path is UTF-8. Convert it to wide characters | |
163 | * for use with the Windows API */ | |
164 | cl_assert(MultiByteToWideChar(CP_UTF8, | |
165 | MB_ERR_INVALID_CHARS, | |
166 | _source, | |
167 | -1, /* Indicates NULL termination */ | |
168 | wsource, | |
c25aa7cd PP |
169 | CLAR_MAX_PATH)); |
170 | ||
171 | translate_path(wsource, CLAR_MAX_PATH); | |
2e6f06a8 VM |
172 | |
173 | /* Does the item exist? If not, we have no work to do */ | |
174 | attrs = GetFileAttributesW(wsource); | |
175 | ||
176 | if (INVALID_FILE_ATTRIBUTES == attrs) | |
177 | return; | |
178 | ||
179 | if (FILE_ATTRIBUTE_DIRECTORY & attrs) | |
180 | fs_rmdir_helper(wsource); | |
181 | else { | |
182 | /* The item is a file. Strip the +R bit */ | |
183 | if (FILE_ATTRIBUTE_READONLY & attrs) | |
184 | cl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY)); | |
185 | ||
186 | cl_assert(DeleteFileW(wsource)); | |
187 | } | |
188 | ||
189 | /* Wait for the DeleteFile or RemoveDirectory call to complete */ | |
190 | cl_assert(0 == fs_rm_wait(wsource)); | |
191 | } | |
192 | ||
193 | static void | |
194 | fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest) | |
195 | { | |
c25aa7cd | 196 | WCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH]; |
2e6f06a8 VM |
197 | HANDLE find_handle; |
198 | WIN32_FIND_DATAW find_data; | |
f3738eba | 199 | size_t buf_source_prefix_len, buf_dest_prefix_len; |
2e6f06a8 | 200 | |
c25aa7cd PP |
201 | wcscpy_s(buf_source, CLAR_MAX_PATH, _wsource); |
202 | wcscat_s(buf_source, CLAR_MAX_PATH, L"\\"); | |
203 | translate_path(buf_source, CLAR_MAX_PATH); | |
2e6f06a8 VM |
204 | buf_source_prefix_len = wcslen(buf_source); |
205 | ||
c25aa7cd PP |
206 | wcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest); |
207 | wcscat_s(buf_dest, CLAR_MAX_PATH, L"\\"); | |
208 | translate_path(buf_dest, CLAR_MAX_PATH); | |
2e6f06a8 VM |
209 | buf_dest_prefix_len = wcslen(buf_dest); |
210 | ||
211 | /* Get an enumerator for the items in the source. */ | |
c25aa7cd | 212 | wcscat_s(buf_source, CLAR_MAX_PATH, L"*"); |
2e6f06a8 VM |
213 | find_handle = FindFirstFileW(buf_source, &find_data); |
214 | cl_assert(INVALID_HANDLE_VALUE != find_handle); | |
215 | ||
216 | /* Create the target directory. */ | |
217 | cl_assert(CreateDirectoryW(_wdest, NULL)); | |
218 | ||
219 | do { | |
220 | /* FindFirstFile/FindNextFile gives back . and .. | |
221 | * entries at the beginning */ | |
222 | if (fs__dotordotdot(find_data.cFileName)) | |
223 | continue; | |
224 | ||
c25aa7cd PP |
225 | wcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName); |
226 | wcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName); | |
2e6f06a8 VM |
227 | |
228 | if (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes) | |
229 | fs_copydir_helper(buf_source, buf_dest); | |
230 | else | |
231 | cl_assert(CopyFileW(buf_source, buf_dest, TRUE)); | |
232 | } | |
233 | while (FindNextFileW(find_handle, &find_data)); | |
234 | ||
235 | /* Ensure that we successfully completed the enumeration */ | |
236 | cl_assert(ERROR_NO_MORE_FILES == GetLastError()); | |
237 | ||
238 | /* Close the find handle */ | |
239 | FindClose(find_handle); | |
240 | } | |
241 | ||
242 | static void | |
243 | fs_copy(const char *_source, const char *_dest) | |
244 | { | |
c25aa7cd | 245 | WCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH]; |
2e6f06a8 VM |
246 | DWORD source_attrs, dest_attrs; |
247 | HANDLE find_handle; | |
248 | WIN32_FIND_DATAW find_data; | |
7be88b4c | 249 | |
2e6f06a8 VM |
250 | /* The input paths are UTF-8. Convert them to wide characters |
251 | * for use with the Windows API. */ | |
252 | cl_assert(MultiByteToWideChar(CP_UTF8, | |
253 | MB_ERR_INVALID_CHARS, | |
254 | _source, | |
255 | -1, | |
256 | wsource, | |
c25aa7cd | 257 | CLAR_MAX_PATH)); |
2e6f06a8 VM |
258 | |
259 | cl_assert(MultiByteToWideChar(CP_UTF8, | |
260 | MB_ERR_INVALID_CHARS, | |
261 | _dest, | |
262 | -1, | |
263 | wdest, | |
c25aa7cd PP |
264 | CLAR_MAX_PATH)); |
265 | ||
266 | translate_path(wsource, CLAR_MAX_PATH); | |
267 | translate_path(wdest, CLAR_MAX_PATH); | |
2e6f06a8 VM |
268 | |
269 | /* Check the source for existence */ | |
270 | source_attrs = GetFileAttributesW(wsource); | |
271 | cl_assert(INVALID_FILE_ATTRIBUTES != source_attrs); | |
272 | ||
273 | /* Check the target for existence */ | |
274 | dest_attrs = GetFileAttributesW(wdest); | |
275 | ||
276 | if (INVALID_FILE_ATTRIBUTES != dest_attrs) { | |
277 | /* Target exists; append last path part of source to target. | |
278 | * Use FindFirstFile to parse the path */ | |
279 | find_handle = FindFirstFileW(wsource, &find_data); | |
280 | cl_assert(INVALID_HANDLE_VALUE != find_handle); | |
c25aa7cd PP |
281 | wcscat_s(wdest, CLAR_MAX_PATH, L"\\"); |
282 | wcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName); | |
2e6f06a8 VM |
283 | FindClose(find_handle); |
284 | ||
285 | /* Check the new target for existence */ | |
286 | cl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest)); | |
287 | } | |
288 | ||
289 | if (FILE_ATTRIBUTE_DIRECTORY & source_attrs) | |
290 | fs_copydir_helper(wsource, wdest); | |
291 | else | |
292 | cl_assert(CopyFileW(wsource, wdest, TRUE)); | |
293 | } | |
294 | ||
295 | void | |
296 | cl_fs_cleanup(void) | |
297 | { | |
298 | fs_rm(fixture_path(_clar_path, "*")); | |
299 | } | |
300 | ||
301 | #else | |
7be88b4c RB |
302 | |
303 | #include <errno.h> | |
304 | #include <string.h> | |
22a2d3d5 UG |
305 | #include <limits.h> |
306 | #include <dirent.h> | |
307 | #include <fcntl.h> | |
308 | #include <unistd.h> | |
309 | #include <sys/types.h> | |
310 | #include <sys/stat.h> | |
311 | ||
312 | #if defined(__linux__) | |
313 | # include <sys/sendfile.h> | |
314 | #endif | |
7be88b4c | 315 | |
22a2d3d5 UG |
316 | #if defined(__APPLE__) |
317 | # include <copyfile.h> | |
318 | #endif | |
319 | ||
320 | static void basename_r(const char **out, int *out_len, const char *in) | |
2e6f06a8 | 321 | { |
22a2d3d5 UG |
322 | size_t in_len = strlen(in), start_pos; |
323 | ||
324 | for (in_len = strlen(in); in_len; in_len--) { | |
325 | if (in[in_len - 1] != '/') | |
326 | break; | |
327 | } | |
328 | ||
329 | for (start_pos = in_len; start_pos; start_pos--) { | |
330 | if (in[start_pos - 1] == '/') | |
331 | break; | |
332 | } | |
2e6f06a8 | 333 | |
22a2d3d5 | 334 | cl_assert(in_len - start_pos < INT_MAX); |
2e6f06a8 | 335 | |
22a2d3d5 UG |
336 | if (in_len - start_pos > 0) { |
337 | *out = &in[start_pos]; | |
338 | *out_len = (in_len - start_pos); | |
339 | } else { | |
340 | *out = "/"; | |
341 | *out_len = 1; | |
2e6f06a8 | 342 | } |
22a2d3d5 UG |
343 | } |
344 | ||
345 | static char *joinpath(const char *dir, const char *base, int base_len) | |
346 | { | |
347 | char *out; | |
348 | int len; | |
2e6f06a8 | 349 | |
22a2d3d5 UG |
350 | if (base_len == -1) { |
351 | size_t bl = strlen(base); | |
352 | ||
353 | cl_assert(bl < INT_MAX); | |
354 | base_len = (int)bl; | |
2e6f06a8 VM |
355 | } |
356 | ||
22a2d3d5 UG |
357 | len = strlen(dir) + base_len + 2; |
358 | cl_assert(len > 0); | |
359 | ||
360 | cl_assert(out = malloc(len)); | |
361 | cl_assert(snprintf(out, len, "%s/%.*s", dir, base_len, base) < len); | |
362 | ||
363 | return out; | |
364 | } | |
365 | ||
366 | static void | |
367 | fs_copydir_helper(const char *source, const char *dest, int dest_mode) | |
368 | { | |
369 | DIR *source_dir; | |
370 | struct dirent *d; | |
371 | ||
372 | mkdir(dest, dest_mode); | |
373 | ||
374 | cl_assert_(source_dir = opendir(source), "Could not open source dir"); | |
375 | while ((d = (errno = 0, readdir(source_dir))) != NULL) { | |
376 | char *child; | |
377 | ||
378 | if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) | |
379 | continue; | |
380 | ||
381 | child = joinpath(source, d->d_name, -1); | |
382 | fs_copy(child, dest); | |
383 | free(child); | |
384 | } | |
385 | ||
386 | cl_assert_(errno == 0, "Failed to iterate source dir"); | |
387 | ||
388 | closedir(source_dir); | |
389 | } | |
390 | ||
391 | static void | |
392 | fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode) | |
393 | { | |
394 | int in, out; | |
395 | ||
396 | cl_must_pass((in = open(source, O_RDONLY))); | |
397 | cl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode))); | |
398 | ||
399 | #if USE_FCOPYFILE && defined(__APPLE__) | |
400 | ((void)(source_len)); /* unused */ | |
401 | cl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA)); | |
402 | #elif USE_SENDFILE && defined(__linux__) | |
403 | { | |
404 | ssize_t ret = 0; | |
405 | ||
406 | while (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) { | |
407 | source_len -= (size_t)ret; | |
408 | } | |
409 | cl_assert(ret >= 0); | |
410 | } | |
411 | #else | |
412 | { | |
413 | char buf[131072]; | |
414 | ssize_t ret; | |
415 | ||
416 | ((void)(source_len)); /* unused */ | |
7be88b4c | 417 | |
22a2d3d5 UG |
418 | while ((ret = read(in, buf, sizeof(buf))) > 0) { |
419 | size_t len = (size_t)ret; | |
420 | ||
421 | while (len && (ret = write(out, buf, len)) > 0) { | |
422 | cl_assert(ret <= (ssize_t)len); | |
423 | len -= ret; | |
424 | } | |
425 | cl_assert(ret >= 0); | |
426 | } | |
427 | cl_assert(ret == 0); | |
428 | } | |
429 | #endif | |
430 | ||
431 | close(in); | |
432 | close(out); | |
2e6f06a8 VM |
433 | } |
434 | ||
435 | static void | |
22a2d3d5 | 436 | fs_copy(const char *source, const char *_dest) |
2e6f06a8 | 437 | { |
22a2d3d5 | 438 | char *dbuf = NULL; |
c25aa7cd | 439 | const char *dest = NULL; |
22a2d3d5 UG |
440 | struct stat source_st, dest_st; |
441 | ||
442 | cl_must_pass_(lstat(source, &source_st), "Failed to stat copy source"); | |
2e6f06a8 | 443 | |
22a2d3d5 UG |
444 | if (lstat(_dest, &dest_st) == 0) { |
445 | const char *base; | |
446 | int base_len; | |
2e6f06a8 | 447 | |
22a2d3d5 UG |
448 | /* Target exists and is directory; append basename */ |
449 | cl_assert(S_ISDIR(dest_st.st_mode)); | |
2e6f06a8 | 450 | |
22a2d3d5 UG |
451 | basename_r(&base, &base_len, source); |
452 | cl_assert(base_len < INT_MAX); | |
2e6f06a8 | 453 | |
22a2d3d5 UG |
454 | dbuf = joinpath(_dest, base, base_len); |
455 | dest = dbuf; | |
456 | } else if (errno != ENOENT) { | |
457 | cl_fail("Cannot copy; cannot stat destination"); | |
458 | } else { | |
459 | dest = _dest; | |
460 | } | |
2e6f06a8 | 461 | |
22a2d3d5 UG |
462 | if (S_ISDIR(source_st.st_mode)) { |
463 | fs_copydir_helper(source, dest, source_st.st_mode); | |
464 | } else { | |
465 | fs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode); | |
466 | } | |
467 | ||
468 | free(dbuf); | |
2e6f06a8 VM |
469 | } |
470 | ||
471 | static void | |
22a2d3d5 | 472 | fs_rmdir_helper(const char *path) |
2e6f06a8 | 473 | { |
22a2d3d5 UG |
474 | DIR *dir; |
475 | struct dirent *d; | |
476 | ||
477 | cl_assert_(dir = opendir(path), "Could not open dir"); | |
478 | while ((d = (errno = 0, readdir(dir))) != NULL) { | |
479 | char *child; | |
480 | ||
481 | if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) | |
482 | continue; | |
2e6f06a8 | 483 | |
22a2d3d5 UG |
484 | child = joinpath(path, d->d_name, -1); |
485 | fs_rm(child); | |
486 | free(child); | |
487 | } | |
488 | ||
489 | cl_assert_(errno == 0, "Failed to iterate source dir"); | |
490 | closedir(dir); | |
491 | ||
492 | cl_must_pass_(rmdir(path), "Could not remove directory"); | |
493 | } | |
2e6f06a8 | 494 | |
22a2d3d5 UG |
495 | static void |
496 | fs_rm(const char *path) | |
497 | { | |
498 | struct stat st; | |
499 | ||
500 | if (lstat(path, &st)) { | |
501 | if (errno == ENOENT) | |
502 | return; | |
503 | ||
504 | cl_fail("Cannot copy; cannot stat destination"); | |
505 | } | |
506 | ||
507 | if (S_ISDIR(st.st_mode)) { | |
508 | fs_rmdir_helper(path); | |
509 | } else { | |
510 | cl_must_pass(unlink(path)); | |
511 | } | |
2e6f06a8 VM |
512 | } |
513 | ||
514 | void | |
515 | cl_fs_cleanup(void) | |
516 | { | |
517 | clar_unsandbox(); | |
518 | clar_sandbox(); | |
519 | } | |
520 | #endif |