simplify gitignore
[vg.git] / dep / randygaul / cute_files.h
1 /*
2 ------------------------------------------------------------------------------
3 Licensing information can be found at the end of the file.
4 ------------------------------------------------------------------------------
5
6 cute_files.h - v1.0
7
8 To create implementation (the function definitions)
9 #define CUTE_FILES_IMPLEMENTATION
10 in *one* C/CPP file (translation unit) that includes this file
11
12 Summary:
13 Utility header for traversing directories to apply a function on each found file.
14 Recursively finds sub-directories. Can also be used to iterate over files in a
15 folder manually. All operations done in a cross-platform manner (thx posix!).
16
17 This header does no dynamic memory allocation, and performs internally safe string
18 copies as necessary. Strings for paths, file names and file extensions are all
19 capped, and intended to use primarily the C run-time stack memory. Feel free to
20 modify the defines in this file to adjust string size limitations.
21
22 Read the header for specifics on each function.
23
24 Here's an example to print all files in a folder:
25 cf_dir_t dir;
26 cf_dir_open(&dir, "a");
27
28 while (dir.has_next)
29 {
30 cf_file_t file;
31 cf_read_file(&dir, &file);
32 printf("%s\n", file.name);
33 cf_dir_next(&dir);
34 }
35
36 cf_dir_close(&dir);
37 */
38
39 #if !defined(CUTE_FILES_H)
40
41 #define CUTE_FILES_WINDOWS 1
42 #define CUTE_FILES_MAC 2
43 #define CUTE_FILES_UNIX 3
44
45 #if defined(_WIN32)
46 #define CUTE_FILES_PLATFORM CUTE_FILES_WINDOWS
47 #if !defined(_CRT_SECURE_NO_WARNINGS)
48 #define _CRT_SECURE_NO_WARNINGS
49 #endif
50 #elif defined(__APPLE__)
51 #define CUTE_FILES_PLATFORM CUTE_FILES_MAC
52 #else
53 #define CUTE_FILES_PLATFORM CUTE_FILES_UNIX
54 #endif
55
56 #include <string.h> // strerror, strncpy
57
58 // change to 0 to compile out any debug checks
59 #define CUTE_FILES_DEBUG_CHECKS 1
60
61 #if CUTE_FILES_DEBUG_CHECKS
62
63 #include <stdio.h> // printf
64 #include <assert.h> // assert
65 #include <errno.h>
66 #define CUTE_FILES_ASSERT assert
67
68 #else
69
70 #define CUTE_FILES_ASSERT(...)
71
72 #endif // CUTE_FILES_DEBUG_CHECKS
73
74 #define CUTE_FILES_MAX_PATH 1024
75 #define CUTE_FILES_MAX_FILENAME 256
76 #define CUTE_FILES_MAX_EXT 32
77
78 struct cf_file_t;
79 struct cf_dir_t;
80 struct cf_time_t;
81 typedef struct cf_file_t cf_file_t;
82 typedef struct cf_dir_t cf_dir_t;
83 typedef struct cf_time_t cf_time_t;
84 typedef void (cf_callback_t)(cf_file_t* file, void* udata);
85
86 // Stores the file extension in cf_file_t::ext, and returns a pointer to
87 // cf_file_t::ext
88 const char* cf_get_ext(cf_file_t* file);
89
90 // Applies a function (cb) to all files in a directory. Will recursively visit
91 // all subdirectories. Useful for asset management, file searching, indexing, etc.
92 void cf_traverse(const char* path, cf_callback_t* cb, void* udata);
93
94 // Fills out a cf_file_t struct with file information. Does not actually open the
95 // file contents, and instead performs more lightweight OS-specific calls.
96 int cf_read_file(cf_dir_t* dir, cf_file_t* file);
97
98 // Once a cf_dir_t is opened, this function can be used to grab another file
99 // from the operating system.
100 void cf_dir_next(cf_dir_t* dir);
101
102 // Performs lightweight OS-specific call to close internal handle.
103 void cf_dir_close(cf_dir_t* dir);
104
105 // Performs lightweight OS-specific call to open a file handle on a directory.
106 int cf_dir_open(cf_dir_t* dir, const char* path);
107
108 // Compares file last write times. -1 if file at path_a was modified earlier than path_b.
109 // 0 if they are equal. 1 if file at path_b was modified earlier than path_a.
110 int cf_compare_file_times_by_path(const char* path_a, const char* path_b);
111
112 // Retrieves time file was last modified, returns 0 upon failure
113 int cf_get_file_time(const char* path, cf_time_t* time);
114
115 // Compares file last write times. -1 if time_a was modified earlier than path_b.
116 // 0 if they are equal. 1 if time_b was modified earlier than path_a.
117 int cf_compare_file_times(cf_time_t* time_a, cf_time_t* time_b);
118
119 // Returns 1 of file exists, otherwise returns 0.
120 int cf_file_exists(const char* path);
121
122 // Returns 1 if the file's extension matches the string in ext
123 // Returns 0 otherwise
124 int cf_match_ext(cf_file_t* file, const char* ext);
125
126 // Prints detected errors to stdout
127 void cf_do_unit_tests();
128
129 #if CUTE_FILES_PLATFORM == CUTE_FILES_WINDOWS
130
131 #if !defined _CRT_SECURE_NO_WARNINGS
132 #define _CRT_SECURE_NO_WARNINGS
133 #endif
134 #include <windows.h>
135
136 struct cf_file_t
137 {
138 char path[CUTE_FILES_MAX_PATH];
139 char name[CUTE_FILES_MAX_FILENAME];
140 char ext[CUTE_FILES_MAX_EXT];
141 int is_dir;
142 int is_reg;
143 size_t size;
144 };
145
146 struct cf_dir_t
147 {
148 char path[CUTE_FILES_MAX_PATH];
149 int has_next;
150 HANDLE handle;
151 WIN32_FIND_DATAA fdata;
152 };
153
154 struct cf_time_t
155 {
156 FILETIME time;
157 };
158
159 #elif CUTE_FILES_PLATFORM == CUTE_FILES_MAC || CUTE_FILES_PLATFORM == CUTE_FILES_UNIX
160
161 #include <sys/stat.h>
162 #include <dirent.h>
163 #include <unistd.h>
164 #include <time.h>
165
166 struct cf_file_t
167 {
168 char path[CUTE_FILES_MAX_PATH];
169 char name[CUTE_FILES_MAX_FILENAME];
170 char ext[CUTE_FILES_MAX_EXT];
171 int is_dir;
172 int is_reg;
173 int size;
174 struct stat info;
175 };
176
177 struct cf_dir_t
178 {
179 char path[CUTE_FILES_MAX_PATH];
180 int has_next;
181 DIR* dir;
182 struct dirent* entry;
183 };
184
185 struct cf_time_t
186 {
187 time_t time;
188 };
189
190 #endif
191
192 #define CUTE_FILES_H
193 #endif
194
195 #ifdef CUTE_FILES_IMPLEMENTATION
196 #ifndef CUTE_FILES_IMPLEMENTATION_ONCE
197 #define CUTE_FILES_IMPLEMENTATION_ONCE
198
199 #define cf_safe_strcpy(dst, src, n, max) cf_safe_strcpy_internal(dst, src, n, max, __FILE__, __LINE__)
200 static int cf_safe_strcpy_internal(char* dst, const char* src, int n, int max, const char* file, int line)
201 {
202 int c;
203 const char* original = src;
204
205 do
206 {
207 if (n >= max)
208 {
209 if (!CUTE_FILES_DEBUG_CHECKS) break;
210 printf("ERROR: String \"%s\" too long to copy on line %d in file %s (max length of %d).\n"
211 , original
212 , line
213 , file
214 , max);
215 CUTE_FILES_ASSERT(0);
216 }
217
218 c = *src++;
219 dst[n] = c;
220 ++n;
221 } while (c);
222
223 return n;
224 }
225
226 const char* cf_get_ext(cf_file_t* file)
227 {
228 char* name = file->name;
229 char* period = NULL;
230 while (*name++) if (*name == '.') period = name;
231 if (period) cf_safe_strcpy(file->ext, period, 0, CUTE_FILES_MAX_EXT);
232 else file->ext[0] = 0;
233 return file->ext;
234 }
235
236 void cf_traverse(const char* path, cf_callback_t* cb, void* udata)
237 {
238 cf_dir_t dir;
239 cf_dir_open(&dir, path);
240
241 while (dir.has_next)
242 {
243 cf_file_t file;
244 int res = cf_read_file(&dir, &file);
245
246 if (res == 0) {
247 cf_dir_next(&dir);
248 continue;
249 }
250
251 if (file.is_dir && file.name[0] != '.')
252 {
253 char path2[CUTE_FILES_MAX_PATH];
254 int n = cf_safe_strcpy(path2, path, 0, CUTE_FILES_MAX_PATH);
255 n = cf_safe_strcpy(path2, "/", n - 1, CUTE_FILES_MAX_PATH);
256 cf_safe_strcpy(path2, file.name, n -1, CUTE_FILES_MAX_PATH);
257 cf_traverse(path2, cb, udata);
258 }
259
260 if (file.is_reg) cb(&file, udata);
261 cf_dir_next(&dir);
262 }
263
264 cf_dir_close(&dir);
265 }
266
267 int cf_match_ext(cf_file_t* file, const char* ext)
268 {
269 return !strcmp(file->ext, ext);
270 }
271
272 #if CUTE_FILES_PLATFORM == CUTE_FILES_WINDOWS
273
274 int cf_read_file(cf_dir_t* dir, cf_file_t* file)
275 {
276 CUTE_FILES_ASSERT(dir->handle != INVALID_HANDLE_VALUE);
277
278 int n = 0;
279 char* fpath = file->path;
280 char* dpath = dir->path;
281
282 n = cf_safe_strcpy(fpath, dpath, 0, CUTE_FILES_MAX_PATH);
283 n = cf_safe_strcpy(fpath, "/", n - 1, CUTE_FILES_MAX_PATH);
284
285 char* dname = dir->fdata.cFileName;
286 char* fname = file->name;
287
288 cf_safe_strcpy(fname, dname, 0, CUTE_FILES_MAX_FILENAME);
289 cf_safe_strcpy(fpath, fname, n - 1, CUTE_FILES_MAX_PATH);
290
291 size_t max_dword = MAXDWORD;
292 file->size = ((size_t)dir->fdata.nFileSizeHigh * (max_dword + 1)) + (size_t)dir->fdata.nFileSizeLow;
293 cf_get_ext(file);
294
295 file->is_dir = !!(dir->fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
296 file->is_reg = !!(dir->fdata.dwFileAttributes & FILE_ATTRIBUTE_NORMAL) ||
297 !(dir->fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
298
299 return 1;
300 }
301
302 void cf_dir_next(cf_dir_t* dir)
303 {
304 CUTE_FILES_ASSERT(dir->has_next);
305
306 if (!FindNextFileA(dir->handle, &dir->fdata))
307 {
308 dir->has_next = 0;
309 DWORD err = GetLastError();
310 CUTE_FILES_ASSERT(err == ERROR_SUCCESS || err == ERROR_NO_MORE_FILES);
311 }
312 }
313
314 void cf_dir_close(cf_dir_t* dir)
315 {
316 dir->path[0] = 0;
317 dir->has_next = 0;
318 if (dir->handle != INVALID_HANDLE_VALUE) FindClose(dir->handle);
319 }
320
321 int cf_dir_open(cf_dir_t* dir, const char* path)
322 {
323 int n = cf_safe_strcpy(dir->path, path, 0, CUTE_FILES_MAX_PATH);
324 n = cf_safe_strcpy(dir->path, "\\*", n - 1, CUTE_FILES_MAX_PATH);
325 dir->handle = FindFirstFileA(dir->path, &dir->fdata);
326 dir->path[n - 3] = 0;
327
328 if (dir->handle == INVALID_HANDLE_VALUE)
329 {
330 printf("ERROR: Failed to open directory (%s): %s.\n", path, strerror(errno));
331 cf_dir_close(dir);
332 CUTE_FILES_ASSERT(0);
333 return 0;
334 }
335
336 dir->has_next = 1;
337
338 return 1;
339 }
340
341 int cf_compare_file_times_by_path(const char* path_a, const char* path_b)
342 {
343 FILETIME time_a = { 0 };
344 FILETIME time_b = { 0 };
345 WIN32_FILE_ATTRIBUTE_DATA data;
346
347 if (GetFileAttributesExA(path_a, GetFileExInfoStandard, &data)) time_a = data.ftLastWriteTime;
348 if (GetFileAttributesExA(path_b, GetFileExInfoStandard, &data)) time_b = data.ftLastWriteTime;
349 return CompareFileTime(&time_a, &time_b);
350 }
351
352 int cf_get_file_time(const char* path, cf_time_t* time)
353 {
354 FILETIME initialized_to_zero = { 0 };
355 time->time = initialized_to_zero;
356 WIN32_FILE_ATTRIBUTE_DATA data;
357 if (GetFileAttributesExA(path, GetFileExInfoStandard, &data))
358 {
359 time->time = data.ftLastWriteTime;
360 return 1;
361 }
362 return 0;
363 }
364
365 int cf_compare_file_times(cf_time_t* time_a, cf_time_t* time_b)
366 {
367 return CompareFileTime(&time_a->time, &time_b->time);
368 }
369
370 int cf_file_exists(const char* path)
371 {
372 WIN32_FILE_ATTRIBUTE_DATA unused;
373 return GetFileAttributesExA(path, GetFileExInfoStandard, &unused);
374 }
375
376 #elif CUTE_FILES_PLATFORM == CUTE_FILES_MAC || CUTE_FILES_PLATFORM == CUTE_FILES_UNIX
377
378 int cf_read_file(cf_dir_t* dir, cf_file_t* file)
379 {
380 CUTE_FILES_ASSERT(dir->entry);
381
382 int n = 0;
383 char* fpath = file->path;
384 char* dpath = dir->path;
385
386 n = cf_safe_strcpy(fpath, dpath, 0, CUTE_FILES_MAX_PATH);
387 n = cf_safe_strcpy(fpath, "/", n - 1, CUTE_FILES_MAX_PATH);
388
389 char* dname = dir->entry->d_name;
390 char* fname = file->name;
391
392 cf_safe_strcpy(fname, dname, 0, CUTE_FILES_MAX_FILENAME);
393 cf_safe_strcpy(fpath, fname, n - 1, CUTE_FILES_MAX_PATH);
394
395 if (stat(file->path, &file->info))
396 return 0;
397
398 file->size = file->info.st_size;
399 cf_get_ext(file);
400
401 file->is_dir = S_ISDIR(file->info.st_mode);
402 file->is_reg = S_ISREG(file->info.st_mode);
403
404 return 1;
405 }
406
407 void cf_dir_next(cf_dir_t* dir)
408 {
409 CUTE_FILES_ASSERT(dir->has_next);
410 dir->entry = readdir(dir->dir);
411 dir->has_next = dir->entry ? 1 : 0;
412 }
413
414 void cf_dir_close(cf_dir_t* dir)
415 {
416 dir->path[0] = 0;
417 if (dir->dir) closedir(dir->dir);
418 dir->dir = 0;
419 dir->has_next = 0;
420 dir->entry = 0;
421 }
422
423 int cf_dir_open(cf_dir_t* dir, const char* path)
424 {
425 cf_safe_strcpy(dir->path, path, 0, CUTE_FILES_MAX_PATH);
426 dir->dir = opendir(path);
427
428 if (!dir->dir)
429 {
430 printf("ERROR: Failed to open directory (%s): %s.\n", path, strerror(errno));
431 cf_dir_close(dir);
432 CUTE_FILES_ASSERT(0);
433 return 0;
434 }
435
436 dir->has_next = 1;
437 dir->entry = readdir(dir->dir);
438 if (!dir->dir) dir->has_next = 0;
439
440 return 1;
441 }
442
443 // Warning : untested code! (let me know if it breaks)
444 int cf_compare_file_times_by_path(const char* path_a, const char* path_b)
445 {
446 time_t time_a;
447 time_t time_b;
448 struct stat info;
449 if (stat(path_a, &info)) return 0;
450 time_a = info.st_mtime;
451 if (stat(path_b, &info)) return 0;
452 time_b = info.st_mtime;
453 return (int)difftime(time_a, time_b);
454 }
455
456 // Warning : untested code! (let me know if it breaks)
457 int cf_get_file_time(const char* path, cf_time_t* time)
458 {
459 struct stat info;
460 if (stat(path, &info)) return 0;
461 time->time = info.st_mtime;
462 return 1;
463 }
464
465 // Warning : untested code! (let me know if it breaks)
466 int cf_compare_file_times(cf_time_t* time_a, cf_time_t* time_b)
467 {
468 return (int)difftime(time_a->time, time_b->time);
469 }
470
471 // Warning : untested code! (let me know if it breaks)
472 int cf_file_exists(const char* path)
473 {
474 return access(path, F_OK) != -1;
475 }
476
477 #endif // CUTE_FILES_PLATFORM
478
479 #endif // CUTE_FILES_IMPLEMENTATION_ONCE
480 #endif // CUTE_FILES_IMPLEMENTATION
481
482 /*
483 ------------------------------------------------------------------------------
484 This software is available under 2 licenses - you may choose the one you like.
485 ------------------------------------------------------------------------------
486 ALTERNATIVE A - zlib license
487 Copyright (c) 2017 Randy Gaul http://www.randygaul.net
488 This software is provided 'as-is', without any express or implied warranty.
489 In no event will the authors be held liable for any damages arising from
490 the use of this software.
491 Permission is granted to anyone to use this software for any purpose,
492 including commercial applications, and to alter it and redistribute it
493 freely, subject to the following restrictions:
494 1. The origin of this software must not be misrepresented; you must not
495 claim that you wrote the original software. If you use this software
496 in a product, an acknowledgment in the product documentation would be
497 appreciated but is not required.
498 2. Altered source versions must be plainly marked as such, and must not
499 be misrepresented as being the original software.
500 3. This notice may not be removed or altered from any source distribution.
501 ------------------------------------------------------------------------------
502 ALTERNATIVE B - Public Domain (www.unlicense.org)
503 This is free and unencumbered software released into the public domain.
504 Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
505 software, either in source code form or as a compiled binary, for any purpose,
506 commercial or non-commercial, and by any means.
507 In jurisdictions that recognize copyright laws, the author or authors of this
508 software dedicate any and all copyright interest in the software to the public
509 domain. We make this dedication for the benefit of the public at large and to
510 the detriment of our heirs and successors. We intend this dedication to be an
511 overt act of relinquishment in perpetuity of all present and future rights to
512 this software under copyright law.
513 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
514 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
515 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
516 AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
517 ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
518 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
519 ------------------------------------------------------------------------------
520 */