GCC Code Coverage Report


Directory: src/
File: src/mpq-file.c
Date: 2026-10-06 13:47:41
Exec Total Coverage
Lines: 165 217 76.0%
Functions: 17 18 94.4%
Branches: 120 192 62.5%

Line Branch Exec Source
1 /*
2 * mpq-file.c -- private file and directory operations.
3 *
4 * Copyright (c) 2003-2026 Maik Broemme <mbroemme@libmpq.org>
5 *
6 * This file is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation; either version 2.1 of the License, or
9 * (at your option) any later version.
10 *
11 * This file is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this file; if not, see <https://www.gnu.org/licenses/>.
18 */
19
20 #ifdef HAVE_CONFIG_H
21 #include "config.h"
22 #endif
23
24 #include "mpq-file.h"
25 #include <errno.h>
26 #include <limits.h>
27 #include <stdlib.h>
28 #include <string.h>
29
30 #ifdef _WIN32
31 #include <windows.h>
32
33 #include <aclapi.h>
34 #include <bcrypt.h>
35 #include <fcntl.h>
36 #include <io.h>
37 #include <sddl.h>
38 #else
39 #include <fcntl.h>
40 #include <sys/stat.h>
41 #include <unistd.h>
42 #endif
43
44 /* Both backends require the same 32-character random suffix placeholder. */
45 static int
46 593 valid_template(const char *suffix)
47 {
48 size_t length;
49
50
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 592 times.
593 if (suffix == NULL)
51 1 return 0;
52 592 length = strlen(suffix);
53
4/4
✓ Branch 0 taken 591 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 590 times.
✓ Branch 3 taken 1 times.
592 return length >= 32 && strcmp(suffix + length - 32, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") == 0;
54 }
55
56 #ifdef _WIN32
57 static wchar_t *wide_path(const char *path);
58 #endif
59
60 char *
61 4437 libmpq__string_duplicate(const char *text)
62 {
63 size_t size;
64
65
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4437 times.
4437 if (text == NULL)
66 ✗ return NULL;
67 4437 size = strlen(text) + 1;
68 4437 char *copy = malloc(size);
69
70
1/2
✓ Branch 0 taken 4437 times.
✗ Branch 1 not taken.
4437 if (copy != NULL)
71 4437 memcpy(copy, text, size);
72 4437 return copy;
73 }
74
75 char *
76 87 libmpq__file_absolute_path(const char *path)
77 {
78 #ifdef _WIN32
79 wchar_t *wide;
80 wchar_t *absolute;
81 DWORD characters;
82 int bytes;
83 char *result;
84
85 if (path == NULL || path[0] == '\0')
86 return NULL;
87 wide = wide_path(path);
88 if (wide == NULL)
89 return NULL;
90 characters = GetFullPathNameW(wide, 0, NULL, NULL);
91 if (characters == 0) {
92 free(wide);
93 return NULL;
94 }
95 absolute = malloc((size_t)characters * sizeof(*absolute));
96 if (absolute == NULL) {
97 free(wide);
98 return NULL;
99 }
100 if (GetFullPathNameW(wide, characters, absolute, NULL) >= characters) {
101 free(absolute);
102 free(wide);
103 return NULL;
104 }
105 free(wide);
106 bytes = WideCharToMultiByte(CP_UTF8, 0, absolute, -1, NULL, 0, NULL, NULL);
107 result = bytes > 0 ? malloc((size_t)bytes) : NULL;
108 if (result != NULL)
109 WideCharToMultiByte(CP_UTF8, 0, absolute, -1, result, bytes, NULL, NULL);
110 free(absolute);
111 return result;
112 #else
113 87 size_t size = 256;
114 char *directory;
115 char *result;
116 size_t directory_size;
117 size_t path_size;
118
119
2/4
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 87 times.
87 if (path == NULL || path[0] == '\0')
120 ✗ return NULL;
121
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 87 times.
87 if (path[0] == '/')
122 ✗ return libmpq__string_duplicate(path);
123 for (;;) {
124 87 directory = malloc(size);
125
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 87 times.
87 if (directory == NULL)
126 ✗ return NULL;
127
1/2
✓ Branch 1 taken 87 times.
✗ Branch 2 not taken.
87 if (getcwd(directory, size) != NULL)
128 87 break;
129 ✗ free(directory);
130 ✗ if (errno != ERANGE || size > SIZE_MAX / 2U)
131 ✗ return NULL;
132 ✗ size *= 2U;
133 }
134 87 directory_size = strlen(directory);
135 87 path_size = strlen(path);
136
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 87 times.
87 if (directory_size > SIZE_MAX - path_size - 2U) {
137 ✗ free(directory);
138 ✗ return NULL;
139 }
140 87 result = malloc(directory_size + path_size + 2U);
141
1/2
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
87 if (result != NULL) {
142 87 memcpy(result, directory, directory_size);
143 87 result[directory_size] = '/';
144 87 memcpy(result + directory_size + 1U, path, path_size + 1U);
145 }
146 87 free(directory);
147 87 return result;
148 #endif
149 }
150
151 int32_t
152 47513 libmpq__file_seek(FILE *file, uint64_t offset, int origin)
153 {
154
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 47512 times.
47513 if (file == NULL)
155 1 return LIBMPQ_ERROR_EXIST;
156
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 47511 times.
47512 if (offset > INT64_MAX)
157 1 return LIBMPQ_ERROR_SEEK;
158 #ifdef _WIN32
159 return _fseeki64(file, (int64_t)offset, origin) == 0 ? 0 : LIBMPQ_ERROR_SEEK;
160 #else
161
162 /* Check before narrowing on platforms with a smaller signed off_t. */
163 if (sizeof(off_t) < sizeof(int64_t) &&
164 offset > (UINT64_MAX >> (65U - sizeof(off_t) * CHAR_BIT)))
165 return LIBMPQ_ERROR_SEEK;
166
1/2
✓ Branch 1 taken 47511 times.
✗ Branch 2 not taken.
47511 return fseeko(file, (off_t)offset, origin) == 0 ? 0 : LIBMPQ_ERROR_SEEK;
167 #endif
168 }
169
170 libmpq__off_t
171 10650 libmpq__file_tell(FILE *file)
172 {
173
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 10649 times.
10650 if (file == NULL)
174 1 return LIBMPQ_ERROR_EXIST;
175 #ifdef _WIN32
176 int64_t position = _ftelli64(file);
177 #else
178 10649 off_t position = ftello(file);
179 #endif
180
181
1/2
✓ Branch 0 taken 10649 times.
✗ Branch 1 not taken.
10649 return position < 0 ? LIBMPQ_ERROR_SEEK : (libmpq__off_t)position;
182 }
183
184 #ifdef _WIN32
185
186 struct mpq_directory
187 {
188 HANDLE handle;
189 };
190
191 static wchar_t *
192 wide_path(const char *path)
193 {
194 int count;
195 wchar_t *wide;
196
197 if (path == NULL) {
198 errno = EINVAL;
199 return NULL;
200 }
201 count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, NULL, 0);
202 if (count == 0) {
203 errno = EINVAL;
204 return NULL;
205 }
206 wide = malloc((size_t)count * sizeof(*wide));
207 if (wide != NULL)
208 MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide, count);
209 return wide;
210 }
211
212 static FILE *
213 handle_stream(HANDLE handle, const char *mode)
214 {
215 int flags = _O_BINARY | _O_NOINHERIT;
216 int fd;
217 FILE *file;
218
219 if (strchr(mode, '+') != NULL)
220 flags |= _O_RDWR;
221 else if (mode[0] == 'w' || mode[0] == 'a')
222 flags |= _O_WRONLY;
223 else
224 flags |= _O_RDONLY;
225 if (mode[0] == 'a')
226 flags |= _O_APPEND;
227 fd = _open_osfhandle((intptr_t)handle, flags);
228
229 if (fd < 0) {
230 CloseHandle(handle);
231 return NULL;
232 }
233 file = _fdopen(fd, mode);
234 if (file == NULL)
235 _close(fd);
236 return file;
237 }
238
239 FILE *
240 libmpq__file_open(const char *path, const char *mode)
241 {
242 wchar_t *wide;
243 HANDLE handle;
244 DWORD error;
245 DWORD access;
246 DWORD disposition;
247
248 if (path == NULL || path[0] == '\0' || mode == NULL || mode[0] == '\0') {
249 errno = EINVAL;
250 return NULL;
251 }
252 wide = wide_path(path);
253 if (wide == NULL)
254 return NULL;
255 if (strchr(mode, '+') != NULL)
256 access = GENERIC_READ | GENERIC_WRITE;
257 else if (mode[0] == 'w' || mode[0] == 'a')
258 access = GENERIC_WRITE;
259 else
260 access = GENERIC_READ;
261 disposition = mode[0] == 'w' ? CREATE_ALWAYS : mode[0] == 'a' ? OPEN_ALWAYS : OPEN_EXISTING;
262 handle = CreateFileW(
263 wide, access, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, disposition,
264 FILE_ATTRIBUTE_NORMAL, NULL
265 );
266 error = GetLastError();
267 free(wide);
268 if (handle == INVALID_HANDLE_VALUE) {
269 errno = error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND ? ENOENT : EACCES;
270 return NULL;
271 }
272 return handle_stream(handle, mode);
273 }
274
275 int32_t
276 libmpq__file_identity(FILE *file, uint64_t *device, uint64_t *inode)
277 {
278 BY_HANDLE_FILE_INFORMATION info;
279
280 if (device != NULL)
281 *device = 0;
282 if (inode != NULL)
283 *inode = 0;
284 if (file == NULL || device == NULL || inode == NULL)
285 return LIBMPQ_ERROR_EXIST;
286 if (!GetFileInformationByHandle((HANDLE)_get_osfhandle(_fileno(file)), &info))
287 return LIBMPQ_ERROR_OPEN;
288 *device = info.dwVolumeSerialNumber;
289 *inode = ((uint64_t)info.nFileIndexHigh << 32) | info.nFileIndexLow;
290 return 0;
291 }
292
293 /* Resolve against the retained handle, not the process working directory. */
294 static wchar_t *
295 directory_path(mpq_directory_s *directory, const char *name)
296 {
297 DWORD size = GetFinalPathNameByHandleW(directory->handle, NULL, 0, FILE_NAME_NORMALIZED);
298 wchar_t *suffix = wide_path(name);
299 wchar_t *path;
300 size_t length;
301
302 if (size == 0 || suffix == NULL) {
303 free(suffix);
304 return NULL;
305 }
306 length = wcslen(suffix);
307 path = malloc(((size_t)size + length + 2) * sizeof(*path));
308 if (path != NULL) {
309 DWORD used = GetFinalPathNameByHandleW(directory->handle, path, size, FILE_NAME_NORMALIZED);
310
311 if (used == 0 || used >= size) {
312 free(path);
313 path = NULL;
314 } else {
315 if (path[used - 1] != L'\\')
316 path[used++] = L'\\';
317 memcpy(path + used, suffix, (length + 1) * sizeof(*path));
318 }
319 }
320 free(suffix);
321 return path;
322 }
323
324 FILE *
325 libmpq__directory_file_open(mpq_directory_s *directory, const char *name, const char *mode)
326 {
327 wchar_t *path;
328 HANDLE handle;
329 FILE *file = NULL;
330
331 if (directory == NULL || name == NULL || name[0] == '\0' || mode == NULL ||
332 strcmp(mode, "rb") != 0) {
333 errno = EINVAL;
334 return NULL;
335 }
336 path = directory_path(directory, name);
337 if (path != NULL) {
338 handle = CreateFileW(
339 path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
340 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL
341 );
342 if (handle != INVALID_HANDLE_VALUE) {
343 BY_HANDLE_FILE_INFORMATION information;
344
345 if (GetFileInformationByHandle(handle, &information) &&
346 (information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
347 file = handle_stream(handle, mode);
348 else
349 CloseHandle(handle);
350 }
351 }
352 free(path);
353 return file;
354 }
355
356 int32_t
357 libmpq__directory_file_validate(mpq_directory_s *directory, const char *name)
358 {
359 wchar_t *path;
360 DWORD attributes;
361
362 if (directory == NULL || name == NULL || name[0] == '\0')
363 return LIBMPQ_ERROR_EXIST;
364 path = directory_path(directory, name);
365 if (path == NULL)
366 return LIBMPQ_ERROR_MALLOC;
367 attributes = GetFileAttributesW(path);
368 free(path);
369 if (attributes == INVALID_FILE_ATTRIBUTES)
370 return LIBMPQ_ERROR_EXIST;
371 if ((attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0)
372 return LIBMPQ_ERROR_FORMAT;
373 return 0;
374 }
375
376 int32_t
377 libmpq__directory_open(const char *path, mpq_directory_s **directory, char **name)
378 {
379 wchar_t *wide;
380 wchar_t *absolute = NULL;
381 wchar_t *basename = NULL;
382 DWORD size;
383 int bytes;
384 HANDLE handle;
385
386 if (directory != NULL)
387 *directory = NULL;
388 if (name != NULL)
389 *name = NULL;
390 if (directory == NULL || name == NULL || path == NULL || path[0] == '\0')
391 return LIBMPQ_ERROR_EXIST;
392 wide = wide_path(path);
393 if (wide == NULL)
394 return LIBMPQ_ERROR_OPEN;
395 size = GetFullPathNameW(wide, 0, NULL, NULL);
396 if (size == 0) {
397 free(wide);
398 return LIBMPQ_ERROR_OPEN;
399 }
400 absolute = malloc((size_t)size * sizeof(*absolute));
401 if (absolute == NULL) {
402 free(wide);
403 return LIBMPQ_ERROR_MALLOC;
404 }
405 if (GetFullPathNameW(wide, size, absolute, &basename) >= size || basename == NULL ||
406 *basename == 0) {
407 free(wide);
408 free(absolute);
409 return LIBMPQ_ERROR_OPEN;
410 }
411 free(wide);
412 bytes = WideCharToMultiByte(CP_UTF8, 0, basename, -1, NULL, 0, NULL, NULL);
413 *name = malloc((size_t)bytes);
414 if (*name == NULL) {
415 free(absolute);
416 return LIBMPQ_ERROR_MALLOC;
417 }
418 WideCharToMultiByte(CP_UTF8, 0, basename, -1, *name, bytes, NULL, NULL);
419 *basename = 0;
420 handle = CreateFileW(
421 absolute, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
422 NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL
423 );
424 free(absolute);
425 if (handle == INVALID_HANDLE_VALUE) {
426 free(*name);
427 *name = NULL;
428 return LIBMPQ_ERROR_OPEN;
429 }
430 *directory = malloc(sizeof(**directory));
431 if (*directory == NULL) {
432 CloseHandle(handle);
433 free(*name);
434 *name = NULL;
435 return LIBMPQ_ERROR_MALLOC;
436 }
437 (*directory)->handle = handle;
438 return 0;
439 }
440
441 int32_t
442 libmpq__directory_remove(mpq_directory_s *directory, const char *name)
443 {
444 wchar_t *path;
445 int32_t result;
446
447 if (directory == NULL || name == NULL || name[0] == '\0')
448 return LIBMPQ_ERROR_EXIST;
449 path = directory_path(directory, name);
450 result = path != NULL && DeleteFileW(path) ? 0 : LIBMPQ_ERROR_WRITE;
451
452 free(path);
453 return result;
454 }
455
456 int32_t
457 libmpq__directory_replace(
458 mpq_directory_s *directory, const char *temporary, const char *destination
459 )
460 {
461 wchar_t *source;
462 wchar_t *target;
463 int32_t result = LIBMPQ_ERROR_WRITE;
464
465 if (directory == NULL || temporary == NULL || destination == NULL || temporary[0] == '\0' ||
466 destination[0] == '\0')
467 return LIBMPQ_ERROR_EXIST;
468 source = directory_path(directory, temporary);
469 target = directory_path(directory, destination);
470 if (source != NULL && target != NULL &&
471 MoveFileExW(source, target, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
472 result = 0;
473 free(source);
474 free(target);
475 return result;
476 }
477
478 int32_t
479 libmpq__directory_copy_security(
480 mpq_directory_s *directory, const char *source_name, const char *destination_name
481 )
482 {
483 PSECURITY_DESCRIPTOR descriptor = NULL;
484 PACL dacl = NULL;
485 wchar_t *source;
486 wchar_t *destination;
487 DWORD error;
488
489 if (directory == NULL || source_name == NULL || destination_name == NULL ||
490 source_name[0] == '\0' || destination_name[0] == '\0')
491 return LIBMPQ_ERROR_EXIST;
492 source = directory_path(directory, source_name);
493 destination = directory_path(directory, destination_name);
494 if (source == NULL || destination == NULL) {
495 free(source);
496 free(destination);
497 return LIBMPQ_ERROR_MALLOC;
498 }
499 error = GetNamedSecurityInfoW(
500 source, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, &dacl, NULL, &descriptor
501 );
502 if (error == ERROR_SUCCESS) {
503 SECURITY_DESCRIPTOR_CONTROL control;
504 DWORD revision;
505 SECURITY_INFORMATION information = DACL_SECURITY_INFORMATION;
506
507 if (!GetSecurityDescriptorControl(descriptor, &control, &revision)) {
508 error = GetLastError();
509 } else {
510 information |= (control & SE_DACL_PROTECTED) != 0
511 ? PROTECTED_DACL_SECURITY_INFORMATION
512 : UNPROTECTED_DACL_SECURITY_INFORMATION;
513 error = SetNamedSecurityInfoW(
514 destination, SE_FILE_OBJECT, information, NULL, NULL, dacl, NULL
515 );
516 }
517 }
518 if (descriptor != NULL)
519 LocalFree(descriptor);
520 free(source);
521 free(destination);
522 return error == ERROR_SUCCESS ? 0 : LIBMPQ_ERROR_WRITE;
523 }
524
525 void
526 libmpq__directory_close(mpq_directory_s *directory)
527 {
528 if (directory != NULL) {
529 CloseHandle(directory->handle);
530 free(directory);
531 }
532 }
533
534 /* Grant only the effective user access; do not inherit directory permissions. */
535 static int
536 owner_descriptor(PSECURITY_DESCRIPTOR *descriptor)
537 {
538 static const wchar_t prefix[] = L"D:P(A;;GA;;;";
539 HANDLE token;
540 DWORD size = 0;
541 TOKEN_USER *user = NULL;
542 wchar_t *sid = NULL;
543 wchar_t *text = NULL;
544 int result = 0;
545
546 if (!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &token)) {
547 if (GetLastError() != ERROR_NO_TOKEN ||
548 !OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
549 return 0;
550 }
551 (void)GetTokenInformation(token, TokenUser, NULL, 0, &size);
552 if (size != 0)
553 user = malloc(size);
554 if (user != NULL && GetTokenInformation(token, TokenUser, user, size, &size) &&
555 ConvertSidToStringSidW(user->User.Sid, &sid)) {
556 size_t prefix_length = sizeof(prefix) / sizeof(*prefix) - 1;
557 size_t sid_length = wcslen(sid);
558
559 text = malloc((prefix_length + sid_length + 2) * sizeof(*text));
560 if (text != NULL) {
561 memcpy(text, prefix, prefix_length * sizeof(*text));
562 memcpy(text + prefix_length, sid, sid_length * sizeof(*text));
563 text[prefix_length + sid_length] = L')';
564 text[prefix_length + sid_length + 1] = 0;
565 result = ConvertStringSecurityDescriptorToSecurityDescriptorW(
566 text, SDDL_REVISION_1, descriptor, NULL
567 ) != 0;
568 }
569 }
570 free(text);
571 if (sid != NULL)
572 LocalFree(sid);
573 free(user);
574 CloseHandle(token);
575 return result;
576 }
577
578 static int32_t
579 directory_temporary(
580 mpq_directory_s *directory, const char *suffix, int private_file, int reopenable, char **name,
581 FILE **file
582 )
583 {
584 static const char hex[] = "0123456789abcdef";
585 uint8_t random[16];
586 char *candidate;
587 size_t length;
588 unsigned attempt;
589 PSECURITY_DESCRIPTOR descriptor = NULL;
590 SECURITY_ATTRIBUTES security = { sizeof(security), NULL, FALSE };
591
592 if (name != NULL)
593 *name = NULL;
594 if (file != NULL)
595 *file = NULL;
596 if (directory == NULL || name == NULL || file == NULL || !valid_template(suffix))
597 return LIBMPQ_ERROR_EXIST;
598 length = strlen(suffix);
599 candidate = libmpq__string_duplicate(suffix);
600 if (candidate == NULL)
601 return LIBMPQ_ERROR_MALLOC;
602
603 /* Protected owner-only DACL: plaintext must not inherit directory grants. */
604 if (private_file && !owner_descriptor(&descriptor)) {
605 free(candidate);
606 return LIBMPQ_ERROR_OPEN;
607 }
608 security.lpSecurityDescriptor = descriptor;
609 for (attempt = 0; attempt < 128; ++attempt) {
610 wchar_t *path;
611 HANDLE handle;
612 DWORD error;
613 size_t i;
614
615 if (BCryptGenRandom(NULL, random, sizeof(random), BCRYPT_USE_SYSTEM_PREFERRED_RNG) != 0)
616 break;
617 for (i = 0; i < 32; ++i)
618 candidate[length - 32 + i] = hex[(i & 1) ? random[i / 2] & 15 : random[i / 2] >> 4];
619 path = directory_path(directory, candidate);
620 if (path == NULL)
621 break;
622 handle = CreateFileW(
623 path, GENERIC_READ | GENERIC_WRITE,
624 reopenable ? FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE : 0,
625 private_file ? &security : NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL
626 );
627 error = GetLastError();
628 if (handle != INVALID_HANDLE_VALUE) {
629 *file = handle_stream(handle, "w+b");
630 if (*file == NULL)
631 DeleteFileW(path);
632 free(path);
633 break;
634 }
635 free(path);
636 if (error != ERROR_FILE_EXISTS && error != ERROR_ALREADY_EXISTS)
637 break;
638 }
639 if (descriptor != NULL)
640 LocalFree(descriptor);
641 if (*file == NULL) {
642 free(candidate);
643 return LIBMPQ_ERROR_OPEN;
644 }
645 *name = candidate;
646 return 0;
647 }
648
649 int32_t
650 libmpq__directory_temporary(
651 mpq_directory_s *directory, const char *suffix, int private_file, char **name, FILE **file
652 )
653 {
654 return directory_temporary(directory, suffix, private_file, 0, name, file);
655 }
656
657 int32_t
658 libmpq__directory_temporary_reopenable(
659 mpq_directory_s *directory, const char *suffix, int private_file, char **name, FILE **file
660 )
661 {
662 return directory_temporary(directory, suffix, private_file, 1, name, file);
663 }
664
665 #else
666
667 struct mpq_directory
668 {
669 int fd;
670 };
671
672 #ifndef O_CLOEXEC
673 static int set_cloexec(int fd);
674 #endif
675
676 FILE *
677 122 libmpq__directory_file_open(mpq_directory_s *directory, const char *name, const char *mode)
678 {
679 int file;
680 122 int flags = O_RDONLY;
681 FILE *stream;
682
683
4/8
✓ Branch 0 taken 122 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 122 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 122 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 122 times.
✗ Branch 7 not taken.
122 if (directory == NULL || name == NULL || name[0] == '\0' || mode == NULL ||
684
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 122 times.
122 strcmp(mode, "rb") != 0) {
685 ✗ errno = EINVAL;
686 ✗ return NULL;
687 }
688 #ifdef O_CLOEXEC
689 122 flags |= O_CLOEXEC;
690 #endif
691 #ifdef O_NOFOLLOW
692 122 flags |= O_NOFOLLOW;
693 #endif
694 122 file = openat(directory->fd, name, flags);
695
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 95 times.
122 if (file < 0)
696 27 return NULL;
697 #ifndef O_CLOEXEC
698 if (!set_cloexec(file)) {
699 (void)close(file);
700 return NULL;
701 }
702 #endif
703 95 stream = fdopen(file, mode);
704
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 95 times.
95 if (stream == NULL) {
705 ✗ (void)close(file);
706 ✗ return NULL;
707 }
708 95 return stream;
709 }
710
711 int32_t
712 60 libmpq__directory_file_validate(mpq_directory_s *directory, const char *name)
713 {
714 struct stat status;
715
716
3/6
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 60 times.
60 if (directory == NULL || name == NULL || name[0] == '\0')
717 ✗ return LIBMPQ_ERROR_EXIST;
718
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
60 if (fstatat(directory->fd, name, &status, AT_SYMLINK_NOFOLLOW) != 0)
719 ✗ return LIBMPQ_ERROR_EXIST;
720
2/2
✓ Branch 0 taken 59 times.
✓ Branch 1 taken 1 times.
60 return S_ISREG(status.st_mode) ? 0 : LIBMPQ_ERROR_FORMAT;
721 }
722
723 FILE *
724 2092 libmpq__file_open(const char *path, const char *mode)
725 {
726
6/8
✓ Branch 0 taken 2091 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 2091 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2090 times.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 2090 times.
2092 if (path == NULL || path[0] == '\0' || mode == NULL || mode[0] == '\0') {
727 2 errno = EINVAL;
728 2 return NULL;
729 }
730 2090 return fopen(path, mode);
731 }
732
733 int32_t
734 1946 libmpq__file_identity(FILE *file, uint64_t *device, uint64_t *inode)
735 {
736 struct stat status;
737
738
1/2
✓ Branch 0 taken 1946 times.
✗ Branch 1 not taken.
1946 if (device != NULL)
739 1946 *device = 0;
740
1/2
✓ Branch 0 taken 1946 times.
✗ Branch 1 not taken.
1946 if (inode != NULL)
741 1946 *inode = 0;
742
4/6
✓ Branch 0 taken 1945 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1945 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1945 times.
1946 if (file == NULL || device == NULL || inode == NULL)
743 1 return LIBMPQ_ERROR_EXIST;
744
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 1945 times.
1945 if (fstat(fileno(file), &status) != 0)
745 ✗ return LIBMPQ_ERROR_OPEN;
746 1945 *device = (uint64_t)status.st_dev;
747 1945 *inode = (uint64_t)status.st_ino;
748 1945 return 0;
749 }
750
751 int32_t
752 621 libmpq__directory_remove(mpq_directory_s *directory, const char *name)
753 {
754
5/6
✓ Branch 0 taken 620 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 619 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 619 times.
621 if (directory == NULL || name == NULL || name[0] == '\0')
755 2 return LIBMPQ_ERROR_EXIST;
756
2/2
✓ Branch 1 taken 461 times.
✓ Branch 2 taken 158 times.
619 return unlinkat(directory->fd, name, 0) == 0 ? 0 : LIBMPQ_ERROR_WRITE;
757 }
758
759 void
760 266 libmpq__directory_close(mpq_directory_s *directory)
761 {
762
1/2
✓ Branch 0 taken 266 times.
✗ Branch 1 not taken.
266 if (directory != NULL) {
763 266 close(directory->fd);
764 266 free(directory);
765 }
766 266 }
767
768 /* Mark a descriptor close-on-exec when atomic creation did not provide it. */
769 #ifndef O_CLOEXEC
770 static int
771 set_cloexec(int fd)
772 {
773 int flags = fcntl(fd, F_GETFD);
774
775 return flags >= 0 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == 0;
776 }
777 #endif
778
779 /* Fill a short temporary suffix from the operating system random source. */
780 static int32_t
781 590 random_bytes(uint8_t *buffer, size_t size)
782 {
783 int fd;
784 590 size_t offset = 0;
785
786 #ifdef O_CLOEXEC
787 590 fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
788 #else
789 fd = open("/dev/urandom", O_RDONLY);
790 #endif
791
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 590 times.
590 if (fd < 0)
792 ✗ return LIBMPQ_ERROR_OPEN;
793 #ifndef O_CLOEXEC
794 if (!set_cloexec(fd)) {
795 (void)close(fd);
796 return LIBMPQ_ERROR_OPEN;
797 }
798 #endif
799
2/2
✓ Branch 0 taken 590 times.
✓ Branch 1 taken 590 times.
1180 while (offset < size) {
800 590 ssize_t read_size = read(fd, buffer + offset, size - offset);
801
802
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 590 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
590 if (read_size < 0 && errno == EINTR)
803 ✗ continue;
804
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 590 times.
590 if (read_size <= 0) {
805 ✗ (void)close(fd);
806 ✗ return LIBMPQ_ERROR_OPEN;
807 }
808 590 offset += (size_t)read_size;
809 }
810
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 590 times.
590 if (close(fd) != 0)
811 ✗ return LIBMPQ_ERROR_OPEN;
812 590 return 0;
813 }
814
815 /* Create one temporary file using an unpredictable O_EXCL basename. */
816 static int32_t
817 590 temporary_output_file(
818 int directory, char *path, size_t random_offset, size_t random_size, mode_t mode, int *file
819 )
820 {
821 static const char hex[] = "0123456789abcdef";
822 590 uint8_t random[16] = { 0 };
823 size_t i;
824 uint32_t attempt;
825 590 int flags = O_RDWR | O_CREAT | O_EXCL;
826
827
2/4
✓ Branch 0 taken 590 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 590 times.
590 if (random_size == 0 || random_size > 2U * sizeof(random))
828 ✗ return LIBMPQ_ERROR_EXIST;
829 #ifdef O_CLOEXEC
830 590 flags |= O_CLOEXEC;
831 #endif
832
1/2
✓ Branch 0 taken 590 times.
✗ Branch 1 not taken.
590 for (attempt = 0; attempt < 128U; ++attempt) {
833 590 int32_t result = random_bytes(random, (random_size + 1U) / 2U);
834
835
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 590 times.
590 if (result != 0)
836 ✗ return result;
837
2/2
✓ Branch 0 taken 18880 times.
✓ Branch 1 taken 590 times.
19470 for (i = 0; i < random_size; ++i) {
838 18880 uint8_t byte = random[i / 2U];
839
840
2/2
✓ Branch 0 taken 9440 times.
✓ Branch 1 taken 9440 times.
18880 path[random_offset + i] = hex[(i & 1U) == 0 ? byte >> 4 : byte & 0x0fU];
841 }
842 590 *file = openat(directory, path, flags, mode);
843
1/2
✓ Branch 0 taken 590 times.
✗ Branch 1 not taken.
590 if (*file >= 0) {
844 #ifndef O_CLOEXEC
845 if (!set_cloexec(*file)) {
846 (void)close(*file);
847 (void)unlinkat(directory, path, 0);
848 return LIBMPQ_ERROR_OPEN;
849 }
850 #endif
851 590 return 0;
852 }
853 ✗ if (errno != EEXIST)
854 ✗ return LIBMPQ_ERROR_OPEN;
855 }
856 ✗ return LIBMPQ_ERROR_OPEN;
857 }
858
859 /* Split a destination once and retain its directory descriptor for later publication. */
860 int32_t
861 269 libmpq__directory_open(const char *destination, mpq_directory_s **directory, char **name)
862 {
863 const char *slash;
864 const char *directory_path;
865 269 char *allocated_directory = NULL;
866 char *destination_name;
867 size_t directory_size;
868 269 int flags = O_RDONLY;
869 int fd;
870 struct stat status;
871
872
2/2
✓ Branch 0 taken 268 times.
✓ Branch 1 taken 1 times.
269 if (directory != NULL)
873 268 *directory = NULL;
874
2/2
✓ Branch 0 taken 268 times.
✓ Branch 1 taken 1 times.
269 if (name != NULL)
875 268 *name = NULL;
876
7/8
✓ Branch 0 taken 268 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 268 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 267 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 266 times.
269 if (destination == NULL || destination[0] == '\0' || directory == NULL || name == NULL)
877 3 return LIBMPQ_ERROR_EXIST;
878 266 slash = (strrchr)(destination, '/');
879
2/2
✓ Branch 0 taken 75 times.
✓ Branch 1 taken 191 times.
266 if (slash == NULL) {
880 75 directory_path = ".";
881 75 destination_name = libmpq__string_duplicate(destination);
882 } else {
883 191 directory_size = (size_t)(slash - destination);
884
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 191 times.
191 if (slash[1] == '\0')
885 ✗ return LIBMPQ_ERROR_EXIST;
886
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 191 times.
191 if (directory_size == 0) {
887 ✗ directory_path = "/";
888 } else {
889 191 allocated_directory = malloc(directory_size + 1U);
890
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 191 times.
191 if (allocated_directory == NULL)
891 ✗ return LIBMPQ_ERROR_MALLOC;
892 191 memcpy(allocated_directory, destination, directory_size);
893 191 allocated_directory[directory_size] = '\0';
894 191 directory_path = allocated_directory;
895 }
896 191 destination_name = libmpq__string_duplicate(slash + 1);
897 }
898
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 266 times.
266 if (destination_name == NULL) {
899 ✗ free(allocated_directory);
900 ✗ return LIBMPQ_ERROR_MALLOC;
901 }
902 #ifdef O_DIRECTORY
903 266 flags |= O_DIRECTORY;
904 #endif
905 #ifdef O_CLOEXEC
906 266 flags |= O_CLOEXEC;
907 #endif
908 266 fd = open(directory_path, flags);
909 266 free(allocated_directory);
910
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 266 times.
266 if (fd < 0) {
911 ✗ free(destination_name);
912 ✗ return LIBMPQ_ERROR_OPEN;
913 }
914 #ifndef O_CLOEXEC
915 if (!set_cloexec(fd)) {
916 (void)close(fd);
917 free(destination_name);
918 return LIBMPQ_ERROR_OPEN;
919 }
920 #endif
921
2/4
✓ Branch 1 taken 266 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 266 times.
266 if (fstat(fd, &status) != 0 || !S_ISDIR(status.st_mode)) {
922 ✗ (void)close(fd);
923 ✗ free(destination_name);
924 ✗ return LIBMPQ_ERROR_OPEN;
925 }
926 266 *directory = malloc(sizeof(**directory));
927
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 266 times.
266 if (*directory == NULL) {
928 ✗ close(fd);
929 ✗ free(destination_name);
930 ✗ return LIBMPQ_ERROR_MALLOC;
931 }
932 266 (*directory)->fd = fd;
933 266 *name = destination_name;
934 266 return 0;
935 }
936
937 /* Securely create one private temporary file below an already-opened directory. */
938 int32_t
939 596 libmpq__directory_temporary(
940 mpq_directory_s *directory, const char *suffix, int private_file, char **path, FILE **file
941 )
942 {
943 char *template_path;
944 size_t suffix_size;
945 int32_t result;
946 int fd;
947
948
2/2
✓ Branch 0 taken 595 times.
✓ Branch 1 taken 1 times.
596 if (path != NULL)
949 595 *path = NULL;
950
2/2
✓ Branch 0 taken 595 times.
✓ Branch 1 taken 1 times.
596 if (file != NULL)
951 595 *file = NULL;
952
8/8
✓ Branch 0 taken 595 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 594 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 593 times.
✓ Branch 5 taken 1 times.
✓ Branch 7 taken 3 times.
✓ Branch 8 taken 590 times.
596 if (directory == NULL || path == NULL || file == NULL || !valid_template(suffix))
953 6 return LIBMPQ_ERROR_EXIST;
954 590 suffix_size = strlen(suffix);
955 590 template_path = libmpq__string_duplicate(suffix);
956
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 590 times.
590 if (template_path == NULL)
957 ✗ return LIBMPQ_ERROR_MALLOC;
958
2/2
✓ Branch 0 taken 504 times.
✓ Branch 1 taken 86 times.
590 result = temporary_output_file(
959 directory->fd, template_path, suffix_size - 32U, 32U, private_file ? 0600 : 0666, &fd
960 );
961
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 590 times.
590 if (result != 0) {
962 ✗ free(template_path);
963 ✗ return result;
964 }
965 590 *file = fdopen(fd, "w+b");
966
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 590 times.
590 if (*file == NULL) {
967 ✗ (void)close(fd);
968 ✗ (void)unlinkat(directory->fd, template_path, 0);
969 ✗ free(template_path);
970 ✗ return LIBMPQ_ERROR_OPEN;
971 }
972 590 *path = template_path;
973 590 return 0;
974 }
975
976 int32_t
977 391 libmpq__directory_temporary_reopenable(
978 mpq_directory_s *directory, const char *suffix, int private_file, char **path, FILE **file
979 )
980 {
981 391 return libmpq__directory_temporary(directory, suffix, private_file, path, file);
982 }
983
984 /*
985 * Publish a complete same-directory encrypted temporary output. On POSIX,
986 * renameat is the atomic replacement primitive: observers see either the old
987 * destination or the complete encrypted replacement, never a partial file.
988 */
989 int32_t
990 136 libmpq__directory_replace(
991 mpq_directory_s *directory, const char *temporary, const char *destination
992 )
993 {
994
7/8
✓ Branch 0 taken 135 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 134 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 133 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 133 times.
✗ Branch 7 not taken.
136 if (directory == NULL || temporary == NULL || destination == NULL || temporary[0] == '\0' ||
995
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 133 times.
133 destination[0] == '\0')
996 3 return LIBMPQ_ERROR_EXIST;
997 133 return renameat(directory->fd, temporary, directory->fd, destination) == 0 ? 0
998
1/2
✓ Branch 0 taken 133 times.
✗ Branch 1 not taken.
133 : LIBMPQ_ERROR_WRITE;
999 }
1000
1001 int32_t
1002 ✗ libmpq__directory_copy_security(
1003 mpq_directory_s *directory, const char *source, const char *destination
1004 )
1005 {
1006 ✗ if (directory == NULL || source == NULL || destination == NULL || source[0] == '\0' ||
1007 ✗ destination[0] == '\0')
1008 ✗ return LIBMPQ_ERROR_EXIST;
1009 ✗ return 0;
1010 }
1011
1012 #endif
1013