Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_files.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
27
28#include "nilorea/n_files.h"
29
30#include "nilorea/n_common.h" /* Malloc/Free, TRUE/FALSE, __windows__ */
31
32#include <errno.h>
33#include <stdio.h>
34#include <string.h>
35
36#ifdef __windows__
37#include "nilorea/n_windows.h" /* MoveFileExA */
38#include <io.h> /* _commit */
39#else
40#include <fcntl.h> /* open, O_* */
41#include <sys/stat.h> /* fchmod, mode_t */
42#include <unistd.h> /* fsync, fchmod */
43#endif
44
62
63#include <stdio.h>
64#include <stdlib.h>
65#include <string.h>
66#include <stdint.h>
67#include <dirent.h>
68#include <sys/stat.h>
69#include <errno.h>
70
76void n_free_file_info(void* ptr) {
77 __n_assert(ptr, return);
78 N_FILE_INFO* file = (N_FILE_INFO*)ptr;
79 if (file->name)
80 free(file->name);
81 free(file);
82 return;
83}
84
94char* n_path_join(const char* dir, const char* name) {
95 if (!dir || !name) return NULL;
96
97 size_t dl = strlen(dir);
98 size_t nl = strlen(name);
99
100 int need_sep = 1;
101 if (dl == 0) {
102 need_sep = 0;
103 } else {
104 char last = dir[dl - 1];
105 if (last == '/' || last == '\\') need_sep = 0;
106 }
107
108 /* We use '/' for output paths. */
109 size_t out_len = dl + (need_sep ? 1 : 0) + nl + 1;
110 char* out = (char*)malloc(out_len);
111 if (!out) return NULL;
112
113 if (need_sep)
114 snprintf(out, out_len, "%s/%s", dir, name);
115 else
116 snprintf(out, out_len, "%s%s", dir, name);
117
118 return out;
119}
120
137int n_comp_file_info(const void* a, const void* b) {
138 const N_FILE_INFO* f1 = (const N_FILE_INFO*)a;
139 const N_FILE_INFO* f2 = (const N_FILE_INFO*)b;
140
141 if (!f1) return -1;
142 if (!f2) return 1;
143
144 if (f1->filetime < f2->filetime) return -1;
145 if (f1->filetime > f2->filetime) return 1;
146
147 if (f1->filetime_nsec < f2->filetime_nsec) return -1;
148 if (f1->filetime_nsec > f2->filetime_nsec) return 1;
149
150 return 0;
151}
152
162void n_set_time_from_stat(N_FILE_INFO* file, const struct stat* st) {
163 if (!file || !st) return;
164
165#if defined(__linux__) || defined(__sun)
166 file->filetime = (int64_t)st->st_mtim.tv_sec;
167 file->filetime_nsec = (int32_t)st->st_mtim.tv_nsec;
168#else
169 file->filetime = (int64_t)st->st_mtime;
170 file->filetime_nsec = 0;
171#endif
172}
173
196int n_scan_dir(const char* dir, LIST* result, const int recurse) {
197 DIR* dp = NULL;
198 const struct dirent* entry = NULL;
199
200 __n_assert(dir, return FALSE);
201 __n_assert(result, return FALSE);
202
203 int error = 0;
204 dp = opendir(dir);
205 error = errno;
206 if (!dp) {
207 n_log(LOG_ERR, "cannot open directory: %s, %s", dir, strerror(error));
208 return FALSE;
209 }
210
211 while ((entry = readdir(dp)) != NULL) {
212 const char* name = entry->d_name;
213
214 if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
215 errno = 0;
216 continue;
217 }
218
219 char* fullpath = n_path_join(dir, name);
220 if (!fullpath) {
221 n_log(LOG_ERR, "malloc failed building path for %s/%s", dir, name);
222 closedir(dp);
223 return FALSE;
224 }
225
226 struct stat st;
227 if (stat(fullpath, &st) == -1) {
228 error = errno;
229 n_log(LOG_ERR, "unable to stat %s, %s", fullpath, strerror(error));
230 free(fullpath);
231 continue;
232 }
233
234 if (S_ISDIR(st.st_mode)) {
235 if (recurse != FALSE) {
236 if (n_scan_dir(fullpath, result, recurse) == FALSE) {
237 n_log(LOG_ERR, "error while recursively scanning %s", fullpath);
238 }
239 }
240 } else if (S_ISREG(st.st_mode)) {
241 N_FILE_INFO* file = NULL;
242 Malloc(file, N_FILE_INFO, 1);
243
244 if (file) {
245 /* Store full path to avoid ambiguity in recursion results */
246 file->name = strdup(fullpath);
247 n_set_time_from_stat(file, &st);
248
250 }
251 } else {
252 /* Ignore non-regular / non-directory entries (symlinks, sockets, etc.) */
253 /* If you want to include symlinks, use lstat() and handle S_ISLNK. */
254 }
255
256 free(fullpath);
257 errno = 0;
258 }
259
260 if (errno != 0) {
261 n_log(LOG_ERR, "readdir failed for %s: %s", dir, strerror(errno));
262 closedir(dp);
263 return FALSE;
264 }
265
266 closedir(dp);
267 return TRUE;
268}
269
270int n_file_read_bounded(const char* path, size_t max_size, unsigned char** out, size_t* out_len) {
271 __n_assert(path, return FALSE);
272 __n_assert(out, return FALSE);
273
274 *out = NULL;
275 if (out_len) *out_len = 0;
276
277 FILE* f = fopen(path, "rb");
278 if (!f) {
279 n_log(LOG_ERR, "n_file_read_bounded: cannot open %s: %s", path, strerror(errno));
280 return FALSE;
281 }
282 if (fseek(f, 0, SEEK_END) != 0) {
283 fclose(f);
284 return FALSE;
285 }
286 long sz = ftell(f);
287 if (sz < 0) {
288 fclose(f);
289 return FALSE;
290 }
291 if ((size_t)sz > max_size) {
292 n_log(LOG_ERR, "n_file_read_bounded: %s is %ld bytes, over the %zu cap", path, sz, max_size);
293 fclose(f);
294 return FALSE;
295 }
296 /* not rewind(): it returns void and reports failure only by setting errno, so a
297 failed seek would go unnoticed and leave the read at the wrong offset. fseek
298 reports the same failure by return value. */
299 if (fseek(f, 0, SEEK_SET) != 0) {
300 n_log(LOG_ERR, "n_file_read_bounded: cannot seek back to the start of %s: %s", path, strerror(errno));
301 fclose(f);
302 return FALSE;
303 }
304
305 size_t len = (size_t)sz;
306 unsigned char* buf = NULL;
307 Malloc(buf, unsigned char, len + 1);
308 if (!buf) {
309 fclose(f);
310 return FALSE;
311 }
312 if (len && fread(buf, 1, len, f) != len) {
313 n_log(LOG_ERR, "n_file_read_bounded: short read on %s", path);
314 Free(buf);
315 fclose(f);
316 return FALSE;
317 }
318 buf[len] = '\0';
319 fclose(f);
320
321 *out = buf;
322 if (out_len) *out_len = len;
323 return TRUE;
324}
325
327#define N_FILE_COPY_BLOCK 262144
328
335static int n_file_streams_equal(FILE* a, FILE* b) {
336 unsigned char* x = NULL;
337 unsigned char* y = NULL;
338 int same = TRUE;
339
340 Malloc(x, unsigned char, N_FILE_COPY_BLOCK);
341 Malloc(y, unsigned char, N_FILE_COPY_BLOCK);
342 if (!x || !y) {
343 Free(x);
344 Free(y);
345 return FALSE;
346 }
347 for (;;) {
348 size_t got_a = fread(x, 1, N_FILE_COPY_BLOCK, a);
349 size_t got_b = fread(y, 1, N_FILE_COPY_BLOCK, b);
350
351 if (got_a != got_b || (got_a && memcmp(x, y, got_a) != 0)) {
352 same = FALSE;
353 break;
354 }
355 if (got_a < N_FILE_COPY_BLOCK) {
356 /* Both ended together; an error on either is not "equal". */
357 if (ferror(a) || ferror(b)) same = FALSE;
358 break;
359 }
360 }
361 Free(x);
362 Free(y);
363 return same;
364}
365
366int n_file_same_content(const char* a, const char* b) {
367 FILE* fa = NULL;
368 FILE* fb = NULL;
369 int same = FALSE;
370
371 __n_assert(a, return FALSE);
372 __n_assert(b, return FALSE);
373 fa = fopen(a, "rb");
374 fb = fa ? fopen(b, "rb") : NULL;
375 if (fa && fb) same = n_file_streams_equal(fa, fb);
376 if (fa) fclose(fa);
377 if (fb) fclose(fb);
378 return same;
379}
380
381int n_file_copy_verified(const char* from, const char* to, size_t* copied) {
382 FILE* in = NULL;
383 FILE* out = NULL;
384 unsigned char* block = NULL;
385 char* part = NULL;
386 size_t plen = 0, total = 0;
387 int ok = TRUE;
388
389 __n_assert(from, return FALSE);
390 __n_assert(to, return FALSE);
391 if (copied) *copied = 0;
392 /* Never over a file that is there: the caller decides what an existing one is. */
393 in = fopen(to, "rb");
394 if (in) {
395 fclose(in);
396 n_log(LOG_ERR, "n_file_copy_verified: %s exists, not replaced", to);
397 return FALSE;
398 }
399 plen = strlen(to);
400 Malloc(part, char, plen + 6); /* ".part" + NUL */
401 Malloc(block, unsigned char, N_FILE_COPY_BLOCK);
402 if (!part || !block) {
403 Free(part);
404 Free(block);
405 return FALSE;
406 }
407 memcpy(part, to, plen);
408 memcpy(part + plen, ".part", 6);
409
410 in = fopen(from, "rb");
411 out = in ? fopen(part, "wb") : NULL;
412 if (!in || !out) {
413 n_log(LOG_ERR, "n_file_copy_verified: cannot open %s: %s", in ? part : from, strerror(errno));
414 ok = FALSE;
415 }
416 while (ok) {
417 size_t got = fread(block, 1, N_FILE_COPY_BLOCK, in);
418
419 if (got && fwrite(block, 1, got, out) != got) {
420 n_log(LOG_ERR, "n_file_copy_verified: writing %s: %s", part, strerror(errno));
421 ok = FALSE;
422 break;
423 }
424 total += got;
425 if (got < N_FILE_COPY_BLOCK) {
426 if (ferror(in)) {
427 n_log(LOG_ERR, "n_file_copy_verified: reading %s: %s", from, strerror(errno));
428 ok = FALSE;
429 }
430 break;
431 }
432 }
433 if (out) {
434 if (ok && fflush(out) != 0) ok = FALSE;
435 if (ok) {
436 int fd = fileno(out);
437#ifdef __windows__
438 if (fd >= 0) (void)_commit(fd);
439#else
440 if (fd >= 0) (void)fsync(fd);
441#endif
442 }
443 if (fclose(out) != 0) ok = FALSE;
444 out = NULL;
445 }
446 if (in) {
447 fclose(in);
448 in = NULL;
449 }
450 /* Read back from disk, not from what was meant to be written. */
451 if (ok && !n_file_same_content(from, part)) {
452 n_log(LOG_ERR, "n_file_copy_verified: the copy of %s does not match it", from);
453 ok = FALSE;
454 }
455 if (ok && rename(part, to) != 0) {
456 n_log(LOG_ERR, "n_file_copy_verified: cannot name %s: %s", to, strerror(errno));
457 ok = FALSE;
458 }
459 if (!ok) remove(part);
460 if (ok && copied) *copied = total;
461 Free(part);
462 Free(block);
463 return ok;
464}
465
466int n_file_write_atomic(const char* path, const unsigned char* data, size_t len, int mode) {
467 __n_assert(path, return FALSE);
468 if (!data && len > 0) {
469 n_log(LOG_ERR, "n_file_write_atomic: NULL data with len %zu", len);
470 return FALSE;
471 }
472
473 size_t plen = strlen(path);
474 char* tmp = NULL;
475 Malloc(tmp, char, plen + 5); /* ".tmp" + NUL */
476 __n_assert(tmp, return FALSE);
477 memcpy(tmp, path, plen);
478 memcpy(tmp + plen, ".tmp", 5);
479
480 /* the temporary carries the final mode from the moment it exists. Creating it with the
481 * default mode and chmod'ing it afterwards leaves the content readable by anyone between
482 * the write and the chmod, which matters because callers use this to persist secrets. */
483#ifdef __windows__
484 FILE* f = fopen(tmp, "wb");
485#else
486 int tmpfd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, (mode != 0) ? (mode_t)mode : (mode_t)0666);
487 FILE* f = (tmpfd >= 0) ? fdopen(tmpfd, "wb") : NULL;
488 if (!f && tmpfd >= 0) close(tmpfd);
489#endif
490 if (!f) {
491 n_log(LOG_ERR, "n_file_write_atomic: cannot create %s: %s", tmp, strerror(errno));
492 Free(tmp);
493 return FALSE;
494 }
495
496 int ok = TRUE;
497 if (len && fwrite(data, 1, len, f) != len) ok = FALSE;
498 if (ok && fflush(f) != 0) ok = FALSE;
499 if (ok) {
500 int fd = fileno(f);
501 if (fd >= 0) {
502#ifdef __windows__
503 (void)_commit(fd); /* FlushFileBuffers under the hood */
504 (void)mode; /* no POSIX mode bits on Windows */
505#else
506 (void)fsync(fd);
507 /* a temporary left behind by an earlier crash keeps the mode it was created
508 * with, so the mode is still asserted here rather than trusted from open() */
509 if (mode != 0 && fchmod(fd, (mode_t)mode) != 0) ok = FALSE;
510#endif
511 }
512 }
513 if (fclose(f) != 0) ok = FALSE;
514
515 if (ok) {
516#ifdef __windows__
517 /* Windows rename() fails if the target exists; MoveFileEx replaces it
518 * atomically on NTFS, WRITE_THROUGH for durability. */
519 ok = MoveFileExA(tmp, path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) ? TRUE : FALSE;
520#else
521 ok = (rename(tmp, path) == 0) ? TRUE : FALSE;
522#endif
523 }
524 if (!ok) {
525 remove(tmp);
526 Free(tmp);
527 return FALSE;
528 }
529 Free(tmp);
530 return TRUE;
531}
static N_GUI_MOUSE_EVENT last
the last event the widget was told of, and how many
static int mode
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:204
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:279
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:263
int list_push_sorted(LIST *list, void *ptr, int(*comparator)(const void *a, const void *b), void(*destructor)(void *ptr))
Add a pointer sorted in the list , starting by the end of the list.
Definition n_list.c:261
Structure of a generic LIST container.
Definition n_list.h:59
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
time_t filetime
file creation time
Definition n_files.h:50
time_t filetime_nsec
Definition n_files.h:50
char * name
file name
Definition n_files.h:48
int n_file_same_content(const char *a, const char *b)
Do two files hold the same bytes.
Definition n_files.c:366
int n_file_write_atomic(const char *path, const unsigned char *data, size_t len, int mode)
durably and atomically write len bytes to path: write a "<path>.tmp" sibling created with mode from t...
Definition n_files.c:466
int n_file_copy_verified(const char *from, const char *to, size_t *copied)
Copy a file, check the copy against the original, and only then give it its name.
Definition n_files.c:381
int n_file_read_bounded(const char *path, size_t max_size, unsigned char **out, size_t *out_len)
read a whole file into a freshly allocated, NUL-terminated buffer, refusing a file larger than max_si...
Definition n_files.c:270
int n_scan_dir(const char *dir, LIST *result, const int recurse)
Scan a directory and append only regular files into a sorted list (oldest first).
Definition n_files.c:196
void n_free_file_info(void *ptr)
Cross-platform directory scanning (Linux / Solaris / Windows via MinGW) WITHOUT chdir().
Definition n_files.c:76
common file information
Definition n_files.h:46
Common headers and low-level functions & define.
void n_set_time_from_stat(N_FILE_INFO *file, const struct stat *st)
Fill filetime and filetime_nsec from a struct stat (mtime).
Definition n_files.c:162
char * n_path_join(const char *dir, const char *name)
Build "dir + '/' + name" (or without extra slash if dir already ends with '/' or '\').
Definition n_files.c:94
#define N_FILE_COPY_BLOCK
Block size for the streaming copy and compare.
Definition n_files.c:327
static int n_file_streams_equal(FILE *a, FILE *b)
Compare two open streams to their ends.
Definition n_files.c:335
int n_comp_file_info(const void *a, const void *b)
Comparison function for sorting N_FILE_INFO by time (oldest first).
Definition n_files.c:137
Files configuration header.