5 ex_disk_cache_8c example
gullradriel edited this page 2026-10-09 00:51:57 +02:00

ex_disk_cache.c

a bounded on-disk cache keyed by content Makes a scratch directory, stores and reads entries, checks the path a key maps to, then gives three entries known last-used times and checks the size prune removes the least recently used first, the age expiry removes only the old, and neither ever touches a file the cache did not make. Last, two caches pruned as a group share one budget and one order. ```c

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #include <sys/stat.h>

#include "nilorea/n_common.h" #include "nilorea/n_log.h" #include "nilorea/n_disk_cache.h"

#ifdef windows #include <direct.h> #include <sys/utime.h> #define ex_mkdir(p) _mkdir(p) #else #include <utime.h> #define ex_mkdir(p) mkdir(p, 0700) #endif

#define EX_DIR "ex_disk_cache.dir"

static int log_level = LOG_ERR; static int failures = 0;

static void expect_true(const char* what, int ok) { if (!ok) { n_log(LOG_ERR, "FAIL: %s", what); failures++; } else { n_log(LOG_INFO, "ok: %s", what); } }

static int process_args(int argc, char** argv) { int opt = 0;

while ((opt = getopt(argc, argv, "V:")) != -1) {
    if (opt == 'V') {
        log_level = atoi(optarg);
    } else {
        return -1;
    }
}
return 0;

}

static int exists(const char* path) { struct stat st;

return stat(path, &st) == 0;

}

static void write_foreign(const char* name) { char path[512]; FILE* f = NULL;

snprintf(path, sizeof(path), "%s/%s", EX_DIR, name);
f = fopen(path, "wb");
if (f) {
    fputs("not the cache's", f);
    fclose(f);
}

}

static void put_aged(const N_DISK_CACHE* cache, const char* key, long ago) { char data[100]; char path[512]; struct utimbuf when;

memset(data, key[0], sizeof(data));
n_disk_cache_put(cache, key, strlen(key), ".bin", data, sizeof(data));
n_disk_cache_path(cache, key, strlen(key), ".bin", path, sizeof(path));
when.actime = time(NULL) - ago;
when.modtime = when.actime;
utime(path, &when);

}

static int has(const N_DISK_CACHE* cache, const char* key) { char path[512];

return n_disk_cache_path(cache, key, strlen(key), ".bin", path, sizeof(path)) && exists(path);

}

int main(int argc, char** argv) { N_DISK_CACHE* cache = NULL; unsigned char* data = NULL; size_t len = 0; uint64_t total = 0; char p1[512], p2[512], p3[512]; const char* upper = "0123456789ABCDEF0123456789ABCDEF.bin";

set_log_level(LOG_ERR);
if (process_args(argc, argv) != 0) {
    fprintf(stderr, "Usage: %s [-V LOG_LEVEL]\n", argv[0]);
    return 2;
}
set_log_level(log_level);

expect_true("a directory that is not there is refused", n_disk_cache_open(EX_DIR "/nope") == NULL);
ex_mkdir(EX_DIR);
cache = n_disk_cache_open(EX_DIR "/");
expect_true("an existing directory opens", cache != NULL);
if (!cache) return 1;
expect_true("its name loses the trailing separator", strcmp(n_disk_cache_dir(cache), EX_DIR) == 0);

/* keys to paths */
expect_true("a key has a path", n_disk_cache_path(cache, "k", 1, ".png", p1, sizeof(p1)));
n_disk_cache_path(cache, "k", 1, ".png", p2, sizeof(p2));
n_disk_cache_path(cache, "k", 1, ".jpg", p3, sizeof(p3));
expect_true("the same key has the same path", strcmp(p1, p2) == 0);
expect_true("another extension, another file", strcmp(p1, p3) != 0);
expect_true("the path is the directory, 32 digits and the extension",
            strlen(p1) == strlen(EX_DIR) + 1 + N_DISK_CACHE_NAME_LEN + 4 && strcmp(p1 + strlen(p1) - 4, ".png") == 0);
expect_true("an extension with a separator is refused", n_disk_cache_path(cache, "k", 1, "/x", p1, sizeof(p1)) == FALSE);
expect_true("a path that does not fit is refused", n_disk_cache_path(cache, "k", 1, ".png", p1, 8) == FALSE);

/* put and get */
expect_true("put stores", n_disk_cache_put(cache, "hello", 5, NULL, "world", 5));
expect_true("get finds it", n_disk_cache_get(cache, "hello", 5, NULL, 1024, &data, &len) && len == 5 && memcmp(data, "world", 5) == 0 && data[5] == '\0');
free(data);
data = NULL;
expect_true("an entry over the limit is a miss", n_disk_cache_get(cache, "hello", 5, NULL, 4, &data, &len) == FALSE && data == NULL);
expect_true("another key is a miss", n_disk_cache_get(cache, "hellO", 5, NULL, 1024, &data, &len) == FALSE);
expect_true("put replaces", n_disk_cache_put(cache, "hello", 5, NULL, "again!", 6) &&
                                n_disk_cache_get(cache, "hello", 5, NULL, 1024, &data, &len) && len == 6);
free(data);
data = NULL;
expect_true("remove removes", n_disk_cache_remove(cache, "hello", 5, NULL) && !n_disk_cache_get(cache, "hello", 5, NULL, 1024, &data, &len));
expect_true("removing what is not there is fine", n_disk_cache_remove(cache, "hello", 5, NULL));

/* files the cache did not make */
write_foreign("notes.txt");
write_foreign(upper);

/* least recently used first */
put_aged(cache, "a", 3000);
put_aged(cache, "b", 2000);
put_aged(cache, "c", 1000);
expect_true("size counts only the entries", n_disk_cache_size(cache, &total) == 3 && total == 300);
expect_true("prune to 250 bytes removes one", n_disk_cache_prune(cache, 250) == 1);
expect_true("the least recently used", !has(cache, "a") && has(cache, "b") && has(cache, "c"));
expect_true("prune under budget removes none", n_disk_cache_prune(cache, 1000) == 0);
expect_true("prune to 0 removes the rest", n_disk_cache_prune(cache, 0) == 2 && !has(cache, "b") && !has(cache, "c"));

/* by age */
put_aged(cache, "d", 1000);
put_aged(cache, "e", 0);
expect_true("expire at 500 s removes the old one", n_disk_cache_expire(cache, 500) == 1 && !has(cache, "d") && has(cache, "e"));
expect_true("expire at 0 removes nothing", n_disk_cache_expire(cache, 0) == 0 && has(cache, "e"));
n_disk_cache_prune(cache, 0);

snprintf(p1, sizeof(p1), "%s/notes.txt", EX_DIR);
snprintf(p2, sizeof(p2), "%s/%s", EX_DIR, upper);
expect_true("a file that is not an entry is never removed", exists(p1) && exists(p2));
remove(p1);
remove(p2);

/* two caches, one budget: the oldest entries go whichever directory they
 * are in */
{
    N_DISK_CACHE* other = NULL;
    const N_DISK_CACHE* both[2];

    ex_mkdir(EX_DIR "/other");
    other = n_disk_cache_open(EX_DIR "/other");
    expect_true("a second cache opens", other != NULL);
    if (other) {
        both[0] = cache;
        both[1] = other;
        put_aged(cache, "f", 4000);
        put_aged(other, "g", 3000);
        put_aged(cache, "h", 2000);
        put_aged(other, "i", 1000);
        expect_true("a group prune to 250 bytes removes two", n_disk_cache_prune_group(both, 2, 250) == 2);
        expect_true("the two oldest, one from each", !has(cache, "f") && !has(other, "g") && has(cache, "h") && has(other, "i"));
        expect_true("under budget removes none", n_disk_cache_prune_group(both, 2, 1000) == 0);
        expect_true("a NULL cache is skipped", n_disk_cache_prune_group((const N_DISK_CACHE* const[]){NULL, other}, 2, 0) == 1 && !has(other, "i") && has(cache, "h"));
        expect_true("no caches is an error", n_disk_cache_prune_group(both, 0, 0) == -1);
        n_disk_cache_prune(cache, 0);
        n_disk_cache_close(&other);
    }
    expect_true("the second scratch directory is empty again", rmdir(EX_DIR "/other") == 0);
}

n_disk_cache_close(&cache);
expect_true("close sets the handle to NULL", cache == NULL);
n_disk_cache_close(&cache);
n_disk_cache_close(NULL);
expect_true("the scratch directory is empty again", rmdir(EX_DIR) == 0);

if (failures) {
    fprintf(stderr, "ex_disk_cache: %d failure(s)\n", failures);
    return 1;
}
printf("ex_disk_cache: ok\n");
return 0;

}


_Filename: ex_disk_cache.c_