egra: checkbox cosmetix
[iv.d.git] / vfs / main.d
blob99f4efb3e555fb309644fbc29d261c8566995c1d
1 /* Invisible Vector Library
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 // VFS pathes and data files
18 module iv.vfs.main /*is aliced*/;
19 private:
21 import core.time;
22 import iv.alice;
23 import iv.vfs.config;
24 import iv.vfs.pred;
25 import iv.vfs.error;
26 import iv.vfs.vfile;
27 import iv.vfs.posixci;
28 import iv.vfs.koi8;
29 static import core.sync.mutex;
32 // ////////////////////////////////////////////////////////////////////////// //
33 private shared int vfsLockedFlag = 0; // !0: can't add/remove drivers
34 shared bool vflagIgnoreCasePak = true; // ignore file name case in pak files; driver can ignore this, but it shouldn't
35 shared bool vflagIgnoreCaseDisk = false; // ignore file name case for disk files
37 /// get "ingore filename case" flag (default: true)
38 public @property bool vfsIgnoreCasePak() () nothrow @trusted @nogc { import core.atomic : atomicLoad; return atomicLoad(vflagIgnoreCasePak); }
40 /// set "ingore filename case" flag
41 public @property void vfsIgnoreCasePak() (bool v) nothrow @trusted @nogc { import core.atomic : atomicStore; return atomicStore(vflagIgnoreCasePak, v); }
43 /// get "ingore filename case" flag when no archive files are attached (default: false)
44 public @property bool vfsIgnoreCaseDisk() () nothrow @trusted @nogc { import core.atomic : atomicLoad; return atomicLoad(vflagIgnoreCaseDisk); }
46 /// set "ingore filename case" flag when no archive files are attached
47 public @property void vfsIgnoreCaseDisk() (bool v) nothrow @trusted @nogc { import core.atomic : atomicStore; return atomicStore(vflagIgnoreCaseDisk, v); }
50 // ////////////////////////////////////////////////////////////////////////// //
51 private import core.stdc.stdio : FILE;
53 private FILE* dofopen (const(char)* fname, const(char)* mode) nothrow @nogc {
54 version(Posix) {
55 import core.sys.posix.stdio;
56 return fopen64(fname, mode);
57 } else {
58 import core.stdc.stdio;
59 return fopen(fname, mode);
64 // ////////////////////////////////////////////////////////////////////////// //
65 public struct VFSVariant {
66 public:
67 enum Type : ubyte {
68 Empty = 0,
69 Bool,
70 Int,
71 Long,
72 Float,
73 Double,
74 String,
77 private:
78 union {
79 bool bv;
80 int iv;
81 long lv;
82 float fv;
83 double dv;
85 string sv; // moved here, so GC will not scan numbers
86 Type vtype = Type.Empty;
88 public:
89 string toString () const {
90 import core.stdc.stdio : snprintf;
91 char[256] buf = void;
92 final switch (vtype) {
93 case Type.Empty: return "(empty)";
94 case Type.Bool: return (bv ? "true" : "false");
95 case Type.Int: auto len = snprintf(buf.ptr, buf.length, "%d", iv); return buf[0..len].idup;
96 case Type.Long: auto len = snprintf(buf.ptr, buf.length, "%lld", lv); return buf[0..len].idup;
97 case Type.Float: auto len = snprintf(buf.ptr, buf.length, "%f", cast(double)fv); return buf[0..len].idup;
98 case Type.Double: auto len = snprintf(buf.ptr, buf.length, "%f", dv); return buf[0..len].idup;
99 case Type.String: return sv;
103 nothrow @trusted @nogc {
104 this (bool v) { vtype = Type.Bool; bv = v; }
105 this (int v) { vtype = Type.Int; iv = v; }
106 this (long v) { vtype = Type.Long; lv = v; }
107 this (float v) { vtype = Type.Float; fv = v; }
108 this (double v) { vtype = Type.Double; dv = v; }
109 this(T:const(char)[]) (T v) {
110 vtype = Type.String;
111 static if (is(T == typeof(null))) sv = null;
112 else static if (is(T == string)) sv = v;
113 else sv = v.idup;
116 @property Type type () const pure { return vtype; }
118 @property bool hasValue () const pure { return (vtype != Type.Empty); }
119 @property bool isString () const pure { return (vtype == Type.String); }
120 @property bool isInteger () const pure { return (vtype == Type.Int || vtype == Type.Long); }
121 @property bool isFloating () const pure { return (vtype == Type.Float || vtype == Type.Double); }
124 @property T get(T) () const /*pure*/ {
125 static if (is(T == bool)) {
126 final switch (vtype) {
127 case Type.Empty: return false;
128 case Type.Bool: return bv;
129 case Type.Int: return (iv != 0);
130 case Type.Long: return (lv != 0);
131 case Type.Float: return (fv != 0);
132 case Type.Double: return (dv != 0);
133 case Type.String: return (sv.length != 0);
135 } else static if (is(T == int) || is(T == uint) || is(T == long) || is(T == ulong) || is(T == float) || is(T == double)) {
136 final switch (vtype) {
137 case Type.Empty: return cast(T)0;
138 case Type.Bool: return cast(T)(bv ? 1 : 0);
139 case Type.Int: return cast(T)iv;
140 case Type.Long: return cast(T)lv;
141 case Type.Float: return cast(T)fv;
142 case Type.Double: return cast(T)dv;
143 case Type.String: return cast(T)0; //assert(0, "cannot get string value as number from VFSVariant");
145 } else static if (is(T == string)) {
146 return this.toString();
147 } else {
148 static assert(0, "cannot convert VFSVariant to '"~T.stringof~"'");
154 // ////////////////////////////////////////////////////////////////////////// //
155 __gshared core.sync.mutex.Mutex ptlock;
156 shared static this () { ptlock = new core.sync.mutex.Mutex; }
158 // non-empty msg: check lockflag
159 private struct VFSLock {
160 int locked = 0;
162 @disable this (this);
164 ~this () {
165 if (locked) {
166 import core.atomic;
167 if (locked < 0) assert(0, "vfs: internal error");
168 atomicOp!"-="(vfsLockedFlag, 1);
169 ptlock.unlock();
170 locked = -1;
175 private auto vfsLockIntr() (string msg=null) {
176 import core.atomic;
177 ptlock.lock();
178 if (msg.length) {
179 if (atomicLoad(vfsLockedFlag)) {
180 ptlock.unlock();
181 throw new VFSException(msg);
184 VFSLock lk;
185 lk.locked = 1;
186 atomicOp!"+="(vfsLockedFlag, 1);
187 return lk;
191 // ////////////////////////////////////////////////////////////////////////// //
192 /// abstract class for VFS drivers
193 public abstract class VFSDriver {
194 /// for dir range
195 public static struct DirEntry {
196 VFSDriverId drvid; // assigned only in `vfsFileList()` or `vfsForEachFile()`
197 usize index; // should be set by the driver
198 string name; // for disk: doesn't include base path; ends with '/'
199 long size; // can be -1 if size is not known; for dirs means nothing
201 this(T:const(char)[]) (T aname, long asize) {
202 static if (is(T == typeof(null))) name = null;
203 else static if (is(T == string)) name = aname;
204 else name = aname.idup;
205 index = index.max; // just in case
206 size = asize;
209 this(T:const(char)[]) (uint idx, T aname, long asize) {
210 static if (is(T == typeof(null))) name = null;
211 else static if (is(T == string)) name = aname;
212 else name = aname.idup;
213 index = idx;
214 size = asize;
217 VFSVariant stat (const(char)[] propname) const { return vfsStat(this, propname); }
220 /// this constructor is used for disk drivers
221 this () {}
223 /// this constructor is used for archive drivers.
224 /// `prefixpath`: this will be prepended to each name from archive, unmodified.
225 this (VFile fl, const(char)[] prefixpath) { throw new VFSException("not implemented for abstract driver"); }
227 /// try to find and open the file in archive.
228 /// should return `VFile.init` if no file was found.
229 /// should not throw (except for VERY unrecoverable error).
230 /// doesn't do any security checks, 'cause i don't care.
231 abstract VFile tryOpen (const(char)[] fname, bool ignoreCase);
233 /// get number of entries in archive directory.
234 @property usize dirLength () { return 0; }
236 /// get directory entry with the given index. can throw, but it's not necessary.
237 DirEntry dirEntry (usize idx) { return DirEntry.init; }
239 /** query various file properties; driver-specific.
240 * properties of interest:
241 * "cretime" -- creation time; unixtime, UTC
242 * "modtime" -- modify time; unixtime, UTC
243 * "pksize" -- packed file size (for archives)
244 * "crc32" -- crc32 value for some archives
246 VFSVariant stat (usize idx, const(char)[] propname) { return VFSVariant(); }
248 @property bool isDisk () { return false; }
252 /// abstract class for "pak" files
253 public abstract class VFSDriverDetector {
254 /// return null if it can't open the thing.
255 /// `prefixpath`: this will be prepended to each name from archive, unmodified.
256 abstract VFSDriver tryOpen (VFile fl, const(char)[] prefixpath);
260 // ////////////////////////////////////////////////////////////////////////// //
261 /// you can register this driver to try disk files with the given data path
262 public VFSDriver vfsNewDiskDriver(T:const(char)[]) (T dpath=null) {
263 return new VFSDriverDiskImpl!true(dpath);
266 /// you can register this driver to try disk files with the given data path
267 public VFSDriver vfsNewDiskDriverListed(bool needtime=false, T:const(char)[]) (T dpath=null) {
268 return new VFSDriverDiskListedImpl!needtime(dpath);
271 /// you can register this driver to try disk files with the given data path
272 public VFSDriver vfsNewFailDriver () {
273 return new VFSDriverAlwaysFail();
277 // ////////////////////////////////////////////////////////////////////////// //
278 // you can register this driver as "last" to prevent disk searches
279 final class VFSDriverAlwaysFail : VFSDriver {
280 override VFile tryOpen (const(char)[] fname, bool ignoreCase) {
281 throw new VFSException("can't open file '"~fname.idup~"'");
285 // this is disk driver implementation; it is templated to cut compile times
286 class VFSDriverDiskImpl(bool dummy) : VFSDriver {
287 protected:
288 string dataPath;
290 public:
291 this () { dataPath = "./"; }
293 this(T : const(char)[]) (T dpath) {
294 static if (is(T == typeof(null))) {
295 dataPath = "./";
296 } else {
297 if (dpath.length == 0) {
298 dataPath = "./";
299 } else if (dpath[$-1] == '/') {
300 static if (is(T == string)) dataPath = dpath; else dataPath = dpath.idup;
301 } else if (dpath[0] == '~') {
302 import iv.vfs.util;
303 auto buf = new char[](dpath.length+1025);
304 auto dp = expandTilde(buf[0..$-1], dpath);
305 if (dp is null) assert(0, "out of memory in iv.vfs disk driver");
306 if (dp[$-1] != '/') { buf[dp.length] = '/'; dp = buf[0..dp.length+1]; }
307 dataPath = cast(string)dp; // it is safe to cast here
308 } else {
309 static if (is(T == string)) dataPath = dpath~"/"; else dataPath = dpath.idup~"/";
314 /// doesn't do any security checks, 'cause i don't care
315 override VFile tryOpen (const(char)[] fname, bool ignoreCase) {
316 static import core.stdc.stdio;
317 import core.stdc.stdlib : alloca;
318 if (fname.length == 0) return VFile.init;
319 if (fname.length > 1024*3) return VFile.init; // arbitrary limit
320 char* nbuf;
321 if (fname[0] == '/') {
322 if (dataPath[0] != '/' || fname.length <= dataPath.length) {
323 bool hit = (ignoreCase ? koi8StrCaseEqu(fname[0..dataPath.length], dataPath) : fname[0..dataPath.length] == dataPath);
324 if (!hit) return VFile.init;
326 nbuf = cast(char*)alloca(fname.length+1);
327 nbuf[0..fname.length] = fname[];
328 nbuf[fname.length] = '\0';
329 } else if (fname[0] == '~') {
330 import iv.vfs.util;
331 uint nepos = 1;
332 while (nepos < fname.length && fname[nepos] != '/') ++nepos;
333 uint len = cast(uint)fname.length-nepos+257;
334 if (len > 1024*3) return VFile.init;
335 nbuf = cast(char*)alloca(len);
336 nbuf[0..len] = 0;
337 auto up = expandTilde(nbuf[0..len-1], fname);
338 if (up is null) return VFile.init;
339 up.ptr[up.length] = 0;
340 } else {
341 if (dataPath.length > 4096 || fname.length > 4096 || dataPath.length+fname.length > 1024*3) return VFile.init;
342 nbuf = cast(char*)alloca(dataPath.length+fname.length+1);
343 nbuf[0..dataPath.length] = dataPath[];
344 nbuf[dataPath.length..dataPath.length+fname.length] = fname[];
345 nbuf[dataPath.length+fname.length] = '\0';
347 static if (VFS_NORMAL_OS) if (ignoreCase) {
348 import core.stdc.string : strlen;
349 uint len = cast(uint)strlen(nbuf);
350 auto pt = findPathCI(nbuf[0..len]);
351 if (pt is null) return VFile.init;
352 nbuf[pt.length] = '\0';
354 static if (VFS_NORMAL_OS) {
355 auto fl = dofopen(nbuf, "r");
356 } else {
357 auto fl = dofopen(nbuf, "rb");
359 if (fl is null) return VFile.init;
360 usize nblen = 0; while (nbuf[nblen] != 0) ++nblen;
361 try { return VFile(fl, nbuf[0..nblen]); } catch (Exception e) {}
362 core.stdc.stdio.fclose(fl);
363 return VFile.init;
366 override @property bool isDisk () { return true; }
369 // same as `VFSDriverDisk`, but provides file list too; it is templated to cut compile times
370 class VFSDriverDiskListedImpl(bool needtime) : VFSDriverDiskImpl!true {
371 private static struct FileEntry {
372 //uint index; // should be set by the driver
373 string name;
374 long size;
375 ulong modtime; // 0: unknown; unixtime
378 protected:
379 FileEntry[] files;
380 bool flistInited;
382 protected:
383 final void buildFileList () {
384 import std.file : DE = DirEntry, dirEntries, SpanMode;
385 if (flistInited) return;
386 try {
387 foreach (DE de; dirEntries(dataPath, SpanMode./*breadth*/depth)) {
388 if (!de.isFile) continue;
389 if (de.name.length <= dataPath.length) continue;
390 static if (needtime) {
391 import std.datetime;
392 files ~= FileEntry(de.name[dataPath.length..$], de.size, de.timeLastModified.toUTC.toUnixTime());
393 } else {
394 files ~= FileEntry(de.name[dataPath.length..$], de.size);
397 } catch (Exception e) {}
398 flistInited = true;
401 public:
402 this(T : const(char)[]) (T dpath) { super(dpath); }
404 /// get number of entries in archive directory.
405 override @property usize dirLength () { buildFileList(); return files.length; }
406 /// get directory entry with the given index. can throw, but it's not necessary.
407 override DirEntry dirEntry (usize idx) { buildFileList(); return DirEntry(idx, files[idx].name, files[idx].size); }
409 override VFSVariant stat (usize idx, const(char)[] name) {
410 buildFileList();
411 if (name == "arcname") return VFSVariant("disk");
412 if (idx < files.length && name == "packed") return VFSVariant(false);
413 if (idx < files.length && name == "modtime" && files[idx].modtime) return VFSVariant(files[idx].modtime);
414 if (idx < files.length && (name == "size" || name == "pksize")) { import std.file : getSize; return VFSVariant(files[idx].name.getSize); }
415 return VFSVariant();
420 // ////////////////////////////////////////////////////////////////////////// //
421 public struct VFSDriverId {
422 private:
423 uint id;
424 static VFSDriverId create () nothrow @trusted @nogc { return VFSDriverId(++lastIdNumber); }
426 public const pure nothrow @safe @nogc {
427 bool opCast(T) () if (is(T == bool)) { return (id != 0); }
428 @property bool valid () { return (id != 0); }
429 bool opEquals() (const VFSDriverId b) { return (id == b.id); }
432 private:
433 __gshared uint lastIdNumber;
436 struct DriverInfo {
437 enum Mode { Normal, First, Last }
438 Mode mode;
439 VFSDriver drv;
440 string fname;
441 string prefixpath;
442 VFSDriverId drvid;
443 bool temp; // temporary? will not be used in listing, and eventually removed
444 MonoTime tempUsedTime; // last used time for temp paks
446 this (Mode amode, VFSDriver adrv, string afname, string apfxpath, VFSDriverId adid, bool atemp) {
447 mode = amode;
448 drv = adrv;
449 fname = afname;
450 prefixpath = apfxpath;
451 drvid = adid;
452 temp = atemp;
453 if (atemp) tempUsedTime = MonoTime.currTime;
457 __gshared DriverInfo[] drivers;
458 __gshared uint tempDrvCount = 0;
460 private void cleanupDrivers (VFSDriverId did=VFSDriverId(0)) {
461 if (tempDrvCount == 0) return;
462 bool ctValid = false;
463 MonoTime ct;
464 auto idx = drivers.length;
465 while (idx > 0) {
466 --idx;
467 if (!drivers.ptr[idx].temp) continue;
468 if (drivers.ptr[idx].drvid == did) continue; // keep this one
469 if (!ctValid) { ct = MonoTime.currTime; ctValid = true; }
470 if ((ct-drivers.ptr[idx].tempUsedTime).total!"seconds" >= 5) {
471 // remove it
472 --tempDrvCount;
473 foreach (immutable c; idx+1..drivers.length) drivers.ptr[c-1] = drivers.ptr[c];
474 drivers.length -= 1;
475 drivers.assumeSafeAppend;
476 if (idx == 0 || tempDrvCount == 0) return;
477 --idx;
483 // ////////////////////////////////////////////////////////////////////////// //
484 /// register new VFS driver
485 /// driver order is: firsts, normals, lasts (obviously ;-).
486 /// search order is from first to last, in reverse order inside each driver.
487 /// you can use returned driver id to unregister the driver later.
488 public VFSDriverId vfsRegister(string mode="normal", bool temp=false) (VFSDriver drv, const(char)[] fname=null, const(char)[] prefixpath=null) {
489 static assert(mode == "normal" || mode == "last" || mode == "first");
490 import core.atomic : atomicOp;
491 if (drv is null) return VFSDriverId.init;
492 auto lock = vfsLockIntr("can't register drivers in list operations");
493 cleanupDrivers();
494 VFSDriverId did = VFSDriverId.create;
495 static if (temp) ++tempDrvCount;
496 static if (mode == "normal") {
497 // normal
498 usize ipos = drivers.length;
499 while (ipos > 0 && drivers[ipos-1].mode == DriverInfo.Mode.First) --ipos;
500 if (ipos == drivers.length) {
501 drivers ~= DriverInfo(DriverInfo.Mode.Normal, drv, fname.idup, prefixpath.idup, did, temp);
502 } else {
503 drivers.length += 1;
504 foreach_reverse (immutable c; ipos+1..drivers.length) drivers[c] = drivers[c-1];
505 drivers[ipos] = DriverInfo(DriverInfo.Mode.Normal, drv, fname.idup, prefixpath.idup, did, temp);
507 } else static if (mode == "first") {
508 // first
509 drivers ~= DriverInfo(DriverInfo.Mode.First, drv, fname.idup, prefixpath.idup, did, temp);
510 } else static if (mode == "last") {
511 drivers = [DriverInfo(DriverInfo.Mode.Last, drv, fname.idup, prefixpath.idup, did, temp)]~drivers;
512 } else {
513 static assert(0, "wtf?!");
515 return did;
519 // ////////////////////////////////////////////////////////////////////////// //
520 /// find driver. returns invalid VFSDriverId if the driver was not found.
521 /// WARNING: don't add new drivers while this is in progress!
522 public VFSDriverId vfsFindDriver() (const(char)[] fname, const(char)[] prefixpath=null) {
523 auto lock = vfsLockIntr();
524 foreach_reverse (immutable idx, ref drv; drivers) {
525 if (!drv.temp && drv.fname == fname && drv.prefixpath == prefixpath) return drv.drvid;
527 return VFSDriverId.init;
531 // ////////////////////////////////////////////////////////////////////////// //
532 /// unregister driver
533 /// WARNING: don't add new drivers while this is in progress!
534 public bool vfsUnregister() (VFSDriverId id) {
535 auto lock = vfsLockIntr("can't unregister drivers in list operations");
536 cleanupDrivers();
537 foreach_reverse (immutable idx, ref drv; drivers) {
538 if (drv.drvid == id) {
539 // i found her!
540 foreach (immutable c; idx+1..drivers.length) drivers[c-1] = drivers[c];
541 drivers.length -= 1;
542 drivers.assumeSafeAppend;
543 return true;
546 return false;
550 // ////////////////////////////////////////////////////////////////////////// //
551 /// list all files known to VFS.
552 /// WARNING: don't add new drivers while this is in process!
553 public VFSDriver.DirEntry[] vfsFileList() () {
554 usize[string] filesSeen;
555 VFSDriver.DirEntry[] res;
557 auto lock = vfsLockIntr();
558 cleanupDrivers();
559 foreach_reverse (ref drvnfo; drivers) {
560 if (drvnfo.temp) continue;
561 foreach_reverse (immutable idx; 0..drvnfo.drv.dirLength) {
562 auto de = drvnfo.drv.dirEntry(idx);
563 if (de.name.length == 0) continue;
564 de.drvid = drvnfo.drvid;
565 if (auto iptr = de.name in filesSeen) {
566 res.ptr[*iptr] = de;
567 } else {
568 filesSeen[de.name] = res.length;
569 res ~= de;
573 return res;
577 /// call callback for each known file in VFS. return non-zero from callback to stop.
578 /// WARNING: don't add new drivers while this is in process!
579 public int vfsForEachFile() (scope int delegate (in ref VFSDriver.DirEntry de) cb) {
580 if (cb is null) return 0;
582 auto lock = vfsLockIntr();
583 cleanupDrivers();
584 bool[string] filesSeen;
585 foreach_reverse (ref drvnfo; drivers) {
586 if (drvnfo.temp) continue;
587 foreach_reverse (immutable idx; 0..drvnfo.drv.dirLength) {
588 auto de = drvnfo.drv.dirEntry(idx);
589 if (de.name !in filesSeen) {
590 filesSeen[de.name] = true;
591 de.drvid = drvnfo.drvid;
592 if (auto res = cb(de)) return res;
596 return 0;
600 /// Ditto.
601 public void vfsForEachFile() (scope void delegate (in ref VFSDriver.DirEntry de) cb) {
602 if (cb !is null) vfsForEachFile((in ref VFSDriver.DirEntry de) { cb(de); return 0; });
606 /// call callback for each known file in the given driver. return non-zero from callback to stop.
607 /// WARNING: don't add new drivers while this is in process!
608 /// WARNING: can return duplicate dir entries!
609 public int vfsForEachFileInPak() (VFSDriverId did, scope int delegate (in ref VFSDriver.DirEntry de) cb) {
610 if (cb is null || !did.valid) return 0;
612 auto lock = vfsLockIntr();
613 cleanupDrivers(did); // but keep did
614 foreach_reverse (ref drvnfo; drivers) {
615 if (drvnfo.drvid != did) continue;
616 foreach_reverse (immutable idx; 0..drvnfo.drv.dirLength) {
617 auto de = drvnfo.drv.dirEntry(idx);
618 de.drvid = drvnfo.drvid;
619 if (auto res = cb(de)) return res;
621 break;
623 return 0;
627 /// Ditto.
628 public void vfsForEachFileInPak() (VFSDriverId did, scope void delegate (in ref VFSDriver.DirEntry de) cb) {
629 if (cb !is null) vfsForEachFileInPak(did, (in ref VFSDriver.DirEntry de) { cb(de); return 0; });
633 /// query various things from driver
634 public VFSVariant vfsStat() (in ref VFSDriver.DirEntry de, const(char)[] propname) {
635 if (!de.drvid.valid) return VFSVariant();
636 auto lock = vfsLockIntr();
637 cleanupDrivers();
638 foreach_reverse (ref drvnfo; drivers) {
639 if (drvnfo.drvid == de.drvid) return drvnfo.drv.stat(de.index, propname);
641 return VFSVariant();
645 // ////////////////////////////////////////////////////////////////////////// //
646 struct ModeOptions {
647 enum bool3 { def = -1, no = 0, yes = 1 }
648 bool3 ignoreCase;
649 bool allowPaks;
650 bool3 allowGZ;
651 bool wantWrite;
652 bool wantRead;
653 bool wantAppend;
654 private char[16] newmodebuf=0; // 0-terminated
655 private int nmblen; // 0 is not included
656 @property const(char)[] mode () const pure nothrow @safe @nogc { return newmodebuf[0..nmblen]; }
658 this (const(char)[] mode) { parse(mode); }
660 void parse (const(char)[] mode) {
661 nmblen = 0;
662 newmodebuf[] = 0;
663 allowPaks = true;
664 allowGZ = bool3.def;
665 wantWrite = false;
666 wantRead = false;
667 ignoreCase = bool3.def;
668 wantAppend = false;
669 bool hasPlus = false;
670 //static if (!VFS_NORMAL_OS) char btm = 'b';
671 foreach (char ch; mode) {
672 if (ch == 'i') { ignoreCase = bool3.yes; continue; }
673 if (ch == 'I') { ignoreCase = bool3.no; continue; }
674 if (ch == 'X') { allowPaks = false; continue; }
675 if (ch == 'x') { allowPaks = true; continue; }
676 if (ch == 'Z') { allowGZ = bool3.no; continue; }
677 if (ch == 'z') { allowGZ = bool3.yes; continue; } // force gzip
678 if (ch == 'r' || ch == 'R') { wantRead = true; continue; }
679 if (ch == 'w' || ch == 'W') { wantWrite = true; continue; }
680 if (ch == 'a' || ch == 'A') { wantAppend = true; continue; }
681 if (ch == '+') { hasPlus = true; continue; }
682 if (ch == 'b' || ch == 't' || ch == 'B' || ch == 'T') { /*btm = ch;*/ continue; }
684 // fix mode
685 if (wantRead && wantWrite) hasPlus = true;
686 if (!wantRead && !wantWrite && !wantAppend) wantRead = true;
687 // build `newmodebuf`
688 if (wantAppend) { newmodebuf.ptr[nmblen++] = 'a'; if (hasPlus) newmodebuf.ptr[nmblen++] = '+'; }
689 else if (wantWrite) { newmodebuf.ptr[nmblen++] = 'w'; if (hasPlus) newmodebuf.ptr[nmblen++] = '+'; }
690 else if (wantRead) { newmodebuf.ptr[nmblen++] = 'r'; if (hasPlus) newmodebuf.ptr[nmblen++] = '+'; }
691 else assert(0, "internal VFS error");
692 // add 'b' for idiotic shitdoze
693 static if (!VFS_NORMAL_OS) newmodebuf.ptr[nmblen++] = 'b';
694 newmodebuf[nmblen++] = '\0';
695 // fix flags
696 if (hasPlus) { wantRead = true; wantWrite = true; }
697 if (wantAppend) wantWrite = true;
702 // ////////////////////////////////////////////////////////////////////////// //
703 /** open file; this is the function used by `VFile("zub")`.
705 * funny additional file modes:
706 * i: ignore case (default is `vflagIgnoreCase` for paks, and `vflagIgnoreCaseNoDat` for no paks)
707 * I: case sensitive
708 * x: allow searching in paks (default)
709 * X: skip paks
710 * z: allow transparent gzip unpacking (default)
711 * Z: disable transparent gzip unpacking
713 public VFile vfsOpenFile(T:const(char)[], bool usefname=true) (T fname, const(char)[] mode=null) {
714 static import core.stdc.stdio;
716 void error (string msg, Throwable e=null, string file=__FILE__, usize line=__LINE__) { throw new VFSException(msg, file, line, e); }
718 void errorfn(T:const(char)[]) (T msg, Throwable e=null, string file=__FILE__, usize line=__LINE__) {
719 static assert(!is(T == typeof(null)), "wtf?!");
720 foreach (char cc; msg) {
721 if (cc == '!') {
722 //import core.memory : GC;
723 char[] str;
724 str.reserve(msg.length+fname.length);
725 //if (str.ptr is GC.addrOf(str.ptr)) GC.setAttr(str.ptr, GC.BlkAttr.NO_INTERIOR);
726 foreach (char ch; msg) {
727 if (ch == '!') {
728 //foreach (char xch; fname) s.put(xch);
729 str ~= fname;
730 } else {
731 //s.put(ch);
732 str ~= ch;
735 throw new VFSException(cast(string)str, file, line, e); // it is safe to cast here
738 throw new VFSException(msg, file, line, e);
741 static if (is(T == typeof(null))) {
742 error("can't open file ''");
743 } else {
744 if (fname.length == 0) error("can't open file ''");
746 auto mopt = ModeOptions(mode);
748 // try ".gz"
749 //TODO: transparently read gzipped files from archives
750 if (mopt.allowGZ != ModeOptions.bool3.no && !mopt.wantWrite) {
751 if (mopt.allowGZ == ModeOptions.bool3.yes) {
752 import etc.c.zlib;
753 import std.internal.cstring;
754 gzFile gf = gzopen(fname[].tempCString, "rb");
755 if (gf !is null) return VFile.OpenGZ(gf, fname);
756 } else {
757 int epe = cast(int)fname.length;
758 while (epe > 1 && fname[epe-1] != '.') --epe;
759 if (fname.length-epe == 2 && (fname[epe] == 'G' || fname[epe] == 'g') && (fname[epe+1] == 'Z' || fname[epe+1] == 'z')) {
760 //{ import core.stdc.stdio : stderr, fprintf; stderr.fprintf("TRYING GZ: '%.*s'\n", cast(int)fname.length, fname.ptr); }
761 import etc.c.zlib;
762 import std.internal.cstring;
763 gzFile gf = gzopen(fname[].tempCString, "rb");
764 if (gf !is null) return VFile.OpenGZ(gf, fname[0..epe-1]);
769 if (mopt.allowGZ == ModeOptions.bool3.yes && mopt.wantWrite) {
770 import etc.c.zlib;
771 import std.internal.cstring;
772 gzFile gf = gzopen(fname[].tempCString, (mopt.wantRead ? "r+\0" : "w\0").ptr);
773 if (gf !is null) return VFile.OpenGZ(gf, fname);
776 if (mopt.wantRead) {
777 auto lock = vfsLockIntr();
778 cleanupDrivers();
779 // try all drivers
780 bool ignoreCase = (mopt.ignoreCase == mopt.bool3.def ? vfsIgnoreCasePak : (mopt.ignoreCase == mopt.bool3.yes));
781 foreach_reverse (ref di; drivers) {
782 try {
783 if (!mopt.wantWrite || !di.drv.isDisk) {
784 auto fl = di.drv.tryOpen(fname, ignoreCase);
785 if (fl.isOpen) {
786 if (mopt.wantWrite) errorfn("can't open file '!' in non-binary non-readonly mode");
787 if (di.temp) di.tempUsedTime = MonoTime.currTime;
788 return fl;
791 } catch (Exception e) {
792 // chain
793 errorfn("can't open file '!'", e);
798 // no drivers found, try disk file
799 //{ import core.stdc.stdio : stderr, fprintf; stderr.fprintf("TRYING DISK: '%.*s'\n", cast(int)fname.length, fname.ptr); }
800 return vfsDiskOpen!(T, usefname)(fname, mode);
805 // ////////////////////////////////////////////////////////////////////////// //
806 struct DetectorInfo {
807 enum Mode { Normal, First, Last }
808 Mode mode;
809 VFSDriverDetector dt;
812 __gshared DetectorInfo[] detectors;
815 // ////////////////////////////////////////////////////////////////////////// //
816 /// register pak (archive) format detector.
817 public void vfsRegisterDetector(string mode="normal") (VFSDriverDetector dt) {
818 static assert(mode == "normal" || mode == "last" || mode == "first");
819 if (dt is null) return;
820 auto lock = vfsLockIntr("can't register drivers in list operations");
821 static if (mode == "normal") {
822 // normal
823 usize ipos = detectors.length;
824 while (ipos > 0 && detectors[ipos-1].mode == DetectorInfo.Mode.Last) --ipos;
825 if (ipos == detectors.length) {
826 detectors ~= DetectorInfo(DetectorInfo.Mode.Normal, dt);
827 } else {
828 detectors.length += 1;
829 foreach_reverse (immutable c; ipos+1..detectors.length) detectors[c] = detectors[c-1];
830 detectors[ipos] = DetectorInfo(DetectorInfo.Mode.Normal, dt);
832 } else static if (mode == "last") {
833 detectors ~= DetectorInfo(DetectorInfo.Mode.First, dt);
834 } else static if (mode == "first") {
835 detectors = [DetectorInfo(DetectorInfo.Mode.Last, dt)]~detectors;
836 } else {
837 static assert(0, "wtf?!");
842 // ////////////////////////////////////////////////////////////////////////// //
843 /// `prefixpath`: this will be prepended to each name from archive, unmodified.
844 public VFSDriverId vfsAddPak(string mode="normal", bool temp=false) (const(char)[] fname, const(char)[] prefixpath=null) {
845 foreach (char ch; fname) {
846 if (ch == ':') {
847 try {
848 return vfsAddPak!(mode, temp)(vfsOpenFile(fname), fname, prefixpath);
849 } catch (Exception) {}
850 return vfsAddPak!(mode, temp)(vfsDiskOpen(fname), fname, prefixpath);
853 try {
854 return vfsAddPak!(mode, temp)(vfsDiskOpen(fname), fname, prefixpath);
855 } catch (Exception) {}
856 return vfsAddPak!(mode, temp)(vfsOpenFile(fname), fname, prefixpath);
860 /// `prefixpath`: this was prepended to each name from archive.
861 public VFSDriverId vfsFindPack (const(char)[] fname, const(char)[] prefixpath=null) {
862 return vfsFindDriver(fname, prefixpath);
867 public bool vfsRemovePak() (VFSDriverId id) {
868 return vfsUnregister(id);
872 /// `prefixpath`: this will be prepended to each name from archive, unmodified.
873 public VFSDriverId vfsAddPak(string mode="normal", bool temp=false, T : const(char)[]) (VFile fl, T fname="", const(char)[] prefixpath=null) {
874 static assert(mode == "normal" || mode == "last" || mode == "first");
875 static if (is(T == typeof(null))) {
876 return vfsAddPak!(mode, temp, T)(fl, "", prefixpath);
877 } else {
878 void error (Throwable e=null, string file=__FILE__, usize line=__LINE__) {
879 if (fname.length == 0) {
880 throw new VFSException("can't open pak file", file, line, e);
881 } else {
882 throw new VFSException("can't open pak file '"~fname.idup~"'", file, line, e);
886 if (!fl.isOpen) error();
888 auto lock = vfsLockIntr("can't register drivers in list operations");
889 // try all detectors
890 foreach (ref di; detectors) {
891 try {
892 fl.seek(0);
893 auto drv = di.dt.tryOpen(fl, prefixpath);
894 if (drv !is null) {
895 // hack!
896 import core.atomic;
897 atomicOp!"-="(vfsLockedFlag, 1);
898 scope(exit) atomicOp!"+="(vfsLockedFlag, 1);
899 return vfsRegister!(mode, temp)(drv, fname, prefixpath);
901 } catch (Exception e) {
902 // chain
903 error(e);
906 error();
907 assert(0);
912 // ////////////////////////////////////////////////////////////////////////// //
913 /// takes into account `vfsIgnoreCaseDisk` flag. you can override it with 'i' (on) or 'I' (off) mode letter.
914 public VFile vfsDiskOpen(T:const(char)[], bool usefname=true) (T fname, const(char)[] mode=null) {
915 static import core.stdc.stdio;
916 static if (is(T == typeof(null))) {
917 throw new VFSException("can't open file ''");
918 } else {
919 if (fname.length == 0) throw new VFSException("can't open file ''");
920 if (fname.length > 2048) throw new VFSException("can't open file '"~fname.idup~"'");
921 auto mopt = ModeOptions(mode);
922 char[2049] nbuf = 0;
923 uint nblen = 0;
924 if (fname[0] == '~') {
925 import iv.vfs.util;
926 auto up = expandTilde(nbuf[0..$-1], fname);
927 if (up is null) throw new VFSException("can't open file '"~fname.idup~"'");
928 up.ptr[up.length] = 0;
929 assert(nbuf[$-1] == 0);
930 nblen = cast(uint)up.length;
931 } else {
932 nbuf[0..fname.length] = fname[];
933 nbuf[fname.length] = '\0';
934 nblen = cast(uint)fname.length;
936 static if (VFS_NORMAL_OS) if (mopt.ignoreCase == mopt.bool3.yes || (mopt.ignoreCase == mopt.bool3.def && vfsIgnoreCaseDisk)) {
937 // we have to lock here, as `findPathCI()` is not thread-safe
938 auto lock = vfsLockIntr();
939 auto pt = findPathCI(nbuf[0..nblen]);
940 if (pt is null) {
941 // restore filename for correct error message
942 nbuf[] = 0;
943 if (fname[0] == '~') {
944 import iv.vfs.util;
945 auto up = expandTilde(nbuf[0..$-1], fname);
946 assert(up !is null);
947 assert(nbuf[$-1] == 0);
948 up.ptr[up.length] = 0;
949 assert(nbuf[$-1] == 0);
950 nblen = cast(uint)up.length;
951 } else {
952 nbuf[0..fname.length] = fname[];
953 nbuf[fname.length] = '\0';
954 nblen = cast(uint)fname.length;
956 } else {
957 nbuf[pt.length] = '\0';
958 nblen = cast(uint)pt.length;
961 // try packs?
962 if (mopt.allowPaks && !mopt.wantWrite) {
963 uint colonpos = 0;
964 version(Windows) {
965 // idiotic shitdoze
966 if (nbuf[0] && nbuf[1] == ':') colonpos = 2;
968 while (nbuf[colonpos] && nbuf[colonpos] != ':') ++colonpos;
969 if (nbuf[colonpos]) {
970 try {
971 import core.stdc.string : strlen;
972 auto fl = openFileWithPaks!(char[], usefname)(nbuf[0..strlen(nbuf.ptr)], mode);
973 if (fl.isOpen) return fl;
974 } catch (Exception e) {
978 // normal disk file
979 //{ import core.stdc.stdio : stderr, fprintf; stderr.fprintf("USING FOPEN: '%s' '%s'\n", nbuf.ptr, mopt.mode.ptr); }
980 auto fl = dofopen(nbuf.ptr, mopt.mode.ptr);
981 if (fl is null) throw new VFSException("can't open file '"~fname.idup~"'");
982 scope(failure) core.stdc.stdio.fclose(fl); // just in case
983 try {
984 static if (usefname) {
985 return VFile(fl, fname);
986 } else {
987 return VFile(fl, nbuf[0..strlen(nbuf)]); //???
989 } catch (Exception e) {
990 // chain
991 //{ import core.stdc.stdio : stderr, fprintf; stderr.fprintf("USING FOPEN ERROR!\n"); }
992 throw new VFSException("can't open file '"~fname.idup~"'", __FILE__, __LINE__, e);
998 //FIXME: usefname!
999 VFile openFileWithPaks(T:const(char)[], bool usefname=true) (T name, const(char)[] mode) {
1000 static assert(!is(T == typeof(null)));
1001 assert(name.length < int.max/4);
1003 // check if name has any prefix at all
1004 int pfxend = cast(int)name.length;
1005 while (pfxend > 0 && name.ptr[pfxend-1] != ':') --pfxend;
1006 version(Windows) {
1007 // idiotic shitdoze
1008 if (pfxend <= 1) return VFile.init; // easy case
1009 } else {
1010 if (pfxend == 0) return VFile.init; // easy case
1013 auto lock = vfsLockIntr();
1015 // cpos: after last succesfull prefix
1016 VFile openit (int cpos) {
1017 bool nopaks = (cpos == 0);
1018 version(Windows) {
1019 // idiotic shitdoze
1020 if (cpos == 0 && name.length > 2 && name.ptr[1] == ':') cpos = 2;
1022 while (cpos < name.length) {
1023 auto ep = cpos;
1024 while (ep < name.length && name.ptr[ep] != ':') ++ep;
1025 if (ep >= name.length) return VFile(name, "rXIz");
1026 if (name.length-ep == 1) throw new VFSException("can't open file '"~name.idup~"'");
1028 // hack!
1029 import core.atomic;
1030 atomicOp!"-="(vfsLockedFlag, 1);
1031 scope(exit) atomicOp!"+="(vfsLockedFlag, 1);
1032 vfsAddPak!("normal", true)(name[0..ep], name[0..ep+1]);
1034 cpos = ep+1;
1036 throw new VFSException("can't open file '"~name.idup~"'"); // just in case
1039 // try to find the longest prefix which already exists
1040 while (pfxend > 0) {
1041 foreach (ref di; drivers) {
1042 if (di.prefixpath == name[0..pfxend]) {
1043 // i found her!
1044 if (di.temp) di.tempUsedTime = MonoTime.currTime;
1045 return openit(pfxend);
1048 --pfxend;
1049 while (pfxend > 0 && name.ptr[pfxend-1] != ':') --pfxend;
1050 version(Windows) {
1051 // idiotic shitdoze
1052 if (pfxend <= 1) break;
1055 return openit(0);
1059 // ////////////////////////////////////////////////////////////////////////// //
1060 /// open VFile with the given name. supports "file.pak:file1.pak:file.ext" pathes.
1061 /// kept for compatibility with old code, standard `VFile(path)` can do that now.
1062 public VFile openFileEx() (const(char)[] name) {
1063 if (name.length >= int.max/4) throw new VFSException("name too long");
1065 // check if name has any prefix at all
1066 int pfxend = cast(int)name.length;
1067 while (pfxend > 0 && name.ptr[pfxend-1] != ':') --pfxend;
1068 version(Windows) {
1069 // idiotic shitdoze
1070 if (pfxend <= 1) return VFile(name); // easy case
1071 } else {
1072 if (pfxend == 0) return VFile(name); // easy case
1075 auto lock = vfsLockIntr();
1077 // cpos: after last succesfull prefix
1078 VFile openit (int cpos) {
1079 version(Windows) {
1080 // idiotic shitdoze
1081 if (cpos == 0 && name.length > 2 && name.ptr[1] == ':') cpos = 2;
1083 while (cpos < name.length) {
1084 auto ep = cpos;
1085 while (ep < name.length && name.ptr[ep] != ':') ++ep;
1086 if (ep >= name.length) return VFile(name);
1087 if (name.length-ep == 1) throw new VFSException("can't open file '"~name.idup~"'");
1089 // hack!
1090 import core.atomic;
1091 atomicOp!"-="(vfsLockedFlag, 1);
1092 scope(exit) atomicOp!"+="(vfsLockedFlag, 1);
1093 vfsAddPak!("normal", true)(name[0..ep], name[0..ep+1]);
1095 cpos = ep+1;
1097 throw new VFSException("can't open file '"~name.idup~"'"); // just in case
1100 // try to find the longest prefix which already exists
1101 while (pfxend > 0) {
1102 foreach (ref di; drivers) {
1103 if (di.prefixpath == name[0..pfxend]) {
1104 // i found her!
1105 if (di.temp) di.tempUsedTime = MonoTime.currTime;
1106 return openit(pfxend);
1109 --pfxend;
1110 while (pfxend > 0 && name.ptr[pfxend-1] != ':') --pfxend;
1111 version(Windows) {
1112 // idiotic shitdoze
1113 if (pfxend <= 1) break;
1116 return openit(0);