1 /**
2  * File utilities.
3  *
4  * Functions and objects dedicated to file I/O and management. TODO: Move here artifacts
5  * from places such as root/ so both the frontend and the backend have access to them.
6  *
7  * Copyright: Copyright (C) 1999-2023 by The D Language Foundation, All Rights Reserved
8  * Authors:   Walter Bright, https://www.digitalmars.com
9  * License:   $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
10  * Source:    $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/common/file.d, common/_file.d)
11  * Documentation: https://dlang.org/phobos/dmd_common_file.html
12  * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/common/file.d
13  */
14 
15 module dmd.common.file;
16 
17 import core.stdc.errno : errno;
18 import core.stdc.stdio : fprintf, remove, rename, stderr;
19 import core.stdc.stdlib : exit;
20 import core.stdc.string : strerror;
21 import core.sys.windows.winbase;
22 import core.sys.windows.winnt;
23 import core.sys.posix.fcntl;
24 import core.sys.posix.unistd;
25 
26 import dmd.common.string;
27 
28 nothrow:
29 
30 /**
31 Encapsulated management of a memory-mapped file.
32 
33 Params:
34 Datum = the mapped data type: Use a POD of size 1 for read/write mapping
35 and a `const` version thereof for read-only mapping. Other primitive types
36 should work, but have not been yet tested.
37 */
38 struct FileMapping(Datum)
39 {
40     static assert(__traits(isPOD, Datum) && Datum.sizeof == 1,
41         "Not tested with other data types yet. Add new types with care.");
42 
43     version(Posix) enum invalidHandle = -1;
44     else version(Windows) enum invalidHandle = INVALID_HANDLE_VALUE;
45 
46     // state {
47     /// Handle of underlying file
48     private auto handle = invalidHandle;
49     /// File mapping object needed on Windows
50     version(Windows) private HANDLE fileMappingObject = invalidHandle;
51     /// Memory-mapped array
52     private Datum[] data;
53     /// Name of underlying file, zero-terminated
54     private const(char)* name;
55     // state }
56 
57   nothrow:
58 
59     /**
60     Open `filename` and map it in memory. If `Datum` is `const`, opens for
61     read-only and maps the content in memory; no error is issued if the file
62     does not exist. This makes it easy to treat a non-existing file as empty.
63 
64     If `Datum` is mutable, opens for read/write (creates file if it does not
65     exist) and fails fatally on any error.
66 
67     Due to quirks in `mmap`, if the file is empty, `handle` is valid but `data`
68     is `null`. This state is valid and accounted for.
69 
70     Params:
71     filename = the name of the file to be mapped in memory
72     */
73     this(const char* filename)
74     {
75         version (Posix)
76         {
77             import core.sys.posix.sys.mman;
78             import core.sys.posix.fcntl : open, O_CREAT, O_RDONLY, O_RDWR, S_IRGRP, S_IROTH, S_IRUSR, S_IWUSR;
79 
80             handle = open(filename, is(Datum == const) ? O_RDONLY : (O_CREAT | O_RDWR),
81                 S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
82 
83             if (handle == invalidHandle)
84             {
85                 static if (is(Datum == const))
86                 {
87                     // No error, nonexisting file in read mode behaves like an empty file.
88                     return;
89                 }
90                 else
91                 {
92                     fprintf(stderr, "open(\"%s\") failed: %s\n", filename, strerror(errno));
93                     exit(1);
94                 }
95             }
96 
97             const size = fileSize(handle);
98 
99             if (size > 0 && size != ulong.max && size <= size_t.max)
100             {
101                 auto p = mmap(null, cast(size_t) size, is(Datum == const) ? PROT_READ : PROT_WRITE, MAP_SHARED, handle, 0);
102                 if (p == MAP_FAILED)
103                 {
104                     fprintf(stderr, "mmap(null, %zu) for \"%s\" failed: %s\n", cast(size_t) size, filename, strerror(errno));
105                     exit(1);
106                 }
107                 // The cast below will always work because it's gated by the `size <= size_t.max` condition.
108                 data = cast(Datum[]) p[0 .. cast(size_t) size];
109             }
110         }
111         else version(Windows)
112         {
113             static if (is(Datum == const))
114             {
115                 enum createFileMode = GENERIC_READ;
116                 enum openFlags = OPEN_EXISTING;
117             }
118             else
119             {
120                 enum createFileMode = GENERIC_READ | GENERIC_WRITE;
121                 enum openFlags = CREATE_ALWAYS;
122             }
123 
124             handle = filename.asDString.extendedPathThen!(p => CreateFileW(p.ptr, createFileMode, 0, null, openFlags, FILE_ATTRIBUTE_NORMAL, null));
125             if (handle == invalidHandle)
126             {
127                 static if (is(Datum == const))
128                 {
129                     return;
130                 }
131                 else
132                 {
133                     fprintf(stderr, "CreateFileW() failed for \"%s\": %d\n", filename, GetLastError());
134                     exit(1);
135                 }
136             }
137             createMapping(filename, fileSize(handle));
138         }
139         else static assert(0);
140 
141         // Save the name for later. Technically there's no need: on Linux one can use readlink on /proc/self/fd/NNN.
142         // On BSD and OSX one can use fcntl with F_GETPATH. On Windows one can use GetFileInformationByHandleEx.
143         // But just saving the name is simplest, fastest, and most portable...
144         import core.stdc.string : strlen;
145         import core.stdc.stdlib : malloc;
146         import core.stdc.string : memcpy;
147         const totalNameLength = filename.strlen() + 1;
148         auto namex = cast(char*) malloc(totalNameLength);
149         if (!namex)
150         {
151             fprintf(stderr, "FileMapping: Out of memory.");
152             exit(1);
153         }
154         name = cast(char*) memcpy(namex, filename, totalNameLength);
155     }
156 
157     /**
158     Common code factored opportunistically. Windows only. Assumes `handle` is
159     already pointing to an opened file. Initializes the `fileMappingObject`
160     and `data` members.
161 
162     Params:
163     filename = the file to be mapped
164     size = the size of the file in bytes
165     */
166     version(Windows) private void createMapping(const char* filename, ulong size)
167     {
168         assert(size <= size_t.max || size == ulong.max);
169         assert(handle != invalidHandle);
170         assert(data is null);
171         assert(fileMappingObject == invalidHandle);
172 
173         if (size == 0 || size == ulong.max)
174             return;
175 
176         static if (is(Datum == const))
177         {
178             enum fileMappingFlags = PAGE_READONLY;
179             enum mapViewFlags = FILE_MAP_READ;
180         }
181         else
182         {
183             enum fileMappingFlags = PAGE_READWRITE;
184             enum mapViewFlags = FILE_MAP_WRITE;
185         }
186 
187         fileMappingObject = CreateFileMappingW(handle, null, fileMappingFlags, 0, 0, null);
188         if (!fileMappingObject)
189         {
190             fprintf(stderr, "CreateFileMappingW(%p) failed for %llu bytes of \"%s\": %d\n",
191                 handle, size, filename, GetLastError());
192             fileMappingObject = invalidHandle;  // by convention always use invalidHandle, not null
193             exit(1);
194         }
195         auto p = MapViewOfFile(fileMappingObject, mapViewFlags, 0, 0, 0);
196         if (!p)
197         {
198             fprintf(stderr, "MapViewOfFile() failed for \"%s\": %d\n", filename, GetLastError());
199             exit(1);
200         }
201         data = cast(Datum[]) p[0 .. cast(size_t) size];
202     }
203 
204     // Not copyable or assignable (for now).
205     @disable this(const FileMapping!Datum rhs);
206     @disable void opAssign(const ref FileMapping!Datum rhs);
207 
208     /**
209     Frees resources associated with this mapping. However, it does not deallocate the name.
210     */
211     ~this() pure nothrow
212     {
213         if (!active)
214             return;
215         fakePure({
216             version (Posix)
217             {
218                 import core.sys.posix.sys.mman : munmap;
219                 import core.sys.posix.unistd : close;
220 
221                 // Cannot call fprintf from inside a destructor, so exiting silently.
222 
223                 if (data.ptr && munmap(cast(void*) data.ptr, data.length) != 0)
224                 {
225                     exit(1);
226                 }
227                 data = null;
228                 if (handle != invalidHandle && close(handle) != 0)
229                 {
230                     exit(1);
231                 }
232                 handle = invalidHandle;
233             }
234             else version(Windows)
235             {
236                 if (data.ptr !is null && UnmapViewOfFile(cast(void*) data.ptr) == 0)
237                 {
238                     exit(1);
239                 }
240                 data = null;
241                 if (fileMappingObject != invalidHandle && CloseHandle(fileMappingObject) == 0)
242                 {
243                     exit(1);
244                 }
245                 fileMappingObject = invalidHandle;
246                 if (handle != invalidHandle && CloseHandle(handle) == 0)
247                 {
248                     exit(1);
249                 }
250                 handle = invalidHandle;
251             }
252             else static assert(0);
253         });
254     }
255 
256     /**
257     Returns the zero-terminated file name associated with the mapping. Can NOT
258     be saved beyond the lifetime of `this`.
259     */
260     private const(char)* filename() const pure @nogc @safe nothrow { return name; }
261 
262     /**
263     Frees resources associated with this mapping. However, it does not deallocate the name.
264     Reinitializes `this` as a fresh object that can be reused.
265     */
266     void close()
267     {
268         __dtor();
269         handle = invalidHandle;
270         version(Windows) fileMappingObject = invalidHandle;
271         data = null;
272         name = null;
273     }
274 
275     /**
276     Deletes the underlying file and frees all resources associated.
277     Reinitializes `this` as a fresh object that can be reused.
278 
279     This function does not abort if the file cannot be deleted, but does print
280     a message on `stderr` and returns `false` to the caller. The underlying
281     rationale is to give the caller the option to continue execution if
282     deleting the file is not important.
283 
284     Returns: `true` iff the file was successfully deleted. If the file was not
285     deleted, prints a message to `stderr` and returns `false`.
286     */
287     static if (!is(Datum == const))
288     bool discard()
289     {
290         // Truncate file to zero so unflushed buffers are not flushed unnecessarily.
291         resize(0);
292         auto deleteme = name;
293         close();
294         // In-memory resource freed, now get rid of the underlying temp file.
295         version(Posix)
296         {
297             import core.sys.posix.unistd : unlink;
298             if (unlink(deleteme) != 0)
299             {
300                 fprintf(stderr, "unlink(\"%s\") failed: %s\n", filename, strerror(errno));
301                 return false;
302             }
303         }
304         else version(Windows)
305         {
306             import core.sys.windows.winbase;
307             if (deleteme.asDString.extendedPathThen!(p => DeleteFileW(p.ptr)) == 0)
308             {
309                 fprintf(stderr, "DeleteFileW error %d\n", GetLastError());
310                 return false;
311             }
312         }
313         else static assert(0);
314         return true;
315     }
316 
317     /**
318     Queries whether `this` is currently associated with a file.
319 
320     Returns: `true` iff there is an active mapping.
321     */
322     bool active() const pure @nogc nothrow
323     {
324         return handle !is invalidHandle;
325     }
326 
327     /**
328     Queries the length of the file associated with this mapping.  If not
329     active, returns 0.
330 
331     Returns: the length of the file, or 0 if no file associated.
332     */
333     size_t length() const pure @nogc @safe nothrow { return data.length; }
334 
335     /**
336     Get a slice to the contents of the entire file.
337 
338     Returns: the contents of the file. If not active, returns the `null` slice.
339     */
340     auto opSlice() pure @nogc @safe nothrow { return data; }
341 
342     /**
343     Resizes the file and mapping to the specified `size`.
344 
345     Params:
346     size = new length requested
347     */
348     static if (!is(Datum == const))
349     void resize(size_t size) pure
350     {
351         assert(handle != invalidHandle);
352         fakePure({
353             version(Posix)
354             {
355                 import core.sys.posix.unistd : ftruncate;
356                 import core.sys.posix.sys.mman;
357 
358                 if (data.length)
359                 {
360                     assert(data.ptr, "Corrupt memory mapping");
361                     // assert(0) here because it would indicate an internal error
362                     munmap(cast(void*) data.ptr, data.length) == 0 || assert(0);
363                     data = null;
364                 }
365                 if (ftruncate(handle, size) != 0)
366                 {
367                     fprintf(stderr, "ftruncate() failed for \"%s\": %s\n", filename, strerror(errno));
368                     exit(1);
369                 }
370                 if (size > 0)
371                 {
372                     auto p = mmap(null, size, PROT_WRITE, MAP_SHARED, handle, 0);
373                     if (cast(ssize_t) p == -1)
374                     {
375                         fprintf(stderr, "mmap() failed for \"%s\": %s\n", filename, strerror(errno));
376                         exit(1);
377                     }
378                     data = cast(Datum[]) p[0 .. size];
379                 }
380             }
381             else version(Windows)
382             {
383                 // Per documentation, must unmap first.
384                 if (data.length > 0 && UnmapViewOfFile(cast(void*) data.ptr) == 0)
385                 {
386                     fprintf(stderr, "UnmapViewOfFile(%p) failed for memory mapping of \"%s\": %d\n",
387                         data.ptr, filename, GetLastError());
388                     exit(1);
389                 }
390                 data = null;
391                 if (fileMappingObject != invalidHandle && CloseHandle(fileMappingObject) == 0)
392                 {
393                     fprintf(stderr, "CloseHandle() failed for memory mapping of \"%s\": %d\n", filename, GetLastError());
394                     exit(1);
395                 }
396                 fileMappingObject = invalidHandle;
397                 LARGE_INTEGER biggie;
398                 biggie.QuadPart = size;
399                 if (SetFilePointerEx(handle, biggie, null, FILE_BEGIN) == 0 || SetEndOfFile(handle) == 0)
400                 {
401                     fprintf(stderr, "SetFilePointer() failed for \"%s\": %d\n", filename, GetLastError());
402                     exit(1);
403                 }
404                 createMapping(name, size);
405             }
406             else static assert(0);
407         });
408     }
409 
410     /**
411     Unconditionally and destructively moves the underlying file to `filename`.
412     If the operation succeeds, returns true. Upon failure, prints a message to
413     `stderr` and returns `false`. In all cases it closes the underlying file.
414 
415     Params: filename = zero-terminated name of the file to move to.
416 
417     Returns: `true` iff the operation was successful.
418     */
419     bool moveToFile(const char* filename)
420     {
421         assert(name !is null);
422 
423         // Fetch the name and then set it to `null` so it doesn't get deallocated
424         auto oldname = name;
425         import core.stdc.stdlib;
426         scope(exit) free(cast(void*) oldname);
427         name = null;
428         close();
429 
430         // Rename the underlying file to the target, no copy necessary.
431         version(Posix)
432         {
433             if (.rename(oldname, filename) != 0)
434             {
435                 fprintf(stderr, "rename(\"%s\", \"%s\") failed: %s\n", oldname, filename, strerror(errno));
436                 return false;
437             }
438         }
439         else version(Windows)
440         {
441             import core.sys.windows.winbase;
442             auto r = oldname.asDString.extendedPathThen!(
443                 p1 => filename.asDString.extendedPathThen!(p2 => MoveFileExW(p1.ptr, p2.ptr, MOVEFILE_REPLACE_EXISTING))
444             );
445             if (r == 0)
446             {
447                 fprintf(stderr, "MoveFileExW(\"%s\", \"%s\") failed: %d\n", oldname, filename, GetLastError());
448                 return false;
449             }
450         }
451         else static assert(0);
452         return true;
453     }
454 }
455 
456 /// Write a file, returning `true` on success.
457 extern(D) static bool writeFile(const(char)* name, const void[] data) nothrow
458 {
459     version (Posix)
460     {
461         int fd = open(name, O_CREAT | O_WRONLY | O_TRUNC, (6 << 6) | (4 << 3) | 4);
462         if (fd == -1)
463             goto err;
464         if (.write(fd, data.ptr, data.length) != data.length)
465             goto err2;
466         if (close(fd) == -1)
467             goto err;
468         return true;
469     err2:
470         close(fd);
471         .remove(name);
472     err:
473         return false;
474     }
475     else version (Windows)
476     {
477         DWORD numwritten; // here because of the gotos
478         const nameStr = name.asDString;
479         // work around Windows file path length limitation
480         // (see documentation for extendedPathThen).
481         HANDLE h = nameStr.extendedPathThen!
482             (p => CreateFileW(p.ptr,
483                                 GENERIC_WRITE,
484                                 0,
485                                 null,
486                                 CREATE_ALWAYS,
487                                 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
488                                 null));
489         if (h == INVALID_HANDLE_VALUE)
490             goto err;
491 
492         if (WriteFile(h, data.ptr, cast(DWORD)data.length, &numwritten, null) != TRUE)
493             goto err2;
494         if (numwritten != data.length)
495             goto err2;
496         if (!CloseHandle(h))
497             goto err;
498         return true;
499     err2:
500         CloseHandle(h);
501         nameStr.extendedPathThen!(p => DeleteFileW(p.ptr));
502     err:
503         return false;
504     }
505     else
506     {
507         static assert(0);
508     }
509 }
510 
511 /// Touch a file to current date
512 bool touchFile(const char* namez)
513 {
514     version (Windows)
515     {
516         FILETIME ft = void;
517         SYSTEMTIME st = void;
518         GetSystemTime(&st);
519         SystemTimeToFileTime(&st, &ft);
520 
521         import core.stdc.string : strlen;
522 
523         // get handle to file
524         HANDLE h = namez[0 .. namez.strlen()].extendedPathThen!(p => CreateFile(p.ptr,
525             FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE,
526             null, OPEN_EXISTING,
527             FILE_ATTRIBUTE_NORMAL, null));
528         if (h == INVALID_HANDLE_VALUE)
529             return false;
530 
531         const f = SetFileTime(h, null, null, &ft); // set last write time
532 
533         if (!CloseHandle(h))
534             return false;
535 
536         return f != 0;
537     }
538     else version (Posix)
539     {
540         import core.sys.posix.utime;
541         return utime(namez, null) == 0;
542     }
543     else
544         static assert(0);
545 }
546 
547 // Feel free to make these public if used elsewhere.
548 /**
549 Size of a file in bytes.
550 Params: fd = file handle
551 Returns: file size in bytes, or `ulong.max` on any error.
552 */
553 version (Posix)
554 private ulong fileSize(int fd)
555 {
556     import core.sys.posix.sys.stat;
557     stat_t buf;
558     if (fstat(fd, &buf) == 0)
559         return buf.st_size;
560     return ulong.max;
561 }
562 
563 /// Ditto
564 version (Windows)
565 private ulong fileSize(HANDLE fd)
566 {
567     ulong result;
568     if (GetFileSizeEx(fd, cast(LARGE_INTEGER*) &result) == 0)
569         return result;
570     return ulong.max;
571 }
572 
573 /**
574 Runs a non-pure function or delegate as pure code. Use with caution.
575 
576 Params:
577 fun = the delegate to run, usually inlined: `fakePure({ ... });`
578 
579 Returns: whatever `fun` returns.
580 */
581 private auto ref fakePure(F)(scope F fun) pure
582 {
583     mixin("alias PureFun = " ~ F.stringof ~ " pure;");
584     return (cast(PureFun) fun)();
585 }