-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathfs.h
More file actions
3757 lines (2780 loc) · 133 KB
/
fs.h
File metadata and controls
3757 lines (2780 loc) · 133 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
File system library. Choice of public domain or MIT-0. See license statements at the end of this file.
fs - v1.0.0 - Release Date TBD
David Reid - mackron@gmail.com
GitHub: https://github.com/mackron/fs
*/
/*
1. Introduction
===============
This library is used to abstract access to the regular file system and archives such as ZIP files.
1.1. Basic Usage
----------------
The main object in the library is the `fs` object. Below is the most basic way to initialize a `fs`
object:
```c
fs_result result;
fs* pFS;
result = fs_init(NULL, &pFS);
if (result != FS_SUCCESS) {
// Failed to initialize.
}
```
The above code will initialize a `fs` object representing the system's regular file system. It uses
stdio under the hood. Once this is set up you can load files:
```c
fs_file* pFile;
result = fs_file_open(pFS, "file.txt", FS_READ, &pFile);
if (result != FS_SUCCESS) {
// Failed to open file.
}
```
If you don't need any of the advanced features of the library, you can just pass in NULL for the
`fs` object which will just use the native file system like normal:
```c
fs_file_open(NULL, "file.txt", FS_READ, &pFile);
```
From here on out, examples will use an `fs` object for the sake of consistency, but all basic IO
APIs that do not use things like mounting and archive registration will work with NULL.
To close a file, use `fs_file_close()`:
```c
fs_file_close(pFile);
```
Reading content from the file is very standard:
```c
size_t bytesRead;
result = fs_file_read(pFile, pBuffer, bytesToRead, &bytesRead);
if (result != FS_SUCCESS) {
// Failed to read file. You can use FS_AT_END to check if reading failed due to being at EOF.
}
```
In the code above, the number of bytes actually read is output to a variable. You can use this to
determine if you've reached the end of the file. You can also check if the result is FS_AT_END. You
can pass in null for the last parameter of `fs_file_read()` in which an error will be returned if
the exact number of bytes requested could not be read.
Writing works the same way as reading:
```c
fs_file* pFile;
result = fs_file_open(pFS, "file.txt", FS_WRITE, &pFile);
if (result != FS_SUCCESS) {
// Failed to open file.
}
result = fs_file_write(pFile, pBuffer, bytesToWrite, &bytesWritten);
```
Formatted writing is also supported:
```c
result = fs_file_writef(pFile, "Hello %s!\n", "World");
if (result != FS_SUCCESS) {
// Failed to write file.
}
va_list args;
va_start(args, format);
result = fs_file_writefv(pFile, "Hello %s!\n", args);
va_end(args);
```
The `FS_WRITE` option will default to overwrite mode. You can use `FS_TRUNCATE` if you want to
truncate the file instead of overwriting it.
```c
fs_file_open(pFS, "file.txt", FS_WRITE | FS_TRUNCATE, &pFile);
```
You can also open a file in append mode with `FS_APPEND`:
```c
fs_file_open(pFS, "file.txt", FS_WRITE | FS_APPEND, &pFile);
```
When using `FS_APPEND` mode, the file will always append to the end of the file and can never
overwrite existing content. This follows POSIX semantics. In this mode, it is not possible to
create sparse files.
To open a file in write mode, but fail if the file already exists, you can use `FS_EXCLUSIVE`:
```c
fs_file_open(pFS, "file.txt", FS_WRITE | FS_EXCLUSIVE, &pFile);
```
Files can be opened for both reading and writing by simply combining the two:
```c
fs_file_open(pFS, "file.txt", FS_READ | FS_WRITE, &pFile);
```
Seeking and telling is very standard as well:
```c
fs_file_seek(pFile, 0, FS_SEEK_END);
fs_int64 cursorPos;
fs_file_tell(pFile, &cursorPos);
```
When seeking, you can seek beyond the end of the file. Attempting to read from beyond the end of
the file will return `FS_AT_END`. Attempting to write beyond the end of the file will create a
hole if supported by the file system, or fill the space with data (the filled data can be left
undefined). When seeking from the end of the file with a negative offset, it will seek backwards
from the end. Seeking to before the start of the file is not allowed and will return an error.
Retrieving information about a file is done with `fs_file_get_info()`:
```c
fs_file_info info;
fs_file_get_info(pFile, &info);
```
If you want to get information about a file without opening it, you can use `fs_info()`:
```c
fs_file_info info;
fs_info(pFS, "file.txt", FS_READ, &info); // FS_READ tells it to check read-only mounts (explained later)
```
A file handle can be duplicated with `fs_file_duplicate()`:
```c
fs_file* pFileDup;
fs_file_duplicate(pFile, &pFileDup);
```
Note that this will only duplicate the file handle. It does not make a copy of the file on the file
system itself. The duplicated file handle will be entirely independent of the original handle,
including having its own separate read/write cursor position. The initial position of the cursor of
the new file handle is undefined and you should explicitly seek to the appropriate location.
Important: `fs_file_duplicate()` can fail or work incorrectly if you use relative paths for mounts
and something changes the working directory. The reason being that it reopens the file based on the
original path to do the duplication.
To delete a file, use `fs_remove()`:
```c
fs_remove(pFS, "file.txt");
```
Note that files are deleted permanently. There is no recycle bin or trash functionality.
Files can be renamed and moved with `fs_rename()`:
```c
fs_rename(pFS, "file.txt", "new-file.txt");
```
To create a directory, use `fs_mkdir()`:
```c
fs_mkdir(pFS, "new-directory", 0);
```
By default, `fs_mkdir()` will create the directory hierarchy for you. If you want to disable this
so it fails if the directory hierarchy doesn't exist, you can use `FS_NO_CREATE_DIRS`:
```c
fs_mkdir(pFS, "new-directory", FS_NO_CREATE_DIRS);
```
1.2. Archives
-------------
To enable support for archives, you need an `fs` object, and it must be initialized with a config:
```c
#include "extras/backends/zip/fs_zip.h" // <-- This is where FS_ZIP is declared.
#include "extras/backends/pak/fs_pak.h" // <-- This is where FS_PAK is declared.
...
fs_archive_type pArchiveTypes[] =
{
{FS_ZIP, "zip"},
{FS_PAK, "pak"}
};
fs_config fsConfig = fs_config_init_default();
fsConfig.pArchiveTypes = pArchiveTypes;
fsConfig.archiveTypeCount = sizeof(pArchiveTypes) / sizeof(pArchiveTypes[0]);
fs* pFS;
fs_init(&fsConfig, &pFS);
```
In the code above we are registering support for ZIP archives (`FS_ZIP`) and Quake PAK archives
(`FS_PAK`). Whenever a file with a "zip" or "pak" extension is found, the library will be able to
access the archive. The library will determine whether or not a file is an archive based on its
extension. You can use whatever extension you would like for a backend, and you can associate
multiple extensions to the same backend. You can also associate different backends to the same
extension, in which case the library will use the first one that works. If the extension of a file
does not match with one of the registered archive types it'll assume it's not an archive and will
skip it. Below is an example of one way you can read from an archive:
```c
result = fs_file_open(pFS, "archive.zip/file-inside-archive.txt", FS_READ, &pFile);
if (result != FS_SUCCESS) {
// Failed to open file.
}
```
In the example above, we've explicitly specified the name of the archive in the file path. The
library also supports the ability to handle archives transparently, meaning you don't need to
explicitly specify the archive. The code below will also work:
```c
fs_file_open(pFS, "file-inside-archive.txt", FS_READ, &pFile);
```
Transparently handling archives like this has overhead because the library needs to scan the file
system and check every archive it finds. To avoid this, you can explicitly disable this feature:
```c
fs_file_open(pFS, "archive.zip/file-inside-archive.txt", FS_READ | FS_VERBOSE, &pFile);
```
In the code above, the `FS_VERBOSE` flag will require you to pass in a verbose file path, meaning
you need to explicitly specify the archive in the path. You can take this one step further by
disabling access to archives in this manner altogether via `FS_OPAQUE`:
```c
result = fs_file_open(pFS, "archive.zip/file-inside-archive.txt", FS_READ | FS_OPAQUE, &pFile);
if (result != FS_SUCCESS) {
// This example will always fail.
}
```
In the example above, opening the file will fail because `FS_OPAQUE` is telling the library to
treat archives as if they're totally opaque which means the files within cannot be accessed.
Up to this point the handling of archives has been done automatically via `fs_file_open()`, however
the library allows you to manage archives manually. To do this you just initialize a `fs` object to
represent the archive:
```c
// Open the archive file itself first.
fs_file* pArchiveFile;
result = fs_file_open(pFS, "archive.zip", FS_READ, &pArchiveFile);
if (result != FS_SUCCESS) {
// Failed to open archive file.
}
// Once we have the archive file we can create the `fs` object representing the archive.
fs* pArchive;
fs_config archiveConfig;
archiveConfig = fs_config_init(FS_ZIP, NULL, fs_file_get_stream(pArchiveFile));
result = fs_init(&archiveConfig, &pArchive);
if (result != FS_SUCCESS) {
// Failed to initialize archive.
}
...
// During teardown, make sure the archive `fs` object is uninitialized before the stream.
fs_uninit(pArchive);
fs_file_close(pArchiveFile);
```
To initialize an `fs` object for an archive you need a stream to provide the raw archive data to
the backend. Conveniently, the `fs_file` object itself is a stream. In the example above we're just
opening a file from a different `fs` object (usually one representing the default file system) to
gain access to a stream. The stream does not need to be a `fs_file`. You can implement your own
`fs_stream` object, and a `fs_memory_stream` is included as stock with the library for when you
want to store the contents of an archive in-memory. Once you have the `fs` object for the archive
you can use it just like any other:
```c
result = fs_file_open(pArchive, "file-inside-archive.txt", FS_READ, &pFile);
if (result != FS_SUCCESS) {
// Failed to open file.
}
```
In addition to the above, you can use `fs_open_archive()` to open an archive from a file:
```c
fs* pArchive;
fs_open_archive(pFS, "archive.zip", FS_READ, &pArchive);
...
// When tearing down, do *not* use `fs_uninit()`. Use `fs_close_archive()` instead.
fs_close_archive(pArchive);
```
Note that you need to use `fs_close_archive()` when opening an archive like this. The reason for
this is that there's some internal reference counting and memory management happening under the
hood. You should only call `fs_close_archive()` if `fs_open_archive()` succeeds.
When opening an archive with `fs_open_archive()`, it will inherit the archive types from the parent
`fs` object and will therefore support archives within archives. Use caution when doing this
because if both archives are compressed you will get a big performance hit. Only the inner-most
archive should be compressed.
1.3. Mounting
-------------
There is no ability to change the working directory in this library. Instead you can mount a
physical directory to a virtual path, similar in concept to Unix operating systems. The difference,
however, is that you can mount multiple directories to the same virtual path in which case a
prioritization system will be used (only for reading - in write mode only a single mount is used).
There are separate mount points for reading and writing. Below is an example of mounting for
reading:
```c
fs_mount(pFS, "/some/actual/path", NULL, FS_READ);
```
To unmount, you need to specify the actual path, not the virtual path:
```c
fs_unmount(pFS, "/some/actual/path", FS_READ);
```
In the example above, using `NULL` for the virtual path is equivalent to an empty path. If, for
example, you have a file with the path "/some/actual/path/file.txt", you can open it like the
following:
```c
fs_file_open(pFS, "file.txt", FS_READ, &pFile);
```
You don't need to specify the "/some/actual/path" part because it's handled by the mount. If you
specify a virtual path, you can do something like the following:
```c
fs_mount(pFS, "/some/actual/path", "assets", FS_READ);
```
In this case, loading files that are physically located in "/some/actual/path" would need to be
prefixed with "assets":
```c
fs_file_open(pFS, "assets/file.txt", FS_READ, &pFile);
```
You can mount multiple paths to the same virtual path in which case a prioritization system will be
used:
```c
fs_mount(pFS, "/usr/share/mygame/gamedata/base", "gamedata", FS_READ); // Base game. Lowest priority.
fs_mount(pFS, "/home/user/.local/share/mygame/gamedata/mod1", "gamedata", FS_READ); // Mod #1. Middle priority.
fs_mount(pFS, "/home/user/.local/share/mygame/gamedata/mod2", "gamedata", FS_READ); // Mod #2. Highest priority.
```
The example above shows a basic system for setting up some kind of modding support in a game. In
this case, attempting to load a file from the "gamedata" mount point will first check the "mod2"
directory, and if it cannot be opened from there, it will check "mod1", and finally it'll fall back
to the base game data.
Internally there are a separate set of mounts for reading and writing. To set up a mount point for
opening files in write mode, you need to specify the `FS_WRITE` option:
```c
fs_mount(pFS, "/home/user/.config/mygame", "config", FS_WRITE);
fs_mount(pFS, "/home/user/.local/share/mygame/saves", "saves", FS_WRITE);
```
To open a file for writing, you need only prefix the path with the mount's virtual path, exactly
like read mode:
```c
fs_file_open(pFS, "config/game.cfg", FS_WRITE, &pFile); // Prefixed with "config", so will use the "config" mount point.
fs_file_open(pFS, "saves/save0.sav", FS_WRITE, &pFile); // Prefixed with "saves", so will use the "saves" mount point.
```
If you want to mount a directory for reading and writing, you can use both `FS_READ` and
`FS_WRITE` together:
```c
fs_mount(pFS, "/home/user/.config/mygame", "config", FS_READ | FS_WRITE);
```
You can set up read and write mount points to the same virtual path:
```c
fs_mount(pFS, "/usr/share/mygame/config", "config", FS_READ);
fs_mount(pFS, "/home/user/.local/share/mygame/config", "config", FS_READ | FS_WRITE);
```
When opening a file for reading, it'll first try searching the second mount point, and if it's not
found will fall back to the first. When opening in write mode, it will only ever use the second
mount point as the output directory because that's the only one set up with `FS_WRITE`. With this
setup, the first mount point is essentially protected from modification.
When mounting a directory for writing, the library will create the directory hierarchy for you. If
you want to disable this functionality, you can use the `FS_NO_CREATE_DIRS` flag:
```c
fs_mount(pFS, "/home/user/.config/mygame", "config", FS_WRITE | FS_NO_CREATE_DIRS);
```
By default, you can move outside the mount point with ".." segments. If you want to disable this
functionality, you can use the `FS_NO_ABOVE_ROOT_NAVIGATION` flag when opening the file:
```c
fs_file_open(pFS, "../file.txt", FS_READ | FS_NO_ABOVE_ROOT_NAVIGATION, &pFile);
```
In addition, any mount points that start with a "/" will be considered absolute and will not allow
any above-root navigation:
```c
fs_mount(pFS, "/usr/share/mygame/gamedata/base", "/gamedata", FS_READ);
```
In the example above, the "/gamedata" mount point starts with a "/", so it will not allow any
above-root navigation which means you cannot navigate above "/usr/share/mygame/gamedata/base". When
opening a file with this kind of mount point, you would need to specify the leading slash:
```c
fs_file_open(pFS, "/gamedata/file.txt", FS_READ, &pFile); // Note how the path starts with "/".
```
Important: When using mount points that start with "/", if the file cannot be opened from the mount,
it will fall back to trying the actual absolute path. To prevent this and ensure files are only
loaded from the mount point, use the `FS_ONLY_MOUNTS` flag when opening files. Alternatively,
simply avoid using "/" prefixed mounts and instead use `FS_NO_ABOVE_ROOT_NAVIGATION` for security.
You can also mount an archive to a virtual path:
```c
fs_mount(pFS, "/usr/share/mygame/gamedata.zip", "gamedata", FS_READ);
```
In order to do this, the `fs` object must have been configured with support for the given archive
type. Note that writing directly into an archive is not supported by this API. To write into an
archive, the backend itself must support writing, and you will need to manually initialize a `fs`
object for the archive and write into it directly.
The examples above have been hard coding paths, but you can use `fs_mount_sysdir()` to mount a
system directory to a virtual path. This is just a convenience helper function, and you need not
use it if you'd rather deal with system directories yourself:
```c
fs_mount_sysdir(pFS, FS_SYSDIR_CONFIG, "myapp", "/config", FS_READ | FS_WRITE);
```
This function requires that you specify a sub-directory of the system directory to mount. The reason
for this is to encourage the application to use good practice to avoid cluttering the file system.
Use `fs_unmount_sysdir()` to unmount a system directory. When using this you must specify the
sub-directory you used when mounting it:
```c
fs_unmount_sysdir(pFS, FS_SYSDIR_CONFIG, "myapp", FS_READ | FS_WRITE);
```
Mounting a `fs` object to a virtual path is also supported.
```c
fs* pSomeOtherFS; // <-- This would have been initialized earlier.
fs_mount_fs(pFS, pSomeOtherFS, "assets.zip", FS_READ);
...
fs_unmount_fs(pFS, pSomeOtherFS, FS_READ);
```
If the file cannot be opened from any mounts it will attempt to open the file from the backend's
default search path. Mounts always take priority. When opening in transparent mode with
`FS_TRANSPARENT` (default), it will first try opening the file as if it were not in an archive. If
that fails, it will look inside archives.
When opening a file, if you pass in NULL for the `pFS` parameter it will open the file like normal
using the standard file system. That is, it'll work exactly as if you were using stdio `fopen()`,
and you will not have access to mount points. Keep in mind that there is no notion of a "current
directory" in this library so you'll be stuck with the initial working directory.
You can also skip mount points when opening a file by using the `FS_IGNORE_MOUNTS` flag:
```c
fs_file_open(pFS, "/absolute/path/to/file.txt", FS_READ | FS_IGNORE_MOUNTS, &pFile);
```
This can be useful when you want to access a file directly without going through the mount system,
such as when working with temporary files.
1.4. Enumeration
----------------
You can enumerate over the contents of a directory like the following:
```c
for (fs_iterator* pIterator = fs_first(pFS, "directory/to/enumerate", FS_NULL_TERMINATED, 0); pIterator != NULL; pIterator = fs_next(pIterator)) {
printf("Name: %s\n", pIterator->pName);
printf("Size: %llu\n", pIterator->info.size);
}
```
If you want to terminate iteration early, use `fs_free_iterator()` to free the iterator object.
`fs_next()` will free the iterator for you when it reaches the end.
Like when opening a file, you can specify `FS_OPAQUE`, `FS_VERBOSE` or `FS_TRANSPARENT` (default)
in `fs_first()` to control which files are enumerated. Enumerated files will be consistent with
what would be opened when using the same option with `fs_file_open()`.
Internally, `fs_first()` will gather all of the enumerated files. This means you should expect
`fs_first()` to be slow compared to `fs_next()`.
Enumerated entries will be sorted by name in terms of `strcmp()`.
Enumeration is not recursive. If you want to enumerate recursively you will need to do it manually.
You can inspect the `directory` member of the `info` member in `fs_iterator` to determine if the
entry is a directory.
1.5. System Directories
-----------------------
It can often be useful to know the exact paths of known standard system directories, such as the
home directory. You can use the `fs_sysdir()` function for this:
```c
char pPath[256];
size_t pathLen = fs_sysdir(FS_SYSDIR_HOME, pPath, sizeof(pPath));
if (pathLen > 0) {
if (pathLen < sizeof(pPath)) {
// Success!
} else {
// The buffer was too small. Expand the buffer to at least `pathLen + 1` and try again.
}
} else {
// An error occurred.
}
```
`fs_sysdir()` will return the length of the path written to `pPath`, or 0 if an error occurred. If
the buffer is too small, it will return the required size, not including the null terminator.
Recognized system directories include the following:
- FS_SYSDIR_HOME
- FS_SYSDIR_TEMP
- FS_SYSDIR_CONFIG
- FS_SYSDIR_DATA
- FS_SYSDIR_CACHE
1.6. Temporary Files
--------------------
You can create a temporary file or folder with `fs_mktmp()`. To create a temporary folder, use the
`FS_MKTMP_DIR` option:
```c
char pTmpPath[256];
fs_result result = fs_mktmp("prefix", pTmpPath, sizeof(pTmpPath), FS_MKTMP_DIR);
if (result != FS_SUCCESS) {
// Failed to create temporary file.
}
```
Similarly, to create a temporary file, use the `FS_MKTMP_FILE` option:
```c
char pTmpPath[256];
fs_result result = fs_mktmp("prefix", pTmpPath, sizeof(pTmpPath), FS_MKTMP_FILE);
if (result != FS_SUCCESS) {
// Failed to create temporary file.
}
```
`fs_mktmp()` will create a temporary file or folder with a unique name based on the provided
prefix and will return the full path to the created file or folder in `pTmpPath`. To open the
temporary file, you can pass in the path to `fs_file_open()`, making sure to ignore mount points
with `FS_IGNORE_MOUNTS`:
```c
fs_file* pFile;
result = fs_file_open(pFS, pTmpPath, FS_WRITE | FS_IGNORE_MOUNTS, &pFile);
if (result != FS_SUCCESS) {
// Failed to open temporary file.
}
```
The prefix can include subdirectories, such as "myapp/subdir". In this case the library will create
the directory hierarchy for you, unless you pass in `FS_NO_CREATE_DIRS`. Note that not all
platforms treat the name portion of the prefix the same. In particular, Windows will only use up to
the first 3 characters of the name portion of the prefix.
If you don't like the behavior of `fs_mktmp()`, you can consider using `fs_sysdir()` with
`FS_SYSDIR_TEMP` and create the temporary file yourself.
2. Thread Safety
================
The following points apply regarding thread safety.
- Opening files across multiple threads is safe. Backends are responsible for ensuring thread
safety when opening files.
- An individual `fs_file` object is not thread safe. If you want to use a specific `fs_file`
object across multiple threads, you will need to synchronize access to it yourself. Using
different `fs_file` objects across multiple threads is safe.
- Mounting and unmounting is not thread safe. You must use your own synchronization if you
want to do this across multiple threads.
- Opening a file on one thread while simultaneously mounting or unmounting on another thread is
not safe. Again, you must use your own synchronization if you need to do this. The recommended
usage is to set up your mount points once during initialization before opening any files.
3. Platform Considerations
============================
3.1. Windows
--------------
On Windows, Unicode support is determined by the `UNICODE` preprocessor define. When `UNICODE` is
defined, the library will use the wide character versions of Windows APIs. When not defined, it
will use the ANSI versions.
3.2. POSIX
------------
On POSIX platforms, `ftruncate()` is unavailable with `-std=c89` unless `_XOPEN_SOURCE` is defined
to >= 500. This may affect the availability of file truncation functionality when using strict C89
compilation.
4. Backends
===========
You can implement custom backends to support different file systems and archive formats. A POSIX
or Win32 backend is the default backend depending on the platform, and is built into the library.
A backend implements the functions in the `fs_backend` structure.
A ZIP backend is included in the "extras" folder of this library's repository. Refer to this for
a complete example for how to implement a backend (not including write support, but I'm sure
you'll figure it out!). A PAK backend is also included in the "extras" folder, and is simpler than
the ZIP backend which might also be a good place to start.
The backend abstraction is designed to relieve backends from having to worry about the
implementation details of the main library. Backends should only concern themselves with their
own local content and not worry about things like mount points, archives, etc. Those details will
be handled at a higher level in the library.
Instances of a `fs` object can be configured with backend-specific configuration data. This is
passed to the backend as a void pointer to the necessary functions. This data will point to a
backend-defined structure that the backend will know how to use.
In order for the library to know how much memory to allocate for the `fs` object, the backend
needs to implement the `alloc_size` function. This function should return the total size of the
backend-specific data to associate with the `fs` object. Internally, this memory will be stored
at the end of the `fs` object. The backend can access this data via `fs_get_backend_data()`:
```c
typedef struct my_fs_data
{
int someData;
} my_fs_data;
...
my_fs_data* pBackendData = (my_fs_data*)fs_get_backend_data(pFS);
assert(pBackendData != NULL);
do_something(pBackendData->someData);
```
This pattern will be a central part of how backends are implemented. If you don't have any
backend-specific data, you can just return 0 from `alloc_size()` and simply not reference the
backend data pointer.
4.1. Backend Functions
----------------------
alloc_size
This function should return the size of the backend-specific data to associate with the `fs`
object. If no additional data is required, return 0.
The main library will allocate the `fs` object, including any additional space specified by the
`alloc_size` function.
init
This function is called after `alloc_size()` and after the `fs` object has been allocated. This
is where you should initialize the backend.
This function will take a pointer to the `fs` object, the backend-specific configuration data,
and a stream object. The stream is used to provide the backend with the raw data of an archive,
which will be required for archive backends like ZIP. If your backend requires this, you should
check for if the stream is null, and if so, return an error. See section "5. Streams" for more
details on how to use streams. You need not take a copy of the stream pointer for use outside
of `init`. Instead you can just use `fs_get_stream()` to get the stream object when you need it.
You should not ever close or otherwise take ownership of the stream - that will be handled
at a higher level.
uninit
This is where you should do any cleanup. Do not close the stream here.
remove
This function is used to delete a file or directory. This is not recursive. If the path is
a directory, the backend should return an error if it is not empty. Backends do not need to
implement this function in which case they can leave the callback pointer as `NULL`, or have
it return `FS_NOT_IMPLEMENTED`.
rename
This function is used to rename a file. This will act as a move if the source and destination
are in different directories. If the destination already exists, it should be overwritten. This
function is optional and can be left as `NULL` or return `FS_NOT_IMPLEMENTED`.
mkdir
This function is used to create a directory. This is not recursive. If the directory already
exists, the backend should return `FS_ALREADY_EXISTS`. If a parent directory does not exist,
the backend should return `FS_DOES_NOT_EXIST`. This function is optional and can be left as
`NULL` or return `FS_NOT_IMPLEMENTED`.
info
This function is used to get information about a file. If the backend does not have the notion
of the last modified or access time, it can set those values to 0. Set `directory` to 1 (or
`FS_TRUE`) if it's a directory. Likewise, set `symlink` to 1 if it's a symbolic link. It is
important that this function return the info of the exact file that would be opened with
`file_open()`. This function is mandatory.
file_alloc_size
Like when initializing a `fs` object, the library needs to know how much backend-specific data
to allocate for the `fs_file` object. This is done with the `file_alloc_size` function. This
function is basically the same as `alloc_size` for the `fs` object, but for `fs_file`. If the
backend does not need any additional data, it can return 0. The backend can access this data
via `fs_file_get_backend_data()`.
file_open
The `file_open` function is where the backend should open the file.
If the `fs` object that owns the file was initialized with a stream, the stream will be
non-null. If your backends requires a stream, you should check that the stream is null, and if
so, return an error. The reason you need to check for this is that the application itself may
erroneously attempt to initialize a `fs` object for your backend without a stream, and since
the library cannot know whether or not a backend requires a stream, it cannot check this on
your behalf.
If the backend requires a stream, it should take a copy of only the pointer and store it for
later use. Do *not* make a duplicate of the stream with `fs_stream_duplicate()`.
Backends need only handle the following open mode flags:
FS_READ
FS_WRITE
FS_TRUNCATE
FS_APPEND
FS_EXCLUSIVE
All other flags are for use at a higher level and should be ignored.
When opening in write mode (`FS_WRITE`), the backend should default to overwrite mode. If
`FS_TRUNCATE` is specified, the file should be truncated to 0 length. If `FS_APPEND` is
specified, all writes should happen at the end of the file regardless of the position of the
write cursor. If `FS_EXCLUSIVE` is specified, opening should fail if the file already exists.
In all write modes, the file should be created if it does not already exist.
If any flags cannot be supported, the backend should return an error.
When opening in read mode, if the file does not exist, `FS_DOES_NOT_EXIST` should be returned.
Similarly, if the file is a directory, `FS_IS_DIRECTORY` should be returned.
Before calling into this function, the library will normalize all paths to use forward slashes.
Therefore, backends must support forward slashes ("/") as path separators.
file_close
This function is where the file should be closed. This is where the backend should release any
resources associated with the file. Do not uninitialize the stream here - it'll be cleaned up
at a higher level.
file_read
This is used to read data from the file. The backend should return `FS_AT_END` when the end of
the file is reached, but only if the number of bytes read is 0.
file_write
This is used to write data to the file. If the file is opened in append mode, the backend
should always ensure writes are appended to the end, regardless of the position of the write
cursor. This is optional and need only be specified if the backend supports writing.
file_seek
The `file_seek` function is used to seek the read/write cursor. The backend should allow
seeking beyond the end of the file. If the file is opened in write mode and data is written
beyond the end of the file, the file should be extended, and if possible made into a sparse
file. If sparse files are not supported, the backend should fill the gap with data, preferably
with zeros if possible.
Attempting to seek to before the start of the file should return `FS_BAD_SEEK`.
file_tell
The `file_tell` function is used to get the current cursor position. There is only one cursor,
even when the file is opened in read and write mode.
file_flush
The `file_flush` function is used to flush any buffered data to the file. This is optional and
can be left as `NULL` or return `FS_NOT_IMPLEMENTED`.
file_truncate
The `file_truncate` function is used to truncate a file to the current cursor position. This is
only useful for write mode, so is therefore optional and can be left as `NULL` or return
`FS_NOT_IMPLEMENTED`.
file_info
The `file_info` function is used to get information about an opened file. It returns the same
information as `info` but for an opened file. This is mandatory.
file_duplicate
The `file_duplicate` function is used to duplicate a file. The destination file will be a new
file and already allocated. The backend need only copy the necessary backend-specific data to
the new file. The backend must ensure that the duplicated file is totally independent of the
original file and has its own independent read/write pointer. If the backend is unable to
support duplicated files having their own independent read/write pointer, it must return an
error.
If a backend cannot support duplication, it can leave this as `NULL` or return
`FS_NOT_IMPLEMENTED`. However, if this is not implemented, the backend will not be able to open
files within archives.
first, next, free_iterator
The `first`, `next` and `free_iterator` functions are used to enumerate the contents of a
directory. If the directory is empty, or an error occurs, `fs_first` should return `NULL`. The
`next` function should return `NULL` when there are no more entries. When `next` returns
`NULL`, the backend needs to free the iterator object. The `free_iterator` function is used to
free the iterator object explicitly. The backend is responsible for any memory management of
the name string. A typical way to deal with this is to allocate additional space for the name
immediately after the `fs_iterator` allocation.
4.2. Thread Safety
------------------
Backends are responsible for guaranteeing thread-safety of different files across different
threads. This should typically be quite easy since most system backends, such as stdio, are already
thread-safe, and archive backends are typically read-only which should make thread-safety trivial
on that front as well. You need not worry about thread-safety of a single individual file handle.
But when you have two different file handles, they must be able to be used on two different threads
at the same time.
5. Streams
==========
Streams are the data delivery mechanism for archive backends. You can implement custom streams, but
this should be uncommon because `fs_file` itself is a stream, and a memory stream is included in
the library called `fs_memory_stream`. Between these two the majority of use cases should be
covered. You can retrieve the stream associated with a `fs_file` using `fs_file_get_stream()`.
A stream is initialized using a specialized initialization function depending on the stream type.
For `fs_file`, simply opening the file is enough. For `fs_memory_stream`, you need to call
`fs_memory_stream_init_readonly()` for a standard read-only stream, or
`fs_memory_stream_init_write()` for a stream with write (and read) support. If you want to
implement your own stream type you would need to implement a similar initialization function.
Use `fs_stream_read()` and `fs_stream_write()` to read and write data from a stream. If the stream
does not support reading or writing, the respective function will return `FS_NOT_IMPLEMENTED`.
The cursor can be set and retrieved with `fs_stream_seek()` and `fs_stream_tell()`. There is only
a single cursor which is shared between reading and writing.
Streams can be duplicated. A duplicated stream is a fully independent stream. This functionality
is used heavily internally by the library so if you build a custom stream you should support it
if you can. Without duplication support, you will not be able to open files within archives. To
duplicate a stream, use `fs_stream_duplicate()`. To delete a duplicated stream, use
`fs_stream_delete_duplicate()`. Do not use implementation-specific uninitialization routines to
uninitialize a duplicated stream - `fs_stream_delete_duplicate()` will deal with that for you.
Streams are not thread safe. If you want to use a stream across multiple threads, you will need to
synchronize access to it yourself. Using different stream objects across multiple threads is safe.
A duplicated stream is entirely independent of the original stream and can be used on a different
thread to the original stream.
The `fs_stream` object is a base class. If you want to implement your own stream, you should make
the first member of your stream object a `fs_stream` object. This will allow you to cast between
`fs_stream*` and your custom stream type.
See `fs_stream_vtable` for a list of functions that need to be implemented for a custom stream. If
the stream does not support writing, the `write` callback can be left as `NULL` or return
`FS_NOT_IMPLEMENTED`.
See `fs_memory_stream` for an example of how to implement a custom stream.
*/
/*
This library has been designed to be amalgamated into other libraries of mine. You will probably
see some random tags and stuff in this file. These are just used for doing a dumb amalgamation.
*/
#ifndef fs_h
#define fs_h
#if defined(__cplusplus)
extern "C" {
#endif
/* BEG fs_platform_detection.h */
#if defined(_WIN32)
#define FS_WIN32
#else
#define FS_POSIX
#endif
/* END fs_platform_detection.h */
/* BEG fs_compiler_compat.h */
#include <stddef.h> /* For size_t. */
#include <stdarg.h> /* For va_list. */
#if defined(SIZE_MAX)
#define FS_SIZE_MAX SIZE_MAX
#else
#define FS_SIZE_MAX 0xFFFFFFFF /* When SIZE_MAX is not defined by the standard library just default to the maximum 32-bit unsigned integer. */
#endif
#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined(_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(__powerpc64__)
#define FS_SIZEOF_PTR 8
#else
#define FS_SIZEOF_PTR 4
#endif
#if FS_SIZEOF_PTR == 8
#define FS_64BIT
#else
#define FS_32BIT
#endif
#if defined(FS_USE_STDINT)
#include <stdint.h>
typedef int8_t fs_int8;
typedef uint8_t fs_uint8;
typedef int16_t fs_int16;
typedef uint16_t fs_uint16;
typedef int32_t fs_int32;
typedef uint32_t fs_uint32;
typedef int64_t fs_int64;
typedef uint64_t fs_uint64;
#else
typedef signed char fs_int8;
typedef unsigned char fs_uint8;
typedef signed short fs_int16;
typedef unsigned short fs_uint16;
typedef signed int fs_int32;
typedef unsigned int fs_uint32;
#if defined(_MSC_VER) && !defined(__clang__)
typedef signed __int64 fs_int64;
typedef unsigned __int64 fs_uint64;
#else
#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)))
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wlong-long"
#if defined(__clang__)
#pragma GCC diagnostic ignored "-Wc++11-long-long"
#endif
#endif
typedef signed long long fs_int64;
typedef unsigned long long fs_uint64;
#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)))
#pragma GCC diagnostic pop
#endif
#endif
#endif /* FS_USE_STDINT */