egeditor: more VaVoom C highlighting
[iv.d.git] / vfs / main.d
blob4f726b9410ef028c9b1080aff88bea832594148e
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, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 // VFS pathes and data files
19 module iv.vfs.main /*is aliced*/;
20 private:
22 import core.time;
23 import iv.alice;
24 import iv.vfs.config;
25 import iv.vfs.pred;
26 import iv.vfs.error;
27 import iv.vfs.vfile;
28 import iv.vfs.posixci;
29 import iv.vfs.koi8;
30 static import core.sync.mutex;
33 // ////////////////////////////////////////////////////////////////////////// //
34 private shared int vfsLockedFlag = 0; // !0: can't add/remove drivers
35 shared bool vflagIgnoreCasePak = true; // ignore file name case in pak files; driver can ignore this, but it shouldn't
36 shared bool vflagIgnoreCaseDisk = false; // ignore file name case for disk files
38 /// get "ingore filename case" flag (default: true)
39 public @property bool vfsIgnoreCasePak() () nothrow @trusted @nogc { import core.atomic : atomicLoad; return atomicLoad(vflagIgnoreCasePak); }
41 /// set "ingore filename case" flag
42 public @property void vfsIgnoreCasePak() (bool v) nothrow @trusted @nogc { import core.atomic : atomicStore; return atomicStore(vflagIgnoreCasePak, v); }
44 /// get "ingore filename case" flag when no archive files are attached (default: false)
45 public @property bool vfsIgnoreCaseDisk() () nothrow @trusted @nogc { import core.atomic : atomicLoad; return atomicLoad(vflagIgnoreCaseDisk); }
47 /// set "ingore filename case" flag when no archive files are attached
48 public @property void vfsIgnoreCaseDisk() (bool v) nothrow @trusted @nogc { import core.atomic : atomicStore; return atomicStore(vflagIgnoreCaseDisk, v); }
51 // ////////////////////////////////////////////////////////////////////////// //
52 private import core.stdc.stdio : FILE;
54 private FILE* dofopen (const(char)* fname, const(char)* mode) nothrow @nogc {
55 version(Posix) {
56 import core.sys.posix.stdio;
57 return fopen64(fname, mode);
58 } else {
59 import core.stdc.stdio;
60 return fopen(fname, mode);
65 // ////////////////////////////////////////////////////////////////////////// //
66 public struct VFSVariant {
67 public:
68 enum Type : ubyte {
69 Empty = 0,
70 Bool,
71 Int,
72 Long,
73 Float,
74 Double,
75 String,
78 private:
79 union {
80 bool bv;
81 int iv;
82 long lv;
83 float fv;
84 double dv;
86 string sv; // moved here, so GC will not scan numbers
87 Type vtype = Type.Empty;
89 public:
90 string toString () const {
91 import core.stdc.stdio : snprintf;
92 char[256] buf = void;
93 final switch (vtype) {
94 case Type.Empty: return "(empty)";
95 case Type.Bool: return (bv ? "true" : "false");
96 case Type.Int: auto len = snprintf(buf.ptr, buf.length, "%d", iv); return buf[0..len].idup;
97 case Type.Long: auto len = snprintf(buf.ptr, buf.length, "%lld", lv); return buf[0..len].idup;
98 case Type.Float: auto len = snprintf(buf.ptr, buf.length, "%f", cast(double)fv); return buf[0..len].idup;
99 case Type.Double: auto len = snprintf(buf.ptr, buf.length, "%f", dv); return buf[0..len].idup;
100 case Type.String: return sv;
104 nothrow @trusted @nogc {
105 this (bool v) { vtype = Type.Bool; bv = v; }
106 this (int v) { vtype = Type.Int; iv = v; }
107 this (long v) { vtype = Type.Long; lv = v; }
108 this (float v) { vtype = Type.Float; fv = v; }
109 this (double v) { vtype = Type.Double; dv = v; }
110 this(T:const(char)[]) (T v) {
111 vtype = Type.String;
112 static if (is(T == typeof(null))) sv = null;
113 else static if (is(T == string)) sv = v;
114 else sv = v.idup;
117 @property Type type () const pure { return vtype; }
119 @property bool hasValue () const pure { return (vtype != Type.Empty); }
120 @property bool isString () const pure { return (vtype == Type.String); }
121 @property bool isInteger () const pure { return (vtype == Type.Int || vtype == Type.Long); }
122 @property bool isFloating () const pure { return (vtype == Type.Float || vtype == Type.Double); }
125 @property T get(T) () const /*pure*/ {
126 static if (is(T == bool)) {
127 final switch (vtype) {
128 case Type.Empty: return false;
129 case Type.Bool: return bv;
130 case Type.Int: return (iv != 0);
131 case Type.Long: return (lv != 0);
132 case Type.Float: return (fv != 0);
133 case Type.Double: return (dv != 0);
134 case Type.String: return (sv.length != 0);
136 } else static if (is(T == int) || is(T == uint) || is(T == long) || is(T == ulong) || is(T == float) || is(T == double)) {
137 final switch (vtype) {
138 case Type.Empty: return cast(T)0;
139 case Type.Bool: return cast(T)(bv ? 1 : 0);
140 case Type.Int: return cast(T)iv;
141 case Type.Long: return cast(T)lv;
142 case Type.Float: return cast(T)fv;
143 case Type.Double: return cast(T)dv;
144 case Type.String: return cast(T)0; //assert(0, "cannot get string value as number from VFSVariant");
146 } else static if (is(T == string)) {
147 return this.toString();
148 } else {
149 static assert(0, "cannot convert VFSVariant to '"~T.stringof~"'");
155 // ////////////////////////////////////////////////////////////////////////// //
156 __gshared core.sync.mutex.Mutex ptlock;
157 shared static this () { ptlock = new core.sync.mutex.Mutex; }
159 // non-empty msg: check lockflag
160 private struct VFSLock {
161 int locked = 0;
163 @disable this (this);
165 ~this () {
166 if (locked) {
167 import core.atomic;
168 if (locked < 0) assert(0, "vfs: internal error");
169 atomicOp!"-="(vfsLockedFlag, 1);
170 ptlock.unlock();
171 locked = -1;
176 private auto vfsLockIntr() (string msg=null) {
177 import core.atomic;
178 ptlock.lock();
179 if (msg.length) {
180 if (atomicLoad(vfsLockedFlag)) {
181 ptlock.unlock();
182 throw new VFSException(msg);
185 VFSLock lk;
186 lk.locked = 1;
187 atomicOp!"+="(vfsLockedFlag, 1);
188 return lk;
192 // ////////////////////////////////////////////////////////////////////////// //
193 /// abstract class for VFS drivers
194 public abstract class VFSDriver {
195 /// for dir range
196 public static struct DirEntry {
197 VFSDriverId drvid; // assigned only in `vfsFileList()` or `vfsForEachFile()`
198 usize index; // should be set by the driver
199 string name; // for disk: doesn't include base path; ends with '/'
200 long size; // can be -1 if size is not known; for dirs means nothing
202 this(T:const(char)[]) (T aname, long asize) {
203 static if (is(T == typeof(null))) name = null;
204 else static if (is(T == string)) name = aname;
205 else name = aname.idup;
206 index = index.max; // just in case
207 size = asize;
210 this(T:const(char)[]) (uint idx, T aname, long asize) {
211 static if (is(T == typeof(null))) name = null;
212 else static if (is(T == string)) name = aname;
213 else name = aname.idup;
214 index = idx;
215 size = asize;
218 VFSVariant stat (const(char)[] propname) const { return vfsStat(this, propname); }
221 /// this constructor is used for disk drivers
222 this () {}
224 /// this constructor is used for archive drivers.
225 /// `prefixpath`: this will be prepended to each name from archive, unmodified.
226 this (VFile fl, const(char)[] prefixpath) { throw new VFSException("not implemented for abstract driver"); }
228 /// try to find and open the file in archive.
229 /// should return `VFile.init` if no file was found.
230 /// should not throw (except for VERY unrecoverable error).
231 /// doesn't do any security checks, 'cause i don't care.
232 abstract VFile tryOpen (const(char)[] fname, bool ignoreCase);
234 /// get number of entries in archive directory.
235 @property usize dirLength () { return 0; }
237 /// get directory entry with the given index. can throw, but it's not necessary.
238 DirEntry dirEntry (usize idx) { return DirEntry.init; }
240 /** query various file properties; driver-specific.
241 * properties of interest:
242 * "cretime" -- creation time; unixtime, UTC
243 * "modtime" -- modify time; unixtime, UTC
244 * "pksize" -- packed file size (for archives)
245 * "crc32" -- crc32 value for some archives
247 VFSVariant stat (usize idx, const(char)[] propname) { return VFSVariant(); }
249 @property bool isDisk () { return false; }
253 /// abstract class for "pak" files
254 public abstract class VFSDriverDetector {
255 /// return null if it can't open the thing.
256 /// `prefixpath`: this will be prepended to each name from archive, unmodified.
257 abstract VFSDriver tryOpen (VFile fl, const(char)[] prefixpath);
261 // ////////////////////////////////////////////////////////////////////////// //
262 /// you can register this driver to try disk files with the given data path
263 public VFSDriver vfsNewDiskDriver(T:const(char)[]) (T dpath=null) {
264 return new VFSDriverDiskImpl!true(dpath);
267 /// you can register this driver to try disk files with the given data path
268 public VFSDriver vfsNewDiskDriverListed(bool needtime=false, T:const(char)[]) (T dpath=null) {
269 return new VFSDriverDiskListedImpl!needtime(dpath);
272 /// you can register this driver to try disk files with the given data path
273 public VFSDriver vfsNewFailDriver () {
274 return new VFSDriverAlwaysFail();
278 // ////////////////////////////////////////////////////////////////////////// //
279 // you can register this driver as "last" to prevent disk searches
280 final class VFSDriverAlwaysFail : VFSDriver {
281 override VFile tryOpen (const(char)[] fname, bool ignoreCase) {
282 throw new VFSException("can't open file '"~fname.idup~"'");
286 // this is disk driver implementation; it is templated to cut compile times
287 class VFSDriverDiskImpl(bool dummy) : VFSDriver {
288 protected:
289 string dataPath;
291 public:
292 this () { dataPath = "./"; }
294 this(T : const(char)[]) (T dpath) {
295 static if (is(T == typeof(null))) {
296 dataPath = "./";
297 } else {
298 if (dpath.length == 0) {
299 dataPath = "./";
300 } else if (dpath[$-1] == '/') {
301 static if (is(T == string)) dataPath = dpath; else dataPath = dpath.idup;
302 } else if (dpath[0] == '~') {
303 import iv.vfs.util;
304 auto buf = new char[](dpath.length+1025);
305 auto dp = expandTilde(buf[0..$-1], dpath);
306 if (dp is null) assert(0, "out of memory in iv.vfs disk driver");
307 if (dp[$-1] != '/') { buf[dp.length] = '/'; dp = buf[0..dp.length+1]; }
308 dataPath = cast(string)dp; // it is safe to cast here
309 } else {
310 static if (is(T == string)) dataPath = dpath~"/"; else dataPath = dpath.idup~"/";
315 /// doesn't do any security checks, 'cause i don't care
316 override VFile tryOpen (const(char)[] fname, bool ignoreCase) {
317 static import core.stdc.stdio;
318 import core.stdc.stdlib : alloca;
319 if (fname.length == 0) return VFile.init;
320 if (fname.length > 1024*3) return VFile.init; // arbitrary limit
321 char* nbuf;
322 if (fname[0] == '/') {
323 if (dataPath[0] != '/' || fname.length <= dataPath.length) {
324 bool hit = (ignoreCase ? koi8StrCaseEqu(fname[0..dataPath.length], dataPath) : fname[0..dataPath.length] == dataPath);
325 if (!hit) return VFile.init;
327 nbuf = cast(char*)alloca(fname.length+1);
328 nbuf[0..fname.length] = fname[];
329 nbuf[fname.length] = '\0';
330 } else if (fname[0] == '~') {
331 import iv.vfs.util;
332 uint nepos = 1;
333 while (nepos < fname.length && fname[nepos] != '/') ++nepos;
334 uint len = cast(uint)fname.length-nepos+257;
335 if (len > 1024*3) return VFile.init;
336 nbuf = cast(char*)alloca(len);
337 nbuf[0..len] = 0;
338 auto up = expandTilde(nbuf[0..len-1], fname);
339 if (up is null) return VFile.init;
340 up.ptr[up.length] = 0;
341 } else {
342 if (dataPath.length > 4096 || fname.length > 4096 || dataPath.length+fname.length > 1024*3) return VFile.init;
343 nbuf = cast(char*)alloca(dataPath.length+fname.length+1);
344 nbuf[0..dataPath.length] = dataPath[];
345 nbuf[dataPath.length..dataPath.length+fname.length] = fname[];
346 nbuf[dataPath.length+fname.length] = '\0';
348 static if (VFS_NORMAL_OS) if (ignoreCase) {
349 import core.stdc.string : strlen;
350 uint len = cast(uint)strlen(nbuf);
351 auto pt = findPathCI(nbuf[0..len]);
352 if (pt is null) return VFile.init;
353 nbuf[pt.length] = '\0';
355 static if (VFS_NORMAL_OS) {
356 auto fl = dofopen(nbuf, "r");
357 } else {
358 auto fl = dofopen(nbuf, "rb");
360 if (fl is null) return VFile.init;
361 usize nblen = 0; while (nbuf[nblen] != 0) ++nblen;
362 try { return VFile(fl, nbuf[0..nblen]); } catch (Exception e) {}
363 core.stdc.stdio.fclose(fl);
364 return VFile.init;
367 override @property bool isDisk () { return true; }
370 // same as `VFSDriverDisk`, but provides file list too; it is templated to cut compile times
371 class VFSDriverDiskListedImpl(bool needtime) : VFSDriverDiskImpl!true {
372 private static struct FileEntry {
373 //uint index; // should be set by the driver
374 string name;
375 long size;
376 ulong modtime; // 0: unknown; unixtime
379 protected:
380 FileEntry[] files;
381 bool flistInited;
383 protected:
384 final void buildFileList () {
385 import std.file : DE = DirEntry, dirEntries, SpanMode;
386 if (flistInited) return;
387 try {
388 foreach (DE de; dirEntries(dataPath, SpanMode./*breadth*/depth)) {
389 if (!de.isFile) continue;
390 if (de.name.length <= dataPath.length) continue;
391 static if (needtime) {
392 import std.datetime;
393 files ~= FileEntry(de.name[dataPath.length..$], de.size, de.timeLastModified.toUTC.toUnixTime());
394 } else {
395 files ~= FileEntry(de.name[dataPath.length..$], de.size);
398 } catch (Exception e) {}
399 flistInited = true;
402 public:
403 this(T : const(char)[]) (T dpath) { super(dpath); }
405 /// get number of entries in archive directory.
406 override @property usize dirLength () { buildFileList(); return files.length; }
407 /// get directory entry with the given index. can throw, but it's not necessary.
408 override DirEntry dirEntry (usize idx) { buildFileList(); return DirEntry(idx, files[idx].name, files[idx].size); }
410 override VFSVariant stat (usize idx, const(char)[] name) {
411 buildFileList();
412 if (name == "arcname") return VFSVariant("disk");
413 if (idx < files.length && name == "packed") return VFSVariant(false);
414 if (idx < files.length && name == "modtime" && files[idx].modtime) return VFSVariant(files[idx].modtime);
415 if (idx < files.length && (name == "size" || name == "pksize")) { import std.file : getSize; return VFSVariant(files[idx].name.getSize); }
416 return VFSVariant();
421 // ////////////////////////////////////////////////////////////////////////// //
422 public struct VFSDriverId {
423 private:
424 uint id;
425 static VFSDriverId create () nothrow @trusted @nogc { return VFSDriverId(++lastIdNumber); }
427 public const pure nothrow @safe @nogc {
428 bool opCast(T) () if (is(T == bool)) { return (id != 0); }
429 @property bool valid () { return (id != 0); }
430 bool opEquals() (const VFSDriverId b) { return (id == b.id); }
433 private:
434 __gshared uint lastIdNumber;
437 struct DriverInfo {
438 enum Mode { Normal, First, Last }
439 Mode mode;
440 VFSDriver drv;
441 string fname;
442 string prefixpath;
443 VFSDriverId drvid;
444 bool temp; // temporary? will not be used in listing, and eventually removed
445 MonoTime tempUsedTime; // last used time for temp paks
447 this (Mode amode, VFSDriver adrv, string afname, string apfxpath, VFSDriverId adid, bool atemp) {
448 mode = amode;
449 drv = adrv;
450 fname = afname;
451 prefixpath = apfxpath;
452 drvid = adid;
453 temp = atemp;
454 if (atemp) tempUsedTime = MonoTime.currTime;
458 __gshared DriverInfo[] drivers;
459 __gshared uint tempDrvCount = 0;
461 private void cleanupDrivers (VFSDriverId did=VFSDriverId(0)) {
462 if (tempDrvCount == 0) return;
463 bool ctValid = false;
464 MonoTime ct;
465 auto idx = drivers.length;
466 while (idx > 0) {
467 --idx;
468 if (!drivers.ptr[idx].temp) continue;
469 if (drivers.ptr[idx].drvid == did) continue; // keep this one
470 if (!ctValid) { ct = MonoTime.currTime; ctValid = true; }
471 if ((ct-drivers.ptr[idx].tempUsedTime).total!"seconds" >= 5) {
472 // remove it
473 --tempDrvCount;
474 foreach (immutable c; idx+1..drivers.length) drivers.ptr[c-1] = drivers.ptr[c];
475 drivers.length -= 1;
476 drivers.assumeSafeAppend;
477 if (idx == 0 || tempDrvCount == 0) return;
478 --idx;
484 // ////////////////////////////////////////////////////////////////////////// //
485 /// register new VFS driver
486 /// driver order is: firsts, normals, lasts (obviously ;-).
487 /// search order is from first to last, in reverse order inside each driver.
488 /// you can use returned driver id to unregister the driver later.
489 public VFSDriverId vfsRegister(string mode="normal", bool temp=false) (VFSDriver drv, const(char)[] fname=null, const(char)[] prefixpath=null) {
490 static assert(mode == "normal" || mode == "last" || mode == "first");
491 import core.atomic : atomicOp;
492 if (drv is null) return VFSDriverId.init;
493 auto lock = vfsLockIntr("can't register drivers in list operations");
494 cleanupDrivers();
495 VFSDriverId did = VFSDriverId.create;
496 static if (temp) ++tempDrvCount;
497 static if (mode == "normal") {
498 // normal
499 usize ipos = drivers.length;
500 while (ipos > 0 && drivers[ipos-1].mode == DriverInfo.Mode.First) --ipos;
501 if (ipos == drivers.length) {
502 drivers ~= DriverInfo(DriverInfo.Mode.Normal, drv, fname.idup, prefixpath.idup, did, temp);
503 } else {
504 drivers.length += 1;
505 foreach_reverse (immutable c; ipos+1..drivers.length) drivers[c] = drivers[c-1];
506 drivers[ipos] = DriverInfo(DriverInfo.Mode.Normal, drv, fname.idup, prefixpath.idup, did, temp);
508 } else static if (mode == "first") {
509 // first
510 drivers ~= DriverInfo(DriverInfo.Mode.First, drv, fname.idup, prefixpath.idup, did, temp);
511 } else static if (mode == "last") {
512 drivers = [DriverInfo(DriverInfo.Mode.Last, drv, fname.idup, prefixpath.idup, did, temp)]~drivers;
513 } else {
514 static assert(0, "wtf?!");
516 return did;
520 // ////////////////////////////////////////////////////////////////////////// //
521 /// find driver. returns invalid VFSDriverId if the driver was not found.
522 /// WARNING: don't add new drivers while this is in progress!
523 public VFSDriverId vfsFindDriver() (const(char)[] fname, const(char)[] prefixpath=null) {
524 auto lock = vfsLockIntr();
525 foreach_reverse (immutable idx, ref drv; drivers) {
526 if (!drv.temp && drv.fname == fname && drv.prefixpath == prefixpath) return drv.drvid;
528 return VFSDriverId.init;
532 // ////////////////////////////////////////////////////////////////////////// //
533 /// unregister driver
534 /// WARNING: don't add new drivers while this is in progress!
535 public bool vfsUnregister() (VFSDriverId id) {
536 auto lock = vfsLockIntr("can't unregister drivers in list operations");
537 cleanupDrivers();
538 foreach_reverse (immutable idx, ref drv; drivers) {
539 if (drv.drvid == id) {
540 // i found her!
541 foreach (immutable c; idx+1..drivers.length) drivers[c-1] = drivers[c];
542 drivers.length -= 1;
543 drivers.assumeSafeAppend;
544 return true;
547 return false;
551 // ////////////////////////////////////////////////////////////////////////// //
552 /// list all files known to VFS.
553 /// WARNING: don't add new drivers while this is in process!
554 public VFSDriver.DirEntry[] vfsFileList() () {
555 usize[string] filesSeen;
556 VFSDriver.DirEntry[] res;
558 auto lock = vfsLockIntr();
559 cleanupDrivers();
560 foreach_reverse (ref drvnfo; drivers) {
561 if (drvnfo.temp) continue;
562 foreach_reverse (immutable idx; 0..drvnfo.drv.dirLength) {
563 auto de = drvnfo.drv.dirEntry(idx);
564 if (de.name.length == 0) continue;
565 de.drvid = drvnfo.drvid;
566 if (auto iptr = de.name in filesSeen) {
567 res.ptr[*iptr] = de;
568 } else {
569 filesSeen[de.name] = res.length;
570 res ~= de;
574 return res;
578 /// call callback for each known file in VFS. return non-zero from callback to stop.
579 /// WARNING: don't add new drivers while this is in process!
580 public int vfsForEachFile() (scope int delegate (in ref VFSDriver.DirEntry de) cb) {
581 if (cb is null) return 0;
583 auto lock = vfsLockIntr();
584 cleanupDrivers();
585 bool[string] filesSeen;
586 foreach_reverse (ref drvnfo; drivers) {
587 if (drvnfo.temp) continue;
588 foreach_reverse (immutable idx; 0..drvnfo.drv.dirLength) {
589 auto de = drvnfo.drv.dirEntry(idx);
590 if (de.name !in filesSeen) {
591 filesSeen[de.name] = true;
592 de.drvid = drvnfo.drvid;
593 if (auto res = cb(de)) return res;
597 return 0;
601 /// Ditto.
602 public void vfsForEachFile() (scope void delegate (in ref VFSDriver.DirEntry de) cb) {
603 if (cb !is null) vfsForEachFile((in ref VFSDriver.DirEntry de) { cb(de); return 0; });
607 /// call callback for each known file in the given driver. return non-zero from callback to stop.
608 /// WARNING: don't add new drivers while this is in process!
609 /// WARNING: can return duplicate dir entries!
610 public int vfsForEachFileInPak() (VFSDriverId did, scope int delegate (in ref VFSDriver.DirEntry de) cb) {
611 if (cb is null || !did.valid) return 0;
613 auto lock = vfsLockIntr();
614 cleanupDrivers(did); // but keep did
615 foreach_reverse (ref drvnfo; drivers) {
616 if (drvnfo.drvid != did) continue;
617 foreach_reverse (immutable idx; 0..drvnfo.drv.dirLength) {
618 auto de = drvnfo.drv.dirEntry(idx);
619 de.drvid = drvnfo.drvid;
620 if (auto res = cb(de)) return res;
622 break;
624 return 0;
628 /// Ditto.
629 public void vfsForEachFileInPak() (VFSDriverId did, scope void delegate (in ref VFSDriver.DirEntry de) cb) {
630 if (cb !is null) vfsForEachFileInPak(did, (in ref VFSDriver.DirEntry de) { cb(de); return 0; });
634 /// query various things from driver
635 public VFSVariant vfsStat() (in ref VFSDriver.DirEntry de, const(char)[] propname) {
636 if (!de.drvid.valid) return VFSVariant();
637 auto lock = vfsLockIntr();
638 cleanupDrivers();
639 foreach_reverse (ref drvnfo; drivers) {
640 if (drvnfo.drvid == de.drvid) return drvnfo.drv.stat(de.index, propname);
642 return VFSVariant();
646 // ////////////////////////////////////////////////////////////////////////// //
647 struct ModeOptions {
648 enum bool3 { def = -1, no = 0, yes = 1 }
649 bool3 ignoreCase;
650 bool allowPaks;
651 bool3 allowGZ;
652 bool wantWrite;
653 bool wantRead;
654 bool wantAppend;
655 private char[16] newmodebuf=0; // 0-terminated
656 private int nmblen; // 0 is not included
657 @property const(char)[] mode () const pure nothrow @safe @nogc { return newmodebuf[0..nmblen]; }
659 this (const(char)[] mode) { parse(mode); }
661 void parse (const(char)[] mode) {
662 nmblen = 0;
663 newmodebuf[] = 0;
664 allowPaks = true;
665 allowGZ = bool3.def;
666 wantWrite = false;
667 wantRead = false;
668 ignoreCase = bool3.def;
669 wantAppend = 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') { wantWrite = true; wantAppend = true; continue; }
681 if (ch == '+') { wantRead = true; wantWrite = true; continue; }
682 if (ch == 'b' || ch == 't') { /*btm = ch;*/ continue; }
684 // fix mode
685 if (!wantRead && !wantWrite) wantRead = true;
686 // build `newmodebuf`
687 if (wantRead && wantWrite && wantAppend) { newmodebuf.ptr[nmblen++] = 'a'; newmodebuf.ptr[nmblen++] = '+'; }
688 else if (wantRead && wantWrite) { newmodebuf.ptr[nmblen++] = 'r'; newmodebuf.ptr[nmblen++] = '+'; }
689 else if (wantRead) newmodebuf.ptr[nmblen++] = 'r';
690 else if (wantWrite) newmodebuf.ptr[nmblen++] = 'w';
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';
699 // ////////////////////////////////////////////////////////////////////////// //
700 /** open file; this is the function used by `VFile("zub")`.
702 * funny additional file modes:
703 * i: ignore case (default is `vflagIgnoreCase` for paks, and `vflagIgnoreCaseNoDat` for no paks)
704 * I: case sensitive
705 * x: allow searching in paks (default)
706 * X: skip paks
707 * z: allow transparent gzip unpacking (default)
708 * Z: disable transparent gzip unpacking
710 public VFile vfsOpenFile(T:const(char)[], bool usefname=true) (T fname, const(char)[] mode=null) {
711 static import core.stdc.stdio;
713 void error (string msg, Throwable e=null, string file=__FILE__, usize line=__LINE__) { throw new VFSException(msg, file, line, e); }
715 void errorfn(T:const(char)[]) (T msg, Throwable e=null, string file=__FILE__, usize line=__LINE__) {
716 static assert(!is(T == typeof(null)), "wtf?!");
717 foreach (char cc; msg) {
718 if (cc == '!') {
719 //import core.memory : GC;
720 char[] str;
721 str.reserve(msg.length+fname.length);
722 //if (str.ptr is GC.addrOf(str.ptr)) GC.setAttr(str.ptr, GC.BlkAttr.NO_INTERIOR);
723 foreach (char ch; msg) {
724 if (ch == '!') {
725 //foreach (char xch; fname) s.put(xch);
726 str ~= fname;
727 } else {
728 //s.put(ch);
729 str ~= ch;
732 throw new VFSException(cast(string)str, file, line, e); // it is safe to cast here
735 throw new VFSException(msg, file, line, e);
738 static if (is(T == typeof(null))) {
739 error("can't open file ''");
740 } else {
741 if (fname.length == 0) error("can't open file ''");
743 auto mopt = ModeOptions(mode);
745 // try ".gz"
746 //TODO: transparently read gzipped files from archives
747 if (mopt.allowGZ != ModeOptions.bool3.no && !mopt.wantWrite) {
748 if (mopt.allowGZ == ModeOptions.bool3.yes) {
749 import etc.c.zlib;
750 import std.internal.cstring;
751 gzFile gf = gzopen(fname[].tempCString, "rb");
752 if (gf !is null) return VFile.OpenGZ(gf, fname);
753 } else {
754 int epe = cast(int)fname.length;
755 while (epe > 1 && fname[epe-1] != '.') --epe;
756 if (fname.length-epe == 2 && (fname[epe] == 'G' || fname[epe] == 'g') && (fname[epe+1] == 'Z' || fname[epe+1] == 'z')) {
757 //{ import core.stdc.stdio : stderr, fprintf; stderr.fprintf("TRYING GZ: '%.*s'\n", cast(int)fname.length, fname.ptr); }
758 import etc.c.zlib;
759 import std.internal.cstring;
760 gzFile gf = gzopen(fname[].tempCString, "rb");
761 if (gf !is null) return VFile.OpenGZ(gf, fname[0..epe-1]);
766 if (mopt.allowGZ == ModeOptions.bool3.yes && mopt.wantWrite) {
767 import etc.c.zlib;
768 import std.internal.cstring;
769 gzFile gf = gzopen(fname[].tempCString, (mopt.wantRead ? "r+\0" : "w\0").ptr);
770 if (gf !is null) return VFile.OpenGZ(gf, fname);
773 if (mopt.wantRead) {
774 auto lock = vfsLockIntr();
775 cleanupDrivers();
776 // try all drivers
777 bool ignoreCase = (mopt.ignoreCase == mopt.bool3.def ? vfsIgnoreCasePak : (mopt.ignoreCase == mopt.bool3.yes));
778 foreach_reverse (ref di; drivers) {
779 try {
780 if (!mopt.wantWrite || !di.drv.isDisk) {
781 auto fl = di.drv.tryOpen(fname, ignoreCase);
782 if (fl.isOpen) {
783 if (mopt.wantWrite) errorfn("can't open file '!' in non-binary non-readonly mode");
784 if (di.temp) di.tempUsedTime = MonoTime.currTime;
785 return fl;
788 } catch (Exception e) {
789 // chain
790 errorfn("can't open file '!'", e);
795 // no drivers found, try disk file
796 //{ import core.stdc.stdio : stderr, fprintf; stderr.fprintf("TRYING DISK: '%.*s'\n", cast(int)fname.length, fname.ptr); }
797 return vfsDiskOpen!(T, usefname)(fname, mode);
802 // ////////////////////////////////////////////////////////////////////////// //
803 struct DetectorInfo {
804 enum Mode { Normal, First, Last }
805 Mode mode;
806 VFSDriverDetector dt;
809 __gshared DetectorInfo[] detectors;
812 // ////////////////////////////////////////////////////////////////////////// //
813 /// register pak (archive) format detector.
814 public void vfsRegisterDetector(string mode="normal") (VFSDriverDetector dt) {
815 static assert(mode == "normal" || mode == "last" || mode == "first");
816 if (dt is null) return;
817 auto lock = vfsLockIntr("can't register drivers in list operations");
818 static if (mode == "normal") {
819 // normal
820 usize ipos = detectors.length;
821 while (ipos > 0 && detectors[ipos-1].mode == DetectorInfo.Mode.Last) --ipos;
822 if (ipos == detectors.length) {
823 detectors ~= DetectorInfo(DetectorInfo.Mode.Normal, dt);
824 } else {
825 detectors.length += 1;
826 foreach_reverse (immutable c; ipos+1..detectors.length) detectors[c] = detectors[c-1];
827 detectors[ipos] = DetectorInfo(DetectorInfo.Mode.Normal, dt);
829 } else static if (mode == "last") {
830 detectors ~= DetectorInfo(DetectorInfo.Mode.First, dt);
831 } else static if (mode == "first") {
832 detectors = [DetectorInfo(DetectorInfo.Mode.Last, dt)]~detectors;
833 } else {
834 static assert(0, "wtf?!");
839 // ////////////////////////////////////////////////////////////////////////// //
840 /// `prefixpath`: this will be prepended to each name from archive, unmodified.
841 public VFSDriverId vfsAddPak(string mode="normal", bool temp=false) (const(char)[] fname, const(char)[] prefixpath=null) {
842 foreach (char ch; fname) {
843 if (ch == ':') {
844 try {
845 return vfsAddPak!(mode, temp)(vfsOpenFile(fname), fname, prefixpath);
846 } catch (Exception) {}
847 return vfsAddPak!(mode, temp)(vfsDiskOpen(fname), fname, prefixpath);
850 try {
851 return vfsAddPak!(mode, temp)(vfsDiskOpen(fname), fname, prefixpath);
852 } catch (Exception) {}
853 return vfsAddPak!(mode, temp)(vfsOpenFile(fname), fname, prefixpath);
857 /// `prefixpath`: this was prepended to each name from archive.
858 public VFSDriverId vfsFindPack (const(char)[] fname, const(char)[] prefixpath=null) {
859 return vfsFindDriver(fname, prefixpath);
864 public bool vfsRemovePak() (VFSDriverId id) {
865 return vfsUnregister(id);
869 /// `prefixpath`: this will be prepended to each name from archive, unmodified.
870 public VFSDriverId vfsAddPak(string mode="normal", bool temp=false, T : const(char)[]) (VFile fl, T fname="", const(char)[] prefixpath=null) {
871 static assert(mode == "normal" || mode == "last" || mode == "first");
872 static if (is(T == typeof(null))) {
873 return vfsAddPak!(mode, temp, T)(fl, "", prefixpath);
874 } else {
875 void error (Throwable e=null, string file=__FILE__, usize line=__LINE__) {
876 if (fname.length == 0) {
877 throw new VFSException("can't open pak file", file, line, e);
878 } else {
879 throw new VFSException("can't open pak file '"~fname.idup~"'", file, line, e);
883 if (!fl.isOpen) error();
885 auto lock = vfsLockIntr("can't register drivers in list operations");
886 // try all detectors
887 foreach (ref di; detectors) {
888 try {
889 fl.seek(0);
890 auto drv = di.dt.tryOpen(fl, prefixpath);
891 if (drv !is null) {
892 // hack!
893 import core.atomic;
894 atomicOp!"-="(vfsLockedFlag, 1);
895 scope(exit) atomicOp!"+="(vfsLockedFlag, 1);
896 return vfsRegister!(mode, temp)(drv, fname, prefixpath);
898 } catch (Exception e) {
899 // chain
900 error(e);
903 error();
904 assert(0);
909 // ////////////////////////////////////////////////////////////////////////// //
910 /// takes into account `vfsIgnoreCaseDisk` flag. you can override it with 'i' (on) or 'I' (off) mode letter.
911 public VFile vfsDiskOpen(T:const(char)[], bool usefname=true) (T fname, const(char)[] mode=null) {
912 static import core.stdc.stdio;
913 static if (is(T == typeof(null))) {
914 throw new VFSException("can't open file ''");
915 } else {
916 if (fname.length == 0) throw new VFSException("can't open file ''");
917 if (fname.length > 2048) throw new VFSException("can't open file '"~fname.idup~"'");
918 auto mopt = ModeOptions(mode);
919 char[2049] nbuf = 0;
920 uint nblen = 0;
921 if (fname[0] == '~') {
922 import iv.vfs.util;
923 auto up = expandTilde(nbuf[0..$-1], fname);
924 if (up is null) throw new VFSException("can't open file '"~fname.idup~"'");
925 up.ptr[up.length] = 0;
926 assert(nbuf[$-1] == 0);
927 nblen = cast(uint)up.length;
928 } else {
929 nbuf[0..fname.length] = fname[];
930 nbuf[fname.length] = '\0';
931 nblen = cast(uint)fname.length;
933 static if (VFS_NORMAL_OS) if (mopt.ignoreCase == mopt.bool3.yes || (mopt.ignoreCase == mopt.bool3.def && vfsIgnoreCaseDisk)) {
934 // we have to lock here, as `findPathCI()` is not thread-safe
935 auto lock = vfsLockIntr();
936 auto pt = findPathCI(nbuf[0..nblen]);
937 if (pt is null) {
938 // restore filename for correct error message
939 nbuf[] = 0;
940 if (fname[0] == '~') {
941 import iv.vfs.util;
942 auto up = expandTilde(nbuf[0..$-1], fname);
943 assert(up !is null);
944 assert(nbuf[$-1] == 0);
945 up.ptr[up.length] = 0;
946 assert(nbuf[$-1] == 0);
947 nblen = cast(uint)up.length;
948 } else {
949 nbuf[0..fname.length] = fname[];
950 nbuf[fname.length] = '\0';
951 nblen = cast(uint)fname.length;
953 } else {
954 nbuf[pt.length] = '\0';
955 nblen = cast(uint)pt.length;
958 // try packs?
959 if (mopt.allowPaks && !mopt.wantWrite) {
960 uint colonpos = 0;
961 version(Windows) {
962 // idiotic shitdoze
963 if (nbuf[0] && nbuf[1] == ':') colonpos = 2;
965 while (nbuf[colonpos] && nbuf[colonpos] != ':') ++colonpos;
966 if (nbuf[colonpos]) {
967 try {
968 import core.stdc.string : strlen;
969 auto fl = openFileWithPaks!(char[], usefname)(nbuf[0..strlen(nbuf.ptr)], mode);
970 if (fl.isOpen) return fl;
971 } catch (Exception e) {
975 // normal disk file
976 //{ import core.stdc.stdio : stderr, fprintf; stderr.fprintf("USING FOPEN: '%s' '%s'\n", nbuf.ptr, mopt.mode.ptr); }
977 auto fl = dofopen(nbuf.ptr, mopt.mode.ptr);
978 if (fl is null) throw new VFSException("can't open file '"~fname.idup~"'");
979 scope(failure) core.stdc.stdio.fclose(fl); // just in case
980 try {
981 static if (usefname) {
982 return VFile(fl, fname);
983 } else {
984 return VFile(fl, nbuf[0..strlen(nbuf)]); //???
986 } catch (Exception e) {
987 // chain
988 //{ import core.stdc.stdio : stderr, fprintf; stderr.fprintf("USING FOPEN ERROR!\n"); }
989 throw new VFSException("can't open file '"~fname.idup~"'", __FILE__, __LINE__, e);
995 //FIXME: usefname!
996 VFile openFileWithPaks(T:const(char)[], bool usefname=true) (T name, const(char)[] mode) {
997 static assert(!is(T == typeof(null)));
998 assert(name.length < int.max/4);
1000 // check if name has any prefix at all
1001 int pfxend = cast(int)name.length;
1002 while (pfxend > 0 && name.ptr[pfxend-1] != ':') --pfxend;
1003 version(Windows) {
1004 // idiotic shitdoze
1005 if (pfxend <= 1) return VFile.init; // easy case
1006 } else {
1007 if (pfxend == 0) return VFile.init; // easy case
1010 auto lock = vfsLockIntr();
1012 // cpos: after last succesfull prefix
1013 VFile openit (int cpos) {
1014 bool nopaks = (cpos == 0);
1015 version(Windows) {
1016 // idiotic shitdoze
1017 if (cpos == 0 && name.length > 2 && name.ptr[1] == ':') cpos = 2;
1019 while (cpos < name.length) {
1020 auto ep = cpos;
1021 while (ep < name.length && name.ptr[ep] != ':') ++ep;
1022 if (ep >= name.length) return VFile(name, "rXIz");
1023 if (name.length-ep == 1) throw new VFSException("can't open file '"~name.idup~"'");
1025 // hack!
1026 import core.atomic;
1027 atomicOp!"-="(vfsLockedFlag, 1);
1028 scope(exit) atomicOp!"+="(vfsLockedFlag, 1);
1029 vfsAddPak!("normal", true)(name[0..ep], name[0..ep+1]);
1031 cpos = ep+1;
1033 throw new VFSException("can't open file '"~name.idup~"'"); // just in case
1036 // try to find the longest prefix which already exists
1037 while (pfxend > 0) {
1038 foreach (ref di; drivers) {
1039 if (di.prefixpath == name[0..pfxend]) {
1040 // i found her!
1041 if (di.temp) di.tempUsedTime = MonoTime.currTime;
1042 return openit(pfxend);
1045 --pfxend;
1046 while (pfxend > 0 && name.ptr[pfxend-1] != ':') --pfxend;
1047 version(Windows) {
1048 // idiotic shitdoze
1049 if (pfxend <= 1) break;
1052 return openit(0);
1056 // ////////////////////////////////////////////////////////////////////////// //
1057 /// open VFile with the given name. supports "file.pak:file1.pak:file.ext" pathes.
1058 /// kept for compatibility with old code, standard `VFile(path)` can do that now.
1059 public VFile openFileEx() (const(char)[] name) {
1060 if (name.length >= int.max/4) throw new VFSException("name too long");
1062 // check if name has any prefix at all
1063 int pfxend = cast(int)name.length;
1064 while (pfxend > 0 && name.ptr[pfxend-1] != ':') --pfxend;
1065 version(Windows) {
1066 // idiotic shitdoze
1067 if (pfxend <= 1) return VFile(name); // easy case
1068 } else {
1069 if (pfxend == 0) return VFile(name); // easy case
1072 auto lock = vfsLockIntr();
1074 // cpos: after last succesfull prefix
1075 VFile openit (int cpos) {
1076 version(Windows) {
1077 // idiotic shitdoze
1078 if (cpos == 0 && name.length > 2 && name.ptr[1] == ':') cpos = 2;
1080 while (cpos < name.length) {
1081 auto ep = cpos;
1082 while (ep < name.length && name.ptr[ep] != ':') ++ep;
1083 if (ep >= name.length) return VFile(name);
1084 if (name.length-ep == 1) throw new VFSException("can't open file '"~name.idup~"'");
1086 // hack!
1087 import core.atomic;
1088 atomicOp!"-="(vfsLockedFlag, 1);
1089 scope(exit) atomicOp!"+="(vfsLockedFlag, 1);
1090 vfsAddPak!("normal", true)(name[0..ep], name[0..ep+1]);
1092 cpos = ep+1;
1094 throw new VFSException("can't open file '"~name.idup~"'"); // just in case
1097 // try to find the longest prefix which already exists
1098 while (pfxend > 0) {
1099 foreach (ref di; drivers) {
1100 if (di.prefixpath == name[0..pfxend]) {
1101 // i found her!
1102 if (di.temp) di.tempUsedTime = MonoTime.currTime;
1103 return openit(pfxend);
1106 --pfxend;
1107 while (pfxend > 0 && name.ptr[pfxend-1] != ':') --pfxend;
1108 version(Windows) {
1109 // idiotic shitdoze
1110 if (pfxend <= 1) break;
1113 return openit(0);