1 /*
2 Copyright (c) 2014-2021 Martin Cejp
3 
4 Boost Software License - Version 1.0 - August 17th, 2003
5 
6 Permission is hereby granted, free of charge, to any person or organization
7 obtaining a copy of the software and accompanying documentation covered by
8 this license (the "Software") to use, reproduce, display, distribute,
9 execute, and transmit the Software, and to prepare derivative works of the
10 Software, and to permit third-parties to whom the Software is furnished to
11 do so, all subject to the following:
12 
13 The copyright notices in the Software and this entire statement, including
14 the above license grant, this restriction and the following disclaimer,
15 must be included in all copies of the Software, in whole or in part, and
16 all derivative works of the Software, unless such copies or derivative
17 works are solely in the form of machine-executable object code generated by
18 a source language processor.
19 
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 DEALINGS IN THE SOFTWARE.
27 */
28 
29 /**
30  * Copyright: Martin Cejp 2014-2021.
31  * License: $(LINK2 boost.org/LICENSE_1_0.txt, Boost License 1.0).
32  * Authors: Martin Cejp
33  */
34 module dlib.filesystem.local;
35 
36 import std.array;
37 import std.conv;
38 import std.datetime;
39 import std.path;
40 import std.range;
41 import std.stdio;
42 import std..string;
43 
44 import dlib.core.stream;
45 import dlib.filesystem.filesystem;
46 import dlib.filesystem.dirrange;
47 
48 version (Posix)
49 {
50     import dlib.filesystem.posix.common;
51     import dlib.filesystem.posix.directory;
52     import dlib.filesystem.posix.file;
53 }
54 else version (Windows)
55 {
56     import dlib.filesystem.windows.common;
57     import dlib.filesystem.windows.directory;
58     import dlib.filesystem.windows.file;
59 }
60 
61 // TODO: Should probably check for FILE_ATTRIBUTE_REPARSE_POINT before recursing
62 
63 /// LocalFileSystem
64 class LocalFileSystem : FileSystem
65 {
66     override InputStream openForInput(string filename)
67     {
68         return cast(InputStream) openFile(filename, read, 0);
69     }
70 
71     override OutputStream openForOutput(string filename, uint creationFlags)
72     {
73         return cast(OutputStream) openFile(filename, write, creationFlags);
74     }
75 
76     override IOStream openForIO(string filename, uint creationFlags)
77     {
78         return openFile(filename, read | write, creationFlags);
79     }
80 
81     override bool createDir(string path, bool recursive)
82     {
83         import std.algorithm;
84 
85         if (recursive)
86         {
87             ptrdiff_t index = max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
88 
89             if (index != -1)
90                 createDir(path[0..index], true);
91         }
92 
93         version(Posix)
94         {
95             return mkdir(toStringz(path), access_0755) == 0;
96         }
97         else version (Windows)
98         {
99             return CreateDirectoryW(toUTF16z(path), null) != 0;
100         }
101         else
102             throw new Exception("Not implemented.");
103     }
104 
105     override Directory openDir(string path)
106     {
107         version(Posix)
108         {
109             DIR* d = opendir(!path.empty ? toStringz(path) : ".");
110 
111             if (d == null)
112                 return null;
113             else
114                 return new PosixDirectory(this, d, !path.empty ? path ~ "/" : "");
115         }
116         else version(Windows)
117         {
118             string npath = !path.empty ? buildNormalizedPath(path) : ".";
119             DWORD attributes = GetFileAttributesW(toUTF16z(npath));
120 
121             if (attributes == INVALID_FILE_ATTRIBUTES)
122                 return null;
123 
124             if (attributes & FILE_ATTRIBUTE_DIRECTORY)
125                 return new WindowsDirectory(this, npath, !path.empty ? path ~ "/" : "");
126             else
127                 return null;
128         }
129         else
130             throw new Exception("Not implemented.");
131     }
132 
133     override bool stat(string path, out FileStat stat_out)
134     {
135         version(Posix)
136         {
137             stat_t st;
138 
139             if (stat_(toStringz(path), &st) != 0)
140                 return false;
141 
142             stat_out.isFile = S_ISREG(st.st_mode);
143             stat_out.isDirectory = S_ISDIR(st.st_mode);
144 
145             stat_out.sizeInBytes = st.st_size;
146             stat_out.creationTimestamp = SysTime(unixTimeToStdTime(st.st_ctime));
147             auto modificationStdTime = unixTimeToStdTime(st.st_mtime);
148             static if (is(typeof(st.st_mtimensec)))
149             {
150                 modificationStdTime += st.st_mtimensec / 100;
151             }
152             stat_out.modificationTimestamp = SysTime(modificationStdTime);
153 
154             if ((st.st_mode & S_IRUSR) | (st.st_mode & S_IRGRP) | (st.st_mode & S_IROTH))
155                 stat_out.permissions |= PRead;
156             if ((st.st_mode & S_IWUSR) | (st.st_mode & S_IWGRP) | (st.st_mode & S_IWOTH))
157                 stat_out.permissions |= PWrite;
158             if ((st.st_mode & S_IXUSR) | (st.st_mode & S_IXGRP) | (st.st_mode & S_IXOTH))
159                 stat_out.permissions |= PExecute;
160 
161             return true;
162         }
163         else version(Windows)
164         {
165             WIN32_FILE_ATTRIBUTE_DATA data;
166             
167             auto p = toUTF16z(path);
168 
169             if (!GetFileAttributesExW(p, GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, &data))
170                 return false;
171 
172             if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
173                 stat_out.isDirectory = true;
174             else
175                 stat_out.isFile = true;
176 
177             stat_out.sizeInBytes = (cast(FileSize) data.nFileSizeHigh << 32) | data.nFileSizeLow;
178             stat_out.creationTimestamp = SysTime(FILETIMEToStdTime(&data.ftCreationTime));
179             stat_out.modificationTimestamp = SysTime(FILETIMEToStdTime(&data.ftLastWriteTime));
180             
181             stat_out.permissions = 0;
182             
183             PACL pacl;
184             PSECURITY_DESCRIPTOR secDesc;
185             TRUSTEE_W trustee;
186             trustee.pMultipleTrustee = null;
187             trustee.MultipleTrusteeOperation = MULTIPLE_TRUSTEE_OPERATION.NO_MULTIPLE_TRUSTEE;
188             trustee.TrusteeForm = TRUSTEE_FORM.TRUSTEE_IS_NAME;
189             trustee.TrusteeType = TRUSTEE_TYPE.TRUSTEE_IS_UNKNOWN;
190             trustee.ptstrName = cast(wchar*)"CURRENT_USER"w.ptr;
191             GetNamedSecurityInfoW(cast(wchar*)p, SE_OBJECT_TYPE.SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, null, null, &pacl, null, &secDesc);
192             if (pacl)
193             {
194                 uint access;
195                 GetEffectiveRightsFromAcl(pacl, &trustee, &access);
196                 
197                 if (access & ACTRL_FILE_READ)
198                     stat_out.permissions |= PRead;
199                 if ((access & ACTRL_FILE_WRITE) && !(data.dwFileAttributes & FILE_ATTRIBUTE_READONLY))
200                     stat_out.permissions |= PWrite;
201                 if (access & ACTRL_FILE_EXECUTE)
202                     stat_out.permissions |= PExecute;
203             }
204 
205             return true;
206         }
207         else
208             throw new Exception("Not implemented.");
209     }
210 
211     /*
212     override bool move(string path, string newPath)
213     {
214         // TODO: should we allow newPath to actually be a directory?
215 
216         return rename(toStringz(path), toStringz(newPath)) == 0;
217     }
218     */
219 
220     override bool remove(string path, bool recursive)
221     {
222         FileStat stat;
223 
224         if (!this.stat(path, stat))
225             return false;
226 
227         return remove(path, stat.isDirectory, recursive);
228     }
229 
230    private:
231     version(Posix)
232     {
233         enum access_0644 = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
234         enum access_0755 = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
235     }
236 
237     IOStream openFile(string filename, uint accessFlags, uint creationFlags)
238     {
239         version(Posix)
240         {
241             int flags;
242 
243             switch (accessFlags & (read | write))
244             {
245                 case read: flags = O_RDONLY; break;
246                 case write: flags = O_WRONLY; break;
247                 case (read | write): flags = O_RDWR; break;
248                 default: flags = 0; break;
249             }
250 
251             if (creationFlags & FileSystem.create)
252                 flags |= O_CREAT;
253 
254             if (creationFlags & FileSystem.truncate)
255                 flags |= O_TRUNC;
256 
257             int fd = open(toStringz(filename), flags, access_0644);
258 
259             if (fd < 0)
260                 return null;
261             else
262                 return new PosixFile(fd, accessFlags);
263         }
264         else version(Windows)
265         {
266             DWORD access = 0;
267 
268             if (accessFlags & read)
269                 access |= GENERIC_READ;
270 
271             if (accessFlags & write)
272                 access |= GENERIC_WRITE;
273 
274             DWORD creationMode;
275 
276             switch (creationFlags & (create | truncate))
277             {
278                 case 0: creationMode = OPEN_EXISTING; break;
279                 case create: creationMode = OPEN_ALWAYS; break;
280                 case truncate: creationMode = TRUNCATE_EXISTING; break;
281                 case (create | truncate): creationMode = CREATE_ALWAYS; break;
282                 default: creationMode = OPEN_EXISTING; break;
283             }
284 
285             HANDLE file = CreateFileW(toUTF16z(filename), access, FILE_SHARE_READ, null, creationMode,
286                 FILE_ATTRIBUTE_NORMAL, null);
287 
288             if (file == INVALID_HANDLE_VALUE)
289                 return null;
290             else
291                 return new WindowsFile(file, accessFlags);
292         }
293         else
294             throw new Exception("Not implemented.");
295     }
296 
297     bool remove(string path, bool isDirectory, bool recursive)
298     {
299         if (isDirectory && recursive)
300         {
301             // Remove contents
302             auto dir = openDir(path);
303 
304             try
305             {
306                 foreach (entry; dir.contents)
307                     remove(path ~ "/" ~ entry.name, entry.isDirectory, recursive);
308             }
309             finally
310             {
311                 dir.close();
312             }
313         }
314 
315         version(Posix)
316         {
317             if (isDirectory)
318                 return rmdir(toStringz(path)) == 0;
319             else
320                 return std.stdio.remove(toStringz(path)) == 0;
321         }
322         else version(Windows)
323         {
324             if (isDirectory)
325                 return RemoveDirectoryW(toUTF16z(path)) != 0;
326             else
327                 return DeleteFileW(toUTF16z(path)) != 0;
328         }
329         else
330             throw new Exception("Not implemented.");
331     }
332 }
333 
334 private ReadOnlyFileSystem rofs;
335 private FileSystem fs;
336 
337 static this()
338 {
339     // decouple dependency from the rest of this module
340     import dlib.filesystem.local;
341 
342     setFileSystem(new LocalFileSystem);
343 }
344 
345 void setFileSystem(FileSystem fs_)
346 {
347     rofs = fs_;
348     fs = fs_;
349 }
350 
351 void setFileSystemReadOnly(ReadOnlyFileSystem rofs_)
352 {
353     rofs = rofs_;
354     fs = null;
355 }
356 
357 // ReadOnlyFileSystem
358 
359 bool stat(string filename, out FileStat stat)
360 {
361     return rofs.stat(filename, stat);
362 }
363 
364 InputStream openForInput(string filename)
365 {
366     InputStream ins = rofs.openForInput(filename);
367 
368     if (ins is null)
369         throw new Exception("Failed to open '" ~ filename ~ "'");
370 
371     return ins;
372 }
373 
374 Directory openDir(string path)
375 {
376     return rofs.openDir(path);
377 }
378 
379 InputRange!DirEntry findFiles(string baseDir, bool recursive)
380 {
381     return dlib.filesystem.filesystem.findFiles(rofs, baseDir, recursive);
382 }
383 
384 // FileSystem
385 
386 OutputStream openForOutput(string filename, uint creationFlags = FileSystem.create | FileSystem.truncate)
387 {
388     OutputStream outs = fs.openForOutput(filename, creationFlags);
389 
390     if (outs is null)
391         throw new Exception("Failed to open '" ~ filename ~ "' for writing");
392 
393     return outs;
394 }
395 
396 IOStream openForIO(string filename, uint creationFlags)
397 {
398     IOStream ios = fs.openForIO(filename, creationFlags);
399 
400     if (ios is null)
401         throw new Exception("Failed to open '" ~ filename ~ "' for writing");
402 
403     return ios;
404 }
405 
406 bool createDir(string path, bool recursive)
407 {
408     return fs.createDir(path, recursive);
409 }
410 
411 /*
412 bool move(string path, string newPath)
413 {
414     return fs.move(path, newPath);
415 }
416 */
417 
418 bool remove(string path, bool recursive)
419 {
420     return fs.remove(path, recursive);
421 }
422 
423 unittest
424 {
425     // TODO: test >4GiB files
426 
427     import std.algorithm;
428     import std.file;
429 
430     alias remove = dlib.filesystem.local.remove;
431 
432     remove("tests/test_data", true);
433     assert(openDir("tests/test_data") is null);
434 
435     assert(createDir("tests/test_data/main", true));
436 
437     enum dir = "tests";
438     auto d = openDir(dir);
439 
440     try
441     {
442         chdir(dir);
443         auto expected = dirEntries("", SpanMode.shallow)
444                                   .filter!(e => e.isFile)
445                                   .array;
446         size_t i;
447         chdir("..");
448 
449         foreach (entry; d.contents)
450         {
451             if (entry.isFile)
452             {
453                 assert(expected[i] == entry.name);
454                 ++i;
455             }
456         }
457     }
458     finally
459     {
460         d.close();
461     }
462 
463     //
464     OutputStream outp = openForOutput("tests/test_data/main/hello_world.txt", FileSystem.create | FileSystem.truncate);
465     string expected = "Hello, World!\n";
466     assert(outp);
467 
468     try
469     {
470         assert(outp.writeArray(expected));
471     }
472     finally
473     {
474         outp.close();
475     }
476 
477     //
478     InputStream inp = openForInput("tests/test_data/main/hello_world.txt");
479     assert(inp);
480 
481     try
482     {
483         while (inp.readable)
484         {
485             char[1] buffer;
486 
487             auto have = inp.readBytes(buffer.ptr, buffer.length);
488             assert(buffer[0..have] == expected[0..have]);
489             expected.popFrontN(have);
490         }
491     }
492     finally
493     {
494         inp.close();
495     }
496 }
497 
498 unittest
499 {
500     import std.algorithm;
501     import std.file;
502 
503     auto expected = dirEntries("", SpanMode.depth)
504                               .filter!(e => e.isFile)
505                               .filter!(e => e.name.baseName.endsWith(".d"))
506                               .map!(e => e.name.replace("\\", "/"))
507                               .array;
508     size_t i;
509 
510     foreach (entry; findFiles("", true)
511             .filter!(entry => entry.isFile)
512             .filter!(e => e.name.baseName.globMatch("*.d")))
513     {
514         FileStat stat_;
515         assert(stat(entry.name, stat_)); // make sure we're getting the expected path
516         assert(expected[i] == entry.name);
517         assert(stat_.sizeInBytes == expected[i].getSize());
518 
519         SysTime modificationTime, accessTime;
520         expected[i].getTimes(accessTime, modificationTime);
521         assert(modificationTime ==  stat_.modificationTimestamp);
522 
523         ++i;
524     }
525 }