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*/;
28 import iv
.vfs
.posixci
;
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 {
56 import core
.sys
.posix
.stdio
;
57 return fopen64(fname
, mode
);
59 import core
.stdc
.stdio
;
60 return fopen(fname
, mode
);
65 // ////////////////////////////////////////////////////////////////////////// //
66 public struct VFSVariant
{
86 string sv
; // moved here, so GC will not scan numbers
87 Type vtype
= Type
.Empty
;
90 string
toString () const {
91 import core
.stdc
.stdio
: snprintf
;
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
) {
112 static if (is(T
== typeof(null))) sv
= null;
113 else static if (is(T
== string
)) sv
= v
;
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();
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
{
163 @disable this (this);
168 if (locked
< 0) assert(0, "vfs: internal error");
169 atomicOp
!"-="(vfsLockedFlag
, 1);
176 private auto vfsLockIntr() (string msg
=null) {
180 if (atomicLoad(vfsLockedFlag
)) {
182 throw new VFSException(msg
);
187 atomicOp
!"+="(vfsLockedFlag
, 1);
192 // ////////////////////////////////////////////////////////////////////////// //
193 /// abstract class for VFS drivers
194 public abstract class VFSDriver
{
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
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
;
218 VFSVariant
stat (const(char)[] propname
) const { return vfsStat(this, propname
); }
221 /// this constructor is used for disk drivers
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
{
292 this () { dataPath
= "./"; }
294 this(T
: const(char)[]) (T dpath
) {
295 static if (is(T
== typeof(null))) {
298 if (dpath
.length
== 0) {
300 } else if (dpath
[$-1] == '/') {
301 static if (is(T
== string
)) dataPath
= dpath
; else dataPath
= dpath
.idup
;
302 } else if (dpath
[0] == '~') {
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
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
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] == '~') {
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
);
338 auto up
= expandTilde(nbuf
[0..len
-1], fname
);
339 if (up
is null) return VFile
.init
;
340 up
.ptr
[up
.length
] = 0;
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");
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
);
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
376 ulong modtime
; // 0: unknown; unixtime
384 final void buildFileList () {
385 import std
.file
: DE
= DirEntry
, dirEntries
, SpanMode
;
386 if (flistInited
) return;
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
) {
393 files
~= FileEntry(de.name
[dataPath
.length
..$], de.size
, de.timeLastModified
.toUTC
.toUnixTime());
395 files
~= FileEntry(de.name
[dataPath
.length
..$], de.size
);
398 } catch (Exception e
) {}
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
) {
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
); }
421 // ////////////////////////////////////////////////////////////////////////// //
422 public struct VFSDriverId
{
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
); }
434 __gshared
uint lastIdNumber
;
438 enum Mode
{ Normal
, First
, Last
}
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
) {
451 prefixpath
= apfxpath
;
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;
465 auto idx
= drivers
.length
;
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) {
474 foreach (immutable c
; idx
+1..drivers
.length
) drivers
.ptr
[c
-1] = drivers
.ptr
[c
];
476 drivers
.assumeSafeAppend
;
477 if (idx
== 0 || tempDrvCount
== 0) return;
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");
495 VFSDriverId did
= VFSDriverId
.create
;
496 static if (temp
) ++tempDrvCount
;
497 static if (mode
== "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
);
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") {
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
;
514 static assert(0, "wtf?!");
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");
538 foreach_reverse (immutable idx
, ref drv
; drivers
) {
539 if (drv
.drvid
== id
) {
541 foreach (immutable c
; idx
+1..drivers
.length
) drivers
[c
-1] = drivers
[c
];
543 drivers
.assumeSafeAppend
;
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();
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
) {
569 filesSeen
[de.name
] = res
.length
;
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();
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
;
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
;
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();
639 foreach_reverse (ref drvnfo
; drivers
) {
640 if (drvnfo
.drvid
== de.drvid
) return drvnfo
.drv
.stat(de.index
, propname
);
646 // ////////////////////////////////////////////////////////////////////////// //
648 enum bool3
{ def
= -1, no
= 0, yes
= 1 }
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
) {
668 ignoreCase
= bool3
.def
;
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; }
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)
705 * x: allow searching in paks (default)
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
) {
719 //import core.memory : GC;
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
) {
725 //foreach (char xch; fname) s.put(xch);
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 ''");
741 if (fname
.length
== 0) error("can't open file ''");
743 auto mopt
= ModeOptions(mode
);
746 //TODO: transparently read gzipped files from archives
747 if (mopt
.allowGZ
!= ModeOptions
.bool3
.no
&& !mopt
.wantWrite
) {
748 if (mopt
.allowGZ
== ModeOptions
.bool3
.yes
) {
750 import std
.internal
.cstring
;
751 gzFile gf
= gzopen(fname
[].tempCString
, "rb");
752 if (gf
!is null) return VFile
.OpenGZ(gf
, fname
);
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); }
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
) {
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
);
774 auto lock = vfsLockIntr();
777 bool ignoreCase
= (mopt
.ignoreCase
== mopt
.bool3
.def ? vfsIgnoreCasePak
: (mopt
.ignoreCase
== mopt
.bool3
.yes
));
778 foreach_reverse (ref di; drivers
) {
780 if (!mopt
.wantWrite ||
!di.drv
.isDisk
) {
781 auto fl
= di.drv
.tryOpen(fname
, ignoreCase
);
783 if (mopt
.wantWrite
) errorfn("can't open file '!' in non-binary non-readonly mode");
784 if (di.temp
) di.tempUsedTime
= MonoTime
.currTime
;
788 } catch (Exception e
) {
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
}
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") {
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);
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
;
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
) {
845 return vfsAddPak
!(mode
, temp
)(vfsOpenFile(fname
), fname
, prefixpath
);
846 } catch (Exception
) {}
847 return vfsAddPak
!(mode
, temp
)(vfsDiskOpen(fname
), fname
, prefixpath
);
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
);
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
);
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");
887 foreach (ref di; detectors
) {
890 auto drv
= di.dt.tryOpen(fl
, prefixpath
);
894 atomicOp
!"-="(vfsLockedFlag
, 1);
895 scope(exit
) atomicOp
!"+="(vfsLockedFlag
, 1);
896 return vfsRegister
!(mode
, temp
)(drv
, fname
, prefixpath
);
898 } catch (Exception e
) {
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 ''");
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
);
921 if (fname
[0] == '~') {
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
;
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
]);
938 // restore filename for correct error message
940 if (fname
[0] == '~') {
942 auto up
= expandTilde(nbuf
[0..$-1], fname
);
944 assert(nbuf
[$-1] == 0);
945 up
.ptr
[up
.length
] = 0;
946 assert(nbuf
[$-1] == 0);
947 nblen
= cast(uint)up
.length
;
949 nbuf
[0..fname
.length
] = fname
[];
950 nbuf
[fname
.length
] = '\0';
951 nblen
= cast(uint)fname
.length
;
954 nbuf
[pt
.length
] = '\0';
955 nblen
= cast(uint)pt
.length
;
959 if (mopt
.allowPaks
&& !mopt
.wantWrite
) {
963 if (nbuf
[0] && nbuf
[1] == ':') colonpos
= 2;
965 while (nbuf
[colonpos
] && nbuf
[colonpos
] != ':') ++colonpos
;
966 if (nbuf
[colonpos
]) {
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
) {
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
981 static if (usefname
) {
982 return VFile(fl
, fname
);
984 return VFile(fl
, nbuf
[0..strlen(nbuf
)]); //???
986 } catch (Exception e
) {
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
);
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
;
1005 if (pfxend
<= 1) return VFile
.init
; // easy case
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);
1017 if (cpos
== 0 && name
.length
> 2 && name
.ptr
[1] == ':') cpos
= 2;
1019 while (cpos
< name
.length
) {
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
~"'");
1027 atomicOp
!"-="(vfsLockedFlag
, 1);
1028 scope(exit
) atomicOp
!"+="(vfsLockedFlag
, 1);
1029 vfsAddPak
!("normal", true)(name
[0..ep
], name
[0..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
]) {
1041 if (di.temp
) di.tempUsedTime
= MonoTime
.currTime
;
1042 return openit(pfxend
);
1046 while (pfxend
> 0 && name
.ptr
[pfxend
-1] != ':') --pfxend
;
1049 if (pfxend
<= 1) break;
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
;
1067 if (pfxend
<= 1) return VFile(name
); // easy case
1069 if (pfxend
== 0) return VFile(name
); // easy case
1072 auto lock = vfsLockIntr();
1074 // cpos: after last succesfull prefix
1075 VFile
openit (int cpos
) {
1078 if (cpos
== 0 && name
.length
> 2 && name
.ptr
[1] == ':') cpos
= 2;
1080 while (cpos
< name
.length
) {
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
~"'");
1088 atomicOp
!"-="(vfsLockedFlag
, 1);
1089 scope(exit
) atomicOp
!"+="(vfsLockedFlag
, 1);
1090 vfsAddPak
!("normal", true)(name
[0..ep
], name
[0..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
]) {
1102 if (di.temp
) di.tempUsedTime
= MonoTime
.currTime
;
1103 return openit(pfxend
);
1107 while (pfxend
> 0 && name
.ptr
[pfxend
-1] != ':') --pfxend
;
1110 if (pfxend
<= 1) break;