7 n__files_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00

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:

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;
}