src/n_files.c
Cross-platform directory scanning (Linux/Solaris/Windows), optionnal recursion. More...
Functions
| Name | |
|---|---|
| void | n_set_time_from_stat(N_FILE_INFO * file, const struct stat * st) Fill filetime and filetime_nsec from a struct stat (mtime). |
| char * | n_path_join(const char * dir, const char * name) Build "dir + '/' + name" (or without extra slash if dir already ends with '/' or ''). |
| int | n_file_streams_equal(FILE * a, FILE * b) Compare two open streams to their ends. |
| int | n_comp_file_info(const void * a, const void * b) Comparison function for sorting N_FILE_INFO by time (oldest first). |
Defines
| Name | |
|---|---|
| N_FILE_COPY_BLOCK Block size for the streaming copy and compare. |
Detailed Description
Cross-platform directory scanning (Linux/Solaris/Windows), optionnal recursion.
Author: Castagnier Mickael
Version: 1.0
Date: 02/11/23
Functions Documentation
function n_set_time_from_stat
void n_set_time_from_stat(
N_FILE_INFO * file,
const struct stat * st
)
Fill filetime and filetime_nsec from a struct stat (mtime).
Parameters:
- file N_FILE_INFO to fill (non-NULL)
- st struct stat source (non-NULL)
On Linux and Solaris, uses st_mtim (seconds + nanoseconds). On other POSIX environments, falls back to st_mtime (seconds only).
function n_path_join
char * n_path_join(
const char * dir,
const char * name
)
Build "dir + '/' + name" (or without extra slash if dir already ends with '/' or '').
Parameters:
- dir Base directory path (non-NULL)
- name Entry name (non-NULL)
Return: Newly allocated full path (free() by caller), or NULL on error.
Uses '/' as a separator for portability. Windows (MinGW) accepts forward slashes.
function n_file_streams_equal
static int n_file_streams_equal(
FILE * a,
FILE * b
)
Compare two open streams to their ends.
Parameters:
- a first stream, positioned at its start
- b second stream, positioned at its start
Return: TRUE when they hold the same bytes
function n_comp_file_info
int n_comp_file_info(
const void * a,
const void * b
)
Comparison function for sorting N_FILE_INFO by time (oldest first).
Parameters:
- a Pointer to first N_FILE_INFO
- b Pointer to second N_FILE_INFO
Return: Ordering result.
Orders by: 1) filetime (seconds) ascending 2) filetime_nsec (nanoseconds) ascending
Return values:
- < 0 if a is older than b
- = 0 if equal timestamps
-
0 if a is newer than b
Macros Documentation
define N_FILE_COPY_BLOCK
#define N_FILE_COPY_BLOCK 262144
Block size for the streaming copy and compare.
Source code
/*
* Nilorea Library
* Copyright (C) 2005-2026 Castagnier Mickael
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "nilorea/n_files.h"
#include "nilorea/n_common.h" /* Malloc/Free, TRUE/FALSE, __windows__ */
#include <errno.h>
#include <stdio.h>
#include <string.h>
#ifdef __windows__
#include "nilorea/n_windows.h" /* MoveFileExA */
#include <io.h> /* _commit */
#else
#include <fcntl.h> /* open, O_* */
#include <sys/stat.h> /* fchmod, mode_t */
#include <sys/statvfs.h> /* statvfs */
#include <unistd.h> /* fsync, fchmod */
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <dirent.h>
#include <sys/stat.h>
#include <errno.h>
void n_free_file_info(void* ptr) {
__n_assert(ptr, return);
N_FILE_INFO* file = (N_FILE_INFO*)ptr;
if (file->name)
free(file->name);
free(file);
return;
}
char* n_path_join(const char* dir, const char* name) {
if (!dir || !name) return NULL;
size_t dl = strlen(dir);
size_t nl = strlen(name);
int need_sep = 1;
if (dl == 0) {
need_sep = 0;
} else {
char last = dir[dl - 1];
if (last == '/' || last == '\\') need_sep = 0;
}
/* We use '/' for output paths. */
size_t out_len = dl + (need_sep ? 1 : 0) + nl + 1;
char* out = (char*)malloc(out_len);
if (!out) return NULL;
if (need_sep)
snprintf(out, out_len, "%s/%s", dir, name);
else
snprintf(out, out_len, "%s%s", dir, name);
return out;
}
int n_comp_file_info(const void* a, const void* b) {
const N_FILE_INFO* f1 = (const N_FILE_INFO*)a;
const N_FILE_INFO* f2 = (const N_FILE_INFO*)b;
if (!f1) return -1;
if (!f2) return 1;
if (f1->filetime < f2->filetime) return -1;
if (f1->filetime > f2->filetime) return 1;
if (f1->filetime_nsec < f2->filetime_nsec) return -1;
if (f1->filetime_nsec > f2->filetime_nsec) return 1;
return 0;
}
void n_set_time_from_stat(N_FILE_INFO* file, const struct stat* st) {
if (!file || !st) return;
#if defined(__linux__) || defined(__sun)
file->filetime = (int64_t)st->st_mtim.tv_sec;
file->filetime_nsec = (int32_t)st->st_mtim.tv_nsec;
#else
file->filetime = (int64_t)st->st_mtime;
file->filetime_nsec = 0;
#endif
}
int n_scan_dir(const char* dir, LIST* result, const int recurse) {
DIR* dp = NULL;
const struct dirent* entry = NULL;
__n_assert(dir, return FALSE);
__n_assert(result, return FALSE);
int error = 0;
dp = opendir(dir);
error = errno;
if (!dp) {
n_log(LOG_ERR, "cannot open directory: %s, %s", dir, strerror(error));
return FALSE;
}
while ((entry = readdir(dp)) != NULL) {
const char* name = entry->d_name;
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
errno = 0;
continue;
}
char* fullpath = n_path_join(dir, name);
if (!fullpath) {
n_log(LOG_ERR, "malloc failed building path for %s/%s", dir, name);
closedir(dp);
return FALSE;
}
struct stat st;
if (stat(fullpath, &st) == -1) {
error = errno;
n_log(LOG_ERR, "unable to stat %s, %s", fullpath, strerror(error));
free(fullpath);
continue;
}
if (S_ISDIR(st.st_mode)) {
if (recurse != FALSE) {
if (n_scan_dir(fullpath, result, recurse) == FALSE) {
n_log(LOG_ERR, "error while recursively scanning %s", fullpath);
}
}
} else if (S_ISREG(st.st_mode)) {
N_FILE_INFO* file = NULL;
Malloc(file, N_FILE_INFO, 1);
if (file) {
/* Store full path to avoid ambiguity in recursion results */
file->name = strdup(fullpath);
n_set_time_from_stat(file, &st);
list_push_sorted(result, file, n_comp_file_info, n_free_file_info);
}
} else {
/* Ignore non-regular / non-directory entries (symlinks, sockets, etc.) */
/* If you want to include symlinks, use lstat() and handle S_ISLNK. */
}
free(fullpath);
errno = 0;
}
if (errno != 0) {
n_log(LOG_ERR, "readdir failed for %s: %s", dir, strerror(errno));
closedir(dp);
return FALSE;
}
closedir(dp);
return TRUE;
}
int n_file_read_bounded(const char* path, size_t max_size, unsigned char** out, size_t* out_len) {
__n_assert(path, return FALSE);
__n_assert(out, return FALSE);
*out = NULL;
if (out_len) *out_len = 0;
FILE* f = fopen(path, "rb");
if (!f) {
n_log(LOG_ERR, "n_file_read_bounded: cannot open %s: %s", path, strerror(errno));
return FALSE;
}
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return FALSE;
}
long sz = ftell(f);
if (sz < 0) {
fclose(f);
return FALSE;
}
if ((size_t)sz > max_size) {
n_log(LOG_ERR, "n_file_read_bounded: %s is %ld bytes, over the %zu cap", path, sz, max_size);
fclose(f);
return FALSE;
}
/* not rewind(): it returns void and reports failure only by setting errno, so a
failed seek would go unnoticed and leave the read at the wrong offset. fseek
reports the same failure by return value. */
if (fseek(f, 0, SEEK_SET) != 0) {
n_log(LOG_ERR, "n_file_read_bounded: cannot seek back to the start of %s: %s", path, strerror(errno));
fclose(f);
return FALSE;
}
size_t len = (size_t)sz;
unsigned char* buf = NULL;
Malloc(buf, unsigned char, len + 1);
if (!buf) {
fclose(f);
return FALSE;
}
if (len && fread(buf, 1, len, f) != len) {
n_log(LOG_ERR, "n_file_read_bounded: short read on %s", path);
Free(buf);
fclose(f);
return FALSE;
}
buf[len] = '\0';
fclose(f);
*out = buf;
if (out_len) *out_len = len;
return TRUE;
}
#define N_FILE_COPY_BLOCK 262144
static int n_file_streams_equal(FILE* a, FILE* b) {
unsigned char* x = NULL;
unsigned char* y = NULL;
int same = TRUE;
Malloc(x, unsigned char, N_FILE_COPY_BLOCK);
Malloc(y, unsigned char, N_FILE_COPY_BLOCK);
if (!x || !y) {
Free(x);
Free(y);
return FALSE;
}
for (;;) {
size_t got_a = fread(x, 1, N_FILE_COPY_BLOCK, a);
size_t got_b = fread(y, 1, N_FILE_COPY_BLOCK, b);
if (got_a != got_b || (got_a && memcmp(x, y, got_a) != 0)) {
same = FALSE;
break;
}
if (got_a < N_FILE_COPY_BLOCK) {
/* Both ended together; an error on either is not "equal". */
if (ferror(a) || ferror(b)) same = FALSE;
break;
}
}
Free(x);
Free(y);
return same;
}
int n_file_same_content(const char* a, const char* b) {
FILE* fa = NULL;
FILE* fb = NULL;
int same = FALSE;
__n_assert(a, return FALSE);
__n_assert(b, return FALSE);
fa = fopen(a, "rb");
fb = fa ? fopen(b, "rb") : NULL;
if (fa && fb) same = n_file_streams_equal(fa, fb);
if (fa) fclose(fa);
if (fb) fclose(fb);
return same;
}
int n_file_copy_verified(const char* from, const char* to, size_t* copied) {
FILE* in = NULL;
FILE* out = NULL;
unsigned char* block = NULL;
char* part = NULL;
size_t plen = 0, total = 0;
int ok = TRUE;
__n_assert(from, return FALSE);
__n_assert(to, return FALSE);
if (copied) *copied = 0;
/* Never over a file that is there: the caller decides what an existing one is. */
in = fopen(to, "rb");
if (in) {
fclose(in);
n_log(LOG_ERR, "n_file_copy_verified: %s exists, not replaced", to);
return FALSE;
}
plen = strlen(to);
Malloc(part, char, plen + 6); /* ".part" + NUL */
Malloc(block, unsigned char, N_FILE_COPY_BLOCK);
if (!part || !block) {
Free(part);
Free(block);
return FALSE;
}
memcpy(part, to, plen);
memcpy(part + plen, ".part", 6);
in = fopen(from, "rb");
out = in ? fopen(part, "wb") : NULL;
if (!in || !out) {
n_log(LOG_ERR, "n_file_copy_verified: cannot open %s: %s", in ? part : from, strerror(errno));
ok = FALSE;
}
while (ok) {
size_t got = fread(block, 1, N_FILE_COPY_BLOCK, in);
if (got && fwrite(block, 1, got, out) != got) {
n_log(LOG_ERR, "n_file_copy_verified: writing %s: %s", part, strerror(errno));
ok = FALSE;
break;
}
total += got;
if (got < N_FILE_COPY_BLOCK) {
if (ferror(in)) {
n_log(LOG_ERR, "n_file_copy_verified: reading %s: %s", from, strerror(errno));
ok = FALSE;
}
break;
}
}
if (out) {
if (ok && fflush(out) != 0) ok = FALSE;
if (ok) {
int fd = fileno(out);
#ifdef __windows__
if (fd >= 0) (void)_commit(fd);
#else
if (fd >= 0) (void)fsync(fd);
#endif
}
if (fclose(out) != 0) ok = FALSE;
out = NULL;
}
if (in) {
fclose(in);
in = NULL;
}
/* Read back from disk, not from what was meant to be written. */
if (ok && !n_file_same_content(from, part)) {
n_log(LOG_ERR, "n_file_copy_verified: the copy of %s does not match it", from);
ok = FALSE;
}
if (ok && rename(part, to) != 0) {
n_log(LOG_ERR, "n_file_copy_verified: cannot name %s: %s", to, strerror(errno));
ok = FALSE;
}
if (!ok) remove(part);
if (ok && copied) *copied = total;
Free(part);
Free(block);
return ok;
}
int n_file_write_atomic(const char* path, const unsigned char* data, size_t len, int mode) {
__n_assert(path, return FALSE);
if (!data && len > 0) {
n_log(LOG_ERR, "n_file_write_atomic: NULL data with len %zu", len);
return FALSE;
}
size_t plen = strlen(path);
char* tmp = NULL;
Malloc(tmp, char, plen + 5); /* ".tmp" + NUL */
__n_assert(tmp, return FALSE);
memcpy(tmp, path, plen);
memcpy(tmp + plen, ".tmp", 5);
/* the temporary carries the final mode from the moment it exists. Creating it with the
* default mode and chmod'ing it afterwards leaves the content readable by anyone between
* the write and the chmod, which matters because callers use this to persist secrets. */
#ifdef __windows__
FILE* f = fopen(tmp, "wb");
#else
int tmpfd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, (mode != 0) ? (mode_t)mode : (mode_t)0666);
FILE* f = (tmpfd >= 0) ? fdopen(tmpfd, "wb") : NULL;
if (!f && tmpfd >= 0) close(tmpfd);
#endif
if (!f) {
n_log(LOG_ERR, "n_file_write_atomic: cannot create %s: %s", tmp, strerror(errno));
Free(tmp);
return FALSE;
}
int ok = TRUE;
if (len && fwrite(data, 1, len, f) != len) ok = FALSE;
if (ok && fflush(f) != 0) ok = FALSE;
if (ok) {
int fd = fileno(f);
if (fd >= 0) {
#ifdef __windows__
(void)_commit(fd); /* FlushFileBuffers under the hood */
(void)mode; /* no POSIX mode bits on Windows */
#else
(void)fsync(fd);
/* a temporary left behind by an earlier crash keeps the mode it was created
* with, so the mode is still asserted here rather than trusted from open() */
if (mode != 0 && fchmod(fd, (mode_t)mode) != 0) ok = FALSE;
#endif
}
}
if (fclose(f) != 0) ok = FALSE;
if (ok) {
#ifdef __windows__
/* Windows rename() fails if the target exists; MoveFileEx replaces it
* atomically on NTFS, WRITE_THROUGH for durability. */
ok = MoveFileExA(tmp, path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) ? TRUE : FALSE;
#else
ok = (rename(tmp, path) == 0) ? TRUE : FALSE;
#endif
}
if (!ok) {
remove(tmp);
Free(tmp);
return FALSE;
}
Free(tmp);
return TRUE;
}
int n_file_free_space(const char* path, uint64_t* bytes) {
__n_assert(path, return FALSE);
__n_assert(bytes, return FALSE);
*bytes = 0;
#ifdef __windows__
{
ULARGE_INTEGER avail;
/* the caller's share, quotas included, not the volume's total free */
if (!GetDiskFreeSpaceExA(path, &avail, NULL, NULL)) {
n_log(LOG_ERR, "cannot read the free space of %s (error %lu)", path, (unsigned long)GetLastError());
return FALSE;
}
*bytes = (uint64_t)avail.QuadPart;
}
#else
{
struct statvfs fs;
if (statvfs(path, &fs) != 0) {
n_log(LOG_ERR, "cannot read the free space of %s: %s", path, strerror(errno));
return FALSE;
}
/* f_bavail, not f_bfree: the blocks kept back for root are not ours */
*bytes = (uint64_t)fs.f_bavail * (uint64_t)fs.f_frsize;
}
#endif
return TRUE;
}
Modules
- AABB3D
- ACCEPT_POOL
- ALLEGRO4
- ALLEGRO5
- ANIM_LIB
- ASTAR
- ATLAS_LIB
- AVRO
- CLOCK_SYNC
- COMMONS
- CONFIGFILE
- CYPHER_BASE64
- CYPHER_VIGENERE
- DEAD_RECKONING
- ENUMS
- EXCEPTIONS
- GAME_ENV
- GIT
- GUI
- HASH_TABLE
- ISOMETRIC_ENGINE
- LIST
- LOG
- LOGNODUP
- LZ4
- NETWORKING
- NETWORK_MSG
- N_COOKIES
- N_DIFF
- N_DIGEST
- N_DISK_CACHE
- N_DNS
- N_ECC
- N_ENTROPY
- N_FILES
- N_FLUIDS
- N_GUI_COLOR
- N_GUI_COLORWHEEL
- N_GUI_CURVE
- N_GUI_GRID
- N_GUI_POPUP
- N_HEX
- N_HTTP3
- N_HTTP_COOKIE
- N_HTTP_FORM
- N_HTTP_MIME
- N_HTTP_REQ
- N_HTTP_RESP
- N_HTTP_ROUTER
- N_HTTP_SERVER
- N_ICC
- N_JSON
- N_KAFKA
- N_LINALG
- N_MODULE
- N_OPENSSL_COMPAT
- N_PRETTY
- N_PTH
- N_QUERY
- N_RANDOM
- N_SHA256
- N_STR
- N_TIFF
- N_TIME
- N_USER
- N_X509
- N_ZIP
- PARTICLES
- PCRE
- PHYSICS
- SIGNALS
- STACK
- THREADS
- TRAJECTORY
- TREE
- ZLIB