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