Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_disk_cache.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 <errno.h>
29#include <stdio.h>
30#include <stdlib.h>
31#include <string.h>
32#include <time.h>
33
35#include "nilorea/n_hash.h"
36#include "nilorea/n_log.h"
37
38#ifdef __windows__
39#include <windows.h>
40#else
41#include <dirent.h>
42#include <sys/stat.h>
43#include <unistd.h>
44#endif
45
48#define N_DISK_CACHE_SEED 0x4E444331u
49
51#define N_DISK_CACHE_PATH_MAX 4096
52
56 char* dir;
57};
58
60typedef struct {
64 uint64_t size;
66 int64_t used;
68
69#ifdef __windows__
71static wchar_t* _n_dc_wide(const char* path) {
72 int n = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0);
73 wchar_t* w = NULL;
74
75 if (n <= 0) return NULL;
76 w = (wchar_t*)malloc(sizeof(wchar_t) * (size_t)n);
77 if (!w) return NULL;
78 if (MultiByteToWideChar(CP_UTF8, 0, path, -1, w, n) <= 0) {
79 free(w);
80 return NULL;
81 }
82 return w;
83}
84
86static int64_t _n_dc_filetime(const FILETIME* ft) {
87 uint64_t t = ((uint64_t)ft->dwHighDateTime << 32) | (uint64_t)ft->dwLowDateTime;
88
89 if (t == 0) return 0;
90 /* 100 ns ticks since 1601 */
91 return (int64_t)(t / 10000000ULL) - 11644473600LL;
92}
93#endif
94
96static FILE* _n_dc_fopen(const char* path, const char* mode) {
97#ifdef __windows__
98 wchar_t* wp = _n_dc_wide(path);
99 wchar_t* wm = _n_dc_wide(mode);
100 FILE* f = (wp && wm) ? _wfopen(wp, wm) : NULL;
101
102 free(wp);
103 free(wm);
104 return f;
105#else
106 return fopen(path, mode);
107#endif
108}
109
111static int _n_dc_remove(const char* path) {
112#ifdef __windows__
113 wchar_t* wp = _n_dc_wide(path);
114
115 if (!wp) return FALSE;
116 int ok = DeleteFileW(wp) || GetLastError() == ERROR_FILE_NOT_FOUND;
117 free(wp);
118 return ok ? TRUE : FALSE;
119#else
120 return (unlink(path) == 0 || errno == ENOENT) ? TRUE : FALSE;
121#endif
122}
123
125static int _n_dc_replace(const char* from, const char* to) {
126#ifdef __windows__
127 wchar_t* wf = _n_dc_wide(from);
128 wchar_t* wt = _n_dc_wide(to);
129 int ok = (wf && wt && MoveFileExW(wf, wt, MOVEFILE_REPLACE_EXISTING)) ? TRUE : FALSE;
130
131 free(wf);
132 free(wt);
133 return ok;
134#else
135 return rename(from, to) == 0 ? TRUE : FALSE;
136#endif
137}
138
140static int _n_dc_is_dir(const char* path) {
141#ifdef __windows__
142 wchar_t* wp = _n_dc_wide(path);
143 DWORD attr = wp ? GetFileAttributesW(wp) : INVALID_FILE_ATTRIBUTES;
144
145 free(wp);
146 return (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) ? TRUE : FALSE;
147#else
148 struct stat st;
149
150 return (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) ? TRUE : FALSE;
151#endif
152}
153
156static int _n_dc_owned(const char* name) {
157 int i = 0;
158
159 for (i = 0; i < N_DISK_CACHE_NAME_LEN; i++) {
160 char c = name[i];
161
162 if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return FALSE;
163 }
164 return (name[N_DISK_CACHE_NAME_LEN] == '\0' || name[N_DISK_CACHE_NAME_LEN] == '.') ? TRUE : FALSE;
165}
166
168static int _n_dc_join(const N_DISK_CACHE* cache, const char* name, char* out, size_t out_len) {
169 int n = snprintf(out, out_len, "%s/%s", cache->dir, name);
170
171 return (n > 0 && (size_t)n < out_len) ? TRUE : FALSE;
172}
173
176static int _n_dc_walk(const N_DISK_CACHE* cache, _N_DC_ENTRY** out) {
177 _N_DC_ENTRY* list = NULL;
178 int count = 0, room = 0;
179
180 *out = NULL;
181#ifdef __windows__
182 {
183 char pattern[N_DISK_CACHE_PATH_MAX];
184 wchar_t* wp = NULL;
185 WIN32_FIND_DATAW fd;
186 HANDLE h = INVALID_HANDLE_VALUE;
187
188 if (!_n_dc_join(cache, "*", pattern, sizeof(pattern))) return -1;
189 wp = _n_dc_wide(pattern);
190 if (!wp) return -1;
191 h = FindFirstFileW(wp, &fd);
192 free(wp);
193 if (h == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_FILE_NOT_FOUND ? 0 : -1;
194 do {
195 char name[N_DISK_CACHE_NAME_LEN + 64];
196 int64_t used = 0;
197
198 if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue;
199 /* an entry's name is ASCII; anything that does not convert, or
200 * does not fit, is not one */
201 if (WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, name, (int)sizeof(name), NULL, NULL) <= 0) continue;
202 if (!_n_dc_owned(name)) continue;
203 if (count == room) {
204 _N_DC_ENTRY* grown = NULL;
205
206 room = room ? room * 2 : 256;
207 grown = (_N_DC_ENTRY*)realloc(list, sizeof(*list) * (size_t)room);
208 if (!grown) {
209 free(list);
210 FindClose(h);
211 return -1;
212 }
213 list = grown;
214 }
215 snprintf(list[count].name, sizeof(list[count].name), "%s", name);
216 list[count].size = ((uint64_t)fd.nFileSizeHigh << 32) | (uint64_t)fd.nFileSizeLow;
217 used = _n_dc_filetime(&fd.ftLastAccessTime);
218 list[count].used = used ? used : _n_dc_filetime(&fd.ftLastWriteTime);
219 count++;
220 } while (FindNextFileW(h, &fd));
221 FindClose(h);
222 }
223#else
224 {
225 DIR* dp = opendir(cache->dir);
226 const struct dirent* de = NULL;
227
228 if (!dp) return -1;
229 while ((de = readdir(dp)) != NULL) {
230 char path[N_DISK_CACHE_PATH_MAX];
231 struct stat st;
232 size_t name_len = 0;
233
234 if (!_n_dc_owned(de->d_name)) continue;
235 name_len = strlen(de->d_name);
236 if (name_len >= sizeof(list[0].name)) continue;
237 if (!_n_dc_join(cache, de->d_name, path, sizeof(path))) continue;
238 /* gone since readdir, or not a file: not an entry */
239 if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) continue;
240 if (count == room) {
241 _N_DC_ENTRY* grown = NULL;
242
243 room = room ? room * 2 : 256;
244 grown = (_N_DC_ENTRY*)realloc(list, sizeof(*list) * (size_t)room);
245 if (!grown) {
246 free(list);
247 closedir(dp);
248 return -1;
249 }
250 list = grown;
251 }
252 memcpy(list[count].name, de->d_name, name_len + 1);
253 list[count].size = (uint64_t)st.st_size;
254 list[count].used = (int64_t)(st.st_atime ? st.st_atime : st.st_mtime);
255 count++;
256 }
257 closedir(dp);
258 }
259#endif
260 *out = list;
261 return count;
262}
263
265static int _n_dc_older(const void* a, const void* b) {
266 int64_t ua = ((const _N_DC_ENTRY*)a)->used;
267 int64_t ub = ((const _N_DC_ENTRY*)b)->used;
268
269 return (ua > ub) - (ua < ub);
270}
271
273 N_DISK_CACHE* cache = NULL;
274 size_t len = 0;
275
276 __n_assert(dir, return NULL);
277 len = strlen(dir);
278 /* the root alone would lose its only separator below */
279 while (len > 1 && (dir[len - 1] == '/' || dir[len - 1] == '\\')) len--;
280 if (len == 0 || len >= N_DISK_CACHE_PATH_MAX - N_DISK_CACHE_NAME_LEN - 64) {
281 n_log(LOG_ERR, "disk cache: unusable directory name");
282 return NULL;
283 }
284 Malloc(cache, N_DISK_CACHE, 1);
285 __n_assert(cache, return NULL);
286 Malloc(cache->dir, char, len + 1);
287 if (!cache->dir) {
288 Free(cache);
289 return NULL;
290 }
291 memcpy(cache->dir, dir, len);
292 cache->dir[len] = '\0';
293 if (!_n_dc_is_dir(cache->dir)) {
294 n_log(LOG_ERR, "disk cache: %s is not a directory", cache->dir);
295 Free(cache->dir);
296 Free(cache);
297 return NULL;
298 }
299 return cache;
300}
301
303 if (!cache || !*cache) return;
304 Free((*cache)->dir);
305 Free(*cache);
306}
307
308const char* n_disk_cache_dir(const N_DISK_CACHE* cache) {
309 return cache ? cache->dir : "";
310}
311
312int n_disk_cache_path(const N_DISK_CACHE* cache, const void* key, size_t key_len, const char* ext, char* out, size_t out_len) {
313 uint64_t hash[2] = {0, 0};
314 char name[N_DISK_CACHE_NAME_LEN + 64];
315 int n = 0;
316
317 __n_assert(cache, return FALSE);
318 __n_assert(key, return FALSE);
319 __n_assert(out, return FALSE);
320 if (!ext) ext = "";
321 if (strchr(ext, '/') || strchr(ext, '\\') || strlen(ext) >= sizeof(name) - N_DISK_CACHE_NAME_LEN) return FALSE;
323 n = snprintf(name, sizeof(name), "%016llx%016llx%s", (unsigned long long)hash[0], (unsigned long long)hash[1], ext);
324 if (n <= 0 || (size_t)n >= sizeof(name)) return FALSE;
325 return _n_dc_join(cache, name, out, out_len);
326}
327
328int n_disk_cache_put(const N_DISK_CACHE* cache, const void* key, size_t key_len, const char* ext, const void* data, size_t len) {
329 char path[N_DISK_CACHE_PATH_MAX];
330 char tmp[N_DISK_CACHE_PATH_MAX + 8];
331 FILE* f = NULL;
332 int ok = TRUE;
333
334 __n_assert(cache, return FALSE);
335 if (!data && len) return FALSE;
336 if (!n_disk_cache_path(cache, key, key_len, ext, path, sizeof(path))) return FALSE;
337 /* A sibling then a rename, so a reader sees the old entry or the new one.
338 * Not flushed to stable storage: a cache entry lost to a power cut is a
339 * miss, which is what a cache is for. The temporary's name is still an
340 * entry's, so a crash's leftover is pruned like any other. */
341 snprintf(tmp, sizeof(tmp), "%s.tmp", path);
342 f = _n_dc_fopen(tmp, "wb");
343 if (!f) {
344 n_log(LOG_DEBUG, "disk cache: cannot write %s", tmp);
345 return FALSE;
346 }
347 if (len && fwrite(data, 1, len, f) != len) ok = FALSE;
348 if (fclose(f) != 0) ok = FALSE;
349 if (ok) ok = _n_dc_replace(tmp, path);
350 if (!ok) {
351 _n_dc_remove(tmp);
352 n_log(LOG_DEBUG, "disk cache: cannot store %s", path);
353 }
354 return ok;
355}
356
357int n_disk_cache_get(const N_DISK_CACHE* cache, const void* key, size_t key_len, const char* ext, size_t max_len, unsigned char** data, size_t* len) {
358 char path[N_DISK_CACHE_PATH_MAX];
359 unsigned char* buf = NULL;
360 FILE* f = NULL;
361 long size = 0;
362
363 __n_assert(data, return FALSE);
364 __n_assert(len, return FALSE);
365 *data = NULL;
366 *len = 0;
367 __n_assert(cache, return FALSE);
368 if (!n_disk_cache_path(cache, key, key_len, ext, path, sizeof(path))) return FALSE;
369 f = _n_dc_fopen(path, "rb");
370 if (!f) return FALSE;
371 if (fseek(f, 0, SEEK_END) != 0 || (size = ftell(f)) < 0 || (uint64_t)size > (uint64_t)max_len || fseek(f, 0, SEEK_SET) != 0) {
372 fclose(f);
373 return FALSE;
374 }
375 buf = (unsigned char*)malloc((size_t)size + 1);
376 if (!buf) {
377 fclose(f);
378 return FALSE;
379 }
380 if (size && fread(buf, 1, (size_t)size, f) != (size_t)size) {
381 free(buf);
382 fclose(f);
383 return FALSE;
384 }
385 fclose(f);
386 buf[size] = '\0';
387 *data = buf;
388 *len = (size_t)size;
389 return TRUE;
390}
391
392int n_disk_cache_remove(const N_DISK_CACHE* cache, const void* key, size_t key_len, const char* ext) {
393 char path[N_DISK_CACHE_PATH_MAX];
394
395 __n_assert(cache, return FALSE);
396 if (!n_disk_cache_path(cache, key, key_len, ext, path, sizeof(path))) return FALSE;
397 return _n_dc_remove(path);
398}
399
400int n_disk_cache_size(const N_DISK_CACHE* cache, uint64_t* total) {
401 _N_DC_ENTRY* list = NULL;
402 int count = 0, i = 0;
403
404 __n_assert(cache, return -1);
405 __n_assert(total, return -1);
406 *total = 0;
407 count = _n_dc_walk(cache, &list);
408 for (i = 0; i < count; i++) *total += list[i].size;
409 free(list);
410 return count;
411}
412
413int n_disk_cache_prune(const N_DISK_CACHE* cache, uint64_t max_bytes) {
414 _N_DC_ENTRY* list = NULL;
415 uint64_t total = 0;
416 int count = 0, i = 0, removed = 0;
417
418 __n_assert(cache, return -1);
419 count = _n_dc_walk(cache, &list);
420 if (count < 0) return -1;
421 for (i = 0; i < count; i++) total += list[i].size;
422 if (total > max_bytes || max_bytes == 0) {
423 qsort(list, (size_t)count, sizeof(*list), _n_dc_older);
424 for (i = 0; i < count && (total > max_bytes || max_bytes == 0); i++) {
425 char path[N_DISK_CACHE_PATH_MAX];
426
427 if (!_n_dc_join(cache, list[i].name, path, sizeof(path))) continue;
428 if (_n_dc_remove(path)) {
429 total -= list[i].size;
430 removed++;
431 }
432 }
433 }
434 free(list);
435 if (removed) n_log(LOG_DEBUG, "disk cache: %s, removed %d entries, %llu bytes left", cache->dir, removed, (unsigned long long)total);
436 return removed;
437}
438
446
448static int _n_dc_group_older(const void* a, const void* b) {
449 return _n_dc_older(&((const _N_DC_GROUP_ENTRY*)a)->entry, &((const _N_DC_GROUP_ENTRY*)b)->entry);
450}
451
452int n_disk_cache_prune_group(const N_DISK_CACHE* const* caches, int count, uint64_t max_bytes) {
453 _N_DC_GROUP_ENTRY* all = NULL;
454 size_t total_count = 0, room = 0;
455 uint64_t total = 0;
456 int c = 0, removed = 0;
457
458 __n_assert(caches, return -1);
459 if (count <= 0) return -1;
460 /* every directory is read before anything is removed, so a directory
461 * that cannot be read leaves the others as they were */
462 for (c = 0; c < count; c++) {
463 _N_DC_ENTRY* list = NULL;
464 int n = 0, k = 0;
465
466 if (!caches[c]) continue;
467 n = _n_dc_walk(caches[c], &list);
468 if (n < 0) {
469 free(all);
470 return -1;
471 }
472 if (total_count + (size_t)n > room) {
473 _N_DC_GROUP_ENTRY* grown = NULL;
474
475 room = total_count + (size_t)n + 256;
476 grown = (_N_DC_GROUP_ENTRY*)realloc(all, sizeof(*all) * room);
477 if (!grown) {
478 free(list);
479 free(all);
480 return -1;
481 }
482 all = grown;
483 }
484 for (k = 0; k < n; k++) {
485 all[total_count].entry = list[k];
486 all[total_count].owner = c;
487 total += list[k].size;
488 total_count++;
489 }
490 free(list);
491 }
492 /* nothing found is nothing to sort: all is still NULL then */
493 if (all && total_count && (total > max_bytes || max_bytes == 0)) {
494 size_t i = 0;
495
496 qsort(all, total_count, sizeof(*all), _n_dc_group_older);
497 for (i = 0; i < total_count && (total > max_bytes || max_bytes == 0); i++) {
498 char path[N_DISK_CACHE_PATH_MAX];
499
500 if (!_n_dc_join(caches[all[i].owner], all[i].entry.name, path, sizeof(path))) continue;
501 if (_n_dc_remove(path)) {
502 total -= all[i].entry.size;
503 removed++;
504 }
505 }
506 }
507 free(all);
508 if (removed) n_log(LOG_DEBUG, "disk cache: %d caches, removed %d entries, %llu bytes left", count, removed, (unsigned long long)total);
509 return removed;
510}
511
512int n_disk_cache_expire(const N_DISK_CACHE* cache, int64_t max_age_seconds) {
513 _N_DC_ENTRY* list = NULL;
514 int64_t cutoff = 0;
515 int count = 0, i = 0, removed = 0;
516
517 __n_assert(cache, return -1);
518 if (max_age_seconds <= 0) return 0;
519 count = _n_dc_walk(cache, &list);
520 if (count < 0) return -1;
521 cutoff = (int64_t)time(NULL) - max_age_seconds;
522 for (i = 0; i < count; i++) {
523 char path[N_DISK_CACHE_PATH_MAX];
524
525 if (list[i].used > cutoff) continue;
526 if (_n_dc_join(cache, list[i].name, path, sizeof(path)) && _n_dc_remove(path)) removed++;
527 }
528 free(list);
529 if (removed) n_log(LOG_DEBUG, "disk cache: %s, removed %d entries unused for %lld s", cache->dir, removed, (long long)max_age_seconds);
530 return removed;
531}
static int mode
volatile char key[227]
TRUE while the key is held.
#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
void MurmurHash3_x64_128(const void *key, const size_t len, const uint64_t seed, void *out)
MurmurHash3 was written by Austin Appleby, and is placed in the public domain.
Definition n_hash.c:1204
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_DEBUG
debug-level messages
Definition n_log.h:108
#define LOG_ERR
error conditions
Definition n_log.h:100
const char * n_disk_cache_dir(const N_DISK_CACHE *cache)
The cache's directory, as it was opened.
#define N_DISK_CACHE_NAME_LEN
length of an entry's name before its extension: 128 bits in hexadecimal
int n_disk_cache_get(const N_DISK_CACHE *cache, const void *key, size_t key_len, const char *ext, size_t max_len, unsigned char **data, size_t *len)
Read an entry.
int n_disk_cache_path(const N_DISK_CACHE *cache, const void *key, size_t key_len, const char *ext, char *out, size_t out_len)
The file an entry lives in, whether or not it exists.
void n_disk_cache_close(N_DISK_CACHE **cache)
Forget a cache.
int n_disk_cache_put(const N_DISK_CACHE *cache, const void *key, size_t key_len, const char *ext, const void *data, size_t len)
Store an entry, replacing any under the same key and extension.
int n_disk_cache_prune_group(const N_DISK_CACHE *const *caches, int count, uint64_t max_bytes)
n_disk_cache_prune() over several caches that share one budget.
int n_disk_cache_prune(const N_DISK_CACHE *cache, uint64_t max_bytes)
Remove least recently used entries until the rest fit a budget.
int n_disk_cache_remove(const N_DISK_CACHE *cache, const void *key, size_t key_len, const char *ext)
Remove one entry, for a caller that found it unreadable.
int n_disk_cache_expire(const N_DISK_CACHE *cache, int64_t max_age_seconds)
Remove every entry unused for longer than an age.
int n_disk_cache_size(const N_DISK_CACHE *cache, uint64_t *total)
How many bytes the cache's entries take.
N_DISK_CACHE * n_disk_cache_open(const char *dir)
Use a directory as a cache.
#define N_DISK_CACHE_SEED
seed of the key hash.
char * dir
the directory, without a trailing separator
int owner
which of the caches
static int _n_dc_join(const N_DISK_CACHE *cache, const char *name, char *out, size_t out_len)
helper: join the directory and a file name
static int _n_dc_owned(const char *name)
helper: is name one this module could have made: 32 lowercase hex digits, then the end or a '.
static int _n_dc_remove(const char *path)
helper: remove a UTF-8 path.
_N_DC_ENTRY entry
the entry
static int _n_dc_replace(const char *from, const char *to)
helper: rename over an existing target
#define N_DISK_CACHE_PATH_MAX
longest path this module builds
int64_t used
last used: access time, else write time
uint64_t size
size in bytes
static FILE * _n_dc_fopen(const char *path, const char *mode)
helper: fopen a UTF-8 path
static int _n_dc_group_older(const void *a, const void *b)
qsort order for a group: least recently used first
char name[32+64]
file name, an entry's by construction
static int _n_dc_is_dir(const char *path)
helper: is path a directory
static int _n_dc_walk(const N_DISK_CACHE *cache, _N_DC_ENTRY **out)
helper: every entry of the cache, in a new array the caller frees.
static int _n_dc_older(const void *a, const void *b)
qsort order: least recently used first
one entry found by a walk of the directory
one entry of a group prune: the entry and the cache it is in
a cache: only where it is
A bounded cache of files in one directory, keyed by content.
Hash functions and table.
Generic log system.