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*/;
27 import iv
.vfs
.posixci
;
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 {
55 import core
.sys
.posix
.stdio
;
56 return fopen64(fname
, mode
);
58 import core
.stdc
.stdio
;
59 return fopen(fname
, mode
);
64 // ////////////////////////////////////////////////////////////////////////// //
65 public struct VFSVariant
{
85 string sv
; // moved here, so GC will not scan numbers
86 Type vtype
= Type
.Empty
;
89 string
toString () const {
90 import core
.stdc
.stdio
: snprintf
;
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
) {
111 static if (is(T
== typeof(null))) sv
= null;
112 else static if (is(T
== string
)) sv
= v
;
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();
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
{
162 @disable this (this);
167 if (locked
< 0) assert(0, "vfs: internal error");
168 atomicOp
!"-="(vfsLockedFlag
, 1);
175 private auto vfsLockIntr() (string msg
=null) {
179 if (atomicLoad(vfsLockedFlag
)) {
181 throw new VFSException(msg
);
186 atomicOp
!"+="(vfsLockedFlag
, 1);
191 // ////////////////////////////////////////////////////////////////////////// //
192 /// abstract class for VFS drivers
193 public abstract class VFSDriver
{
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
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
;
217 VFSVariant
stat (const(char)[] propname
) const { return vfsStat(this, propname
); }
220 /// this constructor is used for disk drivers
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
{
291 this () { dataPath
= "./"; }
293 this(T
: const(char)[]) (T dpath
) {
294 static if (is(T
== typeof(null))) {
297 if (dpath
.length
== 0) {
299 } else if (dpath
[$-1] == '/') {
300 static if (is(T
== string
)) dataPath
= dpath
; else dataPath
= dpath
.idup
;
301 } else if (dpath
[0] == '~') {
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
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
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] == '~') {
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
);
337 auto up
= expandTilde(nbuf
[0..len
-1], fname
);
338 if (up
is null) return VFile
.init
;
339 up
.ptr
[up
.length
] = 0;
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");
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
);
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
375 ulong modtime
; // 0: unknown; unixtime
383 final void buildFileList () {
384 import std
.file
: DE
= DirEntry
, dirEntries
, SpanMode
;
385 if (flistInited
) return;
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
) {
392 files
~= FileEntry(de.name
[dataPath
.length
..$], de.size
, de.timeLastModified
.toUTC
.toUnixTime());
394 files
~= FileEntry(de.name
[dataPath
.length
..$], de.size
);
397 } catch (Exception e
) {}
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
) {
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
); }
420 // ////////////////////////////////////////////////////////////////////////// //
421 public struct VFSDriverId
{
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
); }
433 __gshared
uint lastIdNumber
;
437 enum Mode
{ Normal
, First
, Last
}
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
) {
450 prefixpath
= apfxpath
;
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;
464 auto idx
= drivers
.length
;
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) {
473 foreach (immutable c
; idx
+1..drivers
.length
) drivers
.ptr
[c
-1] = drivers
.ptr
[c
];
475 drivers
.assumeSafeAppend
;
476 if (idx
== 0 || tempDrvCount
== 0) return;
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");
494 VFSDriverId did
= VFSDriverId
.create
;
495 static if (temp
) ++tempDrvCount
;
496 static if (mode
== "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
);
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") {
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
;
513 static assert(0, "wtf?!");
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");
537 foreach_reverse (immutable idx
, ref drv
; drivers
) {
538 if (drv
.drvid
== id
) {
540 foreach (immutable c
; idx
+1..drivers
.length
) drivers
[c
-1] = drivers
[c
];
542 drivers
.assumeSafeAppend
;
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();
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
) {
568 filesSeen
[de.name
] = res
.length
;
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();
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
;
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
;
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();
638 foreach_reverse (ref drvnfo
; drivers
) {
639 if (drvnfo
.drvid
== de.drvid
) return drvnfo
.drv
.stat(de.index
, propname
);
645 // ////////////////////////////////////////////////////////////////////////// //
647 enum bool3
{ def
= -1, no
= 0, yes
= 1 }
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
) {
667 ignoreCase
= bool3
.def
;
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; }
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';
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)
708 * x: allow searching in paks (default)
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
) {
722 //import core.memory : GC;
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
) {
728 //foreach (char xch; fname) s.put(xch);
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 ''");
744 if (fname
.length
== 0) error("can't open file ''");
746 auto mopt
= ModeOptions(mode
);
749 //TODO: transparently read gzipped files from archives
750 if (mopt
.allowGZ
!= ModeOptions
.bool3
.no
&& !mopt
.wantWrite
) {
751 if (mopt
.allowGZ
== ModeOptions
.bool3
.yes
) {
753 import std
.internal
.cstring
;
754 gzFile gf
= gzopen(fname
[].tempCString
, "rb");
755 if (gf
!is null) return VFile
.OpenGZ(gf
, fname
);
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); }
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
) {
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
);
777 auto lock = vfsLockIntr();
780 bool ignoreCase
= (mopt
.ignoreCase
== mopt
.bool3
.def ? vfsIgnoreCasePak
: (mopt
.ignoreCase
== mopt
.bool3
.yes
));
781 foreach_reverse (ref di; drivers
) {
783 if (!mopt
.wantWrite ||
!di.drv
.isDisk
) {
784 auto fl
= di.drv
.tryOpen(fname
, ignoreCase
);
786 if (mopt
.wantWrite
) errorfn("can't open file '!' in non-binary non-readonly mode");
787 if (di.temp
) di.tempUsedTime
= MonoTime
.currTime
;
791 } catch (Exception e
) {
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
}
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") {
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);
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
;
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
) {
848 return vfsAddPak
!(mode
, temp
)(vfsOpenFile(fname
), fname
, prefixpath
);
849 } catch (Exception
) {}
850 return vfsAddPak
!(mode
, temp
)(vfsDiskOpen(fname
), fname
, prefixpath
);
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
);
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
);
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");
890 foreach (ref di; detectors
) {
893 auto drv
= di.dt.tryOpen(fl
, prefixpath
);
897 atomicOp
!"-="(vfsLockedFlag
, 1);
898 scope(exit
) atomicOp
!"+="(vfsLockedFlag
, 1);
899 return vfsRegister
!(mode
, temp
)(drv
, fname
, prefixpath
);
901 } catch (Exception e
) {
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 ''");
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
);
924 if (fname
[0] == '~') {
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
;
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
]);
941 // restore filename for correct error message
943 if (fname
[0] == '~') {
945 auto up
= expandTilde(nbuf
[0..$-1], fname
);
947 assert(nbuf
[$-1] == 0);
948 up
.ptr
[up
.length
] = 0;
949 assert(nbuf
[$-1] == 0);
950 nblen
= cast(uint)up
.length
;
952 nbuf
[0..fname
.length
] = fname
[];
953 nbuf
[fname
.length
] = '\0';
954 nblen
= cast(uint)fname
.length
;
957 nbuf
[pt
.length
] = '\0';
958 nblen
= cast(uint)pt
.length
;
962 if (mopt
.allowPaks
&& !mopt
.wantWrite
) {
966 if (nbuf
[0] && nbuf
[1] == ':') colonpos
= 2;
968 while (nbuf
[colonpos
] && nbuf
[colonpos
] != ':') ++colonpos
;
969 if (nbuf
[colonpos
]) {
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
) {
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
984 static if (usefname
) {
985 return VFile(fl
, fname
);
987 return VFile(fl
, nbuf
[0..strlen(nbuf
)]); //???
989 } catch (Exception e
) {
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
);
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
;
1008 if (pfxend
<= 1) return VFile
.init
; // easy case
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);
1020 if (cpos
== 0 && name
.length
> 2 && name
.ptr
[1] == ':') cpos
= 2;
1022 while (cpos
< name
.length
) {
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
~"'");
1030 atomicOp
!"-="(vfsLockedFlag
, 1);
1031 scope(exit
) atomicOp
!"+="(vfsLockedFlag
, 1);
1032 vfsAddPak
!("normal", true)(name
[0..ep
], name
[0..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
]) {
1044 if (di.temp
) di.tempUsedTime
= MonoTime
.currTime
;
1045 return openit(pfxend
);
1049 while (pfxend
> 0 && name
.ptr
[pfxend
-1] != ':') --pfxend
;
1052 if (pfxend
<= 1) break;
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
;
1070 if (pfxend
<= 1) return VFile(name
); // easy case
1072 if (pfxend
== 0) return VFile(name
); // easy case
1075 auto lock = vfsLockIntr();
1077 // cpos: after last succesfull prefix
1078 VFile
openit (int cpos
) {
1081 if (cpos
== 0 && name
.length
> 2 && name
.ptr
[1] == ':') cpos
= 2;
1083 while (cpos
< name
.length
) {
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
~"'");
1091 atomicOp
!"-="(vfsLockedFlag
, 1);
1092 scope(exit
) atomicOp
!"+="(vfsLockedFlag
, 1);
1093 vfsAddPak
!("normal", true)(name
[0..ep
], name
[0..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
]) {
1105 if (di.temp
) di.tempUsedTime
= MonoTime
.currTime
;
1106 return openit(pfxend
);
1110 while (pfxend
> 0 && name
.ptr
[pfxend
-1] != ':') --pfxend
;
1113 if (pfxend
<= 1) break;